Desktop Material

Tab-strip overflow dropdown

When more repository tabs are open than the strip can show, the tabs that do not fit move into a "more tabs" dropdown instead of being clipped or reachable only by horizontal scrolling. Every open repository stays one click away, and the strip itself never scrolls sideways.

Behavior and configuration

All labels ship in English, playful Hong Kong Cantonese, and the compact bilingual mode through the shared translation catalog. The dropdown's description line is the one tonal string here, so it honours the per-language funny level: tabs.overflowDescription.plain (levels 1–2), .light (level 3), and .playful (levels 4–5), read per language from the persisted audio settings so English can stay plain while Cantonese is playful. Every band states the same facts — these tabs did not fit in the strip, and they can be searched, switched to, or restyled from here. Tab names, repository paths, and counts are never restyled or translated in any mode.

Persistence

The overflow split itself is derived entirely from measured geometry at render time and is never written to disk, and the search query is per-opening state that is not persisted either. The one persisted value this surface owns is the search's filter mode, under filter-mode/tab-overflow, so a user who works in regex does not have to re-select it every time the menu opens. Tab identity, order, grouping, pinning, and per-tab appearance continue to persist through their existing stores, unchanged.

Accessibility

The strip keeps its tablist semantics. The more-tabs button exposes an accessible name with the hidden-tab count, aria-haspopup="dialog", and aria-expanded. The search field is a combobox with aria-controls, aria-expanded, and aria-activedescendant pointing at the highlighted row. The dropdown is a labelled listbox of option rows with aria-selected tracking the highlight and aria-activedescendant on the list. Each row's appearance button carries the tab's exact name in a concise single-language accessible name, and an invalid regular expression is announced through a role="alert" message while the row count updates in the polite status line. Every control is keyboard reachable with a visible focus ring, and the button and rows meet the strip's contrast and hit-target norms.

Narrow widths, bilingual mode, and display scaling

The sheet is bounded (max-width: calc(100vw - 52px)) and never scrolls horizontally. Inside a row, the option is the flexible part (flex: 1 1 auto with min-width: 0) and the appearance button is fixed at 32×32, so a long tab name or a doubled bilingual label ellipsizes instead of pushing the button out of the sheet. The search field shrinks the same way. Below 520px wide or 560px tall — which is also where a 200% display scale lands a normal window in CSS pixels — the results list drops to 220px and the regex builder's text label is hidden, leaving its icon launcher and the mode cluster intact.

Failure modes and recovery

Security considerations

The dropdown renders localized static copy, tab labels, and repository paths that already appear in the strip and existing tab search. Search queries are evaluated in-process by the shared matcher and are never persisted or transmitted; only the chosen filter mode is stored. Per-tab styles reach inline CSS only through the same validated tabTitleStyleToCss / tabFrameStyleToCss helpers used by the strip, which drop any value that is not a known-safe hex color, curated font, or bounded numeric — so a persisted style can never inject arbitrary CSS.

Verification

tab-overflow-test.ts covers the pure split geometry: the empty strip, the all-fit case, the exact-fit rounding guard, trailing overflow, overflow-button width reservation, the active-tab guarantee sliding the window (and not sliding when unnecessary), order preservation across both partitions, the always-show-one and lone-oversized-tab guards, and variable-width tabs.

ui/tab-overflow-popover-test.tsx covers the search surface and the customization route: filtering under each shared mode, the fuzzy plain-text default with regex reached only by explicit opt-in, an invalid pattern raising the alert while keeping every row, keyboard navigation from both the field and the list (including Space being typed text in the field and an activation in the list), the per-row appearance button, the row context menu, bilingual copy with concise accessible names, and the per-language funny bands keeping their facts. Two of its cases drive the real RepositoryTabStrip with forced measurements so an overflow genuinely exists, then reach the appearance editor for a tab that has no element in the strip at all.

collection-surface-registry-test.ts enforces the one-to-one binding between the tab-overflow registry entry, the literal data-search-surface-id on the input, and the FilterModeControl that owns the regex builder. repository-tab-actions-style-test.ts pins the sheet and list bounds, the row/option/field flex contract that prevents clipping, the 32px appearance button, the focus rings, and the compact-viewport rules. Typechecking (npx tsc --noEmit) covers the component wiring.