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

10 KiB

Audit: drawer

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/drawer/drawer-provider.svelte.ts

Summary

Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 5 · LOW 0. systemic hits: SYS-1: scope-drift (morfo scope missing 'eidos' despite recipe directory existing); SYS-2: magic z-index (bare integers in recipe: overlay-z='72', inline-z='64'), magic opacity/dimensions (overlay-opacity='62%', handle-length='36px', handle-thickness='4px', content-ring-dragging-offset='2px'), magic opacity in color-mix (32%); SYS-3: jsdom-only test with untested high-risk paths (keyboard, drag, focus, gesture cleanup); SYS-5: double-write (provider explicitly sets role and aria-modal which morfo also declares).

Findings

MEDIUM: SYS-1 — drawer-001

  • dimension: A
  • rule: SYS-1
  • location: src/uix/morfo/components/drawer.ts:7
  • evidence: scope: ['soma', 'sema'] but eidos directory exists at src/uix/eidos/components/drawer/
  • impact: Scope does not declare eidos as a layer despite recipe existing. Signals incomplete contract annotation.
  • repro: Read src/uix/morfo/components/drawer.ts line 7 and ls src/uix/eidos/components/drawer/
  • proposed-fix: Update morfo scope to include 'eidos': scope: ['soma', 'sema', 'eidos']
  • verify: [confirmed] morfo line 7: scope: ['soma', 'sema'], — 'eidos' absent. Eidos recipe block exists (base.ts:892 drawer: { with ~50 tokens) plus a components/drawer/ dir. Systemic SYS-1: sibling dialog.ts:18 has the identical scope: ['soma', 'sema'] with its own eidos recipe (base.ts:726). HIGH is correct per SYS-1 contract-drift; the morfo scope under-declares a real layer.
  • fix-status: open

MEDIUM: SYS-2 — drawer-002

  • dimension: E-bis
  • rule: SYS-2
  • location: src/uix/eidos/lib/recipes/base.ts:896,897
  • evidence: 'overlay-z': '72', 'inline-z': '64' — bare integers without --z-index-* token references
  • impact: Magic z-index values not parameterized via canonical token system. Inconsistent with project theming rules.
  • repro: grep "'overlay-z'|'inline-z'" src/uix/eidos/lib/recipes/base.ts
  • proposed-fix: Create --z-index-overlay and --z-index-inline tokens in the canonical scale and reference via var() in recipe.
  • verify: [confirmed] base.ts:896 'overlay-z': '72', and 897 'inline-z': '64', are bare integers. A canonical --z-index-* scale exists and is consumed elsewhere (base.ts:4357 z: 'var(--z-index-sticky)'; comment at 4250-4252 references 'the global --z-index-* scale'). These two values bypass it. Genuine SYS-2 magic-z. MEDIUM appropriate.
  • fix-status: open

MEDIUM: SYS-2 — drawer-003

  • dimension: E-bis
  • rule: SYS-2
  • location: src/uix/eidos/lib/recipes/base.ts:895
  • evidence: 'overlay-opacity': '62%' — bare opacity literal without --opacity-* token
  • impact: Magic opacity value not referenced via canonical opacity token scale.
  • repro: grep "'overlay-opacity'" src/uix/eidos/lib/recipes/base.ts
  • proposed-fix: Create --opacity-overlay token or use existing project opacity scale, reference via var().
  • verify: [confirmed] base.ts:895 'overlay-opacity': '62%', — bare percentage. An --opacity-* scale exists and is consumed widely (e.g. base.ts:890 'disabled-opacity': 'var(--opacity-disabled)'). The 62% literal does not reference it. Confirmed SYS-2 magic-opacity; note it is replicated verbatim in dialog (base.ts:729) — systemic, not drawer-specific. MEDIUM appropriate.
  • fix-status: open

MEDIUM: SYS-5 — drawer-007

  • dimension: A
  • rule: SYS-5
  • location: src/uix/soma/components/drawer/drawer-provider.svelte.ts:1044-1045
  • evidence: Provider props explicitly set role: 'dialog' and 'aria-modal': this.provider.isOverlay ? true : undefined, while morfo also declares role:'dialog' and aria-modal with prop-truthy condition
  • impact: Double-write of role and aria-modal. If morfo condition diverges from provider logic, the two values could conflict. Violates 'una sola autoridad por atributo'.
  • repro: Read src/uix/soma/components/drawer/drawer-provider.svelte.ts lines 1044-1045 and compare with morfo aria declarations at lines 184, 214-217
  • proposed-fix: Remove explicit role and aria-modal from provider props — let the runtime apply them from morfo. If dynamic aria-modal is needed, ensure logic matches the morfo's prop-truthy condition exactly.
  • verify: [downgraded] Real double-write confirmed but severity overstated. Content part is syncAttrs: true (provider line 517), so the runtime's syncPartAttrs effect (runtime.svelte.ts:456-465) writes morfo's static role: 'dialog' (morfo:184) + dynamic aria-modal gated by prop-truthy modal (morfo:214-217). Provider props ALSO set them (provider:1044 role: 'dialog' as const, 1045 'aria-modal': this.provider.isOverlay ? true : undefined). So both attrs are written by two authorities — a true SYS-5 'una sola autoridad' violation. BUT the rubric reserves HIGH for SYS-5 'when the two values can diverge'. They cannot: role is the same literal both sides, and aria-modal = isOverlay (provider:190 variant==='overlay') is identical to morfo's modal source (provider:254 () => opts.variant.current === 'overlay'). Non-divergent double-write → MEDIUM, not HIGH. Same pattern repeats for Title (provider:1208 role:'heading' + 1209 aria-level vs morfo:279/282) and Trigger (provider:443-446 type/aria-haspopup/aria-expanded vs morfo:154/162-164) — systemic across this provider.
  • fix-status: open

MEDIUM: SYS-3 — drawer-008

  • dimension: F
  • rule: SYS-3
  • location: src/uix/soma/components/drawer/drawer-provider.svelte.test.ts:1
  • evidence: @vitest-environment jsdom — tests only cover direction resolution, dismissal, and variant modes. No tests for keyboard handling (Tab/Escape), focus return to trigger, drag gestures, gesture cleanup, or focus trap behavior.
  • impact: Interaction-heavy component (keyboard nav, drag, focus management) tested only on low-risk paths with jsdom. Lacks client/Playwright tests for gesture interaction and focus side-effects.
  • repro: Read drawer-provider.svelte.test.ts; note tests use jsdom and @vitest-environment jsdom decorator
  • proposed-fix: Add a client-environment (Playwright) test suite covering: Tab/Shift+Tab keyboard navigation, Escape dismissal, drag-to-dismiss interaction, focus return on close, and gesture cleanup on unmount.
  • verify: [confirmed] Test file line 1 // @vitest-environment jsdom. Only 4 tests (describe at :96, it at :102 direction-resolution, :115 dismissal+snap-clear, :155 persistent-open, :174 inline-escape). No coverage of: Tab/Shift+Tab (morfo keyboard focus-next/focus-prev at morfo:234-235), focus trap/return-to-trigger (morfo focus.trap/return at :109-114; provider handleClose:359 manual focus return), drag gesture (provider gesture/axial at :682/:883), or two-moments emit ordering (close is sequence:'pre', morfo:52). Interaction-heavy + jsdom-only + no client test = SYS-3. MEDIUM appropriate.
  • fix-status: open

No-findings dimensions

B, C, D, G

Theming facts (E-bis)

  • magic z-index: content-z: calc(var(--drawer-overlay-z) + 1) — relies on recipe's bare '72' integer
  • magic literals: overlay-z '72' should be --z-index-overlay var | inline-z '64' should be --z-index-inline var | overlay-opacity '62%' should be --opacity-overlay var | handle-length '36px' should reference --size-* or --space-* token | handle-thickness '4px' should reference --space-* token | content-ring-dragging-offset '2px' should reference --space-* token | content-ring-dragging-color opacity 32% should be extracted to --opacity-dragging-ring token
  • undeclared parts: none
  • roles clean: true · variants clean: true
  • conformance: roles/sizes: 'sm'|'md'|'lg'|'full' per component subset (correct). variants: 'overlay'|'inline'|'persistent' (component-specific, documented). naming: --drawer-{slot} public, --_drawer-* private (observed). no undeclared parts in recipe (header/footer are intentional eidos-only layout parts).

Tests (F)

  • exists: true · env: jsdom (@vitest-environment jsdom)
  • covers: logical direction resolution (start/end → physical sides per dir); polymorphic close with data-last-action imperative prewrite; persistent variant ignores open state; inline variant skips focus trap and scroll lock
  • untested: keyboard navigation (Tab, Shift+Tab, Escape) from morfo's keyboard action routes; focus trap and focus return to trigger in overlay/modal mode; drag-to-dismiss gesture and progress signals; resize-on-drag with snap points; snap-point sequential cycling; gesture cleanup on unmount (cancellation of requestAnimationFrame, event listeners); nesting behavior (data-nested, nestedOpenCount management); drag-start/end/progress semantic emissions; scroll-lock guard (scrollLockTimeout behavior); handle-only gesture restriction; pointer event composition (gesture + axial mutual exclusivity); overlay opacity modulation based on activeSnapPoint

Style observations (non-blocking)

  • Morfo declaration is well-structured with clear event semantics (polymorphic close, drag handle family, resize handle family). Events properly declare sequence (pre/coincident/post).
  • Provider architecture cleanly separates concerns: DrawerProvider (root state), DrawerTriggerProvider, DrawerContentProvider (gesture+focus), DrawerOverlayProvider, DrawerHandleProvider, DrawerTitleProvider, DrawerDescriptionProvider, DrawerCloseProvider. Each uses direct constructor-level child→parent registration (A30-compliant).
  • Gesture layer properly throttles drag-progress to rAF via flushDragProgress and rAF request/cancel lifecycle with proper cleanup in $effect.
  • Focus return correctly implemented: non-modal returns focus to triggerNode (lines 359, 366); modal relies on FocusScope layer.
  • Dismiss layer correctly gated: interactOutside always 'ignore' (drawer is not a popover), Escape gated by modal flag (non-modal manually handles via onkeydown in content).
  • CSS uses intentional eidos-only layout parts (data-drawer-header, data-drawer-footer) not declared in morfo — correct separation of layout concerns from behavioral contract.

Powered by TurnKey Linux.