Toggle as the eidos pilot: subdirectory layout (recipe + Svelte wrapper +
types + index + README) replacing the flat CSS-only form. Pattern is
documented in eidos/components/README.md and the toggle README.
Shared types live in eidos/lib/types.ts. First export is `Size` (8 values
xxs..xxl + full); components narrow with `Extract<Size, ...>` per the
per-component-subset doctrine. No `Eidos` prefix on types — module path
already conveys the layer.
API doctrine:
- soma stays compound (Toggle.Provider) for symmetry with multi-part
- eidos exports both default + Provider so single-part components
accept both `<Toggle>` (ergonomic) and `<Toggle.Provider>`
(compound-style consumers)
SoundChannel eager-init fixes the autoplay race: AudioContext is created
+ resumed synchronously on the first user gesture (capture-phase
listener registered in the constructor), avoiding the previous race
where the first emit() scheduled the resume in a microtask outside the
gesture window.
Demo page (web/routes/toggle/+page.svelte) restructured so the live
preview renders ALWAYS above the tablist — Sema-tab Play buttons can
fire on the real toggle. Motion preview amplifies scale ×8 visually
only; doctrinal values stay in the <dl>.
Conventions 7-13 added to src/docs/sema-implementation-guide.md
covering: directory structure, wrapper composition, no Eidos prefix,
soma compound vs eidos flat, iconOnly sr-only body, sound eager-init,
docs-preview amplification.
CLAUDE.md gets a session hand-off block listing where things stand and
next concrete steps (migrate switch/collapsible/dialog/drawer/popover/
toast/avatar; wire topbar sound mute to masterGain; rename theme
tokens to drop the success/warning/danger fallback aliases).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First demos under the new web/routes location (configured by phase C
of the cleanup as `kit.files.routes`). One demo per component:
toggle, switch, collapsible, avatar, dialog, drawer, popover, toast.
Layout (web/routes/+layout.svelte):
- Bootstraps the UIX via createActiveUix({ lang: { schema: {},
defaultLocale: 'es' } }) — minimal config, full UIX surface.
- Wraps in <Uix> which sets the active-uix context and creates the
Soma scope so providers can require it.
- Imports the eidos index.css so the visual layer is applied.
- Tiny demo-only stylesheet (web/routes/layout.css) for the page
chrome (topbar, demo grid, sections) — explicitly NOT part of
eidos.
Each demo page exercises the public API:
- toggle: bound, initially-pressed, disabled
- switch: bound, disabled
- collapsible: bound show/hide, disabled trigger
- avatar: loaded, error fallback, delayed
- dialog: basic with overlay + focus trap + escape dismissal
- drawer: right / left / bottom (with handle)
- popover: click + hover trigger
- toast: 4 intents (fulfill, risk, threat, neutral) via createToaster
Verified end-to-end with scripts/demo-probe.ts (Playwright):
- All 9 routes (landing + 8 demos) serve 200.
- Zero console errors except avatar's pravatar.cc network failure
(sandbox limitation, not a real bug).
- Interactive checks: dialog open shows modal + overlay backdrop +
focused Cancel button; popover open positions floating content
next to trigger; toast Fulfill shows the green-tinted shadow
driven by data-event-intent='fulfill' from sema's visual channel
reacting to the morfo's announce event semantics — confirms the
full morfo → sema → eidos pipeline works end-to-end on a migrated
soma component.
scripts/demo-probe.ts: small Playwright runner that loads each route,
captures console errors, takes a screenshot of the static state, then
clicks the trigger on dialog/collapsible/popover/toast for a second
"open" screenshot. Output goes to g:/tmp/demos/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consumes `compileMorfo(morfo).contracts.cssSelectors` to classify every
`[data-*]` selector in an eidos CSS file:
- morfo-backed: the runtime writes the attr; eidos reacts to it.
- eidos-only: the marker is present (`[data-toggle]`) but at least
one `data-*` is not declared on the morfo part. Legitimate design
tokens (`data-color`, `data-size`, `data-variant`) and drift cases
(`data-dialog-header` — a part the morfo no longer declares) both
land here; the lint tool reports them so authors can decide.
- invalid: the attr IS declared on the morfo, but with a value
outside the declared enum. Bug.
Also reports `unusedFromContract`: selectors the compiler promises
that no eidos rule consumes — the inverse direction of drift.
Two CLI entry points:
- `scripts/eidos-lint.ts <component>` — single-component report
- `scripts/eidos-lint-all.ts` — sweep across every eidos component
Initial sweep finds 0 invalid, 134 eidos-only, 82 unused contract
selectors across 11 components. Real drift hotspots: dialog/drawer
`header`/`footer` (eidos parts the morfos don't declare), popover's
`data-side × data-last-action` compounds, tooltip's
`data-state='delayed-open'`. Deliberate left as-is for the user to
triage — fixes belong in a separate commit.
Tests cover toggle (clean baseline), dialog (drift detector for
header/footer + dead `data-disabled`), and a synthetic invalid case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inject a MorfoRuntime side-channel in DialogProvider that fires the
prewrite → semantic.emit → handler cadence for the close-* events
declared in the dialog morfo. The legacy provider machinery
(assertProps + resolveMorfoProps + Provider class hierarchy) stays —
runtime is alongside, not replacing.
Wiring:
- DialogProvider creates the runtime in its constructor with
`events.{close-save, close-cancel, close-dismiss,
close-dismiss-outside, close-after-fail}` all delegating to
handleClose(). The runtime gets dom + semantic from soma + state
sources for `open` + prop sources for `disabled`/`modal`.
- DialogContentProvider registers the content part with the runtime
when its constructor runs, so trigger() can resolve
`event.target = partRef('content')` to a real DOM node.
- DialogTriggerProvider.onclick keeps direct handleOpen() — the
'open' event in the morfo targets content, but content isn't
in the DOM until AFTER open flips. Chicken-and-egg.
- DialogCloseProvider.onclick → trigger('close-cancel').
- DialogContentProvider's dismissal callbacks:
onEscapeKeydown → trigger('close-dismiss')
onInteractOutside → trigger('close-dismiss-outside')
What this unlocks:
Each close path now writes `data-last-action` (saved/cancelled/
dismissed/dismissed-outside/failed) BEFORE flipping data-state,
visible during the exit animation. The dialog recipe's tint rules
in eidos finally fire in vivo.
Verification (playwright check-eidos-recipes):
@8ms post-click lastAction=cancelled state=open (prewrite)
@241ms lastAction=cancelled state=open (signal hold)
@378ms lastAction=cancelled state=closed (exit)
@493ms opacity=0.017 (exiting)
The 240ms wait between click and exit is sema's family='emerge' hold.
The user can override per-event via `morfo.events[].hold` if 240ms feels
too long for a dialog cancel. Smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the V1.5 hand-rolled motion/easing/tint/opacity/focus-ring
tokens with the air-translated equivalents already in eidos/themes/
base/_static.css and eidos/themes/base/light.css.
Substitutions applied to archetypes.css, events.css, components/
dialog.css:
--eidos-motion-fast → --eidos-duration-fast (120ms)
--eidos-motion-base → --eidos-duration-slow (320ms)
--eidos-easing-standard → --eidos-ease-default
--eidos-easing-decelerate → --eidos-ease-out
--eidos-easing-emphasized → --eidos-ease-spring
--eidos-tint-neutral → --eidos-color-neutral-element
--eidos-tint-affirm → --eidos-color-success-element
--eidos-tint-fulfill → --eidos-color-success-element
--eidos-tint-risk → --eidos-color-warning-element
--eidos-tint-threat → --eidos-color-danger-element
var(--eidos-opacity-hover) → 0.85 (single-site, archetype-internal)
var(--eidos-opacity-disabled) → 0.5 (single-site, archetype-internal)
Focus ring tokens (--eidos-focus-ring-{color,width,offset}) unchanged
— same names already exist in air's _static.css/light.css.
`affirm` and `fulfill` collapse to the same `success-element` because
air's intent vocabulary doesn't distinguish them. The morfo's
data-intent attr still preserves the distinction at the structural
layer; the visual mapping is just the same color.
Updated scripts/check-eidos-recipes.ts to navigate to the per-component
demo URLs (/test/eidos/{toggle,switch,dialog}) since the index page
no longer renders the recipes directly.
Verification: smoke 68/68, morfo-check 66/66. Playwright entrance/exit
curves intact (opacity interpolates over the configured durations).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the single-point waitForTimeout snapshots with a rAF sampler
that records opacity at every frame for 500ms. Lets the report show
the actual easing curve, not just one ambiguous data point.
Output now includes the full entrance + exit timing per frame, so a
glance at the log tells whether the configured transition duration is
respected end-to-end (no premature unmount, no clipped curve).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recipe was missing transitions on the Presence-layer markers
(data-starting-style / data-ending-style). The dialog snapped into
view and snapped out — duration 0ms is sub-perceptual.
Add fade + scale on Content (320ms = --eidos-motion-base = "brief")
and fade on Overlay. Both interpolate via the Presence lifecycle:
open → element mounts with [data-starting-style] briefly →
CSS shows opacity:0 + scale(0.96) →
marker removed next rAF →
transition runs to opacity:1 + scale(1)
close → [data-ending-style] applied →
CSS animates opacity:0 + scale(0.96) →
Presence waits getAnimations().finished →
element unmounts
Verification (Playwright):
@8ms entrance: opacity 0.028, scale 0.972 (interpolating)
@80ms exit: opacity 0.46, data-ending-style still present
Both opacity AND transform animate so getAnimations().finished has
something to wait for in either direction. Easings: standard for
opacity, decelerate for transform (pops in cleaner).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dialog overlay self-positions
Two bugs surfaced by Playwright verification at /test/eidos:
1) Focus ring: archetypes.css used `outline: ...` for the focus-visible
ring. The project's layout.css has a Tailwind layer/components rule
`*:not(body):not(.focus-override) { outline: none !important }` that
kills every outline. The eidos rule was never visible.
Switch to a two-layer box-shadow ring (inner transparent gap +
outer color). Box-shadow follows border-radius, so the ring also
looks right on pills (Switch) and rounded corners (Toggle, Dialog
buttons). Ring also stands out against accent-filled states because
it sits OUTSIDE the element, not on top of it.
2) Dialog overlay: the recipe relied on [data-archetype='overlay']
from archetypes.css for `position: fixed; inset: 0`, but Dialog isn't
migrated to MorfoRuntime yet — its provider uses legacy createAttrs
which doesn't read morfo's `archetype` field. The overlay element
had `position: static, height: 0`, so the backdrop was invisible.
Inline `position: fixed; inset: 0` directly in the recipe. Component
recipes shouldn't depend on archetype rules for foundational behavior;
archetype is a transversal extra, not a foundation.
Adds scripts/check-eidos-recipes.ts — Playwright script that opens
/test/eidos, tabs into Toggle/Switch, opens the dialog, prints
computed styles + saves screenshots to g:/tmp/eidos. Used to catch
both bugs; reproducible for future visual checks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bulk classification pass that gives every part a cross-component archetype
where one applies. Done with `scripts/catalog-archetypes.ts` — a one-shot
mechanical mapping based on a kebab → archetype table, plus three manual
overrides for components where Provider IS the interactive element.
Coverage
- 440 part declarations examined.
- ~265 received an archetype (provider, trigger, content, overlay, viewport,
item, indicator, thumb, track, label, title, description, close, action,
header, image, fallback, arrow, separator, group, input, segment).
- ~175 left without one — genuinely component-specific (Slider.Range,
PinInput.Segment internals, color-picker channel-slider, etc.).
Manual overrides (Provider IS the trigger)
- Toggle, Switch, Checkbox: their `Provider` part is a `<button>` that
the user actually interacts with, not a root container. Archetype set to
`'trigger'` instead of the default `'provider'` so eidos can style them
alongside Dialog.Trigger / Popover.Trigger / etc.
Script (scripts/catalog-archetypes.ts)
- Walks every `*.ts` under `src/uix/morfo/components/`.
- Inserts `archetype: '...'` directly after the matching `kebab:` line when
a mapping exists.
- Idempotent — skips parts that already declare an archetype.
- Indent-aware — only operates on part kebabs (≥2 tabs), never the morfo
root kebab. Bug discovered + fixed mid-pass when radio-group's root
kebab `'radio-group'` matched the `'radio-group' → 'group'` alias and
inserted at the wrong level (cleaned up by hand).
- Reports per-file counts + the list of unmapped kebabs at the end so
future passes can extend the mapping.
Tests
- Updated `omits data-archetype when not declared` test to use a tiny
inline morfo (Toggle's Provider now has archetype 'trigger', so it can't
be the "no archetype" fixture anymore).
Verification
- 22/22 runtime tests, full focused suite green.
- 66/66 morfo-check (every component still validates against its DOM —
archetype is additive, doesn't change emitted attrs apart from the new
`data-archetype="..."` which morfo-check doesn't enforce yet).
- 68/68 smoke routes.
- svelte-check unchanged at 155.
One-shot Playwright probe that drives the live Toast demo, installs a
MutationObserver on the toast item, clicks dismiss, and reports the full
attribute timeline.
Used to confirm that `runtime.trigger('dismiss')` actually fires the
`data-event*` perceptual signal in the browser. The signal lives ~13ms
in the DOM (1 awaited rAF + 1 hold frame + cleanup), which is too brief
for DevTools' attribute panel to refresh — this probe captures the full
sequence with millisecond timestamps so the contract is observable.
Run:
1. `npm run dev`
2. `node --import tsx/esm scripts/probe-toast-dismiss.ts`
Output ends with "Signal observed: ✅ YES" when the runtime is wired
correctly. Useful as a regression probe after touching the trigger
sequence or the Toast wiring.
Bulk-instruments the remaining 49 demo pages with `<meta name="perm-axes"
content="dir" />`, bringing total coverage from 15/66 demos to 63/66.
Only checkbox, radio-group, and switch remain skipped — they have no
demo directory at `/test/soma/` (pre-existing gap, unrelated).
## Runner change
`scripts/permutation-check.ts` now allows meta-only demos (no
`data-perm-step` annotations) to participate: if a demo declares any
axis via the meta tag, the runner still visits its baseline + RTL
variants and runs the `[initial seed]` morfo validation at each, even
without a click cycle. This gives passive LTR/RTL coverage to demos
where a click-driven perm sequence doesn't apply naturally (leaf
components like Avatar, Progress, Meter; grid/list components; pickers
with inline controls).
Key change in `probeDemo()`: a demo is "instrumented" if EITHER the meta
tag OR at least one `data-perm-step` is present. Step-only demos get
the v1 click cycle at baseline; meta-only demos get the v3 axis matrix
with passive `[initial seed]` validation at each variant; declaring
both is the full treatment.
## Bulk instrumentation
For each of the 49 remaining demos, added:
```svelte
<svelte:head>
<title>… · Soma</title>
<meta name="perm-axes" content="dir" />
</svelte:head>
```
Baseline axis (`dir`) only. Demos that benefit from additional axes
(`orientation`, `disabled`, etc.) opt in incrementally — the bulk edit
doesn't touch their `$state` seeds, only adds the meta tag.
## Coverage delta
| Metric | Before | After |
|--------|-------:|------:|
| Instrumented demos | 15/66 | **63/66** |
| URL variants | 35 | **131** |
| Step validations | 106 | **202** |
~4× increase in URL variants visited per CI run. Most new demos
contribute 2 variants each (ltr + rtl), validating morfo under direction
flip. Fully-instrumented demos (tabs, toolbar, toggle-group, listbox)
still contribute 3-4 variants with click cycles.
## Deferred
- `context-menu` — right-click activation needs a `data-perm-mode='contextmenu'`
runner extension.
- `navigation-menu` — hover-driven, same as Tooltip. Tooltip was
instrumented via `data-perm-mode="focus"` + long settle; NavigationMenu
could follow the same pattern in a targeted batch.
- `color-picker` — complex pointer state machine on Area / ChannelSlider.
Meta-only covers the baseline structure.
## Verification
- `npm run check`: 0 errors in soma/morfo/test-route scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 63/63 instrumented demos · 202 steps · 131 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v2 ran every demo under a hard-coded `ltr` + `rtl` matrix. v3 lets each
demo declare which additional axes it honours — `orientation`, `disabled`,
`readonly`, `invalid`, `loading` — and the runner adds ONE variant per
declared axis (axis-at-a-time, not cartesian, so the matrix stays bounded
at O(N) instead of O(2^N)).
## Convention
```svelte
<svelte:head>
<meta name="perm-axes" content="dir,orientation,disabled" />
</svelte:head>
```
`dir` is universal (always runs). The other axes are opt-in: they only
produce variants when the demo both lists them in the meta AND wires the
corresponding state from `Perms.getOr(...)`. Without the meta, only
`ltr` + `rtl` run.
## Runner changes
- New `discoverAxes(base, morfoKebab)` pass: loads the baseline URL, reads
`<meta name="perm-axes">`, returns the declared axis list.
- `buildVariants(declaredAxes)` composes `ltr` + `rtl` + one `?perm.X=Y`
variant per declared axis from the `AXIS_FLIP` map.
- `[initial seed]` step validates morfo at each URL before any click,
so URL-seeded state alone is exercised (catches seed regressions even
when the click cycle never fires).
## Demo opt-in (this commit)
- `dialog` — `perm-axes="dir"` (no orientation / disabled / loading axes
apply to Dialog).
- `tabs` — `perm-axes="dir,orientation,disabled"` + wires `orientation` +
`disabled` to the first `Tabs.Provider`.
- `toolbar` — `perm-axes="dir,orientation"` + wires `orientation` to the
first `Toolbar.Provider`.
Result: 3 demos, 9 URL variants, 34 step validations per CI run
(v2 was 22 steps across 6 variants).
## Docs
`src/uix/morfo/PERMUTATION_RUNNER.md` v3 section: supported axis table,
3-step opt-in recipe, roadmap updated (v4 = morfo-inferred value cycling,
v5 = MutationObserver ordering for Sema).
## Verification
- `npm run check`: 0 errors in soma/morfo/test-route scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 3/3 demos · 34 steps · 9 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1 shipped click-based state cycles. v2 adds URL-seeded initial state so
the runner can reach any state the component exposes without walking a
click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL)
to catch direction-specific regressions like the slider thumb transform
that landed this week.
## New test-route infrastructure
- `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query
params into a typed `Perms` record (naive coercion: `'true'` → boolean,
digits → number, else string). Exposed via `Perms.getOr({})` context.
Underscore prefix so SvelteKit does not route it.
- `+layout.svelte` — parses perms once at mount, sets context, honours
`?perm.dir=rtl` and `?perm.locale=en` by routing through
`App.setDir` / `App.setLocale` so the existing control strip stays in
sync.
## Demo opt-in
Three demos seed initial state from URL params (v1 + v2 combined):
- `dialog` — `?perm.open=true` lands open.
- `tabs` — `?perm.value=tab-2` lands on that tab.
- `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect
toggles.
## Runner extension
`scripts/permutation-check.ts` now runs each instrumented demo under a
matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the
URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then
runs the v1 click cycle. Output format updated to show per-variant
blocks.
Result: 3 instrumented demos × 2 variants = 22 step validations per CI
run. RTL seeding reproduces the specific state machine the slider
`translate(50%, -50%)` bug needed to fail — had v2 been in place that
day, the regression would have fired before shipping.
## Docs
`src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout
plumbing, demo opt-in, URL variant table, `[initial seed]` step
semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred
per-component axis matrix) and v4 (MutationObserver ordering for Sema)
remain on the roadmap.
## Verification
- `npm run check`: 0 errors in soma/morfo/test-route scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 3/3 demos · 22 steps across 2 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Why
`morfo:check` validates single-state DOM at page load; `smoke` catches
hydration-time errors. Neither ejercices state TRANSITIONS — and that is
where every reactivity bug we caught this week actually lived:
- toolbar A35 loop (mount-time, not caught by either)
- form A36 microtask loop (mount-time with onChange)
- slider RTL transform (static value, off-by-thumb-width)
All passed the existing CI. All would have failed a "click, re-validate"
pass.
## What
- **`scripts/permutation-check.ts`** — Playwright runner. For each demo,
collects `data-perm-step="N"` elements, executes them in ascending
order (re-discovering after each step so portal-gated controls get
counted), and re-validates the component's morfo + captures any
`pageerror` / `console.error` between steps.
- **`data-perm-*` authoring convention** — step index, human label, mode
(click / focus / type / key), settle delay, skip-validate flag. Demos
opt in incrementally; the runner SKIPs annotation-less pages without
failing.
- **`src/uix/morfo/PERMUTATION_RUNNER.md`** — full design doc covering
what it validates, the annotation convention, example, failure format,
and v2–v4 roadmap (URL-driven states → morfo-inferred cycles →
MutationObserver ordering for Sema).
- **COMPONENT_GUIDE A37 + checklist item 40** — instrumentation is now a
ship-gate rule; doc lists coverage targets (overlays / toggleables /
composite roving / forms / RTL) and explicit skips (alerts, file
pickers).
- **`npm run perm:check`** — pipeline entry, exit codes 0/1/2
parallelling `morfo:check`.
## Instrumented demos (v1 seed)
- dialog — 2 steps: open via trigger → close via Close button. Step 1 is
inside `{#if open}` + Portal; runner's dynamic re-discovery handles it.
- tabs — 3 steps: initial tab 1 → switch to tab 2 → switch to tab 3.
- toolbar — 3 steps: toggle bold / italic / underline GroupItems
(exercises the A35 pattern that previously looped).
Result: 8/8 permutations pass; 63 demos SKIPPED pending instrumentation.
## Verification
- `npm run check`: 0 errors in soma/morfo/scripts scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 3/3 instrumented demos, 8/8 permutations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Naming coherence across the part layer: the root part's `kebab` field now
matches its `name: 'Provider'`. The `createAttrs` special case still emits
`data-{component}` (no suffix) for the provider part — the DOM contract is
unchanged, only the authoring name changes.
Affected:
- createAttrs special-case: 'root' → 'provider'
- 66 morfos: kebab 'root' → 'provider'
- 77 soma provider files: `attrs.root` → `attrs.provider`, constructor
part arg 'root' → 'provider'
- Docs: A2 rule rewritten, morfo/README, soma/README example updates
- Test / schema / morfo-check: updated for new kebab
Toolbar fix: remove `itemsVersion` counter and per-item `$effect` blocks
that called `notifyItemsChanged()`. The pattern was an A30 violation —
each item's $effect read `ref.current`/`disabled` and wrote back to the
provider, which cascaded through `firstTabStop` → per-item `props`
$derived → DOM reattach → effect re-run, triggering
`effect_update_depth_exceeded` on page load. `isTabStop` now uses
`untrack` around the DOM walk so per-item `props` derivations only
depend on `lastFocusedElement`.
Verification: 7/7 morfo tests, 65/65 smoke, 66/66 morfo-check, no effect
loops detected across any demo page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 65 soma components now consume their morfo as the cross-layer contract.
createAttrs(morfo) and registerContract(morfo) overloads removed — the
Morfo form is now mandatory. Each component provider has a 3-line morfo
import + wire-up block instead of 20+ lines of inline declarations.
Additions:
sium/core/lazy.ts — lazy() combinator for recursive schemas (unblocks
morfo's eventual lazy part-tree validation; tests included).
sium/examples/ — booking, color, login, settings, signup, survey, tree
form demos.
Route demos at src/routes/test/sium/{booking,color,login,settings,
signup,survey,tree}.
Tooling:
morfo-check.ts — now tolerates sibling parts sharing a physical element
(Link variant of Item, etc.). Queries via allPartAttrs cross-scope.
morfo-vocabulary-check.ts — 28 WARN (legitimate per-component
vocabularies or subsets like switch missing "indeterminate"). All
reviewed, none are bugs.
Fixes caught by morfo-check:
table: data-sorted / data-pinned marked severity:'optional' (only
emitted when sorting/pinning active, not always).
tree-grid: data-expanded on Row severity:'optional' (branches only).
menubar: data-menubar-value declared on Trigger.
splitter: data-splitter-panel-id declared on Panel.
Verify:
npm run check — 0 morfo errors (4 pre-existing in sium/examples,
unrelated).
npx vitest run src/uix/morfo — 7/7 tests pass.
npm run smoke — 65/65 routes pass.
npm run morfo:check — 66/66 morfos validate against real DOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- Add Command Dialog demo (⌘K style) with proper Dialog.Trigger
- Make all inline demos collapsible to prevent UI clutter
- Add keyboard shortcut (cmd+k / ctrl+k) for Command Dialog
- Add close button (✕) and onSelect handlers to close dialog
- Fix CSS import paths in air/ and terra/ test pages
- Fix various syntax errors in test pages (toggle, toggle-group, meter)
- Update Switch and RadioGroup contracts with missing attributes
- Fix missing ctx declarations in time-field and time-range-field
- Fix checkbox group import error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>