|
|
|
@ -0,0 +1,449 @@
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
|
|
|
// Motion demo — the CONTENT domain of the universal `motion` prop (RFC §D.12):
|
|
|
|
|
|
|
|
// · LOOPS (infinite): spin · pulse · ping · bounce, applied to plain SVG via
|
|
|
|
|
|
|
|
// `motionAttrs(preset)` (makes any element a content surface).
|
|
|
|
|
|
|
|
// · ENTER / EXIT: `<Motion motion="…">` plays a preset's enter on mount and its
|
|
|
|
|
|
|
|
// exit on removal — shown on the pigeon SVG lifted from the `/demos` paloma.
|
|
|
|
|
|
|
|
// Both stop automatically under `prefers-reduced-motion` (the generated rules).
|
|
|
|
|
|
|
|
import { motionAttrs, type MotionPresetName } from '$uix/eidos';
|
|
|
|
|
|
|
|
import Pigeon from './pigeon.svelte';
|
|
|
|
|
|
|
|
// A standalone `/demos` page has no ActiveEidos provider, so we load the generated
|
|
|
|
|
|
|
|
// eidos foundation (motion keyframes, presets, loops + the `--duration-*` tokens)
|
|
|
|
|
|
|
|
// statically and replicate `<Motion>`'s tiny exit lifecycle inline (motionLeave).
|
|
|
|
|
|
|
|
// Under /uix — where the provider lives — this section would just be `<Motion>`.
|
|
|
|
|
|
|
|
import '@/uix/eidos/index.css';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const loops: { name: MotionPresetName; blurb: string }[] = [
|
|
|
|
|
|
|
|
{ name: 'spin', blurb: 'loaders, sync, progress' },
|
|
|
|
|
|
|
|
{ name: 'pulse', blurb: 'skeletons, “live”, breathing' },
|
|
|
|
|
|
|
|
{ name: 'ping', blurb: 'notification radar' },
|
|
|
|
|
|
|
|
{ name: 'bounce', blurb: 'scroll cues, attention' }
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Enter / exit playground ────────────────────────────────────────────
|
|
|
|
|
|
|
|
let show = $state(true);
|
|
|
|
|
|
|
|
let enterExit = $state<MotionPresetName>('scale-fade');
|
|
|
|
|
|
|
|
let pigeonLoop = $state<MotionPresetName>('bounce');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const enterExitPresets: MotionPresetName[] = ['scale-fade', 'slide-fade', 'fade', 'slide-full'];
|
|
|
|
|
|
|
|
const pigeonLoops: MotionPresetName[] = ['none', 'bounce', 'pulse'];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// `<Motion>`'s exit mechanism, inlined: on removal flip to the exit state so the
|
|
|
|
|
|
|
|
// preset's exit rule plays, then hold the node for its declared duration before
|
|
|
|
|
|
|
|
// Svelte unmounts it. (Enter is just `motionAttrs` on mount.)
|
|
|
|
|
|
|
|
function motionLeave(node: HTMLElement) {
|
|
|
|
|
|
|
|
node.setAttribute('data-state', 'closed');
|
|
|
|
|
|
|
|
const cs = getComputedStyle(node);
|
|
|
|
|
|
|
|
const ms = (parseFloat(cs.animationDelay) + parseFloat(cs.animationDuration)) * 1000 || 200;
|
|
|
|
|
|
|
|
// `tick` (a no-op) makes Svelte run the transition timeline for `duration` and
|
|
|
|
|
|
|
|
// THEN unmount — a `{ duration }`-only return never completes (the node sticks at
|
|
|
|
|
|
|
|
// the exit frame, invisible, and a re-show reuses the dead node).
|
|
|
|
|
|
|
|
return { duration: ms, tick: () => {} };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── Staggered list — container-driven cascade, enter AND exit ──────────────
|
|
|
|
|
|
|
|
const STAGGER_EACH_MS = 65;
|
|
|
|
|
|
|
|
const listItems = ['Inbox', 'Drafts', 'Sent', 'Archive', 'Trash'];
|
|
|
|
|
|
|
|
const listPresets: MotionPresetName[] = ['slide-fade', 'scale-fade', 'fade'];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let listMounted = $state(true);
|
|
|
|
|
|
|
|
let listState = $state<'open' | 'closed'>('open');
|
|
|
|
|
|
|
|
let listPreset = $state<MotionPresetName>('slide-fade');
|
|
|
|
|
|
|
|
let listClosing = $state(false);
|
|
|
|
|
|
|
|
let closeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// The pragmatic CSS bridge: a closing container drives the child EXIT cascade
|
|
|
|
|
|
|
|
// (reverse stagger) while it is RETAINED in the DOM — we hold it for the stagger
|
|
|
|
|
|
|
|
// window (N × each + the item exit), then unmount. The container's `data-state`
|
|
|
|
|
|
|
|
// flips to `closed`; the items inherit it and cascade out, last-first.
|
|
|
|
|
|
|
|
function toggleList() {
|
|
|
|
|
|
|
|
clearTimeout(closeTimer);
|
|
|
|
|
|
|
|
if (listState === 'open') {
|
|
|
|
|
|
|
|
listState = 'closed';
|
|
|
|
|
|
|
|
listClosing = true;
|
|
|
|
|
|
|
|
const windowMs = listItems.length * STAGGER_EACH_MS + 200;
|
|
|
|
|
|
|
|
closeTimer = setTimeout(() => {
|
|
|
|
|
|
|
|
listMounted = false;
|
|
|
|
|
|
|
|
listClosing = false;
|
|
|
|
|
|
|
|
}, windowMs);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
listMounted = true;
|
|
|
|
|
|
|
|
listState = 'open';
|
|
|
|
|
|
|
|
listClosing = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="page">
|
|
|
|
|
|
|
|
<header class="head">
|
|
|
|
|
|
|
|
<p class="eyebrow">UIX · motion · content domain</p>
|
|
|
|
|
|
|
|
<h1>Loops & lifecycle</h1>
|
|
|
|
|
|
|
|
<p class="lede">
|
|
|
|
|
|
|
|
A <code>motion</code> preset on plain content. <strong>Loops</strong> run forever while present;
|
|
|
|
|
|
|
|
<strong>enter / exit</strong> ride the element's mount & removal. No JS engine — the foundation
|
|
|
|
|
|
|
|
CSS does it, and reduced-motion stops it.
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ── LOOPS ─────────────────────────────────────────────────────────── -->
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
|
|
<h2>Loops <span class="muted">— infinite, content domain</span></h2>
|
|
|
|
|
|
|
|
<div class="loop-grid">
|
|
|
|
|
|
|
|
{#each loops as loop (loop.name)}
|
|
|
|
|
|
|
|
<figure class="tile">
|
|
|
|
|
|
|
|
<div class="art">
|
|
|
|
|
|
|
|
{#if loop.name === 'spin'}
|
|
|
|
|
|
|
|
<svg viewBox="0 0 48 48" class="glyph" {...motionAttrs('spin')}>
|
|
|
|
|
|
|
|
<circle cx="24" cy="24" r="17" fill="none" stroke="var(--track)" stroke-width="5" />
|
|
|
|
|
|
|
|
<circle
|
|
|
|
|
|
|
|
cx="24"
|
|
|
|
|
|
|
|
cy="24"
|
|
|
|
|
|
|
|
r="17"
|
|
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
|
|
stroke="#6366f1"
|
|
|
|
|
|
|
|
stroke-width="5"
|
|
|
|
|
|
|
|
stroke-linecap="round"
|
|
|
|
|
|
|
|
stroke-dasharray="70 200"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
|
|
{:else if loop.name === 'pulse'}
|
|
|
|
|
|
|
|
<svg viewBox="0 0 48 48" class="glyph" {...motionAttrs('pulse')}>
|
|
|
|
|
|
|
|
<circle cx="24" cy="24" r="15" fill="#8b5cf6" />
|
|
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
|
|
{:else if loop.name === 'ping'}
|
|
|
|
|
|
|
|
<svg viewBox="0 0 48 48" class="glyph">
|
|
|
|
|
|
|
|
<circle
|
|
|
|
|
|
|
|
cx="24"
|
|
|
|
|
|
|
|
cy="24"
|
|
|
|
|
|
|
|
r="9"
|
|
|
|
|
|
|
|
fill="#ef4444"
|
|
|
|
|
|
|
|
{...motionAttrs('ping')}
|
|
|
|
|
|
|
|
style="transform-origin:center; transform-box:fill-box"
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
<circle cx="24" cy="24" r="9" fill="#ef4444" />
|
|
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
|
|
{:else}
|
|
|
|
|
|
|
|
<svg viewBox="0 0 48 48" class="glyph bounce-art" {...motionAttrs('bounce')}>
|
|
|
|
|
|
|
|
<circle cx="24" cy="22" r="11" fill="#10b981" />
|
|
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<figcaption>
|
|
|
|
|
|
|
|
<code>motion="{loop.name}"</code>
|
|
|
|
|
|
|
|
<span class="muted">{loop.blurb}</span>
|
|
|
|
|
|
|
|
</figcaption>
|
|
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<p class="note">
|
|
|
|
|
|
|
|
Applied with <code>{'{...motionAttrs(preset)}'}</code> on the SVG (or the ring, for <code>ping</code>).
|
|
|
|
|
|
|
|
The preset is the primary trigger — no event, no state to compose with.
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ── ENTER / EXIT ──────────────────────────────────────────────────── -->
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
|
|
<h2>Enter / exit <span class="muted">— the content lifecycle</span></h2>
|
|
|
|
|
|
|
|
<div class="lab">
|
|
|
|
|
|
|
|
<div class="stage">
|
|
|
|
|
|
|
|
{#if show}
|
|
|
|
|
|
|
|
<div class="motion-box" {...motionAttrs(enterExit)} out:motionLeave>
|
|
|
|
|
|
|
|
<div class="pigeon-holder" {...motionAttrs(pigeonLoop)}>
|
|
|
|
|
|
|
|
<Pigeon />
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
|
|
<button class="primary" onclick={() => (show = !show)}>
|
|
|
|
|
|
|
|
{show ? 'Hide' : 'Show'} the pigeon
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label class="ctl">
|
|
|
|
|
|
|
|
<span>enter / exit preset</span>
|
|
|
|
|
|
|
|
<span class="chips" role="radiogroup">
|
|
|
|
|
|
|
|
{#each enterExitPresets as p (p)}
|
|
|
|
|
|
|
|
<button class="chip" data-active={enterExit === p} onclick={() => (enterExit = p)}
|
|
|
|
|
|
|
|
>{p}</button
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label class="ctl">
|
|
|
|
|
|
|
|
<span>loop while shown</span>
|
|
|
|
|
|
|
|
<span class="chips" role="radiogroup">
|
|
|
|
|
|
|
|
{#each pigeonLoops as p (p)}
|
|
|
|
|
|
|
|
<button class="chip" data-active={pigeonLoop === p} onclick={() => (pigeonLoop = p)}
|
|
|
|
|
|
|
|
>{p}</button
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<p class="note">
|
|
|
|
|
|
|
|
<code><Motion motion="{enterExit}"></code> plays enter on mount, exit on removal
|
|
|
|
|
|
|
|
(it retains the node for the eidos-declared duration). The loop on the inner element runs the
|
|
|
|
|
|
|
|
whole time it's present — <strong>enter → loop → exit</strong>, the full content lifecycle.
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ── STAGGERED LIST (container-driven cascade, enter + exit) ──────────── -->
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
|
|
|
<h2>Staggered list <span class="muted">— container-driven cascade, in & out</span></h2>
|
|
|
|
|
|
|
|
<div class="lab">
|
|
|
|
|
|
|
|
<div class="stage stage-list">
|
|
|
|
|
|
|
|
{#if listMounted}
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="stagger-list"
|
|
|
|
|
|
|
|
data-stagger
|
|
|
|
|
|
|
|
data-state={listState}
|
|
|
|
|
|
|
|
style="--motion-stagger-each: {STAGGER_EACH_MS}ms"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{#each listItems as item (item)}
|
|
|
|
|
|
|
|
<div class="stagger-item" data-animation-style={listPreset}>
|
|
|
|
|
|
|
|
<span class="dot"></span>{item}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
|
|
<button class="primary" onclick={toggleList} disabled={listClosing}>
|
|
|
|
|
|
|
|
{listState === 'open' ? 'Cascade out' : 'Cascade in'}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label class="ctl">
|
|
|
|
|
|
|
|
<span>item preset</span>
|
|
|
|
|
|
|
|
<span class="chips" role="radiogroup">
|
|
|
|
|
|
|
|
{#each listPresets as p (p)}
|
|
|
|
|
|
|
|
<button class="chip" data-active={listPreset === p} onclick={() => (listPreset = p)}
|
|
|
|
|
|
|
|
>{p}</button
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<p class="note">
|
|
|
|
|
|
|
|
The <code>[data-stagger]</code> container's <code>data-state</code> drives its direct children.
|
|
|
|
|
|
|
|
Enter cascades in (first item first); <strong>exit cascades out in reverse</strong> (last leaves
|
|
|
|
|
|
|
|
first) via <code>--motion-stagger-index-rev</code>. The pragmatic CSS bridge: the container is
|
|
|
|
|
|
|
|
retained for the stagger window, then unmounts — no JS orchestration engine.
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
|
|
.page {
|
|
|
|
|
|
|
|
--track: #e2e8f0;
|
|
|
|
|
|
|
|
max-width: 920px;
|
|
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
|
|
padding: 3rem 1.5rem 5rem;
|
|
|
|
|
|
|
|
color: #0f172a;
|
|
|
|
|
|
|
|
font-family:
|
|
|
|
|
|
|
|
system-ui,
|
|
|
|
|
|
|
|
-apple-system,
|
|
|
|
|
|
|
|
sans-serif;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.head {
|
|
|
|
|
|
|
|
margin-bottom: 2.5rem;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.eyebrow {
|
|
|
|
|
|
|
|
margin: 0 0 0.4rem;
|
|
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
|
|
color: #6366f1;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
h1 {
|
|
|
|
|
|
|
|
margin: 0 0 0.6rem;
|
|
|
|
|
|
|
|
font-size: 2.4rem;
|
|
|
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.lede {
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
max-width: 60ch;
|
|
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
section {
|
|
|
|
|
|
|
|
margin-top: 3rem;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
h2 {
|
|
|
|
|
|
|
|
margin: 0 0 1.2rem;
|
|
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.muted {
|
|
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.loop-grid {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.tile {
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
gap: 0.75rem;
|
|
|
|
|
|
|
|
padding: 1.25rem;
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
|
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.art {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
height: 92px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.glyph {
|
|
|
|
|
|
|
|
width: 56px;
|
|
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
|
|
overflow: visible;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
figcaption {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
gap: 0.2rem;
|
|
|
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
code {
|
|
|
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
|
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
|
|
padding: 0.1em 0.4em;
|
|
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.note {
|
|
|
|
|
|
|
|
margin: 1.2rem 0 0;
|
|
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.lab {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
|
|
gap: 1.5rem;
|
|
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.stage {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
|
|
|
background: radial-gradient(circle at 50% 35%, #eef2ff 0%, #fff 70%);
|
|
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.motion-box {
|
|
|
|
|
|
|
|
transform-origin: center;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.pigeon-holder {
|
|
|
|
|
|
|
|
width: 230px;
|
|
|
|
|
|
|
|
height: 230px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.stage-list {
|
|
|
|
|
|
|
|
padding: 1.5rem;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.stagger-list {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
gap: 0.6rem;
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
max-width: 260px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.stagger-item {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 0.7rem;
|
|
|
|
|
|
|
|
padding: 0.75rem 1rem;
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
border: 1px solid #e2e8f0;
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.05);
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.stagger-item .dot {
|
|
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
|
|
background: #6366f1;
|
|
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.controls {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
gap: 1.1rem;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.primary {
|
|
|
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
|
|
|
padding: 0.7rem 1.4rem;
|
|
|
|
|
|
|
|
background: #6366f1;
|
|
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
box-shadow: 0 10px 20px rgba(99, 102, 241, 0.25);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.primary:hover {
|
|
|
|
|
|
|
|
background: #4f46e5;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.ctl {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
gap: 0.45rem;
|
|
|
|
|
|
|
|
font-size: 0.78rem;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.chips {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.chip {
|
|
|
|
|
|
|
|
padding: 0.35rem 0.7rem;
|
|
|
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
color: #475569;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.chip[data-active='true'] {
|
|
|
|
|
|
|
|
background: #e0e7ff;
|
|
|
|
|
|
|
|
border-color: #6366f1;
|
|
|
|
|
|
|
|
color: #4338ca;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
|
|
.loop-grid {
|
|
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.lab {
|
|
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
</style>
|