@udixio/ui-react 2.10.12 → 2.10.14
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/dist/index.cjs +3 -3
- package/dist/index.js +2696 -2710
- package/dist/lib/effects/ThemeProvider.d.ts.map +1 -1
- package/dist/theme.worker.js +6633 -0
- package/package.json +4 -1
- package/.eslintrc.mjs +0 -22
- package/.storybook/main.ts +0 -20
- package/.storybook/preview.ts +0 -1
- package/CHANGELOG.md +0 -1130
- package/dist/scrollDriven-AP2yWhzi.js +0 -121
- package/postcss.config.mjs +0 -5
- package/src/index.css +0 -4
- package/src/index.ts +0 -1
- package/src/lib/components/AnchorPositioner.tsx +0 -185
- package/src/lib/components/Button.tsx +0 -208
- package/src/lib/components/Card.tsx +0 -47
- package/src/lib/components/Carousel.tsx +0 -437
- package/src/lib/components/CarouselItem.tsx +0 -61
- package/src/lib/components/Checkbox.tsx +0 -120
- package/src/lib/components/Chip.tsx +0 -341
- package/src/lib/components/Chips.tsx +0 -331
- package/src/lib/components/ContextMenu.tsx +0 -109
- package/src/lib/components/DatePicker.tsx +0 -432
- package/src/lib/components/Divider.tsx +0 -20
- package/src/lib/components/Fab.tsx +0 -127
- package/src/lib/components/FabMenu.tsx +0 -239
- package/src/lib/components/IconButton.tsx +0 -146
- package/src/lib/components/Menu.tsx +0 -88
- package/src/lib/components/MenuGroup.tsx +0 -34
- package/src/lib/components/MenuHeadline.tsx +0 -9
- package/src/lib/components/MenuItem.tsx +0 -215
- package/src/lib/components/NavigationRail.tsx +0 -186
- package/src/lib/components/NavigationRailItem.tsx +0 -227
- package/src/lib/components/ProgressIndicator.tsx +0 -214
- package/src/lib/components/SideSheet.tsx +0 -135
- package/src/lib/components/Slider.tsx +0 -374
- package/src/lib/components/Snackbar.tsx +0 -77
- package/src/lib/components/Switch.tsx +0 -107
- package/src/lib/components/Tab.tsx +0 -123
- package/src/lib/components/TabGroup.tsx +0 -66
- package/src/lib/components/TabGroupContext.tsx +0 -16
- package/src/lib/components/TabPanel.tsx +0 -27
- package/src/lib/components/TabPanels.tsx +0 -76
- package/src/lib/components/Tabs.tsx +0 -105
- package/src/lib/components/TextField.tsx +0 -586
- package/src/lib/components/Tooltip.tsx +0 -217
- package/src/lib/components/index.ts +0 -34
- package/src/lib/config/config.interface.ts +0 -9
- package/src/lib/config/define-config.ts +0 -16
- package/src/lib/config/index.ts +0 -2
- package/src/lib/effects/AnimateOnScroll.ts +0 -391
- package/src/lib/effects/State.tsx +0 -90
- package/src/lib/effects/SyncedFixedWrapper.tsx +0 -62
- package/src/lib/effects/ThemeProvider.tsx +0 -172
- package/src/lib/effects/block-scroll.effect.tsx +0 -313
- package/src/lib/effects/custom-scroll/custom-scroll.effect.tsx +0 -407
- package/src/lib/effects/custom-scroll/custom-scroll.interface.ts +0 -29
- package/src/lib/effects/custom-scroll/custom-scroll.style.ts +0 -32
- package/src/lib/effects/custom-scroll/index.ts +0 -3
- package/src/lib/effects/index.ts +0 -7
- package/src/lib/effects/ripple/RippleEffect.tsx +0 -116
- package/src/lib/effects/ripple/index.tsx +0 -1
- package/src/lib/effects/scrollDriven.ts +0 -239
- package/src/lib/effects/smooth-scroll.effect.tsx +0 -112
- package/src/lib/effects/theme.worker.ts +0 -97
- package/src/lib/hooks/index.ts +0 -10
- package/src/lib/hooks/useTooltipTrigger.ts +0 -270
- package/src/lib/icon/icon.tsx +0 -125
- package/src/lib/icon/index.ts +0 -1
- package/src/lib/index.ts +0 -8
- package/src/lib/interfaces/button.interface.ts +0 -65
- package/src/lib/interfaces/card.interface.ts +0 -11
- package/src/lib/interfaces/carousel-item.interface.ts +0 -12
- package/src/lib/interfaces/carousel.interface.ts +0 -41
- package/src/lib/interfaces/checkbox.interface.ts +0 -39
- package/src/lib/interfaces/chip.interface.ts +0 -97
- package/src/lib/interfaces/chips.interface.ts +0 -37
- package/src/lib/interfaces/date-picker.interface.ts +0 -79
- package/src/lib/interfaces/divider.interface.ts +0 -7
- package/src/lib/interfaces/fab-menu.interface.ts +0 -12
- package/src/lib/interfaces/fab.interface.ts +0 -27
- package/src/lib/interfaces/icon-button.interface.ts +0 -38
- package/src/lib/interfaces/index.ts +0 -26
- package/src/lib/interfaces/menu-group.interface.ts +0 -13
- package/src/lib/interfaces/menu-item.interface.ts +0 -29
- package/src/lib/interfaces/menu.interface.ts +0 -19
- package/src/lib/interfaces/navigation-rail-item.interface.ts +0 -39
- package/src/lib/interfaces/navigation-rail.interface.ts +0 -39
- package/src/lib/interfaces/progress-indicator.interface.ts +0 -41
- package/src/lib/interfaces/side-sheet.interface.tsx +0 -28
- package/src/lib/interfaces/slider.interface.ts +0 -27
- package/src/lib/interfaces/snackbar.interface.ts +0 -13
- package/src/lib/interfaces/switch.interface.ts +0 -14
- package/src/lib/interfaces/tab-group.interface.ts +0 -13
- package/src/lib/interfaces/tab-panels.interface.ts +0 -21
- package/src/lib/interfaces/tab.interface.ts +0 -31
- package/src/lib/interfaces/tabs.interface.ts +0 -22
- package/src/lib/interfaces/text-field.interface.ts +0 -61
- package/src/lib/interfaces/tooltip.interface.ts +0 -61
- package/src/lib/styles/button.style.ts +0 -136
- package/src/lib/styles/card.style.ts +0 -29
- package/src/lib/styles/carousel-item.style.ts +0 -24
- package/src/lib/styles/carousel.style.ts +0 -22
- package/src/lib/styles/checkbox.style.ts +0 -64
- package/src/lib/styles/chip.style.ts +0 -62
- package/src/lib/styles/chips.style.ts +0 -20
- package/src/lib/styles/date-picker.style.ts +0 -43
- package/src/lib/styles/divider.style.ts +0 -31
- package/src/lib/styles/fab-menu.style.ts +0 -29
- package/src/lib/styles/fab.style.ts +0 -49
- package/src/lib/styles/icon-button.style.ts +0 -168
- package/src/lib/styles/index.ts +0 -25
- package/src/lib/styles/menu-group.style.ts +0 -34
- package/src/lib/styles/menu-headline.style.ts +0 -20
- package/src/lib/styles/menu-item.style.ts +0 -45
- package/src/lib/styles/menu.style.ts +0 -32
- package/src/lib/styles/navigation-rail-item.style.ts +0 -56
- package/src/lib/styles/navigation-rail.style.ts +0 -36
- package/src/lib/styles/progress-indicator.style.ts +0 -72
- package/src/lib/styles/side-sheet.style.ts +0 -45
- package/src/lib/styles/slider.style.ts +0 -41
- package/src/lib/styles/snackbar.style.ts +0 -26
- package/src/lib/styles/switch.style.ts +0 -67
- package/src/lib/styles/tab-panels.style.ts +0 -35
- package/src/lib/styles/tab.style.ts +0 -78
- package/src/lib/styles/tabs.style.ts +0 -22
- package/src/lib/styles/text-field.style.ts +0 -115
- package/src/lib/styles/tooltip.style.ts +0 -48
- package/src/lib/utils/component-helper.ts +0 -134
- package/src/lib/utils/component.ts +0 -34
- package/src/lib/utils/index.ts +0 -7
- package/src/lib/utils/string.ts +0 -9
- package/src/lib/utils/styles/classnames.ts +0 -49
- package/src/lib/utils/styles/get-classname.ts +0 -96
- package/src/lib/utils/styles/index.ts +0 -4
- package/src/lib/utils/styles/use-classnames.ts +0 -25
- package/src/stories/action/button.stories.tsx +0 -86
- package/src/stories/action/fab.stories.tsx +0 -54
- package/src/stories/action/icon-button.stories.tsx +0 -134
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +0 -5
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +0 -15
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +0 -3
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +0 -12
- package/src/stories/assets/youtube.svg +0 -4
- package/src/stories/communication/ProgressIndicator.stories.tsx +0 -57
- package/src/stories/communication/SnackBar.stories.tsx +0 -32
- package/src/stories/communication/tool-tip.stories.tsx +0 -133
- package/src/stories/containment/card.stories.tsx +0 -42
- package/src/stories/containment/carousel.stories.tsx +0 -65
- package/src/stories/containment/divider.stories.tsx +0 -35
- package/src/stories/containment/slide-sheet.stories.tsx +0 -45
- package/src/stories/effect/smooth-scroll.stories.tsx +0 -54
- package/src/stories/navigation/navigation-rail/navigation-rail-item.stories.tsx +0 -65
- package/src/stories/navigation/navigation-rail/navigation-rail.stories.tsx +0 -122
- package/src/stories/navigation/tabs/tab.stories.tsx +0 -57
- package/src/stories/navigation/tabs/tabs.stories.tsx +0 -102
- package/src/stories/selection/slider.stories.tsx +0 -85
- package/src/stories/selection/switch.stories.tsx +0 -46
- package/src/stories/text-inputs/text-field.stories.tsx +0 -135
- package/src/tests/Button.spec.tsx +0 -67
- package/src/tests/useClassNames.spec.tsx +0 -82
- package/src/udixio.css +0 -120
- package/theme.config.ts +0 -7
- package/tsconfig.json +0 -16
- package/tsconfig.lib.json +0 -51
- package/tsconfig.spec.json +0 -37
- package/tsconfig.storybook.json +0 -38
- package/vite.config.ts +0 -82
- /package/dist/{scrollDriven-DWAu7CR0.cjs → scrollDriven.js} +0 -0
|
@@ -1,437 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
-
import { animate } from 'motion/react';
|
|
3
|
-
import { CarouselInterface, CarouselItemInterface } from '../interfaces';
|
|
4
|
-
|
|
5
|
-
import { useCarouselStyle } from '../styles';
|
|
6
|
-
import { CustomScroll } from '../effects';
|
|
7
|
-
import { ReactProps } from '../utils';
|
|
8
|
-
import { CarouselItem, normalize } from './CarouselItem';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Carousels show a collection of items that can be scrolled on and off the screen
|
|
12
|
-
*
|
|
13
|
-
* @status beta
|
|
14
|
-
* @category Layout
|
|
15
|
-
* @devx
|
|
16
|
-
* - Only `CarouselItem` children are rendered; other children are ignored.
|
|
17
|
-
* - Use `index` for controlled positioning; otherwise relies on internal scroll state.
|
|
18
|
-
* @limitations
|
|
19
|
-
* - Responsive behavior on mobile is not supported.
|
|
20
|
-
* - Only the default (hero) variant is supported.
|
|
21
|
-
* - No keyboard navigation or focus management.
|
|
22
|
-
*/
|
|
23
|
-
export const Carousel = ({
|
|
24
|
-
variant = 'hero',
|
|
25
|
-
className,
|
|
26
|
-
children,
|
|
27
|
-
ref: optionalRef,
|
|
28
|
-
marginPourcent = 0,
|
|
29
|
-
inputRange = [0.21, 0.65],
|
|
30
|
-
outputRange = [42, 300],
|
|
31
|
-
gap = 8,
|
|
32
|
-
onChange,
|
|
33
|
-
onMetricsChange,
|
|
34
|
-
index,
|
|
35
|
-
scrollSensitivity = 1.25,
|
|
36
|
-
...restProps
|
|
37
|
-
}: ReactProps<CarouselInterface>) => {
|
|
38
|
-
const defaultRef = useRef<HTMLDivElement>(null);
|
|
39
|
-
const ref = optionalRef || defaultRef;
|
|
40
|
-
|
|
41
|
-
const styles = useCarouselStyle({
|
|
42
|
-
index,
|
|
43
|
-
className,
|
|
44
|
-
children,
|
|
45
|
-
variant,
|
|
46
|
-
inputRange,
|
|
47
|
-
outputRange,
|
|
48
|
-
marginPourcent,
|
|
49
|
-
onChange,
|
|
50
|
-
gap,
|
|
51
|
-
scrollSensitivity,
|
|
52
|
-
onMetricsChange,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const items = React.Children.toArray(children).filter(
|
|
56
|
-
(child) => React.isValidElement(child) && child.type === CarouselItem,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const trackRef = useRef<HTMLDivElement>(null);
|
|
60
|
-
|
|
61
|
-
// OPTIMIZATION: We no longer store width and translate in React state to avoid laggy 60fps re-renders.
|
|
62
|
-
// We use refs instead.
|
|
63
|
-
const getScrollState = useRef({
|
|
64
|
-
scrollProgress: 0,
|
|
65
|
-
scrollTotal: 0,
|
|
66
|
-
scrollVisible: 0,
|
|
67
|
-
scroll: 0,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Smoothed scroll progress using framer-motion animate()
|
|
71
|
-
const smoothedProgressRef = useRef(0);
|
|
72
|
-
const scrollAnimationRef = useRef<ReturnType<typeof animate> | null>(null);
|
|
73
|
-
|
|
74
|
-
const itemRefs = useRef<React.RefObject<HTMLDivElement | null>[]>([]).current;
|
|
75
|
-
const [selectedItem, setSelectedItem] = useState(0);
|
|
76
|
-
|
|
77
|
-
if (itemRefs.length !== items.length) {
|
|
78
|
-
itemRefs.length = 0; // reset
|
|
79
|
-
items.forEach((_, i) => {
|
|
80
|
-
itemRefs[i] = React.createRef<HTMLDivElement>();
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Pure mathematical logic - kept from original
|
|
85
|
-
const updateLayoutFromCalculations = useCallback(() => {
|
|
86
|
-
if (!trackRef.current || !ref.current) return;
|
|
87
|
-
const currentScrollProgress = smoothedProgressRef.current;
|
|
88
|
-
|
|
89
|
-
// Need dimensions
|
|
90
|
-
const scrollVisible = getScrollState.current.scrollVisible || (ref.current as any).clientWidth || 0;
|
|
91
|
-
|
|
92
|
-
function assignRelativeIndexes(values: number[], progressScroll: number) {
|
|
93
|
-
return values.map((value, idx) => {
|
|
94
|
-
const relativeIndex = (value - progressScroll) / Math.abs(values[1] - values[0]);
|
|
95
|
-
return { itemScrollXCenter: value, relativeIndex, index: idx, width: 0 };
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const itemsScrollXCenter = items.map((_, idx) => {
|
|
100
|
-
// Calculate original center normalized
|
|
101
|
-
const itemScrollXCenter = idx / Math.max(1, items.length - 1);
|
|
102
|
-
return normalize(itemScrollXCenter, [0, 1], [0, 1]);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const itemValues = assignRelativeIndexes(
|
|
106
|
-
itemsScrollXCenter,
|
|
107
|
-
currentScrollProgress,
|
|
108
|
-
).sort((a, b) => a.index - b.index);
|
|
109
|
-
|
|
110
|
-
let widthLeft = scrollVisible + gap + outputRange[0] + gap;
|
|
111
|
-
|
|
112
|
-
let localSelected = selectedItem;
|
|
113
|
-
const visibleItemValues = itemValues
|
|
114
|
-
.sort((a, b) => Math.abs(a.relativeIndex) - Math.abs(b.relativeIndex))
|
|
115
|
-
.map((item, idx) => {
|
|
116
|
-
if (widthLeft <= 0) return undefined;
|
|
117
|
-
if (idx === 0) localSelected = item.index;
|
|
118
|
-
|
|
119
|
-
item.width = normalize(
|
|
120
|
-
widthLeft - gap,
|
|
121
|
-
[outputRange[0], outputRange[1]],
|
|
122
|
-
[outputRange[0], outputRange[1]],
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
widthLeft -= item.width + gap;
|
|
126
|
-
|
|
127
|
-
if (widthLeft !== 0 && widthLeft < (outputRange[0] + gap) * 2) {
|
|
128
|
-
const newWidth = item.width - ((outputRange[0] + gap) * 2 - widthLeft);
|
|
129
|
-
widthLeft += item.width;
|
|
130
|
-
item.width = newWidth;
|
|
131
|
-
widthLeft -= item.width;
|
|
132
|
-
} else if (widthLeft === 0 && item.width >= outputRange[0] * 2 + gap) {
|
|
133
|
-
const newWidth = item.width - (outputRange[0] + gap - widthLeft);
|
|
134
|
-
widthLeft += item.width;
|
|
135
|
-
item.width = newWidth;
|
|
136
|
-
widthLeft -= item.width;
|
|
137
|
-
}
|
|
138
|
-
return item;
|
|
139
|
-
})
|
|
140
|
-
.filter(Boolean) as { itemScrollXCenter: number; relativeIndex: number; index: number; width: number; }[];
|
|
141
|
-
|
|
142
|
-
const reverseItemsVisible = [...visibleItemValues].reverse();
|
|
143
|
-
const itemsVisibleByIndex = [...visibleItemValues].sort((a, b) => Math.abs(a.index) - Math.abs(b.index));
|
|
144
|
-
|
|
145
|
-
reverseItemsVisible.forEach((item, idx) => {
|
|
146
|
-
const nextItem = reverseItemsVisible[idx + 1];
|
|
147
|
-
if (!nextItem) return;
|
|
148
|
-
|
|
149
|
-
const test = 1 - (Math.abs(item.relativeIndex) - Math.abs(nextItem.relativeIndex));
|
|
150
|
-
const newWidth = normalize(test, [0, 2], [item.width + widthLeft, nextItem.width]);
|
|
151
|
-
|
|
152
|
-
widthLeft += item.width;
|
|
153
|
-
item.width = newWidth;
|
|
154
|
-
widthLeft -= item.width;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const percentMax = visibleItemValues.length / 2;
|
|
158
|
-
const percent = normalize(
|
|
159
|
-
Math.abs(itemsVisibleByIndex[0].relativeIndex),
|
|
160
|
-
[itemsVisibleByIndex[0].index === 0 ? 0 : percentMax - 1, percentMax],
|
|
161
|
-
[0, 1],
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
const translate = normalize(percent, [0, 1], [0, 1]) * -(outputRange[0] + gap);
|
|
165
|
-
|
|
166
|
-
// ===================================
|
|
167
|
-
// DOM INJECTION OPTIMIZATION
|
|
168
|
-
// ===================================
|
|
169
|
-
|
|
170
|
-
// Apply width to each visible item using DOM instead of setItemWidths React state
|
|
171
|
-
// First, fallback everything to outputRange[0] (or hide them)
|
|
172
|
-
itemRefs.forEach((refItem, i) => {
|
|
173
|
-
if (refItem.current) {
|
|
174
|
-
const match = visibleItemValues.find(v => v.index === i);
|
|
175
|
-
if (match) {
|
|
176
|
-
refItem.current.style.setProperty('--carousel-item-width', `${match.width}px`);
|
|
177
|
-
refItem.current.style.display = 'block';
|
|
178
|
-
} else {
|
|
179
|
-
refItem.current.style.setProperty('--carousel-item-width', `${outputRange[0]}px`);
|
|
180
|
-
refItem.current.style.display = 'none';
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Apply track translate directly via DOM
|
|
186
|
-
trackRef.current.style.transform = `translateX(${translate}px)`;
|
|
187
|
-
|
|
188
|
-
if (localSelected !== selectedItem) {
|
|
189
|
-
setSelectedItem(localSelected);
|
|
190
|
-
}
|
|
191
|
-
}, [items.length, outputRange, gap, selectedItem]);
|
|
192
|
-
|
|
193
|
-
useLayoutEffect(() => {
|
|
194
|
-
updateLayoutFromCalculations();
|
|
195
|
-
}, [updateLayoutFromCalculations, items.length]);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
if (onChange) onChange(selectedItem);
|
|
200
|
-
}, [selectedItem, onChange]);
|
|
201
|
-
|
|
202
|
-
// accessibility and interaction states
|
|
203
|
-
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
204
|
-
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
setFocusedIndex(selectedItem);
|
|
207
|
-
}, [selectedItem]);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const centerOnIndex = (idx: number, opts: { animate?: boolean } = {}) => {
|
|
211
|
-
if (!items.length) return 0;
|
|
212
|
-
const itemRef = itemRefs[idx];
|
|
213
|
-
if (!itemRef || !itemRef.current || !trackRef.current) return 0;
|
|
214
|
-
|
|
215
|
-
const itemScrollXCenter = normalize(
|
|
216
|
-
idx / Math.max(1, items.length - 1),
|
|
217
|
-
[0, 1],
|
|
218
|
-
[0, 1],
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
setFocusedIndex(idx);
|
|
222
|
-
|
|
223
|
-
const track = trackRef.current as HTMLElement;
|
|
224
|
-
track.dispatchEvent(
|
|
225
|
-
new CustomEvent('udx:customScroll:set', {
|
|
226
|
-
bubbles: true,
|
|
227
|
-
detail: {
|
|
228
|
-
progress: itemScrollXCenter,
|
|
229
|
-
orientation: 'horizontal',
|
|
230
|
-
animate: opts.animate !== false,
|
|
231
|
-
},
|
|
232
|
-
}),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
return itemScrollXCenter;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
useEffect(() => {
|
|
239
|
-
if (typeof index === 'number' && items.length > 0 && index !== selectedItem) {
|
|
240
|
-
centerOnIndex(index);
|
|
241
|
-
}
|
|
242
|
-
}, [index, items.length]);
|
|
243
|
-
|
|
244
|
-
const handleScroll = (args: {
|
|
245
|
-
scrollProgress: number;
|
|
246
|
-
scrollTotal: number;
|
|
247
|
-
scrollVisible: number;
|
|
248
|
-
scroll: number;
|
|
249
|
-
}) => {
|
|
250
|
-
getScrollState.current = args;
|
|
251
|
-
|
|
252
|
-
if (args.scrollTotal > 0) {
|
|
253
|
-
scrollAnimationRef.current?.stop();
|
|
254
|
-
const from = smoothedProgressRef.current ?? 0;
|
|
255
|
-
const to = args.scrollProgress ?? 0;
|
|
256
|
-
|
|
257
|
-
scrollAnimationRef.current = animate(from, to, {
|
|
258
|
-
type: 'spring',
|
|
259
|
-
stiffness: 260,
|
|
260
|
-
damping: 32,
|
|
261
|
-
mass: 0.6,
|
|
262
|
-
restDelta: 0.0005,
|
|
263
|
-
onUpdate: (v) => {
|
|
264
|
-
smoothedProgressRef.current = v;
|
|
265
|
-
requestAnimationFrame(() => {
|
|
266
|
-
updateLayoutFromCalculations(); // Apply DOM updates synchronously to animation
|
|
267
|
-
});
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
// Keep latest onMetricsChange in a ref to avoid effect dependency loops
|
|
274
|
-
const onMetricsChangeRef = useRef(onMetricsChange);
|
|
275
|
-
useEffect(() => {
|
|
276
|
-
onMetricsChangeRef.current = onMetricsChange;
|
|
277
|
-
}, [onMetricsChange]);
|
|
278
|
-
|
|
279
|
-
const lastMetricsRef = useRef<any>(null);
|
|
280
|
-
|
|
281
|
-
useEffect(() => {
|
|
282
|
-
const cb = onMetricsChangeRef.current;
|
|
283
|
-
if (!cb || !ref?.current || items.length <= 0) return;
|
|
284
|
-
const total = items.length;
|
|
285
|
-
const viewportWidth = (ref.current as any).clientWidth || 0;
|
|
286
|
-
const itemMaxWidth = outputRange[1];
|
|
287
|
-
const sProgress = smoothedProgressRef.current;
|
|
288
|
-
const visibleApprox = (viewportWidth + gap) / (itemMaxWidth + gap);
|
|
289
|
-
const visibleFull = Math.max(1, Math.floor(visibleApprox));
|
|
290
|
-
const stepHalf = Math.max(1, Math.round(visibleFull * (2 / 3)));
|
|
291
|
-
const selectedIndexSafe = Math.min(Math.max(0, selectedItem), Math.max(0, total - 1));
|
|
292
|
-
|
|
293
|
-
const metrics = {
|
|
294
|
-
total,
|
|
295
|
-
selectedIndex: selectedIndexSafe,
|
|
296
|
-
visibleApprox,
|
|
297
|
-
visibleFull,
|
|
298
|
-
stepHalf,
|
|
299
|
-
canPrev: selectedIndexSafe > 0,
|
|
300
|
-
canNext: selectedIndexSafe < total - 1,
|
|
301
|
-
scrollProgress: sProgress,
|
|
302
|
-
viewportWidth,
|
|
303
|
-
itemMaxWidth,
|
|
304
|
-
gap,
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const last = lastMetricsRef.current;
|
|
308
|
-
let changed = !last;
|
|
309
|
-
if (!changed) {
|
|
310
|
-
for (const k in metrics) {
|
|
311
|
-
if ((metrics as any)[k] !== last[k]) {
|
|
312
|
-
changed = true;
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (changed) {
|
|
319
|
-
lastMetricsRef.current = metrics;
|
|
320
|
-
cb(metrics);
|
|
321
|
-
}
|
|
322
|
-
}, [ref, items.length, selectedItem, gap, outputRange]);
|
|
323
|
-
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
return () => {
|
|
326
|
-
scrollAnimationRef.current?.stop();
|
|
327
|
-
};
|
|
328
|
-
}, []);
|
|
329
|
-
|
|
330
|
-
const [scrollSize, setScrollSize] = useState(0);
|
|
331
|
-
useLayoutEffect(() => {
|
|
332
|
-
let maxWidth = outputRange[1];
|
|
333
|
-
const scrollState = getScrollState.current;
|
|
334
|
-
if (scrollState && maxWidth > scrollState.scrollVisible && scrollState.scrollVisible > 0) {
|
|
335
|
-
maxWidth = scrollState.scrollVisible;
|
|
336
|
-
}
|
|
337
|
-
const result = ((maxWidth + gap) * items.length) / scrollSensitivity;
|
|
338
|
-
setScrollSize(result || 400); // Fail-safe
|
|
339
|
-
}, [ref, items.length, gap, outputRange, scrollSensitivity]);
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
343
|
-
if (!items.length) return;
|
|
344
|
-
const idx = focusedIndex ?? selectedItem;
|
|
345
|
-
switch (e.key) {
|
|
346
|
-
case 'ArrowLeft':
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
centerOnIndex(Math.max(0, idx - 1));
|
|
349
|
-
break;
|
|
350
|
-
case 'ArrowRight':
|
|
351
|
-
e.preventDefault();
|
|
352
|
-
centerOnIndex(Math.min(items.length - 1, idx + 1));
|
|
353
|
-
break;
|
|
354
|
-
case 'Home':
|
|
355
|
-
e.preventDefault();
|
|
356
|
-
centerOnIndex(0);
|
|
357
|
-
break;
|
|
358
|
-
case 'End':
|
|
359
|
-
e.preventDefault();
|
|
360
|
-
centerOnIndex(items.length - 1);
|
|
361
|
-
break;
|
|
362
|
-
case 'Enter':
|
|
363
|
-
case ' ':
|
|
364
|
-
e.preventDefault();
|
|
365
|
-
centerOnIndex(idx);
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
useEffect(() => {
|
|
371
|
-
const root = ref.current as any;
|
|
372
|
-
if (!root) return;
|
|
373
|
-
const handler = (ev: Event) => {
|
|
374
|
-
const detail = (ev as CustomEvent).detail;
|
|
375
|
-
if (detail && typeof detail.index === 'number') {
|
|
376
|
-
centerOnIndex(detail.index);
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
root.addEventListener('udx:carousel:centerIndex', handler);
|
|
380
|
-
return () => {
|
|
381
|
-
root.removeEventListener('udx:carousel:centerIndex', handler);
|
|
382
|
-
};
|
|
383
|
-
}, [ref, items.length]);
|
|
384
|
-
|
|
385
|
-
const renderItems = items.map((child, idx) => {
|
|
386
|
-
const existingOnClick = (child as any).props?.onClick;
|
|
387
|
-
const handleClick = (e: any) => {
|
|
388
|
-
existingOnClick?.(e);
|
|
389
|
-
// centerOnIndex(idx);
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
return React.cloneElement(
|
|
393
|
-
child as React.ReactElement<ReactProps<CarouselItemInterface>>,
|
|
394
|
-
{
|
|
395
|
-
outputRange,
|
|
396
|
-
ref: itemRefs[idx],
|
|
397
|
-
key: idx,
|
|
398
|
-
index: idx,
|
|
399
|
-
role: 'option',
|
|
400
|
-
'aria-selected': selectedItem === idx,
|
|
401
|
-
tabIndex: selectedItem === idx ? 0 : -1,
|
|
402
|
-
onClick: handleClick,
|
|
403
|
-
onFocus: () => setFocusedIndex(idx),
|
|
404
|
-
// NOTE: We REMOVED the 'width' prop from here!
|
|
405
|
-
} as any,
|
|
406
|
-
);
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
return (
|
|
410
|
-
<div
|
|
411
|
-
className={styles.carousel}
|
|
412
|
-
ref={ref}
|
|
413
|
-
role="listbox"
|
|
414
|
-
aria-orientation="horizontal"
|
|
415
|
-
onKeyDown={handleKeyDown}
|
|
416
|
-
{...restProps}
|
|
417
|
-
>
|
|
418
|
-
<CustomScroll
|
|
419
|
-
draggable
|
|
420
|
-
orientation={'horizontal'}
|
|
421
|
-
onScroll={handleScroll}
|
|
422
|
-
scrollSize={scrollSize}
|
|
423
|
-
>
|
|
424
|
-
<div
|
|
425
|
-
className={styles.track}
|
|
426
|
-
ref={trackRef}
|
|
427
|
-
style={{
|
|
428
|
-
gap: `${gap}px`,
|
|
429
|
-
willChange: 'transform',
|
|
430
|
-
}}
|
|
431
|
-
>
|
|
432
|
-
{renderItems}
|
|
433
|
-
</div>
|
|
434
|
-
</CustomScroll>
|
|
435
|
-
</div>
|
|
436
|
-
);
|
|
437
|
-
};
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import React, { useRef } from 'react';
|
|
2
|
-
import { CarouselItemInterface } from '../interfaces';
|
|
3
|
-
import { useCarouselItemStyle } from '../styles';
|
|
4
|
-
import { MotionProps } from '../utils';
|
|
5
|
-
|
|
6
|
-
export const normalize = (
|
|
7
|
-
value: number,
|
|
8
|
-
inputRange: [number, number],
|
|
9
|
-
outputRange: [number, number] = [0, 1],
|
|
10
|
-
): number => {
|
|
11
|
-
const [inputMin, inputMax] = inputRange;
|
|
12
|
-
const [outputMin, outputMax] = outputRange;
|
|
13
|
-
|
|
14
|
-
const clampedValue = Math.max(inputMin, Math.min(value, inputMax));
|
|
15
|
-
|
|
16
|
-
const normalizedValue = (clampedValue - inputMin) / (inputMax - inputMin);
|
|
17
|
-
|
|
18
|
-
return outputMin + normalizedValue * (outputMax - outputMin);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @status beta
|
|
23
|
-
* @parent Carousel
|
|
24
|
-
* @devx
|
|
25
|
-
* - Intended for use inside `Carousel`; width and outputRange drive sizing.
|
|
26
|
-
* @limitations
|
|
27
|
-
* - Requires `outputRange` for min/max sizing; missing values can break layout.
|
|
28
|
-
*/
|
|
29
|
-
export const CarouselItem = ({
|
|
30
|
-
className,
|
|
31
|
-
children,
|
|
32
|
-
width,
|
|
33
|
-
index = 0,
|
|
34
|
-
outputRange,
|
|
35
|
-
ref: optionalRef,
|
|
36
|
-
...restProps
|
|
37
|
-
}: MotionProps<CarouselItemInterface>) => {
|
|
38
|
-
const defaultRef = useRef(null);
|
|
39
|
-
const ref: React.RefObject<null | HTMLDivElement> = optionalRef || defaultRef;
|
|
40
|
-
|
|
41
|
-
const styles = useCarouselItemStyle({
|
|
42
|
-
className,
|
|
43
|
-
index,
|
|
44
|
-
children,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div
|
|
49
|
-
ref={ref}
|
|
50
|
-
style={{
|
|
51
|
-
width: 'var(--carousel-item-width, 100%)',
|
|
52
|
-
maxWidth: outputRange ? outputRange[1] + 'px' : undefined,
|
|
53
|
-
minWidth: outputRange ? outputRange[0] + 'px' : undefined,
|
|
54
|
-
}}
|
|
55
|
-
className={styles.carouselItem}
|
|
56
|
-
{...restProps}
|
|
57
|
-
>
|
|
58
|
-
{children}
|
|
59
|
-
</div>
|
|
60
|
-
);
|
|
61
|
-
};
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useId, useState } from 'react';
|
|
2
|
-
import { AnimatePresence, motion } from 'motion/react';
|
|
3
|
-
import { useCheckboxStyle } from '../styles/checkbox.style';
|
|
4
|
-
import { classNames } from '../utils';
|
|
5
|
-
import { ReactProps } from '../utils/component';
|
|
6
|
-
import { CheckboxInterface } from '../interfaces/checkbox.interface';
|
|
7
|
-
import { Icon } from '../icon';
|
|
8
|
-
import { faCheck, faMinus } from '@fortawesome/free-solid-svg-icons';
|
|
9
|
-
import { State } from '../effects';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Checkboxes allow the user to select one or more items from a set.
|
|
13
|
-
* @status beta
|
|
14
|
-
* @category Selection
|
|
15
|
-
* @devx
|
|
16
|
-
* - Supports controlled (`checked`) and uncontrolled (`defaultChecked`) modes.
|
|
17
|
-
* - Handles `indeterminate` state strictly as visual (updates UI, but underlying input is checked/unchecked based on logic).
|
|
18
|
-
* @a11y
|
|
19
|
-
* - Uses native input for accessibility.
|
|
20
|
-
* - Supports standard keyboard interaction.
|
|
21
|
-
*/
|
|
22
|
-
export const Checkbox = ({
|
|
23
|
-
checked: checkedProp,
|
|
24
|
-
defaultChecked,
|
|
25
|
-
indeterminate = false,
|
|
26
|
-
disabled = false,
|
|
27
|
-
error = false,
|
|
28
|
-
onChange,
|
|
29
|
-
id: idProp,
|
|
30
|
-
name,
|
|
31
|
-
value,
|
|
32
|
-
style,
|
|
33
|
-
className,
|
|
34
|
-
...restProps
|
|
35
|
-
}: ReactProps<CheckboxInterface>) => {
|
|
36
|
-
const generatedId = useId();
|
|
37
|
-
const id = idProp || generatedId;
|
|
38
|
-
|
|
39
|
-
const isControlled = checkedProp !== undefined;
|
|
40
|
-
const [internalChecked, setInternalChecked] = useState(
|
|
41
|
-
defaultChecked ?? false,
|
|
42
|
-
);
|
|
43
|
-
const isChecked = isControlled ? checkedProp : internalChecked;
|
|
44
|
-
|
|
45
|
-
const [isFocused, setIsFocused] = useState(false);
|
|
46
|
-
|
|
47
|
-
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
48
|
-
|
|
49
|
-
// Sync indeterminate state to input element for a11y
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (inputRef.current) {
|
|
52
|
-
inputRef.current.indeterminate = indeterminate;
|
|
53
|
-
}
|
|
54
|
-
}, [indeterminate]);
|
|
55
|
-
|
|
56
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
-
if (disabled) return;
|
|
58
|
-
|
|
59
|
-
if (!isControlled) {
|
|
60
|
-
setInternalChecked(e.target.checked);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (onChange) {
|
|
64
|
-
onChange(e);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const styles = useCheckboxStyle({
|
|
69
|
-
isChecked: !!isChecked,
|
|
70
|
-
isIndeterminate: indeterminate,
|
|
71
|
-
isDisabled: disabled,
|
|
72
|
-
isError: error,
|
|
73
|
-
isFocused,
|
|
74
|
-
isHovered: false, // Not used in style logic but requested by strict type if defined in interface? style config keys are flexible usually.
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div
|
|
79
|
-
className={classNames(styles.checkbox, className, 'group/checkbox')}
|
|
80
|
-
style={style}
|
|
81
|
-
>
|
|
82
|
-
<State
|
|
83
|
-
stateClassName={styles.stateLayer}
|
|
84
|
-
colorName={isChecked || indeterminate ? 'primary' : 'on-surface'}
|
|
85
|
-
>
|
|
86
|
-
<input
|
|
87
|
-
ref={inputRef}
|
|
88
|
-
type="checkbox"
|
|
89
|
-
id={id}
|
|
90
|
-
name={name}
|
|
91
|
-
value={value}
|
|
92
|
-
checked={isChecked}
|
|
93
|
-
disabled={disabled}
|
|
94
|
-
onChange={handleChange}
|
|
95
|
-
onFocus={() => setIsFocused(true)}
|
|
96
|
-
onBlur={() => setIsFocused(false)}
|
|
97
|
-
className={styles.input}
|
|
98
|
-
{...(restProps as any)}
|
|
99
|
-
/>
|
|
100
|
-
<div className={styles.box}></div>
|
|
101
|
-
<AnimatePresence>
|
|
102
|
-
{(isChecked || indeterminate) && (
|
|
103
|
-
<motion.div
|
|
104
|
-
initial={{ opacity: 0, scale: 0.5 }}
|
|
105
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
106
|
-
exit={{ opacity: 0, scale: 0.5 }}
|
|
107
|
-
transition={{ duration: 0.15 }}
|
|
108
|
-
className={styles.icon}
|
|
109
|
-
>
|
|
110
|
-
<Icon
|
|
111
|
-
icon={indeterminate ? faMinus : faCheck}
|
|
112
|
-
className="size-3.5" // ~14px icon
|
|
113
|
-
/>
|
|
114
|
-
</motion.div>
|
|
115
|
-
)}
|
|
116
|
-
</AnimatePresence>
|
|
117
|
-
</State>
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
};
|