@udixio/ui-react 2.4.2 → 2.5.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/index.cjs +2 -2
  3. package/dist/index.js +1712 -1437
  4. package/dist/lib/components/Carousel.d.ts +7 -2
  5. package/dist/lib/components/Carousel.d.ts.map +1 -1
  6. package/dist/lib/components/CarouselItem.d.ts +1 -1
  7. package/dist/lib/components/CarouselItem.d.ts.map +1 -1
  8. package/dist/lib/components/Slider.d.ts.map +1 -1
  9. package/dist/lib/effects/custom-scroll/custom-scroll.effect.d.ts +1 -1
  10. package/dist/lib/effects/custom-scroll/custom-scroll.effect.d.ts.map +1 -1
  11. package/dist/lib/effects/custom-scroll/custom-scroll.interface.d.ts +2 -0
  12. package/dist/lib/effects/custom-scroll/custom-scroll.interface.d.ts.map +1 -1
  13. package/dist/lib/effects/custom-scroll/custom-scroll.style.d.ts +4 -0
  14. package/dist/lib/effects/custom-scroll/custom-scroll.style.d.ts.map +1 -1
  15. package/dist/lib/interfaces/carousel-item.interface.d.ts +1 -0
  16. package/dist/lib/interfaces/carousel-item.interface.d.ts.map +1 -1
  17. package/dist/lib/interfaces/carousel.interface.d.ts +19 -1
  18. package/dist/lib/interfaces/carousel.interface.d.ts.map +1 -1
  19. package/dist/lib/styles/carousel-item.style.d.ts +2 -0
  20. package/dist/lib/styles/carousel-item.style.d.ts.map +1 -1
  21. package/dist/lib/styles/carousel.style.d.ts +5 -1
  22. package/dist/lib/styles/carousel.style.d.ts.map +1 -1
  23. package/dist/lib/styles/icon-button.style.d.ts.map +1 -1
  24. package/package.json +3 -3
  25. package/src/lib/components/Carousel.tsx +535 -52
  26. package/src/lib/components/CarouselItem.tsx +12 -6
  27. package/src/lib/components/Slider.tsx +25 -28
  28. package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +111 -2
  29. package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +4 -0
  30. package/src/lib/interfaces/carousel-item.interface.ts +1 -0
  31. package/src/lib/interfaces/carousel.interface.ts +20 -1
  32. package/src/lib/styles/carousel-item.style.ts +5 -2
  33. package/src/lib/styles/carousel.style.ts +1 -3
  34. package/src/lib/styles/icon-button.style.ts +5 -15
  35. 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 = 1,
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: width,
40
+ width,
41
41
  children,
42
+ outputRange,
42
43
  });
43
44
 
44
45
  return (
45
- <motion.div
46
+ <div
46
47
  ref={ref}
47
- animate={{ width: width + 'px' }}
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
- </motion.div>
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 getPourcentFromValue = (value: number) => {
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 getValueFromPourcent = (pourcent: number) => {
59
+ const getValueFrompercent = (percent: number) => {
60
60
  const min = getMin(false);
61
61
  const max = getMax(false);
62
- return ((max - min) * pourcent) / 100 + 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 [pourcent, setPourcent] = useState(getPourcentFromValue(defaultValue));
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 pourcent = ((clientX - refPosition) / current.offsetWidth) * 100;
133
+ const percent = ((clientX - refPosition) / current.offsetWidth) * 100;
134
134
 
135
- updateSliderValues({ pourcent });
135
+ updateSliderValues({ percent });
136
136
  }
137
137
  };
138
138
  const updateSliderValues = ({
139
- pourcent,
139
+ percent,
140
140
  value,
141
141
  }: {
142
- pourcent?: number;
142
+ percent?: number;
143
143
  value?: number;
144
144
  }) => {
145
- if (pourcent) {
146
- if (pourcent >= 100) {
145
+ if (percent) {
146
+ if (percent >= 100) {
147
147
  setValue(getMax(true));
148
- setPourcent(100);
148
+ setpercent(100);
149
149
  return;
150
150
  }
151
- if (pourcent <= 0) {
151
+ if (percent <= 0) {
152
152
  setValue(getMin(true));
153
- setPourcent(0);
153
+ setpercent(0);
154
154
  return;
155
155
  }
156
156
 
157
- value = getValueFromPourcent(pourcent);
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
- setPourcent(100);
167
+ setpercent(100);
168
168
  return;
169
169
  }
170
170
  if (value <= getMin()) {
171
171
  setValue(getMin(true));
172
- setPourcent(0);
172
+ setpercent(0);
173
173
  return;
174
174
  }
175
- pourcent = getPourcentFromValue(value);
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
- pourcent = getPourcentFromValue(value);
209
+ percent = getpercentFromValue(value);
210
210
 
211
211
  setValue(value);
212
- setPourcent(pourcent);
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 - pourcent / 100 }}
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 = getPourcentFromValue(mark.value);
338
+ const markPercent = getpercentFromValue(mark.value);
342
339
 
343
- if (markPercent <= pourcent - handleAndGapPercent) {
340
+ if (markPercent <= percent - handleAndGapPercent) {
344
341
  isUnderActiveTrack = true;
345
- } else if (markPercent >= pourcent + handleAndGapPercent) {
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: `${getPourcentFromValue(mark.value)}%`,
355
+ left: `${getpercentFromValue(mark.value)}%`,
359
356
  }}
360
357
  ></div>
361
358
  );
@@ -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 = throttle(
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
  };
@@ -6,6 +6,7 @@ export interface CarouselItemInterface {
6
6
  children?: ReactNode | undefined;
7
7
  width?: number;
8
8
  index?: number;
9
+ outputRange?: [number, number];
9
10
  };
10
11
  elements: ['carouselItem'];
11
12
  }
@@ -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 hero'
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
- (shape === 'rounded' ||
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
- 'group-active:rounded-[12px]': size === 'xSmall' || size == 'small',
46
- 'group-active:rounded-[16px]': size === 'medium',
47
- 'group-active:rounded-[28px]': size === 'large' || size == 'xLarge',
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([