@udixio/ui-react 2.4.3 → 2.5.1
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 +31 -0
- package/dist/index.cjs +2 -2
- package/dist/index.js +1745 -1460
- package/dist/lib/components/Carousel.d.ts +7 -2
- package/dist/lib/components/Carousel.d.ts.map +1 -1
- package/dist/lib/components/CarouselItem.d.ts +1 -1
- package/dist/lib/components/CarouselItem.d.ts.map +1 -1
- package/dist/lib/components/Slider.d.ts.map +1 -1
- package/dist/lib/effects/AnimateOnScroll.d.ts.map +1 -1
- package/dist/lib/effects/custom-scroll/custom-scroll.effect.d.ts +1 -1
- package/dist/lib/effects/custom-scroll/custom-scroll.effect.d.ts.map +1 -1
- package/dist/lib/effects/custom-scroll/custom-scroll.interface.d.ts +2 -0
- package/dist/lib/effects/custom-scroll/custom-scroll.interface.d.ts.map +1 -1
- package/dist/lib/effects/custom-scroll/custom-scroll.style.d.ts +4 -0
- package/dist/lib/effects/custom-scroll/custom-scroll.style.d.ts.map +1 -1
- package/dist/lib/interfaces/carousel-item.interface.d.ts +1 -0
- package/dist/lib/interfaces/carousel-item.interface.d.ts.map +1 -1
- package/dist/lib/interfaces/carousel.interface.d.ts +19 -1
- package/dist/lib/interfaces/carousel.interface.d.ts.map +1 -1
- package/dist/lib/styles/carousel-item.style.d.ts +2 -0
- package/dist/lib/styles/carousel-item.style.d.ts.map +1 -1
- package/dist/lib/styles/carousel.style.d.ts +5 -1
- package/dist/lib/styles/carousel.style.d.ts.map +1 -1
- package/dist/lib/styles/icon-button.style.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/lib/components/Carousel.tsx +535 -52
- package/src/lib/components/CarouselItem.tsx +12 -6
- package/src/lib/components/Slider.tsx +25 -28
- package/src/lib/effects/AnimateOnScroll.ts +41 -0
- package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +111 -2
- package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +4 -0
- package/src/lib/interfaces/carousel-item.interface.ts +1 -0
- package/src/lib/interfaces/carousel.interface.ts +20 -1
- package/src/lib/styles/carousel-item.style.ts +5 -2
- package/src/lib/styles/carousel.style.ts +1 -3
- package/src/lib/styles/icon-button.style.ts +5 -15
- package/src/lib/styles/slider.style.ts +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useRef } from 'react';
|
|
2
2
|
import { CarouselItemInterface } from '../interfaces';
|
|
3
|
-
import { motion } from 'motion/react';
|
|
4
3
|
import { carouselItemStyle } from '../styles';
|
|
5
4
|
import { MotionProps } from '../utils';
|
|
6
5
|
|
|
@@ -26,8 +25,9 @@ export const normalize = (
|
|
|
26
25
|
export const CarouselItem = ({
|
|
27
26
|
className,
|
|
28
27
|
children,
|
|
29
|
-
width
|
|
28
|
+
width,
|
|
30
29
|
index = 0,
|
|
30
|
+
outputRange,
|
|
31
31
|
ref: optionalRef,
|
|
32
32
|
...restProps
|
|
33
33
|
}: MotionProps<CarouselItemInterface>) => {
|
|
@@ -37,14 +37,20 @@ export const CarouselItem = ({
|
|
|
37
37
|
const styles = carouselItemStyle({
|
|
38
38
|
className,
|
|
39
39
|
index,
|
|
40
|
-
width
|
|
40
|
+
width,
|
|
41
41
|
children,
|
|
42
|
+
outputRange,
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
return (
|
|
45
|
-
<
|
|
46
|
+
<div
|
|
46
47
|
ref={ref}
|
|
47
|
-
|
|
48
|
+
style={{
|
|
49
|
+
width: width + 'px',
|
|
50
|
+
maxWidth: outputRange[1] + 'px',
|
|
51
|
+
minWidth: outputRange[0] + 'px',
|
|
52
|
+
willChange: 'width',
|
|
53
|
+
}}
|
|
48
54
|
transition={{
|
|
49
55
|
duration: 0.5,
|
|
50
56
|
ease: 'linear',
|
|
@@ -53,6 +59,6 @@ export const CarouselItem = ({
|
|
|
53
59
|
{...restProps}
|
|
54
60
|
>
|
|
55
61
|
{children}
|
|
56
|
-
</
|
|
62
|
+
</div>
|
|
57
63
|
);
|
|
58
64
|
};
|
|
@@ -31,7 +31,7 @@ export const Slider = ({
|
|
|
31
31
|
onChange,
|
|
32
32
|
...restProps
|
|
33
33
|
}: ReactProps<SliderInterface>) => {
|
|
34
|
-
const
|
|
34
|
+
const getpercentFromValue = (value: number) => {
|
|
35
35
|
const min = getMin();
|
|
36
36
|
const max = getMax();
|
|
37
37
|
|
|
@@ -56,10 +56,10 @@ export const Slider = ({
|
|
|
56
56
|
return min == -Infinity ? marks[0].value : min;
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
const
|
|
59
|
+
const getValueFrompercent = (percent: number) => {
|
|
60
60
|
const min = getMin(false);
|
|
61
61
|
const max = getMax(false);
|
|
62
|
-
return ((max - min) *
|
|
62
|
+
return ((max - min) * percent) / 100 + min;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
const [isChanging, setIsChanging] = useState(false);
|
|
@@ -68,7 +68,7 @@ export const Slider = ({
|
|
|
68
68
|
ref || defaultRef;
|
|
69
69
|
|
|
70
70
|
const [value, setValue] = useState(defaultValue);
|
|
71
|
-
const [
|
|
71
|
+
const [percent, setpercent] = useState(getpercentFromValue(defaultValue));
|
|
72
72
|
const [mouseDown, setMouseDown] = useState(false);
|
|
73
73
|
const handleMouseDown = (e: any) => {
|
|
74
74
|
setMouseDown(true);
|
|
@@ -130,31 +130,31 @@ export const Slider = ({
|
|
|
130
130
|
? event.touches[0].clientX
|
|
131
131
|
: event.clientX;
|
|
132
132
|
|
|
133
|
-
const
|
|
133
|
+
const percent = ((clientX - refPosition) / current.offsetWidth) * 100;
|
|
134
134
|
|
|
135
|
-
updateSliderValues({
|
|
135
|
+
updateSliderValues({ percent });
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
const updateSliderValues = ({
|
|
139
|
-
|
|
139
|
+
percent,
|
|
140
140
|
value,
|
|
141
141
|
}: {
|
|
142
|
-
|
|
142
|
+
percent?: number;
|
|
143
143
|
value?: number;
|
|
144
144
|
}) => {
|
|
145
|
-
if (
|
|
146
|
-
if (
|
|
145
|
+
if (percent) {
|
|
146
|
+
if (percent >= 100) {
|
|
147
147
|
setValue(getMax(true));
|
|
148
|
-
|
|
148
|
+
setpercent(100);
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
if (
|
|
151
|
+
if (percent <= 0) {
|
|
152
152
|
setValue(getMin(true));
|
|
153
|
-
|
|
153
|
+
setpercent(0);
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
value =
|
|
157
|
+
value = getValueFrompercent(percent);
|
|
158
158
|
if (value == getMin()) {
|
|
159
159
|
value = getMin(true);
|
|
160
160
|
}
|
|
@@ -164,15 +164,15 @@ export const Slider = ({
|
|
|
164
164
|
} else if (value != undefined) {
|
|
165
165
|
if (value >= getMax()) {
|
|
166
166
|
setValue(getMax(true));
|
|
167
|
-
|
|
167
|
+
setpercent(100);
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
170
|
if (value <= getMin()) {
|
|
171
171
|
setValue(getMin(true));
|
|
172
|
-
|
|
172
|
+
setpercent(0);
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
|
-
|
|
175
|
+
percent = getpercentFromValue(value);
|
|
176
176
|
} else {
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
@@ -206,10 +206,10 @@ export const Slider = ({
|
|
|
206
206
|
value = getMin(true);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
percent = getpercentFromValue(value);
|
|
210
210
|
|
|
211
211
|
setValue(value);
|
|
212
|
-
|
|
212
|
+
setpercent(percent);
|
|
213
213
|
if (onChange) {
|
|
214
214
|
onChange(value);
|
|
215
215
|
}
|
|
@@ -295,10 +295,7 @@ export const Slider = ({
|
|
|
295
295
|
{...restProps}
|
|
296
296
|
>
|
|
297
297
|
<input type="hidden" name={name} value={value} />
|
|
298
|
-
<div
|
|
299
|
-
className={styles.activeTrack}
|
|
300
|
-
style={{ flex: pourcent / 100 }}
|
|
301
|
-
></div>
|
|
298
|
+
<div className={styles.activeTrack} style={{ flex: percent / 100 }}></div>
|
|
302
299
|
<div className={styles.handle}>
|
|
303
300
|
<AnimatePresence>
|
|
304
301
|
{isChanging && (
|
|
@@ -325,7 +322,7 @@ export const Slider = ({
|
|
|
325
322
|
</div>
|
|
326
323
|
<div
|
|
327
324
|
className={styles.inactiveTrack}
|
|
328
|
-
style={{ flex: 1 -
|
|
325
|
+
style={{ flex: 1 - percent / 100 }}
|
|
329
326
|
></div>
|
|
330
327
|
<div
|
|
331
328
|
className={
|
|
@@ -338,11 +335,11 @@ export const Slider = ({
|
|
|
338
335
|
|
|
339
336
|
const handleAndGapPercent =
|
|
340
337
|
((isChanging ? 9 : 10) / sliderWidth) * 100;
|
|
341
|
-
const markPercent =
|
|
338
|
+
const markPercent = getpercentFromValue(mark.value);
|
|
342
339
|
|
|
343
|
-
if (markPercent <=
|
|
340
|
+
if (markPercent <= percent - handleAndGapPercent) {
|
|
344
341
|
isUnderActiveTrack = true;
|
|
345
|
-
} else if (markPercent >=
|
|
342
|
+
} else if (markPercent >= percent + handleAndGapPercent) {
|
|
346
343
|
isUnderActiveTrack = false;
|
|
347
344
|
}
|
|
348
345
|
return (
|
|
@@ -355,7 +352,7 @@ export const Slider = ({
|
|
|
355
352
|
isUnderActiveTrack != null && !isUnderActiveTrack,
|
|
356
353
|
})}
|
|
357
354
|
style={{
|
|
358
|
-
left: `${
|
|
355
|
+
left: `${getpercentFromValue(mark.value)}%`,
|
|
359
356
|
}}
|
|
360
357
|
></div>
|
|
361
358
|
);
|
|
@@ -176,6 +176,41 @@ function resetRunFlags(el: HTMLElement, prefix: string): void {
|
|
|
176
176
|
// IO thresholds centralized for clarity
|
|
177
177
|
const IO_THRESHOLD: number[] = [0, 0.2];
|
|
178
178
|
|
|
179
|
+
// Track which elements have animation lifecycle listeners attached
|
|
180
|
+
const listenersAttached = new WeakSet<Element>();
|
|
181
|
+
|
|
182
|
+
function addAnimationLifecycle(el: HTMLElement, prefix: string): void {
|
|
183
|
+
if (listenersAttached.has(el)) return;
|
|
184
|
+
listenersAttached.add(el);
|
|
185
|
+
|
|
186
|
+
const onStart = (e: AnimationEvent) => {
|
|
187
|
+
if (e.target !== el) return;
|
|
188
|
+
// Only mark as animating if this animation was initiated by our run flags.
|
|
189
|
+
// This avoids setting data-{prefix}-animating during hydration or passive CSS animations
|
|
190
|
+
// which would block the initial in/out trigger from IntersectionObserver.
|
|
191
|
+
if (
|
|
192
|
+
el.hasAttribute(`data-${prefix}-in-run`) ||
|
|
193
|
+
el.hasAttribute(`data-${prefix}-out-run`) ||
|
|
194
|
+
el.hasAttribute(`data-${prefix}-run`)
|
|
195
|
+
) {
|
|
196
|
+
el.setAttribute(`data-${prefix}-animating`, ``);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const onEndOrCancel = (e: AnimationEvent) => {
|
|
201
|
+
if (e.target !== el) return;
|
|
202
|
+
el.removeAttribute(`data-${prefix}-animating`);
|
|
203
|
+
// Clear directional run flags so a new trigger can happen after completion
|
|
204
|
+
el.removeAttribute(`data-${prefix}-in-run`);
|
|
205
|
+
el.removeAttribute(`data-${prefix}-out-run`);
|
|
206
|
+
// Note: keep generic data-{prefix}-run for style stability
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
el.addEventListener('animationstart', onStart as EventListener);
|
|
210
|
+
el.addEventListener('animationend', onEndOrCancel as EventListener);
|
|
211
|
+
el.addEventListener('animationcancel', onEndOrCancel as EventListener);
|
|
212
|
+
}
|
|
213
|
+
|
|
179
214
|
export type AnimateOnScrollOptions = {
|
|
180
215
|
prefix?: string;
|
|
181
216
|
once?: boolean;
|
|
@@ -202,6 +237,9 @@ export function initAnimateOnScroll(
|
|
|
202
237
|
|
|
203
238
|
if (!isJsObserverCandidate(el)) continue;
|
|
204
239
|
|
|
240
|
+
// If an animation is in progress, avoid re-triggering or flipping direction
|
|
241
|
+
if (el.hasAttribute(`data-${prefix}-animating`)) continue;
|
|
242
|
+
|
|
205
243
|
const isOut = el.classList.contains(`${prefix}-out`);
|
|
206
244
|
|
|
207
245
|
if (!isOut && entry.isIntersecting) {
|
|
@@ -211,6 +249,7 @@ export function initAnimateOnScroll(
|
|
|
211
249
|
setRunFlag(el, prefix, 'out');
|
|
212
250
|
if (once) io.unobserve(el);
|
|
213
251
|
} else if (!once) {
|
|
252
|
+
// Only reset flags if not currently animating (already checked), to prevent rapid restarts
|
|
214
253
|
resetRunFlags(el, prefix);
|
|
215
254
|
}
|
|
216
255
|
}
|
|
@@ -224,6 +263,7 @@ export function initAnimateOnScroll(
|
|
|
224
263
|
if (observed.has(el)) continue;
|
|
225
264
|
observed.add(el);
|
|
226
265
|
io.observe(el);
|
|
266
|
+
addAnimationLifecycle(el, prefix);
|
|
227
267
|
}
|
|
228
268
|
};
|
|
229
269
|
|
|
@@ -258,6 +298,7 @@ export function initAnimateOnScroll(
|
|
|
258
298
|
if (!observed.has(t)) {
|
|
259
299
|
observed.add(t);
|
|
260
300
|
io.observe(t);
|
|
301
|
+
addAnimationLifecycle(t as HTMLElement, prefix);
|
|
261
302
|
}
|
|
262
303
|
}
|
|
263
304
|
}
|
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { motion, useMotionValueEvent, useScroll } from 'motion/react';
|
|
3
|
-
import { throttle } from 'throttle-debounce';
|
|
4
3
|
import { CustomScrollInterface } from './custom-scroll.interface';
|
|
5
4
|
import { customScrollStyle } from './custom-scroll.style';
|
|
6
5
|
import { ReactProps } from '../../utils';
|
|
7
6
|
|
|
7
|
+
// Throttle helper that guarantees execution of the latest call after the wait window (leading + trailing)
|
|
8
|
+
function createScrollThrottle(
|
|
9
|
+
wait: number,
|
|
10
|
+
fn: (latestValue: number, scrollOrientation: 'x' | 'y') => void,
|
|
11
|
+
) {
|
|
12
|
+
let lastInvokeTime = 0;
|
|
13
|
+
let trailingTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
14
|
+
let lastArgs: { v: number; o: 'x' | 'y' } | null = null;
|
|
15
|
+
|
|
16
|
+
const invoke = (v: number, o: 'x' | 'y') => {
|
|
17
|
+
lastInvokeTime = Date.now();
|
|
18
|
+
fn(v, o);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return (v: number, o: 'x' | 'y') => {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const remaining = wait - (now - lastInvokeTime);
|
|
24
|
+
|
|
25
|
+
if (remaining <= 0) {
|
|
26
|
+
if (trailingTimeout) {
|
|
27
|
+
clearTimeout(trailingTimeout);
|
|
28
|
+
trailingTimeout = null;
|
|
29
|
+
}
|
|
30
|
+
invoke(v, o);
|
|
31
|
+
} else {
|
|
32
|
+
// Save the latest call and schedule a trailing execution
|
|
33
|
+
lastArgs = { v, o };
|
|
34
|
+
if (!trailingTimeout) {
|
|
35
|
+
trailingTimeout = setTimeout(() => {
|
|
36
|
+
trailingTimeout = null;
|
|
37
|
+
const args = lastArgs;
|
|
38
|
+
lastArgs = null;
|
|
39
|
+
if (args) invoke(args.v, args.o);
|
|
40
|
+
}, remaining);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
8
46
|
export const CustomScroll = ({
|
|
9
47
|
children,
|
|
10
48
|
orientation = 'vertical',
|
|
@@ -13,6 +51,8 @@ export const CustomScroll = ({
|
|
|
13
51
|
className,
|
|
14
52
|
draggable = false,
|
|
15
53
|
throttleDuration = 75,
|
|
54
|
+
scroll,
|
|
55
|
+
setScroll,
|
|
16
56
|
}: ReactProps<CustomScrollInterface>) => {
|
|
17
57
|
const ref = useRef<HTMLDivElement>(null);
|
|
18
58
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
@@ -87,7 +127,7 @@ export const CustomScroll = ({
|
|
|
87
127
|
>(null);
|
|
88
128
|
|
|
89
129
|
if (!handleScrollThrottledRef.current) {
|
|
90
|
-
handleScrollThrottledRef.current =
|
|
130
|
+
handleScrollThrottledRef.current = createScrollThrottle(
|
|
91
131
|
throttleDuration,
|
|
92
132
|
(latestValue, scrollOrientation: 'x' | 'y') => {
|
|
93
133
|
if (
|
|
@@ -96,6 +136,12 @@ export const CustomScroll = ({
|
|
|
96
136
|
!ref.current
|
|
97
137
|
)
|
|
98
138
|
return;
|
|
139
|
+
|
|
140
|
+
// Notify simple percentage if requested
|
|
141
|
+
if (scrollOrientation === (orientation === 'horizontal' ? 'x' : 'y')) {
|
|
142
|
+
setScroll?.(latestValue);
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
if (onScroll) {
|
|
100
146
|
if (orientation === 'horizontal' && scrollOrientation === 'x') {
|
|
101
147
|
onScroll({
|
|
@@ -136,6 +182,22 @@ export const CustomScroll = ({
|
|
|
136
182
|
if (dimensions.height) handleScroll(scrollYProgress.get(), 'y'); // Nouvelle ligne : mise à jour pour la hauteur
|
|
137
183
|
}, [dimensions]);
|
|
138
184
|
|
|
185
|
+
// Apply controlled scroll percentage to DOM when provided
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const container = ref.current;
|
|
188
|
+
const content = contentRef.current;
|
|
189
|
+
if (!container || !content) return;
|
|
190
|
+
if (typeof scroll !== 'number') return;
|
|
191
|
+
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));
|
|
192
|
+
if (orientation === 'horizontal') {
|
|
193
|
+
const total = Math.max(0, (scrollSize ?? content.scrollWidth) - container.clientWidth);
|
|
194
|
+
container.scrollLeft = clamp(scroll * total, 0, total);
|
|
195
|
+
} else {
|
|
196
|
+
const total = Math.max(0, (scrollSize ?? content.scrollHeight) - container.clientHeight);
|
|
197
|
+
container.scrollTop = clamp(scroll * total, 0, total);
|
|
198
|
+
}
|
|
199
|
+
}, [scroll, orientation, scrollSize]);
|
|
200
|
+
|
|
139
201
|
useMotionValueEvent(scrollXProgress, 'change', (latestValue) => {
|
|
140
202
|
handleScroll(latestValue, 'x');
|
|
141
203
|
});
|
|
@@ -228,6 +290,53 @@ export const CustomScroll = ({
|
|
|
228
290
|
};
|
|
229
291
|
}, []);
|
|
230
292
|
|
|
293
|
+
// External controller: listen for bubbling custom event to set scroll programmatically
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
const el = ref.current;
|
|
296
|
+
if (!el) return;
|
|
297
|
+
const handler = (ev: Event) => {
|
|
298
|
+
const detail = (ev as CustomEvent).detail as
|
|
299
|
+
| { progress?: number; scroll?: number; orientation?: 'horizontal' | 'vertical' }
|
|
300
|
+
| undefined;
|
|
301
|
+
const container = ref.current;
|
|
302
|
+
if (!container || !detail) return;
|
|
303
|
+
const ori = detail.orientation ?? orientation;
|
|
304
|
+
if (typeof detail.progress === 'number') {
|
|
305
|
+
if (ori === 'horizontal') {
|
|
306
|
+
const total = Math.max(
|
|
307
|
+
0,
|
|
308
|
+
(contentScrollSize.current?.width ?? 0) - container.clientWidth,
|
|
309
|
+
);
|
|
310
|
+
container.scrollLeft = Math.min(total, Math.max(0, detail.progress * total));
|
|
311
|
+
} else {
|
|
312
|
+
const total = Math.max(
|
|
313
|
+
0,
|
|
314
|
+
(contentScrollSize.current?.height ?? 0) - container.clientHeight,
|
|
315
|
+
);
|
|
316
|
+
container.scrollTop = Math.min(total, Math.max(0, detail.progress * total));
|
|
317
|
+
}
|
|
318
|
+
} else if (typeof detail.scroll === 'number') {
|
|
319
|
+
if (ori === 'horizontal') {
|
|
320
|
+
const total = Math.max(
|
|
321
|
+
0,
|
|
322
|
+
(contentScrollSize.current?.width ?? 0) - container.clientWidth,
|
|
323
|
+
);
|
|
324
|
+
container.scrollLeft = Math.min(total, Math.max(0, detail.scroll));
|
|
325
|
+
} else {
|
|
326
|
+
const total = Math.max(
|
|
327
|
+
0,
|
|
328
|
+
(contentScrollSize.current?.height ?? 0) - container.clientHeight,
|
|
329
|
+
);
|
|
330
|
+
container.scrollTop = Math.min(total, Math.max(0, detail.scroll));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
el.addEventListener('udx:customScroll:set', handler as EventListener);
|
|
335
|
+
return () => {
|
|
336
|
+
el.removeEventListener('udx:customScroll:set', handler as EventListener);
|
|
337
|
+
};
|
|
338
|
+
}, [orientation]);
|
|
339
|
+
|
|
231
340
|
return (
|
|
232
341
|
<div
|
|
233
342
|
className={styles.customScroll}
|
|
@@ -10,6 +10,10 @@ type Props = {
|
|
|
10
10
|
scrollTotal: number;
|
|
11
11
|
scrollVisible: number;
|
|
12
12
|
}) => void;
|
|
13
|
+
// Controlled percentage (0..1). If provided, the container will reflect this progress.
|
|
14
|
+
scroll?: number;
|
|
15
|
+
// Callback fired with the latest scroll percentage (0..1) when user scrolls/drags.
|
|
16
|
+
setScroll?: (progress: number) => void;
|
|
13
17
|
draggable?: boolean;
|
|
14
18
|
throttleDuration?: number;
|
|
15
19
|
};
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
2
|
import { CarouselItem } from '../components';
|
|
3
3
|
|
|
4
|
+
export interface CarouselMetrics {
|
|
5
|
+
total: number;
|
|
6
|
+
selectedIndex: number;
|
|
7
|
+
visibleApprox: number; // fractional approximate number of visible items
|
|
8
|
+
visibleFull: number; // floored count of fully visible-width items
|
|
9
|
+
stepHalf: number; // suggested step = half of visibleFull (>=1)
|
|
10
|
+
canPrev: boolean;
|
|
11
|
+
canNext: boolean;
|
|
12
|
+
scrollProgress: number; // 0..1 (smoothed)
|
|
13
|
+
viewportWidth: number;
|
|
14
|
+
itemMaxWidth: number;
|
|
15
|
+
gap: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
export interface CarouselInterface {
|
|
5
19
|
type: 'div';
|
|
6
20
|
props: {
|
|
7
21
|
children?: ReactElement<typeof CarouselItem>[];
|
|
8
22
|
marginPourcent?: number;
|
|
9
23
|
onChange?: (index: number) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Receive live metrics to better control the carousel externally
|
|
26
|
+
*/
|
|
27
|
+
onMetricsChange?: (metrics: CarouselMetrics) => void;
|
|
28
|
+
index?: number; // Controlled index for programmatic centering
|
|
10
29
|
variant?:
|
|
11
30
|
| 'hero'
|
|
12
|
-
| 'center-aligned
|
|
31
|
+
| 'center-aligned'
|
|
13
32
|
| 'multi-browse'
|
|
14
33
|
| 'un-contained'
|
|
15
34
|
| 'full-screen';
|
|
@@ -3,9 +3,12 @@ import { classNames, defaultClassNames } from '../utils';
|
|
|
3
3
|
|
|
4
4
|
export const carouselItemStyle = defaultClassNames<CarouselItemInterface>(
|
|
5
5
|
'carouselItem',
|
|
6
|
-
() => {
|
|
6
|
+
({ width }) => {
|
|
7
7
|
return {
|
|
8
|
-
carouselItem: classNames('rounded-[28px] overflow-hidden flex-none'
|
|
8
|
+
carouselItem: classNames('rounded-[28px] overflow-hidden flex-none', {
|
|
9
|
+
hidden: width === undefined,
|
|
10
|
+
'flex-1': width == null,
|
|
11
|
+
}),
|
|
9
12
|
};
|
|
10
13
|
},
|
|
11
14
|
);
|
|
@@ -5,8 +5,6 @@ export const carouselStyle = defaultClassNames<CarouselInterface>(
|
|
|
5
5
|
'carousel',
|
|
6
6
|
() => ({
|
|
7
7
|
carousel: classNames(['w-full h-[400px]']),
|
|
8
|
-
track: classNames(
|
|
9
|
-
'grid grid-flow-col h-full transition-transform ease-out w-fit',
|
|
10
|
-
),
|
|
8
|
+
track: classNames('flex h-full w-full'),
|
|
11
9
|
}),
|
|
12
10
|
);
|
|
@@ -20,31 +20,21 @@ export const iconButtonStyle = defaultClassNames<IconButtonInterface>(
|
|
|
20
20
|
{
|
|
21
21
|
'cursor-default': disabled,
|
|
22
22
|
},
|
|
23
|
-
|
|
24
|
-
(shape === 'squared' &&
|
|
25
|
-
onToggle &&
|
|
26
|
-
!disabled &&
|
|
27
|
-
isActive &&
|
|
28
|
-
allowShapeTransformation)) && {
|
|
23
|
+
shape === 'rounded' && {
|
|
29
24
|
'rounded-[30px]': size === 'xSmall' || size == 'small',
|
|
30
25
|
'rounded-[40px]': size === 'medium',
|
|
31
26
|
'rounded-[70px]': size === 'large' || size == 'xLarge',
|
|
32
27
|
},
|
|
33
|
-
(shape === 'squared' ||
|
|
34
|
-
(shape === 'rounded' &&
|
|
35
|
-
onToggle &&
|
|
36
|
-
!disabled &&
|
|
37
|
-
isActive &&
|
|
38
|
-
allowShapeTransformation)) && {
|
|
28
|
+
(shape === 'squared' || (allowShapeTransformation && isActive)) && {
|
|
39
29
|
'rounded-[12px]': size === 'xSmall' || size == 'small',
|
|
40
30
|
'rounded-[16px]': size === 'medium',
|
|
41
31
|
'rounded-[28px]': size === 'large' || size == 'xLarge',
|
|
42
32
|
},
|
|
43
33
|
allowShapeTransformation &&
|
|
44
34
|
!disabled && {
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
35
|
+
'active:rounded-[12px]': size === 'xSmall' || size == 'small',
|
|
36
|
+
'active:rounded-[16px]': size === 'medium',
|
|
37
|
+
'active:rounded-[28px]': size === 'large' || size == 'xLarge',
|
|
48
38
|
},
|
|
49
39
|
variant === 'filled' && [
|
|
50
40
|
!disabled && {
|
|
@@ -14,7 +14,7 @@ export const sliderStyle = defaultClassNames<SliderInterface>(
|
|
|
14
14
|
'h-4 relative transition-all duration-100 bg-primary-container rounded-r-full overflow-hidden',
|
|
15
15
|
]),
|
|
16
16
|
handle: classNames([
|
|
17
|
-
'transform transition-all duration-100 bg-primary h-full rounded-full ',
|
|
17
|
+
'transform relative transition-all duration-100 bg-primary h-full rounded-full ',
|
|
18
18
|
{ 'w-0.5': isChanging, 'w-1': !isChanging },
|
|
19
19
|
]),
|
|
20
20
|
valueIndicator: classNames([
|