etudes 5.1.0 → 5.3.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 (58) hide show
  1. package/components/Accordion.js +285 -0
  2. package/components/BurgerButton.js +127 -0
  3. package/components/Carousel.js +194 -0
  4. package/components/Collection.js +146 -0
  5. package/components/Counter.js +71 -0
  6. package/components/CoverImage.js +53 -0
  7. package/components/CoverVideo.js +54 -0
  8. package/components/DebugConsole.js +76 -0
  9. package/components/Dial.js +102 -0
  10. package/components/Dropdown.js +188 -0
  11. package/components/FlatSVG.js +44 -0
  12. package/components/Image.js +47 -0
  13. package/components/MasonryGrid.js +258 -0
  14. package/components/Panorama.js +96 -0
  15. package/components/PanoramaSlider.js +144 -0
  16. package/components/RangeSlider.js +240 -0
  17. package/components/RotatingGallery.js +58 -0
  18. package/components/SelectableButton.js +24 -0
  19. package/components/Slider.js +223 -0
  20. package/components/StepwiseSlider.js +303 -0
  21. package/components/SwipeContainer.js +67 -0
  22. package/components/TextField.js +20 -0
  23. package/components/Video.js +65 -0
  24. package/components/WithTooltip.js +250 -0
  25. package/components/index.js +24 -0
  26. package/hooks/index.js +17 -0
  27. package/hooks/useClickOutsideEffect.js +27 -0
  28. package/hooks/useDragEffect.js +65 -0
  29. package/hooks/useDragValueEffect.js +85 -0
  30. package/hooks/useImageSize.js +44 -0
  31. package/hooks/useInterval.js +23 -0
  32. package/hooks/useLoadImageEffect.js +36 -0
  33. package/hooks/useLoadVideoMetadataEffect.js +32 -0
  34. package/hooks/useMounted.js +11 -0
  35. package/hooks/usePrevious.js +14 -0
  36. package/hooks/useRect.js +24 -0
  37. package/hooks/useResizeEffect.js +28 -0
  38. package/hooks/useScrollPositionEffect.js +38 -0
  39. package/hooks/useSearchParamState.js +54 -0
  40. package/hooks/useSize.js +23 -0
  41. package/hooks/useTimeout.js +21 -0
  42. package/hooks/useVideoSize.js +44 -0
  43. package/hooks/useViewportSize.js +20 -0
  44. package/operators/Conditional.js +13 -0
  45. package/operators/Each.js +10 -0
  46. package/operators/ExtractChild.js +25 -0
  47. package/operators/ExtractChildren.js +17 -0
  48. package/operators/Repeat.js +10 -0
  49. package/operators/index.js +5 -0
  50. package/package.json +27 -42
  51. package/providers/ScrollPositionProvider.js +67 -0
  52. package/providers/index.js +1 -0
  53. package/utils/asClassNameDict.js +3 -0
  54. package/utils/asComponentDict.js +18 -0
  55. package/utils/asStyleDict.js +3 -0
  56. package/utils/cloneStyledElement.js +15 -0
  57. package/utils/index.js +5 -0
  58. package/utils/styles.js +6 -0
@@ -0,0 +1,285 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import isDeepEqual from 'fast-deep-equal/react';
4
+ import { forwardRef, useEffect, useRef } from 'react';
5
+ import { Each } from '../operators/Each.js';
6
+ import { asStyleDict, cloneStyledElement, styles } from '../utils/index.js';
7
+ import { Collection } from './Collection.js';
8
+ import { FlatSVG } from './FlatSVG.js';
9
+ /**
10
+ * A collection of selectable items laid out in sections in an accordion. Items
11
+ * are generated based on the provided `ItemComponent` while each section header
12
+ * is optionally provided by `HeaderComponent` or generated automatically.
13
+ */
14
+ export const Accordion = forwardRef(({ children, style, autoCollapseSections = false, collapseIconSvg, expandedSectionIndices: externalExpandedSectionIndices, expandIconSvg, orientation = 'vertical', sectionPadding = 0, sections, selection: externalSelection, selectionMode = 'single', usesDefaultStyles = false, onActivateAt, onCollapseSectionAt, onDeselectAt, onExpandedSectionsChange, onExpandSectionAt, onHeaderCustomEvent, onItemCustomEvent, onSelectAt, onSelectionChange, HeaderComponent, ItemComponent, ...props }, ref) => {
15
+ const isSectionIndexOutOfRange = (sectionIndex) => {
16
+ if (sectionIndex >= sections.length)
17
+ return true;
18
+ if (sectionIndex < 0)
19
+ return true;
20
+ return false;
21
+ };
22
+ const isItemIndexOutOfRange = (itemIndex, sectionIndex) => {
23
+ if (isSectionIndexOutOfRange(sectionIndex))
24
+ return true;
25
+ const items = sections[sectionIndex].items;
26
+ if (itemIndex >= items.length)
27
+ return true;
28
+ if (itemIndex < 0)
29
+ return true;
30
+ return false;
31
+ };
32
+ const isSelectedAt = (itemIndex, sectionIndex) => (selection[sectionIndex]?.indexOf(itemIndex) ?? -1) >= 0;
33
+ const sanitizeExpandedSectionIndices = (sectionIndices) => sortIndices(sectionIndices).filter(t => !isSectionIndexOutOfRange(t));
34
+ const sanitizeSelection = (selection) => {
35
+ const newValue = {};
36
+ for (const sectionIndex in sections) {
37
+ if (!Object.hasOwn(sections, sectionIndex))
38
+ continue;
39
+ const indices = sortIndices([...selection[sectionIndex] ?? []]);
40
+ newValue[Number(sectionIndex)] = sortIndices(indices).filter(t => !isItemIndexOutOfRange(t, Number(sectionIndex)));
41
+ }
42
+ return newValue;
43
+ };
44
+ const isSectionExpandedAt = (sectionIndex) => expandedSectionIndices.indexOf(sectionIndex) >= 0;
45
+ const toggleSectionAt = (sectionIndex) => {
46
+ let transform;
47
+ if (isSectionExpandedAt(sectionIndex)) {
48
+ transform = val => val.filter(t => t !== sectionIndex);
49
+ }
50
+ else if (autoCollapseSections) {
51
+ transform = val => [sectionIndex];
52
+ }
53
+ else {
54
+ transform = val => [...val.filter(t => t !== sectionIndex), sectionIndex];
55
+ }
56
+ handleExpandedSectionsChange(expandedSectionIndices, transform(expandedSectionIndices));
57
+ };
58
+ const handleSelectAt = (itemIndex, sectionIndex) => {
59
+ if (isSelectedAt(itemIndex, sectionIndex))
60
+ return;
61
+ let transform;
62
+ switch (selectionMode) {
63
+ case 'multiple':
64
+ transform = val => ({
65
+ ...val,
66
+ [sectionIndex]: sortIndices([...(val[sectionIndex] ?? []).filter(t => t !== itemIndex), itemIndex]),
67
+ });
68
+ break;
69
+ case 'single':
70
+ transform = val => ({
71
+ [sectionIndex]: [itemIndex],
72
+ });
73
+ break;
74
+ default:
75
+ return;
76
+ }
77
+ const newValue = transform(selection);
78
+ prevSelectionRef.current = newValue;
79
+ handleSelectionChange(selection, newValue);
80
+ };
81
+ const handleDeselectAt = (itemIndex, sectionIndex) => {
82
+ if (!isSelectedAt(itemIndex, sectionIndex))
83
+ return;
84
+ const transform = (val) => ({
85
+ ...val,
86
+ [sectionIndex]: (val[sectionIndex] ?? []).filter(t => t !== itemIndex),
87
+ });
88
+ const newValue = transform(selection);
89
+ prevSelectionRef.current = newValue;
90
+ handleSelectionChange(selection, newValue);
91
+ };
92
+ const handleExpandedSectionsChange = (oldValue, newValue) => {
93
+ if (isDeepEqual(oldValue, newValue))
94
+ return;
95
+ const collapsed = oldValue?.filter(t => newValue.indexOf(t) === -1) ?? [];
96
+ const expanded = newValue.filter(t => oldValue?.indexOf(t) === -1);
97
+ collapsed.forEach(t => onCollapseSectionAt?.(t));
98
+ expanded.forEach(t => onExpandSectionAt?.(t));
99
+ onExpandedSectionsChange?.(newValue);
100
+ };
101
+ const handleSelectionChange = (oldValue, newValue) => {
102
+ if (isDeepEqual(oldValue, newValue))
103
+ return;
104
+ const numSections = sections.length;
105
+ let allDeselected = [];
106
+ let allSelected = [];
107
+ for (let i = 0; i < numSections; i++) {
108
+ const oldSection = oldValue?.[i] ?? [];
109
+ const newSection = newValue[i] ?? [];
110
+ const deselected = oldSection.filter(t => newSection.indexOf(t) === -1);
111
+ const selected = newSection.filter(t => oldSection?.indexOf(t) === -1);
112
+ allDeselected = [...allDeselected, ...deselected.map(t => [t, i])];
113
+ allSelected = [...allSelected, ...selected.map(t => [t, i])];
114
+ }
115
+ allDeselected.forEach(t => onDeselectAt?.(t[0], t[1]));
116
+ allSelected.forEach(t => onSelectAt?.(t[0], t[1]));
117
+ onSelectionChange?.(newValue);
118
+ };
119
+ const selection = sanitizeSelection(externalSelection ?? {});
120
+ const expandedSectionIndices = sanitizeExpandedSectionIndices(externalExpandedSectionIndices ?? []);
121
+ const fixedStyles = getFixedStyles({ orientation });
122
+ const defaultStyles = usesDefaultStyles ? getDefaultStyles({ orientation }) : undefined;
123
+ const prevSelectionRef = useRef();
124
+ const prevSelection = prevSelectionRef.current;
125
+ useEffect(() => {
126
+ prevSelectionRef.current = selection;
127
+ if (prevSelection === undefined)
128
+ return;
129
+ handleSelectionChange(prevSelection, selection);
130
+ }, [JSON.stringify(selection)]);
131
+ return (_jsx("div", { ...props, ref: ref, "data-component": 'accordion', style: styles(style, fixedStyles.root), children: _jsx(Each, { in: sections, children: (section, sectionIndex) => {
132
+ const { collectionPadding = 0, items, itemLength = 50, itemPadding = 0, isSelectionTogglable, layout = 'list', maxVisible = -1, numSegments = 1 } = section;
133
+ const allVisible = layout === 'list' ? items.length : Math.ceil(items.length / numSegments);
134
+ const numVisible = maxVisible < 0 ? allVisible : Math.min(allVisible, maxVisible);
135
+ const maxLength = itemLength * numVisible + itemPadding * (numVisible - 1);
136
+ const isCollapsed = !isSectionExpandedAt(sectionIndex);
137
+ const expandIconComponent = expandIconSvg ? _jsx(FlatSVG, { style: defaultStyles?.expandIcon, svg: expandIconSvg }) : _jsx(_Fragment, {});
138
+ const collapseIconComponent = collapseIconSvg ? _jsx(FlatSVG, { style: defaultStyles?.collapseIcon, svg: collapseIconSvg }) : expandIconComponent;
139
+ return (_jsxs("div", { style: styles(fixedStyles.section, orientation === 'vertical' ? {
140
+ marginTop: sectionIndex === 0 ? '0px' : `${sectionPadding}px`,
141
+ } : {
142
+ marginLeft: sectionIndex === 0 ? '0px' : `${sectionPadding}px`,
143
+ }), children: [HeaderComponent ? (_jsx(HeaderComponent, { className: clsx({ collapsed: isCollapsed, expanded: !isCollapsed }), "data-child": 'header', index: sectionIndex, isCollapsed: isCollapsed, section: section, style: styles(fixedStyles.header), onClick: () => toggleSectionAt(sectionIndex), onCustomEvent: (name, info) => onHeaderCustomEvent?.(sectionIndex, name, info) })) : (_jsxs("button", { className: clsx({ collapsed: isCollapsed, expanded: !isCollapsed }), "data-child": 'header', style: styles(fixedStyles.header, defaultStyles?.header), onClick: () => toggleSectionAt(sectionIndex), children: [_jsx("span", { dangerouslySetInnerHTML: { __html: section.label }, style: styles(defaultStyles?.headerLabel) }), cloneStyledElement(isCollapsed ? expandIconComponent : collapseIconComponent, {
144
+ style: styles(isCollapsed ? fixedStyles.expandIcon : fixedStyles.collapseIcon),
145
+ })] })), _jsx(Collection, { className: clsx({ collapsed: isCollapsed, expanded: !isCollapsed }), "data-child": 'collection', isSelectionTogglable: isSelectionTogglable, ItemComponent: ItemComponent, itemLength: itemLength, itemPadding: itemPadding, items: items, layout: layout, numSegments: numSegments, orientation: orientation, selection: selection[sectionIndex] ?? [], selectionMode: selectionMode, style: styles(fixedStyles.list, defaultStyles?.collection, orientation === 'vertical' ? {
146
+ width: '100%',
147
+ height: isCollapsed ? '0px' : `${maxLength}px`,
148
+ marginTop: isCollapsed ? '0px' : `${collectionPadding}px`,
149
+ overflowY: maxVisible < 0 || maxVisible >= allVisible ? 'hidden' : 'scroll',
150
+ } : {
151
+ marginLeft: isCollapsed ? '0px' : `${collectionPadding}px`,
152
+ overflowX: maxVisible < 0 || maxVisible >= allVisible ? 'hidden' : 'scroll',
153
+ width: isCollapsed ? '0px' : `${maxLength}px`,
154
+ height: '100%',
155
+ }), onActivateAt: itemIndex => {
156
+ onActivateAt?.(itemIndex, sectionIndex);
157
+ }, onDeselectAt: itemIndex => {
158
+ handleDeselectAt?.(itemIndex, sectionIndex);
159
+ }, onItemCustomEvent: (itemIndex, name, info) => {
160
+ onItemCustomEvent?.(itemIndex, sectionIndex, name, info);
161
+ }, onSelectAt: itemIndex => {
162
+ handleSelectAt?.(itemIndex, sectionIndex);
163
+ } })] }));
164
+ } }) }));
165
+ });
166
+ Object.defineProperty(Accordion, 'displayName', { value: 'Accordion', writable: false });
167
+ function sortIndices(indices) {
168
+ return indices.sort((a, b) => a - b);
169
+ }
170
+ function getFixedStyles({ orientation = 'vertical' }) {
171
+ return asStyleDict({
172
+ root: {
173
+ alignItems: 'center',
174
+ boxSizing: 'border-box',
175
+ display: 'flex',
176
+ flex: '0 0 auto',
177
+ justifyContent: 'flex-start',
178
+ padding: '0',
179
+ ...orientation === 'vertical' ? {
180
+ flexDirection: 'column',
181
+ height: 'auto',
182
+ } : {
183
+ flexDirection: 'row',
184
+ width: 'auto',
185
+ },
186
+ },
187
+ section: {
188
+ alignItems: 'flex-start',
189
+ display: 'flex',
190
+ flex: '0 0 auto',
191
+ justifyContent: 'flex-start',
192
+ margin: '0',
193
+ padding: '0',
194
+ ...orientation === 'vertical' ? {
195
+ flexDirection: 'column',
196
+ width: '100%',
197
+ } : {
198
+ flexDirection: 'row',
199
+ height: '100%',
200
+ },
201
+ },
202
+ list: {},
203
+ header: {
204
+ cursor: 'pointer',
205
+ margin: '0',
206
+ ...orientation === 'vertical' ? {
207
+ width: '100%',
208
+ } : {
209
+ height: '100%',
210
+ },
211
+ },
212
+ expandIcon: {
213
+ margin: '0',
214
+ padding: '0',
215
+ },
216
+ collapseIcon: {
217
+ margin: '0',
218
+ padding: '0',
219
+ },
220
+ });
221
+ }
222
+ function getDefaultStyles({ orientation = 'vertical' }) {
223
+ return asStyleDict({
224
+ collection: {
225
+ transitionDuration: '100ms',
226
+ transitionTimingFunction: 'ease-out',
227
+ ...orientation === 'vertical' ? {
228
+ transitionProperty: 'height, margin',
229
+ } : {
230
+ transitionProperty: 'width, margin',
231
+ },
232
+ },
233
+ header: {
234
+ border: 'none',
235
+ outline: 'none',
236
+ alignItems: 'center',
237
+ background: '#fff',
238
+ boxSizing: 'border-box',
239
+ display: 'flex',
240
+ flexDirection: 'row',
241
+ justifyContent: 'space-between',
242
+ padding: '0 10px',
243
+ transitionDuration: '100ms',
244
+ transitionProperty: 'transform, opacity, background, color',
245
+ transitionTimingFunction: 'ease-out',
246
+ ...orientation === 'vertical' ? {
247
+ height: '50px',
248
+ } : {
249
+ width: '50px',
250
+ },
251
+ },
252
+ headerLabel: {
253
+ color: 'inherit',
254
+ fontFamily: 'inherit',
255
+ fontSize: 'inherit',
256
+ fontWeight: 'inherit',
257
+ letterSpacing: 'inherit',
258
+ lineHeight: 'inherit',
259
+ pointerEvents: 'none',
260
+ transition: 'inherit',
261
+ },
262
+ expandIcon: {
263
+ boxSizing: 'border-box',
264
+ display: 'block',
265
+ fill: '#000',
266
+ height: '15px',
267
+ transformOrigin: 'center',
268
+ transitionDuration: '100ms',
269
+ transitionProperty: 'transform',
270
+ transitionTimingFunction: 'ease-out',
271
+ width: '15px',
272
+ },
273
+ collapseIcon: {
274
+ boxSizing: 'border-box',
275
+ display: 'block',
276
+ fill: '#000',
277
+ height: '15px',
278
+ transformOrigin: 'center',
279
+ transitionDuration: '100ms',
280
+ transitionProperty: 'transform',
281
+ transitionTimingFunction: 'ease-out',
282
+ width: '15px',
283
+ },
284
+ });
285
+ }
@@ -0,0 +1,127 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import clsx from 'clsx';
3
+ import { forwardRef, useEffect, useState } from 'react';
4
+ import { Repeat } from '../operators/Repeat.js';
5
+ import { asClassNameDict, asComponentDict, asStyleDict, cloneStyledElement, styles } from '../utils/index.js';
6
+ /**
7
+ * Three-striped burger button component that transforms into an "X" when
8
+ * selected.
9
+ *
10
+ * @exports BurgerButtonBar Component for each line on the burger button.
11
+ */
12
+ export const BurgerButton = forwardRef(({ children, className, style, height = 20, isActive: externalIsActive = false, isDoubleJointed = false, isLastBarHalfWidth = false, thickness = 2, transitionDuration = 200, usesDefaultStyles = false, width = 20, onActivate, onDeactivate, ...props }, ref) => {
13
+ const [isActive, setIsActive] = useState(externalIsActive);
14
+ useEffect(() => {
15
+ if (isActive === externalIsActive)
16
+ return;
17
+ setIsActive(externalIsActive);
18
+ }, [externalIsActive]);
19
+ useEffect(() => {
20
+ if (isActive) {
21
+ onActivate?.();
22
+ }
23
+ else {
24
+ onDeactivate?.();
25
+ }
26
+ }, [isActive]);
27
+ const components = asComponentDict(children, {
28
+ bar: BurgerButtonBar,
29
+ });
30
+ const fixedClassNames = asClassNameDict({
31
+ root: clsx({
32
+ active: isActive,
33
+ }),
34
+ bar: clsx({
35
+ active: isActive,
36
+ }),
37
+ });
38
+ const fixedStyles = getFixedStyles({ height, width, isDoubleJointed, thickness, isActive, isLastBarHalfWidth });
39
+ const defaultStyles = usesDefaultStyles ? getDefaultStyles() : undefined;
40
+ return (_jsx("button", { ...props, ref: ref, className: clsx(className, fixedClassNames.root), "data-component": 'burger-button', style: styles(style, fixedStyles.root), onClick: () => setIsActive(!isActive), children: _jsx(Repeat, { count: isDoubleJointed ? 2 : 1, children: j => (_jsx("div", { "data-child": 'joint', style: styles(fixedStyles.joint, fixedStyles[`joint${j}`]), children: _jsx(Repeat, { count: 3, children: i => cloneStyledElement(components.bar ?? _jsx(BurgerButtonBar, { style: defaultStyles?.bar }), {
41
+ 'className': clsx(fixedClassNames.bar),
42
+ 'style': styles(fixedStyles.bar, fixedStyles[`bar${j}${i}`]),
43
+ 'data-index': i,
44
+ }) }) })) }) }));
45
+ });
46
+ Object.defineProperty(BurgerButton, 'displayName', { value: 'BurgerButton', writable: false });
47
+ export const BurgerButtonBar = ({ ...props }) => _jsx("span", { ...props, "data-child": 'bar' });
48
+ function getFixedStyles({ height = 0, width = 0, isDoubleJointed = false, thickness = 0, isActive = false, isLastBarHalfWidth = false }) {
49
+ return asStyleDict({
50
+ root: {
51
+ background: 'transparent',
52
+ border: 'none',
53
+ display: 'block',
54
+ height: `${height}px`,
55
+ outline: 'none',
56
+ width: `${width}px`,
57
+ },
58
+ joint: {
59
+ height: '100%',
60
+ position: 'absolute',
61
+ width: isDoubleJointed ? '50%' : '100%',
62
+ },
63
+ joint0: {
64
+ left: '0',
65
+ top: '0',
66
+ },
67
+ joint1: {
68
+ right: '0',
69
+ top: '0',
70
+ },
71
+ bar: {
72
+ height: `${thickness}px`,
73
+ margin: '0',
74
+ padding: '0',
75
+ position: 'absolute',
76
+ width: '100%',
77
+ },
78
+ bar00: {
79
+ left: '0',
80
+ top: '0',
81
+ transform: isActive ? `translate3d(0, ${height * 0.5 - thickness * 0.5}px, 0) rotate(45deg)` : 'translate3d(0, 0, 0) rotate(0deg)',
82
+ transformOrigin: isDoubleJointed ? 'right center' : 'center',
83
+ },
84
+ bar01: {
85
+ left: '0',
86
+ top: `${height * 0.5 - thickness * 0.5}px`,
87
+ transform: isActive ? 'translate3d(0, 0, 0) scale(0)' : 'translate3d(0, 0, 0) scale(1)',
88
+ transformOrigin: isDoubleJointed ? 'right center' : 'center',
89
+ },
90
+ bar02: {
91
+ left: '0',
92
+ top: `${height - thickness}px`,
93
+ transform: isActive ? `translate3d(0, ${thickness * 0.5 - height * 0.5}px, 0) rotate(-45deg)` : 'translate3d(0, 0, 0) rotate(0deg)',
94
+ transformOrigin: isDoubleJointed ? 'right center' : 'center',
95
+ width: isActive || isDoubleJointed ? '100%' : `${isLastBarHalfWidth ? '50%' : '100%'}`,
96
+ },
97
+ bar10: {
98
+ left: '0',
99
+ top: '0',
100
+ transform: isActive ? `translate3d(0, ${height * 0.5 - thickness * 0.5}px, 0) rotate(-45deg)` : 'translate3d(0, 0, 0) rotate(0deg)',
101
+ transformOrigin: 'left center',
102
+ },
103
+ bar11: {
104
+ left: '0',
105
+ top: `${height * 0.5 - thickness * 0.5}px`,
106
+ transform: isActive ? 'translate3d(0, 0, 0) scale(0)' : 'translate3d(0, 0, 0) scale(1)',
107
+ transformOrigin: 'left center',
108
+ },
109
+ bar12: {
110
+ left: '0',
111
+ top: `${height - thickness}px`,
112
+ transform: isActive ? `translate3d(0, ${thickness * 0.5 - height * 0.5}px, 0) rotate(45deg)` : 'translate3d(0, 0, 0) rotate(0deg)',
113
+ transformOrigin: 'left center',
114
+ width: isLastBarHalfWidth && !isActive ? '0' : '100%',
115
+ },
116
+ });
117
+ }
118
+ function getDefaultStyles() {
119
+ return asStyleDict({
120
+ bar: {
121
+ background: '#fff',
122
+ transitionDuration: '100ms',
123
+ transitionProperty: 'width, height, transform, opacity, background',
124
+ transitionTimingFunction: 'ease-out',
125
+ },
126
+ });
127
+ }
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useRef, useState } from 'react';
3
+ import { Point, Rect } from 'spase';
4
+ import { useDragEffect } from '../hooks/useDragEffect.js';
5
+ import { useTimeout } from '../hooks/useTimeout.js';
6
+ import { Each } from '../operators/Each.js';
7
+ import { asStyleDict, styles } from '../utils/index.js';
8
+ export const Carousel = forwardRef(({ style, autoAdvanceInterval = 0, index = 0, isDragEnabled = true, items = [], orientation = 'horizontal', tracksItemExposure = false, onAutoAdvancePause, onAutoAdvanceResume, onIndexChange, ItemComponent, ...props }, ref) => {
9
+ const getItemExposures = () => {
10
+ const viewportElement = viewportRef.current;
11
+ if (!viewportElement)
12
+ return undefined;
13
+ const exposures = [];
14
+ for (let i = 0; i < viewportElement.children.length; i++) {
15
+ exposures.push(getItemExposureAt(i));
16
+ }
17
+ return exposures;
18
+ };
19
+ const getItemExposureAt = (idx) => {
20
+ const viewportElement = viewportRef.current;
21
+ const child = viewportElement?.children[idx];
22
+ if (!child)
23
+ return 0;
24
+ const intersection = Rect.intersecting(child, viewportElement);
25
+ if (!intersection)
26
+ return 0;
27
+ switch (orientation) {
28
+ case 'horizontal':
29
+ return Math.max(0, Math.min(1, Math.round((intersection.width / viewportElement.clientWidth + Number.EPSILON) * 1000) / 1000));
30
+ case 'vertical':
31
+ return Math.max(0, Math.min(1, Math.round((intersection.height / viewportElement.clientHeight + Number.EPSILON) * 1000) / 1000));
32
+ default:
33
+ throw new Error(`Unsupported orientation '${orientation}'`);
34
+ }
35
+ };
36
+ const handleIndexChange = (newValue) => {
37
+ onIndexChange?.(newValue);
38
+ };
39
+ const handlePointerDown = (event) => {
40
+ pointerDownPositionRef.current = new Point([event.clientX, event.clientY]);
41
+ setIsPointerDown(true);
42
+ };
43
+ const handlePointerUp = (event) => {
44
+ pointerUpPositionRef.current = new Point([event.clientX, event.clientY]);
45
+ setIsPointerDown(false);
46
+ };
47
+ const handleClick = (event) => {
48
+ const downPosition = pointerDownPositionRef.current;
49
+ const upPosition = pointerUpPositionRef.current;
50
+ if (!downPosition || !upPosition)
51
+ return;
52
+ const threshold = 5;
53
+ const delta = downPosition.subtract(upPosition);
54
+ if (Math.abs(delta.x) > threshold || Math.abs(delta.y) > threshold) {
55
+ event.stopPropagation();
56
+ }
57
+ pointerDownPositionRef.current = undefined;
58
+ pointerUpPositionRef.current = undefined;
59
+ };
60
+ const autoScrollToCurrentIndex = () => {
61
+ const viewportElement = viewportRef.current;
62
+ if (!viewportElement)
63
+ return;
64
+ const top = orientation === 'horizontal' ? 0 : viewportElement.clientHeight * index;
65
+ const left = orientation === 'horizontal' ? viewportElement.clientWidth * index : 0;
66
+ viewportElement.scrollTo({ top, left, behavior: 'smooth' });
67
+ clearTimeout(autoScrollTimeoutRef.current);
68
+ autoScrollTimeoutRef.current = setTimeout(() => {
69
+ clearTimeout(autoScrollTimeoutRef.current);
70
+ autoScrollTimeoutRef.current = undefined;
71
+ }, autoScrollTimeoutMs);
72
+ };
73
+ const prevIndexRef = useRef();
74
+ const viewportRef = useRef(null);
75
+ const pointerDownPositionRef = useRef();
76
+ const pointerUpPositionRef = useRef();
77
+ const [exposures, setExposures] = useState(getItemExposures());
78
+ const autoScrollTimeoutRef = useRef();
79
+ const autoScrollTimeoutMs = 1000;
80
+ const [isPointerDown, setIsPointerDown] = useState(false);
81
+ useEffect(() => {
82
+ const viewportElement = viewportRef.current;
83
+ if (!viewportElement)
84
+ return;
85
+ const scrollHandler = () => {
86
+ if (tracksItemExposure) {
87
+ setExposures(getItemExposures());
88
+ }
89
+ if (autoScrollTimeoutRef.current !== undefined)
90
+ return;
91
+ const newIndex = orientation === 'horizontal'
92
+ ? Math.round(viewportElement.scrollLeft / viewportElement.clientWidth)
93
+ : Math.round(viewportElement.scrollTop / viewportElement.clientHeight);
94
+ const clampedIndex = Math.max(0, Math.min(items.length - 1, newIndex));
95
+ if (clampedIndex === index)
96
+ return;
97
+ // Set previous index ref here to avoid the side-effect of handling index
98
+ // changes from prop/state.
99
+ prevIndexRef.current = clampedIndex;
100
+ handleIndexChange(clampedIndex);
101
+ };
102
+ viewportElement.addEventListener('scroll', scrollHandler);
103
+ return () => {
104
+ viewportElement.removeEventListener('scroll', scrollHandler);
105
+ };
106
+ }, [index, orientation]);
107
+ useEffect(() => {
108
+ const isInitialRender = prevIndexRef.current === undefined;
109
+ const isIndexModifiedFromManualScrolling = prevIndexRef.current === index;
110
+ if (isIndexModifiedFromManualScrolling)
111
+ return;
112
+ prevIndexRef.current = index;
113
+ if (isInitialRender)
114
+ return;
115
+ handleIndexChange(index);
116
+ autoScrollToCurrentIndex();
117
+ }, [index, orientation]);
118
+ useEffect(() => {
119
+ if (autoAdvanceInterval <= 0)
120
+ return;
121
+ if (isPointerDown) {
122
+ onAutoAdvancePause?.();
123
+ }
124
+ else {
125
+ onAutoAdvanceResume?.();
126
+ }
127
+ }, [isPointerDown]);
128
+ useDragEffect(viewportRef, {
129
+ isEnabled: isDragEnabled && items.length > 1,
130
+ onDragMove: (displacement) => {
131
+ switch (orientation) {
132
+ case 'horizontal':
133
+ requestAnimationFrame(() => {
134
+ if (!viewportRef.current)
135
+ return;
136
+ viewportRef.current.scrollLeft += displacement.x * 1.5;
137
+ });
138
+ break;
139
+ case 'vertical':
140
+ requestAnimationFrame(() => {
141
+ if (!viewportRef.current)
142
+ return;
143
+ viewportRef.current.scrollTop += displacement.y * 1.5;
144
+ });
145
+ break;
146
+ default:
147
+ throw Error(`Unsupported orientation '${orientation}'`);
148
+ }
149
+ },
150
+ });
151
+ useTimeout(() => handleIndexChange((index + items.length + 1) % items.length), (isPointerDown || autoAdvanceInterval <= 0) ? -1 : autoAdvanceInterval, [isPointerDown, index, items.length]);
152
+ const fixedStyles = getFixedStyles({ isPointerDown, orientation });
153
+ return (_jsx("div", { ...props, ref: ref, "data-component": 'carousel', style: styles(style, fixedStyles.root), onClick: event => handleClick(event), onPointerDown: event => handlePointerDown(event), onPointerLeave: event => handlePointerUp(event), onPointerUp: event => handlePointerUp(event), children: _jsx("div", { ref: viewportRef, "data-child": 'viewport', style: styles(fixedStyles.viewport), children: _jsx(Each, { in: items, children: ({ style: itemStyle, ...itemProps }, idx) => (_jsx("div", { style: styles(fixedStyles.itemContainer), children: _jsx(ItemComponent, { "data-child": 'item', exposure: tracksItemExposure ? exposures?.[idx] : undefined, style: styles(itemStyle, fixedStyles.item), ...itemProps }) })) }) }) }));
154
+ });
155
+ Object.defineProperty(Carousel, 'displayName', { value: 'Carousel', writable: false });
156
+ function getFixedStyles({ isPointerDown = false, orientation = 'horizontal' }) {
157
+ return asStyleDict({
158
+ root: {},
159
+ viewport: {
160
+ alignItems: 'center',
161
+ display: 'flex',
162
+ height: '100%',
163
+ userSelect: isPointerDown ? 'none' : 'auto',
164
+ justifyContent: 'flex-start',
165
+ scrollBehavior: isPointerDown ? 'auto' : 'smooth',
166
+ scrollSnapStop: isPointerDown ? 'unset' : 'always',
167
+ WebkitOverflowScrolling: 'touch',
168
+ width: '100%',
169
+ ...orientation === 'horizontal' ? {
170
+ flexDirection: 'row',
171
+ overflowX: 'scroll',
172
+ overflowY: 'hidden',
173
+ scrollSnapType: isPointerDown ? 'none' : 'x mandatory',
174
+ } : {
175
+ flexDirection: 'column',
176
+ overflowX: 'hidden',
177
+ overflowY: 'scroll',
178
+ scrollSnapType: isPointerDown ? 'none' : 'y mandatory',
179
+ },
180
+ },
181
+ itemContainer: {
182
+ height: '100%',
183
+ overflow: 'hidden',
184
+ scrollSnapAlign: 'start',
185
+ width: '100%',
186
+ scrollBehavior: 'smooth',
187
+ },
188
+ item: {
189
+ flex: '0 0 auto',
190
+ height: '100%',
191
+ width: '100%',
192
+ },
193
+ });
194
+ }