|
1 | 1 | # @effex/dom |
2 | 2 |
|
| 3 | +## 1.2.0 |
| 4 | + |
| 5 | +### Minor Changes |
| 6 | + |
| 7 | +- b650fd8: Add animation groups — declarative sequencing across multiple animated blocks. Solves the "chained word-by-word intro" case where each word is its own `each` and word N should only start after word N-1 finishes. |
| 8 | + |
| 9 | + ```ts |
| 10 | + import { $, Animation, collect, each, stagger } from "@effex/dom"; |
| 11 | + |
| 12 | + const App = () => |
| 13 | + Effect.gen(function* () { |
| 14 | + const [greeting, name, tagline] = yield* Animation.sequence(3); |
| 15 | + return $.div( |
| 16 | + {}, |
| 17 | + collect( |
| 18 | + each(greetingLetters, { |
| 19 | + key: (l) => l.id, |
| 20 | + render: (l) => $.span({}, $.of(l.char)), |
| 21 | + animate: { |
| 22 | + enter: "letter-in", |
| 23 | + stagger: stagger(40), |
| 24 | + group: greeting, |
| 25 | + }, |
| 26 | + }), |
| 27 | + each(nameLetters, { |
| 28 | + key: (l) => l.id, |
| 29 | + render: (l) => $.span({}, $.of(l.char)), |
| 30 | + animate: { enter: "letter-in", stagger: stagger(40), group: name }, |
| 31 | + }), |
| 32 | + each(taglineLetters, { |
| 33 | + key: (l) => l.id, |
| 34 | + render: (l) => $.span({}, $.of(l.char)), |
| 35 | + animate: { |
| 36 | + enter: "letter-in", |
| 37 | + stagger: stagger(40), |
| 38 | + group: tagline, |
| 39 | + }, |
| 40 | + }), |
| 41 | + ), |
| 42 | + ); |
| 43 | + }); |
| 44 | + ``` |
| 45 | + |
| 46 | + New API: |
| 47 | + - `Animation.group()` — creates a group with a gate (unresolved) and a completion signal. |
| 48 | + - `Animation.sequence(count)` — returns `count` groups wired end-to-end: group 0's gate is open immediately, group N's gate opens when group N-1's registered animations all complete. |
| 49 | + - `Animation.parallel(count)` — returns `count` groups with all gates open (useful nested inside `sequence` for concurrent segments in a follow-up release). |
| 50 | + - `animate.group: AnimationGroup` — new option on any animated control (`each`, `when`, `match`, ...). The animation registers with the group synchronously, awaits the gate before starting, and signals completion when finished. |
| 51 | + |
| 52 | + Groups finalize when their pending count returns to zero for the first time after having been non-zero. Late registrations that arrive after finalization run immediately (gate is already open) — intended semantics for one-shot intros where post-sequence additions behave like ordinary animations. |
| 53 | + |
| 54 | +- ee8a4d1: Add an `intro?: boolean` flag on `each` — and symmetrically on `when`, `match`, `matchOption`, `matchEither`, and `redraw` — to opt into re-animating SSR/SSG-rendered content during hydration. |
| 55 | + |
| 56 | + Default behaviour stays as-is: hydration attaches handlers to pre-existing DOM without re-running enter animations — the right choice for content lists (feeds, sidebars, todos) that shouldn't jitter into view on every page load. Setting `intro: true` flips that for decorative sequences where the animation _is_ the point: |
| 57 | + |
| 58 | + ```ts |
| 59 | + each(letters, { |
| 60 | + key: (l) => l.id, |
| 61 | + render: (l) => $.span({}, $.of(Readable.map(l, (v) => v.char))), |
| 62 | + animate: { |
| 63 | + enterFrom: "opacity-0 translate-y-4", |
| 64 | + enter: "opacity-100 translate-y-0 transition duration-300", |
| 65 | + stagger: stagger(40), |
| 66 | + }, |
| 67 | + intro: true, |
| 68 | + }); |
| 69 | + ``` |
| 70 | + |
| 71 | + The same flag makes sense on single-slot controls too — a hero fade-in for a `when`-gated banner, or an animated card for a `match`-selected state: |
| 72 | + |
| 73 | + ```ts |
| 74 | + when(isReady, { |
| 75 | + onTrue: () => Hero(), |
| 76 | + onFalse: () => Placeholder(), |
| 77 | + animate: { |
| 78 | + enterFrom: "opacity-0", |
| 79 | + enter: "opacity-100 transition duration-500", |
| 80 | + }, |
| 81 | + intro: true, |
| 82 | + }); |
| 83 | + ``` |
| 84 | + |
| 85 | + On the client, `intro` is a no-op — animations already fire normally when there's no pre-existing DOM. It only affects the hydration path. |
| 86 | + |
| 87 | + **FOUC caveat.** Between the SSR paint and hydration applying the `enterFrom` state, there's a brief visual flash of the final state. To eliminate it, hide the container in CSS until hydration completes (e.g. `visibility: hidden` on a class you toggle from your client entry). A first-class FOUC-prevention mechanism is planned for a follow-up. |
| 88 | + |
| 89 | + Respects `prefers-reduced-motion` via `runEnterAnimation`. |
| 90 | + |
| 91 | +### Patch Changes |
| 92 | + |
| 93 | +- 12654be: Deprecate five under-used animation helpers with `@deprecated` JSDoc tags. All continue to work — they'll be removed in a future major. |
| 94 | + - `staggerEased` — compose your own: `(index, total) => easingFn(index / (total - 1)) * totalDurationMs` |
| 95 | + - `delay` — use `Effect.delay(effect, ms)` directly |
| 96 | + - `sequence` — use `Effect.all([...], { concurrency: 1 })` directly |
| 97 | + - `parallel` — use `Effect.all([...], { concurrency: "unbounded" })` directly |
| 98 | + - `calculateStaggerDelay` — was only ever an internal helper; not re-exported by anything downstream |
| 99 | + |
| 100 | + These wrapped effect combinators without adding real value beyond a slightly shorter name; the framework should be a thin layer over Effect rather than shadow its stdlib. |
| 101 | + |
| 102 | +- 0dd6440: Fix inline animation serialization in `addSlot` / `removeSlot`. Previously each enter/exit animation was awaited via `yield*`, which serialized the `reconcile` sync loop — for a list of `[a, b, c]` added at once, item `b`'s animation would only start after item `a`'s finished. Made `stagger` configs effectively double-counted (each addSlot's stagger delay applied _after_ the previous animation completed rather than concurrently). |
| 103 | + |
| 104 | + Now enter animations are forked into the slot's scope so multiple slots animate concurrently, and exit animations run in a fiber attached to the parent scope so `removeSlot` returns immediately (letting the reconcile loop continue) while the exit still plays before the DOM node is removed. If a slot is removed mid-enter, closing the slot scope interrupts the enter animation before exit starts. This applies to `ClientControlCtx` and both factories in `HydrationControlCtx`. |
| 105 | + |
| 106 | + `@effex/router` will receive an automatic patch via `updateInternalDependencies` since it depends on `@effex/dom` as a workspace dependency. |
| 107 | + |
3 | 108 | ## 1.1.1 |
4 | 109 |
|
5 | 110 | ### Patch Changes |
|
0 commit comments