# 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.