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

9.6 KiB

Audit: context-menu

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

Summary

Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 1. systemic hits: SYS-2 (magic-z and magic-literals); SYS-3 (jsdom-only keyboard testing).

Findings

MEDIUM: SYS-2 — context-menu-002

  • dimension: E-bis
  • rule: SYS-2
  • location: src/uix/eidos/lib/recipes/base.ts:4303-4322
  • evidence: The context-menu recipe block (lines 4303-4322) does NOT declare 'content-z' token, unlike dropdown-menu which declares 'content-z': '80' at the equivalent location. Context-menu content floats with z-index: auto, risking paint-through by positioned page elements.
  • impact: Missing z-index token is a known systemic SYS-2 issue. Without explicit z-index, the portaled content panel may render behind elements with lower z-index that have explicit positioning (e.g. a toggle-group-item at z-index 1).
  • proposed-fix: Add 'content-z': '80' to the context-menu recipe block at base.ts:4304 (right after the opening brace), matching the dropdown-menu z-index for consistency since both are floating overlay menus.
  • verify: [confirmed] CONFIRMED real SYS-2 divergence. base.ts context-menu block (4303-4322) has NO 'content-z' token; the sibling dropdown-menu block declares 'content-z': '80' (4279) with a comment: 'Without this the portaled panel inherits z-index: auto and any positioned page element with a positive z-index (e.g. a selected [data-toggle-group-item], z-index 1) paints THROUGH it.' context-menu.css panel rule (22-34) has NO z-index declaration, whereas dropdown-menu.css (34) sets z-index: var(--dropdown-menu-content-z) and notes 'The soma floating layer reads this computed z-index and mirrors it onto the positioner wrapper' (31-32). context-menu is an identically-portaled cursor-anchored overlay, so the same paint-through risk applies and is unmitigated. MEDIUM is correct.
  • fix-status: open

MEDIUM: SYS-3 — context-menu-005

  • dimension: F
  • rule: SYS-3
  • location: src/uix/soma/components/context-menu/context-menu-provider.svelte.test.ts:1
  • evidence: Test file header: '@vitest-environment jsdom'. All keyboard tests (lines 191-281, 283-336) run in jsdom, which does not simulate real DOM focus or keyboard event routing like a browser environment.
  • impact: Keyboard routing (onkeydown handlers, arrow nav, Home/End, Escape) and focus-sync behavior (lines 334-371 in provider, lines 1021-1063 in SubContent) are tested in jsdom but should also be verified in a client environment (Playwright). Focus management edge cases (focus return on close, focus trap behavior) are HIGH-RISK for regressions.
  • proposed-fix: Add a client/Playwright test file covering: (1) focus enters at first item on open, (2) arrow keys navigate correctly with loop=true/false, (3) focus returns to trigger on Escape/outside-click close, (4) submenu focus scope is isolated (arrow-left exits submenu, arrow-right enters), (5) Home/End jump correctly when items are disabled.
  • verify: [confirmed] CONFIRMED SYS-3. Test header line 1 is // @vitest-environment jsdom. The test bodies (it() at 130/170/191/229/283/338) cover: open-from-contextmenu, getItems scoping, item activation via click + Space(216), checkbox/radio selection + Enter(275), submenu open via hover/click/ArrowRight(331), group/separator props. CRITICALLY UNTESTED: the Content onkeydown arrow-navigation index math (provider 334-371) — the loop modulo-vs-clamp logic at 348-355, Home/End at 356-361 — plus focus-return-to-trigger on close and Escape-close. The highest-risk keyboard route (directional index math) is exercised on zero paths, and runs jsdom-only with no client/Playwright companion. Matches the 'complex behavior tested only on its easy path (selection/open-close) while keyboard/focus untested' SYS-3 profile. MEDIUM correct.
  • fix-status: open

MEDIUM: 2-of-3 — context-menu-006

  • dimension: A
  • rule: 2-of-3
  • location: src/uix/morfo/components/context-menu.ts:73-77 (radio-group part)
  • evidence: RadioGroup part (kebab: 'radio-group', archetype: 'group') declares role='group' and data/aria as empty arrays (lines 174-176). No data attributes are bound. Only soma registers it at line 675; sema does not select it; eidos does not consume it.
  • impact: RadioGroup part violates 2-of-3: used by soma (runtime.part registration) but not by sema or eidos. It is a structural grouping container with no perceptual or visual binding. This is acceptable if it is purely structural, but it should be marked optional or have a docstring clarifying its role.
  • proposed-fix: RadioGroup is intentionally structural-only (holding radio items that self-manage checked state via the RadioItem logic). This is a design choice, not a bug. Document in morfo: /* Structural container; individual RadioItems manage state and selection. Eidos/sema do not target this part directly. */
  • verify: [confirmed] Confirmed as the candidate itself frames it (confidence:low, proposedFix concedes 'This is a design choice, not a bug'). Morfo RadioGroup (168-177): role='group', empty data:[]/aria:[]. Provider registers it (675) for context publication (ContextMenuRadioGroupProvider.ctx) so RadioItems can read the group value. Grep of context-menu.css finds NO data-context-menu-radio-group selector (eidos uses display: contents only implicitly — actually no rule targets it at all), and the sema pack does not select it. So it is soma-only on the 2-of-3 axis, but legitimately: it is a pure structural/context container with no perceptual or visual surface, role='group' carries the a11y semantics declaratively, and RadioItems self-manage checked state. Acceptable structural exception, not a defect. LOW doc-nit at most.
  • fix-status: open

LOW: SYS-2 — context-menu-004

  • dimension: E-bis
  • rule: SYS-2
  • location: src/uix/eidos/components/context-menu/context-menu.css:171
  • evidence: Trigger outline on data-state='open' hardcodes '1px dashed' border: 'outline: 1px dashed var(--color-border-default);'. Outline width and style are magic literals without token backing.
  • impact: The outline appearance cannot scale with theme or design system constraints. No way to override or scale the 1px width globally.
  • proposed-fix: Create recipe tokens 'trigger-focus-outline-width': '1px' and 'trigger-focus-outline-style': 'dashed', then reference them: 'outline: var(--context-menu-trigger-focus-outline-width, 1px) var(--context-menu-trigger-focus-outline-style, dashed) ...'.
  • verify: [downgraded] Partially valid kernel, over-stated. context-menu.css:171 outline: 1px dashed var(--color-border-default). The bare 1px IS a minor drift: every sibling component uses var(--border-width)/var(--border-width-medium) for border/outline widths (announce.css:29, combobox.css:96, dialog.css:28, listbox.css:41, etc.); this line is the only bare 1px outline-width across the eidos components grep. So 1px->var(--border-width) is a legitimate tokenization. BUT the candidate's proposed custom tokens for dashed over-reach — outline-style is a discrete keyword with no scale to reference. --color-border-default is a valid role token. Impact is minimal: this is a purely decorative courtesy outline on the otherwise-inert trigger when open (per the CSS comment 156-164). LOW magic-literal nit, not MEDIUM.
  • fix-status: open

No-findings dimensions

B, C, D, E, G

Theming facts (E-bis)

  • magic z-index: context-menu-002: recipe missing 'content-z' token (SYS-2) | context-menu-004: trigger outline 1px hardcoded
  • magic literals: context-menu-003: sub-trigger arrow size 0.625rem, rotate 45deg (SYS-2) | context-menu-004: trigger outline 1px dashed (SYS-2) | src/uix/eidos/components/context-menu/context-menu.css:114: opacity 0.55 (but covered by token var(--context-menu-item-disabled-opacity)) | src/uix/eidos/components/context-menu/context-menu.css:130: opacity var(--opacity-muted) - correct
  • undeclared parts: context-menu-001: 'sub' part declared in morfo but never registered via runtime.part() in provider
  • roles clean: true · variants clean: true

Tests (F)

  • exists: true · env: jsdom only (vitest-environment jsdom at line 1)
  • covers: contextmenu trigger event and anchor point capture (lines 130-168); item scoping to current content, excluding nested submenus (lines 170-189); item activation by click and keyboard (lines 191-227); checkbox and radio state toggling (lines 229-281); submenu open via pointer hover with 100ms delay (lines 283-336); group heading and separator accessibility attributes (lines 338-365)
  • untested: Arrow key navigation in content (loops, clamping when loop=false); Home/End key jump to first/last item; Escape key close menu and focus return to trigger; Tab key escape from menu (trap=false expected); Typeahead character matching; Submenu arrow-key navigation (left/right to close/open submenu); Focus sync when items are disabled (skip to next focusable); SafePolygon hover exit behavior; Dismissal on outside-click and interact-outside events; Focus trap behavior (when trapFocus=true)

Style observations (non-blocking)

  • Context-menu recipe tokens closely mirror dropdown-menu except for missing 'content-z' and arrow size tokens, which suggests the component was copied but not fully reconciled for floating layer z-index and icon sizing patterns.
  • The trigger outline affordance (dashed outline on open) is a nice UX cue but lacks a way to disable or customize the width/style globally.

Powered by TurnKey Linux.