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/components/menu-dial.md

91 lines
13 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.

# Audit: menu-dial
audit-version: 1
audited-at: 2026-06-26
scope: ['soma','sema','eidos'] (SCOPE-DRIFT → SYS-1)
method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead.
provider: G:/dev/svelte/vicen/src/uix/soma/components/menu-dial
## Summary
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 3.
systemic hits: SYS-1 (scope-drift: sema declared but not implemented); SYS-3 (keyboard/focus untested at component level, only nav logic tested); SYS-2 (magic z-index fallbacks, bare duration literals).
## Findings
### MEDIUM: THEMING — menu-dial-002 <!-- id: menu-dial-002 -->
- dimension: A
- rule: THEMING
- location: src/uix/morfo/components/menu-dial.ts:19
- evidence: Morfo declares scope: ['soma','sema','eidos'] but no recipe file found in src/uix/eidos/lib/recipes/base.ts or recipe subdirectory for menu-dial
- impact: If the morfo declares eidos in scope, the component requires either a recipe in base.ts or a recipe directory (src/uix/eidos/lib/recipes/menu-dial/), but neither exists. CSS is present but not declared in the recipe system.
- proposed-fix: Verify intent: if eidos styling is complete via CSS alone, document this exception. If tokens should be in base.ts, add a menu-dial recipe entry.
- verify: [confirmed] Confirmed via grep: `grep menu-dial base.ts` returns NOTHING — there is no `'menu-dial':` recipe key in src/uix/eidos/lib/recipes/base.ts. The morfo declares `scope: ['soma','sema','eidos']` (line 19). Crucially the TWIN component it repeatedly mirrors, `onion-menu`, DOES have a recipe block (base.ts:4368) whose header comment reads 'this owns the magic numbers that USED to be literals in onion-menu.css.' Menu-dial's CSS still carries those exact literals un-tokenized (28ms stagger, 2px glyph thickness — base.ts:4374 names onion's as `glyph-bar-thickness: '2px'`). So this is a genuine THEMING/recipe gap, not just a documentation note. Downgraded HIGH→MEDIUM: it's a token-coverage inconsistency vs the sibling, not a user-visible break. Confirmed at MEDIUM.
- fix-status: open
### MEDIUM: SYS-3 (keyboard route test coverage) — menu-dial-005 <!-- id: menu-dial-005 -->
- dimension: F
- rule: SYS-3 (keyboard route test coverage)
- location: src/uix/soma/components/menu-dial/menu-dial-nav.test.ts
- evidence: Only menu-dial-nav.test.ts exists (tests pure navigation logic). No provider test (no menu-dial-provider.svelte.test.ts or equivalent in eidos). The nav tests verify wrapping, disabled-skipping, Home/End but do NOT test keyboard routing through the soma/eidos integration: Enter/Space to open/close, Arrow routing through list while open, focus sync, dismiss on Escape.
- impact: Keyboard behavior (the primary interaction mode per APG) is untested at the component level. The nav helpers are tested in isolation but their integration with focus, dismissal, and the open/close state is not exercised.
- proposed-fix: Add a @vitest-environment=jsdom or Playwright client test file (e.g., menu-dial-provider.svelte.test.ts) covering: trigger keydown (Enter/Arrow to open), list keydown (Arrow/Home/End navigation, Escape to close), focus sync after open, Tab-out dismiss. Mark high-risk paths.
- verify: [confirmed] Confirmed: `ls src/uix/soma/components/menu-dial/` shows only `menu-dial-nav.test.ts` (verified, reads it) which tests the PURE `dialNav*` index math (count 4 wrap, disabled-skip [1,2], Home/End, degenerate empty/all-disabled) in isolation — no DOM, no @vitest-environment. There is NO provider/integration test anywhere (grep'd both soma and eidos menu-dial dirs). The integration surface is entirely untested: trigger keydown→open (runtime.keydown('trigger')), list Arrow/Home/End routing through focus (`focusActionAt` + actionEls() querySelectorAll), Escape→close, focusout/outside-pointerdown dismiss ($effect lines 254-291), focus-return-to-trigger (line 176), the pendingFocus $effect (270-279). This is a textbook SYS-3: interaction-heavy component, the easy pure-math path tested, every keyboard/focus/dismissal integration path untested. Confirmed MEDIUM.
- fix-status: open
### MEDIUM: THEMING (recipe ownership of magic numbers) / 2-of-3 — menu-dial-101 <!-- id: menu-dial-101 -->
- dimension: A, E-bis
- rule: THEMING (recipe ownership of magic numbers) / 2-of-3
- location: src/uix/eidos/components/menu-dial/menu-dial.css:25,291-292 vs src/uix/eidos/lib/recipes/base.ts:4368-4377
- evidence: menu-dial.css line 25 `--_menu-dial-stagger-step: 28ms;` and line 291-292 `block-size: 2px; margin-block-start: -1px;` (the glyph bar) are un-tokenized literals. The TWIN component onion-menu — which menu-dial's own comments say it 'mirrors' — moved exactly these into a base.ts recipe: base.ts:4374 `'glyph-bar-thickness': '2px'`, and onion's header (4364-4366) explicitly says the recipe 'owns the magic numbers that USED to be literals in onion-menu.css.' menu-dial has NO base.ts recipe block at all (grep 'menu-dial' base.ts → empty).
- impact: menu-dial regressed on the precise pattern the framework already established for its sibling: the speed-dial's tunable timing (28ms stagger) and glyph thickness (2px) are frozen in CSS with no recipe knob, so a theme cannot retune them and they can silently drift from onion-menu's tokenized equivalents. This is the concrete, code-grounded core of candidate findings 002+006.
- repro: grep -n "menu-dial" src/uix/eidos/lib/recipes/base.ts # → no matches; compare base.ts:4368 onion-menu block
- proposed-fix: Add a `'menu-dial':` block to base.ts recipes mirroring onion-menu: e.g. `'stagger-step': '28ms'`, `'glyph-bar-thickness': '2px'` (referencing the same source onion uses), and consume `var(--menu-dial-glyph-bar-thickness)` / `var(--menu-dial-stagger-step)` in the CSS.
- verify: [verifier-added] added by adversarial verify pass
- fix-status: open
### LOW: A22 (aria-controls, aria-labelledby) — menu-dial-003 <!-- id: menu-dial-003 -->
- dimension: A
- rule: A22 (aria-controls, aria-labelledby)
- location: src/uix/eidos/components/menu-dial/menu-dial.svelte:314-339
- evidence: Morfo declares aria-controls on trigger (line 84-88 of morfo: 'aria-controls: value: v.partRef("list")', recommended severity) and aria-labelledby on list (line 112-116: 'aria-labelledby: value: v.partRef("trigger")'), but neither is set in the Svelte implementation. Trigger has no aria-controls attribute; list has no aria-labelledby attribute.
- impact: Accessibility regression: screen readers cannot programmatically associate the trigger button with the menu it controls, violating ARIA authoring practices. The list is labeled aria-label=ariaLabel (same as trigger), creating ambiguity.
- proposed-fix: Generate unique IDs for trigger/list on mount. Add aria-controls=listId to Fab trigger and aria-labelledby=triggerId to list div. Both attributes marked as 'recommended', so failing to set them should be flagged but component is still functional.
- verify: [downgraded] Confirmed factually: morfo lines 83-88 declare trigger `aria-controls: v.partRef('list')` and lines 110-116 declare list `aria-labelledby: v.partRef('trigger')`. grep for `aria-controls|aria-labelledby|id=|useId` in the menu-dial eidos dir returns ZERO hits — neither attr is emitted, and no IDs are generated. menu-dial.svelte sets only `aria-haspopup='menu'` + `aria-expanded` on the Fab (lines 321-322) and `aria-label={ariaLabel}` on both trigger and list (lines 320, 335). HOWEVER both morfo declarations carry `severity: 'recommended'` (lines 87, 115) — NOT 'required'. The component is functional and named: trigger has aria-label + aria-haspopup + aria-expanded; list has aria-label + role='menu'. The sibling onion-menu also omits aria-controls (grep confirmed), so this is a family-wide pattern, not a unique regression. A missing 'recommended' attr on an otherwise-named control is a LOW/nice-to-have a11y nit, not a HIGH a11y break. Downgraded HIGH→LOW.
- fix-status: open
### LOW: TWO-MOMENTS (sequence pre vs post) — menu-dial-004 <!-- id: menu-dial-004 -->
- dimension: B
- rule: TWO-MOMENTS (sequence pre vs post)
- location: src/uix/eidos/components/menu-dial/menu-dial.svelte:169-177
- evidence: closeMenu() sets open=false (line 171) BEFORE calling runtime.trigger('close', ...) (line 175). Morfo declares close event has sequence='pre', meaning it should fire BEFORE state changes. The reactive data-state binding (line 334: data-state={open ? 'open' : 'closed'}) will update synchronously when open changes, moving the list to data-state='closed' before the event is emitted.
- impact: For 'pre' sequence events, the content (list with data-state) should still be marked as open when the event fires (for announcements, sounds), but the binding changes the state first. Same issue in openMenu (line 164).
- proposed-fix: Reorder: trigger the event BEFORE updating open state, or untrack the state update so the event fires first. e.g., `runtime.trigger('close', ...); open = false;` or use $effect to emit after measuring.
- verify: [downgraded] Factually correct: closeMenu (eidos line 169-177) sets `open = false` (line 171) BEFORE `runtime.trigger('close', ...)` (line 175); same for openMenu (line 164 before 166). The template binds `data-state={open ? 'open' : 'closed'}` reactively (line 334). BUT the impact is overstated for two reasons I verified: (1) runtime.svelte.ts:741-753 shows `sequence` ONLY orders `runEmit` vs an optional `handler` — menu-dial passes NO handler to trigger(), so 'pre' vs 'post' is functionally moot here; the morfo `commits` is also inert because eidos does not register the list via `runtime.part(...)`, it drives data-state itself via the Svelte binding. (2) The emit target is `listEl` (fallbackTarget, line 175), the list ELEMENT which persists in the DOM regardless of `data-state` — so the sema `data-event-*` stamp + hold are NOT lost; only the visual `data-state='closed'` flips one tick early. The perceptual signal still fires on a live element. This is a mild ordering smell, not a dropped-signal bug. Downgraded MEDIUM→LOW.
- fix-status: open
### LOW: THEMING (bare numbers in transition/animation) — menu-dial-006 <!-- id: menu-dial-006 -->
- dimension: E-bis
- rule: THEMING (bare numbers in transition/animation)
- location: src/uix/eidos/components/menu-dial/menu-dial.css:25,136,312
- evidence: Line 25: --_menu-dial-stagger-step: 28ms (bare millisecond value); line 136 & 312: fallback 150ms (bare ms in var() fallback); line 283 transition uses var(--duration-normal, 300ms) — fallback hardcoded.
- impact: Bare duration literals (28ms, 150ms, 300ms) are not canonical tokens. Should reference --duration-* scale (e.g., --duration-fast, --duration-normal). Fallbacks in var() are fragile.
- proposed-fix: Define --_menu-dial-stagger-step as a component-scoped alias of a canonical duration token (or expose it in recipes). Replace magic durations: 28ms should be a named token if used for stagger timing; 150ms/300ms should use canonical tokens (--duration-fast, --duration-normal, --duration-slow).
- verify: [downgraded] Split verdict on the cited literals. The `var(--duration-fast, 150ms)` (line 136) and `var(--duration-normal, 300ms)` (line 283) are NOT drift — they reference the canonical tokens (verified `--duration-fast`/`--ease-default`/`--ease-out` are defined in generated/base.css); the ms fallback is dead/redundant noise, and the IDENTICAL pattern appears in sibling onion-menu.css (lines 43, 182) — so flagging it as a defect here while the family uses it is inconsistent. The ONE genuine un-tokenized literal is `--_menu-dial-stagger-step: 28ms` (line 25): unlike onion-menu's `glyph-bar-thickness` it has no recipe token backing it, and it's a real animation-timing magic number that the recipe (absent, see 002) should own. So the finding has a kernel of truth (28ms) wrapped in two false flags (150ms/300ms fallbacks are token-backed). Downgraded MEDIUM→LOW and folded into 002's recipe gap.
- fix-status: open
## No-findings dimensions
C, D, G
## Theming facts (E-bis)
- magic z-index: src/uix/eidos/components/menu-dial/menu-dial.css:67: var(--z-index-sticky, 1100) | src/uix/eidos/components/menu-dial/menu-dial.css:310: calc(var(--z-index-sticky, 1100) - 1)
- magic literals: src/uix/eidos/components/menu-dial/menu-dial.css:25: 28ms (stagger-step) | src/uix/eidos/components/menu-dial/menu-dial.css:136: 150ms (visibility fallback) | src/uix/eidos/components/menu-dial/menu-dial.css:312: 150ms (animation fallback) | src/uix/eidos/components/menu-dial/menu-dial.css:283: 300ms (duration-normal fallback)
- undeclared parts: none
- roles clean: true · variants clean: true
## Tests (F)
- exists: true · env: node (pure nav logic, no vitest-environment annotation)
- covers: dialNavNext wrapping; dialNavPrev wrapping; dialNavFirst/Last; disabled skipping
- untested: keyboard route integration (Enter/Arrow/Escape on trigger/list); focus sync and focus return; open/close perceptual emit timing; dismiss on Tab-out; commit-select lifecycle; aria-label / aria-controls resolution
## Style observations (non-blocking)
- The CSS is well-structured with clear placement zones (9 fixed positions) and arc math (cos/sin layout). The stagger via CSS custom property --_menu-dial-i is elegant. Hover close delay (260ms) is reasonable for crossing the gap. Glyph rotation from + to × is smooth. No redundancy vs other components observed; this design is specific to radial fan layout.
- The arc geometry calculation in CSS (radius := max(r-clear, r-spread)) is mathematically sound and self-contained.

Powered by TurnKey Linux.