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.
90 lines
10 KiB
90 lines
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 <!-- id: 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 <!-- id: 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 <!-- id: 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 <!-- id: 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 <!-- id: 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.
|