@jorgemadrid/open-carousel 0.1.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/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/index.cjs +2529 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +618 -0
- package/dist/index.d.ts +618 -0
- package/dist/index.js +2471 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +102 -0
- package/package.json +71 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode, PointerEvent, MouseEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
declare const VISUAL_CONFIG: {
|
|
6
|
+
readonly MAX_DIST_MOBILE: 320;
|
|
7
|
+
readonly MAX_DIST_TABLET: 400;
|
|
8
|
+
readonly MAX_DIST_DESKTOP: 500;
|
|
9
|
+
readonly BASE_SCALE_MOBILE: 0.8;
|
|
10
|
+
readonly BASE_SCALE_TABLET: 0.82;
|
|
11
|
+
readonly BASE_SCALE_DESKTOP: 0.85;
|
|
12
|
+
readonly VIEW_BUFFER: 200;
|
|
13
|
+
readonly CENTER_THRESHOLD: 10;
|
|
14
|
+
readonly DISABLE_DYNAMIC_SHADOW: true;
|
|
15
|
+
};
|
|
16
|
+
declare const TIMING_CONFIG: {
|
|
17
|
+
readonly BOUNCE_DISTANCE_PX: 30;
|
|
18
|
+
readonly BOUNCE_PHASE1_MS: 150;
|
|
19
|
+
readonly BOUNCE_PHASE2_MS: 450;
|
|
20
|
+
readonly PRE_TELEPORT_CLEAR_DELAY_MS: 100;
|
|
21
|
+
readonly SCROLL_IDLE_FALLBACK_MS: 300;
|
|
22
|
+
readonly SCROLL_DEBOUNCE_FALLBACK_MS: 50;
|
|
23
|
+
readonly SNAP_RESTORE_DELAY_MS: 100;
|
|
24
|
+
readonly RESIZE_DEBOUNCE_MS: 100;
|
|
25
|
+
readonly SCROLL_PERSIST_DEBOUNCE_MS: 150;
|
|
26
|
+
readonly SCROLL_COMPLETION_DEBOUNCE_MS: 150;
|
|
27
|
+
readonly SCROLL_TARGET_TOLERANCE_RATIO: 0.5;
|
|
28
|
+
};
|
|
29
|
+
declare const LAYOUT_CONFIG: {
|
|
30
|
+
readonly GAP_MOBILE: 12;
|
|
31
|
+
readonly GAP_DESKTOP: 16;
|
|
32
|
+
readonly GAP_BREAKPOINT: 768;
|
|
33
|
+
readonly MIN_BUFFER_COUNT: 50;
|
|
34
|
+
readonly EDGE_TOLERANCE_START: 20;
|
|
35
|
+
readonly EDGE_TOLERANCE_END: 5;
|
|
36
|
+
readonly INITIAL_CARD_WIDTH: 180;
|
|
37
|
+
readonly INITIAL_GAP: 16;
|
|
38
|
+
};
|
|
39
|
+
declare const DEBUG_CONFIG: {
|
|
40
|
+
ENABLED: boolean;
|
|
41
|
+
HISTORY_SIZE: number;
|
|
42
|
+
CHANNELS: {
|
|
43
|
+
ALL: boolean;
|
|
44
|
+
TELEPORT: boolean;
|
|
45
|
+
VISUALS: boolean;
|
|
46
|
+
LAYOUT: boolean;
|
|
47
|
+
INIT: boolean;
|
|
48
|
+
CACHE: boolean;
|
|
49
|
+
COORDINATOR: boolean;
|
|
50
|
+
NAV: boolean;
|
|
51
|
+
INTERACT: boolean;
|
|
52
|
+
PERF: boolean;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
declare const FEATURE_FLAGS: {
|
|
56
|
+
readonly USE_RAF_FRAME_SEPARATION: true;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type DebugChannel = keyof typeof DEBUG_CONFIG.CHANNELS;
|
|
60
|
+
type ChannelConfig = Partial<Record<DebugChannel, boolean>> | 'ALL';
|
|
61
|
+
interface LoggerConfig {
|
|
62
|
+
/** Override channel activation for this instance (or 'ALL' for everything) */
|
|
63
|
+
channels?: ChannelConfig;
|
|
64
|
+
/** Max entries in local buffer (default: 100) */
|
|
65
|
+
bufferSize?: number;
|
|
66
|
+
}
|
|
67
|
+
declare class CarouselLoggerInstance {
|
|
68
|
+
readonly id: string;
|
|
69
|
+
private localHistory;
|
|
70
|
+
private maxLocalHistory;
|
|
71
|
+
private channelOverrides;
|
|
72
|
+
private registry;
|
|
73
|
+
constructor(id: string, config?: LoggerConfig);
|
|
74
|
+
/**
|
|
75
|
+
* Check if a channel should log to console.
|
|
76
|
+
* Resolution order: instance override → global config
|
|
77
|
+
*/
|
|
78
|
+
private shouldLog;
|
|
79
|
+
/**
|
|
80
|
+
* Log a message to history buffers and optionally to console.
|
|
81
|
+
*/
|
|
82
|
+
log(channel: DebugChannel, message: string, data?: unknown): void;
|
|
83
|
+
/**
|
|
84
|
+
* Dump this instance's local history to console.
|
|
85
|
+
*/
|
|
86
|
+
dump(): string;
|
|
87
|
+
/** Clear local history */
|
|
88
|
+
clear(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Create a performance timer for tracking elapsed time.
|
|
91
|
+
* Usage:
|
|
92
|
+
* const timer = logger.createTimer()
|
|
93
|
+
* // ... do work ...
|
|
94
|
+
* logger.log('INIT', 'Completed', { elapsedMs: timer.elapsed() })
|
|
95
|
+
*/
|
|
96
|
+
createTimer(): {
|
|
97
|
+
elapsed: () => number;
|
|
98
|
+
reset: () => void;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a new logger instance for a carousel.
|
|
103
|
+
*
|
|
104
|
+
* @param id - Unique identifier for this carousel (e.g., 'hero', 'related-products')
|
|
105
|
+
* @param config - Optional configuration for channel overrides and buffer size
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```tsx
|
|
109
|
+
* // In BaseCarousel
|
|
110
|
+
* const logger = createLogger(debugId, { channels: { NAV: true } })
|
|
111
|
+
*
|
|
112
|
+
* // Pass to hooks
|
|
113
|
+
* const navigation = useCarouselNavigation({ ..., logger })
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare function createLogger(id: string, config?: LoggerConfig): CarouselLoggerInstance;
|
|
117
|
+
declare const carouselLogger: CarouselLoggerInstance;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
/** Available CSS variable names for carousel item widths */
|
|
121
|
+
type CarouselWidthVar = 'default' | 'review' | 'compact' | 'collection' | 'wide';
|
|
122
|
+
interface BaseCarouselProps<T> {
|
|
123
|
+
items: T[];
|
|
124
|
+
getItemKey: (item: T, index: number) => string;
|
|
125
|
+
renderItem: (item: T, index: number, helpers: {
|
|
126
|
+
scrollToItem: () => void;
|
|
127
|
+
}) => ReactNode;
|
|
128
|
+
infinite?: boolean;
|
|
129
|
+
onEndReached?: () => void;
|
|
130
|
+
hasNextPage?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* CSS variable name for item width. Uses native CSS media queries for responsive widths.
|
|
133
|
+
* Options: 'default' | 'review' | 'compact' | 'collection' | 'wide'
|
|
134
|
+
* @deprecated Use itemWidthCssVar for full flexibility
|
|
135
|
+
*/
|
|
136
|
+
itemWidthVar?: CarouselWidthVar;
|
|
137
|
+
/**
|
|
138
|
+
* Custom CSS variable name for item width (e.g., '--my-carousel-item-width').
|
|
139
|
+
* Use this for full flexibility - define your own CSS variable with responsive breakpoints.
|
|
140
|
+
* Takes precedence over itemWidthVar when provided.
|
|
141
|
+
*/
|
|
142
|
+
itemWidthCssVar?: string;
|
|
143
|
+
/**
|
|
144
|
+
* Fallback width in pixels when using itemWidthCssVar and the CSS variable is undefined.
|
|
145
|
+
* Required when using itemWidthCssVar. Defaults to 200 if not provided.
|
|
146
|
+
*/
|
|
147
|
+
fallbackWidth?: number;
|
|
148
|
+
itemClassName?: string;
|
|
149
|
+
snapType?: 'mandatory' | 'proximity';
|
|
150
|
+
disableOpacityEffect?: boolean;
|
|
151
|
+
disableScaleEffect?: boolean;
|
|
152
|
+
/** Custom vertical padding for the carousel container. Defaults to '20px'. */
|
|
153
|
+
verticalPadding?: string;
|
|
154
|
+
snap?: boolean;
|
|
155
|
+
/** Optional custom skeleton renderer. Receives index. */
|
|
156
|
+
renderSkeleton?: (index: number) => ReactNode;
|
|
157
|
+
/**
|
|
158
|
+
* Optional key for persisting scroll position in sessionStorage.
|
|
159
|
+
* When provided, the carousel will restore its scroll position after navigation.
|
|
160
|
+
* Use a unique key per carousel instance, e.g., 'homepage-recommended'.
|
|
161
|
+
*/
|
|
162
|
+
persistKey?: string;
|
|
163
|
+
onActiveItemChange?: (item: T) => void;
|
|
164
|
+
/** Optional explicit gap value in pixels. If not provided, uses LAYOUT_CONFIG based on viewport. */
|
|
165
|
+
gap?: number;
|
|
166
|
+
/** Optional id for debug logging - helps identify which carousel in console */
|
|
167
|
+
debugId?: string;
|
|
168
|
+
/**
|
|
169
|
+
* Optional per-instance debug config.
|
|
170
|
+
* Set `channels` to override global config for this carousel only.
|
|
171
|
+
* Use 'ALL' to enable all channels.
|
|
172
|
+
*/
|
|
173
|
+
debug?: {
|
|
174
|
+
channels?: ChannelConfig;
|
|
175
|
+
bufferSize?: number;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Optional initial index to scroll to on mount.
|
|
179
|
+
* Takes precedence over buffer positioning but yields to persisted position if available.
|
|
180
|
+
*/
|
|
181
|
+
initialIndex?: number;
|
|
182
|
+
/**
|
|
183
|
+
* If true, changes the selection threshold on mobile (viewport < 640px) from 0.5 (50%) to 0.3 (30%)
|
|
184
|
+
* This makes the carousel select the next/prev item with less swipe distance.
|
|
185
|
+
*/
|
|
186
|
+
eagerSelectionOnMobile?: boolean;
|
|
187
|
+
}
|
|
188
|
+
declare function BaseCarouselInner<T>({ items, getItemKey, renderItem, infinite, onEndReached, hasNextPage, itemWidthVar, itemWidthCssVar, fallbackWidth, itemClassName, snapType, disableOpacityEffect, disableScaleEffect, verticalPadding, snap, renderSkeleton, persistKey, onActiveItemChange, gap: gapProp, debugId, debug, eagerSelectionOnMobile, initialIndex, }: BaseCarouselProps<T>): react_jsx_runtime.JSX.Element;
|
|
189
|
+
declare const Carousel: typeof BaseCarouselInner;
|
|
190
|
+
|
|
191
|
+
type Direction = 'left' | 'right';
|
|
192
|
+
interface CarouselArrowProps {
|
|
193
|
+
direction: Direction;
|
|
194
|
+
onClick: () => void;
|
|
195
|
+
disabled?: boolean;
|
|
196
|
+
className?: string;
|
|
197
|
+
}
|
|
198
|
+
declare function CarouselArrow({ direction, onClick, disabled, className, }: CarouselArrowProps): react_jsx_runtime.JSX.Element;
|
|
199
|
+
|
|
200
|
+
type CarouselPhase = 'UNINITIALIZED' | 'IDLE' | 'SCROLLING' | 'BOUNCING' | 'PRE_TELEPORTING' | 'TELEPORTING' | 'DRAGGING';
|
|
201
|
+
interface CarouselContext {
|
|
202
|
+
phase: CarouselPhase;
|
|
203
|
+
pendingTarget: number | null;
|
|
204
|
+
scrollDirection: -1 | 1 | null;
|
|
205
|
+
teleportOffset: number | null;
|
|
206
|
+
isTeleporting: boolean;
|
|
207
|
+
isPreTeleporting: boolean;
|
|
208
|
+
lastActiveItemKey: string | null;
|
|
209
|
+
snapTimeoutId: ReturnType<typeof setTimeout> | null;
|
|
210
|
+
scrollIdleTimeoutId: ReturnType<typeof setTimeout> | null;
|
|
211
|
+
bounceTimeoutId: ReturnType<typeof setTimeout> | null;
|
|
212
|
+
hasScrollEndListener: boolean;
|
|
213
|
+
}
|
|
214
|
+
type CarouselAction = {
|
|
215
|
+
type: 'INITIALIZE';
|
|
216
|
+
} | {
|
|
217
|
+
type: 'ARROW_CLICK';
|
|
218
|
+
direction: -1 | 1;
|
|
219
|
+
targetScroll: number;
|
|
220
|
+
} | {
|
|
221
|
+
type: 'ITEM_CLICK';
|
|
222
|
+
targetScroll: number;
|
|
223
|
+
} | {
|
|
224
|
+
type: 'SCROLL_COMPLETE';
|
|
225
|
+
} | {
|
|
226
|
+
type: 'USER_INTERRUPT';
|
|
227
|
+
} | {
|
|
228
|
+
type: 'START_BOUNCE';
|
|
229
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
230
|
+
} | {
|
|
231
|
+
type: 'END_BOUNCE';
|
|
232
|
+
} | {
|
|
233
|
+
type: 'START_PRE_TELEPORT';
|
|
234
|
+
} | {
|
|
235
|
+
type: 'EXECUTE_TELEPORT';
|
|
236
|
+
offset: number;
|
|
237
|
+
} | {
|
|
238
|
+
type: 'END_TELEPORT';
|
|
239
|
+
} | {
|
|
240
|
+
type: 'START_DRAG';
|
|
241
|
+
} | {
|
|
242
|
+
type: 'END_DRAG';
|
|
243
|
+
} | {
|
|
244
|
+
type: 'SET_SNAP_TIMEOUT';
|
|
245
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
246
|
+
} | {
|
|
247
|
+
type: 'CLEAR_SNAP_TIMEOUT';
|
|
248
|
+
} | {
|
|
249
|
+
type: 'SET_SCROLL_IDLE_TIMEOUT';
|
|
250
|
+
timeoutId: ReturnType<typeof setTimeout>;
|
|
251
|
+
} | {
|
|
252
|
+
type: 'CLEAR_SCROLL_IDLE_TIMEOUT';
|
|
253
|
+
} | {
|
|
254
|
+
type: 'SET_SCROLL_END_LISTENER';
|
|
255
|
+
hasListener: boolean;
|
|
256
|
+
} | {
|
|
257
|
+
type: 'SET_ACTIVE_ITEM_KEY';
|
|
258
|
+
key: string | null;
|
|
259
|
+
} | {
|
|
260
|
+
type: 'SET_TELEPORTING';
|
|
261
|
+
value: boolean;
|
|
262
|
+
} | {
|
|
263
|
+
type: 'SET_PRE_TELEPORTING';
|
|
264
|
+
value: boolean;
|
|
265
|
+
} | {
|
|
266
|
+
type: 'SET_PENDING_TARGET';
|
|
267
|
+
target: number;
|
|
268
|
+
};
|
|
269
|
+
declare function reduce(context: CarouselContext, action: CarouselAction): CarouselContext;
|
|
270
|
+
interface UseCarouselCoordinatorOptions {
|
|
271
|
+
/** Optional logger for debugging */
|
|
272
|
+
logger?: CarouselLoggerInstance;
|
|
273
|
+
}
|
|
274
|
+
interface UseCarouselCoordinatorReturn {
|
|
275
|
+
/** Dispatch an action to transition state */
|
|
276
|
+
transition: (action: CarouselAction) => CarouselContext;
|
|
277
|
+
/** Get current phase */
|
|
278
|
+
getPhase: () => CarouselPhase;
|
|
279
|
+
/** Get full context (read-only snapshot) */
|
|
280
|
+
getContext: () => Readonly<CarouselContext>;
|
|
281
|
+
/** Direct ref access (for passing to child hooks) */
|
|
282
|
+
contextRef: React.MutableRefObject<CarouselContext>;
|
|
283
|
+
/** Check if currently in a "busy" phase (not idle) */
|
|
284
|
+
isBusy: () => boolean;
|
|
285
|
+
/** Check if user interaction should be blocked */
|
|
286
|
+
isBlocking: () => boolean;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Carousel Coordinator Hook
|
|
290
|
+
*
|
|
291
|
+
* Consolidates all carousel state into a single ref-based state machine.
|
|
292
|
+
* No re-renders triggered - all state is in refs.
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```tsx
|
|
296
|
+
* const { transition, getPhase, getContext } = useCarouselCoordinator({ logger })
|
|
297
|
+
*
|
|
298
|
+
* // Check state
|
|
299
|
+
* if (getPhase() === 'IDLE') {
|
|
300
|
+
* transition({ type: 'ARROW_CLICK', direction: 1, targetScroll: 500 })
|
|
301
|
+
* }
|
|
302
|
+
*
|
|
303
|
+
* // Later, on scroll complete
|
|
304
|
+
* transition({ type: 'SCROLL_COMPLETE' })
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
declare function useCarouselCoordinator(options?: UseCarouselCoordinatorOptions): UseCarouselCoordinatorReturn;
|
|
308
|
+
|
|
309
|
+
interface UseCarouselLayoutOptions {
|
|
310
|
+
/** Ref to the scrollable carousel container */
|
|
311
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
312
|
+
/** Callback when layout is measured (for updating visual caches) */
|
|
313
|
+
onLayoutChange?: (layout: {
|
|
314
|
+
cardWidth: number;
|
|
315
|
+
gap: number;
|
|
316
|
+
}) => void;
|
|
317
|
+
/** Debounce delay for resize handling in ms. Default: 100 */
|
|
318
|
+
resizeDebounceMs?: number;
|
|
319
|
+
/** Optional logger for debugging */
|
|
320
|
+
logger?: CarouselLoggerInstance;
|
|
321
|
+
}
|
|
322
|
+
interface UseCarouselLayoutReturn {
|
|
323
|
+
/** Current layout measurements */
|
|
324
|
+
layout: {
|
|
325
|
+
cardWidth: number;
|
|
326
|
+
gap: number;
|
|
327
|
+
domStride: number;
|
|
328
|
+
};
|
|
329
|
+
/** Computed stride (cardWidth + gap) - fallback if domStride unavailable */
|
|
330
|
+
stride: number;
|
|
331
|
+
/** Whether viewport is mobile (< 640px) */
|
|
332
|
+
isMobile: boolean;
|
|
333
|
+
/** Whether viewport is tablet (< 1024px but >= 640px) */
|
|
334
|
+
isTablet: boolean;
|
|
335
|
+
/** Manually trigger a layout measurement */
|
|
336
|
+
measureLayout: () => {
|
|
337
|
+
cardWidth: number;
|
|
338
|
+
gap: number;
|
|
339
|
+
};
|
|
340
|
+
/** Mark layout as dirty (triggers remeasure on next access) */
|
|
341
|
+
invalidateLayout: () => void;
|
|
342
|
+
/**
|
|
343
|
+
* Counter that increments each time ResizeObserver fires.
|
|
344
|
+
* Used to trigger re-checks even when layout values are unchanged.
|
|
345
|
+
*/
|
|
346
|
+
resizeCount: number;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Calculate layout measurements from a carousel container element.
|
|
350
|
+
* Reads the first child's width and determines gap based on breakpoint.
|
|
351
|
+
*
|
|
352
|
+
* @param container - The scrollable carousel container element
|
|
353
|
+
* @returns The measured cardWidth and gap values, or null if children aren't rendered yet
|
|
354
|
+
*/
|
|
355
|
+
declare function measureLayoutFromElement(container: HTMLElement): {
|
|
356
|
+
cardWidth: number;
|
|
357
|
+
gap: number;
|
|
358
|
+
domStride: number;
|
|
359
|
+
} | null;
|
|
360
|
+
/**
|
|
361
|
+
* Hook that manages carousel layout measurements.
|
|
362
|
+
* Handles:
|
|
363
|
+
* - Initial layout measurement from first card element
|
|
364
|
+
* - Viewport detection (mobile, tablet, desktop)
|
|
365
|
+
* - Resize handling with debouncing
|
|
366
|
+
* - Stride calculation
|
|
367
|
+
*/
|
|
368
|
+
declare function useCarouselLayout({ containerRef, onLayoutChange, resizeDebounceMs, logger, }: UseCarouselLayoutOptions): UseCarouselLayoutReturn;
|
|
369
|
+
|
|
370
|
+
interface UseCarouselNavigationOptions {
|
|
371
|
+
/** Ref to the scrollable carousel container */
|
|
372
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
373
|
+
/** Whether this is an infinite carousel */
|
|
374
|
+
infinite: boolean;
|
|
375
|
+
/** Layout measurements */
|
|
376
|
+
layout: {
|
|
377
|
+
cardWidth: number;
|
|
378
|
+
gap: number;
|
|
379
|
+
};
|
|
380
|
+
/** Function to cancel momentum scroll (from useDraggableScroll) */
|
|
381
|
+
cancelMomentum: () => void;
|
|
382
|
+
/** Pre-teleport function for infinite carousels (from useCarouselTeleport) */
|
|
383
|
+
preTeleport?: (targetScroll: number) => number;
|
|
384
|
+
/** Callback when navigating to a new item */
|
|
385
|
+
onNavigate?: (targetScroll: number) => void;
|
|
386
|
+
/** Coordinator for state management (REQUIRED in Phase 2+) */
|
|
387
|
+
coordinator: UseCarouselCoordinatorReturn;
|
|
388
|
+
/** Optional logger for debugging */
|
|
389
|
+
logger?: CarouselLoggerInstance;
|
|
390
|
+
}
|
|
391
|
+
interface UseCarouselNavigationReturn {
|
|
392
|
+
/** Navigate left (previous item) */
|
|
393
|
+
scrollLeft: () => void;
|
|
394
|
+
/** Navigate right (next item) */
|
|
395
|
+
scrollRight: () => void;
|
|
396
|
+
/** Direct access to navigation handler */
|
|
397
|
+
handleScrollNav: (direction: -1 | 1) => void;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Hook that handles arrow navigation for carousels.
|
|
401
|
+
*
|
|
402
|
+
* Phase 2: Uses coordinator as single source of truth for state.
|
|
403
|
+
* No more external ref passing - all state is managed via coordinator.
|
|
404
|
+
*
|
|
405
|
+
* Features:
|
|
406
|
+
* - Rapid-click "Catch-Up & Advance" strategy for smooth multi-click navigation
|
|
407
|
+
* - Bounce animation at edges for finite carousels
|
|
408
|
+
* - Pre-teleport integration for infinite carousels
|
|
409
|
+
* - Scroll completion detection (scrollend or debounce fallback)
|
|
410
|
+
*/
|
|
411
|
+
declare function useCarouselNavigation({ containerRef, infinite, layout, cancelMomentum, preTeleport, onNavigate, coordinator, logger, }: UseCarouselNavigationOptions): UseCarouselNavigationReturn;
|
|
412
|
+
|
|
413
|
+
interface UseCarouselPersistenceOptions {
|
|
414
|
+
/** Unique key for this carousel's scroll position in sessionStorage */
|
|
415
|
+
persistKey?: string;
|
|
416
|
+
/** Debounce delay in ms for saving scroll position. Default: 150 */
|
|
417
|
+
debounceMs?: number;
|
|
418
|
+
}
|
|
419
|
+
interface UseCarouselPersistenceReturn {
|
|
420
|
+
/** Get saved scroll position from sessionStorage (null if not found) */
|
|
421
|
+
getSavedPosition: () => number | null;
|
|
422
|
+
/** Save current scroll position to sessionStorage (debounced) */
|
|
423
|
+
savePosition: (scrollLeft: number) => void;
|
|
424
|
+
/** Immediately save position (no debounce, for unmount) */
|
|
425
|
+
savePositionImmediate: (scrollLeft: number) => void;
|
|
426
|
+
/** Clear saved position from sessionStorage */
|
|
427
|
+
clearPosition: () => void;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Hook for persisting carousel scroll position across navigation.
|
|
431
|
+
*
|
|
432
|
+
* Uses sessionStorage so position survives:
|
|
433
|
+
* - Browser back/forward navigation
|
|
434
|
+
* - Same-tab navigation
|
|
435
|
+
*
|
|
436
|
+
* But resets on:
|
|
437
|
+
* - New tab/window
|
|
438
|
+
* - Browser close
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```tsx
|
|
442
|
+
* const { getSavedPosition, savePosition } = useCarouselPersistence({
|
|
443
|
+
* persistKey: 'homepage-featured'
|
|
444
|
+
* })
|
|
445
|
+
*
|
|
446
|
+
* // On mount
|
|
447
|
+
* useEffect(() => {
|
|
448
|
+
* const saved = getSavedPosition()
|
|
449
|
+
* if (saved !== null) containerRef.current.scrollLeft = saved
|
|
450
|
+
* }, [])
|
|
451
|
+
*
|
|
452
|
+
* // On scroll
|
|
453
|
+
* const handleScroll = () => savePosition(containerRef.current.scrollLeft)
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
declare function useCarouselPersistence({ persistKey, debounceMs, }?: UseCarouselPersistenceOptions): UseCarouselPersistenceReturn;
|
|
457
|
+
|
|
458
|
+
interface UseCarouselTeleportOptions {
|
|
459
|
+
/** Ref to the scrollable carousel container */
|
|
460
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
461
|
+
/** Whether this is an infinite carousel */
|
|
462
|
+
infinite: boolean;
|
|
463
|
+
/** Number of items in the original set */
|
|
464
|
+
itemsCount: number;
|
|
465
|
+
/** Width of each card in pixels */
|
|
466
|
+
cardWidth: number;
|
|
467
|
+
/** Gap between cards in pixels */
|
|
468
|
+
gap: number;
|
|
469
|
+
/** Number of buffer items before the original set */
|
|
470
|
+
bufferBeforeCount: number;
|
|
471
|
+
/** Function to apply visual effects after teleport */
|
|
472
|
+
applyVisuals: (el: HTMLElement, scrollLeft?: number) => void;
|
|
473
|
+
/** Function to adjust scroll tracking in useDraggableScroll */
|
|
474
|
+
adjustScroll: (delta: number) => void;
|
|
475
|
+
/** Delay in ms before clearing pre-teleport flag */
|
|
476
|
+
preTeleportClearDelayMs: number;
|
|
477
|
+
/** Coordinator for state management (required - Phase 3) */
|
|
478
|
+
coordinator: UseCarouselCoordinatorReturn;
|
|
479
|
+
/** Optional logger for debugging */
|
|
480
|
+
logger?: CarouselLoggerInstance;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Hook that handles the teleport logic for infinite carousels.
|
|
484
|
+
*
|
|
485
|
+
* THE TELEPORT LOOP - HYBRID STRATEGY
|
|
486
|
+
* Desktop (mouse): Teleport during scroll - works perfectly, no compositor conflict
|
|
487
|
+
* Mobile (touch):
|
|
488
|
+
* - Teleport on 'scrollend' - natural stop after momentum
|
|
489
|
+
* - Teleport on 'pointerdown' - "catch & reset" when user touches during momentum
|
|
490
|
+
*/
|
|
491
|
+
declare function useCarouselTeleport({ containerRef, infinite, itemsCount, cardWidth, gap, bufferBeforeCount, applyVisuals, adjustScroll, preTeleportClearDelayMs, coordinator, logger, }: UseCarouselTeleportOptions): {
|
|
492
|
+
/** Ref to track if current interaction is touch-based */
|
|
493
|
+
isTouchInteraction: react.MutableRefObject<boolean>;
|
|
494
|
+
/** Proactive pre-teleport for arrow navigation */
|
|
495
|
+
preTeleport: (targetScroll: number) => number;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
interface UseCarouselVisualsOptions {
|
|
499
|
+
/** Layout measurements */
|
|
500
|
+
layout: {
|
|
501
|
+
cardWidth: number;
|
|
502
|
+
gap: number;
|
|
503
|
+
};
|
|
504
|
+
/** Number of items (used to detect cache invalidation) */
|
|
505
|
+
itemsCount: number;
|
|
506
|
+
/** Buffer items before original set */
|
|
507
|
+
bufferBeforeCount: number;
|
|
508
|
+
/** Disable opacity effect */
|
|
509
|
+
disableOpacityEffect: boolean;
|
|
510
|
+
/** Disable scale effect */
|
|
511
|
+
disableScaleEffect: boolean;
|
|
512
|
+
/** Optional logger for debugging */
|
|
513
|
+
logger?: CarouselLoggerInstance;
|
|
514
|
+
}
|
|
515
|
+
interface ChildPosition {
|
|
516
|
+
left: number;
|
|
517
|
+
width: number;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Hook that manages visual effects for carousel items.
|
|
521
|
+
* Handles:
|
|
522
|
+
* - Position cache for children (offsetLeft, offsetWidth)
|
|
523
|
+
* - Container width cache (to avoid layout thrashing)
|
|
524
|
+
* - Apply visual effects (scale, opacity, shadow, z-index)
|
|
525
|
+
* - Viewport culling (skip items outside visible area)
|
|
526
|
+
*/
|
|
527
|
+
declare function useCarouselVisuals({ layout, itemsCount, bufferBeforeCount, disableOpacityEffect, disableScaleEffect, logger, }: UseCarouselVisualsOptions): {
|
|
528
|
+
/** Position cache for all children */
|
|
529
|
+
childrenPositions: react.MutableRefObject<ChildPosition[]>;
|
|
530
|
+
/** Whether the cache needs to be rebuilt */
|
|
531
|
+
isCacheDirty: react.MutableRefObject<boolean>;
|
|
532
|
+
/** Container width cache ref */
|
|
533
|
+
containerWidthRef: react.MutableRefObject<number>;
|
|
534
|
+
/** Whether container width needs to be remeasured */
|
|
535
|
+
isContainerWidthDirty: react.MutableRefObject<boolean>;
|
|
536
|
+
/** Update the position cache */
|
|
537
|
+
updateCache: (el: HTMLElement) => void;
|
|
538
|
+
/** Apply visual effects to visible items */
|
|
539
|
+
applyVisuals: (el: HTMLElement, overrideScrollLeft?: number) => void;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
interface UseDraggableScrollOptions {
|
|
543
|
+
infinite?: boolean;
|
|
544
|
+
hasNextPage?: boolean;
|
|
545
|
+
onEndReached?: () => void;
|
|
546
|
+
cardWidth?: number;
|
|
547
|
+
gap?: number;
|
|
548
|
+
cloneCount?: number;
|
|
549
|
+
friction?: number;
|
|
550
|
+
maxVelocity?: number;
|
|
551
|
+
}
|
|
552
|
+
declare function useDraggableScroll({ infinite, hasNextPage, onEndReached, cardWidth, gap, cloneCount, friction, maxVelocity, }?: UseDraggableScrollOptions): {
|
|
553
|
+
ref: react.RefObject<HTMLDivElement>;
|
|
554
|
+
isDragging: boolean;
|
|
555
|
+
cancelMomentum: () => void;
|
|
556
|
+
adjustScroll: (delta: number) => void;
|
|
557
|
+
events: {
|
|
558
|
+
onPointerDown: (e: PointerEvent<HTMLDivElement>) => void;
|
|
559
|
+
onPointerUp: (e: PointerEvent<HTMLDivElement>) => void;
|
|
560
|
+
onPointerMove: (e: PointerEvent<HTMLDivElement>) => void;
|
|
561
|
+
onLostPointerCapture: (e: PointerEvent<HTMLDivElement>) => void;
|
|
562
|
+
onClickCapture: (e: MouseEvent) => void;
|
|
563
|
+
onDragStart: (e: MouseEvent) => void;
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
interface UseLoadingStateOptions {
|
|
568
|
+
/** Unique key for session cache. If provided, enables caching behavior. */
|
|
569
|
+
cacheKey?: string;
|
|
570
|
+
/** Delay in ms before showing skeleton. Default: 500 */
|
|
571
|
+
skeletonDelay?: number;
|
|
572
|
+
/** Hard fallback timeout in ms. Guarantees isReady after this. Default: 3000 */
|
|
573
|
+
fallbackTimeout?: number;
|
|
574
|
+
/**
|
|
575
|
+
* If true, start in ready state immediately (e.g., for already-mounted carousels).
|
|
576
|
+
* Useful when the component should skip the loading phase entirely.
|
|
577
|
+
*/
|
|
578
|
+
startReady?: boolean;
|
|
579
|
+
}
|
|
580
|
+
interface UseLoadingStateReturn {
|
|
581
|
+
/** True when resource is loaded OR fallback has fired */
|
|
582
|
+
isReady: boolean;
|
|
583
|
+
/** True if loading takes longer than skeletonDelay AND resource is not cached */
|
|
584
|
+
showSkeleton: boolean;
|
|
585
|
+
/** True if resource was cached (instant load, no fade needed) */
|
|
586
|
+
isInstant: boolean;
|
|
587
|
+
/** Call this when your resource finishes loading (e.g., image onLoad) */
|
|
588
|
+
markReady: () => void;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Unified hook for managing loading states with:
|
|
592
|
+
* - First load: fade in, optional skeleton after delay
|
|
593
|
+
* - Cached load: instant (no fade, no skeleton)
|
|
594
|
+
* - Strict fallback: guaranteed ready after timeout
|
|
595
|
+
*/
|
|
596
|
+
declare function useLoadingState({ cacheKey, skeletonDelay, fallbackTimeout, startReady, }?: UseLoadingStateOptions): UseLoadingStateReturn;
|
|
597
|
+
/**
|
|
598
|
+
* Utility to clear the session cache (for testing)
|
|
599
|
+
*/
|
|
600
|
+
declare function clearLoadingStateCache(): void;
|
|
601
|
+
|
|
602
|
+
interface UseScrollCompletionOptions {
|
|
603
|
+
ref: React.RefObject<HTMLElement | null>;
|
|
604
|
+
onComplete: (source: string) => void;
|
|
605
|
+
/** Optional shared ref to track the listener for cleanup */
|
|
606
|
+
listenerRef?: React.MutableRefObject<(() => void) | null>;
|
|
607
|
+
/** Optional shared ref for the safety timeout */
|
|
608
|
+
timeoutRef?: React.MutableRefObject<NodeJS.Timeout | null>;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Hook to handle scroll completion detection across browsers.
|
|
612
|
+
* Uses native `scrollend` event where available, falls back to debounce pattern.
|
|
613
|
+
*/
|
|
614
|
+
declare function useScrollCompletion({ ref, onComplete, listenerRef, timeoutRef }: UseScrollCompletionOptions): {
|
|
615
|
+
waitForScrollCompletion: (timeoutDuration?: 300) => void;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
export { Carousel, type CarouselAction, CarouselArrow, type CarouselContext, CarouselLoggerInstance, type CarouselPhase, type ChannelConfig, type ChildPosition, DEBUG_CONFIG, type DebugChannel, FEATURE_FLAGS, LAYOUT_CONFIG, type LoggerConfig, TIMING_CONFIG, type UseCarouselCoordinatorOptions, type UseCarouselCoordinatorReturn, type UseCarouselLayoutOptions, type UseCarouselLayoutReturn, type UseCarouselNavigationOptions, type UseCarouselNavigationReturn, type UseCarouselPersistenceOptions, type UseCarouselPersistenceReturn, type UseCarouselTeleportOptions, type UseCarouselVisualsOptions, type UseLoadingStateOptions, type UseLoadingStateReturn, type UseScrollCompletionOptions, VISUAL_CONFIG, carouselLogger, clearLoadingStateCache, createLogger, measureLayoutFromElement, reduce, useCarouselCoordinator, useCarouselLayout, useCarouselNavigation, useCarouselPersistence, useCarouselTeleport, useCarouselVisuals, useDraggableScroll, useLoadingState, useScrollCompletion };
|