You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/audit/SHARED_EXTRACTION.md

64 lines
4.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Shared extraction opportunities — rolling
updated-at: 2026-06-26
batches-complete: 1
Ranked by (components affected × LOC removed × divergence risk). "Should extract" vs "looks similar but
legitimately diverges" is called out per entry.
## EX-1 · Directional-nav index helper <!-- id: EX-001 -->
- verdict: **SHOULD EXTRACT** (pure math is identical; only the focus-application differs)
- components: select (content route), combobox (content route), dropdown-menu (content + sub-content). 4+ copies.
- pattern: `next/prev/Home/End` index math —
`loop ? (i±1+len)%len : Math.min/Math.max(i±1, …)` + the `currentIndex===-1` seed.
- shared bug it would kill: the `currentIndex===-1` + prev + loop branch lands `n-2` instead of last
(select-003, dropdown-menu-001).
- proposed: `nextIndex(curr: number, key: string, len: number, opts: {loop; dir; orientation}): number`
(pure, in `soma/keyboard` or a `list-nav.ts` next to `list-selection.ts`). Each caller keeps its own
focus/scroll application (real focus for menu/select-content, virtual highlight for select-virtual).
- est. LOC removed: ~20-35 per site (×4) − ~40 helper = net ~60-100 removed; + 1 unit-tested boundary suite.
- divergence note: select's *trigger* route is virtual-focus and its *content* route is real-focus — the INDEX
math is the same; extract only the index computation, not the focus side-effect.
- **B2 correction:** the `n-2` loop off-by is NOT universal. navigation-menu (and the verified menu loop math) use
modulo WITH a `currentIndex===-1` early-return guard, so they're correct. The off-by lives specifically in
Select's and DropdownMenu's content routes (no guard). The extraction still pays off (one canonical, tested
helper replacing ~6 copies, several already correct), but the *bug fix* is scoped to select + dropdown-menu.
## EX-2 · Dismissal trigger/anchor exclusion <!-- id: EX-002 -->
- verdict: **SHOULD EXTRACT** (every overlay re-implements the same bounce guard)
- components: select, combobox, popover, dropdown-menu (4/5) each hand-roll "ignore an interact-outside that
lands on the trigger (and input)" via `contains()` / `isValidEvent`, with near-identical comments about the
pointerdown-close-then-click-reopen bounce.
- proposed: a `Dismissal` option `excludeRefs: () => (HTMLElement|null)[]` (or `excludeWithin`) that the layer
applies before invoking the close action — removes the per-provider guard + the `isValidEvent` duplication.
- est. LOC removed: ~8-15 per site (×4) net positive; centralizes a subtle, repeatedly-rediscovered bug.
- divergence note: combobox excludes trigger+input, popover/select exclude trigger only — the option takes a
list, so it covers both.
## EX-3 · `selectedSet`/`expandedSet` lifted derivation (A31) — HIGHEST-VALUE EXTRACTION <!-- id: EX-003 -->
- verdict: **SHOULD EXTRACT — confirmed across 7 components, fixes 6 HIGH + 2 MEDIUM with one helper.**
- components (lead-verified, B1+B5): select-007, combobox-004 (MEDIUM) + listbox-001, grid-list-001/002,
tree-view-002, tree-grid-001, tag-group-001 (HIGH). Each: per-item `$derived` → `provider.isSelected(v)` /
`isExpanded(v)` → `opts.value.current.includes(v)` (O(N²)).
- proposed: a tiny shared helper `liftedSet(() => string[]): { has(v): boolean }` returning a provider-level
`$derived(new SvelteSet(arr))`; `isSelected`/`isExpanded` become `.has(value)` (O(1)). The providers already
prove the pattern works — `rovingTargetEl` is lifted exactly this way (with comments). This just extends it to
the selection/expansion path.
- est. LOC: ~2 lines per component (×7) + a 3-line helper; the value is **correctness** (removes the documented
"hangs at 30+" hazard from every large multi-select list/tree/grid), not LOC.
- divergence note: trees need `expandedSet` too (same shape, different array). Single-select select/combobox are
the low-risk variant but the same fix applies and is harmless.
## EX-4 · Reactive registry/cache: SvelteMap over clone-and-reassign (A33) <!-- id: EX-004 -->
- verdict: **SHOULD MIGRATE** (4 sites, fragile pattern)
- components: listbox, grid-list, tree-grid, tag-group each do `this.x = new Map(this.x)` clone-and-reassign for a
registry/cache. virtual-list is the positive model (SvelteMap). combobox already migrated.
- proposed: replace each with `SvelteMap`/`SvelteSet` — `.set`/`.delete` become O(1) + reactive without the clone,
and the "why clone?" foot-gun disappears. Pairs with EX-3 (same providers, same modernization sweep).
## Positive model (already extracted — the bar to match)
- **`list-selection.ts`** (`computeListSelection` + `resolveListItemEl`) is shared correctly between Select and
Combobox: pure state machine, `CSS.escape` in the resolver, no `this.handleClose()` baked in. EX-1/EX-2/EX-3
should follow this shape. (Note: Select's `scrollSelectedIntoView` bypasses `resolveListItemEl` and
reintroduces the unescaped query — select-001; fixing it = "use the helper you already have".)

Powered by TurnKey Linux.