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.
53 lines
7.4 KiB
53 lines
7.4 KiB
|
4 months ago
|
# Audit: splitter
|
||
|
|
audit-version: 1
|
||
|
|
audited-at: 2026-06-26
|
||
|
|
scope: soma, sema, eidos
|
||
|
|
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: src/uix/soma/components/splitter/splitter-provider.svelte.ts
|
||
|
|
sequence-audit: N/A - splitter is not a state-binding control (Checkbox/Toggle/Switch/RadioGroup pattern). It is a direct-manipulation drag component with `commit-set: 'post'` morfo sequence. State changes (panel sizes) are set BEFORE pointerup closes the drag; notifyResizeEnd() routes through runtime.trigger('comm
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
Counts (post-verification): CRITICAL 0 · HIGH 0 · MEDIUM 1 · LOW 1.
|
||
|
|
|
||
|
|
## Findings
|
||
|
|
### MEDIUM: Frontier: eidos CSS must reference morfo event names exactly; mismatch breaks sema pulse v — splitter-001 <!-- id: splitter-001 -->
|
||
|
|
- dimension: Frontier, Data-selector
|
||
|
|
- rule: Frontier: eidos CSS must reference morfo event names exactly; mismatch breaks sema pulse visual feedback
|
||
|
|
- location: src/uix/eidos/components/splitter/splitter.css:115; src/uix/morfo/components/splitter.ts:51,15
|
||
|
|
- evidence: CSS selector `[data-event='commit-resize']` but morfo defines event `name: 'commit-set'`. Morfo comment at line 15 confirms eidos recipe should reference `data-event='commit-set'` — CSS uses wrong event name.
|
||
|
|
- impact: Sema event pulse visual feedback (handle background flash on drag commit) never fires because eidos events layer stamps `data-event='commit-set'` but CSS selector waits for `commit-resize`. User gets no visual confirmation of resize completion.
|
||
|
|
- repro: Drag a splitter resize trigger; release. Observe no background pulse on the handle.
|
||
|
|
- proposed-fix: Change line 115 of splitter.css from `[data-splitter-resize-trigger][data-event='commit-resize']` to `[data-splitter-resize-trigger][data-event='commit-set']`
|
||
|
|
- verify: [confirmed] CONFIRMED that the pulse never fires, but the cited rule/fix is partially wrong and severity is overstated. splitter.css:115 selects `[data-splitter-resize-trigger][data-event='commit-resize'][data-event-phase='active']`; the morfo (splitter.ts:51) names the event `commit-set` and the provider triggers `runtime.trigger('commit-set')` (provider lines 205, 97). The sema engine stamps `data-event='commit-set'`, so the `commit-resize` selector never matches — the comment at css:110-111 and css:5 are stale relics of the old event name. HOWEVER the proposed fix (rename `commit-resize`→`commit-set`) is INSUFFICIENT: the morfo (splitter.ts:55) and sema cascade (sema/components/splitter.ts:49 `onProvider({ eventName: 'commit-set' })`) target `v.partRef('provider')`, so the stamp lands on the provider element `[data-splitter]`, NOT on `[data-splitter-resize-trigger]`. The selector targets both the wrong event name AND the wrong element. To actually pulse the handle on commit the selector would need to key off the provider stamp (e.g. `[data-splitter][data-event='commit-set'] [data-splitter-resize-trigger]`) or the commit event's target would need to move. Severity downgraded HIGH→MEDIUM: this is a purely cosmetic/ornamental sema-pulse that never fired (no functional or a11y impact); the handle still gets its `[data-state='dragging']`/`[data-dragging]` active background via css:77-80 during the actual drag, so the user is not left without resize feedback.
|
||
|
|
- fix-status: open
|
||
|
|
|
||
|
|
### LOW: Theming: opacity literals forbidden; must use canonical --opacity-* token or fallback to - — splitter-003 <!-- id: splitter-003 -->
|
||
|
|
- dimension: Theming, Tokens
|
||
|
|
- rule: Theming: opacity literals forbidden; must use canonical --opacity-* token or fallback to --opacity-disabled.
|
||
|
|
- location: src/uix/eidos/components/splitter/splitter.css:35
|
||
|
|
- evidence: Line 35: `opacity: var(--splitter-disabled-opacity, 0.6);`. Recipe base (recipes/base.ts) defines splitter['disabled-opacity'] = 'var(--opacity-disabled)', which is the canonical fallback. CSS uses magic literal 0.6 instead.
|
||
|
|
- impact: Opacity value diverges from canonical token scale. If --opacity-disabled is updated system-wide, splitter disabled state won't reflect the change because it has a hardcoded literal fallback.
|
||
|
|
- proposed-fix: Change line 35 to `opacity: var(--splitter-disabled-opacity, var(--opacity-disabled));` so the fallback chains to the canonical token instead of a magic literal.
|
||
|
|
- verify: [downgraded] CONFIRMED as token drift but DOWNGRADED MEDIUM→LOW. splitter.css:35 `opacity: var(--splitter-disabled-opacity, 0.6)` uses a magic opacity decimal `0.6` as the inline fallback, violating E-bis (opacity decimal → `--opacity-*`). Verified recipes/base.ts:4231 defines `splitter['disabled-opacity'] = 'var(--opacity-disabled)'`, so under normal operation the recipe value wins and the `0.6` literal NEVER surfaces — it only appears if the recipe layer is entirely absent. Other consumers chain the canonical token (`var(--opacity-disabled)`) as their fallback, so this is a real inconsistency, but it is the lowest-impact token-drift category: cosmetic, no behavioral divergence in practice. Proposed fix is correct (chain to `var(--opacity-disabled)`).
|
||
|
|
- fix-status: open
|
||
|
|
|
||
|
|
## No-findings dimensions
|
||
|
|
Contract: morfo satisfies Morfo and uses as const, Contract: parts registered via runtime.part exist in morfo, Contract: data-{c}/-{part} naming, Behavior: setPointerCapture is immediate (pure handle, no child buttons), Behavior: A12 RTL keyboard via getDirectionalKeys, Behavior: A14 roving not applicable (single handle, not roving group), Behavior: A15 gesture cleanup on unmount (dragSignalFrame, pendingDragPercent cancelled), Behavior: A30 panel registration is direct (sync), not effect-wrapped for props, Behavior: A31 no O(N²) patterns (find/getPanelState are on local panels array, not global), Behavior: A33 no $state(Map/Set), Behavior: A6 requestFrame/cancelFrame properly disposed in effect cleanup, DOM: no querySelector interpolating consumer values, Frontier: soma does not import eidos, Frontier: syncAttrs true with derived props (no double-write), Theming: only standard 9 roles + space tokens referenced, Theming: no raw hex where role alias canonical, Theming: spacing uses --space-* tokens, Theming: no local z-index or canonical --z-index-* used (no z needed for splitter), Test: provider test exists, jsdom env, covers gesture drag, keyboard, collapse
|
||
|
|
|
||
|
|
## Theming facts (E-bis)
|
||
|
|
- magic z-index: none
|
||
|
|
- magic literals: 0.6 (splitter-001-adjacent)
|
||
|
|
- undeclared parts: none
|
||
|
|
- roles clean: true · variants clean: true
|
||
|
|
|
||
|
|
## Tests (F)
|
||
|
|
- exists: true · env: jsdom
|
||
|
|
- covers: panel registration and sizing; resize clamping and min/max constraints; collapse/expand with adjacent space redistribution; pointer drag (down/move/up); keyboard arrows (RTL-aware via getDirectionalKeys); keyboard Home/End (minimize/maximize); aria-valuenow/min/max projection; disabled state blocks resize
|
||
|
|
- untested: gesture on lostpointercapture recovery; double-click toggle collapse; dragSignalFrame cleanup during unmount with active drag
|
||
|
|
|
||
|
|
## Style observations (non-blocking)
|
||
|
|
- Excellent drag-signal throttling with per-frame payload queue (72ms window)
|
||
|
|
- Clean pointer-capture semantics for pure-handle resize (not using Gesture class)
|
||
|
|
- Robust collapse/expand logic respects min/max constraints on both panels
|
||
|
|
- Well-documented keyboard shortcut implementation (Home/End/ArrowKeys)
|