@udixio/ui-react 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,8 @@
1
- import { ReactNode } from 'react';
2
- /**
3
- * AnimateOnScroll
4
- *
5
- * Manages triggers for animations:
6
- * - ScrollDriven animations: use native CSS if supported; otherwise import JS fallback per element set.
7
- * - Other entry/exit animations: handled via IntersectionObserver in JS.
8
- */
9
- export type AnimateOnScrollProps = {
1
+ export type AnimateOnScrollOptions = {
10
2
  prefix?: string;
11
- children?: ReactNode;
12
3
  once?: boolean;
13
4
  };
14
- export declare const AnimateOnScroll: ({ prefix, children, once, }: AnimateOnScrollProps) => import("react/jsx-runtime").JSX.Element;
5
+ export declare function initAnimateOnScroll(options?: AnimateOnScrollOptions): () => void;
6
+ export declare const AnimateOnScrollInit: typeof initAnimateOnScroll;
7
+ export declare const animateOnScroll: typeof initAnimateOnScroll;
15
8
  //# sourceMappingURL=AnimateOnScroll.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AnimateOnScroll.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/AnimateOnScroll.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAA8B,MAAM,OAAO,CAAC;AAE9D;;;;;;GAMG;AAEH,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAqKF,eAAO,MAAM,eAAe,GAAI,6BAI7B,oBAAoB,4CAyKtB,CAAC"}
1
+ {"version":3,"file":"AnimateOnScroll.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/AnimateOnScroll.ts"],"names":[],"mappings":"AAwHA,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,sBAA2B,GACnC,MAAM,IAAI,CAyIZ;AAGD,eAAO,MAAM,mBAAmB,4BAAsB,CAAC;AACvD,eAAO,MAAM,eAAe,4BAAsB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udixio/ui-react",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -36,8 +36,8 @@
36
36
  "devDependencies": {
37
37
  "react": "^19.1.1",
38
38
  "react-dom": "^19.1.1",
39
- "@udixio/tailwind": "2.0.0",
40
- "@udixio/theme": "2.0.0"
39
+ "@udixio/theme": "2.0.0",
40
+ "@udixio/tailwind": "2.2.0"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
@@ -0,0 +1,269 @@
1
+ // Simple script initializer (no React component)
2
+
3
+ /**
4
+ * AnimateOnScroll
5
+ *
6
+ * Manages triggers for animations:
7
+ * - ScrollDriven animations: use native CSS if supported; otherwise import JS fallback per element set.
8
+ * - Other entry/exit animations: handled via IntersectionObserver in JS.
9
+ */
10
+
11
+ function supportsScrollTimeline(): boolean {
12
+ if (typeof window === `undefined`) return false;
13
+ try {
14
+ // @ts-ignore - CSS may not exist in TS lib
15
+ if (window.CSS && typeof window.CSS.supports === `function`) {
16
+ // @ts-ignore
17
+ return (
18
+ CSS.supports(`animation-timeline: view()`) ||
19
+ CSS.supports(`animation-timeline: scroll()`) ||
20
+ // some older implementations used view-timeline-name
21
+ CSS.supports(`view-timeline-name: --a`)
22
+ );
23
+ }
24
+ } catch {}
25
+ return false;
26
+ }
27
+
28
+ function prefersReducedMotion(): boolean {
29
+ if (typeof window === `undefined` || !(`matchMedia` in window)) return false;
30
+ return window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
31
+ }
32
+
33
+ function isScrollDrivenCandidate(el: Element): boolean {
34
+ if (!(el instanceof HTMLElement)) return false;
35
+ const cls = el.classList;
36
+
37
+ return Array.from(cls).some(
38
+ (className) =>
39
+ className.startsWith('anim-') && className.includes('scroll'),
40
+ );
41
+ }
42
+ function isJsObserverCandidate(el: Element): boolean {
43
+ if (!(el instanceof HTMLElement)) return false;
44
+ const cls = el.classList;
45
+ const hasAnimation = Array.from(cls).some((className) =>
46
+ className.startsWith('anim-'),
47
+ );
48
+ if (!hasAnimation) return false;
49
+ // Not scroll-driven
50
+ return !isScrollDrivenCandidate(el);
51
+ }
52
+
53
+ function hydrateElement(el: HTMLElement, prefix: string): void {
54
+ if (!isScrollDrivenCandidate(el)) return;
55
+
56
+ // Map data-anim-scroll to correct axis class if provided
57
+ if (el.hasAttribute(`data-${prefix}-scroll`)) {
58
+ const raw = (el.getAttribute(`data-${prefix}-scroll`) || ``)
59
+ .trim()
60
+ .toLowerCase();
61
+ const axis =
62
+ raw === `x` || raw === `inline`
63
+ ? `inline`
64
+ : raw === `y` || raw === `block`
65
+ ? `block`
66
+ : `auto`;
67
+ const hasAny =
68
+ el.classList.contains(`${prefix}-timeline`) ||
69
+ el.classList.contains(`${prefix}-timeline-inline`) ||
70
+ el.classList.contains(`${prefix}-timeline-block`) ||
71
+ el.classList.contains(`${prefix}-timeline-x`) ||
72
+ el.classList.contains(`${prefix}-timeline-y`);
73
+ if (!hasAny) {
74
+ if (axis === `inline`) el.classList.add(`${prefix}-timeline-inline`);
75
+ else if (axis === `block`) el.classList.add(`${prefix}-timeline-block`);
76
+ else el.classList.add(`${prefix}-scroll`);
77
+ }
78
+ }
79
+
80
+ // Offsets via data-anim-start / data-anim-end (accepts tokens like "entry 20%", "cover 50%", etc.)
81
+ const start = el.getAttribute(`data-${prefix}-start`);
82
+ if (start) el.style.setProperty(`--${prefix}-range-start`, start);
83
+ const end = el.getAttribute(`data-${prefix}-end`);
84
+ if (end) el.style.setProperty(`--${prefix}-range-end`, end);
85
+
86
+ // Ensure play state is running unless explicitly paused
87
+ const explicitlyPaused =
88
+ el.hasAttribute(`data-${prefix}-paused`) ||
89
+ el.classList.contains(`${prefix}-paused`);
90
+ const alreadyRunning =
91
+ el.hasAttribute(`data-${prefix}-run`) ||
92
+ el.classList.contains(`${prefix}-run`);
93
+ if (!explicitlyPaused && !alreadyRunning) {
94
+ el.setAttribute(`data-${prefix}-run`, ``);
95
+ }
96
+ }
97
+
98
+ function queryScrollDrivenCandidates(
99
+ root: ParentNode = document,
100
+ prefix: string,
101
+ ): HTMLElement[] {
102
+ // Select any elements that have an animation class and are marked as scroll-driven
103
+ const animated = Array.from(
104
+ root.querySelectorAll<HTMLElement>(`[class*="${prefix}-"]`),
105
+ );
106
+ return animated.filter((el) => isScrollDrivenCandidate(el));
107
+ }
108
+
109
+ function queryJsObserverCandidates(
110
+ root: ParentNode = document,
111
+ prefix: string,
112
+ ): HTMLElement[] {
113
+ // All anim-in/out that are NOT scroll-driven
114
+ const animated = Array.from(
115
+ root.querySelectorAll<HTMLElement>(`[class*="anim-"]`),
116
+ );
117
+
118
+ return animated.filter((el) => !isScrollDrivenCandidate(el));
119
+ }
120
+
121
+ export type AnimateOnScrollOptions = {
122
+ prefix?: string;
123
+ once?: boolean;
124
+ };
125
+
126
+ export function initAnimateOnScroll(
127
+ options: AnimateOnScrollOptions = {},
128
+ ): () => void {
129
+ const { prefix = 'anim', once = true } = options;
130
+
131
+ if (prefersReducedMotion()) {
132
+ return () => {};
133
+ }
134
+
135
+ const cssSupported = supportsScrollTimeline();
136
+
137
+ // Setup JS observers for non-scroll-driven animations
138
+ const observed = new WeakSet<Element>();
139
+
140
+ const io = new IntersectionObserver(
141
+ (entries) => {
142
+ for (const entry of entries) {
143
+ const el = entry.target as HTMLElement;
144
+
145
+ if (!isJsObserverCandidate(el)) continue;
146
+
147
+ const cls = el.classList;
148
+ const isOut = cls.contains(`${prefix}-out`);
149
+ const isIn = !isOut;
150
+
151
+ if (isIn && entry.isIntersecting) {
152
+ el.setAttribute(`data-${prefix}-in-run`, ``);
153
+ if (once) io.unobserve(el);
154
+ } else if (isOut && !entry.isIntersecting) {
155
+ el.setAttribute(`data-${prefix}-out-run`, ``);
156
+ if (once) io.unobserve(el);
157
+ } else {
158
+ if (!once) {
159
+ const currentAnimationName = el.style.animationName;
160
+ el.style.animationName = 'none';
161
+ el.removeAttribute(`data-${prefix}-in-run`);
162
+ el.removeAttribute(`data-${prefix}-out-run`);
163
+ void el.offsetWidth; // reflow
164
+ el.style.animationName = currentAnimationName;
165
+ }
166
+ }
167
+ }
168
+ },
169
+ { threshold: [0, 0.2] },
170
+ );
171
+
172
+ const observeJsCandidates = (root?: ParentNode) => {
173
+ const candidates = queryJsObserverCandidates(root || document, prefix);
174
+ for (const el of candidates) {
175
+ if (observed.has(el)) continue;
176
+ observed.add(el);
177
+ io.observe(el);
178
+ }
179
+ };
180
+
181
+ // Initial observe
182
+ observeJsCandidates();
183
+
184
+ // Scroll-driven branch per support state
185
+ let cleanupScrollDriven: void | (() => void);
186
+ let mo: MutationObserver | null = null;
187
+ let rafId: number | null = null;
188
+
189
+ if (cssSupported) {
190
+ const schedule = () => {
191
+ if (rafId != null) return;
192
+ rafId = requestAnimationFrame(() => {
193
+ rafId = null;
194
+ const els = queryScrollDrivenCandidates(undefined, prefix);
195
+ for (const el of els) hydrateElement(el, prefix);
196
+ });
197
+ };
198
+
199
+ // Initial hydration
200
+ schedule();
201
+
202
+ mo = new MutationObserver((muts) => {
203
+ for (const m of muts) {
204
+ if (m.type === `attributes`) {
205
+ const t = m.target;
206
+ if (t instanceof HTMLElement) {
207
+ hydrateElement(t as HTMLElement, prefix);
208
+ if (isJsObserverCandidate(t)) {
209
+ if (!observed.has(t)) {
210
+ observed.add(t);
211
+ io.observe(t);
212
+ }
213
+ }
214
+ }
215
+ } else if (m.type === `childList`) {
216
+ if (m.addedNodes && m.addedNodes.length) {
217
+ for (const node of Array.from(m.addedNodes)) {
218
+ if (node instanceof HTMLElement) {
219
+ const sds = queryScrollDrivenCandidates(node, prefix);
220
+ for (const el of sds) hydrateElement(el, prefix);
221
+ observeJsCandidates(node);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ });
228
+
229
+ mo.observe(document.documentElement, {
230
+ subtree: true,
231
+ childList: true,
232
+ attributes: true,
233
+ attributeFilter: [
234
+ `class`,
235
+ `data-${prefix}-scroll`,
236
+ `data-${prefix}-start`,
237
+ `data-${prefix}-end`,
238
+ `data-${prefix}-paused`,
239
+ `data-${prefix}-run`,
240
+ ],
241
+ });
242
+
243
+ cleanupScrollDriven = () => {
244
+ if (rafId != null) cancelAnimationFrame(rafId);
245
+ if (mo) mo.disconnect();
246
+ };
247
+ } else {
248
+ let stop: void | (() => void);
249
+ const existing = queryScrollDrivenCandidates(undefined, prefix);
250
+ if (existing.length > 0) {
251
+ import(`./scrollDriven`).then((m) => {
252
+ stop = m.initScrollViewFallback({ once });
253
+ });
254
+ }
255
+ cleanupScrollDriven = () => {
256
+ if (typeof stop === `function`) (stop as () => void)();
257
+ };
258
+ }
259
+
260
+ // Public cleanup
261
+ return () => {
262
+ if (cleanupScrollDriven) cleanupScrollDriven();
263
+ io.disconnect();
264
+ };
265
+ }
266
+
267
+ // Backward-compatible alias name (non-React):
268
+ export const AnimateOnScrollInit = initAnimateOnScroll;
269
+ export const animateOnScroll = initAnimateOnScroll;
@@ -1,353 +0,0 @@
1
- import { ReactNode, useEffect, useMemo, useRef } from 'react';
2
-
3
- /**
4
- * AnimateOnScroll
5
- *
6
- * Manages triggers for animations:
7
- * - ScrollDriven animations: use native CSS if supported; otherwise import JS fallback per element set.
8
- * - Other entry/exit animations: handled via IntersectionObserver in JS.
9
- */
10
-
11
- export type AnimateOnScrollProps = {
12
- prefix?: string;
13
- children?: ReactNode;
14
- once?: boolean; // if true in JS modes, animate only first time per element
15
- };
16
-
17
- function supportsScrollTimeline(): boolean {
18
- if (typeof window === `undefined`) return false;
19
- try {
20
- // @ts-ignore - CSS may not exist in TS lib
21
- if (window.CSS && typeof window.CSS.supports === `function`) {
22
- // @ts-ignore
23
- return (
24
- CSS.supports(`animation-timeline: view()`) ||
25
- CSS.supports(`animation-timeline: scroll()`) ||
26
- // some older implementations used view-timeline-name
27
- CSS.supports(`view-timeline-name: --a`)
28
- );
29
- }
30
- } catch {}
31
- return false;
32
- }
33
-
34
- function prefersReducedMotion(): boolean {
35
- if (typeof window === `undefined` || !(`matchMedia` in window)) return false;
36
- return window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
37
- }
38
-
39
- function isScrollDrivenCandidate(el: Element): boolean {
40
- if (!(el instanceof HTMLElement)) return false;
41
- const cls = el.classList;
42
-
43
- return Array.from(cls).some(
44
- (className) =>
45
- className.startsWith('anim-') && className.includes('scroll'),
46
- );
47
- }
48
- function isJsObserverCandidate(el: Element): boolean {
49
- if (!(el instanceof HTMLElement)) return false;
50
- const cls = el.classList;
51
- const hasAnimation = Array.from(cls).some((className) =>
52
- className.startsWith('anim-'),
53
- );
54
- if (!hasAnimation) return false;
55
- // Not scroll-driven
56
- return !isScrollDrivenCandidate(el);
57
- }
58
-
59
- // Collect `--anim-names-*` CSS custom properties on the element and update:
60
- // - animations-names: comma-separated list of suffixes (after --anim-names-)
61
- // - will-change: union of comma-separated values from each variable
62
- function updateAnimNamesAndWillChange(el: HTMLElement, prefix: string): void {
63
- if (typeof window === 'undefined') return;
64
- try {
65
- const cs = getComputedStyle(el);
66
-
67
- const animationNames = new Set<string>();
68
-
69
- // Replace the classList loop with this:
70
- Array.from(el.classList).forEach((className) => {
71
- if (className.startsWith('anim')) {
72
- const cssVarName = `--anim-name-${className.replaceAll(/-in|-out|-scroll/g, '').replace(prefix + '-', '')}`;
73
- const animationName = cs.getPropertyValue(cssVarName).trim();
74
- if (animationName) {
75
- animationNames.add(animationName.replace(prefix + '-', ''));
76
- }
77
- }
78
- });
79
-
80
- if (animationNames.size > 0) {
81
- const value = Array.from(animationNames)
82
- .map((name) => {
83
- return `var(--anim-name-${name})`;
84
- })
85
- .join(', ');
86
- const changes = Array.from(animationNames)
87
- .map((name) => {
88
- return `var(--anim-dependencies-${name})`;
89
- })
90
- .join(', ');
91
-
92
- el.style.animationName = value;
93
- el.style.willChange = changes;
94
- }
95
- } catch {
96
- // no-op
97
- }
98
- }
99
-
100
- function hydrateElement(el: HTMLElement, prefix: string): void {
101
- if (!isScrollDrivenCandidate(el)) return;
102
-
103
- // Map data-anim-scroll to correct axis class if provided
104
- if (el.hasAttribute(`data-${prefix}-scroll`)) {
105
- const raw = (el.getAttribute(`data-${prefix}-scroll`) || ``)
106
- .trim()
107
- .toLowerCase();
108
- const axis =
109
- raw === `x` || raw === `inline`
110
- ? `inline`
111
- : raw === `y` || raw === `block`
112
- ? `block`
113
- : `auto`;
114
- const hasAny =
115
- el.classList.contains(`${prefix}-timeline`) ||
116
- el.classList.contains(`${prefix}-timeline-inline`) ||
117
- el.classList.contains(`${prefix}-timeline-block`) ||
118
- el.classList.contains(`${prefix}-timeline-x`) ||
119
- el.classList.contains(`${prefix}-timeline-y`);
120
- if (!hasAny) {
121
- if (axis === `inline`) el.classList.add(`${prefix}-timeline-inline`);
122
- else if (axis === `block`) el.classList.add(`${prefix}-timeline-block`);
123
- else el.classList.add(`${prefix}-scroll`);
124
- }
125
- }
126
-
127
- // Offsets via data-anim-start / data-anim-end (accepts tokens like "entry 20%", "cover 50%", etc.)
128
- const start = el.getAttribute(`data-${prefix}-start`);
129
- if (start) el.style.setProperty(`--${prefix}-range-start`, start);
130
- const end = el.getAttribute(`data-${prefix}-end`);
131
- if (end) el.style.setProperty(`--${prefix}-range-end`, end);
132
-
133
- // Ensure play state is running unless explicitly paused
134
- const explicitlyPaused =
135
- el.hasAttribute(`data-${prefix}-paused`) ||
136
- el.classList.contains(`${prefix}-paused`);
137
- const alreadyRunning =
138
- el.hasAttribute(`data-${prefix}-run`) ||
139
- el.classList.contains(`${prefix}-run`);
140
- if (!explicitlyPaused && !alreadyRunning) {
141
- el.setAttribute(`data-${prefix}-run`, ``);
142
- }
143
-
144
- // Update animations-names and will-change derived from --anim-names-*
145
- updateAnimNamesAndWillChange(el, prefix);
146
- }
147
-
148
- const scrollDrivenSelectorParts = (prefix: string) => [
149
- `.${prefix}-timeline`,
150
- `.${prefix}-timeline-x`,
151
- `.${prefix}-timeline-y`,
152
- `.${prefix}-timeline-inline`,
153
- `.${prefix}-timeline-block`,
154
- `[data-${prefix}-scroll]`,
155
- ];
156
-
157
- function queryScrollDrivenCandidates(
158
- root: ParentNode = document,
159
- prefix: string,
160
- ): HTMLElement[] {
161
- // Select any elements that have an animation class and are marked as scroll-driven
162
- const animated = Array.from(
163
- root.querySelectorAll<HTMLElement>(`[class*="${prefix}-"]`),
164
- );
165
- return animated.filter((el) => isScrollDrivenCandidate(el));
166
- }
167
-
168
- function queryJsObserverCandidates(
169
- root: ParentNode = document,
170
- prefix: string,
171
- ): HTMLElement[] {
172
- // All anim-in/out that are NOT scroll-driven
173
- const animated = Array.from(
174
- root.querySelectorAll<HTMLElement>(`[class*="anim-"]`),
175
- );
176
-
177
- return animated.filter((el) => !isScrollDrivenCandidate(el));
178
- }
179
-
180
- export const AnimateOnScroll = ({
181
- prefix = 'anim',
182
- children,
183
- once = true,
184
- }: AnimateOnScrollProps) => {
185
- const reduced = useMemo(prefersReducedMotion, []);
186
- const cssSupported = useMemo(() => supportsScrollTimeline(), []);
187
- const moRef = useRef<MutationObserver | null>(null);
188
- const ioRef = useRef<IntersectionObserver | null>(null);
189
- const observedSetRef = useRef<WeakSet<Element> | null>(null);
190
-
191
- useEffect(() => {
192
- if (reduced) return; // respect reduced motion
193
-
194
- // Setup JS observers for non-scroll-driven animations
195
- const observed = new WeakSet<Element>();
196
- observedSetRef.current = observed;
197
-
198
- const io = new IntersectionObserver(
199
- (entries) => {
200
- for (const entry of entries) {
201
- const el = entry.target as HTMLElement;
202
-
203
- if (!isJsObserverCandidate(el)) {
204
- continue;
205
- }
206
-
207
- const cls = el.classList;
208
- const isOut = cls.contains(`${prefix}-out`);
209
- const isIn = !isOut;
210
-
211
- if (isIn && entry.isIntersecting) {
212
- el.setAttribute(`data-${prefix}-in-run`, ``);
213
- if (once) io.unobserve(el);
214
- } else if (isOut && !entry.isIntersecting) {
215
- // Play exit when leaving viewport
216
- el.setAttribute(`data-${prefix}-out-run`, ``);
217
- if (once) io.unobserve(el);
218
- } else {
219
- // Pause when not in the triggering state
220
- // Do not aggressively remove attribute if once=true and already ran
221
- if (!once) {
222
- // Store current animation name
223
- const currentAnimationName = el.style.animationName;
224
- // Remove animation name
225
- el.style.animationName = 'none';
226
- el.removeAttribute(`data-${prefix}-in-run`);
227
- el.removeAttribute(`data-${prefix}-out-run`);
228
- void el.offsetWidth; // reflow
229
- // Re-apply animation name
230
- el.style.animationName = currentAnimationName;
231
- }
232
- }
233
- }
234
- },
235
- { threshold: [0, 0.2] },
236
- );
237
- ioRef.current = io;
238
-
239
- const observeJsCandidates = (root?: ParentNode) => {
240
- const candidates = queryJsObserverCandidates(root || document, prefix);
241
- for (const el of candidates) {
242
- if (observed.has(el)) continue;
243
- observed.add(el);
244
- // Update animations meta for non-scroll-driven animated elements
245
- updateAnimNamesAndWillChange(el, prefix);
246
- io.observe(el);
247
- }
248
- };
249
-
250
- // Initial observe
251
- observeJsCandidates();
252
-
253
- // Now handle scroll-driven branch per support state
254
- let cleanupScrollDriven: void | (() => void);
255
-
256
- if (cssSupported) {
257
- let rafId: number | null = null;
258
- const schedule = () => {
259
- if (rafId != null) return;
260
- rafId = requestAnimationFrame(() => {
261
- rafId = null;
262
- const els = queryScrollDrivenCandidates(undefined, prefix);
263
- for (const el of els) hydrateElement(el, prefix);
264
- });
265
- };
266
-
267
- // Initial hydration
268
- schedule();
269
-
270
- // Observe DOM changes to re-hydrate and attach IO to new js candidates
271
- const mo = new MutationObserver((muts) => {
272
- for (const m of muts) {
273
- if (m.type === `attributes`) {
274
- const t = m.target;
275
- if (t instanceof HTMLElement) {
276
- hydrateElement(t as HTMLElement, prefix);
277
- // If an element lost/gained scroll-driven marker, ensure it`s observed appropriately
278
- if (isJsObserverCandidate(t)) {
279
- if (!observed.has(t)) {
280
- observed.add(t);
281
- io.observe(t);
282
- }
283
- }
284
- // Always update animations meta on attribute changes that may affect styles
285
- updateAnimNamesAndWillChange(t as HTMLElement, prefix);
286
- }
287
- } else if (m.type === `childList`) {
288
- // new nodes
289
- if (m.addedNodes && m.addedNodes.length) {
290
- for (const node of Array.from(m.addedNodes)) {
291
- if (node instanceof HTMLElement) {
292
- // hydrate scroll-driven in subtree
293
- const sds = queryScrollDrivenCandidates(node, prefix);
294
- for (const el of sds) hydrateElement(el, prefix);
295
- // observe js candidates in subtree
296
- observeJsCandidates(node);
297
- }
298
- }
299
- }
300
- }
301
- }
302
- });
303
- mo.observe(document.documentElement, {
304
- subtree: true,
305
- childList: true,
306
- attributes: true,
307
- attributeFilter: [
308
- `class`,
309
- `data-${prefix}-scroll`,
310
- `data-${prefix}-start`,
311
- `data-${prefix}-end`,
312
- `data-${prefix}-paused`,
313
- `data-${prefix}-run`,
314
- ],
315
- });
316
- moRef.current = mo;
317
-
318
- cleanupScrollDriven = () => {
319
- if (rafId != null) cancelAnimationFrame(rafId);
320
- mo.disconnect();
321
- moRef.current = null;
322
- };
323
- } else {
324
- // No CSS support: dynamically import the fallback ONLY if there are scroll-driven candidates
325
- let stop: void | (() => void);
326
- const existing = queryScrollDrivenCandidates(undefined, prefix);
327
- // Update animations meta for scroll-driven candidates even without native support
328
- for (const el of existing) updateAnimNamesAndWillChange(el, prefix);
329
- if (existing.length > 0) {
330
- import(`./scrollDriven`).then((m) => {
331
- stop = m.initScrollViewFallback({ once });
332
- });
333
- }
334
- cleanupScrollDriven = () => {
335
- if (typeof stop === `function`) (stop as () => void)();
336
- };
337
- }
338
-
339
- return () => {
340
- // cleanup both branches
341
- if (cleanupScrollDriven) cleanupScrollDriven();
342
- // IO cleanup
343
- if (ioRef.current) {
344
- ioRef.current.disconnect();
345
- ioRef.current = null;
346
- }
347
- observedSetRef.current = null;
348
- };
349
- }, [cssSupported, reduced, once]);
350
-
351
- // Always return only children; no extra DOM
352
- return <>{children}</>;
353
- };