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

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)

  • 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:
    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

  • 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

  • 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.