Skip to content

Add the read-only analysis catalog panel - #234

Open
alex-rawlings-yyc wants to merge 10 commits into
mainfrom
analysis-catalog-panel
Open

Add the read-only analysis catalog panel#234
alex-rawlings-yyc wants to merge 10 commits into
mainfrom
analysis-catalog-panel

Conversation

@alex-rawlings-yyc

@alex-rawlings-yyc alex-rawlings-yyc commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Hoists the analysis store above the cross-book fade curtain so a jump to a
usage in another book cannot dim the panel. Search, sort, filter, and row
windowing are deferred to #231.


This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Added an Analysis Catalog accessible from the Interlinearizer View menu.
    • Browse usage-sorted analysis entries, expand morphemes and usages, and navigate to locations across books.
    • Resize the catalog with mouse or keyboard controls; width and view state are preserved.
    • Added localized labels, controls, empty states, and accessibility support.
  • Bug Fixes

    • Improved language handling with reliable locale fallbacks.
    • Preserved catalog and WebView state during loading and book changes.

@alex-rawlings-yyc alex-rawlings-yyc self-assigned this Aug 14, 2026
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ea102c2-8ae9-4528-bd2d-98883dfbf805

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a80cc977-fcd6-4b6b-82d4-bd017c1318bd

📥 Commits

Reviewing files that changed from the base of the PR and between 50f622e and cb6c39d.

📒 Files selected for processing (11)
  • REVIEW.md
  • src/__tests__/components/AnalysisCatalogPanel.test.tsx
  • src/__tests__/components/InterlinearizerLoader.test.tsx
  • src/__tests__/main.test.ts
  • src/__tests__/test-helpers.ts
  • src/components/AnalysisStore.tsx
  • src/components/CatalogRowView.tsx
  • src/components/InterlinearizerLoader.tsx
  • src/hooks/usePanelResize.ts
  • src/main.ts
  • src/types/interlinearizer.d.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/tests/main.test.ts
  • src/main.ts
  • src/types/interlinearizer.d.ts
  • REVIEW.md
  • src/tests/test-helpers.ts
  • src/components/AnalysisStore.tsx
  • src/tests/components/InterlinearizerLoader.test.tsx
  • src/components/InterlinearizerLoader.tsx

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The Interlinearizer adds a localized, resizable Analysis Catalog with usage navigation, persisted panel state, command wiring, writing-system support, and expanded test coverage.

Changes

Analysis Catalog feature

Layer / File(s) Summary
Command and menu integration
contributions/menus.json, contributions/localizedStrings.json, src/main.ts, src/types/interlinearizer.d.ts, __mocks__/platform-bible-react.tsx, __mocks__/lucide-react.tsx, src/__tests__/main.test.ts
Adds the interlinearizer.openAnalysisCatalog command, View menu entry, localized strings, activation registration, icon mocks, and test support.
Catalog data and loader wiring
src/components/AnalysisStore.tsx, src/hooks/useInterlinearizerBookData.ts, src/components/InterlinearizerLoader.tsx
Exposes book-specific catalog rows and writing-system data. The loader manages provider placement, open state, panel width, fade behavior, and catalog rendering.
Catalog panel and row behavior
src/components/AnalysisCatalogPanel.tsx, src/components/CatalogRowView.tsx, src/hooks/usePanelResize.ts, src/utils/language-tags.ts, REVIEW.md
Adds localized catalog rendering, sorting, expandable row details, usage navigation, bounded resizing, locale collation, and tooltip guidance.
Catalog integration and behavior tests
src/__tests__/components/AnalysisCatalogPanel.test.tsx, src/__tests__/components/InterlinearizerLoader.test.tsx, src/__tests__/hooks/useInterlinearizerBookData.test.ts, src/__tests__/test-helpers.ts, src/__tests__/utils/language-tags.test.ts
Adds coverage for catalog behavior, navigation, resizing, persistence, writing-system fallback, reactive WebView state, and locale collation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🔵 Low · up to cb6c3

The panel behavior is localized, but resizing retains bounded RTL-direction and duplicate-write risks that warrant explicit owner awareness or follow-up before merge; no high-impact correctness or availability risk is indicated.

Sequence Diagram(s)

sequenceDiagram
  participant Menu
  participant InterlinearizerLoader
  participant AnalysisStore
  participant AnalysisCatalogPanel
  participant CatalogRowView
  Menu->>InterlinearizerLoader: openAnalysisCatalog
  InterlinearizerLoader->>AnalysisStore: provide book catalog state
  AnalysisCatalogPanel->>AnalysisStore: select catalog rows
  AnalysisStore-->>AnalysisCatalogPanel: return catalog rows
  AnalysisCatalogPanel->>CatalogRowView: render rows
  CatalogRowView->>InterlinearizerLoader: navigate to selected usage
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a read-only analysis catalog panel.
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.
✨ 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 analysis-catalog-panel

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.

coderabbitai[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as outdated.

Hoists the analysis store above the cross-book fade curtain so a jump to
a
usage in another book cannot dim the panel. Search, sort, filter, and
row
windowing are deferred to #231.
Also commits a released drag width from a ref rather than from inside
the
setDragWidth updater, which React may run more than once.
An unparseable analysis-language tag threw out of Intl.Collator and
blanked
the whole view; the resize handle inverted in right-to-left interfaces;
and
the row toggle's aria-label suppressed the analysis it named. Rows now
share
one localization subscription instead of one apiece.
A pointer released where the window cannot see it — over a native menu,
which takes the pointer with it — left the drag running, so the panel
went
on resizing under a button-less pointer and committed that width at the
next click anywhere. A move reporting no button held now ends the drag
at
the width it had reached, since that move is the only word the window
gets
of such a release.

Arrow keys stand aside while a drag is in flight. They stepped off the
width the drag began at, reporting a width the panel was not showing
only
for the release to overwrite it; the pointer owns the width while it is
held.

The drag test helper now dispatches its moves with a held button, which
a
real one carries and jsdom does not.
The held-focus-request test left the reference on GEN while EXO's view
mounted, a state the host never produces; move it to EXO with the jump.
The simulated drag omitted `buttons`, so the move arrived reporting none
— which the resize hook reads as a release it never saw, ending the drag
before it recorded a width. The panel fell back to its default, and the
remount assertion compared that default against itself. Dropping width
persistence outright left the test green.

Carry `buttons` on the move so the drag resizes, and name the expected
width at both ends rather than checking the two renders agree, since the
default is what a dead drag and a dropped write alike leave behind.
Any button began a drag: the move handler asks only whether some button
is held, so a middle-button press followed the pointer to its release
and persisted the width it reached. A right-button press does the same
wherever the context menu opens on release rather than on press.

Also correct the loader's width-restore comment, which described a move
reporting no buttons held while the move beneath it carries one.

@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.

Review by Claude Opus 5 (1M context), inspired by Devin, briefly reviewed by me.

Everything is inline: eight ⛏️ suggestions and two ❓ decisions. Nothing here looks like a correctness bug.

Comment thread src/components/CatalogRowView.tsx Outdated
* Each row owns its own layout so that its detail can be nested inside it. One element per analysis
* is what lets the list window and be walked by keyboard a row at a time.
*/
export default function CatalogRowView({

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.

⛏️ Consider wrapping this in React.memo. applyCatalogQuery filters and sorts without cloning, so rows keep their identity and the remaining props are stable; without memo, every resize-drag frame (setDragWidth fires per mousemove) and every gloss commit reconciles every row's subtree in an unwindowed list. Worth confirming #231 covers memoizing the row alongside windowing.

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.

Done — MemoizedCatalogRowView, matching the TokenChip / SegmentView pattern. Your premise checks out: applyCatalogQuery is filter + toSorted, so rows keep identity, and the rest of the props are stable (onUsageSelect is a useCallback, localizedStrings comes off the hoisted key array). Filed the windowing half against #231 rather than duplicating it here.

const hiddenUsageCount = row.usages.length - visibleUsages.length;

const usageCountLabel = localizedStrings['%interlinearizer_analysisCatalog_usageCount%'];
const usageCountInBookLabel = formatReplacementString(

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.

usageCountInBookLabel substitutes currentBook verbatim, so the tooltip and screen-reader text read "Uses in GEN". The other place this extension fills a {book} placeholder in user-facing prose resolves it to a name first (Canon.bookIdToEnglishName, src/components/SegmentListView.tsx:220). Raw codes are right for the usage links themselves — those are written as references (GEN 1:1) — but is the code deliberate for the count label?

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.

Not deliberate — good catch. Switched to Canon.bookIdToEnglishName, so it reads "Uses in Genesis" while the usage links stay references. Same treatment and same caveat as SegmentListView.tsx:220 (a platform-localized name needs PAPI wiring this view doesn't have). Covered by a test that layers a resolved value over the key-as-value mock.

{hiddenUsageCount > 0 && (
<Button
data-testid="catalog-usages-show-all"
onClick={() => setShowsAllUsages(true)}

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.

⛏️ showsAllUsages is only ever set to true, and it survives collapsing and re-expanding the row (both flags are row-local state that resets only on unmount), so a row expanded to hundreds of usages cannot be returned to the inline cap. Consider resetting it when the row collapses, or offering a "show fewer" affordance.

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.

Fixed. A handleToggle clears showsAllUsages alongside the expand flag, so collapsing returns the row to the inline cap. Went with the reset rather than a "show fewer" affordance — the expander is already gone by then, so there's nothing to pair it with.


window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {

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.

⛏️ Commits happen only in endDrag, so if the panel unmounts mid-drag this cleanup drops the listeners and the width reached is silently discarded — the panel comes back at its previous width. The close control is not reachable mid-drag, but a draftVersion bump or isDraftLoading flipping true does unmount the panel (src/components/InterlinearizerLoader.tsx:639). A commit in the cleanup would cover it.

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.

Fixed, though not in that cleanup: it also runs when the drag ends normally and when the caller's callback changes identity, so committing there would fire part-way through a gesture. Added a separate mount-lifetime effect that commits dragWidthRef.current on unmount, reading the callback off a ref. Tests cover both unmounting with a drag in flight and without.

Comment thread src/hooks/usePanelResize.ts Outdated
const onKeyDown = useCallback(
(event: ReactKeyboardEvent) => {
// eslint-disable-next-line no-nested-ternary -- a two-key lookup reads worse as a map
const travel = event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0;

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.

⛏️ The splitter handles Left/Right only. Home/End (jump to min/max) is part of the ARIA window-splitter pattern and is a couple of lines in this same handler.

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.

Added. Both jump to the bounds, so they land on the aria-valuemin / aria-valuemax the splitter already announces — which also means they need no RTL mirroring, unlike the arrows. Pulled the key→intent mapping into two small helpers, which retired the no-nested-ternary disable.

Comment thread src/hooks/usePanelResize.ts Outdated
// The arrow that moves the handle the way a drag would widen the panel widens it too,
// whichever side of the container the interface language anchors it to.
const step = travel === widenTravel() ? 1 : -1;
onWidthChange(clampWidth(width + step * KEYBOARD_RESIZE_STEP_PX));

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.

❓ Each arrow press writes straight through to useWebViewState (src/components/InterlinearizerLoader.tsx:394, :687), so holding the key with OS key repeat produces one host write per repeat — the same per-frame write the drag path defers to release. The hook's own doc acknowledges the difference, so this reads as intentional: is the host write cheap enough that the keyboard path does not want the same deferral (commit on keyup, or debounce)?

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.

Intentional, and not only on cost grounds. displayWidth falls back to width, which comes from useWebViewState — and that hook updates its local state only when onDidUpdateWebView comes back. So on the keyboard path the write is the redraw: commit-on-keyup would leave the handle frozen for the whole hold unless the keyboard path also grew its own in-flight width and a blur commit to go with it. The drag path has no such coupling — it already draws from dragWidth — which is why deferral is free there and isn't here. Left as is; the hook doc now also states the unmount commit.

Comment thread src/hooks/usePanelResize.ts Outdated
[width, onWidthChange, clampWidth],
);

return { displayWidth: dragWidth ?? width, onMouseDown, onKeyDown };

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.

⛏️ displayWidth falls back to the caller's committed width unchanged, and that width is read straight out of WebView state — only the drag and arrow paths clamp. Every reachable value is inside WIDTH_BOUNDS today, but dragWidth ?? clampWidth(width) would make the bounds authoritative and keep aria-valuenow within aria-valuemin/aria-valuemax if the bounds are ever narrowed (or a persisted value ever arrives non-numeric).

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.

Done — dragWidth ?? clampWidth(width). Test renders at 5000 and asserts both the drawn width and aria-valuenow come back at the maximum.

Comment thread src/components/AnalysisStore.tsx Outdated
selectAnalysis,
selectAnalysisLanguage,
selectApprovedGloss,
selectCatalogRows,

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.

⛏️ This import sits between selectApprovedGloss and selectApprovedMorphemes, splitting the two Approved entries.

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.

Moved after selectApprovedMorphemes.

Comment thread src/components/AnalysisStore.tsx Outdated
/**
* Returns one row per distinct token analysis in the draft, each carrying the usage data the
* analysis catalog lists it by, in the analysis's own order. Narrowing and ordering are the
* caller's ({@link applyCatalogQuery}), so a keystroke re-runs only that pass.

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.

⛏️ {@link applyCatalogQuery} has no resolvable target in this file — only the CatalogRow type is imported from ../utils/analysis-query (line 37), while every other {@link} in this module names an imported symbol. Either import the symbol or reword to state the guarantee ("narrowing and ordering are the caller's") without naming the collaborator.

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.

Reworded rather than imported. comment-rules has a "link the contract, not the collaborator" rule that makes naming the callee wrong even where it would resolve, so the sentence now just states the guarantee: "Narrowing and ordering are the caller's, so a keystroke re-runs only that pass."

// The store below waits for the draft: it seeds on mount alone, and the draft version that
// remounts it does not bump when the load completes. Nothing is lost by waiting — while the
// draft loads there is only ever a placeholder or an error panel to show.
<div className="tw:flex tw:flex-col tw:flex-1 tw:min-h-0">{loadingOrErrorPanel}</div>

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.

⛏️ This branch lost the data-testid="book-fade-wrapper" element and its opacity transition. Harmless today — draft loading only happens at mount, when fadePhase is idle — but any future path that leaves isDraftLoading true during a cross-book fade would render the placeholder outside the curtain, and a test reaching for book-fade-wrapper while the draft loads would throw rather than fail informatively.

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.

Fixed by extracting a BookFadeWrapper component both branches render, so the draft-loading branch gets the test id and the fade back without a second copy of the zero-duration logic. The store still sits above the curtain, so the catalog stays undimmed. Copying the JSX instead would have left the fadePhase === 'out' arm unreachable on that branch and broken the coverage gate, which is what pushed it to a shared component.

The splitter gains Home/End and commits a drag the panel is unmounted
holding; rows memoize and return to the inline usage cap when collapsed.

@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 made 10 comments.
Reviewable status: 0 of 20 files reviewed, 10 unresolved discussions (waiting on imnasnainaec).

Comment thread src/components/AnalysisStore.tsx Outdated
selectAnalysis,
selectAnalysisLanguage,
selectApprovedGloss,
selectCatalogRows,

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.

Moved after selectApprovedMorphemes.

Comment thread src/components/AnalysisStore.tsx Outdated
/**
* Returns one row per distinct token analysis in the draft, each carrying the usage data the
* analysis catalog lists it by, in the analysis's own order. Narrowing and ordering are the
* caller's ({@link applyCatalogQuery}), so a keystroke re-runs only that pass.

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.

Reworded rather than imported. comment-rules has a "link the contract, not the collaborator" rule that makes naming the callee wrong even where it would resolve, so the sentence now just states the guarantee: "Narrowing and ordering are the caller's, so a keystroke re-runs only that pass."

Comment thread src/components/CatalogRowView.tsx Outdated
* Each row owns its own layout so that its detail can be nested inside it. One element per analysis
* is what lets the list window and be walked by keyboard a row at a time.
*/
export default function CatalogRowView({

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.

Done — MemoizedCatalogRowView, matching the TokenChip / SegmentView pattern. Your premise checks out: applyCatalogQuery is filter + toSorted, so rows keep identity, and the rest of the props are stable (onUsageSelect is a useCallback, localizedStrings comes off the hoisted key array). Filed the windowing half against #231 rather than duplicating it here.

const hiddenUsageCount = row.usages.length - visibleUsages.length;

const usageCountLabel = localizedStrings['%interlinearizer_analysisCatalog_usageCount%'];
const usageCountInBookLabel = formatReplacementString(

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.

Not deliberate — good catch. Switched to Canon.bookIdToEnglishName, so it reads "Uses in Genesis" while the usage links stay references. Same treatment and same caveat as SegmentListView.tsx:220 (a platform-localized name needs PAPI wiring this view doesn't have). Covered by a test that layers a resolved value over the key-as-value mock.

{hiddenUsageCount > 0 && (
<Button
data-testid="catalog-usages-show-all"
onClick={() => setShowsAllUsages(true)}

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.

Fixed. A handleToggle clears showsAllUsages alongside the expand flag, so collapsing returns the row to the inline cap. Went with the reset rather than a "show fewer" affordance — the expander is already gone by then, so there's nothing to pair it with.

// The store below waits for the draft: it seeds on mount alone, and the draft version that
// remounts it does not bump when the load completes. Nothing is lost by waiting — while the
// draft loads there is only ever a placeholder or an error panel to show.
<div className="tw:flex tw:flex-col tw:flex-1 tw:min-h-0">{loadingOrErrorPanel}</div>

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.

Fixed by extracting a BookFadeWrapper component both branches render, so the draft-loading branch gets the test id and the fade back without a second copy of the zero-duration logic. The store still sits above the curtain, so the catalog stays undimmed. Copying the JSX instead would have left the fadePhase === 'out' arm unreachable on that branch and broken the coverage gate, which is what pushed it to a shared component.


window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {

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.

Fixed, though not in that cleanup: it also runs when the drag ends normally and when the caller's callback changes identity, so committing there would fire part-way through a gesture. Added a separate mount-lifetime effect that commits dragWidthRef.current on unmount, reading the callback off a ref. Tests cover both unmounting with a drag in flight and without.

Comment thread src/hooks/usePanelResize.ts Outdated
const onKeyDown = useCallback(
(event: ReactKeyboardEvent) => {
// eslint-disable-next-line no-nested-ternary -- a two-key lookup reads worse as a map
const travel = event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0;

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.

Added. Both jump to the bounds, so they land on the aria-valuemin / aria-valuemax the splitter already announces — which also means they need no RTL mirroring, unlike the arrows. Pulled the key→intent mapping into two small helpers, which retired the no-nested-ternary disable.

Comment thread src/hooks/usePanelResize.ts Outdated
// The arrow that moves the handle the way a drag would widen the panel widens it too,
// whichever side of the container the interface language anchors it to.
const step = travel === widenTravel() ? 1 : -1;
onWidthChange(clampWidth(width + step * KEYBOARD_RESIZE_STEP_PX));

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.

Intentional, and not only on cost grounds. displayWidth falls back to width, which comes from useWebViewState — and that hook updates its local state only when onDidUpdateWebView comes back. So on the keyboard path the write is the redraw: commit-on-keyup would leave the handle frozen for the whole hold unless the keyboard path also grew its own in-flight width and a blur commit to go with it. The drag path has no such coupling — it already draws from dragWidth — which is why deferral is free there and isn't here. Left as is; the hook doc now also states the unmount commit.

Comment thread src/hooks/usePanelResize.ts Outdated
[width, onWidthChange, clampWidth],
);

return { displayWidth: dragWidth ?? width, onMouseDown, onKeyDown };

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.

Done — dragWidth ?? clampWidth(width). Test renders at 5000 and asserts both the drawn width and aria-valuenow come back at the maximum.

A drag seeded from a committed width outside the bounds drew the panel
past
its announced maximum on the press alone. The jump and arrow keys now
skip
writing a width the panel already holds.
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.

Analysis catalog: read-only panel, command, and jump-to-usage navigation

2 participants