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/src/uix/morfo/components/dialog.test.ts

138 lines
5.9 KiB

import { describe, expect, it } from 'vitest'
import { validateMorfo, MorfoInvariantError } from '../schema'
import type { Morfo, MorfoEvent } from '../types'
import { dialogMorfo } from './dialog'
soma/morfo: typed createAttrs + 6 audit batches + form hang fix + A36 ## morfo: typed createAttrs - `types.ts`: all array fields are now `readonly` so morfos can be authored with `as const satisfies Morfo` without casts. - `createAttrs` is a generic `<const M extends MorfoShape>` that infers the emitted attr map from the morfo's literal shape. Typos like `attrs.trigerr` are now compile errors; autocomplete lists every valid part. - All 66 morfos migrated: `export const xMorfo = { ... } as const satisfies Morfo`. - Two latent bugs the new typing caught: `dateRangeFieldAttrs.root` and `timeRangeFieldAttrs.root` — legacy from the root→provider refactor, fixed to `.provider`. - Docs (morfo/README, COMPONENT_GUIDE) mandate `as const satisfies Morfo`; the older `: Morfo` annotation widens literals and loses autocomplete. ## Audit batches 1–7 (soma-audit-2026-04-20) Batch 1 — Keyboard a11y (A14/A12): - Tabs: `isFallbackTabStop(el)` using `untrack` → first trigger gets `tabindex=0` when no tab matches (WCAG 2.1.1 was failing). - RatingGroup: `dir` prop + `getDirectionalKeys(dir, 'horizontal')` for RTL. - Accordion P1-1/P1-6 dismissed: roving/labelledby already correct per APG. Batch 2 — A33 reactive collections: Select `labelRegistry` and NavigationMenu `triggerRefs` migrated to `SvelteMap`. Batch 3 — A31 O(N²) fix: RadioGroup lifts `hasSelection` + `isFallbackTabStop` to the provider with `untrack`-wrapped DOM walk. Per-item derivations drop from O(N²) to O(1) per trigger. Batch 4 — Morfo aria sync: toast / time-field / date-field / tabs / tag-group / navigation-menu declare every aria the provider emits (14 entries across 6 files). Batch 5 — Accessible naming props: Tabs.List, ToggleGroup, TreeView, NavigationMenu expose `aria-label?` / `aria-labelledby?` with explicit "required unless labelledby is set" JSDoc. Batch 6 — A30 cleanup: Checkbox `setLabelId` and 5 trigger-ref providers (select/tooltip/link-preview/dropdown-menu/context-menu) migrated from `$effect(() => parent.ref = opts.ref)` to the `Provider` base's `onRefChange` callback (fires via attachRef inside `untrack`). Batch 7 — A11 wrapper thinning: stepper/tree-view wrappers removed `ctx.get()`, providers expose proxy derivations; time/date-range-field inputs use `untrack(() => …)` for one-shot id registration; menubar content's 40-line keyboard nav moved to `MenubarMenuProvider.onContentKeydown`; color-picker area + channel-slider pointer state machines moved to the providers with `onpointerdown/move/up` emitted via `props`. form-auto-fields deferred with justification (reflective schema renderer, not a state-holding wrapper). ## Form hang fix (A36) `runValidate` entered the `isPromiseLike` branch even for sync schemas because sium's Standard-Schema adapter declares `validate: async`. The fallback `return { errors, issues }` read both reactive vars inside the validation `$effect` while the `.then(...)` microtask wrote them → microtask-mediated loop, silent to Svelte's sync effect-depth guard. Symptom: kitchen-sink hung the tab on mount with onChange / onBlur. Fix: `return untrack(() => ({ errors, issues }))`. Regression locked by two new 5s-timeout tests in `form-auto-fields.svelte.test.ts`. Pattern codified as A36 in COMPONENT_GUIDE; CLAUDE.md count updated to A1–A36 / 39 items. ## Verification - `npm run check`: 0 errors in soma/morfo scope (sema demo pre-existing errors untouched). - `npx vitest run src/uix/soma src/uix/morfo`: all tests green (94 + 7 = 101 tests, inc. 2 new form regression tests). - `npm run smoke`: 65/65 routes. - `npx tsx scripts/morfo-check.ts`: 66/66 morfos. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
function cloneMorfo(m: typeof dialogMorfo): Morfo {
return structuredClone(m as Morfo) as Morfo
}
morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired Morfo (src/uix/morfo/): machine-readable contract of a component's public DOM surface, shared between soma/sema/eidos. Shape v5 consolidated across three independent AI reviews (Gemini, Grok, ChatGPT) plus Sema alignment. Types, sium-based validator with cross-field invariants, dialog.ts as the first canonical morfo with data-last-action for Sema-causal exits, 7 passing unit tests. createAttrs(morfo) and registerContract(morfo) now consume a Morfo — legacy inline signature removed. Dialog provider refactored to derive parts + data contract from dialogMorfo. The other 65 soma components need porting (svelte-check lists them) — tracked work. Tooling: scripts/smoke-check.mjs — Playwright smoke over 65 soma demo routes scripts/morfo-check.ts — validates emitted DOM vs morfo scripts/morfo-vocabulary-check.ts — canonical vocabulary consistency npm run smoke / morfo:check / morfo:vocabulary WAI-ARIA APG components added (100% APG coverage now): Announce, Avatar, Clipboard, DragDrop, Feed, GridList, Meter, Progress, SearchField, TagGroup, TreeGrid + Table.RowDetail/Trigger (detail panel pattern; hierarchical Table rows deprecated — use TreeGrid instead). air: Link, Banner. Sema prep: src/uix/sema/sema_pre.md documents the semantic layer's DOM requirements. Morfo already provides everything Sema needs (data-last-action patterns, transition markers, enumerable state values, cross-component consistency). Sema implementation deferred. COMPONENT_GUIDE updated: items 37-39 (translation namespace grep, DOM topology vs .require() audit, npm run smoke is part of done) + A34 rule with incident log. WAI-ARIA pattern listed as soma membership criterion. Pagination translation bug fixed (soma.pagination.page -> idlangref PAGINATION_LANGS.PAGE). Table demo namespace fix (soma.table.* -> components.table.*). Avatar demo now offline-safe (SVG data URL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
describe('dialogMorfo', () => {
it('passes shape + invariant validation', () => {
expect(() => validateMorfo(dialogMorfo)).not.toThrow()
})
morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired Morfo (src/uix/morfo/): machine-readable contract of a component's public DOM surface, shared between soma/sema/eidos. Shape v5 consolidated across three independent AI reviews (Gemini, Grok, ChatGPT) plus Sema alignment. Types, sium-based validator with cross-field invariants, dialog.ts as the first canonical morfo with data-last-action for Sema-causal exits, 7 passing unit tests. createAttrs(morfo) and registerContract(morfo) now consume a Morfo — legacy inline signature removed. Dialog provider refactored to derive parts + data contract from dialogMorfo. The other 65 soma components need porting (svelte-check lists them) — tracked work. Tooling: scripts/smoke-check.mjs — Playwright smoke over 65 soma demo routes scripts/morfo-check.ts — validates emitted DOM vs morfo scripts/morfo-vocabulary-check.ts — canonical vocabulary consistency npm run smoke / morfo:check / morfo:vocabulary WAI-ARIA APG components added (100% APG coverage now): Announce, Avatar, Clipboard, DragDrop, Feed, GridList, Meter, Progress, SearchField, TagGroup, TreeGrid + Table.RowDetail/Trigger (detail panel pattern; hierarchical Table rows deprecated — use TreeGrid instead). air: Link, Banner. Sema prep: src/uix/sema/sema_pre.md documents the semantic layer's DOM requirements. Morfo already provides everything Sema needs (data-last-action patterns, transition markers, enumerable state values, cross-component consistency). Sema implementation deferred. COMPONENT_GUIDE updated: items 37-39 (translation namespace grep, DOM topology vs .require() audit, npm run smoke is part of done) + A34 rule with incident log. WAI-ARIA pattern listed as soma membership criterion. Pagination translation bug fixed (soma.pagination.page -> idlangref PAGINATION_LANGS.PAGE). Table demo namespace fix (soma.table.* -> components.table.*). Avatar demo now offline-safe (SVG data URL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
it('declares the 7 parts the provider emits', () => {
const kebabs = dialogMorfo.parts.map((p) => p.kebab).sort()
morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired Morfo (src/uix/morfo/): machine-readable contract of a component's public DOM surface, shared between soma/sema/eidos. Shape v5 consolidated across three independent AI reviews (Gemini, Grok, ChatGPT) plus Sema alignment. Types, sium-based validator with cross-field invariants, dialog.ts as the first canonical morfo with data-last-action for Sema-causal exits, 7 passing unit tests. createAttrs(morfo) and registerContract(morfo) now consume a Morfo — legacy inline signature removed. Dialog provider refactored to derive parts + data contract from dialogMorfo. The other 65 soma components need porting (svelte-check lists them) — tracked work. Tooling: scripts/smoke-check.mjs — Playwright smoke over 65 soma demo routes scripts/morfo-check.ts — validates emitted DOM vs morfo scripts/morfo-vocabulary-check.ts — canonical vocabulary consistency npm run smoke / morfo:check / morfo:vocabulary WAI-ARIA APG components added (100% APG coverage now): Announce, Avatar, Clipboard, DragDrop, Feed, GridList, Meter, Progress, SearchField, TagGroup, TreeGrid + Table.RowDetail/Trigger (detail panel pattern; hierarchical Table rows deprecated — use TreeGrid instead). air: Link, Banner. Sema prep: src/uix/sema/sema_pre.md documents the semantic layer's DOM requirements. Morfo already provides everything Sema needs (data-last-action patterns, transition markers, enumerable state values, cross-component consistency). Sema implementation deferred. COMPONENT_GUIDE updated: items 37-39 (translation namespace grep, DOM topology vs .require() audit, npm run smoke is part of done) + A34 rule with incident log. WAI-ARIA pattern listed as soma membership criterion. Pagination translation bug fixed (soma.pagination.page -> idlangref PAGINATION_LANGS.PAGE). Table demo namespace fix (soma.table.* -> components.table.*). Avatar demo now offline-safe (SVG data URL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
expect(kebabs).toEqual(
['close', 'content', 'description', 'overlay', 'provider', 'title', 'trigger'].sort()
)
})
morfo: cross-layer contract + 14 new WAI-ARIA components + Dialog wired Morfo (src/uix/morfo/): machine-readable contract of a component's public DOM surface, shared between soma/sema/eidos. Shape v5 consolidated across three independent AI reviews (Gemini, Grok, ChatGPT) plus Sema alignment. Types, sium-based validator with cross-field invariants, dialog.ts as the first canonical morfo with data-last-action for Sema-causal exits, 7 passing unit tests. createAttrs(morfo) and registerContract(morfo) now consume a Morfo — legacy inline signature removed. Dialog provider refactored to derive parts + data contract from dialogMorfo. The other 65 soma components need porting (svelte-check lists them) — tracked work. Tooling: scripts/smoke-check.mjs — Playwright smoke over 65 soma demo routes scripts/morfo-check.ts — validates emitted DOM vs morfo scripts/morfo-vocabulary-check.ts — canonical vocabulary consistency npm run smoke / morfo:check / morfo:vocabulary WAI-ARIA APG components added (100% APG coverage now): Announce, Avatar, Clipboard, DragDrop, Feed, GridList, Meter, Progress, SearchField, TagGroup, TreeGrid + Table.RowDetail/Trigger (detail panel pattern; hierarchical Table rows deprecated — use TreeGrid instead). air: Link, Banner. Sema prep: src/uix/sema/sema_pre.md documents the semantic layer's DOM requirements. Morfo already provides everything Sema needs (data-last-action patterns, transition markers, enumerable state values, cross-component consistency). Sema implementation deferred. COMPONENT_GUIDE updated: items 37-39 (translation namespace grep, DOM topology vs .require() audit, npm run smoke is part of done) + A34 rule with incident log. WAI-ARIA pattern listed as soma membership criterion. Pagination translation bug fixed (soma.pagination.page -> idlangref PAGINATION_LANGS.PAGE). Table demo namespace fix (soma.table.* -> components.table.*). Avatar demo now offline-safe (SVG data URL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 months ago
refactor(dialog/drawer/popover): collapse close-* into polymorphic close (book §5.3) Three overlay components each had five `close-*` events (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail) with per-event prewrite arrays setting `data-last-action`. Per the book §5.3 polymorphism canon, collapse to a single `close` event: semantic: { family: 'emerge', verb: 'close', target: ..., sequence: 'pre', persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal'] } NO prewrite — the provider sets data-last-action imperatively before trigger because polymorphic events can't bind a single prewrite per call. **Provider pattern** (Dialog / Drawer / Popover): - New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail } → { lastAction, semantic: { family, verb, intent? } }. - `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })` then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`. - `triggerClose` is now private; previously-public 2-arg signature collapsed into the cause-driven internal helper. - All internal callsites (escape, outside-click, hover-close, close button) migrated to `dismissWith`. - `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion` is set (today only for the 'fail' case), the live region announces it. **Schema validator relaxed** (`src/uix/morfo/schema.ts`): the "every declared value of `data-last-action.values[]` must be prewritten by some event" check was DROPPED — provider-imperative writes can't satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict. **Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`): - `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'` - `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }` **Eidos CSS**: not touched — already reads `data-last-action` directly. **Public API preserved**: `dismissWith` signature + observable behaviour identical. External consumers see no difference. Tests updated for the new event name. 680/680 pass across sema + morfo + soma + adom scopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
it('declares two semantic events: open + polymorphic close', () => {
const events = dialogMorfo.events?.map((event) => event.name).sort()
refactor(dialog/drawer/popover): collapse close-* into polymorphic close (book §5.3) Three overlay components each had five `close-*` events (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail) with per-event prewrite arrays setting `data-last-action`. Per the book §5.3 polymorphism canon, collapse to a single `close` event: semantic: { family: 'emerge', verb: 'close', target: ..., sequence: 'pre', persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal'] } NO prewrite — the provider sets data-last-action imperatively before trigger because polymorphic events can't bind a single prewrite per call. **Provider pattern** (Dialog / Drawer / Popover): - New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail } → { lastAction, semantic: { family, verb, intent? } }. - `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })` then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`. - `triggerClose` is now private; previously-public 2-arg signature collapsed into the cause-driven internal helper. - All internal callsites (escape, outside-click, hover-close, close button) migrated to `dismissWith`. - `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion` is set (today only for the 'fail' case), the live region announces it. **Schema validator relaxed** (`src/uix/morfo/schema.ts`): the "every declared value of `data-last-action.values[]` must be prewritten by some event" check was DROPPED — provider-imperative writes can't satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict. **Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`): - `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'` - `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }` **Eidos CSS**: not touched — already reads `data-last-action` directly. **Public API preserved**: `dismissWith` signature + observable behaviour identical. External consumers see no difference. Tests updated for the new event name. 680/680 pass across sema + morfo + soma + adom scopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
expect(events).toEqual(['close', 'open'].sort())
})
it('declares the close event as polymorphic with allowedFamilies', () => {
const close = dialogMorfo.events?.find((e) => e.name === 'close')!
expect(close).toBeDefined()
// Default family is emerge; allowedFamilies opens commit + signal.
expect('family' in close.semantic ? close.semantic.family : null).toBe('emerge')
expect('allowedFamilies' in close.semantic ? close.semantic.allowedFamilies : null).toEqual([
'emerge',
'commit',
'signal'
])
})
it('declares data-last-action on Content for causal exits', () => {
const content = dialogMorfo.parts.find((p) => p.kebab === 'content')!
const causal = content.data.find((d) => d.attr === 'data-last-action')
expect(causal).toBeDefined()
refactor(dialog/drawer/popover): collapse close-* into polymorphic close (book §5.3) Three overlay components each had five `close-*` events (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail) with per-event prewrite arrays setting `data-last-action`. Per the book §5.3 polymorphism canon, collapse to a single `close` event: semantic: { family: 'emerge', verb: 'close', target: ..., sequence: 'pre', persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal'] } NO prewrite — the provider sets data-last-action imperatively before trigger because polymorphic events can't bind a single prewrite per call. **Provider pattern** (Dialog / Drawer / Popover): - New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail } → { lastAction, semantic: { family, verb, intent? } }. - `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })` then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`. - `triggerClose` is now private; previously-public 2-arg signature collapsed into the cause-driven internal helper. - All internal callsites (escape, outside-click, hover-close, close button) migrated to `dismissWith`. - `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion` is set (today only for the 'fail' case), the live region announces it. **Schema validator relaxed** (`src/uix/morfo/schema.ts`): the "every declared value of `data-last-action.values[]` must be prewritten by some event" check was DROPPED — provider-imperative writes can't satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict. **Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`): - `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'` - `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }` **Eidos CSS**: not touched — already reads `data-last-action` directly. **Public API preserved**: `dismissWith` signature + observable behaviour identical. External consumers see no difference. Tests updated for the new event name. 680/680 pass across sema + morfo + soma + adom scopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
// Values stay declared for eidos / docs / contract narrowing even
// though they're now set imperatively by the provider — see book
// §5.3 polymorphism rationale on the morfo's `close` event.
expect(causal!.values).toContain('saved')
expect(causal!.values).toContain('cancelled')
})
it('can declare runtime sources for data-* attrs', () => {
const trigger = dialogMorfo.parts.find((p) => p.kebab === 'trigger')!
const state = trigger.data.find((d) => d.attr === 'data-state')
expect(state?.value).toEqual({ kind: 'stateRef', state: 'open' })
})
it('fails validation when a partRef targets a non-existent kebab', () => {
const broken = cloneMorfo(dialogMorfo)
const trigger = broken.parts.find((p) => p.kebab === 'trigger')!
const controls = trigger.aria.find((a) => a.attr === 'aria-controls')!
;(controls.value as { kind: 'partRef'; target: string }).target = 'nonexistent-part'
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError)
})
it('fails validation when stateRef refers to a state not declared in the part', () => {
const broken = cloneMorfo(dialogMorfo)
const trigger = broken.parts.find((p) => p.kebab === 'trigger')!
const expanded = trigger.aria.find((a) => a.attr === 'aria-expanded')!
;(expanded.value as { kind: 'stateRef'; state: string }).state = 'nonexistent-state'
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError)
})
it('fails validation when data.value.stateRef refers to an undeclared state', () => {
const broken = cloneMorfo(dialogMorfo)
const trigger = broken.parts.find((p) => p.kebab === 'trigger')!
const state = trigger.data.find((d) => d.attr === 'data-state')!
;(state.value as { kind: 'stateRef'; state: string }).state = 'nonexistent-state'
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError)
})
it('fails validation when two parts share the same kebab', () => {
const broken = cloneMorfo(dialogMorfo)
;(broken.parts as unknown as { kebab: string }[])[1].kebab = 'content'
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError)
})
it('fails validation with empty scope', () => {
const broken = cloneMorfo(dialogMorfo)
;(broken as unknown as { scope: [] }).scope = []
expect(() => validateMorfo(broken)).toThrow(MorfoInvariantError)
})
it('fails when an event targets a non-existent part', () => {
const broken = cloneMorfo(dialogMorfo)
sema/morfo/eidos: align with GUIA_IMPLEMENTACION_SEMAUIX (canonical guide) The new doctrinal guide at src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md supersedes src/docs/sema-implementation-guide.md and locks the canonical vocabulary, event shape and color tokens. This commit aligns the codebase to that canon in three concrete moves. Phase 1 — sema/verbs.ts vocabulary: - Restructured from flat array to family-keyed Record per the guide. - Moved select / toggle / acknowledge → commit (these fix state, not contact). Removed edit from handle (it's shift.enter-mode now). - Added new verbs per the canon: contact (tap, focus, trigger, release), commit (complete, restore, expire, set, remove, reorder), signal (remind), handle (rotate, scroll, reorder), shift (route, step, return, context), sustain (start, loading, waiting, syncing, processing, streaming, pending, retrying, end). - validateEventName now recognises both naming shapes: {verb}-{variant} (head=verb) and {family}-{verb} (head=family). - New helper familyForVerb. Tests rewritten — 16/16 green. Phase 2 — morfo event shape: - target moved from event-level into event.semantic. Adds optional semantic.verb (advisory, validated against SEMA_VERBS[family]) and optional semantic.sequence ('pre' | 'coincident' | 'post'). - compile.ts and schema.ts updated. The 22 events across toggle, toast, popover, drawer, dialog refactored to the new shape with explicit verb + sequence. - Polymorphic capability (allowedFamilies + defaultSemantic) and a11ySemantic deferred — no current consumer needs them; will land when the first concrete usage appears. Phase 5 — color tokens (doctrinal 8): - themes/base/light.css and dark.css rewritten to expose primary, secondary, neutral, affirm, fulfill, risk, threat, loss. Renames success→fulfill, warning→risk, danger→threat (same hex). New primitives for secondary (slate-blue), affirm (teal-mint, low activation), loss (deep violet-grave, posterior). info palette deleted entirely — per guide, "info no es intent, es signal.announce + neutral". - _static.css legacy compatibility aliases (--color-bg, --color-success, --color-error, --color-accent, ...) deleted. Clean cut, no transition. Consumers reference doctrinal palettes directly. - 29 consumers (token files + recipes) migrated: - All --*-color-info-* declarations dropped. - Component palettes extended with secondary / affirm / loss. - switch + checkbox recipes updated to use [data-color='affirm'| 'risk'|'threat'|'secondary'] (was 'success'|'warning'|'danger'| 'info'). - toast tokens + recipe renamed to affirm/fulfill/risk/threat/loss (was success/error/warning/info). - meter, progress, mark, badge, button tokens renamed. - Contracts (contracts/contract.css + contracts/components/*) updated. - focus-ring-color-error in themes now points to --color-risk-border (validation errors are signal.warn + risk per doctrina, not threat). Phase 4 (partial): - src/docs/sema-implementation-guide.md deleted (superseded by the canonical guide). Tests: 217/217 green across uix. npm run check holds at the 39-error baseline (all pre-existing in unrelated test files; none introduced). Pending in follow-up commits: - Update active_architecture.md, sema/README.md, morfo/README.md, CLAUDE.md to reflect the canon. - Switch component migration to the eidos wrapper pattern. - Persistence field (untilAction / untilFix / stateBound) and holds-by-intent — defer until the first signal.warn / signal.alert consumer appears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
;(broken.events as MorfoEvent[])[0].semantic.target.target = 'no-such-part'
expect(() => validateMorfo(broken)).toThrow(/targets unknown part/)
})
it('fails when a prewrite attr is not declared on the target part', () => {
const broken = cloneMorfo(dialogMorfo)
refactor(dialog/drawer/popover): collapse close-* into polymorphic close (book §5.3) Three overlay components each had five `close-*` events (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail) with per-event prewrite arrays setting `data-last-action`. Per the book §5.3 polymorphism canon, collapse to a single `close` event: semantic: { family: 'emerge', verb: 'close', target: ..., sequence: 'pre', persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal'] } NO prewrite — the provider sets data-last-action imperatively before trigger because polymorphic events can't bind a single prewrite per call. **Provider pattern** (Dialog / Drawer / Popover): - New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail } → { lastAction, semantic: { family, verb, intent? } }. - `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })` then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`. - `triggerClose` is now private; previously-public 2-arg signature collapsed into the cause-driven internal helper. - All internal callsites (escape, outside-click, hover-close, close button) migrated to `dismissWith`. - `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion` is set (today only for the 'fail' case), the live region announces it. **Schema validator relaxed** (`src/uix/morfo/schema.ts`): the "every declared value of `data-last-action.values[]` must be prewritten by some event" check was DROPPED — provider-imperative writes can't satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict. **Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`): - `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'` - `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }` **Eidos CSS**: not touched — already reads `data-last-action` directly. **Public API preserved**: `dismissWith` signature + observable behaviour identical. External consumers see no difference. Tests updated for the new event name. 680/680 pass across sema + morfo + soma + adom scopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const close = (broken.events as MorfoEvent[]).find((event) => event.name === 'close')!
// Synthesize a prewrite for the test — the production morfo has
// none on `close` (provider sets data-last-action imperatively).
close.prewrite = [{ part: close.semantic.target, attr: 'data-bogus', value: 'saved' }]
expect(() => validateMorfo(broken)).toThrow(/data-bogus.*is not declared/)
})
it('fails when a prewrite writes a value outside the declared enum', () => {
const broken = cloneMorfo(dialogMorfo)
refactor(dialog/drawer/popover): collapse close-* into polymorphic close (book §5.3) Three overlay components each had five `close-*` events (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail) with per-event prewrite arrays setting `data-last-action`. Per the book §5.3 polymorphism canon, collapse to a single `close` event: semantic: { family: 'emerge', verb: 'close', target: ..., sequence: 'pre', persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal'] } NO prewrite — the provider sets data-last-action imperatively before trigger because polymorphic events can't bind a single prewrite per call. **Provider pattern** (Dialog / Drawer / Popover): - New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail } → { lastAction, semantic: { family, verb, intent? } }. - `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })` then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`. - `triggerClose` is now private; previously-public 2-arg signature collapsed into the cause-driven internal helper. - All internal callsites (escape, outside-click, hover-close, close button) migrated to `dismissWith`. - `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion` is set (today only for the 'fail' case), the live region announces it. **Schema validator relaxed** (`src/uix/morfo/schema.ts`): the "every declared value of `data-last-action.values[]` must be prewritten by some event" check was DROPPED — provider-imperative writes can't satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict. **Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`): - `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'` - `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }` **Eidos CSS**: not touched — already reads `data-last-action` directly. **Public API preserved**: `dismissWith` signature + observable behaviour identical. External consumers see no difference. Tests updated for the new event name. 680/680 pass across sema + morfo + soma + adom scopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const close = (broken.events as MorfoEvent[]).find((event) => event.name === 'close')!
close.prewrite = [
{ part: close.semantic.target, attr: 'data-last-action', value: 'not-in-enum' }
]
expect(() => validateMorfo(broken)).toThrow(/not-in-enum.*is not declared/)
})
it('fails when an event commits a non-existent state', () => {
const broken = cloneMorfo(dialogMorfo)
refactor(dialog/drawer/popover): collapse close-* into polymorphic close (book §5.3) Three overlay components each had five `close-*` events (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail) with per-event prewrite arrays setting `data-last-action`. Per the book §5.3 polymorphism canon, collapse to a single `close` event: semantic: { family: 'emerge', verb: 'close', target: ..., sequence: 'pre', persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal'] } NO prewrite — the provider sets data-last-action imperatively before trigger because polymorphic events can't bind a single prewrite per call. **Provider pattern** (Dialog / Drawer / Popover): - New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail } → { lastAction, semantic: { family, verb, intent? } }. - `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })` then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`. - `triggerClose` is now private; previously-public 2-arg signature collapsed into the cause-driven internal helper. - All internal callsites (escape, outside-click, hover-close, close button) migrated to `dismissWith`. - `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion` is set (today only for the 'fail' case), the live region announces it. **Schema validator relaxed** (`src/uix/morfo/schema.ts`): the "every declared value of `data-last-action.values[]` must be prewritten by some event" check was DROPPED — provider-imperative writes can't satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict. **Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`): - `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'` - `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }` **Eidos CSS**: not touched — already reads `data-last-action` directly. **Public API preserved**: `dismissWith` signature + observable behaviour identical. External consumers see no difference. Tests updated for the new event name. 680/680 pass across sema + morfo + soma + adom scopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const action = (broken.events as MorfoEvent[]).find((event) => event.name === 'close')!
if (!action.commits) throw new Error('close commits missing')
action.commits.value = 'zombied'
expect(() => validateMorfo(broken)).toThrow(/zombied.*not declared/)
})
it('fails when two events share the same name', () => {
const broken = cloneMorfo(dialogMorfo)
;(broken.events as MorfoEvent[])[1].name = 'open'
expect(() => validateMorfo(broken)).toThrow(/duplicate event name "open"/)
})
refactor(dialog/drawer/popover): collapse close-* into polymorphic close (book §5.3) Three overlay components each had five `close-*` events (close-save / close-cancel / close-dismiss / close-dismiss-outside / close-after-fail) with per-event prewrite arrays setting `data-last-action`. Per the book §5.3 polymorphism canon, collapse to a single `close` event: semantic: { family: 'emerge', verb: 'close', target: ..., sequence: 'pre', persistence: 'transient', allowedFamilies: ['emerge', 'commit', 'signal'] } NO prewrite — the provider sets data-last-action imperatively before trigger because polymorphic events can't bind a single prewrite per call. **Provider pattern** (Dialog / Drawer / Popover): - New `DISMISS_CAUSES` map: { save, cancel, dismiss, dismiss-outside, fail } → { lastAction, semantic: { family, verb, intent? } }. - `dismissWith(action, opts?)` does `dom.apply({ 'data-last-action': cause.lastAction })` then `runtime.trigger('close', { fallbackTarget, semantic: cause.semantic, message })`. - `triggerClose` is now private; previously-public 2-arg signature collapsed into the cause-driven internal helper. - All internal callsites (escape, outside-click, hover-close, close button) migrated to `dismissWith`. - `opts.message` forwarded to runtime — when `a11ySemantic.requiresLiveRegion` is set (today only for the 'fail' case), the live region announces it. **Schema validator relaxed** (`src/uix/morfo/schema.ts`): the "every declared value of `data-last-action.values[]` must be prewritten by some event" check was DROPPED — provider-imperative writes can't satisfy it. The reverse direction (`prewritten ⊆ declared`) stays strict. **Cascade rewrite** (`src/uix/sema/components/{dialog,drawer,popover}.ts`): - `eventNamePrefix: 'close-'` → `eventName: 'close', eventFamily: 'emerge'` - `eventName: 'close-dismiss-outside'` → `eventName: 'close', state: { attr: 'data-last-action', value: 'dismissed-outside' }` **Eidos CSS**: not touched — already reads `data-last-action` directly. **Public API preserved**: `dismissWith` signature + observable behaviour identical. External consumers see no difference. Tests updated for the new event name. 680/680 pass across sema + morfo + soma + adom scopes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
// NOTE: the previous bidirectional check ("data-last-action declares a
// value that no event prewrites") was relaxed in the polymorphism
// refactor — the provider now sets data-last-action imperatively for
// the polymorphic close event. See `validateMorfo` in `schema.ts` for
// the rationale and the remaining one-directional invariant (prewritten
// ⊆ declared) that still holds.
})

Powered by TurnKey Linux.