Skip to content

YPE-642 - verse action popover — highlights, copy & share #269

Merged
cameronapak merged 6 commits into
mainfrom
cp/YPE-642-react-sdk-bible-reader-ui-verse-action-popover-with-highlight-colors-copy-and-share
Jul 7, 2026
Merged

YPE-642 - verse action popover — highlights, copy & share #269
cameronapak merged 6 commits into
mainfrom
cp/YPE-642-react-sdk-bible-reader-ui-verse-action-popover-with-highlight-colors-copy-and-share

Conversation

@cameronapak

@cameronapak cameronapak commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

VIDEO OVERVIEW

YPE-642

Adds a verse action popover to BibleReader: tap verse(s) to highlight, copy, or share.

What's in it

  • Highlights — 5 iOS-matched colors, painted as a 35%-opacity fill. Tap a color to apply, tap its X to remove.
  • Copy / Share — bible.com-style output: curly-quoted text (verse numbers + footnote markers stripped), blank line, then Book Ch:vv VERSION. Non-contiguous picks join with ...; ranges collapse (1-3). Share uses the Web Share API, clipboard fallback.
  • Persistence — localStorage only, keyed by bible_id + verse USFM to mirror the future highlight API shape. No network this PR (server sync is a separate ticket).

Behavior worth a look

  • Sticky bar — when the anchored verse scrolls off, the bar docks to the edge it exited (down → top, up → bottom) so the actions stay reachable.
  • Fades in place — applying a highlight or clicking away fades the bar out where it sits (no anchor-loss jump).
  • No auth gating; highlights render regardless of sign-in. No cross-chapter selection.

Notes

  • Footnote markers darken to --yv-muted-foreground over a highlight for AA contrast.
  • Design rationale + ADRs: docs/adr/YPE-642-verse-action-popover.md.

Testing

  • 164 unit tests pass (incl. new verse-action-popover + verse-share suites); typecheck + lint clean.
  • Storybook: Components/BibleTextView → VerseSelection.

🤖 Generated with Claude Code

Greptile Summary

This PR adds a full verse-action popover to BibleReader: tap verse(s) to reveal a floating bar with five highlight colors (localStorage-persisted), Copy, and Share. It includes the sticky/docked bar when the anchored verse scrolls off-screen, a "frozen snapshot" fade-out animation, and a clean verse-share utility layer.

  • New components: VerseActionPopover (Radix PopoverAnchor virtualRef, IntersectionObserver docking), verse-share.ts (formatting helpers), two icon components; wired into BibleTextView with selectedVerses / highlightedVerses / onVerseSelect.
  • BibleReader API additions: onCopy / onShare override props expose the structured BibleReaderShareData payload so React Native / Expo hosts can forward selections over the native bridge.
  • Persistence: highlights keyed as youversion-platform:highlights:<versionId>{ "<USFM>": "<hex>" } in localStorage; version-switch clears the store synchronously during render to prevent stale highlights flashing over new text.

Confidence Score: 5/5

Safe to merge — no functional regressions or data-integrity issues found in the new verse-action popover, highlight persistence, or copy/share flows.

The core feature implementation is sound: highlight state is correctly scoped per version, the synchronous store-clear during render prevents stale flashes, the frozen-snapshot animation pattern handles the anchor-loss edge case, and the copy/share formatting utility is well-tested. The only findings are a misleading test comment, a theoretical Concurrent Mode edge case on a ref mutation during render, and a silent bad-CSS path when localStorage contains a malformed hex. None affect correctness under normal usage.

packages/ui/src/components/verse-action-popover.test.tsx — AC5a comment overstates which colours are hidden; packages/ui/src/components/verse-action-popover.tsx — frozen-view ref mutation during render; packages/ui/src/components/verse.tsx — hexToRgba lacks a length guard.

Important Files Changed

Filename Overview
packages/ui/src/components/verse-action-popover.tsx New component: Radix-based floating action bar with IntersectionObserver docking, frozen-snapshot fade-out, and color circle controls. Well-structured. The prefers-reduced-motion and non-unique aria-label issues from previous review cycles remain unaddressed.
packages/ui/src/components/bible-reader.tsx Adds selection + highlight state, localStorage load/persist, and popover wiring to Content. Synchronous store-clear during render (React adjust-state-during-render pattern) correctly prevents stale-version flash.
packages/ui/src/lib/verse-share.ts Clean pure-function formatting layer: formatVerseNumbers, joinVerseTexts, buildVerseReference, buildVerseShareText. Well-tested; edge cases (empty version abbreviation, non-contiguous verses) handled correctly.
packages/ui/src/components/verse.tsx Adds hexToRgba + getCleanVerseText helpers, updates useLayoutEffect to paint inline backgroundColor fills, and wires isHighlighted to VerseFootnoteButton for AA contrast on highlight fills.
packages/ui/src/components/verse-action-popover.test.tsx 470-line test file covering all 9 ACs. AC5a test comment misstates expected circle count (says '3 apply' but implementation and AC7 test both confirm 5 apply circles appear when multiple active colors). Assertions pass but the comment is misleading for future maintainers.
packages/ui/src/lib/verse-share.test.ts Good coverage of formatVerseNumbers (ranges, non-contiguous, dedup), joinVerseTexts (gaps, trimming), buildVerseReference, and buildVerseShareText (bible.com format, empty version).
packages/ui/src/styles/global.css Adds .yv-v-selected underline rule in yv-sdk-bible-reader layer. Correct scoping via [data-yv-sdk] prefix.
packages/ui/src/components/verse.stories.tsx VerseSelectionDemo updated to drive the real VerseActionPopover instead of manual Highlight/Clear buttons. Mirrors the bible-reader.tsx wiring faithfully.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant BibleTextView
    participant Content as BibleReader.Content
    participant VAP as VerseActionPopover
    participant LS as localStorage

    User->>BibleTextView: Tap verse N
    BibleTextView->>Content: onVerseSelect([N])
    Content->>Content: handleVerseSelect([N])
    Content->>Content: "querySelector(.yv-v[v=N]) → anchorElement"
    Content->>VAP: "open=true, anchorElement, selectedVerses=[N]"

    VAP->>VAP: IntersectionObserver observes anchorElement
    Note over VAP: Verse scrolls off screen
    VAP->>VAP: setDockEdge(top or bottom)
    VAP->>VAP: Switches to virtual docked anchor

    User->>VAP: Tap highlight color
    VAP->>Content: onHighlight(color)
    Content->>Content: handleHighlight(color)
    Content->>LS: localStorage.setItem(key, JSON)
    Content->>Content: closeAndClearSelection()
    Content->>VAP: "open=false (frozen snapshot animates out)"

    User->>VAP: Tap Copy
    VAP->>Content: onCopy()
    Content->>Content: buildSelectionShareData()
    Content->>BibleTextView: getCleanVerseText(container, verse)
    Content->>Content: buildVerseShareText(...)
    alt onCopy prop provided
        Content->>User: onCopy(BibleReaderShareData)
    else
        Content->>User: navigator.clipboard.writeText(text)
    end
    Content->>Content: closeAndClearSelection()
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant BibleTextView
    participant Content as BibleReader.Content
    participant VAP as VerseActionPopover
    participant LS as localStorage

    User->>BibleTextView: Tap verse N
    BibleTextView->>Content: onVerseSelect([N])
    Content->>Content: handleVerseSelect([N])
    Content->>Content: "querySelector(.yv-v[v=N]) → anchorElement"
    Content->>VAP: "open=true, anchorElement, selectedVerses=[N]"

    VAP->>VAP: IntersectionObserver observes anchorElement
    Note over VAP: Verse scrolls off screen
    VAP->>VAP: setDockEdge(top or bottom)
    VAP->>VAP: Switches to virtual docked anchor

    User->>VAP: Tap highlight color
    VAP->>Content: onHighlight(color)
    Content->>Content: handleHighlight(color)
    Content->>LS: localStorage.setItem(key, JSON)
    Content->>Content: closeAndClearSelection()
    Content->>VAP: "open=false (frozen snapshot animates out)"

    User->>VAP: Tap Copy
    VAP->>Content: onCopy()
    Content->>Content: buildSelectionShareData()
    Content->>BibleTextView: getCleanVerseText(container, verse)
    Content->>Content: buildVerseShareText(...)
    alt onCopy prop provided
        Content->>User: onCopy(BibleReaderShareData)
    else
        Content->>User: navigator.clipboard.writeText(text)
    end
    Content->>Content: closeAndClearSelection()
Loading

Comments Outside Diff (2)

  1. packages/ui/src/components/verse-action-popover.tsx, line 1275-1282 (link)

    P1 Non-unique aria-labels on color circle buttons

    All five "apply" buttons receive the same label ("Apply highlight") and all "clear" buttons receive the same label ("Clear highlight"). A screen reader user navigating the toolbar hears the same announcement for every color swatch and has no way to know which color they are about to apply or remove. This blocks the highlight-by-color feature for keyboard/AT users entirely.

    The i18n keys applyHighlightAriaLabel and clearHighlightAriaLabel are also locked to a bare string in all three locale files, so there is no placeholder available for the color name. A fix requires adding a color-name map (e.g. { fffe00: 'yellow', '5dff79': 'green', ... }) and updating the i18n keys to accept an interpolation variable (e.g. "Apply {{color}} highlight" / "Clear {{color}} highlight").

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/ui/src/components/verse-action-popover.tsx
    Line: 1275-1282
    
    Comment:
    **Non-unique aria-labels on color circle buttons**
    
    All five "apply" buttons receive the same label (`"Apply highlight"`) and all "clear" buttons receive the same label (`"Clear highlight"`). A screen reader user navigating the toolbar hears the same announcement for every color swatch and has no way to know which color they are about to apply or remove. This blocks the highlight-by-color feature for keyboard/AT users entirely.
    
    The i18n keys `applyHighlightAriaLabel` and `clearHighlightAriaLabel` are also locked to a bare string in all three locale files, so there is no placeholder available for the color name. A fix requires adding a color-name map (e.g. `{ fffe00: 'yellow', '5dff79': 'green', ... }`) and updating the i18n keys to accept an interpolation variable (e.g. `"Apply {{color}} highlight"` / `"Clear {{color}} highlight"`).
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Cursor Fix in Codex

  2. packages/ui/src/components/verse-action-popover.tsx, line 1240-1244 (link)

    P2 No prefers-reduced-motion guard on popover animations

    The popover uses fade-in-0, zoom-in-95, slide-in-from-top-2, and related Tailwind animation utilities with a 180 ms custom duration. Users who prefer reduced motion (via the OS setting) will still see these transitions. The review guidelines call out respecting this media query for all animations.

    Adding motion-safe: variants (e.g. yv:motion-safe:data-[state=open]:animate-in) or a global @media (prefers-reduced-motion: reduce) override that sets animation-duration: 0s on [data-yv-sdk] elements would resolve this.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/ui/src/components/verse-action-popover.tsx
    Line: 1240-1244
    
    Comment:
    **No `prefers-reduced-motion` guard on popover animations**
    
    The popover uses `fade-in-0`, `zoom-in-95`, `slide-in-from-top-2`, and related Tailwind animation utilities with a 180 ms custom duration. Users who prefer reduced motion (via the OS setting) will still see these transitions. The review guidelines call out respecting this media query for all animations.
    
    Adding `motion-safe:` variants (e.g. `yv:motion-safe:data-[state=open]:animate-in`) or a global `@media (prefers-reduced-motion: reduce)` override that sets `animation-duration: 0s` on `[data-yv-sdk]` elements would resolve this.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code Fix in Cursor Fix in Codex

Reviews (3): Last reviewed commit: "Merge branch 'main' into cp/YPE-642-reac..." | Re-trigger Greptile

cameronapak and others added 4 commits June 23, 2026 13:58
…642)

Tapping verses in BibleReader selects them (underline) and opens a popover
anchored to the last-selected verse with 5 highlight colors, Copy, and Share.
Highlights paint a 35%-opacity fill, persist to localStorage per Bible version
(shaped like the future highlight API), and can be removed per-color. Copy/Share
mirror bible.com formatting; Share uses the Web Share API with a clipboard
fallback. Footnote markers darken on highlighted verses to stay AA-legible.

Salvages the popover component and tests from the closed PR #131.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…642)

Keep the verse action bar reachable while scrolling. An IntersectionObserver
watches the anchored verse against the reader's scroll container; when the verse
leaves the viewport the bar docks to the edge it exited through (scroll down →
top, scroll up → bottom) instead of following the verse off-screen. A single
Radix popover swaps its anchor between the real verse element and a virtual
element pinned to the container edge, preserving focus/escape/outside-click and
avoiding remount flicker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the popover closes, the parent clears the selection and anchor
synchronously. Without freezing the last rendered state, the
animating bar would jump to a fallback position and flash an empty
layout before fading out.
Drop the unnecessary `typeof navigator` guards inside the Copy/Share click
handlers (handlers run client-side only, matching verse-of-the-day's pattern),
remove a no-op catch reassignment, and fix a stale dock-direction doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bcf2ff8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-react-ui Minor
vite-react Patch
@youversion/platform-core Minor
@youversion/platform-react-hooks Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/ui/src/components/bible-reader.tsx
Comment thread packages/ui/src/components/bible-reader.tsx
@cameronapak cameronapak changed the title feat(ui): verse action popover — highlights, copy & share (YPE-642) YPE-642 - verse action popover — highlights, copy & share Jun 23, 2026
Comment thread packages/ui/src/components/bible-reader.tsx
Comment thread .changeset/verse-action-popover.md
…errides (YPE-642)

Addresses PR #269 review feedback:
- Reset highlightStore synchronously on version change so the previous
  version's highlights never paint over new text for one frame (Greptile P2).
- Add optional onCopy/onShare props to BibleReader. When provided they receive
  the structured selection payload and suppress the default Web Share/clipboard
  flow, so React Native/Expo hosts can bridge natively (mirrors VerseOfTheDay).
- Document the highlightedVerses type change in the changeset (Greptile P2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/ui/src/components/bible-reader.tsx
Comment thread packages/ui/src/components/bible-reader.tsx

@Dustin-Kelley Dustin-Kelley left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

…ction-popover-with-highlight-colors-copy-and-share
@cameronapak cameronapak merged commit af37b90 into main Jul 7, 2026
6 checks passed
@cameronapak cameronapak deleted the cp/YPE-642-react-sdk-bible-reader-ui-verse-action-popover-with-highlight-colors-copy-and-share branch July 7, 2026 15:23
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.

2 participants