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/src/uix/eidos/components/cascade/README.md

7.8 KiB

Cascade

A thin explicit orchestrator that staggers its children's enter/exit. It owns no recipe and no timing of its own — it rides the foundation stagger + the existing state-presets. It is the explicit / content-animation way to sequence a set of elements (you designed them to stagger), as opposed to the event-driven path where a component's own emerge firma + the same foundation stagger do it (see "Two ways" below).

<script>
  import { Cascade } from '$uix/eidos/components/cascade'
  let open = $state(true)
</script>

<Cascade {open} motion="scale-fade" style="display: grid; gap: 0.75rem; --motion-stagger-each: 60ms">
  {#each items as item (item.id)}
    <Cascade.Item>{item.label}</Cascade.Item>
  {/each}
</Cascade>

--motion-stagger-each: 0 (the default) → parallel; N → cascade. The keyframe, duration, easing and reduced-motion all come from the motion preset.

Baseline

No Air baseline — new component (2026-06). The closest prior art in the repo was the ad-hoc per-item transition-delay hand-rolled by early menu demos; Cascade replaces that with the foundation stagger mechanism.

Comparativa

Reference Their shape Difference
Framer Motion (staggerChildren) JS orchestration prop on a motion parent; the library owns the timeline Cascade is CSS-only: the stagger index is structural (:nth-child → --motion-stagger-index), the animation is the child's own state preset — no JS timeline, SSR-safe
Motion One / GSAP stagger() Imperative tween over a NodeList Same difference — Cascade never enumerates children in JS; removed nodes are retained only for their exit
Radix / Ark / Bits No stagger-container primitive A gap they leave to userland; Cascade makes it a composable part
M3 (Compose) AnimatedVisibility + manual delays Per-child delay set by hand Cascade derives the delay from structure; reordering children never desyncs the wave

How it works — it reuses everything

Nothing here is a new system. The cascade is the existing declared motion plus one foundation rule:

Piece Where it's declared / derived
the per-item animation (keyframe, duration, ease, reduced-motion) a state-preset declared in EidosConfig.motion (fade / scale-fade / …), generated by eidos to generated/base.css and registered with uix.motion
the stagger delay (index × --motion-stagger-each, default 0 = parallel) already in every preset's enter/exit rule (lib/render-css.ts)
the structural index (--motion-stagger-index / -rev) the one new foundation rule: [data-stagger] > *:nth-child / :nth-last-child (generated by lib/render-css.ts) — nobody writes it, soma never writes a visual var

<Cascade> just wires those: it marks the container [data-stagger], reflects open as data-state, and passes open + the chosen preset to its items (context), so each item carries data-animation-style + data-state. Enter counts up (:nth-child); exit counts down (:nth-last-child) so the last item leaves first.

Props

<Cascade>

Prop Type Default Notes
open boolean true Drives the staggered enter (forward) / exit (reverse) via data-state.
motion MotionPresetName 'fade' The state-preset every item plays (fade / scale-fade / slide-fade / …). Unified with the overlays' selector (RFC §D.12).

Plus any HTMLAttributes<HTMLDivElement>. Set the layout + the rhythm (--motion-stagger-each) here.

<Cascade.Item>

A participant. On removal it flips to data-state='closed' (the existing exit preset plays, with the reverse structural index), is retained (out:) for the eidos-declared exit duration, then unmounts. Accepts any HTMLAttributes<HTMLDivElement>.

Ownership

Owns Never touches
soma (the wrappers) state (open → data-state, propagated to items) + lifecycle (out: retention) any --motion-* / visual variable — it reads the eidos duration via the ActiveDom getComputedStyle, writes nothing
eidos all the visual — the preset (keyframe/duration/ease/reduced-motion) + the foundation stagger index the semantics / state

Two ways to stagger children

  1. Explicit — <Cascade> (this component). You want these elements to stagger; there is no semantic event. The content/intrinsic animation domain.

  2. Event-driven — a real component (a menu, a list) declares its own emerge firma (the container flourish + sound) and marks its item container [data-stagger]; its items carry a preset (data-animation-style). Same foundation stagger, no <Cascade> wrapper — the appearance flows from the declared event. The container-driven preset rule [data-stagger][data-state='open'] > [data-animation-style] fires the children's enter off the container's data-state (which soma already writes via the morfo commits), so the items carry no per-item data-state — a menuitem isn't open/closed; the menu is. (The firma is generic by event: "children of an emerging container present together, by structural order"; the concrete timing — parallel/cascade, ms, keyframe — is the per-component realization.)

    First concrete consumer: <DropdownMenu> — items fade in staggered on open (src/uix/eidos/components/dropdown-menu/). See its README → Motion. The container-driven rule is enter-only; the coordinated exit cascade (retain the container until its children finish) is the deferred soma PresenceGroup work.

Known limitation — bulk removal

When several items are removed at once (e.g. a slider jumping 10 → 3), the leaving nodes unmount one-by-one as each finishes, so the grid reflows during the cascade and the remaining items' :nth-last-child index re-evaluates. In practice it reads fine, but the glitch-free version wants the container to retain the leaving set as a unit (one reflow at the end) — a soma lifecycle concern (what the retired pending did, without touching the visual). Dragging the slider down step-by-step (a sequence of lone removals) is always clean.

Decisiones

  • No recipe, no --cascade-* namespace, no bespoke keyframe. An earlier prototype forked all of that — it reinvented the existing --motion-stagger-* + state-preset system. This replaces it by reusing them; the only new code is the foundation structural-index writer.
  • Cascade is the EXPLICIT domain. Event-driven children appearance rides the firma + the same foundation stagger, declared in the component's own morfo — not this wrapper.
  • Index from :nth-child, capped 1–24 (the foundation writer). Beyond the cap items share the last index until sibling-index() is broadly supported.

Gaps

  • No demo page yet (deferred to the demo/docs web phase) — disposition: implementar.
  • Bulk removal reflows mid-cascade (see "Known limitation" above) — the fix is the deferred soma PresenceGroup retention — disposition: diferir.
  • No per-cascade rhythm prop; the consumer sets --motion-stagger-each on the container style — disposition: diferir (the var IS the API for now).

Passive justification

Zero events by design: Cascade is the stage for its children's state animations — it declares no interaction, focus, or keyboard surface (the children own theirs), and its own flourish, if any, belongs to a wrapping component's emerge firma. scope: ['eidos'], events: [] in the morfo.

Audit exceptions

  • R-1.1 exception: Cascade deliberately ships no recipe — it rides the foundation's structural stagger index plus each child's state preset; a [data-cascade] rule would style nothing (see Decisiones and the note kept in cascade.css).

Powered by TurnKey Linux.