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.
253 lines
7.3 KiB
253 lines
7.3 KiB
<script lang="ts">
|
|
/**
|
|
* Rail — the SECOND consumer of the coordinated-motion helper
|
|
* (`soma/layers/coordination.ts`). Same coordination as `Reveal`, DIFFERENT shape:
|
|
* the root IS the owner surface, the relation is `together` (all parallel, CSS
|
|
* stagger), it is PARENT-CONTROLLED via `open` (no trigger), and has NO sema events.
|
|
* Proves the helper is reusable — building this was a handful of lines.
|
|
* Inherits the ActiveUix + Soma + Eidos scope from `../+layout.svelte`.
|
|
*/
|
|
import * as Rail from '$soma/components/rail';
|
|
|
|
const ITEMS = ['Inicio', 'Buscar', 'Mensajes', 'Perfil', 'Ajustes'];
|
|
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 duration = $state(300); // ms — per-surface transition speed (exposed as a CSS var)
|
|
let stagger = $state(45); // ms — the rhythm between items (`--motion-stagger-each`)
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Rail · 2º consumidor coordinado</title>
|
|
</svelte:head>
|
|
|
|
<div class="root">
|
|
<header>
|
|
<a class="back" href="/temas/animations">← Motion</a>
|
|
<h1>Rail <small>2º consumidor del helper coordinado</small></h1>
|
|
<p class="lede">
|
|
Misma coordinación que <a href="/temas/animations/reveal">Reveal</a>, otra forma: la raíz
|
|
<strong>ES</strong> la superficie owner, la relación es <code>together</code> (todo en
|
|
paralelo, el escalonado lo pone el CSS), es <strong>controlada por el padre</strong>
|
|
(<code>bind:open</code>, sin trigger) y <strong>no tiene eventos sema</strong>. Todo el wiring
|
|
—PresenceGroup + routing + auto-stagger— sale del helper compartido
|
|
<code>soma/layers/coordination.ts</code>; construir este componente fueron cuatro líneas.
|
|
</p>
|
|
<div class="actions">
|
|
<button class="btn primary" type="button" onclick={() => (open = !open)}>
|
|
{open ? 'Ocultar' : 'Mostrar'} rail
|
|
</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>
|
|
<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>
|
|
</header>
|
|
|
|
<div
|
|
class="stage"
|
|
style="--cascade-duration: {duration}ms; --motion-stagger-each: {stagger}ms; --motion-stagger-count: {ITEMS.length}"
|
|
>
|
|
<Rail.Provider bind:open animation={animStyle}>
|
|
{#each ITEMS as label (label)}
|
|
<Rail.Item class="chip">{label}</Rail.Item>
|
|
{/each}
|
|
</Rail.Provider>
|
|
</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);
|
|
}
|
|
.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: 140px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.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);
|
|
}
|
|
|
|
/* The Rail surfaces — global (rendered by the soma component). HORIZONTAL row;
|
|
soma stamps data-starting/ending-style, the CSS only reacts. */
|
|
:global([data-rail]) {
|
|
display: 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));
|
|
transition:
|
|
opacity var(--cascade-duration, 0.3s) ease,
|
|
transform var(--cascade-duration, 0.3s) ease;
|
|
}
|
|
:global([data-rail-item]) {
|
|
padding: 0.45rem 0.85rem;
|
|
border-radius: 999px;
|
|
background: var(--color-surface-default, #fff);
|
|
border: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.1));
|
|
font-size: 0.85rem;
|
|
white-space: nowrap;
|
|
transition:
|
|
opacity var(--cascade-duration, 0.3s) ease,
|
|
transform var(--cascade-duration, 0.3s) ease;
|
|
transition-delay: calc(var(--motion-stagger-index, 0) * var(--motion-stagger-each, 0ms));
|
|
}
|
|
/* Close = open reversed (last chip leaves first). */
|
|
:global([data-rail-item][data-ending-style]) {
|
|
transition-delay: calc(
|
|
(var(--motion-stagger-count, 1) - 1 - var(--motion-stagger-index, 0)) *
|
|
var(--motion-stagger-each, 0ms)
|
|
);
|
|
}
|
|
|
|
/* `cascade-*` (non-eidos-preset names) — react to the Presence attrs, not data-state. */
|
|
:global([data-animation-style='cascade-slide'][data-starting-style]),
|
|
:global([data-animation-style='cascade-slide'][data-ending-style]) {
|
|
opacity: 0;
|
|
transform: translateX(-12px);
|
|
}
|
|
:global([data-animation-style='cascade-fade'][data-starting-style]),
|
|
:global([data-animation-style='cascade-fade'][data-ending-style]) {
|
|
opacity: 0;
|
|
}
|
|
:global([data-animation-style='cascade-scale'][data-starting-style]),
|
|
:global([data-animation-style='cascade-scale'][data-ending-style]) {
|
|
opacity: 0;
|
|
transform: scale(0.85);
|
|
}
|
|
</style>
|