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/presence-group/+page.svelte

245 lines
7.1 KiB

<script lang="ts">
/**
* PresenceGroup — práctica.
*
* The motion service's presence coordination (RFC: eidos/MOTION_SERVICE_RFC.md
* §7–§9), shown in isolation. Each card is a coordinated list (an owner
* container + its child items) wired through the real soma `PresenceGroup` +
* `Presence`, one per `when` mode. Discovery is by Svelte context (§7.1); the
* visual stagger is pure CSS; soma only sequences WHEN each surface enters /
* exits / unmounts — never the look. Inherits the `ActiveUix + Soma + Eidos`
* scope from `../+layout.svelte`.
*/
import CoordList from './coord-list.svelte';
type Phase = 'before' | 'after' | 'together';
type Mode = { id: string; enter: Phase; exit: Phase; badge: string; desc: string };
const MODES: Mode[] = [
{
id: 'bracket',
enter: 'before',
exit: 'after',
badge: "enter:'before' · exit:'after'",
desc: 'El contenedor BRACKETEA a sus hijos: entra antes que ellos y sale después (reteniendo su DOM hasta que terminan). El caso natural de un contenedor — justo lo que un when simétrico no podía expresar.'
},
{
id: 'exit-heavy',
enter: 'together',
exit: 'after',
badge: "enter:'together' · exit:'after'",
desc: 'Entran a la vez; al cerrar, los hijos salen en stagger y el contenedor aguanta hasta que su salida agrega. La lista exit-heavy de la RFC §13.'
},
{
id: 'together',
enter: 'together',
exit: 'together',
badge: "enter/exit:'together'",
desc: 'Todo en paralelo; el escalonado visual lo pone solo el CSS (animation-delay × índice). soma agrega el finished del árbol.'
}
];
const open = $state<Record<string, boolean>>({
bracket: false,
'exit-heavy': false,
together: false
});
const toggleAll = () => {
const next = !MODES.every((m) => open[m.id]);
for (const m of MODES) open[m.id] = next;
};
// Reversa demo (RFC §8.3): flip the card, then flip it BACK mid-animation (the
// transitions are ~300ms). The group's generation token must keep the surfaces
// mounted through the reversal — re-opening mid-exit must NOT unmount→remount.
const reversa = (id: string) => {
const back = open[id];
open[id] = !back;
setTimeout(() => (open[id] = back), 150);
};
</script>
<svelte:head>
<title>PresenceGroup · práctica</title>
</svelte:head>
<div class="root">
<header>
<a class="back" href="/temas/animations">← Motion</a>
<h1>PresenceGroup <small>coordinación de presencia</small></h1>
<p class="lede">
La capa de coordinación del servicio de motion, en aislado. Cada tarjeta es una lista
coordinada —un contenedor <strong>owner</strong> y sus ítems <strong>child</strong>— cableada
por el <code>PresenceGroup</code> + <code>Presence</code> reales de soma, uno por modo
<code>when</code>. Los ítems descubren el grupo por <strong>context</strong> (§7.1); el
stagger es CSS puro; soma solo decide <strong>cuándo</strong> entra / sale / se desmonta cada
superficie. Fíjate en <code>after</code>: al cerrar, los ítems salen y el contenedor
<strong>aguanta</strong> hasta que terminan. Y <strong>↻ Reversa</strong> reabre a media
salida (§8.3): el token de generación del grupo no desmonta lo que vuelve —
<strong>sin parpadeo</strong>.
</p>
<div class="actions">
<button class="btn primary" type="button" onclick={toggleAll}>Abrir / cerrar todas</button>
<span class="hint"
>Para la animación fluida, abre esta página enfocada en el browser (en preview de fondo el
rAF se throttlea).</span
>
</div>
</header>
<div class="grid">
{#each MODES as m (m.id)}
<section class="card">
<div class="card-head">
<code class="badge">{m.badge}</code>
<div class="card-actions">
<button
class="btn ghost"
type="button"
aria-label="Reversa"
title="Reversa: flip + flip back mid-animation (§8.3)"
onclick={() => reversa(m.id)}
>
↻
</button>
<button class="btn" type="button" onclick={() => (open[m.id] = !open[m.id])}>
{open[m.id] ? 'Cerrar' : 'Abrir'}
</button>
</div>
</div>
<p class="desc">{m.desc}</p>
<div class="stage">
<CoordList enter={m.enter} exit={m.exit} open={open[m.id]} count={5} />
</div>
</section>
{/each}
</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;
max-width: 64ch;
line-height: 1.6;
color: var(--color-content-secondary, #444);
}
.actions {
display: flex;
align-items: center;
gap: 1rem;
margin-block-start: 1.25rem;
flex-wrap: wrap;
}
.hint {
font-size: 0.78rem;
color: var(--color-content-muted, #888);
max-width: 42ch;
}
.grid {
max-width: 880px;
margin-inline: auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1rem;
}
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1.25rem;
border-radius: 16px;
background: var(--color-surface-raised, #f7f7f8);
border: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.1));
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
flex-wrap: wrap;
}
.card-actions {
display: flex;
gap: 0.4rem;
}
.btn.ghost {
background: transparent;
color: var(--color-content-muted, #666);
font-size: 0.95rem;
line-height: 1;
padding: 0.4rem 0.55rem;
}
.badge {
font-family: var(--font-family-mono, 'Roboto Mono', ui-monospace, monospace);
font-size: 0.78rem;
padding: 0.15em 0.5em;
border-radius: 6px;
background: color-mix(in srgb, var(--color-primary-solid, #4f46e5) 12%, transparent);
color: var(--color-primary-text, var(--color-primary-solid, #4f46e5));
}
.desc {
margin: 0;
font-size: 0.82rem;
line-height: 1.5;
color: var(--color-content-secondary, #555);
}
.stage {
min-height: 220px;
display: flex;
align-items: flex-start;
padding: 0.75rem;
border-radius: 12px;
border: 1px dashed var(--color-border-subtle, rgba(0, 0, 0, 0.12));
background: var(--color-surface-default, #fff);
}
.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;
}
.btn:hover {
border-color: var(--color-primary-solid, #4f46e5);
}
</style>

Powered by TurnKey Linux.