Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>active-uix
parent
48b183672a
commit
e9fc493823
@ -0,0 +1,296 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Compuesto · presets de la LIBRERÍA de eidos.
|
||||
*
|
||||
* Unlike the `reveal` / `rail` demos — which hand-write the `cascade-*` CSS on
|
||||
* this page — this one uses the eidos BUILT-IN coordinated presets
|
||||
* (`BUILTIN_COORDINATED_PRESETS` → `generated/base.css`): there is ZERO
|
||||
* animation CSS here. The component just names `animation="cascade-slide"`; the
|
||||
* library supplies the off-state, the transition and the reversible stagger
|
||||
* (gated on the Presence's `data-starting/ending-style`, M6). The picker swaps
|
||||
* the prop live — the LOOK changes with no page CSS touched.
|
||||
*
|
||||
* It also COMPOSES the two orthogonal feedback systems (see /temas/animations
|
||||
* → the sema↔motion explainer): the Rail.Item surfaces cascade in (motion
|
||||
* coordinated, `data-animation-style`), while each <Button> inside fires its
|
||||
* own press firma (sema `contact-activate` → `press-squeeze` on
|
||||
* `data-event-family='contact'`). Two nodes, two axes, no conflict — pressing a
|
||||
* button squeezes it while the row stays put.
|
||||
*
|
||||
* Inherits the ActiveUix + Soma + Eidos scope from `../+layout.svelte`.
|
||||
*/
|
||||
import * as Rail from '$soma/components/rail';
|
||||
import { Button } from '$uix/eidos/components/button';
|
||||
|
||||
// The items are real <Button>s — one per variant, to show the prominence scale
|
||||
// (solid → soft → surface → outline → ghost). The variant is the VISUAL axis;
|
||||
// every button fires the SAME generic `contact` press (no intent), per book
|
||||
// cap. 22 §11 — the press is the gesture's reception, not its evaluation.
|
||||
const ITEMS = [
|
||||
{ label: 'Guardar', variant: 'solid' },
|
||||
{ label: 'Compartir', variant: 'soft' },
|
||||
{ label: 'Duplicar', variant: 'surface' },
|
||||
{ label: 'Archivar', variant: 'outline' },
|
||||
{ label: 'Eliminar', variant: 'ghost' }
|
||||
] as const;
|
||||
|
||||
// The THREE built-in coordinated presets of eidos — nothing page-local.
|
||||
const STYLES = [
|
||||
{ value: 'cascade-slide', label: 'slide' },
|
||||
{ value: 'cascade-fade', label: 'fade' },
|
||||
{ value: 'cascade-scale', label: 'scale' }
|
||||
];
|
||||
|
||||
let open = $state(true);
|
||||
let animStyle = $state('cascade-slide');
|
||||
let duration = $state(320); // ms → drives the preset's `--motion-cascade-duration`
|
||||
let stagger = $state(60); // ms → the rhythm between items (`--motion-stagger-each`)
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Compuesto · presets de la librería de eidos</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="root">
|
||||
<header>
|
||||
<a class="back" href="/temas/animations">← Motion</a>
|
||||
<h1>Compuesto <small>presets predefinidos de eidos</small></h1>
|
||||
<p class="lede">
|
||||
A diferencia de <a href="/temas/animations/rail">rail</a> y
|
||||
<a href="/temas/animations/reveal">reveal</a> —que escriben el CSS de
|
||||
<code>cascade-*</code> a mano en la página— este compuesto usa los
|
||||
<strong>presets coordinados de la LIBRERÍA</strong> de eidos
|
||||
(<code>BUILTIN_COORDINATED_PRESETS</code> → <code>generated/base.css</code>). Esta página no
|
||||
tiene <strong>ni una línea</strong> de CSS de animación: el componente solo nombra
|
||||
<code>animation="cascade-slide"</code> y la librería aporta el off-state, la transición y el
|
||||
stagger reversible (sobre <code>data-starting/ending-style</code>, M6). El selector cambia la
|
||||
<strong>prop en vivo</strong> — cambia el look sin tocar la página.
|
||||
</p>
|
||||
<p class="lede compose">
|
||||
Y <strong>compone los dos ejes</strong>: las superficies <code>Rail.Item</code> entran en
|
||||
cascada (<em>motion coordinado</em>, <code>data-animation-style</code>) mientras cada
|
||||
<code><Button></code> dispara su propia firma de press
|
||||
(<em>sema</em>, <code>contact-activate</code> → <code>press-squeeze</code>). Dos nodos, dos
|
||||
sistemas, sin conflicto: al pulsar un botón se comprime él, la fila no se mueve.
|
||||
</p>
|
||||
<div class="actions">
|
||||
<button class="btn primary" type="button" onclick={() => (open = !open)}>
|
||||
{open ? 'Ocultar' : 'Mostrar'} barra
|
||||
</button>
|
||||
<div class="picker" role="group" aria-label="Preset de animación (librería de eidos)">
|
||||
{#each STYLES as s (s.value)}
|
||||
<button
|
||||
class="chip-btn"
|
||||
class:on={animStyle === s.value}
|
||||
type="button"
|
||||
onclick={() => (animStyle = s.value)}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sliders">
|
||||
<label>
|
||||
duración
|
||||
<input type="range" min="80" max="700" step="20" bind:value={duration} />
|
||||
<span class="val">{duration}ms</span>
|
||||
</label>
|
||||
<label>
|
||||
stagger
|
||||
<input type="range" min="0" max="120" step="5" bind:value={stagger} />
|
||||
<span class="val">{stagger}ms</span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="hint">
|
||||
Para la cascada fluida, abre esta página enfocada en el browser (en preview de fondo el rAF se
|
||||
throttlea). La animación NO está aquí — sale de la foundation generada.
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<!-- The stage feeds the library preset its knobs, which INHERIT to the surfaces:
|
||||
`--motion-cascade-duration` (speed), `--motion-stagger-each` (rhythm) and
|
||||
`--motion-stagger-count` (so the exit stagger can reverse). The per-item
|
||||
`--motion-stagger-index` is written by soma automatically (M6). -->
|
||||
<div
|
||||
class="stage"
|
||||
style="--motion-cascade-duration: {duration}ms; --motion-stagger-each: {stagger}ms; --motion-stagger-count: {ITEMS.length}"
|
||||
>
|
||||
<Rail.Provider bind:open animation={animStyle}>
|
||||
{#each ITEMS as item (item.label)}
|
||||
<Rail.Item>
|
||||
<Button variant={item.variant}>{item.label}</Button>
|
||||
</Rail.Item>
|
||||
{/each}
|
||||
</Rail.Provider>
|
||||
</div>
|
||||
|
||||
<pre class="code">{`<Rail.Provider animation="cascade-slide"> <!-- a library preset, no page CSS -->
|
||||
{#each items as item}
|
||||
<Rail.Item>
|
||||
<Button variant={item.variant}>{item.label}</Button>
|
||||
</Rail.Item>
|
||||
{/each}
|
||||
</Rail.Provider>`}</pre>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
min-height: 100dvh;
|
||||
background: var(--color-surface-default, #fff);
|
||||
color: var(--color-content-primary, #111);
|
||||
font-family: var(--font-family-primary, 'Inter', system-ui, sans-serif);
|
||||
padding: 2.5rem 1.5rem 4rem;
|
||||
}
|
||||
header {
|
||||
max-width: 880px;
|
||||
margin-inline: auto;
|
||||
margin-block-end: 2rem;
|
||||
}
|
||||
.back {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-content-muted, #666);
|
||||
text-decoration: none;
|
||||
}
|
||||
.back:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
h1 {
|
||||
margin: 0.5rem 0 0.75rem;
|
||||
font-size: 1.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
h1 small {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-content-muted, #666);
|
||||
}
|
||||
.lede {
|
||||
margin: 0 0 0.75rem;
|
||||
max-width: 66ch;
|
||||
line-height: 1.6;
|
||||
color: var(--color-content-secondary, #444);
|
||||
}
|
||||
.lede.compose {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
.lede a {
|
||||
color: var(--color-primary-solid, #4f46e5);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-block-start: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.hint {
|
||||
display: inline-block;
|
||||
margin-block-start: 0.75rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-content-muted, #888);
|
||||
max-width: 56ch;
|
||||
}
|
||||
.btn {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
padding: 0.4rem 0.9rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border-default, rgba(0, 0, 0, 0.15));
|
||||
background: var(--color-surface-default, #fff);
|
||||
color: var(--color-content-primary, #111);
|
||||
}
|
||||
.btn.primary {
|
||||
background: var(--color-primary-solid, #4f46e5);
|
||||
color: var(--color-primary-contrast, #fff);
|
||||
border-color: transparent;
|
||||
}
|
||||
.picker {
|
||||
display: inline-flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 10px;
|
||||
background: var(--color-surface-muted, rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
.chip-btn {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
padding: 0.3rem 0.7rem;
|
||||
border-radius: 7px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-content-secondary, #555);
|
||||
}
|
||||
.chip-btn.on {
|
||||
background: var(--color-primary-solid, #4f46e5);
|
||||
color: var(--color-primary-contrast, #fff);
|
||||
}
|
||||
.sliders {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-block-start: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sliders label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-content-secondary, #555);
|
||||
}
|
||||
.sliders input[type='range'] {
|
||||
accent-color: var(--color-primary-solid, #4f46e5);
|
||||
}
|
||||
.sliders .val {
|
||||
min-width: 3.5ch;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--color-content-muted, #888);
|
||||
}
|
||||
.stage {
|
||||
max-width: 880px;
|
||||
margin-inline: auto;
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
border: 1px dashed var(--color-border-subtle, rgba(0, 0, 0, 0.12));
|
||||
background: var(--color-surface-raised, #f7f7f8);
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* The Rail surfaces — STATIC layout only. NO transition, NO off-state: those
|
||||
come entirely from the eidos library preset (the whole point of this demo).
|
||||
soma stamps data-animation-style + data-starting/ending-style; the generated
|
||||
CSS reacts. The item is a transparent animable wrapper around the Button. */
|
||||
:global([data-rail]) {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 12px;
|
||||
background: var(--color-surface-overlay, rgba(99, 102, 241, 0.08));
|
||||
border: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
:global([data-rail-item]) {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.code {
|
||||
max-width: 880px;
|
||||
margin: 1.25rem auto 0;
|
||||
overflow-x: auto;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
background: var(--color-surface-default, #fff);
|
||||
border: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.12));
|
||||
font-family: var(--font-family-mono, 'Roboto Mono', ui-monospace, monospace);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-content-secondary, #555);
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue