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.
54 lines
7.9 KiB
54 lines
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 <!-- id: 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: fixed (212624e0)
|
|
|
|
### HIGH: SEQUENCE-LAG — `collapse` is `sequence:'pre'` but sets state in the handler (the Checkbox-244ms-lag mechanism) — collapsible-NEW-001 <!-- id: 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: fixed (db33f66c)
|
|
|
|
## 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
|