Skip to content

Add search, sort, filter, and windowing to the analysis catalog - #253

Open
alex-rawlings-yyc wants to merge 14 commits into
mainfrom
analysis-catalog-search
Open

Add search, sort, filter, and windowing to the analysis catalog#253
alex-rawlings-yyc wants to merge 14 commits into
mainfrom
analysis-catalog-search

Conversation

@alex-rawlings-yyc

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

Copy link
Copy Markdown
Contributor

Closes #231.

The catalog listed every analysis in one fixed order with no way to narrow it, so a draft of any size was navigable only by scrolling. The query core (#192, PR #211) already backed all of this — applyCatalogQuery, deriveFacets, CatalogSort, and CatalogFilters were built and tested. This is the UI that varies them: the panel's fixed query literal becomes state the controls drive, and nothing in the query core changed.

Search, sort, and filter state is ephemeral useState inside the panel. The panel is mounted only while it is open, so closing it clears the query by construction rather than by a reset effect — a filter that survived a reload would leave rows missing with nothing on screen saying why. Open/closed and width stay tab-scoped in useWebViewState.

Filters sit behind one control

A panel narrow enough to need filtering should not itself be filled with filter controls, so all of them live in a popover whose trigger reports how many are active.

All four groups ship. Against today's data only books ever raises a control: nothing writes part of speech, confidence, or features yet, so deriveFacets correctly yields nothing for them and no control appears. Used nowhere ships too, and matches nothing until PT9 import (#150) or the catalog's own delete/merge paths land — detachTokenAnalysisLink drops a payload with its last link, so no current write path can produce a zero-usage row. Both are the outcome #231 decided on and both are tested for, not gaps.

Facets are derived from every row the draft holds rather than from the rows a filter left standing. A facet judged against its own selection's survivors would collapse to that selection, leaving nothing on screen to widen it back by.

Carrying no value is a choice of its own

CatalogFacets lists the absent value as undefined, which is what lets a reader ask which analyses are still missing a field as readily as which carry a given value. The platform MultiSelectComboBox speaks strings alone, so that choice needs a spelling: \u0000untagged, a leading NUL being one no part of speech, confidence level, or feature value can collide with. Values are read back through a map rather than compared against the sentinel, so undefined is recovered as the choice it is. Worth a look — it is the one place the control's vocabulary and the filter's diverge.

Books is the exception: a usage names a book by construction, so that facet never offers an untagged choice, and the selection is filtered before it reaches CatalogFilters.

Windowing

useRowWindow mounts a growing leading slice of the listing and extends by a chunk each time an end-of-list sentinel comes within reach. Grow-only and anchored to nothing — it never culls from the top and never adjusts the scroll position. Deliberately not useSegmentWindow: that hook is anchored around a scripture reference, and a row list has no counterpart to hold still, so it needs none of that geometry bookkeeping.

Two details worth the reviewer's attention:

  • The window starts over when rows is a different array, adjusted during the render that first sees the new listing rather than in an effect afterwards — an effect would let one frame paint the new rows at the old, grown count before shrinking back. Keyed on array identity rather than length, because a query can narrow a listing to a different set of rows of the same size.
  • The observer re-subscribes on every count change, not just on the elements'. An IntersectionObserver reports only intersection transitions: after an extend the sentinel node is unchanged and may still sit inside the arming margin, where a stale observer would stay silent however far the reader scrolls. A fresh observer re-delivers the current state, extending once per delivery until the sentinel is pushed clear.

Not in the issue: a listing narrowed to nothing says so

Reusing "No analyses recorded yet" for a query that matched nothing would tell readers their draft is empty when they have merely mistyped, and send them looking for lost work. There is now a second message for that case, and both go through the platform EmptyState rather than a hand-rolled paragraph.

Search semantics are unchanged and accepted as-is

applyCatalogQuery matches the whole trimmed, folded query as a single substring against a per-row blob. So a multi-word query never matches across fields, there is no match weighting, and ordering is the chosen sort key alone. The placeholder promises "Search forms and glosses" and nothing more. Multi-term search, relevance ranking, and highlighting are query-core work, not UI work, and separate issues if wanted.

Mocks

Stubs the platform SearchBar, Select, MultiSelectComboBox, and EmptyState, each documenting where it diverges from the component it stands in for — notably that MultiSelectComboBox resolves an entry by label as the real component's own select handler does, so a stub test cannot pass on a collision the real component would drop.

Testing

Covers all eight behaviors #231 lists. Full suite passing, 100% coverage, lint clean.


This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Added an Analysis Catalog panel with searchable, sortable, and filterable analysis entries.
    • Added filtering by book, part of speech, confidence, features, gloss availability, and usage.
    • Added expandable analysis details, usage navigation, selection persistence, and localized empty states.
    • Added virtualized loading for improved performance with large catalogs.
    • Added English localization for catalog controls, labels, messages, and Save As states.
  • Tests

    • Added comprehensive coverage for catalog interactions, filtering, sorting, navigation, localization, and empty states.

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

coderabbitai Bot commented Aug 20, 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: Team

Run ID: e9a06aa5-4303-449d-a897-beb9a1ada9a4

📝 Walkthrough

Walkthrough

Adds a localized Analysis Catalog panel with search, sorting, facet filters, tri-state morpheme filtering, zero-usage filtering, virtualized rows, and expanded test coverage. Jest mocks now support the required platform controls and icons.

Changes

Analysis Catalog

Layer / File(s) Summary
Catalog row windowing
src/hooks/useRowWindow.ts
Adds a grow-only 40-row window that expands when an end sentinel enters the scroll margin and resets when the row array changes.
Catalog panel and query controls
src/components/AnalysisCatalogPanel.tsx, src/components/CatalogQueryControls.tsx, src/components/CatalogFilterPopover.tsx, src/__tests__/components/AnalysisCatalogPanel.test.tsx, contributions/localizedStrings.json, __mocks__/*
Adds localized search, sorting, facet filters, filter toggles, virtualized rendering, usage navigation, selection persistence, and tests for the catalog behaviors. Supporting mocks cover search, select, multi-select, tooltip, resizable panels, toolbar commands, token references, and icons.

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

Merge Risk: 🔵 Low · up to f4b84

The catalog enhancements are otherwise mergeable, but the resize mock can accumulate keydown listeners when its target ref changes, leading to repeated event handling in affected tests or integrations. This is a bounded, localized follow-up item.

Sequence Diagram(s)

sequenceDiagram
  participant CatalogUser
  participant AnalysisCatalogPanel
  participant CatalogQueryControls
  participant CatalogFilterPopover
  participant useRowWindow
  CatalogUser->>CatalogQueryControls: enter search or choose sort
  CatalogUser->>CatalogFilterPopover: choose filters
  CatalogQueryControls->>AnalysisCatalogPanel: update query state
  CatalogFilterPopover->>AnalysisCatalogPanel: update filter state
  AnalysisCatalogPanel->>useRowWindow: pass queried rows
  useRowWindow-->>AnalysisCatalogPanel: return visible row window
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 summarizes the primary changes: search, sorting, filtering, and row windowing for the analysis catalog.
Linked Issues check ✅ Passed The changes implement the objectives in issue #231. They add search, all five sort keys, facet and value filters, ephemeral panel state, facet visibility rules, grow-only windowing, selection persiste…
Out of Scope Changes check ✅ Passed The changes are in scope for issue #231. The mocks, localization strings, implementation components, hook, and tests directly support the analysis catalog controls and row windowing.
Docstring Coverage ✅ Passed Docstring coverage is 97.92% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 21 files. (1 skipped: 1…
Full details: Linked Issues check

Explanation

The changes implement the objectives in issue #231. They add search, all five sort keys, facet and value filters, ephemeral panel state, facet visibility rules, grow-only windowing, selection persistence, distinct empty states, and tests for the specified behaviors.

Full details: Docstring Coverage

Explanation

Docstring coverage is 97.92% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 21 files. (1 skipped: 1 unsupported.)

✨ 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-search

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.

@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the analysis-catalog-search branch 8 times, most recently from 324afc3 to cd98a29 Compare August 21, 2026 16:24
coderabbitai[bot]

This comment was marked as outdated.

@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the analysis-catalog-search branch 5 times, most recently from 378003a to f4b84ce Compare August 26, 2026 19:25
coderabbitai[bot]

This comment was marked as outdated.

@alex-rawlings-yyc
alex-rawlings-yyc marked this pull request as ready for review August 28, 2026 18:57
@alex-rawlings-yyc
alex-rawlings-yyc force-pushed the analysis-catalog-search branch 2 times, most recently from d85e9e3 to 46e7b1d Compare August 31, 2026 17:54
imnasnainaec

This comment was marked as resolved.

@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 13 comments.
Reviewable status: 2 of 12 files reviewed, 13 unresolved discussions (waiting on imnasnainaec).


src/__tests__/utils/language-tags.test.ts line 34 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ Could define a const for en_US in this describe block.

Left as-is. en_US appears in both describe blocks standing for two different things — a tag the collator falls back from, and an interface locale Intl rejects mid-list — so a shared const would have to sit above both and name neither case well. The literal being an obviously hand-typed tag is also the point being made, which a const would hide behind a name.


src/components/AnalysisCatalogPanel.tsx line 134 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ Drafted by Claude:

This rationale is deriveFacets's own, and it is stated there already:

@param rows Every row {@link buildCatalogRows} built, never what {@link applyCatalogQuery}
narrowed them to: a facet judged against the rows its own selection kept collapses as soon as
that selection is made, leaving nothing to widen it back by.

Hover over the call shows that, so the copy here is a second home for a rule that can only ever have one. It is also in two more places downstream — CatalogFilterPopoverProps.facets and CatalogQueryControlsProps.facets — neither of which derives anything; they receive whatever they are handed.

Suggest keeping the rule where the derivation happens and letting the three consumers say what the value is:

const facets = useMemo(() => deriveFacets(catalogRows), [catalogRows]);

(the call is self-evident, so no doc at all reads fine here), and in both props types:

/** The choices worth offering as filters. */
facets: CatalogFacets;

CatalogFacets carries the rest for anyone who follows the type.

Done — dropped the doc here and reduced both props types to /** The choices worth offering as filters. */.


src/components/CatalogFilterPopover.tsx line 151 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ Drafted by Claude:

Three doc comments in this file explain that a field can stop offering choices while a reader is filtered to one, and that a selection with no control left is unclearable: isFilterable, this one, and featureNames. One statement of it is enough, and isFilterable is where it belongs — the gate exists for exactly that case, and both of the others are visibly implementing the same rule against different shapes.

Suggest leaving isFilterable's doc as it stands and reducing these two to what they are:

/** The choices to offer: the field's own, plus any still selected that it no longer lists. */
/**
 * The feature names to raise a control for: those the rows offer choices for, and any a selection
 * still narrows by.
 */

Done, both reduced to your suggested wording. isFilterable keeps the rule.


src/components/CatalogFilterPopover.tsx line 179 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ This second paragraph is redundant with in-line comments. One or the other should probably be removed/reduced.

Dropped the paragraph. The inline comments sit where the marking actually happens and carry the loop bound, so they were the half worth keeping.


src/components/CatalogFilterPopover.tsx line 413 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ Consider only showing the morphemes Select when setting "Show morphology" is on.

Done — the breakdown filter is now offered only where interlinearizer.showMorphology is on. You're right that a project not doing morphological analysis has no use for the question, and since the setting is project-scoped and durable rather than a view toggle, it's a fair read of whether this project works in morphemes at all.

One addition: the control also stays while a breakdown filter is already set, so turning the setting off behind an active has/lacks doesn't leave the list narrowed with nothing on screen to clear it by — the same case isFilterable covers for the facet filters. Both sides are tested.

The setting reaches the panel as a prop from the loader, which already resolves it for the view, rather than being read a second time in the panel.


src/components/CatalogFilterPopover.tsx line 451 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ The wording of this comment is reviewer defense. It can be dramatically cut down.

Cut to one line: Matches nothing until a write path can leave an analysis unused, which none does yet.


src/components/CatalogQueryControls.tsx line 16 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ The second sentence (Covers...) is unnecessary. That's what satisfies Record<... means.

Dropped. CONFIDENCE_LABEL_KEYS in CatalogFilterPopover carried the same sentence about Confidence, so that one went too.


src/components/CatalogQueryControls.tsx line 93 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

This sort Select should probably have some sort of sort icon (e.g., up/down arrows) beside or at one end.

Added — ArrowUpDown inside the trigger, ahead of the value, with a stub added to the lucide-react mock.


src/hooks/usePanelResizeKeys.ts line 73 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ Since widenTravel is now only used once, it would be a touch less obfuscating to delete it and use its content directly here.

Done. Inlining collapsed it further than substitution would suggest: (readDirection() === 'rtl' ? 1 : -1) !== 1 is just readDirection() !== 'rtl', so the gate now reads as "leave it to the handle unless RTL". The one fact in widenTravel's doc the code doesn't show — that direction is settled per press, so a live language change is honored — moved to the hook's doc.


src/hooks/useRowWindow.ts line 44 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ The last (a gloss...) clause of this comment is justifying a commit but is somewhat out-of-context here. May not have enduring value.

Dropped the clause; the sentence now ends at "any edit to the underlying analysis as well."


src/hooks/useRowWindow.ts line 66 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ This comment has too much about a rejected alternative.

Dropped the rejected alternative, keeping only that the elements are held in state so the observer effect re-runs as each attaches.


src/hooks/useRowWindow.ts line 84 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ Much of this comment is stating mechanism that's obvious from the code.

Trimmed to the part the code can't show — that IntersectionObserver reports transitions only, which is why count is a dependency. The re-delivery walkthrough and the line about a full window standing down both went.


src/utils/language-tags.ts line 42 at r1 (raw file):

Previously, imnasnainaec (D. Ror.) wrote…

⛏️ The use of try/catch here is pretty self-explanatory, so the "Weighed one at a time" comment feels unnecessary.

Dropped the comment.

@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 12 files and all commit messages, and resolved 13 discussions.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on alex-rawlings-yyc).

The catalog panel listed every analysis in one fixed order with no way
to
narrow it, so a draft of any size was only navigable by scrolling. The
query
core already supported all of this; only the UI that varies it was
missing.

Search, sort, and filter state is ephemeral useState inside the panel.
The
panel is mounted only while open, so closing it clears the query — a
filter
that survived a reload would leave rows missing with nothing on screen
saying
why.

Filters sit behind one control that reports how many are active, so a
panel
narrow enough to need filtering is not itself filled with them. All four
groups ship: the facet-derived ones (books, part of speech, confidence,
and
each named feature), missing gloss, breakdown, and unused-only. Against
today's data only books raises a control, since no write path records
the
others yet — the facets are rightly absent rather than offering a lone
choice.

Facets are derived from every row rather than from the rows a filter
left
standing, so a selection cannot collapse the facet that would widen it
back.

The new useRowWindow mounts a growing leading slice of the listing,
extending
as the end comes into reach and starting over when the query changes. It
is
deliberately not useSegmentWindow: a row list has no counterpart to the
scripture reference that hook holds still, so it needs none of that
geometry
bookkeeping.

A listing narrowed to nothing now says so, rather than reusing "No
analyses
recorded yet" and telling readers their draft is empty when they have
merely
mistyped. That message and the panel's original one both go through the
platform EmptyState.

Stubs the platform SearchBar, Select, MultiSelectComboBox, and
EmptyState,
each documenting where it diverges from the component it stands in for.
The sort option substituted the raw book code into "Most used in
{book}",
so the dropdown read "Most used in GEN" while the row column beside it
resolved the same book through Canon.bookIdToEnglishName and read "Uses
in
Genesis" — one book named two ways in one open panel.

Resolve the name once in the panel and pass it to both views, so the two
labels cannot disagree. CatalogQueryControls takes the resolved name
rather
than the code, which keeps it presentational and leaves book-name
resolution
in the panel.
The test rerendered through a bare InterlinearNavProvider rather than
the
PanelProviders root it mounted with. React saw a different element type
at
that position and remounted the provider, reinitializing the ref that
holds
the pending request — so the closing assertion found no request because
none had survived the remount, not because navigating past EXO had
abandoned one. Deleting the abandonment effect entirely left the test
green.

Rerender through PanelProviders instead, keeping the provider that owns
the
request mounted across both navigation steps. The test now fails with
"EXO 3:14:8" when the abandonment effect is removed.

Also lift the two collators out of the query memo. They were rebuilt on
every keystroke in the search box, which changes the query but neither
language tag.
Two identical declarations shadowed each other, and neither tsc nor
ESLint covers __mocks__, so nothing flagged it.
alex-rawlings-yyc and others added 10 commits September 2, 2026 12:30
The stub group seeded its layout from `defaultLayout` unconditionally,
where
the real group takes that prop only when it names exactly the panels
mounted
and discards it otherwise. A layout naming a closed panel therefore came
back
by itself here on the next mount, which upstream would have thrown away.

That divergence hid the loader's restoring effect: the width it exists
to
reapply was already in the group's state before it ran, so the effect
could be
made a no-op with every catalog test still passing. Seed after the
panels have
registered instead, matching on the count as the real group does.

Cover the effect with a test that mounts the group closed, leaving it
knowing
only of the view so the stored layout reaches it only by being applied
as the
catalog's panel joins. Disabling the effect now fails five tests.
The window reset exists for a changed query: a reader who narrows a
listing is looking at a new list, not further down the old one. It was
keyed on the rows array's identity instead, which is only a proxy for
that — and a proxy that also turns over on any edit to the underlying
analysis. A gloss approved in the view beside an open catalog therefore
collapsed a deeply scrolled list back to its first chunk, throwing the
reader to the end of forty rows while the sentinel re-extended beneath
them.

useRowWindow now takes the query itself and compares that by reference,
so the hook does what its own doc comment already claimed.

Also withhold the query controls from a draft that has recorded nothing,
where a search box, sort and filter popover narrow an empty listing and
the popover in particular is an invitation to a dead end. The gate reads
the draft rather than the queried rows, so a query that matched nothing
keeps the controls that are the only way to widen it back.
A feature value is free text, so it may be the empty string. The filter
offered such a choice under its own spelling, which the platform control
can neither carry as a value nor show as a label: the choice appeared as
a blank, unclickable row.

Give it a sentinel of its own, alongside the one the absent choice
already uses, and a localized label to be read under. Route the three
places that spelled a choice for the control through one function, so
they cannot disagree about the two choices that need spelling.

Latent for now, no write path recording feature values yet.
A choice can stop being offered while a reader is filtered to it, which
left the control unmounted and the narrowed list with no way back. Also
tell a value spelled like the untagged or empty label apart from it, two
choices sharing a label leaving one unselectable.
The platform combo box resolves a picked option to the first entry whose
label matches, so two choices sharing a label leave the later
unselectable.
Marking a value that read as a placeholder label was a single pass, so
it
collided one level up: a field holding both "(none)" and "(none)
(recorded
value)" gave both the same label, and the recorded value could not be
filtered by.

Build the labels in one pass instead, claiming each as it is taken. The
absent-value choices claim the placeholder labels first, so ordering
cannot
let a value take one from under them, and a value is marked until its
label
is unclaimed rather than once.
* Offer every filter choice under the name the control reports back

The platform combo box resolves a picked option by matching the label the
command list reports back, and that label is trimmed. A choice offered
under a name carrying surrounding whitespace therefore matches no entry,
and the click is dropped with nothing on screen saying why. A part of
speech and a feature value are both free text arriving from whatever
system recorded them, so a padded one is not hypothetical.

Offer each choice under the trimmed spelling instead, which is also what
the collision marking has to compare, or two values differing only in
their padding would take the same name unmarked. A value that is nothing
but whitespace has no name left once trimmed and so borrows the empty
value's, the marking telling those two apart.

The stub trimmed nothing, which is why a padded value looked selectable
under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop the collision marking spinning on a marking that moves nothing

Repeating the marking clears a chain of collisions only while each round
lengthens the name. The marking is a localized template, and one that
drops `{value}` spells back whatever name it was handed, so the loop had
no way out: two colliding choices under such a translation hung the
render, and with it the whole WebView.

Bound the rounds by the number of names already claimed, that many having
produced more distinct spellings than there are names to collide with. A
marking that does move the name still clears every collision within the
bound, so nothing changes for a translation that carries `{value}`; one
that does not leaves two choices sharing a name, and only the later of
them unselectable.

The stub keys its options by value rather than by label, so a caller
offering two choices under one name is left to the assertions rather than
buried under React's complaint about the duplicate key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Name the language the missing-gloss filter asks about

The filter read "Missing gloss in en", the analysis language reaching its
label as the BCP 47 tag the project records it under. A reader who never
chose that tag themselves has no reason to recognize it, and the question
the filter asks is about a language rather than about a code.

Resolve the tag to the language's name in the interface's own language,
keeping the tag for one no host has a name for and for one that cannot be
parsed at all — tags reach the panel as free text, and naming an
unparsable one throws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Return the catalog list to its top when its query changes

The window already started over on a new query, but the scroll container
is the same element throughout and kept the offset it was left at, merely
clamped to the shorter content. A reader who narrowed a deeply scrolled
list therefore landed part way down a listing they had not seen the start
of, the sentinel then extending the window from under them.

Put the scroll back with the count, before paint so the list is never
shown at the old offset first. Keyed on the query the count was reached
against rather than on the rows, so an edit to the analysis beside an open
catalog still leaves a scrolled list where it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stand the row window's observer down once it covers every row

The observer re-subscribes on every count change, so that a sentinel left
inside the arming margin after an extend is reported again rather than
waited on. Once the window holds every row there is nothing left to
extend by, and each further delivery could only put the count back
through unchanged — leaving termination resting on React bailing out on
that, which an edit letting the count grow past the row count would
quietly turn into a loop.

Skip subscribing at all in that state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop the resize factor the direction gate already fixes at one

The gate returns for every key outside a right-to-left interface, so past
it the handle always widens toward the screen's right and the direction
factor is always one. Multiplying by it said nothing the gate had not
already settled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Name the analysis language in the interface's own language

The name was resolved against the host's locale, which the platform's
interface language does not follow: nothing aligns the runtime locale to
the `platform.interfaceLanguage` setting. A reader on an English host with
the interface set to Spanish read "Falta glosa en French" — the template
in one language and the name it carries in another.

Resolve it against the interface languages the panel's own localized
strings were resolved for. An interface locale that cannot be parsed
falls back to the host's, which costs the name its language rather than
costing it the name; that stays separate from an unparsable analysis tag,
which still reads as itself.

The popover takes the name rather than the tag now, so the one place that
reads the setting is the one that already talks to the platform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop interface locales Intl rejects one at a time

`Intl` rejects a whole locale list for any one entry it cannot parse, so
an unusable locale ahead of a usable one cost the naming both and sent it
to the host's locale. The platform resolves a localized string the other
way, walking past the locales it has nothing for, so the two disagreed: a
label resolved in the reader's second interface language beside a name
read in the host's.

Filter the list per entry instead, falling back to the host's locale only
where nothing usable is left.

Reachable rather than hypothetical: interface locales are named by the
localization files carrying them, which nothing holds to BCP 47
structure, and the settings service validates a written value but not a
read one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Trim the marking that tells one filter choice from another

A marking is free to pad what it wraps, and the control resolves a
choice
by the label it reports back trimmed — so a padded name left a marked
choice unselectable, the same way an untrimmed recorded value did.

* Keep the query controls' comments to purpose over mechanism

The scroll reset, collision bound, and locale filter each explained how
they work where the rules ask what they are for, and repeated across a
doc comment and the code below it.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Alex Rawlings <alex.rawlings@wycliffe.ca>
The per-book usage count each row is ranked and labeled by is taken
against the book on screen, so moving to another book is a new listing
rather than more of the old 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.

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

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: search, sort, filter, and row windowing

2 participants