Per the doctrine update in GUIA §13: eidos for multi-part components
exposes BOTH a flat default that auto-composes Provider/Trigger/Content
AND the compound parts for advanced cases. Soma + morfo remain the
universal composable foundation underneath.
Why: with the previous "compound only" rule for multi-part, eidos
collapsible was a pure pass-through that added nothing operational —
the consumer wrote the same import + same shape as soma. The flat
default makes eidos earn its layer by collapsing the 90% case to a
single call:
// 90% case — flat ergonomic
<Collapsible bind:open>
{#snippet trigger()}Show details{/snippet}
<p>Body content.</p>
</Collapsible>
// Advanced — compound (parts in different subtrees, multi-trigger,
// conditional content)
<header><Collapsible.Provider bind:open>
<Collapsible.Trigger>Show</Collapsible.Trigger>
</Collapsible.Provider></header>
<aside>
<Collapsible.Content>...</Collapsible.Content>
</aside>
The architectural virtue: if the flat doesn't fit, a developer drops
to soma + morfo and composes freely. No vendor lock-in in the visual
layer; eidos is the design system convention, soma is the universal
primitive.
Files:
- src/uix/eidos/components/collapsible/collapsible.svelte (NEW): flat
default that auto-composes Provider + Trigger + Content via a
`trigger` snippet for the toggle label and `children` for the body.
- src/uix/eidos/components/collapsible/types.ts: adds
`CollapsibleFlatProps = SomaCollapsibleProps & { trigger?: Snippet }`.
- src/uix/eidos/components/collapsible/index.ts: exports `default`
(flat) plus named `Provider`/`Trigger`/`Content` (compound).
- src/uix/eidos/components/collapsible/{trigger,content,provider}.svelte:
fixed children destructuring so snippets propagate cleanly to soma
(was previously spreading $props() blindly which can lose snippet
bindings in Svelte 5).
- src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md: NEW §13 "Doctrina del API:
soma compound, eidos opinionado" formalises the two-shape rule and
the architectural virtue (soma+morfo as universal foundation).
Renumbered §13/§14 → §14/§15.
- web/routes/collapsible/+page.svelte: live preview now uses the flat
form; eidos snippet shows the flat shape; soma snippet keeps the
compound shape; Notes tab updated.
- src/uix/eidos/components/collapsible/README.md: rewrote API section
with both shapes + the "if neither fits, use soma+morfo" escape.
Tests 218/218 green. npm run check baseline 39.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two real bugs reported on the collapsible demo:
1. **Dark mode broken across all migrated components.**
- The docs site layout sets `document.documentElement.dataset.theme =
'light'|'dark'` — those are the selector names the docs CSS uses.
- The eidos themes (themes/base/{light,dark}.css) only respond to
`[data-theme='base-light'|'base-dark']`. Different namespace.
- Result: docs grays swap in dark mode but eidos doctrinal palette
(primary/secondary/neutral/affirm/fulfill/risk/threat/loss) stays
in light values, so component text renders dark on dark.
- Fix: each eidos theme now accepts BOTH selectors. `[data-theme=
'base-light'], [data-theme='light'], :root { ... }` and equivalent
for dark. Existing consumers that use `base-*` keep working; the
short `light/dark` aliases handle the docs site convention.
2. **Collapsible trigger jumps from center to left when expanded.**
- The preview-stage uses `display: flex; justify-content: center;
align-items: center;` — natural for buttons/switches that don't
grow.
- `Collapsible.Provider` is `display: block` so it shrink-fits its
content as a flex item. Closed = trigger-width (centered visually);
open = trigger + content width (longer, so the visual center
anchors trigger at the left edge).
- Fix: collapsible demo overrides the preview-stage style with
`justify-content: flex-start; align-items: flex-start` and wraps
the provider in a `max-inline-size: 32rem` panel. Disclosure
ergonomics need anchor-left + grow-down, not center.
- Also swapped the body's `--gray-50` (docs site token) for
`--color-surface-raised` + `--color-content-primary` (eidos
doctrinal tokens) so the panel inherits the dark-mode swap.
npm run check holds at the 39-error baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third component migrated to the eidos wrapper subdirectory pattern,
following the new "check air baseline first" discipline.
Air baseline recovered from commit 0a391408^:
- air/components/collapsible: 3 wrappers (Provider/Trigger/Content)
with `class` passthrough, `bind:open`, `onOpenChange`, manual
`air.interaction.play('expansion', enter|exit)` perceptual emit.
- No size, intent, color, or chevron slot.
Migration decisions (presented BEFORE coding per scope-approval rule):
- Two doctrinal events (`expand` + `collapse`, both emerge family,
sequence='pre', no intent — emerge is transitional). Replaces air's
manual `air.interaction.play` with `runtime.trigger('expand'|
'collapse')` which atravieses visual + sound + vibra channels.
- No intent/color/size — air didn't have them and disclosure is
non-evaluative per guide §3.1.
- Multi-part API: Provider + Trigger + Content (no flat default per
doctrine §10).
Files:
- src/uix/morfo/components/collapsible.ts: scope expanded to ['soma',
'sema'], events array added with two emerge events.
- src/uix/soma/components/collapsible/collapsible-provider.svelte.ts:
runtime.events handlers `expand`/`collapse` mutate state; toggle()
fires runtime.trigger(next ? 'expand' : 'collapse').
- src/uix/eidos/components/collapsible/{collapsible.css,
collapsible-provider.svelte, collapsible-trigger.svelte,
collapsible-content.svelte, types.ts, index.ts, README.md}.
- src/uix/eidos/components/collapsible.css (legacy flat) deleted.
- src/uix/eidos/index.css: import path updated to subdirectory.
- web/routes/collapsible/+page.svelte: full interactive testbed with
tabs (Live, Soma, Morfo, Sema, Eidos, Notes) mirroring switch's
structure. Sema tab shows per-channel signature cards for both
expand and collapse events.
Disciplina formalizada en CLAUDE.md hand-off + memory entry: "Check
air baseline first" before any future eidos migration. Switch shipped
without this check and lost ResponsiveProp<Size> (regression). Don't
repeat with collapsible/dialog/drawer/popover/toast/avatar.
npm run check holds at the 39-error baseline. Tests 218/218 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4 (docs alignment with the canonical guide):
- active_architecture.md — Dialog example refactored to the new event
shape (target inside semantic, explicit verb + sequence). Verbs
table grouped by family with the post-canon vocabulary (select/
toggle moved to commit, edit removed from handle, new verbs added).
Section 10 (Estado actual) updated to 2026-05-08 with current
progress: doctrina cerrada, eidos en producción, SoundChannel
eager-init, switch migrated, etc. "Para profundizar" §14 points at
the canonical guide.
- morfo/README.md — Step 5.5 added covering the doctrinal event
shape with all fields (semantic.family, .verb, .target, .sequence,
.intent) and the two naming shapes ({verb}-{variant} and
{family}-{verb}).
- sema/README.md — verbs section restructured to Record-by-family
matching the runtime; documents the verbs that moved between
families per the canon. Naming shapes section updated with both
forms and validateEventName signature.
- CLAUDE.md — "Session hand-off" rewritten with the post-canon
state. Pointer at GUIA_IMPLEMENTACION_SEMAUIX.md as the canonical
source. Pending list refreshed: switch is the immediate next
component, persistence/a11ySemantic/polymorphism deferred until
real consumers appear.
Switch migration to the eidos wrapper pattern:
- morfo/components/switch.ts — adds events: [{ commit-toggle }] with
the doctrinal shape (verb=toggle, sequence=post, intent fromProp
binding to the 4 supported intents). New data-* (data-color with
the 6-token Toggle/Switch subset, data-readonly, data-invalid,
data-required) and corresponding aria-*. scope expanded to
['soma', 'sema'].
- soma/components/switch/types.ts — adds intent, color, readonly,
invalid, aria-labelledby props with full JSDoc. Subset constrained
per guide §3.1 (Toggle/Switch row).
- soma/components/switch/switch-provider.svelte.ts — runtime-direct,
Field integration via FieldProvider.get(), isDisabled/isReadonly/
isRequired/isInvalid OR-merge derivations, intent ↔ color
resolution (intent wins when evaluative), runtime.trigger(
'commit-toggle') in onclick.
- soma/components/switch/components/switch.svelte — wires the new
props through bindProps.
- eidos/components/switch/{switch.css, switch.svelte, types.ts,
index.ts, README.md} — full subdirectory pattern, mirroring
toggle. Recipe drops legacy success/warning/danger/info selectors;
declares the 6 doctrinal data-color values directly. Wrapper
auto-renders Switch.Thumb inside the track and exposes an optional
`thumb` snippet for in-thumb glyphs.
- eidos/components/switch.css (legacy flat) deleted; eidos/index.css
updated to import the subdirectory recipe.
- web/routes/switch/+page.svelte — interactive testbed with all soma
props as live controls + tabs (Live, Soma, Morfo, Sema, Eidos,
Notes). Mirrors toggle's structure.
npm run check holds at the 39-error baseline. Tests 217/217 green.
Pending:
- Migrate collapsible / dialog / drawer / popover / toast / avatar
to the eidos wrapper pattern.
- Persistence + holds-by-intent — defer until first signal.warn /
signal.alert consumer.
- a11ySemantic + polymorphic events — defer until concrete need.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
Updates the canonical semantic vocabulary to match the latest
formulation in src/docs/libro_semantica_completo.txt:
Families (7, was 6):
contact, commit, signal, handle, emerge, shift, sustain
- `alert` → `signal` (rename: same perceptual function — attention /
salience — but the book's name reflects the broader scope including
notify, warn, emphasize, announce).
- `shift` (NEW, transitional): frame change. Modal mount, navigation,
mode change. Differs from emerge in that it reorganizes planes
(heavier backdrop, deeper shadow) — "cruce de umbral" not just
"aparición". Per book rule, the frame doesn't carry intent on its
own; evaluation belongs to a companion `signal` event.
Intents (6, was 5):
neutral, affirm, fulfill, risk, threat, loss
- `loss` (NEW): consummated negative consequence (negative valence,
low activation, posterior). Differs from `threat` (anterior, high
activation, convokes action): loss registers, doesn't convoke. Per
book §11.5 "la amenaza no es la pérdida".
sema-map.ts perceptual values:
- `signal` family entry: kept the prior `alert` numbers verbatim
(semantics is the same — attention/salience) under the new name.
- `shift` family entry: motion 260ms ease-in-out 0.98→1, sound
pitch 500 ascending gain 0.18 (deliberately quieter than emerge's
0.20 to not compete with companion signal), presence with stronger
backdrop (0.5) + shadow (blur 32 y 12 op 0.2) emphasizing
plane reorganization.
- `loss` intent deltas: descending sound contour (-150 pitch),
contraction (scale.to -0.04, motion duration ×0.95 — brief but
grave), color toward purple-grave (hue replace 280, lower
saturation/intensity), withdrawn presence (backdrop 0.05, no glow).
ARIA `role="alert"` (W3C live region) is unrelated and untouched in
soma/announce, soma/toast and morfo announce/field/form. The toast
morfo gains `loss → role:'status' aria-live:'polite'` mapping —
loss is consummated so it shouldn't interrupt.
Verbs added per the book's worked examples: signal {warn, notify,
emphasize}, shift {enter-mode, exit-mode, navigate}, commit {save,
discard, delete}, contact {press, click, hover}, handle {pick, carry,
drop}, emerge {reveal, show, hide}.
Verified: vitest 9 files / 99 tests, smoke 68/68, morfo-check 66/66.
The air → eidos refactor stripped the `.air-accordion-*` class hooks but
left 5 rulesets with their selector empty (just whitespace before `{`).
The most visible damage was line 161:
[data-accordion-trigger][data-state='open'] {
transform: rotate(var(--eidos-accordion-indicator-rotation));
}
Empty selector after the trailing space → the rule matched the trigger
button itself instead of the intended `.air-accordion-trigger-indicator`
descendant. So expanding any item rotated its title 90deg.
The other four orphaned rules (trigger-main flex layout, trigger-label,
indicator size/transition, content-inner padding) were dormant noise but
also dropped — the morfo doesn't declare an Indicator part and the demo
doesn't render the inner wrapper, so there's nothing to style.
Content padding is now applied directly to `[data-accordion-content]`
since there's no inner wrapper. Animation-height includes padding, which
matches the natural scrollHeight measurement.
Verified:
- playwright probe: trigger `transform: none` in both closed and open
state (was `matrix(...)` rotating 90deg before)
- smoke 68/68
The Drawer's "show" semantic was missing from the morfo, so close paths
went straight to handleClose() without the prewrite → emit → handler
cadence. User flagged this — drawer DOES have a present/dismiss
semantic, equivalent in surface to Dialog.
Morfo (`morfo/components/drawer.ts`):
- Added `events[]` mirroring Dialog: present, close-save,
close-cancel, close-dismiss, close-dismiss-outside, close-after-fail
- Added `data-last-action` (saved/cancelled/dismissed/dismissed-outside/
failed) to Content's data so the prewrite has a declared slot
- scope: ['soma', 'sema'] now (sema participates via signal emit)
Eidos (`eidos/components/drawer.css`):
- Reserved a 3px transparent border on the LEADING edge of Content
per side (the edge facing the user, opposite to the slide-out
direction): right→border-left, left→border-right, top→border-bottom,
bottom→border-top
- Tint rules: combine [data-state='closed'] + [data-side] +
[data-last-action] so the right edge gets colored during the close
animation (saved → success, failed → danger, cancelled/dismissed →
neutral)
Soma (`soma/components/drawer/drawer-provider.svelte.ts`):
- DrawerProvider injects a MorfoRuntime (same pattern as Dialog soft-
migration). Registers content part. Wires events.{close-save,
close-cancel, close-dismiss, close-dismiss-outside, close-after-fail}
to handleClose().
- dismissWith(action) public method — same API as Dialog.dismissWith.
- Replaced 4 close paths with runtime.trigger():
Close.onclick → 'close-cancel'
Escape (modal) → 'close-dismiss'
Escape (non-modal) → 'close-dismiss'
Overlay.onclick → 'close-dismiss-outside'
Drag-to-dismiss (gesture) keeps direct handleClose() — the visible
slide-out IS the close, no 240ms hold makes sense.
- DrawerProvider class re-exported from $soma/components/drawer.
Demo (`test/eidos/drawer/+page.svelte`):
- New "Exit tinting by causa" section with Save/Cancel/Fail buttons
that call dismissWith() through a local _dismiss-button.svelte
(same pattern as Dialog's).
Verification (playwright):
save t=83ms lastAction=saved hold=240ms (commit family)
cancel t=119ms lastAction=cancelled hold=240ms (emerge)
fail t=127ms lastAction=failed hold=600ms (alert)
Smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translates `air/components/tooltip/tooltip.css` (small — 31 lines, only
Content + sizes styled) and adds:
- Arrow fill (matches tooltip bg)
- Per-side entrance/exit animations keyed off [data-side] (Floating
layer) and [data-state]. Distance is smaller than Popover (2px vs
4px) — tooltip is lighter visually.
- State handling for tooltip's three-value [data-state]:
'closed' → exit animation
'delayed-open' → entrance animation (hover after openDelay)
'instant-open' → no animation (keyboard focus, deliberate
invocation; the user wants it now, not after a
slide)
Demo (test/eidos/tooltip): 4-side grid + sizes (sm/md/lg).
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translates `air/components/popover/popover.css` (small — 67 lines, only
Content + Close styled) and adds three eidos extensions:
1. Trigger envelope ([data-popover-trigger]) — air relied on the
Button recipe via .air-button class. Eidos applies a baseline
button-style envelope here, consuming Capa 2 directly.
2. Overlay fade ([data-popover-overlay][data-state='open'/'closed'])
— for modal popovers. The morfo declares Overlay as optional;
consumer renders it when modal=true.
3. Per-side entrance/exit animations keyed off [data-side]
(top/right/bottom/left) and [data-state]. Soma's Floating layer
writes data-side based on resolved placement. The popover slides
into place from the trigger edge — fade + small translate (4px)
+ scale (0.985 → 1). Air's "revelation" pattern: ~180ms entrance
/ 120ms exit.
Demo (test/eidos/popover): basic + 4-side grid. Each side button
anchors a Popover.Content with the matching `side` prop so the
animation direction matches placement.
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translates `air/components/drawer/drawer.css` to `eidos/components/
drawer.css` (mechanical: --air-* → --eidos-*, drop .air-* gates) and
adds the entrance/exit animations air's recipe was missing — air drove
drawer transitions through `air.semantic.contextIn/Out` Svelte
transitions, which don't exist in the morfo-driven path. Without them
the drawer snapped in/out instantly.
Per-side keyframes:
right → translateX(100%)
left → translateX(-100%)
top → translateY(-100%)
bottom → translateY(100%)
Plus overlay fade keyed off data-state. All ~240ms entrance / 180ms
exit (moderate / normal). The Presence layer waits for
getAnimations().finished so the content stays mounted during exit.
Demo (test/eidos/drawer):
- Modal section: 4 side buttons, render Drawer.Overlay (backdrop
+ focus trap, page non-interactive while open).
- Non-modal section: 4 side buttons, omit Drawer.Overlay + pass
modal={false}. No backdrop. Page behind stays interactive. Soma's
Dismissal layer auto-adjusts (interactOutsideBehavior default
flips from 'ignore' to 'close').
The Drawer morfo doesn't declare `events` yet, so no runtime.trigger()
cadence + no exit-tinting like Dialog. Recipe + animations only.
Verification (playwright probe):
top t=140ms translateY(-372) → t=406ms 0
right t=99ms translateX(452) → t=389ms 0
bottom t=94ms translateY(372) → t=390ms 0
left t=111ms translateX(-349) → t=428ms 0
Smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Air's dialog relied on the Button recipe (`.air-button` class on
trigger). Eidos has no class composition, so the trigger was inheriting
only the archetype's cursor + focus ring, leaving it with the
browser-default button look.
Add a baseline envelope in dialog.css consuming Capa 2 tokens directly
(--eidos-color-{surface-raised, surface-overlay, border-default,
content-primary}, --eidos-control-height-md, --eidos-radius-md,
--eidos-space-3, --eidos-duration-fast). Hover and data-state='open'
both shift to surface-overlay. Disabled fades to 0.5.
Trigger isn't a separate component with its own Capa 3 tokens — it's
a part of Dialog using the system surface. Direct Capa 2 consumption
is appropriate here (same as archetype rules).
Verification: smoke 68/68, morfo-check 66/66; visual confirmed via
playwright screenshot — Open dialog/Open small/Open large render with
proper button envelope.
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>
Switch (151 lines):
- mechanical: --air-* → --eidos-*, drop .air-* gates
- selectors match morfo as-is ([data-switch], [data-switch-thumb],
[data-state='checked'], [data-disabled])
- a-la-carte variants (data-color, data-size) dormant unless consumer adds
Collapsible (decorated parts only):
- mechanical translation
- dropped air's height-keyframes: they referenced
var(--terra-collapsible-content-height) (a JS-measured value from
the dead terra layer) and soma's Collapsible uses HTML `hidden`
(display:none) which kills CSS animations anyway. Left undecorated;
needs Presence layer integration in soma to enable real transitions.
Toast (substantive remap):
- selector remap due to part naming difference between air and morfo:
air [data-toast-toaster] → eidos [data-toast-viewport]
air [data-toast] (the card) → eidos [data-toast-item]
air [data-toast-message] → DROPPED (no equivalent — soma has
Title + Description direct, not a
separate "message" element)
air .air-toast-status → DROPPED (no status icon part)
air .air-toast-main → DROPPED (no content wrapper part)
- intent vocabulary remap (air data-type ↔ morfo data-intent):
error → threat
warning → risk
success → fulfill (and affirm — same palette)
info → neutral
- data-position on viewport stays a-la-carte (not in morfo)
Verification: smoke 68/68, morfo-check 66/66, focus rings + dialog
entrance/exit working per playwright.
Note: [data-dialog-trigger] has no explicit rule (air relied on
.air-button recipe). The trigger renders with browser defaults +
archetype interactive cursor. To be addressed in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the V1.5 hand-rolled recipes with the translated air originals
adapted to the morfo-driven architecture.
Mechanical translation:
- sed --air-* → --eidos-* (tokens) and --_air-* → --_eidos-* (private)
- drop `.air-{part}` class gates everywhere — eidos applies universally
to whatever data-attrs the morfo emits
Substantive adjustments per recipe:
Toggle:
- kept the existing morfo data-attrs ([data-toggle], [data-state='on'],
[data-disabled]) — match air's selectors directly
- air's a-la-carte variants ([data-color], [data-size], [data-variant],
[data-block], [data-icon-only]) stay dormant: not in toggle morfo,
only fire if a consumer adds them manually
- 229 lines of palette × variant × size variants vs 40 lines hand-rolled
Dialog:
- air's pattern: animation keyframes keyed off [data-state='open'/'closed']
(NOT Presence-layer markers); Soma's Presence waits getAnimations().
finished, so this works
- air ONLY animates the overlay — content snaps in/out instantly, sub-
perceptual. Added eidos-only @keyframes for content entrance/exit
(opacity + scale) so Presence has something to wait for and the user
can perceive the transition
- kept data-last-action exit tinting (border-top by causa during
closing animation) — air doesn't have this concept, dialog morfo
declares the prewrite that writes the attr before data-state flips
Verification:
- smoke 68/68, morfo-check 66/66
- playwright entrance: opacity 0 → 1 over ~120ms (air's overlay-in token)
- playwright exit: opacity 1 → 0 over ~280ms (air's overlay-out token)
- both interpolate, not sub-perceptual; user can tweak via tokens
Note: the existing eidos/tokens.css (legacy hand-rolled) and
eidos/components/{toast,switch,collapsible}.css (still hand-rolled)
continue to load alongside; phase 3 will translate the rest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Translates the foundation of air's visual system into eidos:
- eidos/contracts/ public token interfaces (4 root + 64 components)
- eidos/tokens/ per-component token values (62 components + motion)
- eidos/themes/base/ light + dark + _static + motion (4 files)
Mechanical translation: `--air-*` → `--eidos-*` everywhere, relative
imports preserved. The CSS is currently NOT wired into the legacy air/
folder, which stays for reference until phase 4 cleanup.
The `--eidos-*` prefix is documented in eidos/index.css with the two
reasons it earns its keep:
1. Authorship clarity at debug time (DevTools can identify the layer)
2. Override discipline (consumers know they're targeting public API)
Phase 1 only adds the foundation without touching component recipes.
The hand-rolled recipes (toast, dialog, toggle, switch, collapsible)
and the legacy hand-rolled tokens.css continue to load alongside the
new tree. Phase 2 will replace recipes with air-translated equivalents
and reconcile the duplicate motion/tint tokens.
Architectural note: air targeted terra and gated its CSS with
`.air-{component}` class on a wrapper. Eidos targets soma+morfo and
applies universally to the data-attrs the morfo declares — no class
gates, no Svelte wrappers. The morfo layer is the one that decides
which `data-*` attrs exist, which is the substantive shift between
the two systems.
Verification: smoke 68/68, morfo-check 66/66, focus rings + dialog
entrance/exit still working per Playwright check (curve unchanged).
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>
The ring color was hsl(220 90% 55% / 0.6) — same hue as the default
button accent at 60% alpha, so when the Toggle is in 'on' state or the
Switch is checked (both accent-filled), the ring against the active
background was almost invisible.
Bump to hsl(220 95% 45%) full opacity. Slightly darker than the accent
default, full alpha so it stands out against accent-filled states.
Note: `:focus-visible` only fires on keyboard focus (Tab), not click —
expected browser behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The custom property was added in V1 anticipating multiple recipe
consumers, but only Dialog uses last-action tinting. Premature
abstraction — single-use indirection that obscures what's happening.
Move the 5 selectors directly into dialog.css. events.css drops the
machinery (replaced with a 3-line note pointing to the recipe). If a
second consumer with the same exit-tint pattern shows up later, factor
out then — not before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second per-component recipe. Composes archetype interactive defaults
(trigger, overlay, close, content, focus-visible) with the visual
envelope (button shape, modal card, backdrop) and the asymmetric exit
tinting from events.css.
The recipe consumes `--_eidos-exit-tint` via a top accent stripe on
[data-dialog-content], so close-save tints fulfill, close-after-fail
tints threat, close-cancel/dismiss/dismissed-outside tint neutral. The
private var stays inside eidos — recipes consume it; consumers don't.
events.css extends the data-last-action coverage to dismissed and
dismissed-outside (both map to neutral tint, like cancelled).
Public CSS API: --eidos-dialog-{overlay-bg, overlay-z, content-bg,
content-radius, content-padding, content-shadow, content-max-width,
content-z, button-bg, button-bg-hover, button-border, button-border-hover}.
Default z-indices (overlay 50 / content 51) are sensible defaults the
integrator can override via tokens — Dialog without z-index would be
useless out of the box.
Test page (/test/eidos) gets a Dialog section so the recipe is visible
end-to-end.
Verification: smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supersedes the save/restore patch (b66039ad) — that was a workaround
for a design flaw, not the fix. The flaw: VisualChannel was writing
`data-intent` on the same DOM target where the morfo already had
`data-intent` from `v.propRef('intent')`. Two owners of one attr name,
collapsed by the channel's cleanup. Toast lost its tinting after every
announce because the channel removed the structural attr.
Fix: rename the signal-bound attr to `data-event-intent`. The channel's
attrs all live under `data-event-*` (data-event, data-event-id, data-
event-phase, data-event-family, data-event-intent) — a clean namespace
that can never collide with state attrs (data-state, data-intent,
data-disabled, ...). The channel writes only its own namespace; the
runtime owns state.
CSS in eidos can read either with distinct semantics:
[data-event-intent='risk'] → reacts to a transient occurrence
[data-intent='risk'] → reacts to persistent component state
events.css announce rules now read data-event-intent.
The Toast recipe is unchanged — its [data-toast-item][data-intent='...']
selectors read structural state, which is now never overwritten.
Tests:
- visual.test.ts: rename data-intent → data-event-intent in all
assertions; replace the two save/restore tests with one that proves
state attrs (data-intent, data-state, data-disabled) survive the
full hold cycle untouched.
- emit.test.ts: rename data-intent → data-event-intent.
Verification: sema 40/40, smoke 68/68, morfo-check 66/66.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>