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.
Files changed (24) hide show
  1. package/package.json +1 -1
  2. package/registry/components/primitives/infinite-scroll/infinite-scroll-context.ts +32 -0
  3. package/registry/components/primitives/infinite-scroll/infinite-scroll.module.css +118 -0
  4. package/registry/components/primitives/infinite-scroll/infinite-scroll.tsx +386 -0
  5. package/registry/components/primitives/popover/popover.module.css +16 -21
  6. package/registry/components/primitives/slider/slider-context.ts +50 -0
  7. package/registry/components/primitives/slider/slider.module.css +300 -0
  8. package/registry/components/primitives/slider/slider.tsx +1174 -0
  9. package/registry/components/primitives/slider/types.ts +140 -0
  10. package/registry/components/primitives/slider/utils.ts +88 -0
  11. package/registry/components/ui/breadcrumbs/breadcrumbs.module.css +144 -0
  12. package/registry/components/ui/breadcrumbs/breadcrumbs.tsx +236 -0
  13. package/registry/components/ui/button/button.module.css +3 -9
  14. package/registry/components/ui/chart/chart.module.css +86 -70
  15. package/registry/components/ui/chart/chart.tsx +117 -31
  16. package/registry/components/ui/input-fields/common/input-field-common.module.css +39 -65
  17. package/registry/components/ui/input-fields/date-picker/calendar.module.css +2 -6
  18. package/registry/components/ui/input-fields/date-picker/date-picker.module.css +47 -3
  19. package/registry/components/ui/input-fields/date-picker/date-picker.tsx +12 -12
  20. package/registry/components/ui/input-fields/media-input/media-input.module.css +12 -0
  21. package/registry/components/ui/input-fields/media-input/media-input.tsx +10 -9
  22. package/registry/components/ui/input-fields/media-input/useMediaUpload.tsx +19 -5
  23. package/registry/components/ui/input-fields/media-input/utils.ts +13 -5
  24. package/registry/registry.json +64 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aplance-ui",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "The aplance-ui CLI - scaffold the design system and copy components into your project as source.",
5
5
  "keywords": [
6
6
  "react",
@@ -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
- /* its width can be managed from parent or user component container */
4
- /* e.g. using with custom-select: .container can be anything and this will match it */
5
- width: fit-content;
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
- /* add other styles from props */
17
-
18
- &[data-disabled="true"] {
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
- /* style as needed at usage */
25
- min-width: fit-content;
26
- /* max-width: 100% !important; can be updated with !important */
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
+ };