Skip to content

feat(share): floating share bar for text selected in a post#6352

Draft
tsahimatsliah wants to merge 2 commits into
claude/website-sharing-visibility-be6b32from
claude/share-text-selection
Draft

feat(share): floating share bar for text selected in a post#6352
tsahimatsliah wants to merge 2 commits into
claude/website-sharing-visibility-be6b32from
claude/share-text-selection

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 22, 2026

Copy link
Copy Markdown
Member

Stacks on PR 1 (#6343) — base branch is claude/website-sharing-visibility-be6b32, not main.

What changed

Selecting text inside a post body raises a portaled floating bar anchored to the selection, with three actions:

  • Copy link — deep link to the post; on mobile this hands off to the native share sheet (reuses useShareOrCopyLink, so the selection rides along as the share text)
  • Copy text — the selection itself, via useCopyText (clipboard + toast)
  • Generate quote image — opens /image-generator/quote/[id]?text=… in a new tab

New files

File Purpose
packages/shared/src/hooks/useTextSelectionShare.ts Detects selection end inside a container, exposes the trimmed text + a viewport rect, follows the selection on scroll/resize (capture phase, so inner scrollers count), clears on collapse. enabled: false attaches nothing.
packages/shared/src/components/post/SelectionShareBar.tsx The portaled bar. Flag gate is the outer component; every hook lives in the inner one.
packages/shared/src/components/post/QuoteImageCard.tsx The 1200×630 card the screenshot service renders.
packages/webapp/pages/image-generator/quote/[id].tsx ISR page marked id="screenshot_wrapper", following image-generator/devcards/[userId].tsx and badges/[badgeId].tsx.

Wiring

usePostRedesign swaps the classic bodies for PostFocusCard on both the post page and the post modal, so all three render paths get the bar:

  • PostContent (classic article/video)
  • SquadPostContent (freeform / welcome / share / YouTube)
  • PostFocusCard (redesign)

Each change is a ref on an element that already exists plus one <SelectionShareBar> — which portals, so it adds no DOM in place. PostFocusCard and the classic bodies are a ternary swap, never nested, so there is no double-render.

Behaviour details

  • Anchored position: fixed, centred over the selection, flipping below it when there is no room above (< 64px from the top).
  • Horizontally clamped to visualViewport width and offsetLeft, updated on the visual viewport's scroll/resize — pinch-zoom pans the visual viewport without moving the layout viewport a fixed element sits in, which is the known Android overlay trap.
  • Dismisses on click-away, on Escape, and whenever the browser reports the selection collapsed. Clicks back inside the body are ignored by the outside-click handler so it never races the browser mid-drag.
  • Calm reveal via the existing animate-composer-in utility, with motion-reduce:animate-none.
  • Every control is a real <button type="button"> with an aria-label and a tooltip; the bar is role="toolbar".

Flag

share_text_selection, appended at the end of featureManagement.ts, default false. Gated on that AND the master useSharingVisibility() gate, both via useConditionalFeature with shouldEvaluate. Flag-off renders nothing and mounts none of the inner hooks, so no selection or viewport listeners are attached at all.

Logging

LogEvent.SharePost via usePostLogEvent, with extra: { provider, origin: Origin.TextSelection }. Additions to existing enums:

  • Origin.TextSelection (log.ts, appended in the sharing-visibility section)
  • ShareProvider.CopyText, ShareProvider.QuoteImage (share.ts) — the existing providers are all link targets, and neither of these is one

Backend dependency ⚠️

The screenshot service must be taught the /image-generator/quote/:id route before "Generate quote image" produces an actual PNG. The frontend renders the screenshot_wrapper page regardless, and today the button opens that page in a new tab — so the feature degrades to "here is your quote card" until the service picks up the route. The quote text is passed as a ?text= query param (capped at 280 chars) so the ISR'd shell is reused across every quote of the same post.

og:image currently points at the existing https://og.daily.dev/api/posts/:id endpoint; once the screenshot service serves quote images it can be repointed.

Storybook

  • Components/Share/SelectionShareBar — Desktop, Mobile, FlippedBelow, FlagOff. Storybook aliases @growthbook/growthbook to a mock that coerces falsy flag defaults to the truthy string 'control', so flag-off is simulated by holding FeaturesReadyContext as not-ready (same approach as PR 6).
  • Components/Share/QuoteImageCard — Default, LongQuote, SourceOnly.

Verification

Check Result
node ./scripts/typecheck-strict-changed.js ✅ pass
pnpm --filter shared lint ✅ pass
pnpm --filter webapp lint ✅ pass
NODE_ENV=test pnpm --filter shared test ✅ 296 suites / 1997 tests
pnpm --filter webapp test ✅ 45 suites / 300 tests

New tests:

  • SelectionShareBar.spec.tsx — copy link writes to clipboard + toast; copy text writes the selection + toast; the mobile path calls navigator.share; generate-image opens the quote URL with the encoded selection; the bar dismisses on click-away; flag-off renders nothing and never calls useTextSelectionShare (proving no listeners), including the case where only the per-surface flag is on.
  • useTextSelectionShare.spec.ts — reports text + rect on selection end, ignores selections outside the container, clears on collapse, inert when disabled.
  • QuoteImageGeneratorPage.spec.tsxgetStaticProps attribution + twitter:card=summary_large_image + og image, 404 on a missing post, and the page renders the #screenshot_wrapper element.

Deliberately left out

  • No OG image library was added (none exists in this repo) — the quote image goes through the existing screenshot-service pattern.
  • The bar does not offer the full social network row; that lives in ShareActions and would be too heavy for a selection popover.
  • No new keyframes or design tokens — the reveal reuses animate-composer-in.

🤖 Generated with Claude Code

Preview domain

https://claude-share-text-selection.preview.app.daily.dev

Selecting text inside a post body raises a portaled floating bar anchored to
the selection with three actions: copy link to the post (native share sheet on
mobile), copy the selection itself, and generate a shareable quote image.

- `useTextSelectionShare` detects selection end inside a container, exposes the
  text plus a viewport rect, follows it on scroll/resize and clears on collapse
- `SelectionShareBar` portals the bar to the root, clamps to the visual
  viewport (pinch-zoom/Android), flips below the selection when there is no
  room above, dismisses on click-away and Escape, and respects reduced motion
- `QuoteImageCard` + `/image-generator/quote/[id]` follow the existing
  screenshot-service pattern (`#screenshot_wrapper`, ISR) and carry
  `og:image` / `twitter:card=summary_large_image`
- Wired into all three post bodies: classic `PostContent`, `SquadPostContent`,
  and the redesign `PostFocusCard` (post page and post modal)

Gated by the new `share_text_selection` flag AND the `sharing_visibility`
master gate. Flag-off mounts none of the hooks, so no listeners are attached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Jul 22, 2026 8:07pm
storybook Building Building Preview Jul 22, 2026 8:07pm

Request Review

Opening the raw /image-generator/quote page landed users on a bare 1200x630
bitmap template with no download, share or back affordance. The route,
QuoteImageCard and the URL builder stay for the follow-up that previews and
shares a real PNG once the screenshot service serves the route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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