1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
import { gsap } from "gsap";
import type { TransitionConfig } from "svelte/transition";
/**
* GSAP height animations for sidebar panels — the DOM EFFECT half (stable
* panel identity lives in `../logic/panel-keys.ts`).
*
* Every animation changes ONLY the panel wrapper's height (clipped with
* `overflow: hidden`); the panel's contents keep their natural size the whole
* time — the wrapper reveals / hides them like a drawer, it never scales
* them. The wrapper's bottom padding is the gap between panels, so it grows /
* shrinks with the panel and the panels below slide smoothly.
*
* - `panelGrowIn` — Svelte `in:` transition: an added panel grows 0 → auto.
* - `panelShrinkOut` — Svelte `out:` transition: a closed panel shrinks to 0
* while staying in flow (Svelte keeps the node for the returned duration).
* - `morphPanelHeight` — a swapped view: the panel BOX (the well, so its
* edges move with it — no empty gap below a shrinking box) animates from
* the old view's height to the new one's.
*
* Svelte's `in:` does not run on first mount, so loading the page never
* animates. Disabled when the user prefers reduced motion (or the
* environment can't tell, e.g. jsdom) — then every change is instant.
*/
const DURATION = 0.35;
const EASE = "power2.inOut";
export function panelMotionAllowed(): boolean {
return (
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
);
}
export function panelGrowIn(node: HTMLElement): TransitionConfig {
if (!panelMotionAllowed()) return { duration: 0 };
gsap.killTweensOf(node);
gsap.fromTo(
node,
{ height: 0, overflow: "hidden" },
{
height: "auto",
duration: DURATION,
ease: EASE,
clearProps: "height,overflow",
},
);
return { duration: DURATION * 1000 };
}
export function panelShrinkOut(node: HTMLElement): TransitionConfig {
if (!panelMotionAllowed()) return { duration: 0 };
// e.g. closed mid-grow / mid-swap: shrink from wherever it is.
gsap.killTweensOf([node, ...node.querySelectorAll(":scope > *")]);
gsap.to(node, {
height: 0,
overflow: "hidden",
duration: DURATION,
ease: EASE,
});
return { duration: DURATION * 1000 };
}
/**
* Animate `node` from `fromHeight` (its height before the view swapped) to
* its new natural height. Call AFTER the DOM shows the new view.
*/
export function morphPanelHeight(node: HTMLElement, fromHeight: number): void {
if (!panelMotionAllowed()) return;
gsap.killTweensOf(node);
gsap.fromTo(
node,
{ height: fromHeight, overflow: "hidden" },
{
height: "auto",
duration: DURATION,
ease: EASE,
clearProps: "height,overflow",
},
);
}
|