Skip to content

Adopt platform-bible-react components in place of raw HTML (Tier 1) - #168

Merged
imnasnainaec merged 8 commits into
mainfrom
165-adopt-platform-components
Jul 27, 2026
Merged

Adopt platform-bible-react components in place of raw HTML (Tier 1)#168
imnasnainaec merged 8 commits into
mainfrom
165-adopt-platform-components

Conversation

@imnasnainaec

@imnasnainaec imnasnainaec commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Resolves Tier 1 of #165.

What & why

platform-bible-react ships equivalents for several elements the WebView hand-rolled as raw HTML (paired with bespoke tw: utilities that duplicated their styling). Migrating to the platform components gives visual/behavioral consistency with the host app, theming, and accessibility for free, and lets us delete the duplicated CSS.

Changes (Tier 1)

<button>Button — mapped the old pill-btn-* / icon-button classes to variant/size, preserving every data-testid, aria-label, tabIndex, event handler, and disabled rule. Icons inside migrated buttons are sized with size-* (e.g. tw:size-3), not h-*/w-*buttonVariants forces any child SVG lacking a size- class to size-4, so h-*/w-* alone is silently overridden:

  • Confirm-bar controls: EditPhraseControls, UnlinkPhraseConfirm
  • MorphemeEditor Delete / Cancel / Done
  • Icon buttons: ContinuousView nav arrows, PhraseBox edit/unlink, TokenChip remove / define-morphemes / suggestion-add, TokenLinkIcon link/unlink, ArcOverlay split, SegmentListView merge & scroll-to-active, PhraseStripParts boundary
  • Not migrated: MorphemeBox's morpheme-form button intentionally stays raw <button> — it shares its className with sibling plain <span> grid cells so the row stays pixel-aligned; wrapping it in Button would add shadcn's border/rounding/focus-ring/disabled-opacity chrome and break that parity.
  • ViewOptionsDropdown's settings icon, ProjectMetadataModal's delete icon, and SelectInterlinearProjectModal's info icon (all already Buttons predating this PR) had the same icon-oversizing bug — via an h-*/w-* className for the first, a numeric size prop for the other two — and are fixed alongside the rest (the latter two as tw:size-[13px]/tw:size-[15px] to preserve their original dimensions), though none of those components are otherwise touched here. ViewOptionsDropdown is slated for a Popover conversion in Tier 3.

<input>/<textarea>/<label>Input/Textarea/Label:

  • CreateProjectModal, SaveAsProjectModal (dropped modal-form-*)
  • ProjectMetadataModal — kept its distinct section-label / modal-metadata-input styling (the issue grouped it with the modal-form-* modals, but it actually uses different utilities, so those are intentionally retained)

CSS cleanup — deleted the now-unused utilities: pill-btn-primary, pill-btn-secondary, pill-btn-destructive, modal-form-input, modal-form-label, icon-button.

Tests — extended __mocks__/platform-bible-react.tsx with Input/Textarea stubs and taught the Button stub to forward the extra props the icon buttons rely on (tabIndex, style, title, mouse handlers, aria-controls/aria-hidden). The stub also throws if a descendant SVG uses an h-* or w-* className without size-, or a numeric size prop without a size- className, so the icon-oversizing footgun above fails a test instead of shipping silently. No product test needed changing.

Docs — added an AGENTS.md “Components” note to prefer platform-bible-react components over raw HTML, including the size-* icon-sizing rule.

Also included: boundary-row reflow fix (unrelated to the Button migration)

PhraseStripParts.tsx's BoundaryControl used to unmount its whole reserved-height row for an intra-segment slot whenever the split marker wasn't shown (Alt up, or the not-mid-phrase guard), so that row collapsed to zero height and remounted at min-h-4 purely from Alt going up/down — growing the SegmentView strip unevenly, since rows with a live boundary never move (their merge button's wrapper reserves that height permanently). The wrapper now always renders; only the marker inside it is conditional, matching the link-icon row's existing mount-and-fade pattern.

This bug predates the Button migration (it came in with earlier split-UI work) and is unrelated to it — flagging it here explicitly since it's riding along on this branch rather than shipping as its own PR.

Deferred, with rationale (the issue's acceptance criteria permits deferring Tier 2/3)

  • Tier 2 — content-sized gloss inputs (field-sizing: content) and the WipeModal radio group. Platform Input does forward style/className/ref, so the gloss inputs are technically feasible, but they carry bespoke ARIA-combobox and auto-size behavior that deserves a focused pass.
  • Tier 3ViewOptionsDropdownPopover (a clean standalone cleanup, good as its own PR) and the SuggestionDropdown / TokenChip combobox (the issue itself notes this "may warrant its own issue").

🤖 Generated with Claude Code


Devin: https://app.devin.ai/review/sillsdev/interlinearizer-extension/pull/168


This change is Reviewable

Summary by CodeRabbit

  • Style

    • Migrated interactive UI controls (buttons, inputs, textareas, labels) to consistent shared platform components across the app.
    • Standardized button sizing/variants and updated icon sizing to use unified size-* classes.
    • Removed obsolete local Tailwind utility styles that are no longer needed after the component migration.
  • Documentation

    • Added developer guidance on preferring shared platform components and keeping test mocks in sync after component substitutions.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@imnasnainaec, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e102f102-df33-4419-99e1-f21532130047

📥 Commits

Reviewing files that changed from the base of the PR and between 8e3d6ff and 4cde5ce.

📒 Files selected for processing (4)
  • __mocks__/platform-bible-react.tsx
  • src/components/PhraseStripParts.tsx
  • src/components/modals/ProjectMetadataModal.tsx
  • src/components/modals/SelectInterlinearProjectModal.tsx
📝 Walkthrough

Walkthrough

Native buttons and modal form elements are migrated to platform-bible-react components across editor, navigation, linking, token, and project-modal views. Jest mocks gain matching component stubs and forwarded props, while obsolete Tailwind utilities and component-usage guidance are updated.

Changes

Platform component migration

Layer / File(s) Summary
Component guidance and test contracts
AGENTS.md, __mocks__/platform-bible-react.tsx
Documents preferred platform components and expands Jest stubs for Button, Input, and Textarea.
Interactive control migration
src/components/{ArcOverlay,ContinuousView,MorphemeEditor,PhraseBox,PhraseStripParts,SegmentListView,TokenChip,TokenLinkIcon}.tsx, src/components/controls/*, src/components/modals/UnlinkPhraseConfirm.tsx
Replaces native interactive buttons with configured platform Button components while preserving existing handlers, labels, state, and test identifiers.
Modal form component migration
src/components/modals/{CreateProjectModal,ProjectMetadataModal,SaveAsProjectModal}.tsx
Replaces native labels, inputs, and textareas with platform primitives while retaining form bindings and field identifiers.
Local utility removal
src/tailwind.css
Removes obsolete icon-button, modal-form, and pill-button utility definitions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: alex-rawlings-yyc

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: migrating Tier 1 raw HTML elements to platform-bible-react components.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 165-adopt-platform-components

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@imnasnainaec imnasnainaec self-assigned this Jul 22, 2026
@imnasnainaec
imnasnainaec marked this pull request as ready for review July 24, 2026 10:53

@alex-rawlings-yyc alex-rawlings-yyc 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.

Everything looks good except that some button sizes have changed. Here's how to bring them back:

Icon buttons render oversized after the <button>Button migration

The icons inside the migrated Buttons are being resized by the platform component, so several icon buttons render noticeably larger than the raw <button>s they replaced.

Root cause. buttonVariants' base class string (paranext-core/lib/platform-bible-react/src/components/shadcn-ui/button.tsx:16) ends with:

tw:[&_svg:not([class*=size-])]:size-4

That's a descendant rule the platform applies to any child SVG whose class attribute doesn't contain the substring size-. It's how shadcn gives icon buttons a consistent icon size, and each size variant overrides it (xs/icon-xssize-3, smsize-3.5).

Our icons are sized with tw:h-3 tw:w-3. That string contains no size-, so the :not() guard matches and the platform rule applies. It also wins on specificity — the compiled selector is .tw\:[&_svg...]\:size-4 svg:not([class*=size-]) at (0,2,1), versus .tw\:h-3 at (0,1,0). Our width/height silently lose.

Fix. Size the icons with the size-* shorthand instead of separate h-*/w-*, which opts them out of the platform's guard:

-<Link2 className="tw:h-3 tw:w-3" />
+<Link2 className="tw:size-3" />

Eight sites across five files:

File Line Icon
TokenLinkIcon.tsx 264 Unlink2tw:size-3
TokenLinkIcon.tsx 333 Link2tw:size-3
PhraseStripParts.tsx 166 Mergetw:size-3
PhraseStripParts.tsx 234 Splittw:size-3
PhraseBox.tsx 375 Trash2tw:size-3
SegmentListView.tsx 85 Mergetw:size-3 tw:rotate-90
SegmentListView.tsx 389 LocateFixedtw:size-4
ViewOptionsDropdown.tsx 185 Settingstw:size-4

Note the last three are the same latent bug even where the button already passes a size variant (icon-xs/icon-sm) — the variant just swaps which forced size wins, so the icon's own h-*/w-* is still being ignored.

Worth knowing for the rest of the migration: any lucide-react icon placed inside a platform Button must use size-*, never h-*/w-*, or the platform silently overrides it. Might be worth a line in AGENTS.md next to the existing component-preference note.

Also flagging: the Jest mock (__mocks__/platform-bible-react.tsx) renders Button as a plain <button> and explicitly ignores variant and size. That's reasonable for behavioral tests, but it means the suite can't catch sizing regressions of this kind — worth knowing before relying on green tests for the remaining tiers.

@alex-rawlings-yyc reviewed 16 files and all commit messages, and made 1 comment.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on imnasnainaec).

imnasnainaec and others added 4 commits July 27, 2026 14:16
Swap raw <button>/<input>/<textarea>/<label> for Button/Input/Textarea/Label across the confirm-bar controls, the morpheme editor, icon buttons, and the Create/Save As/Project Metadata modals; delete the now-duplicated pill-btn-*, modal-form-*, and icon-button utilities; extend the jest mock (Input/Textarea plus the extra Button props the icon buttons rely on); and note the component preference in AGENTS.md.

Tier 2/3 (gloss inputs, WipeModal radios, ViewOptionsDropdown Popover, the combobox) are deferred. lint clean; 1578 tests pass; coverage 100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The deleted modal-form-label utility supplied mb-1 between each label
and its field; the migrated Label component has no default margin, so
CreateProjectModal and SaveAsProjectModal fields lost their gap.
Icons inside migrated Button components were losing their h-*/w-* sizing
to buttonVariants' `[&_svg:not([class*=size-])]:size-4` rule, which wins
on specificity. Switching to the size-* shorthand opts them out of that
guard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@imnasnainaec
imnasnainaec force-pushed the 165-adopt-platform-components branch from 4405f68 to e82324d Compare July 27, 2026 18:17
@alex-rawlings-yyc

Copy link
Copy Markdown
Contributor

src/components/PhraseStripParts.tsx line 177 at r2 (raw file):

  // explanation; an Alt+click there is a silent no-op).
  const splittable = altHeld && !straddledBoundaryRefs.has(nextTokenRef);
  if (!splittable) return undefined;

src/components/PhraseStripParts.tsx — line 177 (if (!splittable) return undefined;)

Alt-hold grows rows by ~16px in segview

Holding Alt in SegmentView (contview off) expands the rows to make room for the split markers. The strip shouldn't reflow just from revealing them.

Cause. BoundaryControl returns undefined for an intra-segment slot whenever Alt is up, so that slot's boundary row contributes zero height. Pressing Alt mounts the min-h-4 wrapper and adds 16px. Since a row's height is set by its tallest slot, every row whose slots are all intra-segment grows — while rows that already contain a live boundary stay put, because the merge button's wrapper (line 161) reserves that height permanently. That asymmetry is why the strip shifts unevenly rather than uniformly.

The pattern to follow is already right above it. The link-icon row (~line 370) never unmounts — it stays in flow at minHeight: '1rem' and fades with opacity: 0. The boundary row just isn't doing the same thing.

Suggested fix — keep the wrapper unconditional, gate only the marker inside it:

const splittable = altHeld && !straddledBoundaryRefs.has(nextTokenRef);

return (
  <span className="tw:inline-flex tw:min-h-4 tw:items-center">
    {splittable && (
      <SplitMarker
        label={boundarySplitLabel}
        onSplit={() => { /* unchanged */ }}
      />
    )}
  </span>
);

The earlier return undefined paths should stay as they are — leading/trailing slots (lines 136–143) and the phrase-mode guard (line 147) can never carry a boundary edit, so they must keep collapsing or this would add height where there previously was none.

Also fixes a non-Alt case: a slot suppressed by the not-mid-phrase guard collapses today as well, so rows can differ in height from one another even with Alt held down. The same change stabilizes those.

The @returns line and the "intra-segment slots while Alt is up" sentence in the JSDoc (~line 103) describe the old contract and would need updating too.

Verified locally: 1578 tests pass, 100% coverage on this file, lint clean.

Note this predates this PR — it came in with the split-UI work, not the Button migration — so it may belong in its own commit rather than here.

@alex-rawlings-yyc alex-rawlings-yyc 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.

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

@alex-rawlings-yyc alex-rawlings-yyc 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 I threw in one more nit that can be ignored and deferred to a new issue if we just want to merge this in.

@alex-rawlings-yyc made 1 comment.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on imnasnainaec).

The Button mock now throws if a descendant SVG uses h-*/w-* without
size-, since buttonVariants silently overrides that sizing in
production and jsdom can't reproduce the visual regression otherwise.
Turning that check on caught three more instances the review's site
list missed (ArcOverlay's Link2Off, TokenChip's X and Plus) plus the
pre-existing ViewOptionsDropdown Settings icon; all four are fixed the
same way as the earlier batch. AGENTS.md documents the size-* rule.

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

@alex-rawlings-yyc alex-rawlings-yyc 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.

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

imnasnainaec and others added 2 commits July 27, 2026 15:12
BoundaryControl unmounted the whole split-marker row for intra-segment
slots whenever the marker wasn't shown (Alt up, or the not-mid-phrase
guard), so those rows collapsed to zero height and remounted at
min-h-4 purely from Alt going up/down — growing the strip unevenly,
since rows with a live boundary never move (their merge button's
wrapper reserves that height permanently). The wrapper now always
renders; only the marker inside it is conditional, matching the
link-icon row's existing mount-and-fade pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
assertNoOversizedIconClassName only recognized the h-*/w-*-without-
size- className pattern, missing icons sized via lucide's numeric
size prop instead of a Tailwind class -- but buttonVariants overrides
that the same way, since SVG width/height attributes lose to any
matching CSS rule. Extended the guard to also throw on a numeric size
prop without a size- class, which caught the only two such instances
in src: ProjectMetadataModal's delete Trash2 (size={13}) and
SelectInterlinearProjectModal's info Info icon (size={15}). Both
predate this PR. Fixed with tw:size-[13px]/tw:size-[15px] to keep
their exact original dimensions.

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

@alex-rawlings-yyc alex-rawlings-yyc 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.

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

The className check required both tw:h-* and tw:w-* before throwing,
but buttonVariants overrides an SVG's size the same way whether one
or both dimensions are set -- so a hypothetical single-dimension
regression (e.g. tw:h-3 alone) would have slipped through undetected.
Changed the check to OR. All 1578 tests still pass: every icon site
in this codebase already pairs h-*/w-* together, so nothing currently
relies on the narrower behavior.

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

@alex-rawlings-yyc alex-rawlings-yyc 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.

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

@imnasnainaec
imnasnainaec merged commit 1e59173 into main Jul 27, 2026
9 of 10 checks passed
@imnasnainaec
imnasnainaec deleted the 165-adopt-platform-components branch July 27, 2026 19:56
alex-rawlings-yyc added a commit that referenced this pull request Jul 27, 2026
PR #168 migrated this popover's buttons to platform-bible-react `Button`
but left the breakdown text field and its label as raw HTML. Bring them
in line with the convention the three project modals already follow, so
the field picks up the host's focus ring, disabled styling, and dark-mode
tokens instead of the hand-rolled approximations.

Drops the classes `Input` already supplies (rounded, border, border-input,
px/py, text-sm) and keeps only `w-full` and the `font-mono` that the
morpheme forms want. `Label` defaults to text-sm, so the popover's
smaller `text-xs` stays as an explicit override.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
alex-rawlings-yyc added a commit that referenced this pull request Jul 27, 2026
PR #168 migrated this popover's buttons to platform-bible-react `Button`
but left the breakdown text field and its label as raw HTML. Bring them
in line with the convention the three project modals already follow, so
the field picks up the host's focus ring, disabled styling, and dark-mode
tokens instead of the hand-rolled approximations.

Drops the classes `Input` already supplies (rounded, border, border-input,
px/py, text-sm) and keeps only `w-full` and the `font-mono` that the
morpheme forms want. `Label` defaults to text-sm, so the popover's
smaller `text-xs` stays as an explicit override.

Co-Authored-By: Claude Opus 5 (1M context) <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.

2 participants