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

7.9 KiB

Audit: collapsible

audit-version: 1 audited-at: 2026-06-26 scope: ['soma', 'sema'] (SCOPE-DRIFT → SYS-1) method: adversarially-verified workflow; HIGH lead-verified. B6 ground-truth: checkbox/toggle/switch commit-toggle = sequence post (lag-fixed); radio-group/tabs/accordion/stepper set state at call-site (pre OK); toggle-group + checkbox-group carry the A31 .includes pattern (SYS-7); slider has no gesture-layer A6 leak. provider: G:\dev\svelte\vicen\src\uix\soma\components\collapsible\collapsible-provider.svelte.ts sequence-audit: expand: post-sequence ✓ (state set in handler lines 68-70, morfo sequence:post at line 39). collapse: pre-sequence ✓ (state set in handler lines 71-73, morfo sequence:pre at line 48). Both events fire via runtime.trigger() in toggle() which defers handler execution. No lag risk: handler state-settin

Summary

Counts (post-verification): CRITICAL 0 · HIGH 1 · MEDIUM 1 · LOW 0.

Findings

MEDIUM: SYS-1 scope-drift: eidos directory exists but morfo scope omits 'eidos' — collapsible-001

  • dimension: Contract(morfo)
  • rule: SYS-1 scope-drift: eidos directory exists but morfo scope omits 'eidos'
  • location: src/uix/morfo/components/collapsible.ts:22
  • evidence: morfo declares scope: ['soma', 'sema'] but src/uix/eidos/components/collapsible/ directory exists with full implementation (collapsible.svelte, collapsible.css, index.ts). Accordion (sibling pattern) correctly declares scope: ['soma', 'sema', 'eidos']
  • impact: Scope declaration diverges from actual architecture; contract validators may not catch eidos-layer changes. Documentation of morfo scope is stale.
  • proposed-fix: Update collapsibleMorfo scope to ['soma', 'sema', 'eidos'] to match accordion pattern and actual directory structure
  • verify: [confirmed] CONFIRMED. morfo declares scope: ['soma', 'sema'] at src/uix/morfo/components/collapsible.ts:22, but src/uix/eidos/components/collapsible/ exists with a full implementation (collapsible.svelte, collapsible.css, collapsible-trigger.svelte, collapsible-content.svelte, types.ts, index.ts, README.md — verified via glob). The sibling disclosure pattern accordion correctly declares scope: ['soma', 'sema', 'eidos'] at src/uix/morfo/components/accordion.ts:7. This is SYS-1 scope-drift, MEDIUM. Severity unchanged.
  • fix-status: open

HIGH: SEQUENCE-LAG — collapse is sequence:'pre' but sets state in the handler (the Checkbox-244ms-lag mechanism) — collapsible-NEW-001

  • dimension: Behavior(soma)
  • rule: SEQUENCE / TWO-MOMENTS — a control whose functional state is set INSIDE the runtime.trigger HANDLER must use morfo sequence: 'post'; with 'pre' the perceptual hold (~240ms) blocks the state change (documented Checkbox 244ms-lag incident).
  • location: src/uix/morfo/components/collapsible.ts:48 (collapse sequence:'pre') + src/uix/soma/components/collapsible/collapsible-provider.svelte.ts:71-73 (handler sets state)
  • evidence: The collapse event sets the functional state INSIDE the trigger handler: provider lines 71-73 collapse: () => { this.opts.open.current = false; }. The morfo declares sequence: 'pre' at collapsible.ts:48. For 'pre', the runtime serializes the perceptual emit BEFORE the handler: runtime.svelte.ts:749-752 else { await runEmit(); if (handler) await handler(); }. runEmit awaits sources.eventEngine.emit (runtime.svelte.ts:696 await sources.eventEngine.emit(...)), whose engine awaits the visual channel hold (engine.ts:250 await visualChannel.handle(enriched, effective)), which does await sleep(holdMs) (visual.ts:79-81). collapsible declares no per-event hold and the sema pack (sema/components/collapsible.ts) declares none, so emerge falls back to label brief (visual.ts:43) = 240ms (durations.ts:27). Result: the content does NOT receive hidden until ~240ms after the click — the exact magnitude of the documented Checkbox 244ms-lag. (The expand event is correctly post at collapsible.ts:39, so opening has no lag — only closing lags.) This refutes the prompt's sequenceAudit line which marked collapse: pre-sequence ✓.
  • impact: User clicks the trigger to collapse an open section; the panel stays fully visible for ~240ms before hiding — a perceptible dead delay on every close, identical in mechanism and magnitude to the Checkbox 244ms-lag that the project already documented and fixed by flipping to sequence:'post'. The morfo's inline justification (collapsible.ts:10-13, 'collapse remains pre-sequence so the exit signal can play while the content is still visible') is based on a false concurrency assumption: the runtime does NOT overlap emit and handler for 'pre' — it serializes emit-then-handler, so the content is held visible for the FULL hold, then hidden, rather than the exit sound playing during a CSS exit transition.
  • repro: Open a Collapsible (expand — instant), then click the trigger to collapse. Measure the delay between click and hidden/data-state='closed' taking effect on the content. Expect ~240ms (the emerge brief hold) rather than near-zero. The expand direction does not exhibit the delay.
  • proposed-fix: Apply the documented Checkbox/Toggle/Switch doctrine: change collapse to sequence: 'post' (collapsible.ts:48) so the handler flips open=false first and the exit signal then acknowledges the resolved state, matching how accordion's symmetric close event is handled (accordion close is post per accordion.ts pattern). If a visible-during-exit cue is genuinely wanted, drive it from eidos CSS keyed on data-state='closed' + a CSS exit transition rather than blocking the functional state behind the sema hold. Alternatively set state at the call-site in toggle() (collapsible-provider.svelte.ts:85-89) before runtime.trigger, which would make 'pre' tolerable (the RadioGroup/Tabs pattern).
  • verify: [verifier-added → LEAD-CONFIRMED HIGH] The analyze agent (and the morfo's own inline comment) marked this CLEAN, assuming 'pre' overlaps emit+handler so the exit plays during the hold. The verifier traced the actual runtime and proved it SERIALIZES (runtime.svelte.ts:749-752: await runEmit(); await handler()), so open=false is delayed the full ~240ms brief emerge hold. I initially LEANED toward MEDIUM (reasoning "collapse is emerge.dismiss 'pre', the canonical animate-before-hide pattern like dialog/popover"), but that masking only holds for components that animate on data-event/Presence DURING the hold (dialog does); collapsible's exit is keyed on data-state='closed', which doesn't flip until AFTER the hold — so the 240ms is a real dead delay, not a filled exit animation. Restored to HIGH. Caveat: like the checkbox fix, the user-perceptible magnitude should be confirmed by a frame-by-frame browser measurement before/after; the MECHANISM (and the refuted morfo justification) are confirmed in code.
  • fix-status: open

No-findings dimensions

Behavior(soma) [except the sequence-lag above], DOM-selector, Frontier, TSC, Theming, Tests

Theming facts (E-bis)

  • magic z-index: none
  • magic literals: collapsible.css:4,19 translateY(-4px) — LOCAL animation amplitude in @keyframes, acceptable
  • undeclared parts: none
  • roles clean: true · variants clean: true

Tests (F)

  • exists: true · env: vitest jsdom
  • covers: ID linking via partRef; expand/collapse events + state toggle; disabled guard + disabled attr projection
  • untested: onOpenChange callback firing

Style observations (non-blocking)

  • CSS uses canonical token vars (--space-, --color-, --font-, --duration-, --ease-, --opacity-, --radius-*)
  • Recipe collapsible: in base.ts lines 712-725 with 12 tokens, all reference --*; no raw hex/px/rem/em
  • Naming: --collapsible-{slot}.{property} consistent (trigger-color, trigger-padding, content-padding, etc)
  • Animations in @keyframes use local px values for amplitude (-4px translateY), not tied to design tokens (acceptable for local animation logic)
  • No magic z-index or opacity decimals

Powered by TurnKey Linux.