Skip to content

feat(profile): share the profile as an image or a link - #6580

Open
tomeredlich wants to merge 29 commits into
mainfrom
claude/profile-content-draft-pr-368a12
Open

feat(profile): share the profile as an image or a link#6580
tomeredlich wants to merge 29 commits into
mainfrom
claude/profile-content-draft-pr-368a12

Conversation

@tomeredlich

@tomeredlich tomeredlich commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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

  • Snapshot in five places: the profile header's action row (beside Edit), the Reading Overview, Badges & Awards and Achievements widget headers, and each unlocked achievement card. Each captures the matching designed 1080px card from features/snapshot (feat(snapshot): designed share cards for the sharing initiative #6544) through main's target-based SnapshotButton (feat(snapshot): share placements for the post page #6556): image to the clipboard, download as the fallback, no link beside it, no sound.
  • Copy link in the header at Medium Float, next to Snapshot. It copies through useCopyLink's shorten path with the ShareProfile referral campaign, the same attribution the ⋯ menu's Share uses, and keeps the write inside the click for Safari.
  • DevCard: Download stays at Float and a Primary Share leads, opening the native sheet on mobile and copying the profile link on desktop.
  • The profile's Public profile & URL copy button confirms with CopyStateIcon, like the header.
  • useCopyLink no longer shows the copied state when there was no link to copy.
  • The reading card formats the longest streak with 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

  • The header card's reads stat is labelled Posts read in the last months, the Reading Overview's own wording for the same number. It is the Reading Overview window (about six months), not a lifetime total, and no query the profile runs returns a lifetime count, so a bare "Posts read" beside Joined and Reputation claimed something it wasn't. When the reading history hasn't loaded, the stat is left out instead of showing 0. SnapshotStat now centres its label so the longer one wraps evenly.
  • The reading overview card drops the streak tiles when there is no streak and the tags heading when there are no tags, like the page, instead of showing a 0 streak nobody measured.

Review first

  • features/snapshot/ProfileSnapshotButton.tsx: the one wrapper all five placements use. It follows TextSnapshotButton: useArmedCard mounts the card off-screen on hover, touch or focus, portalled to the body. The card comes from a renderCard callback 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 now profileReadingHistoryQueryOptions (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, download or error), the same shape as the post page's snapshots.

Action Event target_type / target_id provider origin
Header snapshot share profile profile page / user id snapshot profile header
Header copy link share profile profile page / user id copy link profile header
Reading Overview snapshot share profile profile page / user id snapshot reading overview
Badges & Awards snapshot share profile profile page / user id snapshot badges and awards
Achievements widget snapshot share profile profile page / user id snapshot achievements widget
Achievement card snapshot share profile achievement card / achievement id snapshot achievement card
DevCard share share devcard user id native or copy link devcard

Experiment

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-changed passes; the full webapp tsc has no errors outside the known __tests__ backlog.
  • New specs: ProfileSnapshotButton.spec.tsx (no card until armed; a press logs share profile with provider, origin and result) and useCopy.spec.ts (a blocked clipboard and a missing link both report the failure and do not show the copied state).
  • Rendered the header card with and without the reads stat, and with a two-line name and bio: the wrapped label stays inside the 1080px frame.
  • Storybook: the profile surface mockups (Surfaces/Profile, placements 6 and 7 in Button placements) are removed now that the real profile is the reference; the ReadingOverview story passes the new user prop. Button placements also 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:

  • Have you done sanity checks in the webapp?
  • Have you done sanity checks in the extension?
  • Does this not break anything in companion?

Did you test the modified components media queries?

  • MobileL (420px)
  • Tablet (656px)
  • Laptop (1020px)

Preview domain

https://claude-profile-content-draft-pr.preview.app.daily.dev

@vercel

vercel Bot commented Sep 2, 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 10, 2026 1:23pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
storybook Ignored Ignored Sep 10, 2026 1:23pm UTC

Request Review

tomeredlich and others added 4 commits September 2, 2026 17:36
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
tomeredlich force-pushed the claude/profile-content-draft-pr-368a12 branch from 88cede3 to 3e9e531 Compare September 2, 2026 14:48
@tomeredlich tomeredlich changed the title feat(profile): snapshot the profile header, its widgets and achievements feat(profile): share the profile as an image or a link Sep 2, 2026
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>
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>
tomeredlich and others added 6 commits September 8, 2026 14:21
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.
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.

3 participants