-
Notifications
You must be signed in to change notification settings - Fork 3
YPE-642 - verse action popover — highlights, copy & share #269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cameronapak
wants to merge
4
commits into
main
Choose a base branch
from
cp/YPE-642-react-sdk-bible-reader-ui-verse-action-popover-with-highlight-colors-copy-and-share
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
b20bf1a
feat(ui): add verse action popover with highlights, copy, share (YPE-…
cameronapak c18332c
feat(ui): dock verse action popover to the edge its verse exits (YPE-…
cameronapak e8fb9d2
Freeze verse action popover layout on close
cameronapak 3e7a7c1
refactor(ui): trim redundant guards and stale comments (YPE-642)
cameronapak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@youversion/platform-react-ui": minor | ||
| --- | ||
|
|
||
| Add a verse action popover to `BibleReader`. Tapping verses selects them (shown with an underline) and opens a popover anchored to the last-selected verse with five highlight colors, Copy, and Share. Highlights apply a translucent fill, persist to `localStorage` per Bible version (shaped like the future highlight API), and can be removed individually. Copy/Share output mirrors bible.com formatting: the verse text in curly quotes, gaps in a non-contiguous selection joined with `...`, followed by the `Book Chapter:verses VERSION` reference. Share uses the Web Share API and falls back to copying where it isn't available. | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| # YPE-642 — Verse Action Popover (highlights, copy, share) | ||
|
|
||
| Status: **In design** (grilling session 2026-06-23) | ||
| Component: `packages/ui/src/components/bible-reader.tsx` (+ `verse.tsx`) | ||
| Prior art: PR #131 (CLOSED) — salvage, don't rebuild. | ||
|
|
||
| ## Why #131 stalled | ||
|
|
||
| The popover logic was never the problem. It died because the **Bible HTML | ||
| structure** couldn't render highlights cleanly (line gaps, footnote color | ||
| breaks), and fixing that + getting stakeholder buy-in took a long time. That | ||
| structure fix has since landed: verses are wrapped in one-or-more `.yv-v[v="N"]` | ||
| elements, so a per-verse background can apply as a solid block. **The blocker is | ||
| gone.** #131's `VerseActionPopover` (correct AC logic, tested, already uses Radix | ||
| `PopoverAnchor virtualRef`) is salvage-grade. | ||
|
|
||
| ## Glossary | ||
|
|
||
| | Term | Meaning | | ||
| |---|---| | ||
| | **Selection** | Verses currently tapped. Ephemeral. Drives the popover. Keyed by verse number within the rendered chapter. | | ||
| | **Highlight** | A persisted color on a verse. Survives deselection. Modeled like the API `highlight` object. | | ||
| | **Active highlights** | Distinct colors present across the current selection → drives the X (remove) buttons. | | ||
| | **Apply circle** | Color button that adds a highlight. | | ||
| | **Remove circle (X)** | Color button that clears an existing highlight. | | ||
| | **Anchor** | DOM element the popover triangle points at — the last-selected verse's `.yv-v` element. | | ||
| | **Swatch** | The full-saturation color shown in the circle. | | ||
| | **Fill** | The faded (~20-30% alpha) background painted on the verse. | | ||
|
|
||
| ## Decisions (ADRs) | ||
|
|
||
| ### ADR-001 — localStorage only this PR | ||
| Highlights persist client-side only. Server sync is a **separate ticket**. | ||
| No network, no API client this PR. | ||
|
|
||
| ### ADR-002 — Local model mirrors the future API `highlight` object | ||
| The API shape (from spec) is: | ||
| ``` | ||
| highlight { | ||
| bible_id: int32 // e.g. 3034 | ||
| passage_id: string // verse USFM, e.g. "MAT.1.1" | ||
| color: string // /^[0-9a-f]{6}$/ hex, no '#', e.g. "44aa44" | ||
| } | ||
| ``` | ||
| Local store is shaped the same so the API swap later is mechanical: | ||
| - Keyed/scoped by `bible_id` + `passage_id` (full verse USFM). | ||
| - `passage_id` derived in-chapter as `${book}.${chapter}.${verseNumber}` from | ||
| BibleReader context. | ||
| - In-render, `verse.tsx` still works in verse **numbers**; the persistence layer | ||
| maps number ↔ `passage_id`. The on-wire/on-disk truth is USFM. | ||
|
|
||
| ### ADR-003 — Salvage #131's popover, don't rewrite | ||
| Bring back `verse-action-popover.tsx` + its tests verbatim where possible. It | ||
| already implements all 9 ACs and uses Radix `PopoverAnchor virtualRef`. Changes | ||
| needed: real YV colors (ADR-005), alpha fill (ADR-005), wire into BibleReader | ||
| (ADR-004), real copy/share formatting (open). **Drop** the vestigial | ||
| `@oddbird/css-anchor-positioning` polyfill — the popover never used it. | ||
|
|
||
| ### ADR-004 — BibleReader.Root owns selection + highlights; BibleTextView stays presentational | ||
| - Selection state and the highlight map live in `BibleReader.Root` context, | ||
| using the existing `useControllableState` idiom (uncontrolled by default; | ||
| optional controlled `highlights` / `onHighlightsChange`, `onCopy`, `onShare`). | ||
| - `BibleTextView` stays dumb: receives `selectedVerses` + `highlightedVerses` | ||
| (color map), emits `onVerseSelect`. It already does exactly this — we change | ||
| the highlight value type from `boolean` to color hex and wire the props that | ||
| `Content` currently omits. | ||
| - The popover lives in `Content`, opens when selection is non-empty, anchored | ||
| via `PopoverAnchor virtualRef` to the last-selected `.yv-v` element resolved by | ||
| `querySelector('.yv-v[v="N"]')`. | ||
| - **Breaking-ish:** `highlightedVerses` changes `Record<number, boolean>` → | ||
| `Record<number, string>` (hex). It's wired nowhere in the composite today, so | ||
| blast radius is small, but it is a public prop on `BibleTextView`. | ||
|
|
||
| ### ADR-005 — Hardcoded hex palette, matching the iOS app | ||
| Theme tokens don't carry these exact colors (the iOS app hardcodes them), so the | ||
| palette is hardcoded here too. Simpler than mapping to tokens. | ||
|
|
||
| | Highlight | Hex (stored / API + swatch) | | ||
| |---|---| | ||
| | yellow | `fffe00` | | ||
| | green | `5dff79` | | ||
| | blue | `00d6ff` | | ||
| | orange | `ffc66f` | | ||
| | pink | `ff95ef` | | ||
|
|
||
| - Lowercased to satisfy the API `color` pattern `/^[0-9a-f]{6}$/`. | ||
| - **Swatch** (circle) = `#<hex>` solid + a `1px #121212 @ 20%` inner stroke | ||
| (applies to all swatches). | ||
| - **Fill** (verse) = the hex at **35% opacity** behind the text | ||
| (`rgba(<hex>, 0.35)`, `HIGHLIGHT_FILL_OPACITY` in `verse.tsx`). | ||
| - **Active/remove swatch** = the solid color circle with a **24px X icon** in the | ||
| Text/Everdark color (`--yv-gray-50` = `#121212`, theme-invariant) — replaces the | ||
| old stroke-based selected indicator. | ||
| - No theme tokens, no dark variant. | ||
|
|
||
| ### ADR-006 — Copy/Share format = bible.com behavior (supersedes AC3) | ||
| AC3's inline `"text" - Book Ch:V Version` is **wrong**. The real format, per | ||
| Cam's example, mimics bible.com: | ||
| ``` | ||
| <verse text, gaps joined by " ... "> | ||
|
|
||
| <Book> <Chapter>:<verses> <VERSION> | ||
| ``` | ||
| - Text and reference separated by a blank line (`\n\n`). No dash. | ||
| - Non-contiguous verses → ` ... ` between the gap (e.g. selecting v1+v3 of | ||
| Proverbs 19 → `…perverse. ... A person's own folly…`). | ||
| - Reference: full book name, `1-3` for contiguous range, `1,3` for | ||
| non-contiguous, `1` for single. Version = abbreviation. | ||
| - Verse **numbers and footnote markers must be stripped** from copied text — | ||
| `.yv-v` textContent includes them; clean prose only. | ||
| - Share = same string, Web Share `{ text }`, **no URL / deep link**. | ||
| - Quote-character style (straight vs curly, single vs double) is OPEN — match | ||
| bible.com exactly. | ||
|
|
||
| ## Resolved (round 2) | ||
| - Palette → theme.css expressive (ADR-005). Copy format → bible.com (ADR-006). | ||
| - Share = text only, no deep link. No auth gating. Apply/copy/share + outside | ||
| click all clear selection and close the popover. | ||
|
|
||
| ## Resolved (round 3) | ||
| - **Copy text cleaning:** strip verse numbers + footnote markers; clean prose only. | ||
| - **Multi-wrapper verses:** highlight paints every `.yv-v[v="N"]` wrapper; copy | ||
| concatenates them in document order. | ||
| - **Per-version:** highlights scoped by `bible_id` (NIV highlight ≠ ESV). Yes. | ||
| - **localStorage:** key `yv:highlights:<bible_id>` → `{ "<passage_id>": "<hex>" }`. | ||
| Load on mount + version change; filter to visible chapter for render. | ||
| - **Quote char:** curly double `"…"` wrapping the whole text block. | ||
| - **No cap.** No cross-chapter highlighting, so selection is bounded by the | ||
| chapter's verse count. Number ↔ passage_id mapping is always sufficient. | ||
|
|
||
| ### ADR-007 — Selection lifecycle tied to navigation | ||
| Changing `book`, `chapter`, or `versionId` clears selection and closes the | ||
| popover (those verses no longer exist / highlights reload for the new scope). | ||
| Selection is always enabled in BibleReader (no opt-out prop for now; YAGNI). | ||
|
|
||
| ### ADR-008 — Selection visual = underline; stacks over highlight fill | ||
| - Selected (not highlighted): **underline** (bottom border) in the foreground | ||
| color, touching the text bottom (the Notion "underline has an offset" note was | ||
| about fixing this exact thing). | ||
| - Highlighted: 25%-alpha color fill (ADR-005). | ||
| - Selected **and** highlighted: fill stays, underline drawn on top — reads as | ||
| both. Underline over a color fill is legible; a second bg wouldn't be. | ||
| - Tunable; swap to a ring/bg later if Figma says otherwise. | ||
|
|
||
| ## As-built notes (deviations from the design above) | ||
| - **ADR-004 revised:** selection + highlights live in `Content`, **not** Root | ||
| context. Copy/Share/anchor all need the rendered verse DOM (which lives in | ||
| Content), so Root ownership would fragment the feature. BibleTextView stays | ||
| presentational. No new Root props — smaller API surface. | ||
| - **ADR-005 mechanism:** fill uses `rgba(r,g,b,0.25)` (computed from the stored | ||
| hex in `verse.tsx` `hexToRgba`), not `color-mix`. Same alpha-composite result, | ||
| zero browser-support caveats. | ||
| - **Verse-tap vs outside-click:** ADR-007 says outside-click clears selection, | ||
| but Radix treats a *second verse tap* as an outside-click too. The popover's | ||
| `onInteractOutside` calls `preventDefault()` when the target is inside | ||
| `.yv-v[v]`, so tapping more verses re-anchors instead of dismissing. Only a | ||
| true outside tap clears (matches the YV apps). | ||
| - **localStorage key:** `youversion-platform:highlights:<versionId>` → | ||
| `{ "<passage_id>": "<hex>" }`. | ||
| - **Files:** new `verse-action-popover.tsx` (+ tests, restored from #131), | ||
| `lib/verse-share.ts` (+ tests), `icons/box-stack`, `icons/box-arrow-up`; | ||
| `verse.tsx` (color fill + `getCleanVerseText`), `bible-reader.tsx` (Content | ||
| wiring), `verse.stories.tsx` (VerseSelection story now drives the real | ||
| popover), i18n (en/fr/es), `global.css` (selection underline). The `@oddbird` | ||
| polyfill was never reintroduced — the popover anchors via Radix `virtualRef`. | ||
|
|
||
| ## Build-time risks (not blocking design, flag for implementation) | ||
| - **Footnote color break** (Notion): even post structure-fix, `<sup>`/footnote | ||
| markers inside a verse may interrupt the fill. Verify the fill covers them. | ||
| - **Footnote contrast (handled):** on a highlighted verse the footnote marker | ||
| switches from `--yv-gray-20` to `--yv-foreground` (theme-adaptive, AA over all | ||
| 5 fills in both themes) via the `isHighlighted` prop on `VerseFootnoteButton`. | ||
| - `verse.tsx` uses `useLayoutEffect` (line 283) for the class toggle; AGENTS.md | ||
| (SSR) says prefer `useEffect`. Pre-existing; clean up while here. | ||
| - #131 leftovers to handle: replace the old verse-selection Storybook story + | ||
| remove demo highlight CSS; add a changeset. |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.