feat(profile): share the profile as an image or a link - #6580
Open
tomeredlich wants to merge 29 commits into
Open
feat(profile): share the profile as an image or a link#6580tomeredlich wants to merge 29 commits into
tomeredlich wants to merge 29 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Adds a Snapshot control that rasterizes any element with snapdom, fits it inside a 1200x630 frame on the current theme's background and draws the daily.dev logo bar. The PNG goes to the clipboard, because a paste beats a file in Downloads for every place we share to, and falls back to a download where ClipboardItem is unavailable. A cross-origin image without CORS headers leaves snapdom's inliner pending forever, so the capture times out rather than spinning the button. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five placements: the header action row beside edit, the Reading Overview, Badges & Awards and Achievements widget headers, and each achievement card on hover or keyboard focus. The achievement card's control sits out of flow. In flow it took 28px from the middle column and pushed long names into an ellipsis to reserve room for a button that is invisible until hover. It is positioned from a wrapper element because SnapshotButton sets `relative` on itself, which beats an `absolute` passed through className. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header gains a copy-link control beside snapshot, matched to the buttons already there at Medium Float: sharing a profile is for getting followed, and an image cannot be followed. It reuses the existing ShareProfile event, so the header stops being a blind spot beside the ⋯ menu's Share. The DevCard flips its default from private save to public post. Download keeps its place at Float; Share leads at Primary, opening the native sheet on mobile and copying the link on desktop, under the ShareDevcard event that already existed and had no caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design page behind these controls: where each one sits, on desktop and mobile, against the alternatives that were rejected. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
force-pushed
the
claude/profile-content-draft-pr-368a12
branch
from
September 2, 2026 14:48
88cede3 to
3e9e531
Compare
The header's copy link relied on the toast alone. It now swaps to the upvote button's filled avocado arrow and spins through the same curve, so the gesture that means "that worked" looks the same in both places. The Storybook page drew each of the three surfaces twice, before and after, plus a louder copy-link treatment we did not take. Only the shipped state remains, and the props that switched between states go with the halves they served. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three widget headers took Button's default Small, which sat heavier than the Learn more and 12/40 links beside them. XSmall matches the achievement card's control and the weight of the text it shares the row with. The header button keeps Medium, where it is matched to edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design page has served its purpose: the controls it compared are shipped and reviewable on the profile itself. Removing it takes the surface chrome with it, since nothing else imported either file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A refused clipboard write rejected out of useCopyLink, so the caller got no toast, no copied state and an unhandled rejection: the button read as dead. It now reports the failure, and the copied state is set only after the write lands, so a confirmation cannot claim a copy that did not happen. The missing-link path stops reporting a copy for the same reason. Two strict-mode errors in the file surfaced once it entered the changed set. An optional getLink was invoked unconditionally, and useCopyText passed a possibly undefined value to writeText, which would have put the string "undefined" on the clipboard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
marked this pull request as ready for review
September 3, 2026 14:39
The header's copy link already swapped to the upvote arrow, but the profile page's other copy control — the Public profile & URL row — only filled its copy icon, so the same gesture confirmed two different ways on one page. Both now render CopyConfirmIcon, which carries the arrow, the avocado and the spin in one place instead of each caller repeating the class list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The arrow borrowed from the upvote button read as an upvote sitting in the profile header. A copy is confirmed with the success checkmark instead, on the status-success token the success toast already uses, so the button and the toast it raises agree. The spin is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spin borrowed from the upvote button did not read like the rest of the product. The copy controls now use CopyStateIcon from claude/snapshot-surfaces-topic-directory-3b3fd5, taken verbatim with its spec so the two branches do not ship rival components: both glyphs sit in one grid cell and cross-fade on easeOutExpo, scaling and blurring out rather than spinning, and collapsing to an instant swap under prefers-reduced-motion. CopyConfirmIcon goes, and the copy-confirm keyframe with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A snapshot was a screenshot: the live element fitted into a 1200x630 frame with a logo bar drawn under it. It looked nothing like the designed square cards the surface pages compare against. Brings the card system over from snapshot-share-images verbatim — the 1080px SnapshotFrame with its lit edge and seeded gradient, the identity and stat rows, and the five profile cards — along with useSnapshotCapture, which mounts a card off-screen and rasterizes it, and that branch's SnapshotButton. The button renders on hover or focus rather than on press, so the press itself still owns the gesture the clipboard needs; a press that lands first falls back to a download. That replaces the shutter sound, the sweep animation and the Snapshot icon, which go with it. The achievement card is wired to AchievementSnapshotCard, and only where the achievement is unlocked: a locked one has no completion date to show. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ards Every profile placement now rasterizes a designed card instead of the live element, so no snapshot is a screenshot any more. The values come from whatever the page itself renders, not a parallel derivation, because a share image that disagrees with the page is worse than no share image. The rarest-unlocked sort moves into sortAchievements so the widget's five and the card's ten come from one comparator. CalendarHeatmap exports its bins, so the card's cells are bucketed exactly as the profile heatmap buckets them. Badge counts read topReaders[0].total and tag labels read tagTitles, matching the widgets beside them. ProfileHeader needed posts read, which only the widgets column had. That query moves into useProfileReadingHistory: the window was never part of the key, so the header and the column share one cache entry and one request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adopting snapshot-share-images' button wholesale brought its identity with it: the control became "Share as image" with a share-or-download glyph, and the Snapshot icon, the shutter and the sweep went in the bin. The ask was for the captured image to match the designed cards, not for the button to become something else. The card mechanism stays — render on hover or focus so the press still owns the gesture the clipboard needs — under the Snapshot icon, the Snapshot label, and the shutter and sweep on press. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adopting the other branch's share action changed what the press does. It led with the native sheet where the platform had one, worded the copy toast differently, and said nothing at all when it fell back to a download. The press copies again: clipboard first, download as the fallback, "Image copied" or "Image saved" to say which. A rasterization that fails now says so too, instead of going quiet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The cards were copied a few days ago and that branch has redesigned them since. Refreshes every file this branch carries to its tip, verbatim again. The surface label moves off the identity row and onto the logo row as SnapshotEyebrow, a sibling of the mark rather than a headline for the copy under it. SnapshotFrame gains the grow and wide treatments, and the capture now measures a mounted card instead of assuming the square, so a card that grows is neither letterboxed nor padded out with dead gradient. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card draws one cell per value it is given and stops at its grid, so handing it a day per entry showed the oldest eighty-eight days and dropped every month since. On a profile whose reading picked up recently that is a field of empty cells: seventy-one of eighty-eight rendered at level zero. The window is compressed into as many buckets as the grid has cells instead, summed per bucket and binned by CalendarHeatmap's own thresholds, so the strip carries the same shape the profile heatmap shows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Take the merged captureShareImage from #6426 and keep the width, height and capture timeout the snapshot cards need on top of it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Main now owns the snapshot foundation this branch had vendored (#6556 share placements, #6544 designed share cards). Every conflict takes main's side: SnapshotButton, the Achievements, Badges, Profile and ReadingOverview cards, SnapshotFrame, SnapshotStats, useCopy, captureShareImage, copyShareImage and utilities.css. The vendored files main deleted or never kept are removed here: useSnapshotCapture, shutterSound and public/sounds/shutter.mp3, and the spec for the old card-prop SnapshotButton, which no longer exists. The profile placements still call the old card-prop SnapshotButton; they are ported onto main's target API in the next commit.
The five profile placements called the card-prop SnapshotButton this branch had vendored, which main replaced with a target-based one. They now go through ProfileSnapshotButton, which follows the post page's TextSnapshotButton: useArmedCard mounts the 1080px card off-screen on hover, touch or focus, portalled to the body so neither a widget's overflow nor a hover card's transform reaches it. The card is built by a renderCard callback that only runs once armed. Before, every profile view built all five cards' props on each render: the reading heatmap buckets, the rarest-ten sort, the tag title lookup, and the header subscribed to the reading history query it only needed for one number. Now none of that runs until someone heads for a button. The reading history query becomes profileReadingHistoryQueryOptions in graphql/users.ts, per the options-creator convention, instead of a one-query hook. The key is unchanged, so the header's card is a cache read of what the widgets column fetched. sumReadHistory replaces the two copies of the reads reducer. Analytics: every snapshot logs ShareProfile with provider snapshot, a per-placement origin (profile header, reading overview, badges and awards, achievements widget, achievement card) and the press result, the shape useLogSnapshot uses for posts. The single achievement card targets the achievement (TargetType.AchievementCard), as tracking does. The header's copy link now logs its origin and copies through the shorten path with the ShareProfile campaign, like the menu's share, so a visit from it is attributed and the write stays inside the gesture. The achievement card's control was opacity-0 until hover on every device, so on touch screens it was an invisible button over the end of the title. It now hides only for a fine pointer and sits in flow on touch. The hover group is named so an ancestor's group cannot reveal it.
useCopyLink toasted "link is missing" and then fell through to set the copied state, so the button flipped to its green check for a copy that never happened. It now returns after the error, as the blocked-clipboard path already does. The spec this branch carried for its own useCopy is kept and pointed at main's messages.
ShareDevcard now carries the user as target_id and origin devcard next to the provider, matching the scheme the other share placements use.
Covers the two things ProfileSnapshotButton adds over main's snapshot pieces: renderCard does not run until the button is armed, so a profile view builds no card, and a press logs ShareProfile with provider, origin and result.
The longest streak printed the raw number while the tile beside it and the profile page both use largeNumberFormat, so a four-digit streak read 1165 on the card and 1.2K on the page.
surfaces/Profile.stories.tsx and placements 6 and 7 in Button placements drew a fake profile header, widgets and achievement box to argue for the controls this branch now ships on the real profile, so the live page is the reference, as the post page already is in the Overview. The ReadingOverview story passes the user the widget now needs for its card.
The profile header card showed "Posts read" beside Joined and Reputation, so it read as a lifetime total, but the number is the Reading Overview's window (about the last six months). It now carries the page's own wording, "Posts read in the last months", and is left out when the reading history has not loaded instead of printing 0. SnapshotStat centres its label so the longer one wraps evenly. The reading overview card now drops the streak tiles when there is no streak and the tags heading when there are no tags, as the page does, instead of stating a 0 streak it never measured.
The Button placements story carried numbers nobody measured: follower,
member and post counts on real tags, sources and squads, a reply count, a
hot take count, a "Top 20" feed claim, and a referral offer ("a month of
Plus") that does not exist. They are removed rather than swapped for other
numbers. The leaderboard placement only existed to show real users beside
invented scores and levels, so it goes, and the watercooler mock no longer
puts an invented post under a real person's name. The header no longer
claims placements 2 to 5 are live; only the post page is.
The profile share specs use a fictional user instead of a real one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gives the profile the share controls it was missing: a Snapshot of the profile and of each of its widgets, a copy link in the header, and a Share button on the DevCard. No feature flag, so the preview shows exactly what merging ships.
What ships
Reading Overview,Badges & AwardsandAchievementswidget headers, and each unlocked achievement card. Each captures the matching designed 1080px card fromfeatures/snapshot(feat(snapshot): designed share cards for the sharing initiative #6544) through main's target-basedSnapshotButton(feat(snapshot): share placements for the post page #6556): image to the clipboard, download as the fallback, no link beside it, no sound.useCopyLink's shorten path with theShareProfilereferral campaign, the same attribution the ⋯ menu's Share uses, and keeps the write inside the click for Safari.Public profile & URLcopy button confirms withCopyStateIcon, like the header.useCopyLinkno longer shows the copied state when there was no link to copy.largeNumberFormat, as the page does.The controls render for the owner and for visitors; every card reads only what the page's own queries returned.
Every stat on the images says what it is
SnapshotStatnow centres its label so the longer one wraps evenly.Review first
features/snapshot/ProfileSnapshotButton.tsx: the one wrapper all five placements use. It followsTextSnapshotButton:useArmedCardmounts the card off-screen on hover, touch or focus, portalled to the body. The card comes from arenderCardcallback that only runs once armed, so a profile view builds no card and derives nothing for one (heatmap buckets, rarest-ten sort, tag titles).graphql/users.ts: the reading history query is nowprofileReadingHistoryQueryOptions(options creator, key unchanged). The header's card reads it only when armed, which on the profile page is a cache read of what the widgets column fetched. No new query or request.AchievementCard.tsx: the per-card button is revealed on hover only for a fine pointer (mouse:variant) and sits in flow on touch, where an opacity-0 button would be an invisible tap target over the title.Events
All reuse existing events. Snapshot events carry
result(clipboard,downloadorerror), the same shape as the post page's snapshots.share profileprofile page/ user idsnapshotprofile headershare profileprofile page/ user idcopy linkprofile headershare profileprofile page/ user idsnapshotreading overviewshare profileprofile page/ user idsnapshotbadges and awardsshare profileprofile page/ user idsnapshotachievements widgetshare profileachievement card/ achievement idsnapshotachievement cardshare devcardnativeorcopy linkdevcardExperiment
None. Everything is unconditional on the profile and the DevCard page.
Testing
pnpm --filter shared test: 409 suites, 2955 tests passed.pnpm --filter webapp test: 86 suites, 685 tests passed.typecheck-strict-changedpasses; the full webapptschas no errors outside the known__tests__backlog.ProfileSnapshotButton.spec.tsx(no card until armed; a press logsshare profilewith provider, origin and result) anduseCopy.spec.ts(a blocked clipboard and a missing link both report the failure and do not show the copied state).Surfaces/Profile, placements 6 and 7 inButton placements) are removed now that the real profile is the reference; theReadingOverviewstory passes the newuserprop.Button placementsalso loses its invented numbers: follower, member and post counts on tags, sources and squads, a reply count, a hot take count, a "Top 20" feed claim and a made-up referral offer. The leaderboard placement goes too, since it only showed real users next to invented scores. The watercooler mock no longer puts a real name on an invented post, and the header no longer claims placements 2 to 5 are live.On those affected packages:
Did you test the modified components media queries?
Preview domain
https://claude-profile-content-draft-pr.preview.app.daily.dev