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

184 lines
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.escape`s), 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.