Skip to content

Link tokens#91

Merged
alex-rawlings-yyc merged 57 commits into
mainfrom
link-tokens
Jun 11, 2026
Merged

Link tokens#91
alex-rawlings-yyc merged 57 commits into
mainfrom
link-tokens

Conversation

@alex-rawlings-yyc

@alex-rawlings-yyc alex-rawlings-yyc commented May 29, 2026

Copy link
Copy Markdown
Contributor

This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Phrase linking/unlinking UI with arc visuals, split previews, and per-phrase controls
    • Phrase edit flow with token-level edits, gloss editing, and unlink confirmation
    • View options dropdown with toggles for continuous scroll, hiding inactive link buttons, and simplifying phrases
  • Improvements

    • More reliable token-focused navigation and scrolling behavior in continuous view
    • Enhanced hover, highlight and destructive styling feedback for phrase interactions
    • Localization and project setting support for new view options

@alex-rawlings-yyc alex-rawlings-yyc self-assigned this May 29, 2026
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89784ce6-68e3-49b9-ba6c-0835ad9c3e9a

📥 Commits

Reviewing files that changed from the base of the PR and between 713e095 and bb9ae8d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (81)
  • .github/workflows/lint.yml
  • .vscode/settings.json
  • AGENTS.md
  • REVIEW.md
  • __mocks__/lucide-react.tsx
  • __mocks__/papi-frontend-react.ts
  • __mocks__/platform-bible-react.tsx
  • contributions/localizedStrings.json
  • contributions/projectSettings.json
  • cspell.json
  • jest.config.ts
  • jest.setup.resize-observer.js
  • src/__tests__/components/AnalysisStore.test.tsx
  • src/__tests__/components/ArcOverlay.test.tsx
  • src/__tests__/components/ContinuousScrollToggle.test.tsx
  • src/__tests__/components/ContinuousView.test.tsx
  • src/__tests__/components/Interlinearizer.test.tsx
  • src/__tests__/components/InterlinearizerLoader.test.tsx
  • src/__tests__/components/PhraseBox.test.tsx
  • src/__tests__/components/PhraseStripContext.test.tsx
  • src/__tests__/components/PhraseStripParts.test.tsx
  • src/__tests__/components/SegmentView.test.tsx
  • src/__tests__/components/TokenChip.test.tsx
  • src/__tests__/components/TokenLinkIcon.test.tsx
  • src/__tests__/components/controls/EditPhraseControls.test.tsx
  • src/__tests__/components/controls/ScriptureNavControls.test.tsx
  • src/__tests__/components/controls/ViewOptionsDropdown.test.tsx
  • src/__tests__/components/modals/CreateProjectModal.test.tsx
  • src/__tests__/components/modals/ProjectMetadataModal.test.tsx
  • src/__tests__/components/modals/ProjectModals.test.tsx
  • src/__tests__/components/modals/SelectInterlinearProjectModal.test.tsx
  • src/__tests__/components/modals/UnlinkPhraseConfirm.test.tsx
  • src/__tests__/components/test-helpers.tsx
  • src/__tests__/hooks/useArcPaths.test.ts
  • src/__tests__/hooks/useInterlinearizerBookData.test.ts
  • src/__tests__/hooks/useOptimisticBooleanSetting.test.ts
  • src/__tests__/hooks/usePhraseHoverState.test.ts
  • src/__tests__/interlinearizer.web-view.test.tsx
  • src/__tests__/main.test.ts
  • src/__tests__/parsers/papi/bookTokenizer.test.ts
  • src/__tests__/services/projectStorage.test.ts
  • src/__tests__/store/analysisSlice.test.ts
  • src/__tests__/test-helpers.ts
  • src/__tests__/utils/phrase-arc.test.ts
  • src/__tests__/utils/token-layout.test.ts
  • src/components/AnalysisStore.tsx
  • src/components/ArcOverlay.tsx
  • src/components/ContinuousScrollToggle.tsx
  • src/components/ContinuousView.tsx
  • src/components/Interlinearizer.tsx
  • src/components/InterlinearizerLoader.tsx
  • src/components/PhraseBox.tsx
  • src/components/PhraseStripContext.tsx
  • src/components/PhraseStripParts.tsx
  • src/components/SegmentView.tsx
  • src/components/TokenChip.tsx
  • src/components/TokenLinkIcon.tsx
  • src/components/__mocks__/TokenChip.tsx
  • src/components/component-types.ts
  • src/components/controls/EditPhraseControls.tsx
  • src/components/controls/ScriptureNavControls.tsx
  • src/components/controls/ViewOptionsDropdown.tsx
  • src/components/modals/CreateProjectModal.tsx
  • src/components/modals/ProjectMetadataModal.tsx
  • src/components/modals/ProjectModals.tsx
  • src/components/modals/SelectInterlinearProjectModal.tsx
  • src/components/modals/UnlinkPhraseConfirm.tsx
  • src/hooks/useArcPaths.ts
  • src/hooks/useOptimisticBooleanSetting.ts
  • src/hooks/usePhraseHoverState.ts
  • src/main.ts
  • src/services/projectStorage.ts
  • src/store/analysisSlice.ts
  • src/tailwind.css
  • src/types/empty-factories.ts
  • src/types/interlinearizer.d.ts
  • src/types/phrase-mode.ts
  • src/types/token-layout.ts
  • src/types/type-guards.ts
  • src/utils/phrase-arc.ts
  • src/utils/token-layout.ts

📝 Walkthrough

Walkthrough

Adds phrase-mode state, store APIs, token-layout and arc utilities, phrase-strip components, view/loader rewrites, Tailwind utilities, Jest/jsdom ResizeObserver test stub, and extensive unit/integration tests for phrase interactions.

Changes

Phrase interaction pipeline

Layer / File(s) Summary
Phrase state and store APIs
src/types/phrase-mode.ts, src/types/empty-factories.ts, src/types/type-guards.ts, src/store/analysisSlice.ts, src/components/AnalysisStore.tsx, src/types/interlinearizer.d.ts, contributions/projectSettings.json, contributions/localizedStrings.json
PhraseMode type, empty factories, a word-token type guard, phrase reducers/selectors and store hooks were added; two interlinearizer boolean project settings and localized strings were added.
Token layout and arc engine
src/types/token-layout.ts, src/utils/token-layout.ts, src/utils/phrase-arc.ts, src/hooks/useArcPaths.ts, src/hooks/usePhraseHoverState.ts, jest.setup.resize-observer.js, jest.config.ts
Token focus/grouping utilities, arc geometry and split helpers, arc measurement hook with echo-guard, hover-state hook, and Jest ResizeObserver test setup and config changes were added.
Phrase strip and components
src/components/PhraseStripContext.tsx, src/components/PhraseStripParts.tsx, src/components/PhraseBox.tsx, src/components/TokenChip.tsx, src/components/TokenLinkIcon.tsx, src/components/ArcOverlay.tsx
Phrase-strip context/provider, slot/group mapping, PhraseBox rewrite (gloss input, edit/confirm modes), TokenChip enhancements (remove/disabled/split-free), TokenLinkIcon linking/unlinking logic, and ArcOverlay split-hover/render tiering were implemented.
Views, loader, and wiring
src/components/ContinuousView.tsx, src/components/SegmentView.tsx, src/components/Interlinearizer.tsx, src/components/InterlinearizerLoader.tsx, src/components/controls/ViewOptionsDropdown.tsx, src/components/controls/EditPhraseControls.tsx, src/components/modals/UnlinkPhraseConfirm.tsx, src/main.ts, src/services/projectStorage.ts, src/tailwind.css
ContinuousView/SegmentView refactors to focused-token-driven strip rendering, Interlinearizer threading of phraseMode, loader gating with per-key optimistic settings, new view options dropdown and edit controls, unlink confirm modal, main lifecycle validators, service import adjustments, and Tailwind utility additions.
Mocks, docs, workspace, and tooling
__mocks__/lucide-react.tsx, __mocks__/platform-bible-react.tsx, __mocks__/papi-frontend-react.ts, src/components/__mocks__/TokenChip.tsx, .github/workflows/lint.yml, AGENTS.md, REVIEW.md, .vscode/settings.json, cspell.json
Added/updated Jest mocks (icons, button ref forwarding), updated lint workflow, documentation for reviewers, VSCode settings, and spellchecker words.

Phrase interaction pipeline validation

Layer / File(s) Summary
Store and hook coverage
src/__tests__/components/AnalysisStore.test.tsx, src/__tests__/store/analysisSlice.test.tsx, src/__tests__/hooks/usePhraseHoverState.test.ts, src/__tests__/hooks/useArcPaths.test.ts, src/__tests__/utils/token-layout.test.ts, src/__tests__/utils/phrase-arc.test.ts, src/__tests__/test-helpers.ts, src/__tests__/components/test-helpers.tsx
Tests cover phrase store actions/selectors, phrase gloss writes, hover state, arc measurement and echo-guard behavior, token-layout helpers, phrase-arc geometry and split logic, and new test fixtures/helpers.
Phrase UI and view coverage
src/__tests__/components/ArcOverlay.test.tsx, src/__tests__/components/ContinuousView.test.tsx, src/__tests__/components/Interlinearizer.test.tsx, src/__tests__/components/InterlinearizerLoader.test.tsx, src/__tests__/components/PhraseBox.test.tsx, src/__tests__/components/PhraseStripContext.test.tsx, src/__tests__/components/PhraseStripParts.test.tsx, src/__tests__/components/SegmentView.test.tsx, src/__tests__/components/TokenChip.test.tsx, src/__tests__/components/TokenLinkIcon.test.tsx, src/__tests__/components/controls/EditPhraseControls.test.tsx, src/__tests__/components/controls/ViewOptionsDropdown.test.tsx, src/__tests__/components/modals/UnlinkPhraseConfirm.test.tsx
Component tests exercise arc overlays, continuous/segment views, phrase boxes and strip parts, token chips/link icons, view options dropdown, edit controls, and unlink confirmation behaviors.

Sequence Diagram(s)

sequenceDiagram
  participant Interlinearizer
  participant Views as Continuous/Segment Views
  participant Strip as PhraseStrip/ArcOverlay
  participant Store as AnalysisStore (Redux)
  Interlinearizer->>Views: pass focusedTokenRef, phraseMode, lookup maps
  Views->>Strip: render stripItems, arcPaths, hover/focus inputs
  Strip->>Store: create/update/delete/mergePhrase, writePhraseGloss via dispatch hooks
  Views->>Store: splitPhraseAtBoundary (ArcOverlay/TokenLinkIcon)
  Store-->>Views: selectors (phraseLink maps, gloss) update views
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

🟥High

Suggested reviewers

  • imnasnainaec
  • jasonleenaylor

Poem

🐇 I hopped through arcs and phraseful vines,
With link buttons, glosses, and tidy lines.
I nudged split midpoints, measured every span,
And stitched focus tokens into the plan.
Hooray — the strip now hums, all set to scan.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch link-tokens

@alex-rawlings-yyc alex-rawlings-yyc linked an issue May 29, 2026 that may be closed by this pull request
…om lint git workflow (it was causing a failure)
coderabbitai[bot]

This comment was marked as outdated.

Replace the token-ref-keyed linear search (`[...map.values()].find(l => l.analysisId === id)`) with a new `selectPhraseLinkByAnalysisId` selector and `usePhraseLinkByIdMap` hook, giving O(1) phrase lookup by id in `ArcOverlay`, `useArcSplitHandler`, and `SegmentView`.

Also:
- Move the phrase-revert effect from `PhraseBox` up to `InterlinearizerInner` so it fires even when all tokens are removed from the phrase
- Fix `TokenChip` state sync to use `useEffect` instead of inline mutation during render
- Fix `useArcPaths` dep array to use a version counter instead of a spread to satisfy the rules of hooks
- Reset `phraseMode` on project switch in `InterlinearizerLoader`
- Use a stable empty map constant in `PhraseBox` to avoid breaking memo
- Improve arc key to include `phraseId` and `splitAfterTokenRef`
coderabbitai[bot]

This comment was marked as outdated.

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@alex-rawlings-yyc resolved 4 discussions.
Reviewable status: 0 of 44 files reviewed, all discussions resolved (waiting on alex-rawlings-yyc).

@alex-rawlings-yyc

This comment was marked as outdated.

@coderabbitai

This comment was marked as outdated.

@alex-rawlings-yyc alex-rawlings-yyc marked this pull request as ready for review June 2, 2026 15:33
* Merge mostly redundant tests

* Update settings for VSCode to match our stylelint use

* Add JSDocs

* Fix pump() overwriting observerCallback via stub constructor

new ResizeObserver(() => {}) inside pump() was invoking the stub
constructor, replacing the stored observerCallback with a no-op on
every call after the first. Replace with a pre-declared stubObserver
plain object so pump() never touches the constructor again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Sort cspell words

* Fix brit spelling now covered in AGENTS.md

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@imnasnainaec made 1 comment.
Reviewable status: 15 of 78 files reviewed, 5 unresolved discussions (waiting on alex-rawlings-yyc).


a discussion (no related file):
🐛 Linking is misaligned when out-of-segment links are hidden:
image.png
image copy 1.png

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@alex-rawlings-yyc resolved 3 discussions.
Reviewable status: 15 of 78 files reviewed, 1 unresolved discussion (waiting on imnasnainaec).

@alex-rawlings-yyc

Copy link
Copy Markdown
Contributor Author
Previously, imnasnainaec (D. Ror.) wrote…

🐛 Linking is misaligned when out-of-segment links are hidden:
image.png
image copy 1.png

Would we rather the links become invisible and leave space or the gaps be closed? I'm leaning towards the gaps being closed, but it will be more work

@alex-rawlings-yyc

Copy link
Copy Markdown
Contributor Author
Previously, alex-rawlings-yyc (Alex Rawlings) wrote…

Would we rather the links become invisible and leave space or the gaps be closed? I'm leaning towards the gaps being closed, but it will be more work

Never mind, this is a redraw timing issue. Fix is simple

coderabbitai[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as outdated.

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@imnasnainaec reviewed 5 files and all commit messages, and made 1 comment.
Reviewable status: 20 of 78 files reviewed, 1 unresolved discussion (waiting on alex-rawlings-yyc).


a discussion (no related file):

Previously, alex-rawlings-yyc (Alex Rawlings) wrote…

Never mind, this is a redraw timing issue. Fix is simple

The arcs still surf all over the place when "Hide out-of-segment link buttons" is toggled on and off. I think it's fine to keep the space there rather than close the gap.

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@imnasnainaec partially reviewed 24 files.
Reviewable status: 44 of 78 files reviewed, 1 unresolved discussion (waiting on alex-rawlings-yyc).

…ct test helpers and reorganize types (#100)

* Reserve link-slot space when hidden to prevent arc re-alignment

Inactive link slots now use visibility:hidden instead of max-width collapse,
so toggling hideInactiveLinkButtons no longer shifts phrase box positions.
Removes the slotAnimationTick rAF loop and the hideInactiveLinkButtons
re-center effect that compensated for the old layout shift.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix punctuation jumping when link icon is hidden

link-slot was flex-col, stacking the icon wrapper above the punctuation.
Changing to flex-row keeps them side-by-side so punctuation position
no longer depends on the icon wrapper's height.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix in-phrase punctuation duplicating on every focus change

buildRenderUnits was pushing into the shared TokenGroup.punctuationBetween
arrays, which are owned by memoized objects that outlive a single render.
Replace push() with assignment so each call resets the gap array instead
of accumulating.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix crash on punctuation after the last token of a phrase group

After the gapIndex fix, pendingIntraGroup pointed to a non-existent gap
when the current token was the last in its group, causing a push() onto
undefined when post-phrase punctuation arrived. Guard the assignment so
the last token clears pendingIntraGroup instead; punctuation then falls
into the inter-group LinkSlot as intended. Adds a regression test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Cut 'yet'

* Tune link button and arc contrast

Link buttons: use foreground/60 (active) and foreground/20 (inactive)
instead of muted-foreground at 100%/50%, widening the visual gap between
actionable and suppressed slots.

Arcs: reduce focused-phrase stroke opacity from 1.0 to 0.75 so the
current phrase's arcs are less dominant relative to other phrases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix punctuation layout and reduce arc contrast gap

Punctuation layout: revert link-slot to flex-col so punctuation sits
below the icon. Remove visibility:hidden from the icon wrapper and use
opacity:0 instead; add display:inline-flex + min-height to ensure the
wrapper always reserves the same vertical space even when TokenLinkIcon
returns null.

Arc contrast: reduce focused-phrase stroke opacity 0.75→0.60 and raise
dimmed-phrase stroke opacity 0.8→1.0 (border is already low-contrast),
narrowing the visual gap between the two states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Guard mergePhrases reducer against targetPhraseId === absorbedPhraseId

Without the guard the update succeeds but the subsequent delete removes
the phrase that was just updated, leaving orphaned token references.
Invariant was previously enforced only by callers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Move controls/modals tests into matching subdirectories

Aligns __tests__/components/controls/ and __tests__/components/modals/ with
src/components/controls/ and src/components/modals/. Updates relative import
paths accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Disable pointer events and hide from a11y tree when link icon is suppressed

opacity:0 alone left the invisible TokenLinkIcon hittable and focusable.
Add pointerEvents:'none' and aria-hidden when suppressLinkIcon is true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Import defaultScrRef from test-helpers instead of redefining it

Two test files redeclared the same constant that test-helpers.ts already
exports. Remove the duplicates and import the canonical export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Extract makeWordToken fixture factory into test-helpers

The same minimal word-token shape was defined six times across three
test files under three different local names (mkWord, mkToken, mk).
A single exported factory with a stable API replaces all six.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Extract emptyAnalysis factory into src/types/emptyFactories.ts

Consolidates six independently constructed all-empty TextAnalysis objects
across two source files and four test files into a single shared factory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Reorganize types: rename to kebab-case, add @file headers, move token-layout types

- emptyFactories.ts → empty-factories.ts, typeGuards.ts → type-guards.ts
- Extract FocusContext/SlotFocusInfo/TokenGroup/LinkSlot/RenderUnit from
  utils/token-layout.ts into types/token-layout.ts
- Add @file JSDoc to all three type files
- Update all import sites (20 files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Unexport ArcStrokeProps — no consumers outside phrase-arc.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: extract makeStubProject and reuse project fixtures

* Fix idempotent punctuation routing in buildRenderUnits

* Update comments

* Make common withAnalysisStore test-helper

* Extract in-file project setting helpers

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@alex-rawlings-yyc

alex-rawlings-yyc commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

@imnasnainaec GitHub's auth API is down so we may have to override the reviewable requirement, assuming that merging your sub-PR made this PR mergeable in your eyes. If it is, go ahead and merge.

EDIT: GitHub auth API seems to be back up for now so Reviewable is a go

@coderabbitai

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as outdated.

@alex-rawlings-yyc alex-rawlings-yyc left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@alex-rawlings-yyc resolved 3 discussions.
Reviewable status: 30 of 86 files reviewed, 1 unresolved discussion (waiting on imnasnainaec).

imnasnainaec and others added 2 commits June 11, 2026 10:29

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@imnasnainaec partially reviewed 56 files and all commit messages.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on alex-rawlings-yyc).

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@imnasnainaec resolved 1 discussion.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on alex-rawlings-yyc).

@alex-rawlings-yyc alex-rawlings-yyc merged commit 171afdc into main Jun 11, 2026
11 checks passed
@alex-rawlings-yyc alex-rawlings-yyc deleted the link-tokens branch June 11, 2026 15:12
This was referenced Jun 11, 2026
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.

Link tokens together into the same phrase

2 participants