feat(sources): offer a copy link on the topic and directory surfaces - #6566
Merged
idoshamun merged 20 commits intoSep 10, 2026
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Wraps useShareOrCopyLink so a surface can offer a link directly instead of burying it in an overflow menu: the native sheet on mobile, a clipboard copy on desktop. The hook already raises a toast on copy, so the button stays static rather than swapping in a confirmation icon. Defaults to Float because it usually sits beside a Block or overflow button; surfaces whose neighbours are tertiary pass their own variant.
All four of these surfaces ended in a live feed and hid sharing behind the same overflow menu. On the squad card that menu is `invisible group-hover/menu:visible`, so touch had no share route at all. Give each one a copy link in the spot its own layout allows: - tag page, between Block and the overflow menu - source page, beside the follow and notify buttons, behind `showCopyLink` so PostUsersHighlights keeps its current row - squad directory card, before Join so it never competes with the conversion - archive index, beside a heading row that carried no controls - source directory rows, revealed on hover from laptop up and always present below it, using opacity so the button stays focusable SourceActions passed an optional source.id straight into follow/unfollow, which never type-checked in strict mode; route both through one helper that fails fast instead of firing a mutation it cannot form.
One story covering the four surfaces, each showing the placement that ships across desktop, tablet and mobile, so the layouts can be compared side by side without opening four pages. Mockup-to-eng-pass: 1
tomeredlich
force-pushed
the
claude/snapshot-surfaces-topic-directory-3b3fd5
branch
from
September 2, 2026 15:05
0dd55be to
0132963
Compare
The copy link control relied on the toast alone, so a press had no feedback on the button itself. It now swaps to a filled avocado arrow and spins through the same 0.4s curve as the upvote button, then reverts when useCopy clears its 1s window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The directory boxes are SquadGrid and UnfeaturedSquadGrid, not the SquadEntityCard the earlier pass changed — that one is the hover preview shown from SourceButton and the post widgets, so the control never appeared where the directory actually renders. Move it onto the two real cards and reveal it on hover from laptop up, keeping it visible below that for touch. Both cards lay a CardLink across the whole surface, so the button renders after the content with its own z-index rather than relying on stacking order. The squad page header gets one beside the bell, sharing its order so it stays put as the row wraps. SquadHeaderBar passed an optional squad.id and a possibly-undefined user into hooks typed to require them, which never type-checked in strict mode. Widen the two hook props to match the guards they already run, and narrow the id where the component needs it.
Replace the spinning avocado arrow with CopyStateIcon, taken verbatim from snapshot-happening-now so the two branches cannot drift: the link glyph and a green check share one grid cell and cross-fade over 200ms on easeOutExpo, collapsing to an instant swap under prefers-reduced-motion. Nothing resizes mid-swap, which the spin could not promise. Drop the copy-confirm keyframes with it, now that nothing references them. On the squad directory cards the control loses its fill and stroke, and takes the Medium size SquadActionButton leaves Button on, so it lines up with Join rather than sitting a step smaller beside it.
It floated over the banner, top right, where it sat on the artwork and read as part of the image rather than the card's controls. Put it at the bottom right instead, sharing the row with Join, which keeps every control on the card in one place. Join flexes to fill what is left, so the row still reads as one primary action with an affordance beside it.
The featured card carries one squad at full size, so there is room for the control to simply be there. Hover-gating it also left a gap beside Join where the transparent button still took its space. The unfeatured grid keeps the reveal: those cards sit many to a row, where a visible icon on every one competes with the names.
The squad section documented SquadEntityCard, which is the hover preview card the post widgets render, not the directory. Redraw it as the two cards the page actually uses, and add the two surfaces the story never covered: the squad page header and the source directory rows. Drop the copy link from SquadEntityCard with it. Nothing asked for a control on the post page previews, and it reached four surfaces through that card. Cover the placement on both squad cards, so the control cannot quietly stop rendering.
tomeredlich
marked this pull request as ready for review
September 3, 2026 14:53
…ces-topic-directory-3b3fd5 Brings in #6556 (post page share placements) and #6544 (designed share cards). Conflicts: surfaceChrome.tsx and surfaces/Directories.stories.tsx were added on both sides. Took main's version of both, since main's surfaceChrome is the shared helper the other surface stories import. CopyStateIcon and its spec were identical on both sides and merged cleanly.
The copy link placements logged ShareTag/ShareSource with only a
target, so a press on the new button could not be told apart from the
overflow menu's Share item on the same page, and native share could not
be told apart from a copy.
CopyLinkButton now takes a required origin and adds
extra: { provider, origin } to the caller's event, the same extra shape
#6556 uses for SharePost. Tag, source and squad pages reuse their page
origins; the source directory lists and the best-of archive get new
SourceDirectory and ArchiveIndex values.
The global best-of archive has no tag or source, but fell through to
ShareSource with no target and the share_source campaign. It now logs
a new ShareArchive event with the generic campaign.
Also imports LinkIcon from its own file rather than the icons barrel.
The copy path awaited the URL shortener before writing to the clipboard. Safari refuses a clipboard write once the task that handled the click has ended, and useCopy now reports that refusal, so every signed-in copy from CopyLinkButton (and the other callers of this hook, including ShareActions) would end in a "blocked the clipboard" toast. It now writes the tracked link straight away and hands the shortening to useCopyLink's shorten path, which swaps the short link in through a pending ClipboardItem, the same approach #6556 took for the post page. The native share path still resolves the short link first, since the text it shares has to carry it. The CopyLinkButton spec pins the synchronous write and the logged provider and origin. The two squad card tests that only asserted the button exists are dropped: they catch nothing the new spec does not.
The source directory rows and the unfeatured squad cards hid the copy link from laptop width up and revealed it on hover. A touch device that wide (an iPad in landscape, a touch laptop) has no hover, so the control stayed invisible there. Gating on laptop:mouse:, the pairing the feed cards already use, keeps it visible on any touch screen. It still fades by opacity rather than visibility, so it stays reachable by keyboard. Also imports largeNumberFormat from lib/numberFormat, not the lib barrel, in the file this touched.
The story drew mock tag, source, squad and archive pages to compare copy link placements. This PR ships those placements on the real pages, so the mockup can only drift from them. The Overview row now points at the live pages, like the post page row does.
SourceActions and TagTopicPage read ReferralCampaignKey, which the copy link's shareProps now depends on, through the hooks barrel. Import it from lib/referral, and the hook beside it from its own file.
Below the MobileL breakpoint the squad directory renders SquadList rows instead of the grid cards, and those rows only had Join, so phones had no copy link on /squads/discover even though touch was the reason for the placement. The same rows back the featured, category and my squads pages on phones. The row now carries the same CopyLinkButton as the grid cards (squad directory origin, share source event, share source campaign). It sits above the row's overlay link, so a tap copies instead of opening the squad, and it only hides behind hover for a fine pointer on laptop and up, like the unfeatured grid card. The text column now shrinks with min-w-0 instead of a max width sized for a single button.
A member who can invite saw the existing "Invitation link" next to the new "Copy link", two controls for nearly the same thing. The copy link now renders only when the invitation link does not: for non-members of a public squad and for members without the invite permission. The invite permission check moves up from SquadInviteButton so both controls read the same condition.
The best-of index pages had a page-level copy link but the month and year pages they link to did not, for every scope. ArchiveFeedPage now renders the same control beside its heading. The scope to event and campaign mapping moves into ArchiveCopyLinkButton so both pages log the same scheme: share archive for the global scope, share tag or share source otherwise, with the archive index origin.
idoshamun
added a commit
that referenced
this pull request
Sep 10, 2026
The DevCard's Share copied the bare permalink, with no cid or userid and no short link, while the profile header's copy link gave a tracked short link. The share now builds the ShareProfile tracked link without a request, so it reaches the share sheet and the clipboard inside the press, and the copy swaps in the short link through useCopyLink's shorten path once the shortener answers. It no longer goes through useShareOrCopyLink, which awaits the shortener before writing and loses Safari's user gesture (#6566 fixes that hook separately). The header's copy link now writes the tracked link first too, as useSharePost does, instead of the bare permalink with the tracked one only in the short link. When the short link cannot be written, the clipboard keeps an attributed link either way.
6 tasks
idoshamun
deleted the
claude/snapshot-surfaces-topic-directory-3b3fd5
branch
September 10, 2026 15:22
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.
Sharing on the topic and directory surfaces was buried behind an overflow menu. On the squad directory cards that menu is
invisible group-hover/menu:visible, so on touch there was no share route at all.One shared
CopyLinkButton(components/share/CopyLinkButton.tsx) now sits on each of them:TagTopicPage.tsxSourceActions/index.tsx(opt in withshowCopyLink, used bypages/sources/[source].tsx)SourceTopList.tsxSquadGrid.tsx,UnfeaturedSquadGrid.tsx/squads/discoverand the featured and category pages;/squads/discover/myat every size)SquadList.tsxSquadHeaderBar.tsxArchiveIndexPage.tsxArchiveFeedPage.tsxIt goes through
useShareOrCopyLink: the native share sheet on mobile, a copy on desktop. The confirmation is main'sCopyStateIcon(link glyph cross-fading to a check). Where the control is hover-revealed (source rows, unfeatured squad cards, squad list rows) it only hides behindlaptop:mouse:, so it is always visible on touch, and it fades by opacity so it stays reachable by keyboard.Ships unflagged: there is no GrowthBook flag, and it renders on merge.
Review first
hooks/useShareOrCopyLink.ts: the copy path used toawaitthe URL shortener before writing to the clipboard. Safari refuses a write once the click task has ended, anduseCopynow reports that as an error toast, so signed-in copies would have failed there. It now writes the tracked link inside the click and letsuseCopyLink'sshortenpath swap in the short link, the same approach feat(snapshot): share placements for the post page #6556 took on the post page. This hook is shared, so the fix also reachesShareActions,CustomFeedOptionsMenu, the profile share widget and the other existing callers. The native share path is unchanged.CopyLinkButtontakes a requiredoriginand addsextra: { provider, origin }to the caller's share event, so a press on the new button can be told apart from the overflow menu on the same page, and a copy from a native share.SquadHeaderBar: a member who can invite already has "Invitation link", so the header shows exactly one link control. The copy link renders for non-members of a public squad and for members without the invite permission. The invite permission check moved fromSquadInviteButtoninto the header so both controls read the same condition.SquadList: the copy link sits above the row's overlayCardLink(relative z-0), so a tap copies instead of opening the squad. The text column now shrinks withmin-w-0instead of amax-wsized for one button.ArchiveCopyLinkButton: one place maps the archive scope to its event and campaign, used by both the index and the month/year pages. The global best-of has no tag or source, so it logsshare archivewith the generic campaign instead of falling through toshare sourcewith no target.Events
Every action logs the entity's existing share event (these are not posts, so not
SharePost) withtarget_idandextra: { provider, origin }.provideriscopy linkon desktop andnativewhen the OS share sheet is used.share tagtag pageshare sourcesource pageshare sourcesource directory(new)share sourcesquad directoryshare sourcesquad pageshare tag/share sourcearchive index(new)share archive(new)archive index(new)The month and year pages share the
archive indexorigin with the index page; the event does not say which of them it came from.Experiment
None. No flag, no preview-host forcing.
Also here
Two files carried pre-existing strict-type errors that CI surfaces once a branch touches them, so they are fixed rather than worked around:
SourceActionspassed an optionalsource.idintofollow/unfollow; both now go through one helper that fails fast instead of firing a mutation it cannot form.SquadHeaderBarpassed an optionalsquad.idand a possibly undefined user intouseSourceIntegrationQueryanduseGetSquadAwardAdmin. Both hooks already guarded for those at runtime, so the prop types are widened to match.The Storybook "Topic & directory pages" mockup is removed now that the placements ship on the real pages, and the Overview row points at the live pages instead.
Testing
CopyLinkButton.spec.tsx: the clipboard write happens inside the click with the tracked link, and the event carries the provider and origin. Both fail against the old hook.SquadList.spec.tsx: the row's copy link writes the tracked squad link, logsshare sourcewith thesquad directoryorigin, and does not click the row link.SquadHeaderBar.spec.tsx: a member with the invite permission sees only "Invitation link"; a non-member sees only "Copy link".ArchiveFeedPage.spec.tsx: the copy link on a global monthly best-of page logsshare archivewith thearchive indexorigin.pnpm --filter shared test: 410 suites, 2957 tests pass.pnpm --filter webapp test: 86 suites, 689 tests pass.node ./scripts/typecheck-strict-changed.js origin/main: passes.tsc --noEmit: no errors outside the known__tests__backlog.Preview pages:
/tags/javascript,/sources/theverge,/sources,/squads/discover(cards from the tablet breakpoint, list rows below 656px),/squads/discover/featured, a squad page as a member and as a non-member,/posts/best-of,/posts/best-of/2025,/posts/best-of/2025/8.Preview domain
https://claude-snapshot-surfaces-topic-d.preview.app.daily.dev