PT-4540: Select a paragraph marker to change it (host) - #2867
Draft
jolierabideau wants to merge 10 commits into
Draft
jolierabideau wants to merge 10 commits into
jolierabideau wants to merge 10 commits into
Conversation
Base automatically changed from
pt-4488-pt-4539-paragraph-marker-dropdown
to
main
September 25, 2026 19:45
The paragraph dropdown restored the last caret before retagging, which moved a marker selection's retag to another paragraph. Adds the host implementation plan for paragraph-marker selection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The paragraph menu is now controlled so the editor can open it, closes when a style is picked like the character-marker control beside it, and returns focus to the editor on close unless the user clicked elsewhere. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Enter or Alt+Down on a selected marker now opens the toolbar paragraph menu; a structure lock answers with its notification instead of dropping the key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An accent fill and a foreground leading bar that reach across the gutter, on their own channel beside the focus box; the bar token is chosen by measured contrast in all four themes and pinned by a test. The Storybook demo copy of usj-nodes.css is left alone: its stories never show gutter markers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ter rule Equal-specificity rule wins by source order; moving the glyph-strengthening rule into the gutter-markers section, after the base rule, so the contrast- tested --foreground colour actually applies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lexical's focus() falls back to selecting the document end when the editor-state selection is null, and opening the paragraph popover can null it on blur. Picks already restored the caret before applying; Escape and other forced closes only called focus(), so the caret could jump to the chapter end. returnFocusToEditor() wraps restoreSelectionIfLost() and focus() in the right order, and focusEditor now calls it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Radix reports a pointer-down on the trigger button itself as an outside interaction, so closing the paragraph menu by clicking the toolbar button again left focus stranded on the button instead of returning it to the editor, unlike CharacterMarkerControl. Read against a ref on the trigger button so a pointer-down inside it is treated as a normal close. Also pins that the menu's search box receives focus when the editor opens it via the keyboard, matching CharacterMarkerControl; Radix's own auto-focus already provides this, so no product change was needed for that assertion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The selected-row comment in _usj-nodes.scss claimed the editor also sets aria-selected; it exposes the selected glyph via aria-activedescendant on its root instead, so drop the stale parenthetical. Bring .context/designs/2026-09-25-para-marker-selection-host-plan.md in line with what was built: the selected-glyph SCSS block lands after the gutter-markers section's RTL rules (not the verse-delete-armed block), with the reason and the order-pin test added to Task 4; the pinned editor interface names reflect aria-activedescendant; Task 2's behaviour notes cover a trigger re-click close and the caret-restoring onReturnFocusToEditor; and Task 6's manual QA gains the Escape-caret-restore and close-by-re-click steps, with every subsequent step renumbered. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Share the close-on-pick wrapper between the paragraph and character-marker menus (wrapMarkerMenuItemsWithClose) - Add a host regression test that a retag from a selected marker never sets a selection position (Standard-View-Invariants §6) - Keep the selected row visible in forced colors; pair the glyph with --accent-foreground and check its text contrast - Reword the selection comments to state where they diverge from adr-list-selection-on-a-dedicated-visual-channel - Cover Enter pick, Escape after an editor-opened menu, and a forced close in the trigger tests; add an editor-request story - Sweep every keyboard-catalog location instead of per-entry checks; drop a tautological keys test - Export ParagraphMenuOpenState; rename the restoring focus callback; dedupe test helpers - Drop the implementation plan doc from the PR Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Session-URL: <session URL>
jolierabideau
force-pushed
the
pt-4540-para-marker-selection
branch
from
September 29, 2026 16:31
e2d31cb to
b6bcf43
Compare
This branch has not been deployed
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Code Review Summary
Branch: pt-4540-para-marker-selection
Base: origin/main. The review ran against the WI-14 tip (
origin/pt-4488-pt-4539-paragraph-marker-dropdown,9fe661a). WI-14 was squash-merged in the meantime, so the branch was then rebased--onto origin/main.Date: 2026-09-29
Review model: Claude Opus 5.5
Files changed: 17 against
origin/mainafter the rebase. At the start of the review it was 14 PT-4540 files against the WI-14 tip; the review addedmarker-menu.utils.tsand its test, touchedcharacter-marker-control.component.tsx, and dropped the plan doc.Overview
This is the host (paranext-core) half of PT-4540 (Todd NN-2.3). In Simple mode, Saroj can select a paragraph marker itself by clicking its gutter glyph, and change it with the toolbar paragraph dropdown. The editor half (selection, keys, the DOM class) is in scripture-editors branch
pt-4540-para-marker-selection. This PR consumesEditorRef.getSelectedParaMarker(),EditorProps.onParaMarkerMenuRequestand thepsc-para-marker-selectedclass.On the host side:
restoreSelectionIfLosttreats a selected marker as a live selection, so a pick retags the marker's paragraph.ParagraphStyleTrigger's popover is controlled through the newuseParagraphMenuOpenStatehook, so Enter / Alt+↓ on a selected marker opens it. Structure lock shows the lock notification; read-only or no block ignores the request; the menu closes if it becomes unavailable.returnFocusToEditor, which restores the caret beforefocus(). After an outside interaction it leaves focus alone.--foregroundleading bar across the gutter, mirrored for RTL, with a contrast test.The review focused on pattern conformance and on the PT-4540 Definition of Done. During the interview:
Two follow-up bugs came out of the review. Their ticket drafts are on the author's desktop (see Suggested Review Focus).
API Changes
None. No public API surface changed: nothing under
lib/platform-bible-react/,lib/platform-bible-utils/,lib/papi-dts/papi.d.ts, orextensions/src/*/src/types/*.d.ts. Internal to theplatform-scripture-editorextension:restoreSelectionIfLostnow takesgetSelectedParaMarkerand returns early while a marker is selected.returnFocusToEditor(editor, lastFocusOutSelection).ParagraphStyleTriggerhas three new required props:isMenuOpen,onMenuOpenChange,onReturnFocusToEditor. The popover is now controlled.useParagraphMenuOpenState, with the exported typeParagraphMenuOpenState.wrapMarkerMenuItemsWithClose.platform-yalc:EditorRef.getSelectedParaMarker()andEditorProps.onParaMarkerMenuRequest.Findings
Critical — Must address before merge
platform-yalcrevision lacksgetSelectedParaMarkerandonParaMarkerMenuRequest. A clean checkout or CI will fail typecheck inplatform-scripture-editor.web-view.utils.ts,platform-scripture-editor.web-view.tsx, and the tests that mock them. (Author: the PR stays a draft until editor PR 1 lands onplatform-yalcand is taken in withnpm install && npm run verify:dev-packagesplus the lockfile.)Author response: acknowledged. The PR stays a draft until the editor PR is taken in.
Important — Should address before merge
CharacterMarkerControlon an outside click. The character-markeronClosealways runseditorRef.current?.focus(). (Checked during review: this is intentional per editor spec §10, "Closing always returns focus to the editor, unless the user interacted outside the menu". The PR description is reworded so it no longer claims to match the character-marker control on outside click. The character-marker control's barefocus()(no caret restore) may move the caret to the chapter end on an Escape close, so a follow-up ticket draft was written:ticket-character-marker-close-caret-jump.md.)Record the "toolbar marker menus return focus to the editor" exception in an ADR and in(Author: no ADR entry, since the author considers it documented. Note for the reviewer:dismissal-patterns.mdx.dismissal-patterns.mdx:102-112still says to return focus to the opener, and the exception is written down in the editor spec §10, not in the Storybook guideline.)adr-list-selection-on-a-dedicated-visual-channelfor a row fill that ADR rejects. (Fixed during review: the comments in_usj-nodes.scssandusj-nodes-styles.test.tsnow say the rule follows the ADR's dedicated channel but departs from it in two ways. It adds a fill because a paragraph row's background carries no status. The bar is a box-shadow because the gutter lies outside the box, and box-shadow has no logical form, hence the explicit RTL rule.)closingMarkerMenuItemswas copied fromCharacterMarkerControl. (Fixed during review: newmarker-menu.utils.tsexportswrapMarkerMenuItemsWithClose(items, close), with TSDoc and a test file. Both controls use it.)DoD: keyboard selectability deferred without a filed ticket.(Author: some of the keyboard work was done in this PR: Enter / Alt+↓ open the menu, and arrows return to the text. The rest isn't scoped yet, so no follow-up ticket for now. This DoD line is knowingly left open.)TextNodeprefixes, and Simple's decorator prefixes are skipped by boundary normalization. The PR's "Before merge" list now carries it for the invariants owner to confirm with the sign-off.)platform-scripture-editor.utils.test.ts(with a paragraph marker selected). It connects the realgenerateParagraphMenuListItemsto the realrestoreSelectionIfLostand asserts that a retag from a selected marker callsformatParaand neversetSelection, with a positive control. It was revert-checked. The owner's sign-off on the spec §8.1 statement is still pending.)platform-scripture-editorand the engine, notplatform-bible-react, in both the header and Dependencies.).context/designs/2026-09-25-para-marker-selection-host-plan.mdwas dropped from the PR; the author kept a local copy.)Author response: all Important items were resolved, dismissed with a reason, or recorded as knowingly open (keyboard follow-up, owner sign-off).
Minor — Consider
@media (forced-colors: active)addsoutline: 2px solid Highlightto the selected row, pinned by a test.)--accentfill used--foregroundinstead of its semantic pair. (Fixed: the glyph now uses--accent-foreground; the row text is unchanged. Note that--accentagainst--backgroundis only 1.10–1.37:1, so in practice the bar carries the selected state.)--accent-foregroundon--accent; removed the--primary/--ringassertions and kept the reason as a comment; each token tie-back lives only in the contrast test. A shared helper is deferred until a third contrast test exists.)keysassertion and per-entry location checks. (Fixed: removed the tautological test; one sweep now checks every catalog entry'slocations, and it found no stale paths.)OpenedFromTheEditorstory.)usj-nodes-styles.test.ts: theblock()helper duplicated the escaping and comment stripping. (Fixed:escapeand a comment-strippeddeclarationsare hoisted and shared.)useParagraphMenuOpenStatereturned an anonymous type. (Fixed: exportedParagraphMenuOpenStatewith TSDoc.)focusEditorname clash with the\palette's plain focus. (Fixed: the restoring callback is nowrestoreCaretAndFocusEditor; the palette is unchanged.)makeEditorstub duplicated inweb-view.utils.test.ts. (Fixed: hoisted to module level.)The availability gate lives in two places ((No change needed: the inline gate prevents a one-render flash before the effect closes the menu, and a comment says so.)open={isMenuOpen && !isStructureProtected}and the hook's close effect).A(Verified, not reachable: both host palettes are gated on\palette apply with a marker selected might silently do nothing.viewType === 'standard', Standard has no gutter paragraph markers, and a marker can only be selected by clicking a gutter glyph. Even so,applyMarkerMenuSelectioncollapses the marker selection before applying.)onSelectionChange(undefined)for a node selection. The host then clearscurrentSelectionRef, tells the backend there is no selection, andinsertCommentAtCurrentSelection(web-view.tsx:~1416) returns early for the hotkey, the command, and probably the context menu. (Author chose to defer: follow-up ticket draftticket-insert-comment-noop-on-selected-para-marker.md.)Screen-reader announcement of the selected marker.(Engine-owned, viaaria-activedescendantper spec §10; covered by the PR's VoiceOver QA item.)Author response: asked for all minor findings to be fixed. Two were verified with no change needed, one is engine/QA, and the comment no-op was deferred to a follow-up ticket.
Template Propagation
Shared Regions Modified
None.
Extension Config Changes
None.
Positive Observations
returnFocusToEditorputs the "restore beforefocus()" order in one tested helper, which prevents Lexical's fallback to the document end. AninvocationCallOrdertest pins the order.restoreSelectionIfLostwas extended in place rather than forked.useParagraphMenuOpenStateis small, pure state, and fully unit-tested, including "stays closed when availability returns".opengate prevents a one-frame flash.onOpenChange.onCloseAutoFocus+preventDefault+ ref pattern follows repo precedent (find-filters,footnote-type-dropdown,scope-selector).themes.data.json, guards against a sweep that runs nothing, and ties back to the stylesheet, following the ADR precedent.commanddeliberately unset.Interview Notes
platform-yalc.scripture-editors/docs/superpowers/specs/2026-09-23-para-marker-selection-design.md§10, from the 2026-09-25 product/UX review) settles it: closing returns focus to the editor unless the user interacted outside the menu. The author chose to reword the PR description rather than change the behavior. They asked for the character-marker control's possible caret jump to go in a ticket draft.dismissal-patterns.mdxstill states the opposite rule, and that the exception is recorded only in the editor spec. This is worth discussing in the meeting.NodeSelectionwith no offsets; operations act on the owning paragraph; Backspace/Delete goes through the existing$mergeParaIntoPrevious; arrows and typing are redirected; cut, copy, paste and drop are refused; no new exclusion predicate);d269fb5a).getSelection()isundefinedthen;restoreSelectionIfLostmust skip while a marker is selected, or it would restore an out-of-date caret, retag the wrong paragraph and deselect the marker;focus(), because Lexical'sfocus()selects the document end when there is no selection, after which a restore would do nothing;wasClosedByOutsideInteractionRefcovers the outside-interaction path, the trigger re-click is carved out, and the flag resets on every opening.--onto origin/mainafter WI-14's squash-merge. There was one conflict inplatform-scripture-editor.web-view.tsx, resolved by keepingmain'sContentZoomRootwrapper (PT-4585) and addingonParaMarkerMenuRequest. The old branch is kept locally asbackup/pt-4540-pre-rebase.In-Review Quality Check
npm run typecheck: passed.npm run lint: 0 errors (1 unrelated warning inplatform-bible-utils).npm test: every workspace touched by the review passed (core 6279 tests; platform-scripture-editor 102 files).platform-bible-reactStorybook test files fail at import withdoes not provide an export named 'compareProjectsByName'. The export exists in both the utilssrcanddist, so this looks like a stale Storybook vitest dependency cache (node_modules/.cache/storybook). None of the review's changes touchlib/.origin/main:npm run typecheckpassed,npm run lintexited 0, the keyboard catalog suite passed (133 tests), and the platform-scripture-editor suite passed (107 files, 3470 tests).Suggested Review Focus
dismissal-patterns.mdx"Restore focus on close" (WCAG 2.4.3). Is the editor spec §10 a sufficient record, or should the guideline carry an exception note?CharacterMarkerControlstill refocuses the editor. Confirm the character-marker follow-up (ticket draftticket-character-marker-close-caret-jump.md: a barefocus()without a caret restore may move the caret to the chapter end on an Escape close).ticket-insert-comment-noop-on-selected-para-marker.md). Also: isupdateSelectionInternal(undefined)the right signal for other listeners while a marker is selected?--accentagainst--backgroundis only 1.10–1.37:1, so the 4px bar is the real signal. Is that enough of the "whole-row highlight" Jira asked for? Check in manual QA across all four themes.Summary
Host (paranext-core) half of PT-4540 (Todd NN-2.3): in Simple mode Saroj can select a paragraph marker itself (click its gutter glyph) and change it with the toolbar paragraph dropdown. The editor half — selection, keys, DOM class — is scripture-editors branch
pt-4540-para-marker-selection(PR 1); this PR only consumesEditorRef.getSelectedParaMarker(),EditorProps.onParaMarkerMenuRequestand thepsc-para-marker-selectedclass.Why review this
Part of the current epic (Todd's "Simple is coherent for Saroj", must-have 2.3).
Changes
restoreSelectionIfLosttreats a selected marker as a live selection, so picking from the dropdown retags the marker's paragraph instead of restoring an older caret elsewhere.ParagraphStyleTrigger's popover is controlled via a newuseParagraphMenuOpenStatehook, so Enter / Alt+↓ on a selected marker (onParaMarkerMenuRequest) opens it. Structure lock → the existing "Structure is locked" notification; read-only / no block → ignored; the menu closes and stays closed if it becomes unavailable while open.wrapMarkerMenuItemsWithClose)..psc-para-marker-selectedgets an--accentfill and a 4px--foregroundleading bar across the gutter (RTL mirrored), on its own channel beside the active-text focus box. The bar token is pinned by a contrast test (≥ 3:1 vs--backgroundand--accentin all four themes). The block sits after the gutter-marker rules because the selected-glyph rule ties in specificity with the base glyph rule; a test pins that order.command— it only works while a marker is selected).Decisions since the spec (product/UX review)
From the PT-4540 Jira comment (UX) and Discord (Todd):
Hidden-tab case (cross-view-sync rule): not applicable — no cross-view sync is added, and the menu request comes only from a keypress in the focused editor.
Testing
npm run typecheck,npm run lint: clean. Extension suite 3132/3132, catalog suite 31/31 on the rebased branch. Fullnpm testpassed before the rebase; two workspace failures seen once (vitest worker fetch timeout; Storybook browser connection closed) passed when re-run alone.git -C dev-packages/scripture-editors pull && npm run build:editor).aria-activedescendant(spec §4.9).Before merge
mainplatform-yalc+ lockfile, plan Task 0 Phase B)NodeSelectiontarget, with focus returned to the editor after the menu" warrant anArchitecture-Decisions.mdentry or an amendment toadr-list-selection-on-a-dedicated-visual-channel?Risk Level
Medium — changes focus handling of a toolbar popover used on every paragraph retag; behaviour is covered by component tests but not yet exercised in the app.
AI Involvement
AI-assisted — planned and implemented with Claude Code (subagent-driven: per-task implementer + reviewer, then a whole-branch review whose findings were fixed). Reviewed by the author before merge.
🤖 Generated with Claude Code
This change is