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/panel-cascade/+page.svelte

196 lines
6.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts">
/**
* Fase 1 prototype — panel → cards cascade on the VERIFIED model (RFC §D.9).
*
* Doctrine being demonstrated:
* - SOMA role (here the page) owns ONLY state + LIFECYCLE: it flips `data-state`
* on the container, and it RETAINS a removed card during its exit (Svelte-native
* `out:` — Decision A). It writes NO visual variable.
* - EIDOS owns ALL the visual: the cascade is plain global CSS in `panel-cascade.css`
* (a real recipe ships like that — no Svelte `<style>` scoping/pruning). The per-card
* stagger index is computed FROM STRUCTURE via `:nth-child` / `:nth-last-child`.
* - The cards defer their own emerge (`noEmerge`) so the container coordinates.
*
* The per-card cascade is the `--state` moment (driven by `data-state`), NOT the
* `--event` moment: the panel's own `emerge.open` firma (sema → one `data-event-*`
* stamp on the container + sound) would be a SEPARATE, single-target flourish.
*/
import { Card } from '$uix/eidos/components/card'
import { Cascade } from '$uix/eidos/components/cascade'
import './panel-cascade.css'
// SOMA role: state only. Default open so the enter cascade plays on load.
let open = $state(true)
let count = $state(6)
let rhythm = $state(70)
const dataState = $derived(open ? 'open' : 'closed')
const cards = $derived(Array.from({ length: count }, (_, i) => i))
/**
* SOMA role — LIFECYCLE retention, not visual. When a card is removed, Svelte
* would unmount it instantly (no exit). This `out:` keeps the node in the DOM for
* the duration EIDOS declares for its exit, and marks `data-leaving` so the eidos
* CSS (`[data-leaving]` → card-fall) plays. We read the duration FROM the eidos
* rule — soma never invents a timing or writes a visual var; it only retains.
*/
function leave(node: HTMLElement) {
node.setAttribute('data-leaving', '')
// retain for the FULL exit (its staggered delay + the animation), read from the
// eidos rule — soma never invents a timing, it only retains long enough.
const cs = getComputedStyle(node)
const ms = (parseFloat(cs.animationDelay) + parseFloat(cs.animationDuration)) * 1000 || 200
return { duration: ms }
}
</script>
<div class="page">
<header>
<h1>Panel → cards cascade <span class="tag">Fase 1 · modelo verificado</span></h1>
<p>
El estado y el <strong>lifecycle</strong> los lleva la página (rol de soma): cambia
<code>data-state</code> y RETIENE la card que sale (<code>out:</code>). La cascada es
<strong>CSS puro de eidos</strong> (<code>panel-cascade.css</code>); el índice por card sale
de <code>:nth-child</code> — nadie escribe una <code>--var</code> por card.
</p>
</header>
<div class="controls">
<button class="btn" onclick={() => (open = !open)}>
{open ? 'Cerrar panel' : 'Abrir panel'} ({dataState})
</button>
<label>
cards: {count}
<input type="range" min="1" max="16" bind:value={count} />
</label>
<label>
ritmo: {rhythm}ms
<input type="range" min="20" max="150" step="5" bind:value={rhythm} />
</label>
</div>
<!-- The coordinating container. SOMA sets data-state + retains leaving items; the
rhythm is one eidos token on the container (inherited). NO per-card var. -->
<div data-cascade-panel data-state={dataState} style="--card-stagger-each: {rhythm}ms">
{#each cards as i (i)}
<div data-cascade-item out:leave>
<Card noEmerge variant="soft" size="sm">
<Card.Title>Card {i + 1}</Card.Title>
<Card.Description>row index {i} · delay {i}×{rhythm}ms</Card.Description>
<!-- nested: inner content that cascades AFTER the card, composing the
inherited --row with its own --inner-row (both pure CSS / nth-child) -->
<div data-inner-list>
<div data-inner-item></div>
<div data-inner-item></div>
<div data-inner-item></div>
</div>
</Card>
</div>
{/each}
</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>motion="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}
motion="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>
<style>
.page {
max-inline-size: 56rem;
margin-inline: auto;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
font-family: var(--font-ui, system-ui);
color: var(--color-content-primary, #1a1a1a);
}
h1 {
font-size: 1.5rem;
display: flex;
align-items: center;
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 {
font-size: 0.7rem;
font-weight: 500;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-full, 999px);
background: var(--color-primary-element, #e0e7ff);
color: var(--color-primary-text, #3730a3);
}
p {
color: var(--color-content-muted, #555);
line-height: 1.5;
max-inline-size: 48rem;
}
code {
font-family: var(--font-mono, ui-monospace);
background: var(--color-surface-muted, #f1f1f1);
padding: 0.05rem 0.3rem;
border-radius: 4px;
font-size: 0.85em;
}
.controls {
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
padding: 1rem;
border-radius: var(--radius-md, 8px);
background: var(--color-surface-muted, #f6f6f6);
}
.controls label {
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.85rem;
color: var(--color-content-muted, #555);
}
.btn {
padding: 0.5rem 1rem;
border-radius: var(--radius-md, 8px);
border: 1px solid var(--color-border-default, #ccc);
background: var(--color-surface-default, #fff);
font: inherit;
cursor: pointer;
}
.btn:hover {
background: var(--color-surface-raised, #fafafa);
}
</style>

Powered by TurnKey Linux.