@udixio/ui-react 2.5.2 → 2.6.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.
@@ -6,8 +6,6 @@ import { ReactProps } from '../utils';
6
6
  * @status beta
7
7
  * @category Layout
8
8
  * @limitations
9
- * - At the end of the scroll, a residual gap/space may remain visible.
10
- * - In/out behavior is inconsistent at range edges.
11
9
  * - Responsive behavior on mobile is not supported.
12
10
  * - Only the default (hero) variant is supported.
13
11
  */
@@ -1 +1 @@
1
- {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../../src/lib/components/Carousel.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAyB,MAAM,eAAe,CAAC;AAIzE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAGtC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,GAAI,qKActB,UAAU,CAAC,iBAAiB,CAAC,4CAooB/B,CAAC"}
1
+ {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../../src/lib/components/Carousel.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAyB,MAAM,eAAe,CAAC;AAIzE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAOtC;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,GAAI,qKActB,UAAU,CAAC,iBAAiB,CAAC,4CA2f/B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"AnimateOnScroll.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/AnimateOnScroll.ts"],"names":[],"mappings":"AAoNA,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,CAsIZ;AAGD,eAAO,MAAM,mBAAmB,4BAAsB,CAAC;AACvD,eAAO,MAAM,eAAe,4BAAsB,CAAC"}
1
+ {"version":3,"file":"AnimateOnScroll.d.ts","sourceRoot":"","sources":["../../../src/lib/effects/AnimateOnScroll.ts"],"names":[],"mappings":"AA2NA,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,CAgKZ;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.5.2",
3
+ "version": "2.6.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.3.7",
40
- "@udixio/theme": "2.1.2"
39
+ "@udixio/theme": "2.1.2",
40
+ "@udixio/tailwind": "2.3.7"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
@@ -7,14 +7,16 @@ import { CustomScroll } from '../effects';
7
7
  import { ReactProps } from '../utils';
8
8
  import { CarouselItem, normalize } from './CarouselItem';
9
9
 
10
+ function clamp(v: number, min: number, max: number) {
11
+ return Math.max(min, Math.min(max, v));
12
+ }
13
+
10
14
  /**
11
15
  * Carousels show a collection of items that can be scrolled on and off the screen
12
16
  *
13
17
  * @status beta
14
18
  * @category Layout
15
19
  * @limitations
16
- * - At the end of the scroll, a residual gap/space may remain visible.
17
- * - In/out behavior is inconsistent at range edges.
18
20
  * - Responsive behavior on mobile is not supported.
19
21
  * - Only the default (hero) variant is supported.
20
22
  */
@@ -49,6 +51,7 @@ export const Carousel = ({
49
51
  onChange,
50
52
  gap,
51
53
  scrollSensitivity,
54
+ onMetricsChange,
52
55
  });
53
56
 
54
57
  const items = React.Children.toArray(children).filter(
@@ -79,7 +82,6 @@ export const Carousel = ({
79
82
 
80
83
  const scrollVisible =
81
84
  scroll?.scrollVisible ?? (ref.current as any)?.clientWidth ?? 0;
82
- // const scrollProgress = scrollMV.get();
83
85
 
84
86
  function assignRelativeIndexes(
85
87
  values: number[],
@@ -90,13 +92,16 @@ export const Carousel = ({
90
92
  index: number;
91
93
  width: number;
92
94
  }[] {
93
- return values.map((value, index) => ({
94
- itemScrollXCenter: value,
95
- relativeIndex:
96
- (value - progressScroll) / Math.abs(values[1] - values[0]),
97
- index: index,
98
- width: 0,
99
- }));
95
+ return values.map((value, index) => {
96
+ const relativeIndex =
97
+ (value - progressScroll) / Math.abs(values[1] - values[0]);
98
+ return {
99
+ itemScrollXCenter: value,
100
+ relativeIndex,
101
+ index: index,
102
+ width: 0,
103
+ };
104
+ });
100
105
  }
101
106
 
102
107
  const itemsScrollXCenter = items.map((_, index) => {
@@ -113,206 +118,85 @@ export const Carousel = ({
113
118
  itemsScrollXCenter,
114
119
  scroll?.scrollProgress ?? 0,
115
120
  ).sort((a, b) => a.index - b.index);
116
- // const visible =
117
- // ((ref.current?.clientWidth ?? scrollVisible) - (outputRange[0] + gap)) /
118
- // (outputRange[1] + gap);
119
-
120
- const visible =
121
- ((ref.current?.clientWidth ?? scrollVisible) + gap) /
122
- (outputRange[1] + gap);
123
121
 
124
- let widthContent = visible;
125
-
126
- let selectedItem;
122
+ let widthLeft =
123
+ (ref.current?.clientWidth ?? scrollVisible) + gap + outputRange[0] + gap;
127
124
 
128
125
  const visibleItemValues = itemValues
129
126
  .sort((a, b) => Math.abs(a.relativeIndex) - Math.abs(b.relativeIndex))
130
127
  .map((item, index) => {
131
- if (!item) return;
132
-
133
- if (index === 0) {
128
+ if (widthLeft <= 0) {
129
+ return undefined;
130
+ }
131
+ if (index == 0) {
134
132
  setSelectedItem(item.index);
135
- selectedItem = item;
136
133
  }
137
134
 
138
- const el = itemRefs[item.index]?.current;
139
- if (!ref.current || !el) return;
135
+ item.width = normalize(
136
+ widthLeft - gap,
137
+ [outputRange[0], outputRange[1]],
138
+ [outputRange[0], outputRange[1]],
139
+ );
140
+
141
+ widthLeft -= item.width + gap;
140
142
 
141
- if (widthContent <= 0) {
142
- return undefined;
143
- } else {
144
- item.width = outputRange[1];
145
- }
143
+ if (widthLeft != 0 && widthLeft < (outputRange[0] + gap) * 2) {
144
+ const newWidth =
145
+ item.width - ((outputRange[0] + gap) * 2 - widthLeft);
146
+
147
+ widthLeft += item.width;
148
+ item.width = newWidth;
149
+ widthLeft -= item.width;
150
+ } else if (widthLeft == 0 && item.width >= outputRange[0] * 2 + gap) {
151
+ const newWidth = item.width - (outputRange[0] + gap - widthLeft);
146
152
 
147
- --widthContent;
153
+ widthLeft += item.width;
154
+ item.width = newWidth;
155
+ widthLeft -= item.width;
156
+ }
148
157
  return item;
149
158
  })
150
- .filter(Boolean)
151
- .sort((a, b) => a.index - b.index) as {
159
+ .filter(Boolean) as unknown as {
152
160
  itemScrollXCenter: number;
153
161
  relativeIndex: number;
154
162
  index: number;
155
163
  width: number;
156
164
  }[];
157
165
 
158
- let widthLeft = (ref.current?.clientWidth ?? scrollVisible) - gap;
159
-
160
- const dynamicItems = visibleItemValues.filter((item, index, array) => {
161
- let isDynamic = false;
162
- if (item.width == outputRange[1]) {
163
- if (index === 0 || index === array.length - 1) {
164
- isDynamic = true;
165
- }
166
- }
167
- if (isDynamic) {
168
- return true;
169
- } else {
170
- widthLeft -= item.width + gap;
171
- return false;
172
- }
173
- });
166
+ const reverseItemsVisible = visibleItemValues.reverse();
167
+ const itemsVisibleByIndex = [...visibleItemValues].sort(
168
+ (a, b) => Math.abs(a.index) - Math.abs(b.index),
169
+ );
174
170
 
175
- // console.log(dynamicItems, visibleItemValues, visible);
171
+ //dynamic items
176
172
 
177
- let dynamicWidth = 0;
173
+ reverseItemsVisible.forEach((item, index) => {
174
+ const nextItem = reverseItemsVisible[index + 1];
175
+ if (!nextItem) return;
178
176
 
179
- dynamicItems.forEach((item) => {
180
- if (!item) return;
177
+ const test =
178
+ 1 - (Math.abs(item.relativeIndex) - Math.abs(nextItem?.relativeIndex));
181
179
 
182
- const result = normalize(
183
- 1 - Math.abs(scroll.scrollProgress - item.itemScrollXCenter),
184
- [0, 1],
185
- [0, 1],
180
+ const newWidth = normalize(
181
+ test,
182
+ [0, 2],
183
+ [item.width + widthLeft, nextItem.width],
186
184
  );
187
- item.width = result;
188
- dynamicWidth += result;
189
- });
190
-
191
- // let widthLeft =
192
- // (visible -
193
- // visibleItemValues
194
- // .slice(0, visibleItemValues.length - 2)
195
- // .filter((item) => item.width === outputRange[1]).length) *
196
- // outputRange[1];
197
-
198
- // console.log(
199
- // visible,
200
- // widthLeft,
201
- // visibleItemValues
202
- // .slice(0, visibleItemValues.length - 2)
203
- // .filter((item) => item.width === outputRange[1]).length,
204
- // );
205
-
206
- let translate = 0;
207
- dynamicItems.forEach((item, index) => {
208
- if (!item) return;
209
-
210
- if (index == 0) {
211
- const percent = normalize(
212
- item?.relativeIndex,
213
- [-2, item.index == 0 ? 0 : -1],
214
- [0, 1],
215
- );
216
-
217
- if (item.index >= 1) {
218
- itemValues.sort((a, b) => a.index - b.index);
219
185
 
220
- itemValues[item.index - 1].width = outputRange[0];
221
- visibleItemValues.unshift(itemValues[item.index - 1]);
222
- widthLeft -= outputRange[0] + gap;
223
-
224
- translate = normalize(
225
- 1 - percent,
226
- [0, 1],
227
- [0, -(outputRange[0] + gap)],
228
- );
229
- }
230
- widthLeft -= translate;
231
-
232
- // let relative = selectedItem?.relativeIndex * 2;
233
- // relative = relative > 0 ? (1 - relative) * -1 : 1 + relative;
234
-
235
- item.width = normalize(
236
- percent,
237
- [0, 1],
238
- [outputRange[0], outputRange[1]],
239
- );
240
-
241
- widthLeft -= item.width;
242
-
243
- // console.log(widthLeft);
244
- } else {
245
- let dynamicIndex = item.index;
246
- // console.log('n', dynamicIndex, widthLeft);
247
- let isEnd = dynamicIndex == itemValues.length - 1;
248
- const isNearEnd = dynamicIndex == itemValues.length - 2;
249
- // console.log('start');
250
- while (widthLeft > 0) {
251
- // console.log('boucle', widthLeft);
252
- const dynamicItem = itemValues.filter(
253
- (item) => item.index === dynamicIndex,
254
- )[0];
255
-
256
- if (!dynamicItem) {
257
- if (isEnd) {
258
- throw new Error('dynamicItem not found');
259
- }
260
- // dynamicIndex = dynamicItems[0].index;
261
- isEnd = true;
262
- break;
263
- }
264
-
265
- if (!visibleItemValues.includes(dynamicItem)) {
266
- visibleItemValues.push(dynamicItem);
267
- }
268
-
269
- dynamicItem.width = normalize(
270
- widthLeft,
271
- [outputRange[0], outputRange[1] + (gap + outputRange[0]) * 2],
272
- [outputRange[0], outputRange[1]],
273
- );
274
-
275
- widthLeft -= dynamicItem.width + gap;
276
-
277
- if (
278
- (isNearEnd || isEnd) &&
279
- dynamicItem.index == itemValues.length - 1
280
- ) {
281
- let dynamicItemIndexStart = isEnd ? 1 : 1;
282
-
283
- while (widthLeft > 0) {
284
- const dynamicItemStart = visibleItemValues[dynamicItemIndexStart];
285
-
286
- const width =
287
- normalize(
288
- dynamicItemStart.width + widthLeft,
289
- [outputRange[0], outputRange[1]],
290
- [outputRange[0], outputRange[1]],
291
- ) - dynamicItemStart.width;
292
-
293
- dynamicItemStart.width += width;
294
- widthLeft -= width;
295
-
296
- dynamicItemIndexStart--;
297
- // break;
298
- }
299
-
300
- break;
301
- } else {
302
- dynamicIndex++;
303
- }
304
- // }
305
- }
306
- }
186
+ widthLeft += item.width;
187
+ item.width = newWidth;
188
+ widthLeft -= item.width;
189
+ });
307
190
 
308
- // console.log(item, dynamicWidth, visible, selectedItem);
191
+ const percentMax = visibleItemValues.length / 2;
192
+ const percent = normalize(
193
+ Math.abs(itemsVisibleByIndex[0].relativeIndex),
194
+ [itemsVisibleByIndex[0].index == 0 ? 0 : percentMax - 1, percentMax],
195
+ [0, 1],
196
+ );
309
197
 
310
- // item.width = normalize(
311
- // item.width / dynamicWidth,
312
- // [0, 1],
313
- // [0, visible * outputRange[1]],
314
- // );
315
- });
198
+ const translate =
199
+ normalize(percent, [0, 1], [0, 1]) * -(outputRange[0] + gap);
316
200
 
317
201
  setTranslateX(translate);
318
202
 
@@ -414,25 +298,6 @@ export const Carousel = ({
414
298
  );
415
299
  });
416
300
 
417
- // persistent motion value for scroll progress, driven by user scroll and programmatic centering
418
- // const scrollMVRef = useRef(motionValue(0));
419
- // const scrollMV = scrollMVRef.current;
420
-
421
- // const transform = useTransform(
422
- // scrollMV,
423
- // [0, 1],
424
- // [
425
- // 0,
426
- // 1 -
427
- // (ref.current?.clientWidth ?? 0) / (trackRef?.current?.clientWidth ?? 0),
428
- // ],
429
- // );
430
-
431
- // const percentTransform = useTransform(
432
- // transform,
433
- // (value) => `${-value * 100}%`,
434
- // );
435
-
436
301
  const handleScroll = (args: {
437
302
  scrollProgress: number;
438
303
  scrollTotal: number;
@@ -152,10 +152,11 @@ function queryJsObserverCandidates(
152
152
  }
153
153
 
154
154
  // Utility: identify presence of in/out classes
155
- function hasInOutClass(cls: DOMTokenList, prefix: string): boolean {
156
- return cls.contains(`${prefix}-in`) || cls.contains(`${prefix}-out`);
155
+ function hasOutClass(cls: DOMTokenList, prefix: string): boolean {
156
+ return Array.from(cls).some(
157
+ (className) => className.startsWith(prefix) && className.includes('-out'),
158
+ );
157
159
  }
158
-
159
160
  // Utility: set run flags for a given direction ("in" or "out"), always ensuring generic run flag exists
160
161
  function setRunFlag(el: HTMLElement, prefix: string, dir: 'in' | 'out'): void {
161
162
  el.setAttribute(`data-${prefix}-run`, ``);
@@ -163,12 +164,20 @@ function setRunFlag(el: HTMLElement, prefix: string, dir: 'in' | 'out'): void {
163
164
  }
164
165
 
165
166
  // Utility: reset run flags and restart animation timeline without changing computed styles
166
- function resetRunFlags(el: HTMLElement, prefix: string): void {
167
+ function resetRunFlags(
168
+ el: HTMLElement,
169
+ prefix: string,
170
+ direction?: 'in' | 'out',
171
+ ): void {
167
172
  const currentAnimationName = el.style.animationName;
168
173
  el.style.animationName = 'none';
169
174
  el.removeAttribute(`data-${prefix}-run`);
170
- el.removeAttribute(`data-${prefix}-in-run`);
171
- el.removeAttribute(`data-${prefix}-out-run`);
175
+ if (!direction) {
176
+ el.removeAttribute(`data-${prefix}-in-run`);
177
+ el.removeAttribute(`data-${prefix}-out-run`);
178
+ } else {
179
+ el.removeAttribute(`data-${prefix}-${direction}-run`);
180
+ }
172
181
  void (el as HTMLElement).offsetWidth; // reflow to restart animations
173
182
  el.style.animationName = currentAnimationName;
174
183
  }
@@ -198,11 +207,9 @@ function addAnimationLifecycle(el: HTMLElement, prefix: string): void {
198
207
 
199
208
  const onEndOrCancel = (e: AnimationEvent) => {
200
209
  if (e.target !== el) return;
210
+ // If an IN animation just finished, persist a completion flag so it won't replay on upward scroll
211
+
201
212
  el.removeAttribute(`data-${prefix}-animating`);
202
- // Clear directional run flags so a new trigger can happen after completion
203
- el.removeAttribute(`data-${prefix}-in-run`);
204
- el.removeAttribute(`data-${prefix}-out-run`);
205
- // Note: keep generic data-{prefix}-run for style stability
206
213
  };
207
214
 
208
215
  el.addEventListener('animationstart', onStart as EventListener);
@@ -229,6 +236,21 @@ export function initAnimateOnScroll(
229
236
  // Setup JS observers for non-scroll-driven animations
230
237
  const observed = new WeakSet<Element>();
231
238
 
239
+ // Track scroll direction to prevent triggering IN when scrolling up
240
+ let lastScrollY =
241
+ typeof window !== 'undefined'
242
+ ? window.pageYOffset || window.scrollY || 0
243
+ : 0;
244
+ let scrollingDown = true; // default allow initial IN
245
+ const onScrollDir = () => {
246
+ const y = window.pageYOffset || window.scrollY || 0;
247
+ scrollingDown = y >= lastScrollY;
248
+ lastScrollY = y;
249
+ };
250
+ if (typeof window !== 'undefined') {
251
+ window.addEventListener('scroll', onScrollDir, { passive: true });
252
+ }
253
+
232
254
  const io = new IntersectionObserver(
233
255
  (entries) => {
234
256
  for (const entry of entries) {
@@ -239,17 +261,25 @@ export function initAnimateOnScroll(
239
261
  // If an animation is in progress, avoid re-triggering or flipping direction
240
262
  if (el.hasAttribute(`data-${prefix}-animating`)) continue;
241
263
 
242
- const isOut = el.classList.contains(`${prefix}-out`);
264
+ const isOut = hasOutClass(el.classList, prefix);
243
265
 
244
- if (!isOut && entry.isIntersecting) {
266
+ if (entry.isIntersecting) {
267
+ if (isOut) {
268
+ resetRunFlags(el, prefix, 'out');
269
+ }
245
270
  setRunFlag(el, prefix, 'in');
271
+
246
272
  if (once) io.unobserve(el);
247
- } else if (isOut && !entry.isIntersecting) {
248
- setRunFlag(el, prefix, 'out');
249
- if (once) io.unobserve(el);
250
- } else if (!once) {
251
- // Only reset flags if not currently animating (already checked), to prevent rapid restarts
252
- resetRunFlags(el, prefix);
273
+ } else {
274
+ if (!once) {
275
+ if (!scrollingDown) {
276
+ resetRunFlags(el, prefix, 'in');
277
+ }
278
+
279
+ if (isOut) {
280
+ setRunFlag(el, prefix, 'out');
281
+ }
282
+ }
253
283
  }
254
284
  }
255
285
  },
@@ -349,6 +379,9 @@ export function initAnimateOnScroll(
349
379
  // Public cleanup
350
380
  return () => {
351
381
  if (cleanupScrollDriven) cleanupScrollDriven();
382
+ if (typeof window !== 'undefined') {
383
+ window.removeEventListener('scroll', onScrollDir as EventListener);
384
+ }
352
385
  io.disconnect();
353
386
  };
354
387
  }