Formato puro: `npm run format` (dos pases; siete ficheros no convergían en el
primero) sobre todo lo que .prettierignore no excluye (web/ congelado,
.claude/, artefactos generados). Este commit va en .git-blame-ignore-revs.
Neutralidad medida fichero a fichero, compilando y minificando con esbuild (y
con el compilador de Svelte, cliente y servidor, para .svelte) la versión de
HEAD y la formateada: 1 031 ficheros de código idénticos; 9 CSS que solo
difieren en espacios pegados a un paréntesis (`var( --x )` frente a
`var(--x)`), que CSS no tokeniza como significativos; 510 Markdown sin
compilación posible (docs:check los valida). Los dos generados y el guard que
dependía del formato se resolvieron en el commit anterior.
Verificación sobre este árbol: npm run lint exit 0 · gate entero verde salvo
focus-census, arreglado en el commit anterior y re-ejecutado (9/9): check:gate
OK (src/ y scripts/ a cero) · docs:check 0/0 · arts/blocks/packs/rtl/
translations/agent · eidos:lint 0 inválidos · apps:check (boot 27/27, 0
errores) · suite 465/466 → 466/466 con el guard corregido · build de la raíz
exit 0.
Co-Authored-By: Claude Opus 5 <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>