aplance-ui 1.1.0 → 1.2.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.
- package/package.json +1 -1
- package/registry/components/primitives/infinite-scroll/infinite-scroll-context.ts +32 -0
- package/registry/components/primitives/infinite-scroll/infinite-scroll.module.css +118 -0
- package/registry/components/primitives/infinite-scroll/infinite-scroll.tsx +386 -0
- package/registry/components/primitives/popover/popover.module.css +16 -21
- package/registry/components/primitives/slider/slider-context.ts +50 -0
- package/registry/components/primitives/slider/slider.module.css +300 -0
- package/registry/components/primitives/slider/slider.tsx +1174 -0
- package/registry/components/primitives/slider/types.ts +140 -0
- package/registry/components/primitives/slider/utils.ts +88 -0
- package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +144 -0
- package/registry/components/ui/breadcrumbs/breadcrumbs.tsx +236 -0
- package/registry/components/ui/button/button.module.css +3 -9
- package/registry/components/ui/chart/chart.module.css +86 -70
- package/registry/components/ui/chart/chart.tsx +117 -31
- package/registry/components/ui/input-fields/common/input-field-common.module.css +39 -65
- package/registry/components/ui/input-fields/date-picker/calendar.module.css +2 -6
- package/registry/components/ui/input-fields/date-picker/date-picker.module.css +47 -3
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +12 -12
- package/registry/components/ui/input-fields/media-input/media-input.module.css +12 -0
- package/registry/components/ui/input-fields/media-input/media-input.tsx +10 -9
- package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +19 -5
- package/registry/components/ui/input-fields/media-input/utils.ts +13 -5
- package/registry/registry.json +64 -1
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { InfiniteScrollDirection, InfiniteScrollOrientation } from "./infinite-scroll";
|
|
3
|
+
|
|
4
|
+
export type InfiniteScrollApi = {
|
|
5
|
+
orientation: InfiniteScrollOrientation;
|
|
6
|
+
direction: InfiniteScrollDirection;
|
|
7
|
+
itemCount: number;
|
|
8
|
+
copyCount: number;
|
|
9
|
+
setSize: number;
|
|
10
|
+
duration: number;
|
|
11
|
+
isMeasured: boolean;
|
|
12
|
+
isPaused: boolean;
|
|
13
|
+
isManuallyPaused: boolean;
|
|
14
|
+
isHovering: boolean;
|
|
15
|
+
isFocusWithin: boolean;
|
|
16
|
+
pause: () => void;
|
|
17
|
+
resume: () => void;
|
|
18
|
+
toggle: () => void;
|
|
19
|
+
measure: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const InfiniteScrollContext = createContext<InfiniteScrollApi | undefined>(
|
|
23
|
+
undefined,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const useInfiniteScroll = () => {
|
|
27
|
+
const ctx = useContext(InfiniteScrollContext);
|
|
28
|
+
if (!ctx) {
|
|
29
|
+
throw new Error("useInfiniteScroll must be used within an <InfiniteScroll>");
|
|
30
|
+
}
|
|
31
|
+
return ctx;
|
|
32
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* ─── Root + viewport ────────────────────────────────────────────────────────
|
|
2
|
+
Edge fade uses CSS mask so consumers don't need to wrap the children in an
|
|
3
|
+
extra layer. Mask shape switches with orientation via `fade_x` / `fade_y`.
|
|
4
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
5
|
+
|
|
6
|
+
.root {
|
|
7
|
+
--infinite-scroll-fade-size: 10%;
|
|
8
|
+
|
|
9
|
+
position: relative;
|
|
10
|
+
width: 100%;
|
|
11
|
+
min-width: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.viewport {
|
|
15
|
+
width: 100%;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
user-select: none;
|
|
19
|
+
-webkit-user-select: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.viewport_vertical {
|
|
23
|
+
height: 100%;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.fade_x {
|
|
27
|
+
-webkit-mask-image: linear-gradient(
|
|
28
|
+
to right,
|
|
29
|
+
transparent,
|
|
30
|
+
#000 var(--infinite-scroll-fade-size),
|
|
31
|
+
#000 calc(100% - var(--infinite-scroll-fade-size)),
|
|
32
|
+
transparent
|
|
33
|
+
);
|
|
34
|
+
mask-image: linear-gradient(
|
|
35
|
+
to right,
|
|
36
|
+
transparent,
|
|
37
|
+
#000 var(--infinite-scroll-fade-size),
|
|
38
|
+
#000 calc(100% - var(--infinite-scroll-fade-size)),
|
|
39
|
+
transparent
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.fade_y {
|
|
44
|
+
-webkit-mask-image: linear-gradient(
|
|
45
|
+
to bottom,
|
|
46
|
+
transparent,
|
|
47
|
+
#000 var(--infinite-scroll-fade-size),
|
|
48
|
+
#000 calc(100% - var(--infinite-scroll-fade-size)),
|
|
49
|
+
transparent
|
|
50
|
+
);
|
|
51
|
+
mask-image: linear-gradient(
|
|
52
|
+
to bottom,
|
|
53
|
+
transparent,
|
|
54
|
+
#000 var(--infinite-scroll-fade-size),
|
|
55
|
+
#000 calc(100% - var(--infinite-scroll-fade-size)),
|
|
56
|
+
transparent
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ─── Track + keyframes ──────────────────────────────────────────────────────
|
|
61
|
+
The keyframe reads from CSS custom properties set by TSX so a single
|
|
62
|
+
`infinite-scroll` animation drives both axes and both directions. The
|
|
63
|
+
distance is `setSize` measured at runtime, not a hardcoded `-50%`.
|
|
64
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
65
|
+
|
|
66
|
+
.track {
|
|
67
|
+
display: flex;
|
|
68
|
+
width: max-content;
|
|
69
|
+
will-change: transform;
|
|
70
|
+
animation-name: infinite-scroll;
|
|
71
|
+
animation-duration: var(--infinite-scroll-duration, 30s);
|
|
72
|
+
animation-timing-function: linear;
|
|
73
|
+
animation-iteration-count: infinite;
|
|
74
|
+
animation-play-state: var(--infinite-scroll-play-state, running);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.track_vertical {
|
|
78
|
+
width: 100%;
|
|
79
|
+
height: max-content;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.item {
|
|
84
|
+
min-width: 0;
|
|
85
|
+
flex: 0 0 auto;
|
|
86
|
+
|
|
87
|
+
> img,
|
|
88
|
+
> video,
|
|
89
|
+
> picture {
|
|
90
|
+
display: block;
|
|
91
|
+
user-select: none;
|
|
92
|
+
-webkit-user-drag: none;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@keyframes infinite-scroll {
|
|
97
|
+
from {
|
|
98
|
+
transform: translate3d(
|
|
99
|
+
var(--infinite-scroll-from-x, 0),
|
|
100
|
+
var(--infinite-scroll-from-y, 0),
|
|
101
|
+
0
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
to {
|
|
106
|
+
transform: translate3d(
|
|
107
|
+
var(--infinite-scroll-to-x, 0),
|
|
108
|
+
var(--infinite-scroll-to-y, 0),
|
|
109
|
+
0
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@media (--motion-reduce) {
|
|
115
|
+
.track {
|
|
116
|
+
animation: none;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
useCallback,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type ComponentPropsWithoutRef,
|
|
10
|
+
type Ref,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
} from "react";
|
|
13
|
+
import clsx from "clsx";
|
|
14
|
+
import { useResizeObserver } from "../../../hooks/useResizeObserver";
|
|
15
|
+
import {
|
|
16
|
+
InfiniteScrollContext,
|
|
17
|
+
useInfiniteScroll,
|
|
18
|
+
} from "./infinite-scroll-context";
|
|
19
|
+
import s from "./infinite-scroll.module.css";
|
|
20
|
+
|
|
21
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const infiniteScrollOrientations = ["horizontal", "vertical"] as const;
|
|
24
|
+
export type InfiniteScrollOrientation = (typeof infiniteScrollOrientations)[number];
|
|
25
|
+
|
|
26
|
+
export const infiniteScrollDirections = ["left", "right", "up", "down"] as const;
|
|
27
|
+
export type InfiniteScrollDirection = (typeof infiniteScrollDirections)[number];
|
|
28
|
+
|
|
29
|
+
export type InfiniteScrollProps = {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
viewportClassName?: string;
|
|
33
|
+
trackClassName?: string;
|
|
34
|
+
itemClassName?: string;
|
|
35
|
+
style?: CSSProperties;
|
|
36
|
+
ref?: Ref<HTMLDivElement>;
|
|
37
|
+
ariaLabel?: string;
|
|
38
|
+
|
|
39
|
+
/** Axis of continuous motion. Direction implies this when omitted. */
|
|
40
|
+
orientation?: InfiniteScrollOrientation;
|
|
41
|
+
/** Motion direction. Horizontal: left/right. Vertical: up/down. */
|
|
42
|
+
direction?: InfiniteScrollDirection;
|
|
43
|
+
/** Gap between items in px. */
|
|
44
|
+
gap?: number;
|
|
45
|
+
/** Alias for gap for API symmetry with Slider. */
|
|
46
|
+
spaceBetween?: number;
|
|
47
|
+
/** Seconds for one full content-set loop. Ignored when speed is provided. */
|
|
48
|
+
duration?: number;
|
|
49
|
+
/** Pixels per second. Keeps perceived speed stable across content widths. */
|
|
50
|
+
speed?: number;
|
|
51
|
+
/** Number of duplicated content sets. "auto" fills short tracks. */
|
|
52
|
+
copies?: number | "auto";
|
|
53
|
+
/** Pause motion while hovering. */
|
|
54
|
+
pauseOnHover?: boolean;
|
|
55
|
+
/** Pause motion while focus is inside. */
|
|
56
|
+
pauseOnFocus?: boolean;
|
|
57
|
+
/** Soft edge fade. */
|
|
58
|
+
fade?: boolean;
|
|
59
|
+
/** CSS length/percentage for fade size. */
|
|
60
|
+
fadeSize?: string;
|
|
61
|
+
/** Start paused. */
|
|
62
|
+
paused?: boolean;
|
|
63
|
+
/** Render controls or status UI inside the provider without making them scroll items. */
|
|
64
|
+
renderControls?: () => ReactNode;
|
|
65
|
+
} & Omit<ComponentPropsWithoutRef<"div">, "children" | "style">;
|
|
66
|
+
|
|
67
|
+
const directionToOrientation = (
|
|
68
|
+
direction: InfiniteScrollDirection,
|
|
69
|
+
): InfiniteScrollOrientation =>
|
|
70
|
+
direction === "up" || direction === "down" ? "vertical" : "horizontal";
|
|
71
|
+
|
|
72
|
+
// ─── InfiniteScroll ───────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export const InfiniteScroll = ({
|
|
75
|
+
children,
|
|
76
|
+
className,
|
|
77
|
+
viewportClassName,
|
|
78
|
+
trackClassName,
|
|
79
|
+
itemClassName,
|
|
80
|
+
style,
|
|
81
|
+
ref,
|
|
82
|
+
ariaLabel = "Infinite scrolling content",
|
|
83
|
+
orientation: orientationProp,
|
|
84
|
+
direction = "left",
|
|
85
|
+
gap,
|
|
86
|
+
spaceBetween,
|
|
87
|
+
duration = 30,
|
|
88
|
+
speed,
|
|
89
|
+
copies = "auto",
|
|
90
|
+
pauseOnHover = true,
|
|
91
|
+
pauseOnFocus = true,
|
|
92
|
+
fade = false,
|
|
93
|
+
fadeSize = "10%",
|
|
94
|
+
paused = false,
|
|
95
|
+
renderControls,
|
|
96
|
+
onMouseEnter,
|
|
97
|
+
onMouseLeave,
|
|
98
|
+
onFocus,
|
|
99
|
+
onBlur,
|
|
100
|
+
...rest
|
|
101
|
+
}: InfiniteScrollProps) => {
|
|
102
|
+
const items = useMemo(
|
|
103
|
+
() =>
|
|
104
|
+
Children.toArray(children).filter(
|
|
105
|
+
(child) => child !== null && child !== undefined,
|
|
106
|
+
),
|
|
107
|
+
[children],
|
|
108
|
+
);
|
|
109
|
+
const count = items.length;
|
|
110
|
+
const orientation = orientationProp ?? directionToOrientation(direction);
|
|
111
|
+
const isVertical = orientation === "vertical";
|
|
112
|
+
const gapValue = spaceBetween ?? gap ?? 0;
|
|
113
|
+
const resolvedGap = Number.isFinite(gapValue) ? Math.max(0, gapValue) : 0;
|
|
114
|
+
|
|
115
|
+
const viewportRef = useRef<HTMLDivElement>(null);
|
|
116
|
+
const trackRef = useRef<HTMLDivElement>(null);
|
|
117
|
+
const itemRefs = useRef<Array<HTMLDivElement | null>>([]);
|
|
118
|
+
const [setSize, setSetSize] = useState(0);
|
|
119
|
+
const [autoCopies, setAutoCopies] = useState(2);
|
|
120
|
+
const [isHovering, setIsHovering] = useState(false);
|
|
121
|
+
const [isFocusWithin, setIsFocusWithin] = useState(false);
|
|
122
|
+
const [isManuallyPaused, setIsManuallyPaused] = useState(false);
|
|
123
|
+
|
|
124
|
+
const copyCount =
|
|
125
|
+
copies === "auto"
|
|
126
|
+
? autoCopies
|
|
127
|
+
: Number.isFinite(copies)
|
|
128
|
+
? Math.max(2, Math.floor(copies))
|
|
129
|
+
: 2;
|
|
130
|
+
|
|
131
|
+
// ─── Measurement ──────────────────────────────────────────────────────────
|
|
132
|
+
// We measure the position of the first cloned item (index `count`) instead
|
|
133
|
+
// of dividing track size by copyCount; that captures the trailing gap
|
|
134
|
+
// exactly and keeps the loop seam invisible at subpixel widths.
|
|
135
|
+
|
|
136
|
+
const measure = useCallback(() => {
|
|
137
|
+
const viewport = viewportRef.current;
|
|
138
|
+
const track = trackRef.current;
|
|
139
|
+
if (count === 0) {
|
|
140
|
+
setSetSize(0);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!viewport || !track) return;
|
|
144
|
+
|
|
145
|
+
const viewportSize = isVertical ? viewport.clientHeight : viewport.clientWidth;
|
|
146
|
+
const firstClone = itemRefs.current[count];
|
|
147
|
+
const measuredSetSize = firstClone
|
|
148
|
+
? isVertical
|
|
149
|
+
? firstClone.offsetTop
|
|
150
|
+
: firstClone.offsetLeft
|
|
151
|
+
: (isVertical ? track.scrollHeight : track.scrollWidth) /
|
|
152
|
+
Math.max(1, copyCount);
|
|
153
|
+
|
|
154
|
+
setSetSize(measuredSetSize);
|
|
155
|
+
|
|
156
|
+
// `copies="auto"` keeps duplicating until the track covers the viewport
|
|
157
|
+
// plus a buffer, so we never see an empty gap at any width.
|
|
158
|
+
if (copies === "auto" && measuredSetSize > 0) {
|
|
159
|
+
setAutoCopies(Math.max(2, Math.ceil(viewportSize / measuredSetSize) + 2));
|
|
160
|
+
}
|
|
161
|
+
}, [copies, copyCount, count, isVertical]);
|
|
162
|
+
|
|
163
|
+
useLayoutEffect(() => {
|
|
164
|
+
measure();
|
|
165
|
+
}, [measure]);
|
|
166
|
+
useResizeObserver(viewportRef, measure);
|
|
167
|
+
useResizeObserver(trackRef, measure);
|
|
168
|
+
|
|
169
|
+
useLayoutEffect(() => {
|
|
170
|
+
itemRefs.current.length = count * copyCount;
|
|
171
|
+
}, [copyCount, count]);
|
|
172
|
+
|
|
173
|
+
// ─── Animation state ──────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
const shouldPause =
|
|
176
|
+
paused ||
|
|
177
|
+
isManuallyPaused ||
|
|
178
|
+
(pauseOnHover && isHovering) ||
|
|
179
|
+
(pauseOnFocus && isFocusWithin) ||
|
|
180
|
+
count <= 1 ||
|
|
181
|
+
setSize <= 0;
|
|
182
|
+
|
|
183
|
+
const isReverse = direction === "right" || direction === "down";
|
|
184
|
+
const from = isReverse ? -setSize : 0;
|
|
185
|
+
const to = isReverse ? 0 : -setSize;
|
|
186
|
+
const durationValue = Number.isFinite(duration) ? duration : 30;
|
|
187
|
+
const speedValue =
|
|
188
|
+
typeof speed === "number" && Number.isFinite(speed) ? speed : undefined;
|
|
189
|
+
// `speed` (px/s) wins over `duration` because perceived speed should stay
|
|
190
|
+
// constant as content (and thus setSize) changes.
|
|
191
|
+
const resolvedDuration =
|
|
192
|
+
speedValue && speedValue > 0 && setSize > 0
|
|
193
|
+
? setSize / speedValue
|
|
194
|
+
: Math.max(0.1, durationValue);
|
|
195
|
+
const trackStyle = {
|
|
196
|
+
gap: `${resolvedGap}px`,
|
|
197
|
+
["--infinite-scroll-duration" as string]: `${resolvedDuration}s`,
|
|
198
|
+
["--infinite-scroll-play-state" as string]: shouldPause
|
|
199
|
+
? "paused"
|
|
200
|
+
: "running",
|
|
201
|
+
["--infinite-scroll-from-x" as string]: isVertical ? "0px" : `${from}px`,
|
|
202
|
+
["--infinite-scroll-from-y" as string]: isVertical ? `${from}px` : "0px",
|
|
203
|
+
["--infinite-scroll-to-x" as string]: isVertical ? "0px" : `${to}px`,
|
|
204
|
+
["--infinite-scroll-to-y" as string]: isVertical ? `${to}px` : "0px",
|
|
205
|
+
} satisfies CSSProperties;
|
|
206
|
+
|
|
207
|
+
const api = useMemo(
|
|
208
|
+
() => ({
|
|
209
|
+
orientation,
|
|
210
|
+
direction,
|
|
211
|
+
itemCount: count,
|
|
212
|
+
copyCount,
|
|
213
|
+
setSize,
|
|
214
|
+
duration: resolvedDuration,
|
|
215
|
+
isMeasured: count === 0 || setSize > 0,
|
|
216
|
+
isPaused: shouldPause,
|
|
217
|
+
isManuallyPaused,
|
|
218
|
+
isHovering,
|
|
219
|
+
isFocusWithin,
|
|
220
|
+
pause: () => setIsManuallyPaused(true),
|
|
221
|
+
resume: () => setIsManuallyPaused(false),
|
|
222
|
+
toggle: () => setIsManuallyPaused((value) => !value),
|
|
223
|
+
measure,
|
|
224
|
+
}),
|
|
225
|
+
[
|
|
226
|
+
copyCount,
|
|
227
|
+
count,
|
|
228
|
+
direction,
|
|
229
|
+
isFocusWithin,
|
|
230
|
+
isHovering,
|
|
231
|
+
isManuallyPaused,
|
|
232
|
+
measure,
|
|
233
|
+
orientation,
|
|
234
|
+
resolvedDuration,
|
|
235
|
+
setSize,
|
|
236
|
+
shouldPause,
|
|
237
|
+
],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<InfiniteScrollContext.Provider value={api}>
|
|
242
|
+
<div
|
|
243
|
+
ref={ref}
|
|
244
|
+
className={clsx(s.root, className)}
|
|
245
|
+
style={{
|
|
246
|
+
...style,
|
|
247
|
+
["--infinite-scroll-fade-size" as string]: fadeSize,
|
|
248
|
+
}}
|
|
249
|
+
role="region"
|
|
250
|
+
aria-label={ariaLabel}
|
|
251
|
+
onMouseEnter={(event) => {
|
|
252
|
+
onMouseEnter?.(event);
|
|
253
|
+
setIsHovering(true);
|
|
254
|
+
}}
|
|
255
|
+
onMouseLeave={(event) => {
|
|
256
|
+
onMouseLeave?.(event);
|
|
257
|
+
setIsHovering(false);
|
|
258
|
+
}}
|
|
259
|
+
onFocus={(event) => {
|
|
260
|
+
onFocus?.(event);
|
|
261
|
+
setIsFocusWithin(true);
|
|
262
|
+
}}
|
|
263
|
+
onBlur={(event) => {
|
|
264
|
+
onBlur?.(event);
|
|
265
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
266
|
+
setIsFocusWithin(false);
|
|
267
|
+
}
|
|
268
|
+
}}
|
|
269
|
+
{...rest}
|
|
270
|
+
>
|
|
271
|
+
<div
|
|
272
|
+
ref={viewportRef}
|
|
273
|
+
className={clsx(
|
|
274
|
+
s.viewport,
|
|
275
|
+
isVertical && s.viewport_vertical,
|
|
276
|
+
fade && (isVertical ? s.fade_y : s.fade_x),
|
|
277
|
+
viewportClassName,
|
|
278
|
+
)}
|
|
279
|
+
style={{ visibility: count === 0 || setSize > 0 ? undefined : "hidden" }}
|
|
280
|
+
onDragStart={(event) => event.preventDefault()}
|
|
281
|
+
>
|
|
282
|
+
<div
|
|
283
|
+
ref={trackRef}
|
|
284
|
+
className={clsx(s.track, isVertical && s.track_vertical, trackClassName)}
|
|
285
|
+
style={trackStyle}
|
|
286
|
+
>
|
|
287
|
+
{Array.from({ length: copyCount }).flatMap((_, copyIndex) =>
|
|
288
|
+
items.map((item, itemIndex) => {
|
|
289
|
+
const renderedIndex = copyIndex * count + itemIndex;
|
|
290
|
+
return (
|
|
291
|
+
<div
|
|
292
|
+
key={`${itemIndex}-${copyIndex}`}
|
|
293
|
+
ref={(node) => {
|
|
294
|
+
itemRefs.current[renderedIndex] = node;
|
|
295
|
+
}}
|
|
296
|
+
className={clsx(s.item, itemClassName)}
|
|
297
|
+
aria-hidden={copyIndex > 0 || undefined}
|
|
298
|
+
data-clone={copyIndex > 0 ? "true" : undefined}
|
|
299
|
+
>
|
|
300
|
+
{item}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}),
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
{renderControls?.()}
|
|
308
|
+
</div>
|
|
309
|
+
</InfiniteScrollContext.Provider>
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// ─── Control sub-components ───────────────────────────────────────────────────
|
|
314
|
+
// Use these to wire pause/resume/toggle controls anywhere inside the provider.
|
|
315
|
+
// For arbitrary status UI, read `useInfiniteScroll()` directly.
|
|
316
|
+
|
|
317
|
+
type InfiniteScrollControlProps = {
|
|
318
|
+
className?: string;
|
|
319
|
+
children?: ReactNode;
|
|
320
|
+
ariaLabel?: string;
|
|
321
|
+
} & Omit<ComponentPropsWithoutRef<"button">, "children" | "onClick">;
|
|
322
|
+
|
|
323
|
+
export const InfiniteScrollPause = ({
|
|
324
|
+
className,
|
|
325
|
+
children = "Pause",
|
|
326
|
+
ariaLabel = "Pause infinite scroll",
|
|
327
|
+
...rest
|
|
328
|
+
}: InfiniteScrollControlProps) => {
|
|
329
|
+
const { pause, isPaused } = useInfiniteScroll();
|
|
330
|
+
return (
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
className={className}
|
|
334
|
+
{...rest}
|
|
335
|
+
onClick={pause}
|
|
336
|
+
disabled={isPaused}
|
|
337
|
+
aria-label={ariaLabel}
|
|
338
|
+
>
|
|
339
|
+
{children}
|
|
340
|
+
</button>
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export const InfiniteScrollResume = ({
|
|
345
|
+
className,
|
|
346
|
+
children = "Resume",
|
|
347
|
+
ariaLabel = "Resume infinite scroll",
|
|
348
|
+
...rest
|
|
349
|
+
}: InfiniteScrollControlProps) => {
|
|
350
|
+
const { resume, isPaused } = useInfiniteScroll();
|
|
351
|
+
return (
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
className={className}
|
|
355
|
+
{...rest}
|
|
356
|
+
onClick={resume}
|
|
357
|
+
disabled={!isPaused}
|
|
358
|
+
aria-label={ariaLabel}
|
|
359
|
+
>
|
|
360
|
+
{children}
|
|
361
|
+
</button>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const InfiniteScrollToggle = ({
|
|
366
|
+
className,
|
|
367
|
+
children,
|
|
368
|
+
ariaLabel,
|
|
369
|
+
...rest
|
|
370
|
+
}: InfiniteScrollControlProps) => {
|
|
371
|
+
const { toggle, isPaused } = useInfiniteScroll();
|
|
372
|
+
return (
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
className={className}
|
|
376
|
+
{...rest}
|
|
377
|
+
onClick={toggle}
|
|
378
|
+
aria-pressed={isPaused}
|
|
379
|
+
aria-label={ariaLabel ?? (isPaused ? "Resume infinite scroll" : "Pause infinite scroll")}
|
|
380
|
+
>
|
|
381
|
+
{children ?? (isPaused ? "Resume" : "Pause")}
|
|
382
|
+
</button>
|
|
383
|
+
);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export { useInfiniteScroll };
|
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
.container {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
position: relative;
|
|
7
|
-
display: flex;
|
|
2
|
+
.container {
|
|
3
|
+
width: fit-content;
|
|
4
|
+
position: relative;
|
|
5
|
+
display: flex;
|
|
8
6
|
align-items: center;
|
|
9
7
|
justify-content: center;
|
|
10
8
|
|
|
11
9
|
.trigger {
|
|
12
|
-
width: 100%;
|
|
13
|
-
min-width: fit-content;
|
|
14
|
-
display: flex;
|
|
15
|
-
align-items: center;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
pointer-events: none;
|
|
10
|
+
width: 100%;
|
|
11
|
+
min-width: fit-content;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
|
|
15
|
+
&[data-disabled="true"] {
|
|
16
|
+
pointer-events: none;
|
|
20
17
|
}
|
|
21
18
|
}
|
|
22
|
-
|
|
23
|
-
.menu_wrapper {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
}
|
|
19
|
+
|
|
20
|
+
.menu_wrapper {
|
|
21
|
+
min-width: fit-content;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type SliderApi = {
|
|
4
|
+
/** Current logical slide index. */
|
|
5
|
+
index: number;
|
|
6
|
+
/** Alias for index, useful when mirroring carousel terminology. */
|
|
7
|
+
activeIndex: number;
|
|
8
|
+
/** Total number of real slides. */
|
|
9
|
+
count: number;
|
|
10
|
+
/** Total number of snap points/pages. */
|
|
11
|
+
snapCount: number;
|
|
12
|
+
/** How many slides are visible in the current responsive layout. */
|
|
13
|
+
slidesPerView: number | "auto";
|
|
14
|
+
/** How many slides move on next/previous. */
|
|
15
|
+
slidesPerGroup: number;
|
|
16
|
+
/** 0..1 progress through the available snap points. */
|
|
17
|
+
progress: number;
|
|
18
|
+
/** Whether loop mode is active. */
|
|
19
|
+
isLoop: boolean;
|
|
20
|
+
/** Whether the pointer is currently dragging the track. */
|
|
21
|
+
isDragging: boolean;
|
|
22
|
+
/** Whether autoplay is currently paused by hover/focus/drag. */
|
|
23
|
+
isAutoplayPaused: boolean;
|
|
24
|
+
/** True when previous movement is not available in non-loop mode. */
|
|
25
|
+
isFirst: boolean;
|
|
26
|
+
/** True when next movement is not available in non-loop mode. */
|
|
27
|
+
isLast: boolean;
|
|
28
|
+
/** Alias for !isFirst. */
|
|
29
|
+
canSlidePrev: boolean;
|
|
30
|
+
/** Alias for !isLast. */
|
|
31
|
+
canSlideNext: boolean;
|
|
32
|
+
/** Advance by one group, or by a custom step count. */
|
|
33
|
+
next: (steps?: number) => void;
|
|
34
|
+
/** Move back by one group, or by a custom step count. */
|
|
35
|
+
prev: (steps?: number) => void;
|
|
36
|
+
/** Jump to a logical slide/snap index. */
|
|
37
|
+
goTo: (index: number) => void;
|
|
38
|
+
/** Alias for goTo. */
|
|
39
|
+
slideTo: (index: number) => void;
|
|
40
|
+
/** Stop autoplay for the rest of the current mount. */
|
|
41
|
+
stopAutoplay: () => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const SliderContext = createContext<SliderApi | undefined>(undefined);
|
|
45
|
+
|
|
46
|
+
export const useSlider = () => {
|
|
47
|
+
const ctx = useContext(SliderContext);
|
|
48
|
+
if (!ctx) throw new Error("useSlider must be used within a <Slider>");
|
|
49
|
+
return ctx;
|
|
50
|
+
};
|