feat(motion): structural stagger index + Cascade orchestrator on the verified model

The cascade is no longer a separate system — it's the existing state-presets + the
Material stagger + one foundation rule. Retires the --cascade-* reinvention.

- render-css: foundation structural-index writer — `[data-stagger] > *:nth-child` →
  `--motion-stagger-index` (forward) / `:nth-last-child` → `--motion-stagger-index-rev`
  (reverse). Nobody writes the index; soma never writes a visual var. Every preset's
  enter/exit rule consumes it (parallel = `--motion-stagger-each` 0, cascade = N).
- Cascade rewritten as a thin EXPLICIT orchestrator: marks `[data-stagger]`, reflects
  open→data-state, passes the preset to items via context; the `out:` retention flips
  to data-state=closed (reusing the exit preset), reading the duration via dom.getWindow.
  Drops the whole `--cascade-*` namespace + bespoke keyframes.

Docs: MOTION_SERVICE_RFC §D.11 — the final model (three orthogonal axes sema/motion/
eidos, the full lifecycle, the realization ladder snap->transition->animation->JS, the
firma-vs-realization seam, the cascade = existing system + one rule, the motion service
as the engine). eidos-motion aligned.

Verified: browser (enter scale-in/fade-in + nth-child index, exit reverse, zero inline
writes), npm run check 0 mine, motion.test 22/22, eidos-lint 0 invalid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 6307d03055
commit 27012a5c68

@ -1279,6 +1279,136 @@ reparto verificado y **cero `--var` visual escrita por JS** (confirmado por valo
OK en este caso, pero la versión glitch-free pide que el **contenedor orqueste** la retirada como unidad OK en este caso, pero la versión glitch-free pide que el **contenedor orqueste** la retirada como unidad
(lifecycle de soma, lo que hacía el `pending` borrado — sin tocar lo visual). Pendiente para producción. (lifecycle de soma, lo que hacía el `pending` borrado — sin tocar lo visual). Pendiente para producción.
### D.11 — El modelo final: tres ejes ortogonales + el lifecycle completo (2026-06-19)
Tras un re-análisis de fondo (workflow sobre código + canon) y correcciones duras del usuario, **este es el
modelo cerrado**. Supersede los matices de D.2/D.9 donde difieran; D.8 (retirada) y D.10 (prototipo) siguen
vigentes como historia.
#### D.11.0 — Tres ejes ORTOGONALES (el error de raíz era confundirlos)
El sistema NO es «una forma de animar». Son **tres cosas separables** que no hay que mezclar:
| Eje | Qué es | Qué hace EXACTAMENTE |
| --- | --- | --- |
| **sema** (semántico) | el «qué ocurrió» | **SOLO emite `data-event-*`** en el DOM (+ corre sus 2 canales runtime sound/haptic). **NO es animación.** |
| **motion** (`arts/motion`, el SERVICIO) | el **MOTOR** de animación | corre los drivers JS (`spring`/`waapi`/`rect`/`svelte`) + el path CSS declarativo + handoff de velocidad. Motor full, par/superior a Framer. |
| **eidos** (visual) | la materialización | **LEE `data-event-*` (+ `data-state`) y reacciona**: genera el CSS, decide estilo/animación, consume el servicio motion para lo JS. Único dueño visual. |
Y los dos que orquestan alrededor: **morfo** DECLARA (semántico + estructura, cero visual); **soma** DISPARA
(`runtime.trigger`) + ESTADO (`data-state`) + LIFECYCLE (presence/retención), cero var visual.
**La regla mental:** sema = el QUÉ (emite el token) · motion = el MOTOR (corre lo JS) · eidos = el CÓMO-SE-VE
(lee el token y materializa). Mezclar «sema» con «la capacidad de animar», o comparar el suelo-CSS contra el
motor entero de otro framework, es el error a no repetir.
#### D.11.1 — El lifecycle completo de un evento
```
1. DECLARACIÓN (estática, en código)
morfo: el evento (family/intent/verb/target/sequence/hold) + estructura (parts, data-state, data-stagger)
eidos: la realización — presets/signatures/keyframes en EidosConfig.motion
(derivados a generated/base.css + registrados en uix.motion)
2. TRIGGER (runtime, ORIGEN)
soma: runtime.trigger('open') ← origina desde interacción/lifecycle (sin engine sigue: sema es ornamental)
+ escribe data-state (en el contenedor y, propagado, en los items)
3. EMISIÓN (sema PROCESA)
sema: engine.emit → resuelve la firma (cascada 5 capas → SOLO sound/haptic)
→ el META-canal visual ESTAMPA data-event-* en signal.target (UN elemento)
→ abre el HOLD; en paralelo sound/haptic se realizan
4. REACCIÓN (eidos LEE, durante el hold)
eidos: el CSS del componente reacciona a data-event-* (+ data-state) → tres salidas posibles (D.11.2)
5. CLEANUP
sema: al cerrar el hold, des-estampa data-event-* (transient). Persistente → soma lo limpia (clearTarget)
6. PRESENCE (lifecycle de salida)
soma: Presence retiene el nodo durante la salida (await getAnimations / out:), luego desmonta.
La retención es soma; el visual, eidos.
```
#### D.11.2 — La reacción de eidos: una escalera de realización
Leído el `data-event-*` (+ `data-state`), el CSS del componente decide CÓMO se realiza. **No es siempre una
animación** — hay una escalera de cuatro escalones, de menos a más:
```
snap → transition → animation (keyframe / preset) → JS (servicio motion)
```
- **(a) snap — estilo estático.** CSS puro, sin interpolar (la firma en el canal color):
`[data-field][data-event-intent='threat'] { border-color: var(--color-threat-border) }`. Cambia de golpe.
- **(b) transition — estilo INTERPOLADO.** Una `transition` CSS interpola la propiedad cuando su valor cambia:
`transition: border-color var(--duration-fast)`. El borde rojo **entra suave** en vez de saltar. Es el suelo de
las micro-interacciones (hover/focus, `archetypes.css`) y de cambios de estado de UNA propiedad. La corre el
navegador.
- **(c) animation — keyframe / preset.** La firma (`--event`) o el preset (`--state`: enter/exit con forma +
stagger, fill `backwards`). Declarativa; **el navegador la corre**; motion la tiene en registry + Presence la
espera, pero no la ejecuta.
- **(d) JS — el servicio `motion`.** Física (`spring`), FLIP (`rect`), reversa interrumpible. La ejecuta
`uix.motion.run`, disparada por **soma/Presence** leyendo `data-animation-style` + tokens. El CSS NO llama a JS.
Frontera clave: **declarar (CSS) ≠ ejecutar.** El navegador corre (a)/(b)/(c); el servicio motion corre (d),
disparado por soma. El CSS DECLARA qué hace motion; no lo invoca. **Lifecycle:** `Presence` espera tanto los
`CSSAnimation` como los `CSSTransition` vía `getAnimations()` al salir — el servicio no gestiona las `transition`,
pero el lifecycle sí las contempla.
#### D.11.3 — Firma vs realización (el seam, canon §10/§15)
Una firma conserva IDENTIDAD bajo variación; los valores concretos son realización:
- **GENÉRICO (firma, del evento):** la identidad — «un `emerge` aparece» (flourish del contenedor, signature
`present-rise`) y «los hijos de un contenedor que emerge aparecen JUNTOS, por orden estructural».
- **ELÁSTICO (realización, per-componente):** los valores — el preset (keyframe), la duración, el ritmo
(paralelo/cascada), el easing. Un menú a 40ms y un toast a 0ms (paralelo) realizan la MISMA firma distinto.
→ El timing NO es un parámetro genérico de firma (eso resucitaría la Grieta 1). Es **realización per-componente**,
en su estilo de eidos.
#### D.11.4 — La cascada/stagger: el sistema EXISTENTE + una regla
La cascada NO es un sistema nuevo. Es el preset declarado + el stagger que ya existía + **UNA regla de foundation**:
- **El índice (foundation, generado por `render-css.ts`):** `[data-stagger] > *:nth-child(N) { --motion-stagger-index: N-1 }`
(forward) + `:nth-last-child → --motion-stagger-index-rev` (reverse). **Nadie lo escribe; sale de la
estructura.** (El `DomCascade` borrado lo escribía desde soma = la violación raíz.)
- **El delay (en CADA preset, ya estaba):** enter `calc(var(--motion-stagger-index) * var(--motion-stagger-each))`;
exit usa el reverse. **Paralelo = `--motion-stagger-each: 0`; cascada = N.** No hay regla de «tipo»: el tipo
lineal es el VALOR del token, en el estilo del componente.
- **La animación:** el preset que el item lleva en `data-animation-style` (declarado en EidosConfig.motion).
Keyframe + duración + reduced-motion: del preset, gratis.
**Dos dominios para usarlo:** (1) **explícito** — el componente `<Cascade>` (orquestador fino, sin recipe ni
`--cascade-*`): marca `[data-stagger]`, refleja `open`→`data-state`, pasa el preset a los items por contexto;
(2) **event-driven** — un componente real declara en SU morfo el evento `emerge` + `data-stagger` en el
contenedor; sus items llevan el preset + `data-state`. La aparición sale del evento declarado, sin wrapper.
#### D.11.5 — El servicio `motion` es el motor (par/superior a Framer)
`EngineMotion` (`arts/motion` · `uix.motion`): drivers `spring` (integrador de Euler semi-implícito:
overshoot/settle/drag/snap), `waapi`, `rect` (FLIP/genie, medido + batcheado), `svelte`, `css` (suelo
declarativo). **Handoff de velocidad** (`peek`/`takeHandoff`): la reversa continúa desde posición + velocidad
actuales (el caso difícil de la interrupción). **Puerto DOM inyectable** (`MotionDom`) → iframe/popup/SSR-test
(Framer asume `document` global). En capacidad de motor, **par o superior**. Madurez pendiente: el catálogo de
presets-JS-por-nombre está a medio poblar (los CSS vienen de serie; los JS se registran por app).
El `spring` es además el **equivalente INTERRUMPIBLE de una `transition` CSS** (D.11.2.b): una `transition`
**salta** al revertir a media interpolación; el `spring` **continúa desde la velocidad actual** (el handoff). Por
eso la frontera práctica: interpolación simple no-interrumpible → `transition` CSS (eidos, browser); física o
reversa interrumpible → el driver JS del servicio.
#### D.11.6 — Qué se corrigió (vs el primer intento de esta sesión)
El primer intento de productización hizo un `<Cascade>` con recipe propio + un namespace `--cascade-*`
(`index`/`each`/`base`/`pass`) + keyframes propios. **Era reinventar** el `--motion-stagger-*` + los state-presets
que ya existían (+ le faltaba reduced-motion + un `getComputedStyle` crudo violando ActiveDom). Corregido:
**retirado el `--cascade-*` entero**; la cascada monta sobre el preset declarado + el índice de foundation;
`<Cascade>` quedó como orquestador fino del dominio explícito (consume el sistema, no lo duplica). Verificado en
navegador + `npm run check`/`motion.test`/eidos-lint limpios.
--- ---
> **Fuentes.** Canon semántico: [`docs/CANON.md`](../../../docs/CANON.md). Arquitectura de > **Fuentes.** Canon semántico: [`docs/CANON.md`](../../../docs/CANON.md). Arquitectura de

@ -0,0 +1,94 @@
# Cascade
A thin **explicit** orchestrator that staggers its children's enter/exit. It owns **no recipe
and no timing of its own** — it rides the foundation stagger + the existing state-presets. It
is the *explicit* / content-animation way to sequence a set of elements (you designed them to
stagger), as opposed to the *event-driven* path where a component's own `emerge` firma + the
same foundation stagger do it (see "Two ways" below).
```svelte
<script>
import { Cascade } from '$uix/eidos/components/cascade'
let open = $state(true)
</script>
<Cascade {open} animation="scale-fade" style="display: grid; gap: 0.75rem; --motion-stagger-each: 60ms">
{#each items as item (item.id)}
<Cascade.Item>{item.label}</Cascade.Item>
{/each}
</Cascade>
```
`--motion-stagger-each: 0` (the default) → **parallel**; `N` → **cascade**. The keyframe,
duration, easing and reduced-motion all come from the `animation` preset.
## How it works — it reuses everything
Nothing here is a new system. The cascade is the *existing declared motion* plus one
foundation rule:
| Piece | Where it's declared / derived |
| --- | --- |
| the per-item **animation** (keyframe, duration, ease, reduced-motion) | a **state-preset** declared in `EidosConfig.motion` (`fade` / `scale-fade` / …), generated by eidos to `generated/base.css` and registered with `uix.motion` |
| the **stagger delay** (`index × --motion-stagger-each`, default 0 = parallel) | already in **every preset's** enter/exit rule (`lib/render-css.ts`) |
| the **structural index** (`--motion-stagger-index` / `-rev`) | the one new foundation rule: `[data-stagger] > *:nth-child / :nth-last-child` (generated by `lib/render-css.ts`) — **nobody writes it**, soma never writes a visual var |
`<Cascade>` just **wires** those: it marks the container `[data-stagger]`, reflects `open` as
`data-state`, and passes `open` + the chosen preset to its items (context), so each item
carries `data-animation-style` + `data-state`. Enter counts up (`:nth-child`); exit counts
down (`:nth-last-child`) so the last item leaves first.
## Props
### `<Cascade>`
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `open` | `boolean` | `true` | Drives the staggered enter (forward) / exit (reverse) via `data-state`. |
| `animation` | `MotionPresetName` | `'fade'` | The state-preset every item plays (`fade` / `scale-fade` / `slide-fade` / …). |
Plus any `HTMLAttributes<HTMLDivElement>`. Set the layout + the rhythm (`--motion-stagger-each`)
here.
### `<Cascade.Item>`
A participant. On removal it flips to `data-state='closed'` (the existing **exit** preset
plays, with the reverse structural index), is retained (`out:`) for the eidos-declared exit
duration, then unmounts. Accepts any `HTMLAttributes<HTMLDivElement>`.
## Ownership
| | Owns | Never touches |
| --- | --- | --- |
| **soma** (the wrappers) | **state** (`open` → `data-state`, propagated to items) + **lifecycle** (`out:` retention) | any `--motion-*` / visual variable — it reads the eidos duration via the ActiveDom `getComputedStyle`, writes nothing |
| **eidos** | **all the visual** — the preset (keyframe/duration/ease/reduced-motion) + the foundation stagger index | the semantics / state |
## Two ways to stagger children
1. **Explicit** — `<Cascade>` (this component). You want these elements to stagger; there is no
semantic event. The content/intrinsic animation domain.
2. **Event-driven** — a real component (a menu, a list) declares its own `emerge` firma (the
container flourish + sound) **and** marks its item container `[data-stagger]`; its items
carry a preset + the container's `data-state`. Same foundation stagger, no `<Cascade>`
wrapper — the appearance flows from the declared event. (The firma is *generic by event*:
"children of an emerging container present together, by structural order"; the concrete
timing — parallel/cascade, ms, keyframe — is the per-component realization.)
## Known limitation — bulk removal
When **several** items are removed at once (e.g. a slider jumping 10 → 3), the leaving nodes
unmount one-by-one as each finishes, so the grid reflows during the cascade and the remaining
items' `:nth-last-child` index re-evaluates. In practice it reads fine, but the glitch-free
version wants the **container to retain the leaving set as a unit** (one reflow at the end) —
a soma **lifecycle** concern (what the retired `pending` did, without touching the visual).
Dragging the slider down step-by-step (a sequence of lone removals) is always clean.
## Decisions
- **No recipe, no `--cascade-*` namespace, no bespoke keyframe.** An earlier prototype forked
all of that — it reinvented the existing `--motion-stagger-*` + state-preset system. This
replaces it by reusing them; the only new code is the foundation structural-index writer.
- **`Cascade` is the EXPLICIT domain.** Event-driven children appearance rides the firma +
the same foundation stagger, declared in the component's own morfo — not this wrapper.
- **Index from `:nth-child`, capped 1–24** (the foundation writer). Beyond the cap items share
the last index until `sibling-index()` is broadly supported.

@ -0,0 +1,39 @@
<script lang="ts">
/**
* Eidos `<Cascade.Item>` — a participant in the cascade.
*
* It carries the container's `data-state` + the chosen `data-animation-style` preset (from
* context); the foundation gives it its structural index. So the existing preset enter/exit
* rule plays with the stagger — no per-item visual var, no bespoke recipe.
*
* SOMA role (lifecycle only): on removal, flip the node to `data-state='closed'` so the
* EXISTING exit preset plays (with the reverse structural index), retain it for the
* eidos-declared exit duration (read via the ActiveDom-compliant getComputedStyle), then
* unmount. It writes no visual var — only the state flip.
*/
import { ActiveEidos } from '$uix/eidos';
import { getCascadeContext } from './context';
import type { CascadeItemProps } from './types';
let { children, ...rest }: CascadeItemProps = $props();
const ctx = getCascadeContext();
const eidos = ActiveEidos.require();
function leave(node: HTMLElement) {
node.setAttribute('data-state', 'closed');
const cs = eidos.dom.getWindow(node).getComputedStyle(node);
const ms = (parseFloat(cs.animationDelay) + parseFloat(cs.animationDuration)) * 1000 || 200;
return { duration: ms };
}
</script>
<div
data-cascade-item
data-animation-style={ctx?.animation ?? 'fade'}
data-state={ctx?.open ? 'open' : 'closed'}
out:leave
{...rest}
>
{@render children?.()}
</div>

@ -0,0 +1,15 @@
/*
* Cascade has NO recipe of its own — it rides the foundation. This file exists only as the
* component's CSS slot; the actual mechanism lives elsewhere, already declared + generated:
*
* - the structural stagger index — `[data-stagger] > *:nth-child / :nth-last-child` →
* `--motion-stagger-index` / `-rev` (foundation, generated by `lib/render-css.ts`);
* - the per-item animation — the existing state-preset the item carries via
* `data-animation-style` + `data-state` (its enter/exit rule already consumes
* index × `--motion-stagger-each`, default 0 → parallel);
* - the keyframe + duration + reduced-motion — the chosen preset, declared in
* `EidosConfig.motion` and derived by eidos to `uix.motion`.
*
* No `--cascade-*` namespace, no keyframe, no per-item visual var. The earlier prototype
* recipe (which forked all of that) was the reinvention this replaces.
*/

@ -0,0 +1,34 @@
<script lang="ts">
/**
* Eidos `<Cascade>` — a thin EXPLICIT orchestrator (the content/intrinsic animation
* domain). It owns NO recipe and NO timing of its own: it rides the foundation stagger
* + the existing state-presets.
*
* - the container marks itself `[data-stagger]` → the foundation writes each child's
* `--motion-stagger-index` / `-rev` from `:nth-child` / `:nth-last-child` (no JS);
* - it reflects `open` as `data-state` and passes `open` + the chosen preset to its
* items via context, so each item carries `data-animation-style` + `data-state` and
* the EXISTING preset enter/exit rule plays with the structural stagger.
*
* Rhythm: `--motion-stagger-each` (default 0 → parallel, N → cascade). Keyframe +
* reduced-motion come from the preset. Soma writes no visual var.
*/
import './cascade.css';
import { setCascadeContext } from './context';
import type { CascadeProps } from './types';
let { open = true, animation = 'fade', children, ...rest }: CascadeProps = $props();
setCascadeContext({
get open() {
return open;
},
get animation() {
return animation;
}
});
</script>
<div data-stagger data-state={open ? 'open' : 'closed'} {...rest}>
{@render children?.()}
</div>

@ -0,0 +1,22 @@
import { getContext, setContext } from 'svelte';
import type { MotionPresetName } from '$uix/eidos/lib/motion/registry';
const KEY = Symbol('cascade');
/**
* What `<Cascade>` passes to its items: the container's open state + the state-preset the
* items should play. Items read it to carry `data-state` + `data-animation-style` — they
* never receive a timing or a visual var, only these two semantic/structural facts.
*/
export type CascadeContext = {
readonly open: boolean;
readonly animation: MotionPresetName;
};
export function setCascadeContext(ctx: CascadeContext): void {
setContext(KEY, ctx);
}
export function getCascadeContext(): CascadeContext | undefined {
return getContext(KEY);
}

@ -0,0 +1,22 @@
// Cascade — a coordinating container that staggers its children's enter/exit.
//
// import { Cascade } from '$uix/eidos/components/cascade';
//
// <Cascade open={isOpen} animation="scale-fade" style="display: grid; gap: 0.75rem; --motion-stagger-each: 60ms">
// {#each items as item (item.id)}
// <Cascade.Item>{item.label}</Cascade.Item>
// {/each}
// </Cascade>
import CascadeComponent from './cascade.svelte';
import Item from './cascade-item.svelte';
type CascadeNamespace = typeof CascadeComponent & {
Item: typeof Item;
};
const Cascade = CascadeComponent as CascadeNamespace;
Cascade.Item = Item;
export { Cascade };
export default Cascade;
export type { CascadeProps, CascadeItemProps } from './types';

@ -0,0 +1,28 @@
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import type { MotionPresetName } from '$uix/eidos/lib/motion/registry';
/**
* Props for `<Cascade>` — a thin explicit orchestrator that staggers its children's
* enter/exit by riding the foundation stagger + an existing state-preset. Layout is yours;
* set the rhythm with `--motion-stagger-each` (default 0 → parallel) on the element.
*/
export type CascadeProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
/** Whether the container is open — drives the staggered enter (forward) / exit (reverse). @default true */
open?: boolean;
/**
* The state-preset every item plays (its `data-animation-style`) — any registered eidos
* motion preset (`fade`, `scale-fade`, `slide-fade`, …). The keyframe / duration /
* reduced-motion all come from the preset. @default 'fade'
*/
animation?: MotionPresetName;
children?: Snippet;
};
/**
* Props for `<Cascade.Item>` — a participant in the cascade. On removal it is retained
* (Svelte `out:`) for its eidos-declared exit, then unmounted.
*/
export type CascadeItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
children?: Snippet;
};

@ -157,13 +157,17 @@ no sobre-escribe** atributos de estado y viceversa (ownership). NO dice que solo
uno pueda animarse — **eidos lee ambos y anima ambos**. Lo prohibido es pisar el uno pueda animarse — **eidos lee ambos y anima ambos**. Lo prohibido es pisar el
nombre ajeno, no animar sobre los dos ejes. nombre ajeno, no animar sobre los dos ejes.
> **Coordinación entre superficies — en re-diseño.** Hubo un tercer eje «coordinado» > **Coordinación de hijos (stagger / cascada) — modelo CERRADO, RFC §D.11.** Hubo un tercer
> (`PresenceGroup` / presets `cascade-*` sobre `data-starting/ending-style`); se > eje «coordinado» (`PresenceGroup` / presets `cascade-*` sobre `data-starting/ending-style`);
> **retiró** el 2026-06-19 (ver la nota de retirada del > se **retiró** el 2026-06-19. El modelo final: la cascada NO es un sistema aparte — es el
> [`MOTION_SERVICE_RFC.md`](./MOTION_SERVICE_RFC.md)). El principio vigente: la > **momento `--state`** (un preset declarado) + el **stagger que ya existía**
> coordinación padre↔hijo es **un canal de la firma del evento** (la posee sema), > (`índice × --motion-stagger-each`, paralelo = 0, cascada = N) + **una regla de foundation**
> materializada por eidos reaccionando a `data-event-*` — no un motor paralelo. > que escribe el índice desde estructura (`[data-stagger] > *:nth-child → --motion-stagger-index`
> Reconstrucción pendiente: Apéndice D del RFC. > / `:nth-last-child → -rev`, generada en `lib/render-css.ts`). La firma del evento (`emerge` →
> `present-rise`) es solo el flourish del **contenedor** (genérico, sobre `data-event-*`); el
> timing de los hijos es **realización per-componente**, no un canal de sema. Tres ejes
> ortogonales (sema emite · motion es el motor · eidos materializa) + el lifecycle completo:
> [§D.11 del RFC](./MOTION_SERVICE_RFC.md).
--- ---
@ -458,8 +462,8 @@ elemento; el preset las lee con fallback al token:
| `--motion-ease-{enter,exit}` | curva por componente (F3) | `@property inherits:false` | | `--motion-ease-{enter,exit}` | curva por componente (F3) | `@property inherits:false` |
| `--motion-slide-leave` | extra de `slide-full` para paneles inset (drawer) | default `0px` | | `--motion-slide-leave` | extra de `slide-full` para paneles inset (drawer) | default `0px` |
| `--height` / `--collapsed-height` | altura medida del `collapse` (soma la escribe) | — | | `--height` / `--collapsed-height` | altura medida del `collapse` (soma la escribe) | — |
| `--motion-stagger-each` | ritmo del stagger (en el contenedor, hereda) | default `0ms` | | `--motion-stagger-each` | ritmo del stagger (en el contenedor, hereda) | default `0ms` → **paralelo**; N → cascada |
| `--motion-stagger-index` | índice del stagger (por ítem) | `@property inherits:false`, `<integer>` | | `--motion-stagger-index` / `-index-rev` | índice por ítem **desde estructura** — `[data-stagger] > *:nth-child` (fwd, enter) / `:nth-last-child` (rev, exit); generado en `render-css.ts`, **nadie lo escribe** | `@property inherits:false`, `<integer>` |
Keyframes (`EidosConfig.motion.keyframes`, paridad `theme.keyframes` de Chakra): Keyframes (`EidosConfig.motion.keyframes`, paridad `theme.keyframes` de Chakra):
`translate`/`scale` individuales (componen sin pisarse); parametrizados por CSS `translate`/`scale` individuales (componen sin pisarse); parametrizados por CSS

@ -1011,11 +1011,12 @@
--dialog-header-gap: var(--space-3); --dialog-header-gap: var(--space-3);
--dialog-footer-gap: var(--space-3); --dialog-footer-gap: var(--space-3);
--dialog-footer-margin-top: var(--space-2); --dialog-footer-margin-top: var(--space-2);
--dialog-title-font-family: var(--font-heading); --dialog-title-font-family: var(--style-h3-font-family);
--dialog-title-font-size: var(--font-size-lg); --dialog-title-font-size: var(--style-h3-font-size);
--dialog-title-font-weight: var(--font-weight-semibold); --dialog-title-font-weight: var(--style-h3-font-weight);
--dialog-title-line-height: var(--leading-heading); --dialog-title-line-height: var(--style-h3-line-height);
--dialog-title-color: var(--color-content-primary); --dialog-title-letter-spacing: var(--style-h3-letter-spacing);
--dialog-title-color: var(--style-h3-color);
--dialog-description-font-family: var(--font-ui); --dialog-description-font-family: var(--font-ui);
--dialog-description-font-size: var(--font-size-sm); --dialog-description-font-size: var(--font-size-sm);
--dialog-description-line-height: var(--leading-ui); --dialog-description-line-height: var(--leading-ui);
@ -4527,6 +4528,204 @@
initial-value: 0; initial-value: 0;
} }
@property --motion-stagger-index-rev {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
[data-stagger] > *:nth-child(1) {
--motion-stagger-index: 0;
}
[data-stagger] > *:nth-last-child(1) {
--motion-stagger-index-rev: 0;
}
[data-stagger] > *:nth-child(2) {
--motion-stagger-index: 1;
}
[data-stagger] > *:nth-last-child(2) {
--motion-stagger-index-rev: 1;
}
[data-stagger] > *:nth-child(3) {
--motion-stagger-index: 2;
}
[data-stagger] > *:nth-last-child(3) {
--motion-stagger-index-rev: 2;
}
[data-stagger] > *:nth-child(4) {
--motion-stagger-index: 3;
}
[data-stagger] > *:nth-last-child(4) {
--motion-stagger-index-rev: 3;
}
[data-stagger] > *:nth-child(5) {
--motion-stagger-index: 4;
}
[data-stagger] > *:nth-last-child(5) {
--motion-stagger-index-rev: 4;
}
[data-stagger] > *:nth-child(6) {
--motion-stagger-index: 5;
}
[data-stagger] > *:nth-last-child(6) {
--motion-stagger-index-rev: 5;
}
[data-stagger] > *:nth-child(7) {
--motion-stagger-index: 6;
}
[data-stagger] > *:nth-last-child(7) {
--motion-stagger-index-rev: 6;
}
[data-stagger] > *:nth-child(8) {
--motion-stagger-index: 7;
}
[data-stagger] > *:nth-last-child(8) {
--motion-stagger-index-rev: 7;
}
[data-stagger] > *:nth-child(9) {
--motion-stagger-index: 8;
}
[data-stagger] > *:nth-last-child(9) {
--motion-stagger-index-rev: 8;
}
[data-stagger] > *:nth-child(10) {
--motion-stagger-index: 9;
}
[data-stagger] > *:nth-last-child(10) {
--motion-stagger-index-rev: 9;
}
[data-stagger] > *:nth-child(11) {
--motion-stagger-index: 10;
}
[data-stagger] > *:nth-last-child(11) {
--motion-stagger-index-rev: 10;
}
[data-stagger] > *:nth-child(12) {
--motion-stagger-index: 11;
}
[data-stagger] > *:nth-last-child(12) {
--motion-stagger-index-rev: 11;
}
[data-stagger] > *:nth-child(13) {
--motion-stagger-index: 12;
}
[data-stagger] > *:nth-last-child(13) {
--motion-stagger-index-rev: 12;
}
[data-stagger] > *:nth-child(14) {
--motion-stagger-index: 13;
}
[data-stagger] > *:nth-last-child(14) {
--motion-stagger-index-rev: 13;
}
[data-stagger] > *:nth-child(15) {
--motion-stagger-index: 14;
}
[data-stagger] > *:nth-last-child(15) {
--motion-stagger-index-rev: 14;
}
[data-stagger] > *:nth-child(16) {
--motion-stagger-index: 15;
}
[data-stagger] > *:nth-last-child(16) {
--motion-stagger-index-rev: 15;
}
[data-stagger] > *:nth-child(17) {
--motion-stagger-index: 16;
}
[data-stagger] > *:nth-last-child(17) {
--motion-stagger-index-rev: 16;
}
[data-stagger] > *:nth-child(18) {
--motion-stagger-index: 17;
}
[data-stagger] > *:nth-last-child(18) {
--motion-stagger-index-rev: 17;
}
[data-stagger] > *:nth-child(19) {
--motion-stagger-index: 18;
}
[data-stagger] > *:nth-last-child(19) {
--motion-stagger-index-rev: 18;
}
[data-stagger] > *:nth-child(20) {
--motion-stagger-index: 19;
}
[data-stagger] > *:nth-last-child(20) {
--motion-stagger-index-rev: 19;
}
[data-stagger] > *:nth-child(21) {
--motion-stagger-index: 20;
}
[data-stagger] > *:nth-last-child(21) {
--motion-stagger-index-rev: 20;
}
[data-stagger] > *:nth-child(22) {
--motion-stagger-index: 21;
}
[data-stagger] > *:nth-last-child(22) {
--motion-stagger-index-rev: 21;
}
[data-stagger] > *:nth-child(23) {
--motion-stagger-index: 22;
}
[data-stagger] > *:nth-last-child(23) {
--motion-stagger-index-rev: 22;
}
[data-stagger] > *:nth-child(24) {
--motion-stagger-index: 23;
}
[data-stagger] > *:nth-last-child(24) {
--motion-stagger-index-rev: 23;
}
@keyframes fade-in { @keyframes fade-in {
from { from {
opacity: 0; opacity: 0;
@ -4920,6 +5119,7 @@
[data-animation-style='fade'][data-state='closed'] { [data-animation-style='fade'][data-state='closed'] {
animation: fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='scale-fade'][data-state='open'] { [data-animation-style='scale-fade'][data-state='open'] {
@ -4930,6 +5130,7 @@
[data-animation-style='scale-fade'][data-state='closed'] { [data-animation-style='scale-fade'][data-state='closed'] {
animation: scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
transform-origin: var(--floating-transform-origin); transform-origin: var(--floating-transform-origin);
} }
@ -4980,22 +5181,27 @@
[data-animation-style='slide-fade'][data-state='closed'] { [data-animation-style='slide-fade'][data-state='closed'] {
animation: slide-to-bottom var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-bottom var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-fade'][data-side='top'][data-state='closed'] { [data-animation-style='slide-fade'][data-side='top'][data-state='closed'] {
animation: slide-to-bottom var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-bottom var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-fade'][data-side='bottom'][data-state='closed'] { [data-animation-style='slide-fade'][data-side='bottom'][data-state='closed'] {
animation: slide-to-top var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-top var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-fade'][data-side='left'][data-state='closed'] { [data-animation-style='slide-fade'][data-side='left'][data-state='closed'] {
animation: slide-to-right var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-right var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-fade'][data-side='right'][data-state='closed'] { [data-animation-style='slide-fade'][data-side='right'][data-state='closed'] {
animation: slide-to-left var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-left var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, scale-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-motion='reduce'] [data-animation-style='slide-fade'][data-state='open'] { [data-motion='reduce'] [data-animation-style='slide-fade'][data-state='open'] {
@ -5045,22 +5251,27 @@
[data-animation-style='slide-full'][data-state='closed'] { [data-animation-style='slide-full'][data-state='closed'] {
animation: slide-to-right-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-right-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-full'][data-side='top'][data-state='closed'] { [data-animation-style='slide-full'][data-side='top'][data-state='closed'] {
animation: slide-to-top-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-top-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-full'][data-side='bottom'][data-state='closed'] { [data-animation-style='slide-full'][data-side='bottom'][data-state='closed'] {
animation: slide-to-bottom-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-bottom-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-full'][data-side='left'][data-state='closed'] { [data-animation-style='slide-full'][data-side='left'][data-state='closed'] {
animation: slide-to-left-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-left-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-animation-style='slide-full'][data-side='right'][data-state='closed'] { [data-animation-style='slide-full'][data-side='right'][data-state='closed'] {
animation: slide-to-right-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-to-right-full var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-motion='reduce'] [data-animation-style='slide-full'][data-state='open'] { [data-motion='reduce'] [data-animation-style='slide-full'][data-state='open'] {
@ -5090,6 +5301,7 @@
[data-animation-style='collapse'][data-state='closed'] { [data-animation-style='collapse'][data-state='closed'] {
animation: collapse-height var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: collapse-height var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-motion='reduce'] [data-animation-style='collapse'][data-state='open'] { [data-motion='reduce'] [data-animation-style='collapse'][data-state='open'] {
@ -5119,6 +5331,7 @@
[data-animation-style='shared-axis-x'][data-state='closed'] { [data-animation-style='shared-axis-x'][data-state='closed'] {
animation: slide-axis-x-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-axis-x-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-motion='reduce'] [data-animation-style='shared-axis-x'][data-state='open'] { [data-motion='reduce'] [data-animation-style='shared-axis-x'][data-state='open'] {
@ -5148,6 +5361,7 @@
[data-animation-style='shared-axis-y'][data-state='closed'] { [data-animation-style='shared-axis-y'][data-state='closed'] {
animation: slide-axis-y-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: slide-axis-y-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards, fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-motion='reduce'] [data-animation-style='shared-axis-y'][data-state='open'] { [data-motion='reduce'] [data-animation-style='shared-axis-y'][data-state='open'] {
@ -5177,6 +5391,7 @@
[data-animation-style='fade-through'][data-state='closed'] { [data-animation-style='fade-through'][data-state='closed'] {
animation: fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards; animation: fade-out var(--motion-duration-exit, var(--duration-fast)) var(--motion-ease-exit, var(--ease-in)) forwards;
animation-delay: calc(var(--motion-stagger-index-rev, 0) * var(--motion-stagger-each, 0ms));
} }
[data-motion='reduce'] [data-animation-style='fade-through'][data-state='open'] { [data-motion='reduce'] [data-animation-style='fade-through'][data-state='open'] {

@ -720,6 +720,19 @@ function renderMotionBlocks(motion: MotionConfig): string {
blocks.push( blocks.push(
"@property --motion-stagger-index {\n\tsyntax: '<integer>';\n\tinherits: false;\n\tinitial-value: 0;\n}" "@property --motion-stagger-index {\n\tsyntax: '<integer>';\n\tinherits: false;\n\tinitial-value: 0;\n}"
) )
blocks.push(
"@property --motion-stagger-index-rev {\n\tsyntax: '<integer>';\n\tinherits: false;\n\tinitial-value: 0;\n}"
)
// Structural stagger index: a container marks itself `[data-stagger]` and its direct
// children get `--motion-stagger-index` from `:nth-child` (forward, for enter) and
// `--motion-stagger-index-rev` from `:nth-last-child` (reverse, for exit — last leaves
// first) — NO JS writer (the deleted DomCascade wrote it from soma, the root violation).
// Every css preset's enter/exit rule consumes index × `--motion-stagger-each` (default
// 0 → parallel, N → cascade). Capped at 24; past the cap items share the last index.
for (let n = 1; n <= 24; n++) {
blocks.push(`[data-stagger] > *:nth-child(${n}) {\n\t--motion-stagger-index: ${n - 1};\n}`)
blocks.push(`[data-stagger] > *:nth-last-child(${n}) {\n\t--motion-stagger-index-rev: ${n - 1};\n}`)
}
for (const [name, stops] of Object.entries(motion.keyframes ?? {})) { for (const [name, stops] of Object.entries(motion.keyframes ?? {})) {
blocks.push(renderKeyframes(name, stops)) blocks.push(renderKeyframes(name, stops))
@ -789,15 +802,14 @@ function phaseDeclarations(
const ease = `var(--motion-ease-${phase}, var(--ease-${cssPhase.ease ?? defaultEase}))` const ease = `var(--motion-ease-${phase}, var(--ease-${cssPhase.ease ?? defaultEase}))`
const animation = names.map((kf) => `${kf} ${dur} ${ease} ${fill}`).join(', ') const animation = names.map((kf) => `${kf} ${dur} ${ease} ${fill}`).join(', ')
const declarations = [`animation: ${animation};`] const declarations = [`animation: ${animation};`]
// Stagger (Material list choreography, zero JS orchestration): a container // Stagger (Material list choreography, zero JS orchestration): a container sets
// sets `--motion-stagger-each` (rhythm) and each item `--motion-stagger-index`, // `--motion-stagger-each` (rhythm) and each item gets its index FROM STRUCTURE
// so each item's enter is delayed by index × each. Default 0 → no stagger; the // (foundation `[data-stagger]` writer). Enter counts UP (--motion-stagger-index);
// `backwards` fill holds the `from` state until each item's turn. // exit counts DOWN (--motion-stagger-index-rev) so the last item leaves first. Both
if (phase === 'enter') { // default 0 → no stagger (a lone element is unaffected); the `backwards` fill holds
declarations.push( // the enter `from` state until each item's turn.
'animation-delay: calc(var(--motion-stagger-index, 0) * var(--motion-stagger-each, 0ms));' const indexVar = phase === 'enter' ? '--motion-stagger-index' : '--motion-stagger-index-rev'
) declarations.push(`animation-delay: calc(var(${indexVar}, 0) * var(--motion-stagger-each, 0ms));`)
}
if (cssPhase.transformOrigin) { if (cssPhase.transformOrigin) {
declarations.push(`transform-origin: ${cssPhase.transformOrigin};`) declarations.push(`transform-origin: ${cssPhase.transformOrigin};`)
} }

@ -0,0 +1,51 @@
import type { Morfo } from '../types';
import { v } from '../types';
/**
* Cascade — a coordinating container that staggers its children's enter/exit.
*
* Pure `--state` moment, NOT a sema event: the consumer drives `open`, the
* Provider reflects `data-state`, and eidos paints the staggered cascade by
* reacting to `data-state` + the per-item structural index (`:nth-child`, in the
* recipe). Soma's only job is lifecycle — the Item wrapper retains a removed node
* during its exit (`out:`) and marks it leaving so eidos can play the exit.
*
* No events: the container's own perceptual flourish, if any, is a SEPARATE
* `emerge` event declared by whatever wraps it — the Cascade only sequences its
* children. The index is never written: eidos computes it from structure.
*/
export const cascadeMorfo = {
name: 'Cascade',
kebab: 'cascade',
scope: ['eidos'],
events: [],
parts: [
{
name: 'Provider',
kebab: 'provider',
archetype: 'provider',
kind: 'public',
defaultElement: 'div',
optional: false,
states: ['open', 'closed'],
data: [
{
attr: 'data-state',
values: ['open', 'closed'],
value: v.stateRef('open')
}
],
aria: []
},
{
name: 'Item',
kebab: 'item',
archetype: 'content',
kind: 'public',
defaultElement: 'div',
optional: false,
data: [],
aria: []
}
]
} as const satisfies Morfo;

@ -16,6 +16,7 @@
* stamp on the container + sound) would be a SEPARATE, single-target flourish. * stamp on the container + sound) would be a SEPARATE, single-target flourish.
*/ */
import { Card } from '$uix/eidos/components/card' import { Card } from '$uix/eidos/components/card'
import { Cascade } from '$uix/eidos/components/cascade'
import './panel-cascade.css' import './panel-cascade.css'
// SOMA role: state only. Default open so the enter cascade plays on load. // SOMA role: state only. Default open so the enter cascade plays on load.
@ -87,6 +88,31 @@
</div> </div>
{/each} {/each}
</div> </div>
<h2 class="sub">
Productizado: <code>&lt;Cascade&gt;</code> — orquestador sobre el sistema declarado
</h2>
<p class="note">
Sin recipe propio: el contenedor se marca <code>data-stagger</code> (la foundation da el
índice por <code>:nth-child</code>), los items cargan un preset existente
(<code>animation="scale-fade"</code>) + <code>data-state</code>, y el stagger sale del
preset (<code>--motion-stagger-each</code>, 0 = paralelo). Retención <code>out:</code> en el
item. Cero <code>--cascade-*</code>.
</p>
<Cascade
{open}
animation="scale-fade"
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.75rem; padding: 1rem; border: 1px solid var(--color-border-subtle, #e5e5e5); border-radius: var(--radius-lg, 12px); min-block-size: 5rem; --motion-stagger-each: {rhythm}ms"
>
{#each cards as i (i)}
<Cascade.Item>
<Card noEmerge variant="outline" size="sm">
<Card.Title>Card {i + 1}</Card.Title>
<Card.Description>&lt;Cascade.Item&gt; · scale-fade</Card.Description>
</Card>
</Cascade.Item>
{/each}
</Cascade>
</div> </div>
<style> <style>
@ -106,6 +132,19 @@
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
} }
.sub {
font-size: 1.1rem;
font-weight: 600;
margin-block-start: 1.5rem;
}
.note {
margin-block-start: -0.5rem;
}
.bar {
block-size: 0.5rem;
border-radius: 3px;
background: var(--color-content-muted, #8a8a8a);
}
.tag { .tag {
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 500; font-weight: 500;

Loading…
Cancel
Save

Powered by TurnKey Linux.