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

96 lines
6.4 KiB

# Audit: popover
audit-version: 1
audited-at: 2026-06-26
scope: ['soma', 'sema'] (eidos recipe present though 'eidos' not in morfo scope — systemic SCOPE-DRIFT)
files:
- provider: src/uix/soma/components/popover/popover-provider.svelte.ts (present)
- morfo: src/uix/morfo/components/popover.ts (present)
- recipe: src/uix/eidos/components/popover/popover.css + lib/recipes/base.ts §popover (present)
- demo: web/routes/uix/components/popover (present, not inspected this pass)
- test: src/uix/soma/components/popover/popover-provider.svelte.test.ts (present, jsdom)
- readme: present (not inspected this pass)
## Summary
The cleanest of the three overlays audited so far. Popover gets the hard parts right: correct two-moments
(`present`/`close` sequence 'pre', emit-before-state), modal-derived FocusScope/ScrollLock (A16), the
hover-bounce guards (re-fire-while-open no-op; modal+hover loop guard) that match the recorded popover fixes,
dismissal trigger-exclusion via `contains()` (no `querySelector` injection), timer cleanup (A6), a clean
eidos→soma frontier, NO `role` double-write (it lets the morfo own role), and a Close that composes `<Button>`
with no bespoke recipe. Only real defect: the Close button re-declares `type`/`aria-label` that `syncAttrs`
already writes — the same double-write anti-pattern the component's own Trigger comment warns against. Plus the
systemic magic z-index and a jsdom-only test gap on the dismissal/focus paths.
Counts: CRITICAL 0 · HIGH 0 · MEDIUM 3 · LOW 0.
## Findings
### MEDIUM: Close button re-declares `type` + `aria-label` already written by `syncAttrs` (double-write) <!-- id: popover-001 -->
- dimension: D, A
- rule: active_architecture §7.7 (one authority per attribute) + the component's own discipline (trigger comment :423-426)
- location: popover-provider.svelte.ts:739-758 (Close registered `syncAttrs: true`, then props sets `type` + `aria-label`)
- evidence:
```ts
this.runtimePart = this.provider.runtime.part('close', { …, syncAttrs: true });
…
readonly props = $derived.by(() => ({
...this.runtimePart.props,
type: 'button' as const,
'aria-label': this.provider.soma.langs.ts(POPOVER_LANGS.CLOSE),
onclick: this.onclick
}));
```
The morfo Close declares `type` (literal 'button') and `aria-label` (`v.commonRef('buttons.close')`,
morfo:209-216); with `syncAttrs: true` the runtime writes both. The props block writes them again. The
Trigger comment a few hundred lines up explicitly says "Spreading them manually here would duplicate (and
potentially desync) the runtime's authority" — the Close violates exactly that.
- impact: benign today (both resolve to the same value), but it's duplicate authority + desync risk if
`POPOVER_LANGS.CLOSE` and the morfo `commonRef` ever diverge, and an internal inconsistency in the
component's own discipline. (Contrast Dialog where the analogous double-write IS a live bug — dialog-001.)
- proposed-fix: drop `type`/`aria-label` from the Close props; let `syncAttrs` own them (the morfo already
declares both). Keep only `onclick`.
- fix-status: open
### MEDIUM: `content-z: '75'` / `overlay-z: '60'` magic z-index literals <!-- id: popover-002 -->
- dimension: E-bis
- rule: THEMING §35 Bloque C (z-index magic → `--z-index-*` tokens)
- location: lib/recipes/base.ts:1753-1754 (`'content-z': '75'`, `'overlay-z': '60'`); popover.css:53 + :63.
- evidence: bare integers where `var(--z-index-*)` tokens belong. Third instance after `select.content-z: '80'`
and `dialog.overlay-z: '70'` → confirms the SYSTEMIC z-ladder finding (see THEMING_COHERENCE / SUMMARY).
- impact: the overlay stacking ladder lives as scattered per-recipe integers (60/70/75/80…) that can't be
coordinated or reasoned about centrally.
- proposed-fix: introduce a canonical `--z-index-{overlay,popover,modal}` ladder and map all overlay recipes
to it in one sweep.
- fix-status: open
### MEDIUM: Tests cover toggle/hover/labelling; dismissal trigger-exclusion, focus, and polymorphic close causes untested; jsdom-only <!-- id: popover-003 -->
- dimension: F
- rule: dimension-F honesty check
- location: popover-provider.svelte.test.ts (3 tests: toggle, hover open/close via timers + emit, title/description labelling)
- evidence: no test exercises the `onInteractOutside` trigger-exclusion bounce guard (:559-568, a documented
past bug), the Escape path, FocusScope trap+return (jsdom can't), or the `save`/`fail`/`cancel` close causes
(only `dismiss` is hit, via hover-close). `@vitest-environment jsdom` (:1).
- impact: the bounce-guard regression and focus behavior would ship unverified.
- proposed-fix: add a provider test that fires an interact-outside landing on the trigger (asserts no double
toggle), parametrize the close causes, and add a client/Playwright test for focus trap+return.
- fix-status: open
## No-findings dimensions
- **B (behavior):** two-moments ordering correct; hover bounce guards present and correct (re-fire-while-open
no-op :284, modal+hover loop guard :405/:419); FocusScope/ScrollLock derive from `modal` (A16); dismissal
trigger-exclusion via `contains()`; `hoverTimer` cleaned in `$effect` return (A6). Clean.
- **C (selectors):** uses `contains(triggerNode, target)` — no `querySelector` with interpolated user values. Clean.
- **D (frontier, severe half):** provider imports only soma layers + morfo — zero eidos imports.
- **A (contract):** the virtual Provider part declares empty `data` (no unreachable-data issue, unlike Dialog);
Trigger correctly lets the morfo own aria/type/data-state via `syncAttrs` (no override, unlike Dialog's role).
All CSS-targeted parts (`title`/`description`/`arrow`/`overlay`) are morfo-declared (no undeclared-part drift,
unlike Select/Dialog).
- **E (TSC):** no `data-color`, no derived-token-at-root risk; clean.
## Style observations (opinion, non-blocking)
- Popover is the positive reference among the overlays for: (a) Close-via-`<Button>` with no bespoke recipe
(the model Dialog's stale trigger envelope should follow), (b) letting the morfo own Trigger ARIA without a
manual override (the model Dialog's role override violates), and (c) the documented hover-bounce guards.
Worth citing in the SUMMARY as the "do it like Popover" baseline.
- The one inconsistency (Close double-write, popover-001) is small but notable precisely because the same
file's Trigger comment articulates the correct rule — fixing it makes the file self-consistent.

Powered by TurnKey Linux.