You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/temas/animations/dom-cascade/+page.svelte

243 lines
8.3 KiB

<script lang="ts">
/**
* DomCascade — children-DOM stagger (RFC §M9), validated in ISOLATION before the
* menu. Unlike Reveal/Rail (each item is a registered child `Presence`), here the
* owner is ONE `Presence` (the container) and the items are STATIC DOM — exactly
* the shape a real menu/list has. The `DomCascade` mirrors the owner's
* `transitionAttrs` + a per-DOM-order `--motion-stagger-index` onto each item,
* reusing eidos's coordinated preset UNCHANGED (the items end up carrying the same
* attrs a child `Presence` would have produced).
*
* F1b validated the ENTER cascade (the items stagger in when the container opens).
* F1c closes the EXIT: the owner `Presence` cannot see the items' transitions on its
* own node (`getAnimations()`, no subtree), so the `DomCascade` exposes a `pending()`
* that the owner aggregates via `PresenceOptions.pending` — it now HOLDS the subtree
* until the cascade settles instead of dropping the items mid-stagger. Inherits the
* ActiveUix + Soma + Eidos scope from `../+layout.svelte`.
*/
import { ActiveEidos } from '$uix/eidos';
import { readableActive } from '$libs/reactive';
import { Presence } from '$soma/layers/presence.svelte';
import { DomCascade } from '$soma/layers/dom-cascade.svelte';
const eidos = ActiveEidos.require();
const ITEMS = ['Perfil', 'Ajustes', 'Notificaciones', 'Equipo', 'Facturación', 'Salir'];
const STYLES = [
{ value: 'cascade-slide', label: 'slide' },
{ value: 'cascade-fade', label: 'fade' },
{ value: 'cascade-scale', label: 'scale' }
];
let open = $state(false);
let animStyle = $state('cascade-slide');
let containerEl = $state<HTMLElement | null>(null);
// ONE Presence for the owner (the container). It owns the lifecycle; the cascade
// only mirrors it onto the DOM items. F1c: on exit the owner awaits the items'
// cascade (`pending`) before unmounting — their transitions are invisible to the
// owner's `getAnimations()` (no subtree), so without this the container would drop
// mid-stagger on close. `cascade` is referenced lazily (only when `pending` fires).
// Explicit annotations break the inference cycle: `presence.pending` reads
// `cascade`, whose `ownerTransitionAttrs` reads `presence.transitionAttrs`.
const presence: Presence = new Presence({
dom: eidos.dom,
open: readableActive(() => open),
ref: readableActive(() => containerEl),
pending: () => cascade.pending()
});
// The propagator: route `animStyle` to each DOM child, mirror the owner's
// transition attrs, number them by DOM order. `items()` is a plain selector read —
// the same shape a menu's `getItems` uses.
const cascade: DomCascade = new DomCascade({
dom: eidos.dom,
animationStyle: readableActive(() => animStyle),
ownerTransitionAttrs: readableActive(() => presence.transitionAttrs),
items: () =>
containerEl ? Array.from(containerEl.querySelectorAll<HTMLElement>('[data-dc-item]')) : []
});
cascade.watch();
</script>
<svelte:head>
<title>DomCascade · children-DOM (M9 F1c)</title>
</svelte:head>
<div class="root">
<header>
<a class="back" href="/temas/animations">← Motion</a>
<h1>DomCascade <small>children-DOM · M9 F1c</small></h1>
<p class="lede">
La <strong>segunda coordinación</strong> del servicio (RFC §M9). A diferencia de
<a href="/temas/animations/reveal">Reveal</a> / <a href="/temas/animations/rail">Rail</a>
—donde cada ítem es un <code>Presence</code> registrado— aquí el owner es <strong>un solo</strong>
<code>Presence</code> (el contenedor) y los ítems son <strong>DOM estático</strong>: justo la
forma de un menú real. El propagador <code>DomCascade</code>
<strong>espeja</strong> el <code>data-starting/ending-style</code> del owner + un
<code>--motion-stagger-index</code> por orden DOM en cada ítem, <strong>reutilizando el preset
coordinado de eidos sin cambiarlo</strong>. Es la grieta que el menú destapó, ya pavimentada.
</p>
<p class="lede note">
F1b validó la <strong>entrada</strong>; <strong>F1c</strong> cierra la
<strong>salida</strong>: el owner ahora <strong>espera</strong> a los ítems antes de
desmontar. Su <code>getAnimations()</code> no ve las transiciones de los ítems —sin
<code>subtree</code>— así que el <code>DomCascade</code> expone un <code>pending()</code> que el
<code>Presence</code> agrega. Ciérrala: los ítems salen en cascada <strong>antes</strong> de que
el contenedor desaparezca.
</p>
<div class="actions">
<button class="btn primary" type="button" onclick={() => (open = !open)}>
{open ? 'Ocultar' : 'Mostrar'} lista
</button>
<div class="picker" role="group" aria-label="Estilo de animación">
{#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>
</header>
<!-- The stage feeds the cascade its rhythm; the items inherit it. The container is
the owner `Presence` (its transition attrs drive the cascade); the items are
plain DOM the propagator writes onto. -->
<div class="stage" style="--motion-cascade-duration: 320ms; --motion-stagger-each: 55ms">
{#if presence.isPresent}
<div class="container" bind:this={containerEl} {...presence.transitionAttrs}>
{#each ITEMS as label (label)}
<div class="dc-item" data-dc-item>{label}</div>
{/each}
</div>
{/if}
</div>
</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.note {
font-size: 0.85rem;
color: var(--color-content-muted, #777);
}
.lede a {
color: var(--color-primary-solid, #4f46e5);
}
.actions {
display: flex;
align-items: center;
gap: 1rem;
margin-block-start: 1.25rem;
flex-wrap: wrap;
}
.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);
}
.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: 360px;
}
.container {
display: flex;
flex-direction: column;
gap: 0.4rem;
width: min(280px, 100%);
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));
}
/* The item's base look. The off-state + transition + stagger come from eidos's
coordinated preset (generated/base.css), applied via the attrs the DomCascade
propagates — NO animation CSS here. */
.dc-item {
padding: 0.5rem 0.75rem;
border-radius: 8px;
background: var(--color-surface-default, #fff);
border: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.1));
font-size: 0.9rem;
}
</style>

Powered by TurnKey Linux.