@udixio/ui-react 1.6.4 → 2.1.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.
- package/CHANGELOG.md +51 -3
- package/dist/index.cjs +3 -3
- package/dist/index.js +1699 -1466
- package/dist/lib/config/config.interface.d.ts +4 -0
- package/dist/lib/config/config.interface.d.ts.map +1 -0
- package/dist/lib/config/define-config.d.ts +4 -0
- package/dist/lib/config/define-config.d.ts.map +1 -0
- package/dist/lib/config/index.d.ts +3 -0
- package/dist/lib/config/index.d.ts.map +1 -0
- package/dist/lib/effects/AnimateOnScroll.d.ts +7 -0
- package/dist/lib/effects/AnimateOnScroll.d.ts.map +1 -0
- package/dist/lib/effects/ThemeProvider.d.ts +3 -2
- package/dist/lib/effects/ThemeProvider.d.ts.map +1 -1
- package/dist/lib/effects/block-scroll.effect.d.ts +22 -0
- package/dist/lib/effects/block-scroll.effect.d.ts.map +1 -0
- package/dist/lib/effects/custom-scroll/custom-scroll.effect.d.ts.map +1 -1
- package/dist/lib/effects/index.d.ts +1 -0
- package/dist/lib/effects/index.d.ts.map +1 -1
- package/dist/lib/effects/scrollDriven.d.ts +5 -0
- package/dist/lib/effects/scrollDriven.d.ts.map +1 -0
- package/dist/lib/effects/smooth-scroll.effect.d.ts +5 -5
- package/dist/lib/effects/smooth-scroll.effect.d.ts.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/styles/fab.style.d.ts.map +1 -1
- package/dist/scrollDriven-AP2yWhzi.js +121 -0
- package/dist/scrollDriven-DWAu7CR0.cjs +1 -0
- package/package.json +5 -3
- package/src/lib/config/config.interface.ts +9 -0
- package/src/lib/config/define-config.ts +16 -0
- package/src/lib/config/index.ts +2 -0
- package/src/lib/effects/AnimateOnScroll.ts +267 -0
- package/src/lib/effects/ThemeProvider.tsx +78 -52
- package/src/lib/effects/block-scroll.effect.tsx +174 -0
- package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +16 -5
- package/src/lib/effects/index.ts +1 -0
- package/src/lib/effects/scrollDriven.ts +239 -0
- package/src/lib/effects/smooth-scroll.effect.tsx +105 -72
- package/src/lib/index.ts +1 -0
- package/src/lib/styles/card.style.ts +1 -1
- package/src/lib/styles/fab.style.ts +9 -17
- package/src/lib/styles/slider.style.ts +2 -2
- package/src/lib/styles/tab.style.ts +1 -1
- package/src/lib/styles/tabs.style.ts +3 -3
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type API,
|
|
3
|
+
type ConfigInterface,
|
|
4
|
+
ContextOptions,
|
|
5
|
+
loader,
|
|
6
|
+
} from '@udixio/theme';
|
|
2
7
|
import { useEffect, useRef, useState } from 'react';
|
|
3
8
|
import { TailwindPlugin } from '@udixio/tailwind';
|
|
4
9
|
|
|
@@ -11,78 +16,98 @@ export const ThemeProvider = ({
|
|
|
11
16
|
config,
|
|
12
17
|
throttleDelay = 100, // Délai par défaut de 300ms
|
|
13
18
|
onLoad,
|
|
19
|
+
loadTheme = false,
|
|
14
20
|
}: {
|
|
15
|
-
config: ConfigInterface
|
|
21
|
+
config: Readonly<ConfigInterface>;
|
|
16
22
|
onLoad?: (api: API) => void;
|
|
17
23
|
throttleDelay?: number;
|
|
24
|
+
loadTheme?: boolean;
|
|
18
25
|
}) => {
|
|
19
|
-
const [
|
|
26
|
+
const [themeApi, setThemeApi] = useState<API | null>(null);
|
|
20
27
|
|
|
21
|
-
//
|
|
28
|
+
// Charger l'API du thème une fois au montage
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
(async () => {
|
|
31
|
+
const api = await loader(config, loadTheme);
|
|
32
|
+
setThemeApi(api);
|
|
33
|
+
})();
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const [outputCss, setOutputCss] = useState<string | null>(null);
|
|
37
|
+
|
|
38
|
+
// Throttle avec exécution en tête (leading) et en fin (trailing)
|
|
22
39
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
40
|
+
const lastExecTimeRef = useRef<number>(0);
|
|
41
|
+
const lastArgsRef = useRef<Partial<ContextOptions> | null>(null);
|
|
25
42
|
|
|
26
43
|
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
if (isInitialLoadRef.current) {
|
|
29
|
-
isInitialLoadRef.current = false;
|
|
30
|
-
lastSourceColorRef.current = config.sourceColor;
|
|
31
|
-
applyThemeChange(config.sourceColor);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
44
|
+
if (!themeApi) return; // Attendre que l'API soit prête
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const ctx: Partial<ContextOptions> = {
|
|
47
|
+
...config,
|
|
48
|
+
// Assurer la compatibilité avec l'API qui attend sourceColorHex
|
|
49
|
+
sourceColor: config.sourceColor,
|
|
50
|
+
};
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
clearTimeout(timeoutRef.current);
|
|
43
|
-
}
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const timeSinceLast = now - lastExecTimeRef.current;
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
timeoutRef.current = null;
|
|
50
|
-
}, throttleDelay);
|
|
55
|
+
const invoke = async (args: Partial<ContextOptions>) => {
|
|
56
|
+
// applique et notifie
|
|
57
|
+
await applyThemeChange(args);
|
|
58
|
+
};
|
|
51
59
|
|
|
52
|
-
//
|
|
53
|
-
|
|
60
|
+
// Leading: si délai écoulé ou jamais exécuté, exécuter tout de suite
|
|
61
|
+
if (lastExecTimeRef.current === 0 || timeSinceLast >= throttleDelay) {
|
|
54
62
|
if (timeoutRef.current) {
|
|
55
63
|
clearTimeout(timeoutRef.current);
|
|
56
64
|
timeoutRef.current = null;
|
|
57
65
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
lastArgsRef.current = null;
|
|
67
|
+
lastExecTimeRef.current = now;
|
|
68
|
+
void invoke(ctx);
|
|
69
|
+
} else {
|
|
70
|
+
// Sinon, mémoriser la dernière requête et programmer une exécution en trailing
|
|
71
|
+
lastArgsRef.current = ctx;
|
|
72
|
+
if (!timeoutRef.current) {
|
|
73
|
+
const remaining = Math.max(0, throttleDelay - timeSinceLast);
|
|
74
|
+
timeoutRef.current = setTimeout(async () => {
|
|
75
|
+
timeoutRef.current = null;
|
|
76
|
+
const args = lastArgsRef.current;
|
|
77
|
+
lastArgsRef.current = null;
|
|
78
|
+
if (args) {
|
|
79
|
+
lastExecTimeRef.current = Date.now();
|
|
80
|
+
await invoke(args);
|
|
81
|
+
}
|
|
82
|
+
}, remaining);
|
|
83
|
+
}
|
|
64
84
|
}
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
...config,
|
|
70
|
-
sourceColor,
|
|
71
|
-
});
|
|
72
|
-
onLoad?.(api);
|
|
86
|
+
// Cleanup: au changement de dépendances, ne rien faire ici (on gère trailing)
|
|
87
|
+
return () => {};
|
|
88
|
+
}, [config, throttleDelay, themeApi]);
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (generatedCss) {
|
|
79
|
-
setOutputCss(generatedCss);
|
|
90
|
+
const applyThemeChange = async (ctx: Partial<ContextOptions>) => {
|
|
91
|
+
if (typeof ctx.sourceColor == 'string') {
|
|
92
|
+
if (!isValidHexColor(ctx.sourceColor)) {
|
|
93
|
+
throw new Error('Invalid hex color');
|
|
80
94
|
}
|
|
81
|
-
} catch (err) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
err instanceof Error ? err.message : 'Theme loading failed',
|
|
84
|
-
);
|
|
85
95
|
}
|
|
96
|
+
|
|
97
|
+
if (!themeApi) {
|
|
98
|
+
// L'API n'est pas prête; ignorer silencieusement car l'effet principal attend themeApi
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
themeApi.context.update(ctx);
|
|
102
|
+
|
|
103
|
+
await themeApi.load();
|
|
104
|
+
|
|
105
|
+
const outputCss = themeApi?.plugins
|
|
106
|
+
.getPlugin(TailwindPlugin)
|
|
107
|
+
.getInstance().outputCss;
|
|
108
|
+
setOutputCss(outputCss);
|
|
109
|
+
|
|
110
|
+
onLoad?.(themeApi);
|
|
86
111
|
};
|
|
87
112
|
|
|
88
113
|
// Cleanup lors du démontage du composant
|
|
@@ -90,6 +115,7 @@ export const ThemeProvider = ({
|
|
|
90
115
|
return () => {
|
|
91
116
|
if (timeoutRef.current) {
|
|
92
117
|
clearTimeout(timeoutRef.current);
|
|
118
|
+
timeoutRef.current = null;
|
|
93
119
|
}
|
|
94
120
|
};
|
|
95
121
|
}, []);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
type ScrollIntent =
|
|
4
|
+
| {
|
|
5
|
+
type: 'intent';
|
|
6
|
+
source: 'wheel' | 'touch' | 'keyboard';
|
|
7
|
+
deltaX: number;
|
|
8
|
+
deltaY: number;
|
|
9
|
+
originalEvent: Event;
|
|
10
|
+
}
|
|
11
|
+
| {
|
|
12
|
+
type: 'scrollbar';
|
|
13
|
+
scrollTop: number;
|
|
14
|
+
scrollLeft: number;
|
|
15
|
+
maxScrollTop: number;
|
|
16
|
+
maxScrollLeft: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type BlockScrollProps = {
|
|
20
|
+
onScroll?: (evt: ScrollIntent) => void; // log des intentions + du scroll via scrollbar
|
|
21
|
+
touch?: boolean;
|
|
22
|
+
el: HTMLElement;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const BlockScroll: React.FC<BlockScrollProps> = ({
|
|
26
|
+
onScroll,
|
|
27
|
+
el,
|
|
28
|
+
touch = true,
|
|
29
|
+
}) => {
|
|
30
|
+
const lastTouch = useRef<{ x: number; y: number } | null>(null);
|
|
31
|
+
const lastScrollTop = useRef<number>(0);
|
|
32
|
+
const lastScrollLeft = useRef<number>(0);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!el) return;
|
|
36
|
+
|
|
37
|
+
// Initialize last known scroll positions to block scrollbar-based scrolling
|
|
38
|
+
lastScrollTop.current = el.scrollTop;
|
|
39
|
+
lastScrollLeft.current = el.scrollLeft;
|
|
40
|
+
|
|
41
|
+
const emitIntent = (payload: Extract<ScrollIntent, { type: 'intent' }>) => {
|
|
42
|
+
// Log the desired deltaY for every scroll attempt (wheel/touch/keyboard)
|
|
43
|
+
onScroll?.(payload);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onWheel = (e: WheelEvent) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
emitIntent({
|
|
49
|
+
type: 'intent',
|
|
50
|
+
source: 'wheel',
|
|
51
|
+
deltaX: e.deltaX,
|
|
52
|
+
deltaY: e.deltaY,
|
|
53
|
+
originalEvent: e,
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
58
|
+
if (!touch) return;
|
|
59
|
+
const t = e.touches[0];
|
|
60
|
+
if (t) lastTouch.current = { x: t.clientX, y: t.clientY };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
64
|
+
if (!touch) return;
|
|
65
|
+
const t = e.touches[0];
|
|
66
|
+
if (!t || !lastTouch.current) return;
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
const dx = lastTouch.current.x - t.clientX;
|
|
69
|
+
const dy = lastTouch.current.y - t.clientY;
|
|
70
|
+
lastTouch.current = { x: t.clientX, y: t.clientY };
|
|
71
|
+
emitIntent({
|
|
72
|
+
type: 'intent',
|
|
73
|
+
source: 'touch',
|
|
74
|
+
deltaX: dx,
|
|
75
|
+
deltaY: dy,
|
|
76
|
+
originalEvent: e,
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onTouchEnd = () => {
|
|
81
|
+
if (!touch) return;
|
|
82
|
+
lastTouch.current = null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
86
|
+
const line = 40;
|
|
87
|
+
const page = el.clientHeight * 0.9;
|
|
88
|
+
let dx = 0,
|
|
89
|
+
dy = 0;
|
|
90
|
+
|
|
91
|
+
switch (e.key) {
|
|
92
|
+
case 'ArrowDown':
|
|
93
|
+
dy = line;
|
|
94
|
+
break;
|
|
95
|
+
case 'ArrowUp':
|
|
96
|
+
dy = -line;
|
|
97
|
+
break;
|
|
98
|
+
case 'ArrowRight':
|
|
99
|
+
dx = line;
|
|
100
|
+
break;
|
|
101
|
+
case 'ArrowLeft':
|
|
102
|
+
dx = -line;
|
|
103
|
+
break;
|
|
104
|
+
case 'PageDown':
|
|
105
|
+
dy = page;
|
|
106
|
+
break;
|
|
107
|
+
case 'PageUp':
|
|
108
|
+
dy = -page;
|
|
109
|
+
break;
|
|
110
|
+
case 'Home':
|
|
111
|
+
dy = Number.NEGATIVE_INFINITY;
|
|
112
|
+
break;
|
|
113
|
+
case 'End':
|
|
114
|
+
dy = Number.POSITIVE_INFINITY;
|
|
115
|
+
break;
|
|
116
|
+
case ' ':
|
|
117
|
+
dy = e.shiftKey ? -page : page;
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
emitIntent({
|
|
124
|
+
type: 'intent',
|
|
125
|
+
source: 'keyboard',
|
|
126
|
+
deltaX: dx,
|
|
127
|
+
deltaY: dy,
|
|
128
|
+
originalEvent: e,
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// const onScrollEvent = (e) => {
|
|
133
|
+
// const currentScrollTop = e.target.scrollTop;
|
|
134
|
+
// const currentScrollLeft = e.target.scrollLeft;
|
|
135
|
+
//
|
|
136
|
+
// // Check if scroll position changed from last known position
|
|
137
|
+
// if (
|
|
138
|
+
// currentScrollTop !== lastScrollTop.current ||
|
|
139
|
+
// currentScrollLeft !== lastScrollLeft.current
|
|
140
|
+
// ) {
|
|
141
|
+
// console.log('onScrollllllllllll', e, document);
|
|
142
|
+
// onScroll?.({
|
|
143
|
+
// type: 'scrollbar',
|
|
144
|
+
// scrollTop: currentScrollTop,
|
|
145
|
+
// scrollLeft: currentScrollLeft,
|
|
146
|
+
// maxScrollTop: e.target.scrollHeight - e.target.clientHeight,
|
|
147
|
+
// maxScrollLeft: e.target.scrollWidth - e.target.clientWidth,
|
|
148
|
+
// });
|
|
149
|
+
// }
|
|
150
|
+
//
|
|
151
|
+
// // Update last known scroll positions
|
|
152
|
+
// lastScrollTop.current = currentScrollTop;
|
|
153
|
+
// lastScrollLeft.current = currentScrollLeft;
|
|
154
|
+
//
|
|
155
|
+
// document.querySelector('html')?.scrollTo({ top: 0 });
|
|
156
|
+
// };
|
|
157
|
+
|
|
158
|
+
el.addEventListener('wheel', onWheel, { passive: false });
|
|
159
|
+
el.addEventListener('touchstart', onTouchStart, { passive: true });
|
|
160
|
+
el.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
161
|
+
el.addEventListener('touchend', onTouchEnd, { passive: true });
|
|
162
|
+
el.addEventListener('keydown', onKeyDown);
|
|
163
|
+
// el.addEventListener('scroll', onScrollEvent, { passive: true });
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
el.removeEventListener('wheel', onWheel as EventListener);
|
|
167
|
+
el.removeEventListener('touchstart', onTouchStart as EventListener);
|
|
168
|
+
el.removeEventListener('touchmove', onTouchMove as EventListener);
|
|
169
|
+
el.removeEventListener('touchend', onTouchEnd as EventListener);
|
|
170
|
+
el.removeEventListener('keydown', onKeyDown as EventListener);
|
|
171
|
+
// el.removeEventListener('scroll', onScrollEvent as EventListener);
|
|
172
|
+
};
|
|
173
|
+
}, [onScroll]);
|
|
174
|
+
};
|
|
@@ -90,21 +90,32 @@ export const CustomScroll = ({
|
|
|
90
90
|
handleScrollThrottledRef.current = throttle(
|
|
91
91
|
throttleDuration,
|
|
92
92
|
(latestValue, scrollOrientation: 'x' | 'y') => {
|
|
93
|
-
if (
|
|
93
|
+
if (
|
|
94
|
+
!containerSize.current ||
|
|
95
|
+
!contentScrollSize.current ||
|
|
96
|
+
!ref.current
|
|
97
|
+
)
|
|
98
|
+
return;
|
|
94
99
|
if (onScroll) {
|
|
95
100
|
if (orientation === 'horizontal' && scrollOrientation === 'x') {
|
|
96
101
|
onScroll({
|
|
97
102
|
scrollProgress: latestValue,
|
|
98
|
-
scroll:
|
|
99
|
-
|
|
103
|
+
scroll:
|
|
104
|
+
latestValue *
|
|
105
|
+
(contentScrollSize.current.width - ref.current.clientWidth),
|
|
106
|
+
scrollTotal:
|
|
107
|
+
contentScrollSize.current.width - ref.current.clientWidth,
|
|
100
108
|
scrollVisible: containerSize.current.width,
|
|
101
109
|
});
|
|
102
110
|
}
|
|
103
111
|
if (orientation === 'vertical' && scrollOrientation === 'y') {
|
|
104
112
|
onScroll({
|
|
105
113
|
scrollProgress: latestValue,
|
|
106
|
-
scroll:
|
|
107
|
-
|
|
114
|
+
scroll:
|
|
115
|
+
latestValue *
|
|
116
|
+
(contentScrollSize.current.height - ref.current.clientHeight),
|
|
117
|
+
scrollTotal:
|
|
118
|
+
contentScrollSize.current.height - ref.current.clientHeight,
|
|
108
119
|
scrollVisible: containerSize.current.height,
|
|
109
120
|
});
|
|
110
121
|
}
|
package/src/lib/effects/index.ts
CHANGED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
export type InitAnimationOptions = {
|
|
4
|
+
once?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
let initialized = false;
|
|
8
|
+
let teardown: (() => void) | null = null;
|
|
9
|
+
|
|
10
|
+
function supportsScrollTimeline(): boolean {
|
|
11
|
+
if (typeof window === 'undefined') return false;
|
|
12
|
+
try {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
if (window.CSS && typeof window.CSS.supports === 'function') {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
return (
|
|
17
|
+
CSS.supports('animation-timeline: view()') ||
|
|
18
|
+
CSS.supports('animation-timeline: scroll()') ||
|
|
19
|
+
CSS.supports('view-timeline-name: --a')
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function prefersReducedMotion(): boolean {
|
|
27
|
+
if (typeof window === 'undefined' || !('matchMedia' in window)) return false;
|
|
28
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Helpers to read CSS custom properties used by the Tailwind plugin
|
|
32
|
+
function readVar(el: Element, name: string): string | null {
|
|
33
|
+
const v = getComputedStyle(el).getPropertyValue(name).trim();
|
|
34
|
+
return v || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parsePercentFromRangeToken(token?: string | null): number | null {
|
|
38
|
+
if (!token) return null;
|
|
39
|
+
// Expect patterns like "entry 20%", "cover 80%", "center 60%" etc.
|
|
40
|
+
const parts = token.split(/\s+/);
|
|
41
|
+
const last = parts[parts.length - 1];
|
|
42
|
+
if (!last) return null;
|
|
43
|
+
if (last.endsWith('%')) {
|
|
44
|
+
const n = parseFloat(last);
|
|
45
|
+
if (!isNaN(n)) return Math.max(0, Math.min(100, n)) / 100;
|
|
46
|
+
}
|
|
47
|
+
// px not supported for now (would require element size); fallback null
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getRange(el: Element): { start: number; end: number } {
|
|
52
|
+
const startToken = readVar(el, '--udx-range-start');
|
|
53
|
+
const endToken = readVar(el, '--udx-range-end');
|
|
54
|
+
const start = parsePercentFromRangeToken(startToken) ?? 0.2; // default entry 20%
|
|
55
|
+
const end = parsePercentFromRangeToken(endToken) ?? 0.5; // default cover 50%
|
|
56
|
+
// Ensure sane ordering
|
|
57
|
+
const s = Math.max(0, Math.min(1, start));
|
|
58
|
+
const e = Math.max(s + 0.001, Math.min(1, end));
|
|
59
|
+
return { start: s, end: e };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function num(val: string | null | undefined, unit?: 'px' | 'deg'): number | null {
|
|
63
|
+
if (!val) return null;
|
|
64
|
+
const v = val.trim();
|
|
65
|
+
if (unit && v.endsWith(unit)) {
|
|
66
|
+
const n = parseFloat(v);
|
|
67
|
+
return isNaN(n) ? null : n;
|
|
68
|
+
}
|
|
69
|
+
// Try plain number
|
|
70
|
+
const n = parseFloat(v);
|
|
71
|
+
return isNaN(n) ? null : n;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function lerp(a: number, b: number, t: number) {
|
|
75
|
+
return a + (b - a) * t;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function applyProgress(el: HTMLElement, from: CSSProperties, to: CSSProperties, p: number) {
|
|
79
|
+
// Opacity
|
|
80
|
+
const o0 = from.opacity != null ? Number(from.opacity) : 1;
|
|
81
|
+
const o1 = to.opacity != null ? Number(to.opacity) : 1;
|
|
82
|
+
const op = lerp(o0, o1, p);
|
|
83
|
+
el.style.opacity = String(op);
|
|
84
|
+
|
|
85
|
+
// Transform: translateX/Y (px only), scale, rotate (deg)
|
|
86
|
+
const fx = num((from as any)['--tw-enter-translate-x'] as any) ??
|
|
87
|
+
num((from as any)['--tw-exit-translate-x'] as any) ?? 0;
|
|
88
|
+
const fy = num((from as any)['--tw-enter-translate-y'] as any) ??
|
|
89
|
+
num((from as any)['--tw-exit-translate-y'] as any) ?? 0;
|
|
90
|
+
const tx = num((to as any)['--tw-enter-translate-x'] as any) ??
|
|
91
|
+
num((to as any)['--tw-exit-translate-x'] as any) ?? 0;
|
|
92
|
+
const ty = num((to as any)['--tw-enter-translate-y'] as any) ??
|
|
93
|
+
num((to as any)['--tw-exit-translate-y'] as any) ?? 0;
|
|
94
|
+
|
|
95
|
+
const fs = num((from as any)['--tw-enter-scale'] as any) ??
|
|
96
|
+
num((from as any)['--tw-exit-scale'] as any) ?? 1;
|
|
97
|
+
const ts = num((to as any)['--tw-enter-scale'] as any) ??
|
|
98
|
+
num((to as any)['--tw-exit-scale'] as any) ?? 1;
|
|
99
|
+
|
|
100
|
+
const fr = num((from as any)['--tw-enter-rotate'] as any) ??
|
|
101
|
+
num((from as any)['--tw-exit-rotate'] as any) ?? 0;
|
|
102
|
+
const tr = num((to as any)['--tw-enter-rotate'] as any) ??
|
|
103
|
+
num((to as any)['--tw-exit-rotate'] as any) ?? 0;
|
|
104
|
+
|
|
105
|
+
const x = lerp(fx, tx, p);
|
|
106
|
+
const y = lerp(fy, ty, p);
|
|
107
|
+
const s = lerp(fs, ts, p);
|
|
108
|
+
const r = lerp(fr, tr, p);
|
|
109
|
+
|
|
110
|
+
const transforms: string[] = [];
|
|
111
|
+
if (x !== 0 || y !== 0) transforms.push(`translate3d(${x}px, ${y}px, 0)`);
|
|
112
|
+
if (s !== 1) transforms.push(`scale(${s})`);
|
|
113
|
+
if (r !== 0) transforms.push(`rotate(${r}deg)`);
|
|
114
|
+
el.style.transform = transforms.length ? transforms.join(' ') : 'none';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildFromTo(el: Element): { from: CSSProperties; to: CSSProperties } | null {
|
|
118
|
+
const cls = el.classList;
|
|
119
|
+
const isIn = cls.contains('animate-in');
|
|
120
|
+
const isOut = cls.contains('animate-out');
|
|
121
|
+
if (!isIn && !isOut) return null;
|
|
122
|
+
|
|
123
|
+
const cs = getComputedStyle(el as Element);
|
|
124
|
+
const enter = {
|
|
125
|
+
opacity: num(cs.getPropertyValue('--tw-enter-opacity')) ?? undefined,
|
|
126
|
+
'--tw-enter-translate-x': cs.getPropertyValue('--tw-enter-translate-x') || undefined,
|
|
127
|
+
'--tw-enter-translate-y': cs.getPropertyValue('--tw-enter-translate-y') || undefined,
|
|
128
|
+
'--tw-enter-scale': cs.getPropertyValue('--tw-enter-scale') || undefined,
|
|
129
|
+
'--tw-enter-rotate': cs.getPropertyValue('--tw-enter-rotate') || undefined,
|
|
130
|
+
} as CSSProperties & Record<string, any>;
|
|
131
|
+
const exit = {
|
|
132
|
+
opacity: num(cs.getPropertyValue('--tw-exit-opacity')) ?? undefined,
|
|
133
|
+
'--tw-exit-translate-x': cs.getPropertyValue('--tw-exit-translate-x') || undefined,
|
|
134
|
+
'--tw-exit-translate-y': cs.getPropertyValue('--tw-exit-translate-y') || undefined,
|
|
135
|
+
'--tw-exit-scale': cs.getPropertyValue('--tw-exit-scale') || undefined,
|
|
136
|
+
'--tw-exit-rotate': cs.getPropertyValue('--tw-exit-rotate') || undefined,
|
|
137
|
+
} as CSSProperties & Record<string, any>;
|
|
138
|
+
|
|
139
|
+
if (isIn) {
|
|
140
|
+
// from enter vars to neutral
|
|
141
|
+
return {
|
|
142
|
+
from: enter,
|
|
143
|
+
to: { opacity: 1, '--tw-enter-translate-x': '0', '--tw-enter-translate-y': '0', '--tw-enter-scale': '1', '--tw-enter-rotate': '0' } as any,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (isOut) {
|
|
147
|
+
// from neutral to exit vars
|
|
148
|
+
return {
|
|
149
|
+
from: { opacity: 1, '--tw-exit-translate-x': '0', '--tw-exit-translate-y': '0', '--tw-exit-scale': '1', '--tw-exit-rotate': '0' } as any,
|
|
150
|
+
to: exit,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function findTargets(): HTMLElement[] {
|
|
157
|
+
const selector = [
|
|
158
|
+
'.udx-view',
|
|
159
|
+
'.udx-view-x',
|
|
160
|
+
'.udx-view-y',
|
|
161
|
+
'.udx-view-inline',
|
|
162
|
+
'.udx-view-block',
|
|
163
|
+
'[data-udx-view]'
|
|
164
|
+
]
|
|
165
|
+
.map((s) => `${s}.animate-in, ${s}.animate-out`)
|
|
166
|
+
.join(', ');
|
|
167
|
+
return Array.from(document.querySelectorAll<HTMLElement>(selector));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function initScrollViewFallback(options: InitAnimationOptions = {}) {
|
|
171
|
+
if (initialized) return teardown || (() => {});
|
|
172
|
+
if (typeof window === 'undefined') return () => {};
|
|
173
|
+
if (supportsScrollTimeline() || prefersReducedMotion()) {
|
|
174
|
+
initialized = true; // No-op in supporting browsers or reduced motion
|
|
175
|
+
return () => {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
initialized = true;
|
|
179
|
+
const once = options.once ?? true;
|
|
180
|
+
const seen = new WeakSet<Element>();
|
|
181
|
+
let rafId: number | null = null;
|
|
182
|
+
|
|
183
|
+
const measure = () => {
|
|
184
|
+
const targets = findTargets();
|
|
185
|
+
const vh = window.innerHeight || 0;
|
|
186
|
+
for (const el of targets) {
|
|
187
|
+
const rect = el.getBoundingClientRect();
|
|
188
|
+
const visible = Math.min(rect.bottom, vh) - Math.max(rect.top, 0);
|
|
189
|
+
const visibleClamped = Math.max(0, Math.min(visible, rect.height));
|
|
190
|
+
const ratio = rect.height > 0 ? visibleClamped / rect.height : 0;
|
|
191
|
+
|
|
192
|
+
const { start, end } = getRange(el);
|
|
193
|
+
let p = (ratio - start) / (end - start);
|
|
194
|
+
p = Math.max(0, Math.min(1, p));
|
|
195
|
+
|
|
196
|
+
if (once && seen.has(el) && p < 1) p = 1;
|
|
197
|
+
|
|
198
|
+
const fe = buildFromTo(el);
|
|
199
|
+
if (!fe) continue;
|
|
200
|
+
const { from, to } = fe;
|
|
201
|
+
applyProgress(el, from, to, p);
|
|
202
|
+
|
|
203
|
+
if (p >= 1 && once) seen.add(el);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const onScroll = () => {
|
|
208
|
+
if (rafId != null) return;
|
|
209
|
+
rafId = window.requestAnimationFrame(() => {
|
|
210
|
+
rafId = null;
|
|
211
|
+
measure();
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Initial run and listeners
|
|
216
|
+
measure();
|
|
217
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
218
|
+
window.addEventListener('resize', onScroll);
|
|
219
|
+
|
|
220
|
+
const mo = new MutationObserver(() => {
|
|
221
|
+
onScroll();
|
|
222
|
+
});
|
|
223
|
+
mo.observe(document.documentElement, {
|
|
224
|
+
childList: true,
|
|
225
|
+
subtree: true,
|
|
226
|
+
attributes: true,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
teardown = () => {
|
|
230
|
+
window.removeEventListener('scroll', onScroll);
|
|
231
|
+
window.removeEventListener('resize', onScroll);
|
|
232
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
233
|
+
mo.disconnect();
|
|
234
|
+
initialized = false;
|
|
235
|
+
teardown = null;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return teardown;
|
|
239
|
+
}
|