|
|
<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><Cascade></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><Cascade.Item> · 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>
|