Skip to content

feat(snapshot): rasterize designed cards instead of the live DOM - #6614

Closed
tomeredlich wants to merge 6 commits into
claude/profile-content-draft-pr-368a12from
claude/profile-snapshot-cards-preview
Closed

feat(snapshot): rasterize designed cards instead of the live DOM#6614
tomeredlich wants to merge 6 commits into
claude/profile-content-draft-pr-368a12from
claude/profile-snapshot-cards-preview

Conversation

@tomeredlich

@tomeredlich tomeredlich commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6580. A snapshot was a screenshot — the live element fitted into a 1200×630 frame with a logo bar drawn under it — so nothing it produced looked like the designed cards on #6544's surface pages. This swaps the capture subject to those cards.

Changes

  • The card system, taken verbatim from snapshot-share-images: the 1080px SnapshotFrame with its lit hairline edge and seeded gradient, SnapshotIdentity / SnapshotStats, and the five profile cards. Verbatim so the two branches don't ship rival components — whichever lands second gets an identical-file conflict rather than a divergent one.
  • useSnapshotCapture, which mounts a card off-screen at left-[-200vw] and rasterizes it square with padding: 0, branded: false, since the card carries its own logo.
  • That branch's SnapshotButton, which renders on onPointerEnter / onFocus rather than on press. This is the point: the clipboard needs the press's own user activation, and a card cannot be rasterized inside the press because it has to be mounted first. Hovering buys the copy; a press that lands before the render finishes falls back to a download.
  • Achievement cards are wired to AchievementSnapshotCard, and only where the achievement is unlocked — a locked one has no completion date for the card to show.

Removed

The shutter sound, the snapshot-shutter-sweep animation, the Snapshot icon and shutter.mp3 all came in with #6580's button and have no caller now that this branch adopts #6544's.

Every placement, and where its values come from

No placement screenshots any more. Each card reads what the page itself renders rather than a parallel derivation, because a share image that disagrees with the page is worse than no share image.

Placement Card Values
ProfileHeader ProfileSnapshotCard name, handle, bio, cover, reputation from the profile; joined on JoinedDate's own MMMM y; posts read from the shared reading query
ReadingOverview ReadingOverviewSnapshotCard streak.max / streak.total as the widget's own cards show them; tag labels from tagTitles and percentages rounded the way ReadingTagProgress rounds them; heatmap bucketed by CalendarHeatmap's exported getBins/getBin
BadgesAndAwards BadgesSnapshotCard badge count from topReaders[0].total, not the array length, matching the summary card; keywords as flags.title || value; dates on TimeFormatType.TopReaderBadge
AchievementsWidget AchievementsSnapshotCard unlockedCount / totalCount / totalPoints from useProfileAchievements; the rarest unlocked from the shared comparator
AchievementCard AchievementSnapshotCard the achievement's own name, description, image, rarity and tier, only where it is unlocked

Two extractions this forced

  • sortRarestUnlockedAchievements joins sortLockedAchievements. The widget shows five and the card shows ten; with the comparator inline in the widget they could have disagreed about which five. Covered by three new cases.
  • useProfileReadingHistory takes over the query ProfileWidgets owned inline, because ProfileHeader needs posts read and had no way to reach it. The date window was never part of the query key, so both share one cache entry and one request — no second fetch, and the date maths lives in one place.

CalendarHeatmap's getBins/getBin went from module-private to exported for the same reason: the card's cells had to be bucketed by the page's own thresholds, not a second guess at them.

Events

None.

Experiment

None.

Manual Testing

  • shared: 378 suites / 2,755 tests. webapp: 86 / 685. extension: 6 / 52.
  • ReadingOverview.spec.tsx updated for the new required user prop.
  • typecheck-strict-changed clean; lint clean for shared and webapp.
  • SnapshotButton.spec.tsx rewritten for the adopted button: no rasterize until intent, rasterize on hover and on focus, copy when ready, download when the clipboard cannot take an image, and the throw when given neither a card nor a target.

Preview

https://claude-profile-snapshot-cards-pr.preview.app.daily.dev/idoshamun

Not #6580's preview — that one still screenshots the live DOM and always will, because the cards live on this branch. If the profile page you are on produces a screenshot rather than a card, check the URL.

What to open Where
Header, Reading overview, Badges & awards, Achievements /idoshamun
Per-achievement cards, revealed on hover /idoshamun/achievements
Public profile & URL copy row your own profile — the widget is isOwner-gated

Hover the Snapshot button before clicking it. The hover starts the rasterization so the press still owns the gesture the clipboard needs; you get "Image copied" and can paste straight into Slack. Clicking cold renders first and then falls back to a download with "Image saved" — that is the design, not a fault.

Preview domain

https://claude-profile-snapshot-cards-pr.preview.app.daily.dev

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>
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
daily-webapp Ready Ready Preview Sep 8, 2026 10:21am UTC

Request Review

…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>
@tomeredlich

Copy link
Copy Markdown
Contributor Author

Folded into #6580 — the cards now live on that branch, so there is one PR and one preview URL. Landing them separately meant the familiar preview kept producing screenshots, which cost several review rounds.

@tomeredlich tomeredlich closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant