|
|
|
|
# Audit: dialog
|
|
|
|
|
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/dialog/dialog-provider.svelte.ts (present)
|
|
|
|
|
- morfo: src/uix/morfo/components/dialog.ts (present)
|
|
|
|
|
- recipe: src/uix/eidos/components/dialog/dialog.css + lib/recipes/base.ts §dialog (present)
|
|
|
|
|
- demo: web/routes/uix/components/dialog (present, not inspected this pass)
|
|
|
|
|
- test: src/uix/soma/components/dialog/dialog-provider.svelte.test.ts (present, jsdom)
|
|
|
|
|
- readme: present (not inspected this pass)
|
|
|
|
|
|
|
|
|
|
## Summary
|
|
|
|
|
Dialog is the reference for polymorphic close (book §5.3) and gets most of the hard stuff right: nesting via
|
|
|
|
|
`watch`+`untrack` (loop-safe), FocusScope/ScrollLock derived from `modal` (A16), correct two-moments ordering
|
|
|
|
|
(emit-before-state on `close` with sequence 'pre'), a thoughtfully tight `data-color` subset (neutral/risk/
|
|
|
|
|
threat), and a clean eidos→soma frontier. Two real defects stand out: the Content provider **double-writes
|
|
|
|
|
`role`/`aria-roledescription`** against the syncAttrs path (an a11y race for the `alertdialog` variant), and the
|
|
|
|
|
**`[data-dialog-trigger]` CSS envelope is stale dead/conflicting chrome** left behind after the trigger migrated
|
|
|
|
|
to a composed `<Button>`. Plus the systemic magic z-index and jsdom-only test gap.
|
|
|
|
|
|
|
|
|
|
Counts: CRITICAL 0 · HIGH 2 · MEDIUM 2 · LOW 2.
|
|
|
|
|
|
|
|
|
|
## Findings
|
|
|
|
|
|
|
|
|
|
### HIGH: Content double-writes `role` and `aria-roledescription` (syncAttrs + manual props) — racy role for `alertdialog` <!-- id: dialog-001 -->
|
|
|
|
|
- dimension: D, A
|
|
|
|
|
- rule: active_architecture §7.7 ("lo que `dom.apply` escribe, Svelte no lo renderiza — una sola autoridad por atributo") + rule 7.8
|
|
|
|
|
- location: dialog-provider.svelte.ts:391-397 (Content registered `syncAttrs: true`) vs :460-463 (props override)
|
|
|
|
|
- evidence:
|
|
|
|
|
```ts
|
|
|
|
|
this.runtimePart = this.provider.runtime.part('content', { …, syncAttrs: true });
|
|
|
|
|
…
|
|
|
|
|
readonly props = $derived.by(() => ({
|
|
|
|
|
...this.runtimePart.props,
|
|
|
|
|
role: this.provider.opts.variant.current, // 'dialog' | 'alertdialog'
|
|
|
|
|
'aria-roledescription': undefined, // nukes the morfo's value
|
|
|
|
|
…
|
|
|
|
|
}));
|
|
|
|
|
```
|
|
|
|
|
The morfo declares Content `role: 'dialog'` (morfo:150) and `aria-roledescription` (:211-215); with
|
|
|
|
|
`syncAttrs: true` the runtime writes both via `dom.apply`. The provider then ALSO sets `role` (to `variant`)
|
|
|
|
|
and `aria-roledescription: undefined` in the Svelte-rendered props. Two authorities per attribute.
|
|
|
|
|
- impact: for `variant='alertdialog'`, `dom.apply` writes `role="dialog"` while Svelte writes
|
|
|
|
|
`role="alertdialog"` — order-dependent ping-pong; the accessible role of an alert dialog is non-deterministic
|
|
|
|
|
(a WAI-ARIA correctness break). `aria-roledescription` is written-then-blanked. The morfo (the cross-layer
|
|
|
|
|
source of truth) says `role='dialog'` but the component renders a variant-dependent role it can't express.
|
|
|
|
|
- repro: render `<Dialog variant="alertdialog">`, inspect Content `role` across effect ticks.
|
|
|
|
|
- proposed-fix: make `role` morfo-expressible (bind a `propRef('variant')`/stateRef so the morfo owns the
|
|
|
|
|
variant-dependent role), OR drop `syncAttrs` for Content and supply role/aria via `renderProps()` + a single
|
|
|
|
|
override. Don't mix syncAttrs with manual same-attr writes.
|
docs(audit): mark the 16 HIGH code findings as fixed
fix-status: open -> fixed (<commit>) for every HIGH whose code fix landed this
session, across 12 component reports:
- 92f988e7: listbox-001, grid-list-001/002, tree-view-002, tree-grid-001, tag-group-001 (SYS-7 A31)
- 98954a7c: alert-dialog-001, breadcrumb-001, navigation-menu-006, select-001, dialog-002
- db33f66c: select-002, dialog-001, collapsible-NEW-001
- bac67315: combobox-001, select-003
The HIGH tier is now code-complete; the 2 remaining HIGH (select-004,
combobox-002) are test-coverage gaps for Phase 6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
|
|
|
- fix-status: fixed (db33f66c)
|
|
|
|
|
|
|
|
|
|
### HIGH: `[data-dialog-trigger]` CSS envelope is dead/conflicting after the trigger migrated to a composed `<Button>` <!-- id: dialog-002 -->
|
|
|
|
|
- dimension: E-bis, D
|
|
|
|
|
- rule: THEMING §16 "no solapar" (overlapping/mis-owned styles) + COMPONENT_GUIDE §4 (compose, don't reinvent) + project memory `project_dialog_button_2026-06-20`
|
|
|
|
|
- location: dialog.css:22-54 (`[data-dialog-trigger]` full button envelope) vs dialog-trigger.svelte:17-26
|
|
|
|
|
(renders `<Button {...props}>` via soma `child`); recipe `dialog.trigger-font-size` (base.ts:727).
|
|
|
|
|
- evidence: the eidos `<Dialog.Trigger>` composes the framework `<Button>` through soma's `child` snippet, so
|
|
|
|
|
the rendered element carries BOTH `data-button` AND `data-dialog-trigger`. The dialog recipe still defines a
|
|
|
|
|
complete competing chrome on `[data-dialog-trigger]` (height/padding/border/background/hover/disabled +
|
|
|
|
|
`--dialog-trigger-font-size`). Both single-attribute selectors have equal specificity → source-order decides;
|
|
|
|
|
the two recipes fight over the same element. The CSS header comment (:15-21, "the trigger needs its own
|
|
|
|
|
envelope here") is stale — it predates the Button migration.
|
|
|
|
|
- impact: silent visual incoherence (two sources for one button's chrome), order-dependent; the
|
|
|
|
|
`--dialog-trigger-*` tokens are orphan knobs. Exactly the overlap E-bis.5 forbids.
|
|
|
|
|
- repro: inspect a `<Dialog.Trigger>` — it has `data-button` + `data-dialog-trigger`; toggle CSS order to see
|
|
|
|
|
the chrome change.
|
|
|
|
|
- proposed-fix: delete the `[data-dialog-trigger]` envelope + its tokens (Button owns the chrome); keep only a
|
|
|
|
|
dialog-specific concern if any (there is none — the Close recipe is the correct model, dialog.css:319-325).
|
docs(audit): mark the 16 HIGH code findings as fixed
fix-status: open -> fixed (<commit>) for every HIGH whose code fix landed this
session, across 12 component reports:
- 92f988e7: listbox-001, grid-list-001/002, tree-view-002, tree-grid-001, tag-group-001 (SYS-7 A31)
- 98954a7c: alert-dialog-001, breadcrumb-001, navigation-menu-006, select-001, dialog-002
- db33f66c: select-002, dialog-001, collapsible-NEW-001
- bac67315: combobox-001, select-003
The HIGH tier is now code-complete; the 2 remaining HIGH (select-004,
combobox-002) are test-coverage gaps for Phase 6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
|
|
|
- fix-status: fixed (98954a7c)
|
|
|
|
|
|
|
|
|
|
### MEDIUM: `overlay-z: '70'` magic z-index literal (content = `calc(overlay-z + 1)`) <!-- id: dialog-003 -->
|
|
|
|
|
- dimension: E-bis
|
|
|
|
|
- rule: THEMING §35 Bloque C (z-index magic → `--z-index-*` tokens) + feedback "no intentional magic numbers"
|
|
|
|
|
- location: lib/recipes/base.ts:731 (`'overlay-z': '70'`); dialog.css:67 + :91 (`calc(var(--dialog-overlay-z) + 1)`).
|
|
|
|
|
- evidence: a bare `70` where a `var(--z-index-modal)` token belongs. Same class as `select.content-z: '80'` →
|
|
|
|
|
candidate systemic finding (overlay stacking ladder should be one tokenized source, not per-recipe integers).
|
|
|
|
|
- impact: the modal/popover/overlay z-ladder can't be coordinated centrally; integers drift apart.
|
|
|
|
|
- proposed-fix: map dialog overlay/content to `--z-index-modal`/`--z-index-modal-content` (or the canonical
|
|
|
|
|
ladder) and audit all overlay recipes together.
|
|
|
|
|
- fix-status: open
|
|
|
|
|
|
|
|
|
|
### MEDIUM: Tests pin open/close/disabled/modal-derivation; dismissal paths, polymorphic causes, nesting, focus untested; jsdom-only <!-- id: dialog-004 -->
|
|
|
|
|
- dimension: F
|
|
|
|
|
- rule: dimension-F honesty check
|
|
|
|
|
- location: dialog-provider.svelte.test.ts (3 tests)
|
|
|
|
|
- evidence: only `dismissWith('dismiss')` of the five causes is exercised (`save`/`fail`/`cancel`/
|
|
|
|
|
`dismiss-outside` and their distinct `data-last-action` + semantic.family/intent are not). No test for the
|
|
|
|
|
Escape/interact-outside dismissal wiring, `nestedOpenCount` increment/decrement, or focus trap+return (jsdom
|
|
|
|
|
can't exercise FocusScope faithfully — `@vitest-environment jsdom` :1).
|
|
|
|
|
- impact: the polymorphic-close matrix (the component's headline feature) and the focus/nesting behavior ship
|
|
|
|
|
largely unverified; dialog-001's role race wouldn't be caught.
|
|
|
|
|
- proposed-fix: parametrize a test over all five `DISMISS_CAUSES` asserting `data-last-action` + emitted
|
|
|
|
|
family/intent; add nesting-count tests; add a client/Playwright test for focus trap + return.
|
|
|
|
|
- fix-status: open
|
|
|
|
|
|
|
|
|
|
### LOW: Provider part declares `data-state`/`data-color`/`data-disabled` but is `kind:'virtual'` / `defaultElement:'none'` <!-- id: dialog-005 -->
|
|
|
|
|
- dimension: A
|
|
|
|
|
- rule: dimension-A (part data must reach an element) + 2-of-3
|
|
|
|
|
- location: morfo dialog.ts:96-126 (Provider `kind:'virtual'`, `defaultElement:'none'`, yet declares 3 data attrs);
|
|
|
|
|
provider registers it `syncAttrs: true` with optional `ref` (provider renders no DOM).
|
|
|
|
|
- evidence: the DialogProvider renders only children (no host element); a virtual part with no element can't
|
|
|
|
|
carry `data-state`/`data-color`. The same data also lives on Content (where it's actually consumed by the CSS).
|
|
|
|
|
- impact: the Provider-part data declarations are likely unreachable (dead contract) unless the eidos root
|
|
|
|
|
wrapper renders an element and applies them — needs confirmation. If dead, they mislead readers/tools.
|
|
|
|
|
- proposed-fix: confirm whether any element carries `data-dialog` (eidos root). If not, drop the data from the
|
|
|
|
|
virtual Provider part (Content already carries state/color), or give the provider a real host.
|
|
|
|
|
- fix-status: open
|
|
|
|
|
|
|
|
|
|
### LOW: Eidos-only `[data-dialog-header]`/`[data-dialog-footer]` shells not morfo-backed <!-- id: dialog-006 -->
|
|
|
|
|
- dimension: A, D
|
|
|
|
|
- rule: active_architecture §7.6 (eidos consumes declared data-*) — acknowledged exception
|
|
|
|
|
- location: dialog.css:260-290 (commented "Layout shells (eidos-only, no morfo backing)").
|
|
|
|
|
- evidence: pure layout containers with no ARIA/behavior, styled by the recipe but absent from the morfo. The
|
|
|
|
|
CSS comment is explicit about it.
|
|
|
|
|
- impact: minimal (no behavior/ARIA), but it's the same undeclared-part pattern as select-005 — worth a
|
|
|
|
|
consistent disposition (eidos-only parts either get `kind:'internal'` morfo entries or a documented allow-list).
|
|
|
|
|
- proposed-fix: adopt a project-wide convention for eidos-only decorative/layout parts (see SHARED_EXTRACTION /
|
|
|
|
|
systemic UNDECLARED-EIDOS-PARTS).
|
|
|
|
|
- fix-status: open
|
|
|
|
|
|
|
|
|
|
## No-findings dimensions
|
|
|
|
|
- **B (behavior):** two-moments ordering correct (`open`/`close` sequence 'pre' → emit precedes state via
|
|
|
|
|
`await runtime.trigger`); nesting count uses `watch` + `untrack` (loop-safe, A30-spirit); FocusScope +
|
|
|
|
|
ScrollLock derive from `modal` (A16); disabled guards at call-site (rule 10). Clean.
|
|
|
|
|
- **C (selectors):** no `querySelector` with interpolated user values in the provider. Clean.
|
|
|
|
|
- **D (frontier, severe half):** provider imports only soma layers + morfo — zero eidos imports. (role
|
|
|
|
|
double-write filed under D as dialog-001, but it's a runtime/contract issue, not a cross-layer import.)
|
|
|
|
|
- **E (TSC):** `data-color` declared per-part in the morfo (provider+content), CSS consumes it on content; no
|
|
|
|
|
eager-substitution `:root` derived bug. Colors fully tokenized. Clean.
|
|
|
|
|
|
|
|
|
|
## Style observations (opinion, non-blocking)
|
|
|
|
|
- The `data-color` subset (neutral/risk/threat only, with the rationale "a dialog is not a hierarchy and
|
|
|
|
|
irreversible loss should not be a dialog") is an exemplary application of THEMING §4 subset-por-componente —
|
|
|
|
|
worth citing as the positive reference for other components.
|
|
|
|
|
- The polymorphic-close `DISMISS_CAUSES` map is clean and readable; the only gap is test coverage of its breadth.
|