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

14 KiB

Audit: menubar

audit-version: 1 audited-at: 2026-06-26 scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1) method: adversarially-verified workflow (analyze → refute); HIGH/CRITICAL personally re-verified against cited code by the lead. provider: src/uix/soma/components/menubar/menubar-provider.svelte.ts

Summary

Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 4. systemic hits: SYS-1 (scope-drift: morfo omits 'eidos' but eidos recipe exists); SYS-2 (magic z-index: 1 without token); SYS-3 (jsdom-only tests on interaction-heavy component); SYS-4 (duplicated keyboard index math in two methods).

Findings

MEDIUM: SYS-1 scope-drift — menubar-001

  • dimension: A (Contract)
  • rule: SYS-1 scope-drift
  • location: src/uix/morfo/components/menubar.ts:7
  • evidence: scope: ['soma', 'sema'], but a full eidos recipe directory exists at src/uix/eidos/components/menubar/ with 8 files (menubar.svelte, menubar.css, menubar-trigger.svelte, menubar-content.svelte, menubar-context.ts, menubar-menu.svelte, types.ts, index.ts)
  • impact: Morfo declares scope omits 'eidos' while an eidos recipe dir exists, violating the scope-drift rule SYS-1. The eidos layer exists and provides the full visual contract (menubar.css, size context, data-stagger, data-size on provider).
  • repro: Check src/uix/morfo/components/menubar.ts line 7 against presence of src/uix/eidos/components/menubar/ directory.
  • proposed-fix: Add 'eidos' to the scope array: scope: ['soma', 'sema', 'eidos']
  • verify: [confirmed] morfo line 7 reads scope: ['soma', 'sema'], yet a full eidos recipe dir exists at src/uix/eidos/components/menubar/ (menubar.css, menubar.svelte, menubar-content.svelte, menubar-context.ts, types.ts, index.ts). The morfo's OWN comment contradicts the scope: line 8-10 cites the sema pack, and the eidos wrapper stamps data-size + reuses the dropdown recipe. Sibling components with an eidos dir DO list 'eidos' (accordion.ts:7, dropdown-menu.ts:11 both ['soma','sema','eidos']). This is exactly SYS-1 scope-drift. HIGH is justified: the compiled scope is the contract the validators key on, and it's wrong.
  • fix-status: fixed (212624e0)

MEDIUM: SYS-4 keyboard route duplication + index math duplicated — menubar-004

  • dimension: B (Keyboard), G (Redundancy)
  • rule: SYS-4 keyboard route duplication + index math duplicated
  • location: src/uix/soma/components/menubar/menubar-provider.svelte.ts:131-146 and 283-296
  • evidence: navigateAdjacent() method (lines 135-143): if (idx === -1) { next = delta === 1 ? 0 : values.length - 1; } else if (loop) { next = (idx + delta + values.length) % values.length; } else { next = Math.max(0, Math.min(...)); } — IDENTICAL logic in onContentKeydown() (lines 285-291) for nextIdx calculation.
  • impact: The index arithmetic for navigating between triggers is duplicated across two code paths (navigateAdjacent called from trigger onkeydown and directly in onContentKeydown). This duplicated logic risks divergence if one path is later updated. Both methods calculate the next index identically, but SYS-4 flags this as a known pattern to extract.
  • repro: Compare lines 135-143 (navigateAdjacent index calc) with lines 285-291 (onContentKeydown index calc) — they are textually identical.
  • proposed-fix: Extract the index calculation into a private helper method like getNextIndex(currentValue, delta, values, loop) and reuse it in both navigateAdjacent and onContentKeydown.
  • verify: [confirmed] Confirmed as duplication, MEDIUM is right. Lines 137-143 (navigateAdjacent): if (idx === -1) { next = delta === 1 ? 0 : values.length - 1; } else if (this.opts.loop.current) { next = (idx + delta + values.length) % values.length; } else { next = Math.max(0, Math.min(values.length - 1, idx + delta)); } is textually identical to lines 285-291 in onContentKeydown (same three-branch math on nextIdx). This is the SYS-4 nav-dup pattern. IMPORTANT: the off-by-n-2 half of SYS-4 is NOT present here — the idx===-1 branch lands on values.length - 1 (the LAST item) for delta -1, which is correct, not n-2. So it's pure extract-worthy duplication with divergence risk, not a behavioral bug. MEDIUM confirmed.
  • fix-status: open

MEDIUM: Test environment: jsdom only, no client/Playwright coverage — menubar-007

  • dimension: F (Tests)
  • rule: Test environment: jsdom only, no client/Playwright coverage
  • location: src/uix/soma/components/menubar/menubar-provider.svelte.test.ts:1
  • evidence: // @vitest-environment jsdom — tests run in jsdom (headless) only. 268 lines of test code covering MenubarProvider, but no client (Playwright) variant to verify DOM interaction, focus management, floating positioning, or hover behavior in a real browser.
  • impact: SYS-3: jsdom-only tests + interaction-heavy component + keyboard/focus logic. While keyboard routes are tested (ArrowRight, ArrowLeft, Home, End, Enter, Space, ArrowDown), they are mocked event objects in a simplified environment. Focus sync, floating anchor positioning, and real keyboard interaction are untested.
  • repro: Test file environment is jsdom; test coverage focuses on state/logic, not DOM/focus/positioning. No client-side tests found.
  • proposed-fix: Create a parallel menubar-provider.test.ts with @vitest-environment=node or add menubar-provider.e2e.ts with Playwright to verify: (1) real focus shifts, (2) floating content positioning, (3) hover-follow switching, (4) Escape focus return, (5) RTL directional keys.
  • verify: [confirmed] Confirmed. menubar-provider.svelte.test.ts:1 is // @vitest-environment jsdom. Events are plain mocked objects (keyEvent/pointerEvent factories lines 84-98 return { key, target, preventDefault: vi.fn() }). Keyboard ROUTES are exercised (ArrowRight/Left, Home/End, ArrowDown open, content-nav switch), which is better than many SYS-3 cases, but real focus movement is only asserted via a vi.spyOn(dom,'focus') spy — actual DOM focus, roving-tabindex realization, floating positioning, and Escape focus-RETURN-to-trigger (the queueMicrotask path at provider:258-261) are NOT verified in a browser. Interaction-heavy floating component + jsdom-only + no client/Playwright sibling = SYS-3. MEDIUM appropriate.
  • fix-status: open

LOW: Undeclared ARIA attribute (aria-controls) — menubar-002

  • dimension: A (Contract)
  • rule: Undeclared ARIA attribute (aria-controls)
  • location: src/uix/soma/components/menubar/menubar-provider.svelte.ts:431
  • evidence: 'aria-controls': this.isOpen ? this.menu.menu.contentId.current || undefined : undefined, — emitted by soma but not declared in morfo's trigger part aria array
  • impact: The trigger part declares aria: [{ attr: 'type', ... }, { attr: 'aria-haspopup', ... }, { attr: 'aria-expanded', ... }] but aria-controls is emitted dynamically by soma without being in the contract. This breaks the two-of-3 rule (morfo ↔ soma/sema/eidos consistency).
  • repro: grep 'aria-controls' soma provider; verify it is not in morfo trigger aria array at lines 55-59.
  • proposed-fix: Add aria-controls to morfo trigger part: { attr: 'aria-controls', value: v.expr('open ? menuId : undefined') } or similar pattern to declare the dynamic aria-controls as a morfo-driven attribute.
  • verify: [downgraded] Confirmed factually: morfo trigger aria array (lines 55-59) declares only type, aria-haspopup, aria-expanded — no aria-controls. Provider emits it at line 431: 'aria-controls': this.isOpen ? this.menu.menu.contentId.current || undefined : undefined. The sibling dropdown-menu morfo DOES declare it (dropdown-menu.ts:90-95 with condition: { when: 'part-present', part: 'content' }), so the canonical fix is exactly the proposed one. BUT severity HIGH overstates it: the attribute is functionally correct and present at runtime, ARIA is not broken, and it is conditionally emitted only while open. This is a contract-completeness gap (the morfo should declare what soma emits), not a user-visible a11y break or a divergence risk — LOW/contract-noise tier, not HIGH.
  • fix-status: open

LOW: Magic literal: outline-offset: 1px — menubar-005

  • dimension: E-bis (Theming)
  • rule: Magic literal: outline-offset: 1px
  • location: src/uix/eidos/components/menubar/menubar.css:104
  • evidence: [data-menubar-trigger]:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: 1px; ... }
  • impact: A bare 1px literal is used for outline-offset instead of a canonical --outline-offset-* or --focus-offset-* token. This violates E-bis rule: spacing/offset values should use canonical token scale --space-* or a purpose-specific --focus-offset-*.
  • repro: grep 'outline-offset' menubar.css shows a bare pixel value without a var() reference.
  • proposed-fix: Replace outline-offset: 1px with a canonical token like outline-offset: var(--outline-offset-focus) (if defined in the design system) or define one.
  • verify: [downgraded] menubar.css:104 outline-offset: 1px; is a bare literal — factually correct. But this is a pervasive baseline idiom, not a menubar drift: badge.css:183 uses the identical outline-offset: 1px;, and dozens of components use bare outline-offset: 0/2px. There is no --outline-offset-*/--focus-offset-* token in the system for this (the proposed fix invents one). A 1px focus-ring gap is a fixed perceptual constant, not a position on the --space-* rhythm. Real but LOW cosmetic token-nit, not MEDIUM.
  • fix-status: open

LOW: Magic literal: z-index: 1 — menubar-006

  • dimension: E-bis (Theming)
  • rule: Magic literal: z-index: 1
  • location: src/uix/eidos/components/menubar/menubar.css:106
  • evidence: [data-menubar-trigger]:focus-visible { ... z-index: 1; } — a bare integer z-index instead of a named token
  • impact: SYS-2: bare z-index integer without a canonical --z-index-* token. The value 1 is arbitrary and not tied to the design system's z-index scale (e.g., --z-index-overlay, --z-index-dropdown).
  • repro: Line 106 of menubar.css has z-index: 1 without var().
  • proposed-fix: Define or reuse a canonical z-index token: z-index: var(--z-index-focus) or similar, ensuring it fits the system's layering strategy.
  • verify: [downgraded] menubar.css:106 z-index: 1; inside [data-menubar-trigger]:focus-visible (with position: relative) is a bare integer — factually correct. But this is the standard focus/in-flow stacking idiom (raise the focused sibling within a local stacking context so its outline isn't clipped), used by the POSITIVE REFERENCE tabs.css:281 (position: relative; z-index: 1; for triggers over the indicator) and ~19 other components (avatar, button-group, carousel, dialog, combobox, toggle-group, virtual-list...). SYS-2's cited examples are LAYERING-SCALE values (content-z/overlay-z) — e.g. dropdown-menu.css:34 z-index: var(--dropdown-menu-content-z) — NOT focus +1. A local +1 is not a position on the global layering scale and has no canonical --z-index-* token. Real literal but LOW, and flagging it MEDIUM/SYS-2 while the reference component does the identical thing is inconsistent.
  • fix-status: open

LOW: 2-of-3 rule: data-menubar-value consumed only by soma trigger, not by sema or eidos — menubar-008

  • dimension: A (Contract)
  • rule: 2-of-3 rule: data-menubar-value consumed only by soma trigger, not by sema or eidos
  • location: src/uix/morfo/components/menubar.ts:53 and src/uix/soma/components/menubar/menubar-provider.svelte.ts:435
  • evidence: morfo declares { attr: 'data-menubar-value' } in trigger part data array (no values list, consumed by soma). Eidos does not consume it (not in menubar-trigger.svelte), and no sema reference found.
  • impact: The 2-of-3 rule suggests each morfo field should be consumed by >=2 of soma/sema/eidos. data-menubar-value is only used by soma (MenubarTriggerProvider.props emits it). This is a low-priority observation because it may be intentional for internal soma wiring (store the menu value for internal reference), but it's worth confirming the rule intent.
  • repro: data-menubar-value is declared in morfo but appears only in soma provider output, not in eidos CSS selectors or sema logic.
  • proposed-fix: Verify if data-menubar-value is only a soma-internal marker. If it's consumer-facing, ensure eidos or another layer consumes it for styling or state tracking.
  • verify: [confirmed] Confirmed factually and the analyzer correctly rated it LOW. morfo declares { attr: 'data-menubar-value' } in trigger data (menubar.ts:53); provider emits it at menubar-provider.svelte.ts:435 'data-menubar-value': this.menu.opts.value.current. grep for data-menubar-value across src/uix/eidos and src/uix/sema returned EMPTY — neither eidos CSS nor the sema pack consumes it (the sema pack keys on data-menubar/data-menubar-trigger via semaSelector, not the value). So it is consumed by exactly 1 of 3 (soma only). It is a legitimate soma-internal sibling-location marker (cross-menu nav reads the registry, but the attr documents the value on the DOM), so the 2-of-3 carve-out for internal wiring applies. LOW contract-noise, as rated.
  • fix-status: open

No-findings dimensions

C (DOM-selector), D (Frontier), E (TSC)

Theming facts (E-bis)

  • magic z-index: z-index: 1 at menubar.css:106
  • magic literals: outline-offset: 1px at menubar.css:104
  • undeclared parts: data-size on provider (eidos emits, morfo not declares) | data-stagger on content (eidos emits, morfo not declares) | data-floating-gap on content (eidos emits, morfo not declares)
  • roles clean: true · variants clean: true

Tests (F)

  • exists: true · env: jsdom
  • covers: coordinates sibling menus through root value; navigates enabled triggers in DOM order + skips disabled; opens trigger from keyboard without moving focus; switches open menu from content horizontal navigation
  • untested: real DOM focus sync; floating anchor positioning; hover-follow behavior in real browser; Escape focus-return in real browser; RTL directional key behavior (ArrowRight/Left swap); submenu escape and cross-menu navigation in real content; typeahead (if implemented); client-side interaction

Style observations (non-blocking)

  • The eidos recipe is well-structured and reuses dropdown-menu parts effectively for per-menu content.
  • data-shape-nest concentric-radius pattern (menubar-trigger.svelte:18) is a thoughtful design that avoids hand-rolled calc.
  • Hover-follow and cross-menu arrow navigation logic is coherent and well-documented.
  • The size cascade via context (MenubarSizeContext) is clean and follows Svelte patterns.
  • MenubarMenuProvider acts as a clean adapter layer between menubar state and nested DropdownMenu, which is architecturally sound.

Powered by TurnKey Linux.