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/menu-native/+page.svelte

217 lines
6.6 KiB

<script lang="ts">
import { cubicOut, cubicIn } from 'svelte/easing';
/*
* Fase 0 — Svelte-native menu cascade prototype (THROWAWAY).
*
* Close choreography (RFC `when: { exit: 'after' }` bracket):
* 1. the items leave in a reversed cascade (opacity+transform) — you SEE them
* go; they keep their space, so the cascade isn't masked;
* 2. THEN the panel collapses like a blind (its OWN close firma — the size
* channel of `emerge.close` of the panel), delayed so it "drags" the items.
* The blind is CSS grid `1fr → 0fr` (no JS measuring; visual stays in eidos).
* The panel's delay is tunable below so the drag feel can be dialled in.
*/
let open = $state(false);
let opening = $state(false);
let closing = $state(false);
let openTimer: ReturnType<typeof setTimeout> | undefined;
let closeTimer: ReturnType<typeof setTimeout> | undefined;
let items = $state(['Perfil', 'Facturación', 'Equipo', 'Integraciones', 'Ajustes', 'Cerrar sesión']);
let added = 0;
// Panel-blind tuning. delay = how long after the items start leaving the panel
// begins to collapse → the "drag" feel. Tune it live with the slider.
let panelDelay = $state(250);
const panelDur = 280;
function toggle() {
if (open) {
closing = true;
open = false;
clearTimeout(closeTimer);
closeTimer = setTimeout(() => (closing = false), 1200);
return;
}
closing = false;
opening = true;
open = true;
clearTimeout(openTimer);
openTimer = setTimeout(() => (opening = false), 800);
}
function addItem() {
added += 1;
items.push('Nuevo ' + added);
}
function removeLast() {
if (items.length) items.pop();
}
function readMs(node: Element, prop: string, fallback: number): number {
const raw = getComputedStyle(node).getPropertyValue(prop).trim();
if (!raw) return fallback;
if (raw.endsWith('ms')) return parseFloat(raw);
if (raw.endsWith('s')) return parseFloat(raw) * 1000;
const n = parseFloat(raw);
return Number.isFinite(n) ? n : fallback;
}
function readPx(node: Element, prop: string, fallback: number): number {
const n = parseFloat(getComputedStyle(node).getPropertyValue(prop));
return Number.isFinite(n) ? n : fallback;
}
/**
* Item lifecycle shim — opacity + transform only (items keep their space so the
* cascade is visible). Stagger only during the open/close burst; an individual
* add/remove enters/leaves with delay 0. Values from eidos tokens.
*/
function cascade(
node: Element,
{ index, count, dir }: { index: number; count: number; dir: 'in' | 'out' }
) {
const dur = readMs(node, '--motion-cascade-duration', 240);
const each = readMs(node, '--motion-stagger-each', 55);
const shift = readPx(node, '--proto-cascade-shift', -14);
let delay = 0;
if (dir === 'in' && opening) delay = index * each;
else if (dir === 'out' && closing) delay = (count - 1 - index) * each;
return {
delay,
duration: dur,
easing: dir === 'out' ? cubicIn : cubicOut,
css: (t: number, u: number) => `opacity:${t};transform:translateX(${u * shift}px)`
};
}
/**
* Panel blind — the panel's OWN close firma. CSS grid `1fr → 0fr` collapses the
* height with NO JS measurement. Delayed so the items leave first and the panel
* appears to drag them down.
*/
function blind(node: Element, { delay, duration }: { delay: number; duration: number }) {
// Roll-up: collapse the panel's measured height to 0. `height` IS WAAPI-
// animatable; `grid-template-rows` is NOT (Svelte produced no animation for
// it). Real browsers compute layout, so getBoundingClientRect is reliable.
const h = node.getBoundingClientRect().height;
return {
delay,
duration,
easing: cubicIn,
css: (t: number) => `overflow:hidden;height:${t * h}px`
};
}
</script>
<div class="page">
<h1>Fase 0 — cierre con arrastre (persiana del panel)</h1>
<p class="note">
Al cerrar: los ítems salen en cascada (se ven) y <strong>luego</strong> el panel colapsa como persiana
(<code>grid 1fr→0fr</code>, sin medir en JS), con retardo → el panel los «arrastra». Es la firma de cierre
propia del panel (<code>when: exit: 'after'</code>). Ajusta el retardo:
</p>
<label class="slider">
Retardo de la persiana: <strong>{panelDelay}ms</strong>
<input type="range" min="0" max="700" step="10" bind:value={panelDelay} />
</label>
<div class="controls">
<button class="trigger" onclick={toggle} aria-expanded={open}>
{open ? 'Cerrar' : 'Abrir'} menú
</button>
{#if open}
<button class="add" onclick={addItem}>Añadir ítem</button>
<button class="remove" onclick={removeLast}>Quitar último</button>
{/if}
</div>
{#if open}
<div class="panel" data-proto-panel out:blind={{ delay: panelDelay, duration: panelDur }}>
{#each items as label, i (label)}
<div
class="item"
data-proto-item
data-index={i}
in:cascade|global={{ index: i, count: items.length, dir: 'in' }}
out:cascade|global={{ index: i, count: items.length, dir: 'out' }}
>
{label}
</div>
{/each}
</div>
{/if}
</div>
<style>
.page {
display: flex;
flex-direction: column;
gap: var(--space-4, 1rem);
max-inline-size: 32rem;
padding: var(--space-6, 2rem);
color: var(--color-content-primary, #111);
}
.note {
color: var(--color-content-muted, #666);
font-size: var(--font-size-sm, 0.875rem);
line-height: var(--leading-ui, 1.5);
}
.slider {
display: flex;
flex-direction: column;
gap: var(--space-1, 0.25rem);
align-items: flex-start;
font-size: var(--font-size-sm, 0.875rem);
color: var(--color-content-muted, #666);
}
.controls {
display: flex;
gap: var(--space-2, 0.5rem);
}
button {
padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
border: 1px solid var(--color-border-default, #ccc);
border-radius: var(--radius-md, 0.5rem);
background: var(--color-surface-raised, #fff);
color: inherit;
cursor: pointer;
}
.panel {
--motion-cascade-duration: var(--duration-moderate, 240ms);
--motion-stagger-each: 55ms;
overflow: hidden;
display: flex;
flex-direction: column;
inline-size: 14rem;
padding: var(--space-1, 0.25rem);
border: 1px solid var(--color-border-default, #ccc);
border-radius: var(--radius-md, 0.5rem);
background: var(--color-surface-overlay, #fff);
box-shadow: var(--shadow-overlay, 0 8px 24px rgba(0, 0, 0, 0.12));
}
.item {
--proto-cascade-shift: -14px;
box-sizing: border-box;
padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
border-radius: var(--radius-sm, 0.25rem);
font-size: var(--font-size-sm, 0.875rem);
cursor: pointer;
}
.item:hover {
background: var(--color-primary-element, #eef);
}
</style>

Powered by TurnKey Linux.