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/FIX_PLAN.md

12 KiB

UIX Audit — Fix Execution Plan (ready to run)

plan-version: 1 prepared: 2026-06-26 source: this audit (124/124 components, 0 CRITICAL · 18 HIGH · 129 MEDIUM · 60 LOW) branch: active-uix status: NOT STARTED — read-only audit done; this is the authorized fix track.

How to use this plan

Execute phase by phase, top to bottom (ordered by leverage). After EACH phase:

  1. npm run check → 0 new errors.
  2. Run the phase's vitest scope (named per phase).
  3. For HIGH UI/behavior fixes, a browser check (the audit was read-only; some HIGHs — e.g. collapsible sequence-lag — need a frame-by-frame measurement to confirm the fix, like the original Checkbox fix).
  4. Update the finding's fix-status: in audit/components/{kebab}.md (open → fixed), then re-run MODE=reeval later to confirm.

Hard exclusions (every phase): never touch soma/components/words/**, eidos/components/palabras/**, or chronos — active dev tracks, excluded from the audit. When committing, stage explicit paths.

Severity legend: 🔴 HIGH · 🟠 MEDIUM · ⚪ LOW. Each item links its finding id.


Phase 1 — SYS-7: the A31 O(N²) selection cluster ⭐ HIGHEST LEVERAGE

6 HIGH + 3 MEDIUM, one mechanical pattern, ~2 lines per component. This is the single best ROI in the audit.

Pattern: a per-item $derived calls provider.isSelected(v)/isExpanded(v)/isItemPressed(v)/isItemChecked(v) which does array.includes(v) (O(N)). With N items re-deriving on every mutation → O(N²) ("hangs at 30+").

Fix (apply to each provider):

  • Add a provider-level readonly selectedSet = $derived(new SvelteSet(this.opts.value.current)) (import SvelteSet from svelte/reactivity). Trees also add expandedSet.
  • Rewrite the provider method to return this.selectedSet.has(value) (O(1)). The per-item $derived is unchanged.
  • The providers already prove the shape — rovingTargetEl is lifted exactly this way (with a comment citing "avoids an O(N²) cascade"). Mirror it.
🔴/🟠 Component provider method id
🔴 listbox isSelected @ listbox-provider:157-158 (item @ :413) listbox-001
🔴 grid-list isSelected :493 + per-checkbox isChecked :151 grid-list-001/002
🔴 tree-view isSelected/isExpanded :96-98/:145-147 (items :351/:353/:569) tree-view-002
🔴 tree-grid isSelected/isExpanded :148/:189 (rows :578-579) tree-grid-001
🔴 tag-group isSelected :115-117 (item :321 + link :410) tag-group-001
🟠 toggle-group isItemPressed :72 (item :146) toggle-group-001
🟠 checkbox CheckboxGroupProvider.isItemChecked :313-315 checkbox-001
🟠 select isSelected :158 (single-select, low-risk but harmless) select-007
🟠 combobox isSelected :182 (single-select, low-risk) combobox-004

Optional extraction (EX-3): a 3-line shared liftedSet(() => string[]) helper, then all 9 consume it. Decide inline-vs-helper at execution (≥2 consumers justify the helper). Verify: npx vitest run src/uix/soma/components/{listbox,grid-list,tree-view,tree-grid,tag-group,toggle-group,checkbox,select,combobox} + a 50-item browser smoke on listbox/tree.


Phase 2 — the isolated HIGH findings (12)

Each is well-grounded and self-contained. Order within the phase is by risk.

  1. 🔴 dialog-001 — Content double-writes role/aria-roledescription (a11y race for alertdialog). dialog-provider:391-397 registers Content syncAttrs:true, then :460-463 sets role:variant + aria-roledescription:undefined. Fix: make role morfo-expressible (bind a propRef('variant') / stateRef so the morfo owns the variant role) OR drop syncAttrs for Content and supply role/aria via renderProps() + a single override. Don't mix syncAttrs + manual same-attr writes. Verify alertdialog role is stable across ticks.
  2. 🔴 dialog-002 — dead/conflicting [data-dialog-trigger] CSS envelope after the trigger migrated to a composed <Button>. Fix: delete the [data-dialog-trigger] envelope (dialog.css:22-54) + its --dialog-trigger-* tokens (base.ts:727); Button owns the chrome (the Close recipe is the model).
  3. 🔴 combobox-001 — virtual+real focus mix (A17). Morfo declares aria-activedescendant; provider calls dom.focus(items[0]) (:392/408/684). Fix: commit to virtual focus — keep DOM focus on the input, drive selection via highlightedId/data-highlighted + scrollIntoView, never dom.focus(item). (Higher effort; needs a client/Playwright test asserting activeElement stays on the input.)
  4. 🔴 select-001 — scrollSelectedIntoView (:244-246) interpolates the user value without CSS.escape. Fix: reuse the safe resolveListItemEl helper (it already CSS.escapes), or wrap in CSS.escape.
  5. 🔴 select-002 — morfo focus.trap:true/initial:'first-focusable' but provider implements virtual focus (no FocusScope). Fix: change the morfo focus to reflect virtual focus (trap:false, keep return:'trigger'); cross-check combobox.
  6. 🔴 select-003 — two keyboard routes duplicate index math; the content route uses real-focus (activeElement) in a virtual-focus component + the loop off-by (n-2). Fix: extract the pure nextIndex helper (EX-1), delete/repair the content route. (Pairs with combobox-001.)
  7. 🔴 alert-dialog-001 — export const alertDialogMorfo: Morfo = { uses : Morfo. Fix (1 line): export const alertDialogMorfo = { … } as const satisfies Morfo; (alert-dialog.ts:4 + the closer).
  8. 🔴 navigation-menu-006 — undisposed $effect.root (:523-527) leaks per trigger. Fix: replace with a bare $effect in the constructor (auto-disposed), matching the root provider's existing pattern.
  9. 🔴 collapsible-NEW-001 — collapse is sequence:'pre' + sets state in the handler → ~240ms lag (the Checkbox-244ms mechanism). Fix: change collapse to sequence:'post' (collapsible.ts:48) OR set state at the call-site in toggle() before runtime.trigger. Confirm with a frame-by-frame browser measurement before+after (the original Checkbox lag was measured). If a visible-during-exit cue is wanted, drive it from CSS keyed on data-state='closed'.
  10. 🔴 breadcrumb-001 — Item <li> declares archetype:'item' (pulls cursor:pointer/hover). Fix: remove archetype:'item' from the Item part (breadcrumb.ts:50) — Timeline shows the correct pattern (a display <li> omits it). Verify the breadcrumb li no longer shows pointer/hover.

Verify: per-component vitest + a browser pass on dialog (alertdialog role), combobox/select (focus), collapsible (collapse latency), breadcrumb (cursor).


Phase 3 — SYS-1 scope-drift (one decision, then mechanical) 🟠 ~33 components

First decide the semantics (this is a project call, not a per-component fix): does morfo scope enumerate authored layers or all consuming layers? Two options:

  • (a) Add 'eidos' to every morfo whose component has an eidos/components/{c}/ dir + recipe (~33 one-line edits).
  • (b) Document that eidos is implicit and scope lists only soma/sema (then SYS-1 is closed by doc, 0 code edits).

Recommend (a) for explicitness (dropdown-menu/accordion/tabs/checkbox/radio-group/splitter already do it). The list of drifted morfos is in THEMING_COHERENCE.md / each component report's scopeDrift: true. Verify: npm run morfo:check + npm run check.


Phase 4 — THEME-SYS-1: overlay z-index ladder 🟠 ~10 recipes, one sweep

A canonical STATIC_Z_INDEX scale EXISTS (lib/primitives/static.ts:291: base/raised/sticky/dropdown/popover/ tooltip/modal/toast) and is consumed by the depth planes + menu-dial — but ~10 overlay recipes hardcode an ad-hoc 60–99/1200 scale. Fix (precise mapping, base.ts):

  • dropdown-menu/context-menu content-z → var(--z-index-dropdown)
  • select/combobox/popover/link-preview content → var(--z-index-popover)
  • tooltip → var(--z-index-tooltip)
  • dialog/drawer overlay → var(--z-index-modal) (content = +1 or a new --z-index-modal-content)
  • toast toaster-z → var(--z-index-toast)

Verify the rendered stacking order is preserved (60→1200 today, must stay ordered after mapping to 0..900). Verify: browser — overlay stacking (popover under dialog under toast).


Phase 5 — the smaller systemic patterns 🟠

  • SYS-A30-EFFECT (date/time/color-field): replace the $effect-wrapped inputId registration with a direct constructor guard, matching NumberField (number-field-provider:585-587). 3 components.
  • SYS-A33-CLONE (listbox, grid-list, tree-grid, tag-group): migrate the new Map(this.x) clone-and-reassign registries to SvelteMap/SvelteSet (pairs with Phase 1 — same providers). 4 sites.
  • SYS-5 double-write: drawer-007 (Content manual role/aria-modal while morfo declares them), popover-001 (Close re-declares type/aria-label). Drop the manual re-sets; let syncAttrs/renderProps own them.
  • SYS-2 magic-z (non-overlay) + THEME-SYS-2 magic-opacity: dropdown-menu/context-menu item-disabled-opacity 0.55 → var(--opacity-disabled); calendar day-outside-opacity 0.62 → snap to --opacity-* (or one named --opacity-scrim for the recurring 0.62/62%). select item-description-font-size '0.85em' → tokenize.
  • SYS-INERT (5 pickers): either fire open/commit-reset events (perceptual signal) or drop them from the morfo. Decision per picker.

Phase 6 — MEDIUM/LOW hygiene + test gaps 🟠⚪

  • SYS-3 test gaps (pervasive): add the cheap, high-value tests first — the A24 readonly-without-value logger.warn assertions in the segmented fields (the harness already stubs warn), and provider keyboard-route tests for the overlays. Add client/Playwright tests for the focus-bearing overlays (select/combobox/dialog).
  • Calendar Home/End (calendar-006 / range-calendar-002): decide APG-week vs month, then align the morfo action label and the provider impl in BOTH calendar + range-calendar.
  • The remaining LOW magic-literals (em-padding on code/kbd/mark, scroll-frames, radio-cards easing, etc.) — batch them when convenient; none are blocking.
  • ⚠️ calendar / range-calendar are MID-REFACTOR (uncommitted view-switch work) — coordinate with that track before touching their eidos; their findings reflect in-flight state.

Parallel track — VALIDATOR EXTENSIONS (prevent the class) — see VALIDATOR_GAPS.md

Highest leverage of all: fixing the validator stops the whole class recurring. Do the cheap ones first.

VG What it catches Effort
VG-8 ⭐ : Morfo instead of as const satisfies Morfo (a grep) — would've caught alert-dialog-001 trivial
VG-3 syncAttrs:true + props re-setting a morfo attr (dialog-001/popover-001 class) med-high
VG-4 aria-activedescendant morfo + dom.focus(item) (combobox-001 class) medium
VG-12 sequence:'pre' event whose handler sets bound state (collapsible-NEW-001 class) medium
VG-10 morfo event never reached by runtime.trigger (SYS-INERT) medium
VG-1/2/6/9/11 em/% font-size · magic z/opacity/tracking · translationRef-key · $effect.root/$effect-id low-med

Suggested day-1 order (if time-boxed)

  1. Phase 1 (SYS-7) — biggest correctness win, mechanical. (½ day)
  2. alert-dialog-001 + VG-8 — 1-line fix + the grep that prevents it. (15 min)
  3. breadcrumb-001, navigation-menu-006, dialog-002, select-001 — small, self-contained HIGHs. (½ day)
  4. dialog-001, collapsible-NEW-001 — need a browser measurement; do with the dev server up.
  5. combobox-001 + select-002/003 — the focus-strategy refactor (highest effort HIGH); schedule its own block.
  6. Phases 3–6 + remaining VGs as follow-up.

Powered by TurnKey Linux.