scroll-system 1.0.1 → 1.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/README.md +138 -3
- package/dist/components/AriaLiveRegion.d.ts +18 -0
- package/dist/components/AriaLiveRegion.d.ts.map +1 -0
- package/dist/components/ControlledView.d.ts +16 -0
- package/dist/components/ControlledView.d.ts.map +1 -0
- package/dist/components/FullView.d.ts +20 -0
- package/dist/components/FullView.d.ts.map +1 -0
- package/dist/components/LazyView.d.ts +33 -0
- package/dist/components/LazyView.d.ts.map +1 -0
- package/dist/components/NestedScrollView.d.ts +38 -0
- package/dist/components/NestedScrollView.d.ts.map +1 -0
- package/dist/components/ScrollContainer.d.ts +9 -0
- package/dist/components/ScrollContainer.d.ts.map +1 -0
- package/dist/components/ScrollDebugOverlay.d.ts +17 -0
- package/dist/components/ScrollDebugOverlay.d.ts.map +1 -0
- package/dist/components/ScrollLockedView.d.ts +10 -0
- package/dist/components/ScrollLockedView.d.ts.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +21 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useAutoScroll.d.ts +41 -0
- package/dist/hooks/useAutoScroll.d.ts.map +1 -0
- package/dist/hooks/useDragHandler.d.ts +28 -0
- package/dist/hooks/useDragHandler.d.ts.map +1 -0
- package/dist/hooks/useFocusManagement.d.ts +19 -0
- package/dist/hooks/useFocusManagement.d.ts.map +1 -0
- package/dist/hooks/useGestureConfig.d.ts +37 -0
- package/dist/hooks/useGestureConfig.d.ts.map +1 -0
- package/dist/hooks/useGlobalProgress.d.ts +36 -0
- package/dist/hooks/useGlobalProgress.d.ts.map +1 -0
- package/dist/hooks/useHashSync.d.ts +24 -0
- package/dist/hooks/useHashSync.d.ts.map +1 -0
- package/dist/hooks/useInfiniteScroll.d.ts +41 -0
- package/dist/hooks/useInfiniteScroll.d.ts.map +1 -0
- package/dist/hooks/useKeyboardHandler.d.ts +20 -0
- package/dist/hooks/useKeyboardHandler.d.ts.map +1 -0
- package/dist/hooks/useMetricsReporter.d.ts +23 -0
- package/dist/hooks/useMetricsReporter.d.ts.map +1 -0
- package/dist/hooks/useNavigation.d.ts +42 -0
- package/dist/hooks/useNavigation.d.ts.map +1 -0
- package/dist/hooks/useParallax.d.ts +33 -0
- package/dist/hooks/useParallax.d.ts.map +1 -0
- package/dist/hooks/usePreload.d.ts +31 -0
- package/dist/hooks/usePreload.d.ts.map +1 -0
- package/dist/hooks/useScrollAnalytics.d.ts +40 -0
- package/dist/hooks/useScrollAnalytics.d.ts.map +1 -0
- package/dist/hooks/useScrollLock.d.ts +40 -0
- package/dist/hooks/useScrollLock.d.ts.map +1 -0
- package/dist/hooks/useScrollSystem.d.ts +16 -0
- package/dist/hooks/useScrollSystem.d.ts.map +1 -0
- package/dist/hooks/useSnapPoints.d.ts +77 -0
- package/dist/hooks/useSnapPoints.d.ts.map +1 -0
- package/dist/hooks/useTouchHandler.d.ts +12 -0
- package/dist/hooks/useTouchHandler.d.ts.map +1 -0
- package/dist/hooks/useViewProgress.d.ts +22 -0
- package/dist/hooks/useViewProgress.d.ts.map +1 -0
- package/dist/hooks/useViewRegistration.d.ts +25 -0
- package/dist/hooks/useViewRegistration.d.ts.map +1 -0
- package/dist/hooks/useWheelHandler.d.ts +8 -0
- package/dist/hooks/useWheelHandler.d.ts.map +1 -0
- package/dist/index.d.ts +16 -2009
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +860 -284
- package/dist/index.mjs +799 -212
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/navigation.store.d.ts +23 -0
- package/dist/store/navigation.store.d.ts.map +1 -0
- package/dist/types/index.d.ts +315 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/index.d.ts +21 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/normalizeWheel.d.ts +10 -0
- package/dist/utils/normalizeWheel.d.ts.map +1 -0
- package/package.json +4 -2
- package/dist/index.d.mts +0 -2010
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { createContext, useRef, useEffect, useState, useCallback, useMemo, useContext } from 'react';
|
|
2
|
+
import { create } from 'zustand';
|
|
3
|
+
import { subscribeWithSelector } from 'zustand/middleware';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
5
|
|
|
4
|
-
//
|
|
5
|
-
import { create } from "zustand";
|
|
6
|
-
import { subscribeWithSelector } from "zustand/middleware";
|
|
6
|
+
// components/ScrollContainer.tsx
|
|
7
7
|
|
|
8
8
|
// constants.ts
|
|
9
9
|
var DEFAULT_TRANSITION_DURATION = 700;
|
|
@@ -22,6 +22,7 @@ function evaluateStateMachine(capability, progress, viewType, explicitLock) {
|
|
|
22
22
|
if (explicitLock) return explicitLock;
|
|
23
23
|
if (capability === "none") return "unlocked";
|
|
24
24
|
if (viewType === "full") return "unlocked";
|
|
25
|
+
if (viewType === "nested") return "unlocked";
|
|
25
26
|
if (progress >= 0.99) return "unlocked";
|
|
26
27
|
return "locked";
|
|
27
28
|
}
|
|
@@ -45,7 +46,12 @@ var initialState = {
|
|
|
45
46
|
isTransitioning: false,
|
|
46
47
|
isGlobalLocked: false,
|
|
47
48
|
isDragging: false,
|
|
48
|
-
globalProgress: 0
|
|
49
|
+
globalProgress: 0,
|
|
50
|
+
// NEW: AutoScroll state
|
|
51
|
+
isAutoScrolling: false,
|
|
52
|
+
isAutoScrollPaused: false,
|
|
53
|
+
// NEW: Infinite scroll
|
|
54
|
+
infiniteScrollEnabled: false
|
|
49
55
|
};
|
|
50
56
|
var lastNavigationTime = 0;
|
|
51
57
|
var useScrollStore = create()(
|
|
@@ -70,12 +76,15 @@ var useScrollStore = create()(
|
|
|
70
76
|
index: newIndex,
|
|
71
77
|
type: config.type,
|
|
72
78
|
isActive: newIndex === 0,
|
|
79
|
+
isPreloaded: newIndex <= 1,
|
|
80
|
+
// Preload first 2 views by default
|
|
73
81
|
capability: "none",
|
|
74
82
|
navigation: "unlocked",
|
|
75
83
|
explicitLock: null,
|
|
76
84
|
progress: 0,
|
|
77
85
|
metrics: { scrollHeight: 0, clientHeight: 0, scrollTop: 0 },
|
|
78
|
-
config
|
|
86
|
+
config,
|
|
87
|
+
activeSnapPointId: null
|
|
79
88
|
};
|
|
80
89
|
const newViews = [...state.views, newView];
|
|
81
90
|
return {
|
|
@@ -116,7 +125,10 @@ var useScrollStore = create()(
|
|
|
116
125
|
progress,
|
|
117
126
|
navigation
|
|
118
127
|
};
|
|
119
|
-
|
|
128
|
+
const activeView = newViews[state.activeIndex];
|
|
129
|
+
const viewProgress = activeView?.progress ?? 0;
|
|
130
|
+
const globalProgress = (state.activeIndex + viewProgress) / state.totalViews;
|
|
131
|
+
return { views: newViews, globalProgress };
|
|
120
132
|
});
|
|
121
133
|
},
|
|
122
134
|
processIntention: (intention) => {
|
|
@@ -127,10 +139,15 @@ var useScrollStore = create()(
|
|
|
127
139
|
if (intention.type === "navigate") {
|
|
128
140
|
if (intention.direction === "down") {
|
|
129
141
|
if (activeView.navigation === "locked") return false;
|
|
130
|
-
if (state.activeIndex
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
if (state.activeIndex >= state.totalViews - 1) {
|
|
143
|
+
if (state.infiniteScrollEnabled) {
|
|
144
|
+
get().goToView(0);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
133
148
|
}
|
|
149
|
+
get().goToView(state.activeIndex + 1);
|
|
150
|
+
return true;
|
|
134
151
|
} else if (intention.direction === "up") {
|
|
135
152
|
const isAtTop = activeView.metrics.scrollTop <= 1;
|
|
136
153
|
if (activeView.capability === "internal" && !isAtTop) {
|
|
@@ -144,21 +161,28 @@ var useScrollStore = create()(
|
|
|
144
161
|
} else {
|
|
145
162
|
if (activeView.explicitLock === "locked") return false;
|
|
146
163
|
}
|
|
147
|
-
if (state.activeIndex
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
if (state.activeIndex <= 0) {
|
|
165
|
+
if (state.infiniteScrollEnabled) {
|
|
166
|
+
get().goToView(state.totalViews - 1);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
150
170
|
}
|
|
171
|
+
get().goToView(state.activeIndex - 1);
|
|
172
|
+
return true;
|
|
151
173
|
}
|
|
152
174
|
}
|
|
153
175
|
return false;
|
|
154
176
|
},
|
|
155
177
|
goToNext: () => {
|
|
156
178
|
const state = get();
|
|
157
|
-
state.
|
|
179
|
+
const nextIndex = state.infiniteScrollEnabled && state.activeIndex >= state.totalViews - 1 ? 0 : state.activeIndex + 1;
|
|
180
|
+
state.goToView(nextIndex);
|
|
158
181
|
},
|
|
159
182
|
goToPrevious: () => {
|
|
160
183
|
const state = get();
|
|
161
|
-
state.
|
|
184
|
+
const prevIndex = state.infiniteScrollEnabled && state.activeIndex <= 0 ? state.totalViews - 1 : state.activeIndex - 1;
|
|
185
|
+
state.goToView(prevIndex);
|
|
162
186
|
},
|
|
163
187
|
goToView: (indexOrId) => {
|
|
164
188
|
const state = get();
|
|
@@ -166,19 +190,29 @@ var useScrollStore = create()(
|
|
|
166
190
|
if (now - lastNavigationTime < NAVIGATION_COOLDOWN) return;
|
|
167
191
|
lastNavigationTime = now;
|
|
168
192
|
let targetIndex = typeof indexOrId === "string" ? state.views.findIndex((v) => v.id === indexOrId) : indexOrId;
|
|
193
|
+
if (state.infiniteScrollEnabled) {
|
|
194
|
+
if (targetIndex < 0) targetIndex = state.totalViews - 1;
|
|
195
|
+
if (targetIndex >= state.totalViews) targetIndex = 0;
|
|
196
|
+
}
|
|
169
197
|
if (targetIndex < 0 || targetIndex >= state.totalViews) return;
|
|
170
198
|
if (targetIndex === state.activeIndex) return;
|
|
171
|
-
set((s) =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
set((s) => {
|
|
200
|
+
const newViews = s.views.map((v, idx) => {
|
|
201
|
+
const isAdjacent = Math.abs(idx - targetIndex) <= 1 || s.infiniteScrollEnabled && (targetIndex === 0 && idx === s.totalViews - 1 || targetIndex === s.totalViews - 1 && idx === 0);
|
|
202
|
+
return {
|
|
203
|
+
...v,
|
|
204
|
+
isActive: idx === targetIndex,
|
|
205
|
+
isPreloaded: isAdjacent || idx === targetIndex
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
...s,
|
|
210
|
+
isTransitioning: true,
|
|
211
|
+
activeIndex: targetIndex,
|
|
212
|
+
activeId: newViews[targetIndex]?.id ?? null,
|
|
213
|
+
views: newViews
|
|
214
|
+
};
|
|
215
|
+
});
|
|
182
216
|
},
|
|
183
217
|
setViewExplicitLock: (id, lock) => {
|
|
184
218
|
set((state) => {
|
|
@@ -193,9 +227,33 @@ var useScrollStore = create()(
|
|
|
193
227
|
},
|
|
194
228
|
setGlobalLock: (locked) => set({ isGlobalLocked: locked }),
|
|
195
229
|
setDragging: (dragging) => set({ isDragging: dragging }),
|
|
196
|
-
getViewById: (id) => get().views.find((v) => v.id === id),
|
|
197
230
|
startTransition: () => set({ isTransitioning: true }),
|
|
198
231
|
endTransition: () => set({ isTransitioning: false }),
|
|
232
|
+
// NEW: AutoScroll control
|
|
233
|
+
setAutoScrolling: (enabled) => set({ isAutoScrolling: enabled }),
|
|
234
|
+
setAutoScrollPaused: (paused) => set({ isAutoScrollPaused: paused }),
|
|
235
|
+
// NEW: Infinite scroll
|
|
236
|
+
setInfiniteScrollEnabled: (enabled) => set({ infiniteScrollEnabled: enabled }),
|
|
237
|
+
// NEW: Preload
|
|
238
|
+
setViewPreloaded: (id, preloaded) => {
|
|
239
|
+
set((state) => {
|
|
240
|
+
const index = state.views.findIndex((v) => v.id === id);
|
|
241
|
+
if (index === -1) return state;
|
|
242
|
+
const newViews = [...state.views];
|
|
243
|
+
newViews[index] = { ...newViews[index], isPreloaded: preloaded };
|
|
244
|
+
return { views: newViews };
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
// NEW: Snap points
|
|
248
|
+
setActiveSnapPoint: (viewId, snapPointId) => {
|
|
249
|
+
set((state) => {
|
|
250
|
+
const index = state.views.findIndex((v) => v.id === viewId);
|
|
251
|
+
if (index === -1) return state;
|
|
252
|
+
const newViews = [...state.views];
|
|
253
|
+
newViews[index] = { ...newViews[index], activeSnapPointId: snapPointId };
|
|
254
|
+
return { views: newViews };
|
|
255
|
+
});
|
|
256
|
+
},
|
|
199
257
|
resetNavigationCooldown: () => {
|
|
200
258
|
lastNavigationTime = 0;
|
|
201
259
|
}
|
|
@@ -207,15 +265,16 @@ var selectCanNavigateNext = (state) => {
|
|
|
207
265
|
if (state.isTransitioning || state.isGlobalLocked) return false;
|
|
208
266
|
const activeView = state.views[state.activeIndex];
|
|
209
267
|
if (!activeView) return false;
|
|
210
|
-
return activeView.navigation === "unlocked";
|
|
268
|
+
if (state.infiniteScrollEnabled) return activeView.navigation === "unlocked";
|
|
269
|
+
return activeView.navigation === "unlocked" && state.activeIndex < state.totalViews - 1;
|
|
211
270
|
};
|
|
212
271
|
var selectCanNavigatePrevious = (state) => {
|
|
213
272
|
if (state.isTransitioning || state.isGlobalLocked) return false;
|
|
273
|
+
if (state.infiniteScrollEnabled) return state.activeIndex >= 0;
|
|
214
274
|
return state.activeIndex > 0;
|
|
215
275
|
};
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
import { useEffect, useRef } from "react";
|
|
276
|
+
var selectGlobalProgress = (state) => state.globalProgress;
|
|
277
|
+
var selectIsAutoScrolling = (state) => state.isAutoScrolling && !state.isAutoScrollPaused;
|
|
219
278
|
|
|
220
279
|
// utils/normalizeWheel.ts
|
|
221
280
|
function normalizeWheel(event) {
|
|
@@ -254,7 +313,7 @@ function normalizeWheel(event) {
|
|
|
254
313
|
function useWheelHandler() {
|
|
255
314
|
const scrollAccumulator = useRef(0);
|
|
256
315
|
const lastScrollTime = useRef(0);
|
|
257
|
-
|
|
316
|
+
useScrollStore((s) => s.isTransitioning);
|
|
258
317
|
useEffect(() => {
|
|
259
318
|
const handleWheel = (event) => {
|
|
260
319
|
const state = useScrollStore.getState();
|
|
@@ -282,7 +341,6 @@ function useWheelHandler() {
|
|
|
282
341
|
if (handled) {
|
|
283
342
|
scrollAccumulator.current = 0;
|
|
284
343
|
event.preventDefault();
|
|
285
|
-
} else {
|
|
286
344
|
}
|
|
287
345
|
}
|
|
288
346
|
};
|
|
@@ -290,14 +348,11 @@ function useWheelHandler() {
|
|
|
290
348
|
return () => window.removeEventListener("wheel", handleWheel);
|
|
291
349
|
}, []);
|
|
292
350
|
}
|
|
293
|
-
|
|
294
|
-
// hooks/useTouchHandler.ts
|
|
295
|
-
import { useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
296
351
|
function useTouchHandler(options = {}) {
|
|
297
352
|
const { enabled = true } = options;
|
|
298
|
-
const touchStart =
|
|
299
|
-
const touchStartTime =
|
|
300
|
-
|
|
353
|
+
const touchStart = useRef(null);
|
|
354
|
+
const touchStartTime = useRef(0);
|
|
355
|
+
useEffect(() => {
|
|
301
356
|
if (!enabled) return;
|
|
302
357
|
const handleTouchStart = (e) => {
|
|
303
358
|
touchStart.current = {
|
|
@@ -338,12 +393,9 @@ function useTouchHandler(options = {}) {
|
|
|
338
393
|
};
|
|
339
394
|
}, [enabled]);
|
|
340
395
|
}
|
|
341
|
-
|
|
342
|
-
// hooks/useKeyboardHandler.ts
|
|
343
|
-
import { useEffect as useEffect3 } from "react";
|
|
344
396
|
function useKeyboardHandler(options = {}) {
|
|
345
397
|
const { enabled = true, preventDefault = true } = options;
|
|
346
|
-
|
|
398
|
+
useEffect(() => {
|
|
347
399
|
if (!enabled) return;
|
|
348
400
|
const handleKeyDown = (e) => {
|
|
349
401
|
const target = e.target;
|
|
@@ -401,13 +453,10 @@ function useKeyboardHandler(options = {}) {
|
|
|
401
453
|
};
|
|
402
454
|
}, [enabled, preventDefault]);
|
|
403
455
|
}
|
|
404
|
-
|
|
405
|
-
// hooks/useHashSync.ts
|
|
406
|
-
import { useEffect as useEffect4, useRef as useRef3 } from "react";
|
|
407
456
|
function useHashSync(options = {}) {
|
|
408
457
|
const { enabled = true, pushHistory = false, hashPrefix = "" } = options;
|
|
409
|
-
const hasInitialized =
|
|
410
|
-
|
|
458
|
+
const hasInitialized = useRef(false);
|
|
459
|
+
useEffect(() => {
|
|
411
460
|
if (!enabled) return;
|
|
412
461
|
const unsubscribe = useScrollStore.subscribe(
|
|
413
462
|
(state) => state.activeIndex,
|
|
@@ -428,7 +477,7 @@ function useHashSync(options = {}) {
|
|
|
428
477
|
);
|
|
429
478
|
return () => unsubscribe();
|
|
430
479
|
}, [enabled, pushHistory, hashPrefix]);
|
|
431
|
-
|
|
480
|
+
useEffect(() => {
|
|
432
481
|
if (!enabled) return;
|
|
433
482
|
const handlePopState = () => {
|
|
434
483
|
const hash = window.location.hash.slice(1);
|
|
@@ -443,7 +492,7 @@ function useHashSync(options = {}) {
|
|
|
443
492
|
window.addEventListener("popstate", handlePopState);
|
|
444
493
|
return () => window.removeEventListener("popstate", handlePopState);
|
|
445
494
|
}, [enabled, hashPrefix]);
|
|
446
|
-
|
|
495
|
+
useEffect(() => {
|
|
447
496
|
if (!enabled) return;
|
|
448
497
|
const checkAndNavigate = () => {
|
|
449
498
|
const state = useScrollStore.getState();
|
|
@@ -466,9 +515,6 @@ function useHashSync(options = {}) {
|
|
|
466
515
|
checkAndNavigate();
|
|
467
516
|
}, [enabled, hashPrefix]);
|
|
468
517
|
}
|
|
469
|
-
|
|
470
|
-
// hooks/useDragHandler.ts
|
|
471
|
-
import { useRef as useRef4, useEffect as useEffect5, useCallback, useState } from "react";
|
|
472
518
|
var DRAG_THRESHOLD = 50;
|
|
473
519
|
var VELOCITY_THRESHOLD = 0.5;
|
|
474
520
|
var RESISTANCE_FACTOR = 0.4;
|
|
@@ -479,9 +525,9 @@ function useDragHandler(options = {}) {
|
|
|
479
525
|
dragOffset: 0,
|
|
480
526
|
dragDirection: null
|
|
481
527
|
});
|
|
482
|
-
const touchStartRef =
|
|
483
|
-
const lastMoveRef =
|
|
484
|
-
const rafRef =
|
|
528
|
+
const touchStartRef = useRef(null);
|
|
529
|
+
const lastMoveRef = useRef(null);
|
|
530
|
+
const rafRef = useRef(null);
|
|
485
531
|
const updateDragState = useCallback((newState) => {
|
|
486
532
|
setDragState((prev) => {
|
|
487
533
|
const updated = { ...prev, ...newState };
|
|
@@ -489,7 +535,7 @@ function useDragHandler(options = {}) {
|
|
|
489
535
|
return updated;
|
|
490
536
|
});
|
|
491
537
|
}, [onDragUpdate]);
|
|
492
|
-
|
|
538
|
+
useEffect(() => {
|
|
493
539
|
if (!enabled) return;
|
|
494
540
|
const handleTouchStart = (e) => {
|
|
495
541
|
const target = e.target;
|
|
@@ -575,16 +621,13 @@ function useDragHandler(options = {}) {
|
|
|
575
621
|
}, [enabled, updateDragState, onDragEnd]);
|
|
576
622
|
return dragState;
|
|
577
623
|
}
|
|
578
|
-
|
|
579
|
-
// hooks/useFocusManagement.ts
|
|
580
|
-
import { useEffect as useEffect6, useRef as useRef5 } from "react";
|
|
581
624
|
function useFocusManagement(options = {}) {
|
|
582
625
|
const { enabled = true, focusDelay = 100 } = options;
|
|
583
626
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
584
627
|
const activeId = useScrollStore((s) => s.activeId);
|
|
585
628
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
586
|
-
const prevIndexRef =
|
|
587
|
-
|
|
629
|
+
const prevIndexRef = useRef(activeIndex);
|
|
630
|
+
useEffect(() => {
|
|
588
631
|
if (!enabled) return;
|
|
589
632
|
if (activeIndex !== prevIndexRef.current && !isTransitioning && activeId) {
|
|
590
633
|
const timer = setTimeout(() => {
|
|
@@ -601,9 +644,6 @@ function useFocusManagement(options = {}) {
|
|
|
601
644
|
}
|
|
602
645
|
}, [activeIndex, activeId, isTransitioning, enabled, focusDelay]);
|
|
603
646
|
}
|
|
604
|
-
|
|
605
|
-
// hooks/useScrollSystem.ts
|
|
606
|
-
import { useCallback as useCallback2, useMemo } from "react";
|
|
607
647
|
function useScrollSystem() {
|
|
608
648
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
609
649
|
const globalProgress = useScrollStore((s) => s.globalProgress);
|
|
@@ -623,24 +663,24 @@ function useScrollSystem() {
|
|
|
623
663
|
const activeViewProgress = activeView?.progress ?? 0;
|
|
624
664
|
const canNavigateNext = useScrollStore(selectCanNavigateNext);
|
|
625
665
|
const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
|
|
626
|
-
const goToNext =
|
|
666
|
+
const goToNext = useCallback(() => {
|
|
627
667
|
if (canNavigateNext) {
|
|
628
668
|
storeNext();
|
|
629
669
|
return true;
|
|
630
670
|
}
|
|
631
671
|
return false;
|
|
632
672
|
}, [canNavigateNext, storeNext]);
|
|
633
|
-
const goToPrev =
|
|
673
|
+
const goToPrev = useCallback(() => {
|
|
634
674
|
if (canNavigatePrevious) {
|
|
635
675
|
storePrev();
|
|
636
676
|
return true;
|
|
637
677
|
}
|
|
638
678
|
return false;
|
|
639
679
|
}, [canNavigatePrevious, storePrev]);
|
|
640
|
-
const getCurrentIndex =
|
|
641
|
-
const getProgress =
|
|
642
|
-
const getActiveViewProgress =
|
|
643
|
-
const isLocked =
|
|
680
|
+
const getCurrentIndex = useCallback(() => activeIndex, [activeIndex]);
|
|
681
|
+
const getProgress = useCallback(() => globalProgress, [globalProgress]);
|
|
682
|
+
const getActiveViewProgress = useCallback(() => activeViewProgress, [activeViewProgress]);
|
|
683
|
+
const isLocked = useCallback(() => isGlobalLocked || isTransitioning || !canNavigateNext, [isGlobalLocked, isTransitioning, canNavigateNext]);
|
|
644
684
|
return useMemo(() => ({
|
|
645
685
|
goToNext,
|
|
646
686
|
goToPrev,
|
|
@@ -677,6 +717,255 @@ function useScrollSystem() {
|
|
|
677
717
|
isTransitioning
|
|
678
718
|
]);
|
|
679
719
|
}
|
|
720
|
+
function useGlobalProgress(options = {}) {
|
|
721
|
+
const { onProgress, throttle: throttle2 = 16 } = options;
|
|
722
|
+
const progress = useScrollStore(selectGlobalProgress);
|
|
723
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
724
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
if (!onProgress) return;
|
|
727
|
+
let lastCall = 0;
|
|
728
|
+
const now = Date.now();
|
|
729
|
+
if (now - lastCall >= throttle2) {
|
|
730
|
+
lastCall = now;
|
|
731
|
+
onProgress(progress);
|
|
732
|
+
}
|
|
733
|
+
}, [progress, onProgress, throttle2]);
|
|
734
|
+
return {
|
|
735
|
+
progress,
|
|
736
|
+
activeIndex,
|
|
737
|
+
totalViews,
|
|
738
|
+
percentage: Math.round(progress * 100)
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
var DEFAULT_CONFIG = {
|
|
742
|
+
enabled: false,
|
|
743
|
+
interval: 5e3,
|
|
744
|
+
pauseOnInteraction: true,
|
|
745
|
+
resumeDelay: 3e3,
|
|
746
|
+
direction: "forward",
|
|
747
|
+
stopAtEnd: false
|
|
748
|
+
};
|
|
749
|
+
function useAutoScroll(config) {
|
|
750
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
751
|
+
const {
|
|
752
|
+
enabled,
|
|
753
|
+
interval,
|
|
754
|
+
pauseOnInteraction,
|
|
755
|
+
resumeDelay,
|
|
756
|
+
direction,
|
|
757
|
+
stopAtEnd
|
|
758
|
+
} = mergedConfig;
|
|
759
|
+
const intervalRef = useRef(null);
|
|
760
|
+
const resumeTimeoutRef = useRef(null);
|
|
761
|
+
const isAutoScrolling = useScrollStore((s) => s.isAutoScrolling);
|
|
762
|
+
const isAutoScrollPaused = useScrollStore((s) => s.isAutoScrollPaused);
|
|
763
|
+
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
764
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
765
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
766
|
+
const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
|
|
767
|
+
const isDragging = useScrollStore((s) => s.isDragging);
|
|
768
|
+
const setAutoScrolling = useScrollStore((s) => s.setAutoScrolling);
|
|
769
|
+
const setAutoScrollPaused = useScrollStore((s) => s.setAutoScrollPaused);
|
|
770
|
+
const goToNext = useScrollStore((s) => s.goToNext);
|
|
771
|
+
const goToPrevious = useScrollStore((s) => s.goToPrevious);
|
|
772
|
+
const pause = useCallback(() => {
|
|
773
|
+
setAutoScrollPaused(true);
|
|
774
|
+
if (resumeTimeoutRef.current) {
|
|
775
|
+
clearTimeout(resumeTimeoutRef.current);
|
|
776
|
+
resumeTimeoutRef.current = null;
|
|
777
|
+
}
|
|
778
|
+
}, [setAutoScrollPaused]);
|
|
779
|
+
const resume = useCallback(() => {
|
|
780
|
+
setAutoScrollPaused(false);
|
|
781
|
+
}, [setAutoScrollPaused]);
|
|
782
|
+
const toggle = useCallback(() => {
|
|
783
|
+
if (isAutoScrollPaused) {
|
|
784
|
+
resume();
|
|
785
|
+
} else {
|
|
786
|
+
pause();
|
|
787
|
+
}
|
|
788
|
+
}, [isAutoScrollPaused, pause, resume]);
|
|
789
|
+
const reset = useCallback(() => {
|
|
790
|
+
if (intervalRef.current) {
|
|
791
|
+
clearInterval(intervalRef.current);
|
|
792
|
+
intervalRef.current = null;
|
|
793
|
+
}
|
|
794
|
+
}, []);
|
|
795
|
+
useEffect(() => {
|
|
796
|
+
if (!enabled || !pauseOnInteraction) return;
|
|
797
|
+
if (isDragging || isTransitioning) {
|
|
798
|
+
pause();
|
|
799
|
+
if (resumeTimeoutRef.current) {
|
|
800
|
+
clearTimeout(resumeTimeoutRef.current);
|
|
801
|
+
}
|
|
802
|
+
resumeTimeoutRef.current = setTimeout(() => {
|
|
803
|
+
if (enabled) {
|
|
804
|
+
resume();
|
|
805
|
+
}
|
|
806
|
+
}, resumeDelay);
|
|
807
|
+
}
|
|
808
|
+
return () => {
|
|
809
|
+
if (resumeTimeoutRef.current) {
|
|
810
|
+
clearTimeout(resumeTimeoutRef.current);
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}, [isDragging, isTransitioning, enabled, pauseOnInteraction, resumeDelay, pause, resume]);
|
|
814
|
+
useEffect(() => {
|
|
815
|
+
setAutoScrolling(enabled);
|
|
816
|
+
setAutoScrollPaused(false);
|
|
817
|
+
return () => {
|
|
818
|
+
setAutoScrolling(false);
|
|
819
|
+
};
|
|
820
|
+
}, [enabled, setAutoScrolling, setAutoScrollPaused]);
|
|
821
|
+
useEffect(() => {
|
|
822
|
+
if (!enabled || isAutoScrollPaused || isTransitioning) {
|
|
823
|
+
reset();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (stopAtEnd && !infiniteScrollEnabled) {
|
|
827
|
+
if (direction === "forward" && activeIndex >= totalViews - 1) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (direction === "backward" && activeIndex <= 0) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
intervalRef.current = setInterval(() => {
|
|
835
|
+
if (direction === "forward") {
|
|
836
|
+
goToNext();
|
|
837
|
+
} else {
|
|
838
|
+
goToPrevious();
|
|
839
|
+
}
|
|
840
|
+
}, interval);
|
|
841
|
+
return () => {
|
|
842
|
+
reset();
|
|
843
|
+
};
|
|
844
|
+
}, [
|
|
845
|
+
enabled,
|
|
846
|
+
isAutoScrollPaused,
|
|
847
|
+
isTransitioning,
|
|
848
|
+
interval,
|
|
849
|
+
direction,
|
|
850
|
+
stopAtEnd,
|
|
851
|
+
activeIndex,
|
|
852
|
+
totalViews,
|
|
853
|
+
infiniteScrollEnabled,
|
|
854
|
+
goToNext,
|
|
855
|
+
goToPrevious,
|
|
856
|
+
reset
|
|
857
|
+
]);
|
|
858
|
+
return {
|
|
859
|
+
isPlaying: isAutoScrolling && !isAutoScrollPaused,
|
|
860
|
+
isPaused: isAutoScrollPaused,
|
|
861
|
+
pause,
|
|
862
|
+
resume,
|
|
863
|
+
toggle,
|
|
864
|
+
reset
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
var DEFAULT_CONFIG2 = {
|
|
868
|
+
enabled: false,
|
|
869
|
+
loopDirection: "both"
|
|
870
|
+
};
|
|
871
|
+
function useInfiniteScroll(config = false) {
|
|
872
|
+
const normalizedConfig = typeof config === "boolean" ? { ...DEFAULT_CONFIG2, enabled: config } : { ...DEFAULT_CONFIG2, ...config };
|
|
873
|
+
const { enabled, loopDirection } = normalizedConfig;
|
|
874
|
+
const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
|
|
875
|
+
const setInfiniteScrollEnabled = useScrollStore((s) => s.setInfiniteScrollEnabled);
|
|
876
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
877
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
878
|
+
useEffect(() => {
|
|
879
|
+
setInfiniteScrollEnabled(enabled);
|
|
880
|
+
return () => {
|
|
881
|
+
};
|
|
882
|
+
}, [enabled, setInfiniteScrollEnabled]);
|
|
883
|
+
const enable = () => setInfiniteScrollEnabled(true);
|
|
884
|
+
const disable = () => setInfiniteScrollEnabled(false);
|
|
885
|
+
const toggle = () => setInfiniteScrollEnabled(!infiniteScrollEnabled);
|
|
886
|
+
const canLoopForward = infiniteScrollEnabled && (loopDirection === "forward" || loopDirection === "both") && activeIndex === totalViews - 1;
|
|
887
|
+
const canLoopBackward = infiniteScrollEnabled && (loopDirection === "backward" || loopDirection === "both") && activeIndex === 0;
|
|
888
|
+
return {
|
|
889
|
+
isEnabled: infiniteScrollEnabled,
|
|
890
|
+
enable,
|
|
891
|
+
disable,
|
|
892
|
+
toggle,
|
|
893
|
+
canLoopForward,
|
|
894
|
+
canLoopBackward
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
var DEFAULT_CONFIG3 = {
|
|
898
|
+
ahead: 1,
|
|
899
|
+
behind: 1,
|
|
900
|
+
delay: 100
|
|
901
|
+
};
|
|
902
|
+
function usePreload(config = {}) {
|
|
903
|
+
const mergedConfig = { ...DEFAULT_CONFIG3, ...config };
|
|
904
|
+
const { ahead, behind, delay } = mergedConfig;
|
|
905
|
+
const views = useScrollStore((s) => s.views);
|
|
906
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
907
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
908
|
+
const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
|
|
909
|
+
const setViewPreloaded = useScrollStore((s) => s.setViewPreloaded);
|
|
910
|
+
const preloadedViewIds = useMemo(() => {
|
|
911
|
+
const ids = [];
|
|
912
|
+
for (let i = -behind; i <= ahead; i++) {
|
|
913
|
+
let targetIndex = activeIndex + i;
|
|
914
|
+
if (infiniteScrollEnabled) {
|
|
915
|
+
if (targetIndex < 0) targetIndex = totalViews + targetIndex;
|
|
916
|
+
if (targetIndex >= totalViews) targetIndex = targetIndex - totalViews;
|
|
917
|
+
}
|
|
918
|
+
if (targetIndex >= 0 && targetIndex < totalViews) {
|
|
919
|
+
const view = views[targetIndex];
|
|
920
|
+
if (view) {
|
|
921
|
+
ids.push(view.id);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return ids;
|
|
926
|
+
}, [activeIndex, ahead, behind, views, totalViews, infiniteScrollEnabled]);
|
|
927
|
+
useEffect(() => {
|
|
928
|
+
const timer = setTimeout(() => {
|
|
929
|
+
views.forEach((view) => {
|
|
930
|
+
const shouldBePreloaded = preloadedViewIds.includes(view.id);
|
|
931
|
+
if (view.isPreloaded !== shouldBePreloaded) {
|
|
932
|
+
setViewPreloaded(view.id, shouldBePreloaded);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}, delay);
|
|
936
|
+
return () => clearTimeout(timer);
|
|
937
|
+
}, [preloadedViewIds, views, setViewPreloaded, delay]);
|
|
938
|
+
const shouldPreload = (viewId) => {
|
|
939
|
+
return preloadedViewIds.includes(viewId);
|
|
940
|
+
};
|
|
941
|
+
const isPreloaded = (viewId) => {
|
|
942
|
+
const view = views.find((v) => v.id === viewId);
|
|
943
|
+
return view?.isPreloaded ?? false;
|
|
944
|
+
};
|
|
945
|
+
return {
|
|
946
|
+
shouldPreload,
|
|
947
|
+
preloadedViewIds,
|
|
948
|
+
isPreloaded
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
var DEFAULT_GESTURE_CONFIG = {
|
|
952
|
+
swipeThreshold: 50,
|
|
953
|
+
swipeVelocity: 0.5,
|
|
954
|
+
dragResistance: 0.3,
|
|
955
|
+
enableWheel: true,
|
|
956
|
+
enableTouch: true,
|
|
957
|
+
enableKeyboard: true
|
|
958
|
+
};
|
|
959
|
+
var GestureConfigContext = createContext(DEFAULT_GESTURE_CONFIG);
|
|
960
|
+
function useGestureConfig() {
|
|
961
|
+
return useContext(GestureConfigContext);
|
|
962
|
+
}
|
|
963
|
+
function mergeGestureConfig(config) {
|
|
964
|
+
return useMemo(() => ({
|
|
965
|
+
...DEFAULT_GESTURE_CONFIG,
|
|
966
|
+
...config
|
|
967
|
+
}), [config]);
|
|
968
|
+
}
|
|
680
969
|
|
|
681
970
|
// utils/index.ts
|
|
682
971
|
function throttle(fn, limit) {
|
|
@@ -705,9 +994,6 @@ function prefersReducedMotion() {
|
|
|
705
994
|
if (typeof window === "undefined") return false;
|
|
706
995
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
707
996
|
}
|
|
708
|
-
|
|
709
|
-
// components/ScrollContainer.tsx
|
|
710
|
-
import { jsx } from "react/jsx-runtime";
|
|
711
997
|
function ScrollContainer({
|
|
712
998
|
children,
|
|
713
999
|
className = "",
|
|
@@ -725,12 +1011,20 @@ function ScrollContainer({
|
|
|
725
1011
|
// Touch Physics
|
|
726
1012
|
enableDragPhysics = false,
|
|
727
1013
|
// Layout
|
|
728
|
-
orientation = "vertical"
|
|
1014
|
+
orientation = "vertical",
|
|
1015
|
+
// NEW: v1.1.0 Features
|
|
1016
|
+
skipInitialAnimation = false,
|
|
1017
|
+
onProgress,
|
|
1018
|
+
gestureConfig,
|
|
1019
|
+
autoScroll,
|
|
1020
|
+
infiniteScroll = false,
|
|
1021
|
+
preload = true
|
|
729
1022
|
}) {
|
|
730
|
-
const containerRef =
|
|
1023
|
+
const containerRef = useRef(null);
|
|
1024
|
+
const isFirstRender = useRef(true);
|
|
731
1025
|
const isBrowser = typeof window !== "undefined";
|
|
732
|
-
const [reducedMotion, setReducedMotion] =
|
|
733
|
-
|
|
1026
|
+
const [reducedMotion, setReducedMotion] = useState(false);
|
|
1027
|
+
useEffect(() => {
|
|
734
1028
|
if (!isBrowser || !respectReducedMotion) return;
|
|
735
1029
|
setReducedMotion(prefersReducedMotion());
|
|
736
1030
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
@@ -741,9 +1035,9 @@ function ScrollContainer({
|
|
|
741
1035
|
const effectiveDuration = reducedMotion ? 0 : transitionDuration;
|
|
742
1036
|
const { initialize, endTransition } = useScrollStore();
|
|
743
1037
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const prevIndexRef =
|
|
1038
|
+
useScrollStore((s) => s.totalViews);
|
|
1039
|
+
useScrollStore((s) => s.isInitialized);
|
|
1040
|
+
const prevIndexRef = useRef(activeIndex);
|
|
747
1041
|
useWheelHandler();
|
|
748
1042
|
useTouchHandler({ enabled: !enableDragPhysics });
|
|
749
1043
|
useKeyboardHandler();
|
|
@@ -756,14 +1050,39 @@ function ScrollContainer({
|
|
|
756
1050
|
hashPrefix
|
|
757
1051
|
});
|
|
758
1052
|
useFocusManagement({ enabled: enableFocusManagement });
|
|
759
|
-
|
|
1053
|
+
useGlobalProgress({ onProgress });
|
|
1054
|
+
useInfiniteScroll(infiniteScroll);
|
|
1055
|
+
const autoScrollState = useAutoScroll(
|
|
1056
|
+
autoScroll ?? { enabled: false, interval: 5e3 }
|
|
1057
|
+
);
|
|
1058
|
+
const preloadConfig = typeof preload === "boolean" ? preload ? { ahead: 1, behind: 1 } : void 0 : preload;
|
|
1059
|
+
usePreload(preloadConfig ?? {});
|
|
1060
|
+
const mergedGestureConfig = mergeGestureConfig(gestureConfig);
|
|
1061
|
+
useEffect(() => {
|
|
760
1062
|
const timer = setTimeout(() => {
|
|
761
1063
|
initialize();
|
|
762
1064
|
onInitialized?.();
|
|
1065
|
+
isFirstRender.current = false;
|
|
763
1066
|
}, 50);
|
|
764
1067
|
return () => clearTimeout(timer);
|
|
765
1068
|
}, [initialize, onInitialized]);
|
|
766
|
-
|
|
1069
|
+
useEffect(() => {
|
|
1070
|
+
if (!isBrowser) return;
|
|
1071
|
+
const html = document.documentElement;
|
|
1072
|
+
const body = document.body;
|
|
1073
|
+
const originalHtmlOverscroll = html.style.overscrollBehavior;
|
|
1074
|
+
const originalBodyOverscroll = body.style.overscrollBehavior;
|
|
1075
|
+
const originalTouchAction = body.style.touchAction;
|
|
1076
|
+
html.style.overscrollBehavior = "none";
|
|
1077
|
+
body.style.overscrollBehavior = "none";
|
|
1078
|
+
body.style.touchAction = "pan-x pan-y";
|
|
1079
|
+
return () => {
|
|
1080
|
+
html.style.overscrollBehavior = originalHtmlOverscroll;
|
|
1081
|
+
body.style.overscrollBehavior = originalBodyOverscroll;
|
|
1082
|
+
body.style.touchAction = originalTouchAction;
|
|
1083
|
+
};
|
|
1084
|
+
}, [isBrowser]);
|
|
1085
|
+
useEffect(() => {
|
|
767
1086
|
if (prevIndexRef.current !== activeIndex) {
|
|
768
1087
|
onViewChange?.(prevIndexRef.current, activeIndex);
|
|
769
1088
|
const timer = setTimeout(() => {
|
|
@@ -773,37 +1092,34 @@ function ScrollContainer({
|
|
|
773
1092
|
return () => clearTimeout(timer);
|
|
774
1093
|
}
|
|
775
1094
|
}, [activeIndex, effectiveDuration, onViewChange, endTransition]);
|
|
776
|
-
const wrapperStyle =
|
|
1095
|
+
const wrapperStyle = useMemo(() => {
|
|
777
1096
|
const baseOffset = activeIndex * 100;
|
|
778
1097
|
const dragOffset = dragState.isDragging ? dragState.dragOffset * 100 : 0;
|
|
779
1098
|
const transformAxis = orientation === "horizontal" ? "X" : "Y";
|
|
780
1099
|
const sizeUnit = orientation === "horizontal" ? "vw" : "vh";
|
|
1100
|
+
const shouldAnimate = !skipInitialAnimation || !isFirstRender.current;
|
|
1101
|
+
const transitionValue = dragState.isDragging ? "none" : shouldAnimate && effectiveDuration > 0 ? `transform ${effectiveDuration}ms ${transitionEasing}` : "none";
|
|
781
1102
|
return {
|
|
782
1103
|
transform: `translate${transformAxis}(-${baseOffset + dragOffset}${sizeUnit})`,
|
|
783
|
-
transition:
|
|
1104
|
+
transition: transitionValue,
|
|
784
1105
|
height: "100%",
|
|
785
1106
|
width: "100%",
|
|
786
1107
|
display: orientation === "horizontal" ? "flex" : "block",
|
|
787
1108
|
flexDirection: orientation === "horizontal" ? "row" : void 0
|
|
788
1109
|
};
|
|
789
|
-
}, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation]);
|
|
790
|
-
return /* @__PURE__ */ jsx(
|
|
1110
|
+
}, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation, skipInitialAnimation]);
|
|
1111
|
+
return /* @__PURE__ */ jsx(GestureConfigContext.Provider, { value: mergedGestureConfig, children: /* @__PURE__ */ jsx(
|
|
791
1112
|
"div",
|
|
792
1113
|
{
|
|
793
1114
|
ref: containerRef,
|
|
794
1115
|
className: `scroll-container fixed inset-0 overflow-hidden w-screen h-screen ${className}`,
|
|
795
1116
|
role: "main",
|
|
796
1117
|
"aria-label": "Scroll container",
|
|
1118
|
+
"data-auto-scrolling": autoScrollState.isPlaying,
|
|
797
1119
|
children: /* @__PURE__ */ jsx("div", { className: "scroll-wrapper", style: wrapperStyle, children })
|
|
798
1120
|
}
|
|
799
|
-
);
|
|
1121
|
+
) });
|
|
800
1122
|
}
|
|
801
|
-
|
|
802
|
-
// components/FullView.tsx
|
|
803
|
-
import { useMemo as useMemo3 } from "react";
|
|
804
|
-
|
|
805
|
-
// hooks/useViewRegistration.ts
|
|
806
|
-
import { useEffect as useEffect8, useRef as useRef7 } from "react";
|
|
807
1123
|
function useViewRegistration({
|
|
808
1124
|
config,
|
|
809
1125
|
onActivate,
|
|
@@ -817,7 +1133,7 @@ function useViewRegistration({
|
|
|
817
1133
|
const unregisterView = useScrollStore((s) => s.unregisterView);
|
|
818
1134
|
const activeId = useScrollStore((s) => s.activeId);
|
|
819
1135
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
820
|
-
const callbacksRef =
|
|
1136
|
+
const callbacksRef = useRef({
|
|
821
1137
|
onActivate,
|
|
822
1138
|
onDeactivate,
|
|
823
1139
|
onEnterStart,
|
|
@@ -833,20 +1149,20 @@ function useViewRegistration({
|
|
|
833
1149
|
onExitStart,
|
|
834
1150
|
onExitEnd
|
|
835
1151
|
};
|
|
836
|
-
const wasActiveRef =
|
|
837
|
-
const wasTransitioningRef =
|
|
838
|
-
const configRef =
|
|
1152
|
+
const wasActiveRef = useRef(false);
|
|
1153
|
+
const wasTransitioningRef = useRef(false);
|
|
1154
|
+
const configRef = useRef(config);
|
|
839
1155
|
if (JSON.stringify(config) !== JSON.stringify(configRef.current)) {
|
|
840
1156
|
configRef.current = config;
|
|
841
1157
|
}
|
|
842
|
-
|
|
1158
|
+
useEffect(() => {
|
|
843
1159
|
const currentConfig = configRef.current;
|
|
844
1160
|
registerView(currentConfig);
|
|
845
1161
|
return () => unregisterView(currentConfig.id);
|
|
846
1162
|
}, [registerView, unregisterView, configRef.current]);
|
|
847
1163
|
const viewState = useScrollStore((s) => s.views.find((v) => v.id === config.id));
|
|
848
1164
|
const isActive = activeId === config.id;
|
|
849
|
-
|
|
1165
|
+
useEffect(() => {
|
|
850
1166
|
if (isActive && !wasActiveRef.current) {
|
|
851
1167
|
callbacksRef.current.onActivate?.();
|
|
852
1168
|
} else if (!isActive && wasActiveRef.current) {
|
|
@@ -854,7 +1170,7 @@ function useViewRegistration({
|
|
|
854
1170
|
}
|
|
855
1171
|
wasActiveRef.current = isActive;
|
|
856
1172
|
}, [isActive]);
|
|
857
|
-
|
|
1173
|
+
useEffect(() => {
|
|
858
1174
|
const callbacks = callbacksRef.current;
|
|
859
1175
|
const wasActive = wasActiveRef.current;
|
|
860
1176
|
const wasTransitioning = wasTransitioningRef.current;
|
|
@@ -882,9 +1198,6 @@ function useViewRegistration({
|
|
|
882
1198
|
navigation: viewState?.navigation ?? "unlocked"
|
|
883
1199
|
};
|
|
884
1200
|
}
|
|
885
|
-
|
|
886
|
-
// components/FullView.tsx
|
|
887
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
888
1201
|
function FullView({
|
|
889
1202
|
id,
|
|
890
1203
|
children,
|
|
@@ -897,7 +1210,7 @@ function FullView({
|
|
|
897
1210
|
onExitStart,
|
|
898
1211
|
onExitEnd
|
|
899
1212
|
}) {
|
|
900
|
-
const config =
|
|
1213
|
+
const config = useMemo(
|
|
901
1214
|
() => ({
|
|
902
1215
|
id,
|
|
903
1216
|
type: "full",
|
|
@@ -914,7 +1227,7 @@ function FullView({
|
|
|
914
1227
|
onExitStart,
|
|
915
1228
|
onExitEnd
|
|
916
1229
|
});
|
|
917
|
-
return /* @__PURE__ */
|
|
1230
|
+
return /* @__PURE__ */ jsx(
|
|
918
1231
|
"section",
|
|
919
1232
|
{
|
|
920
1233
|
id,
|
|
@@ -934,9 +1247,6 @@ function FullView({
|
|
|
934
1247
|
}
|
|
935
1248
|
);
|
|
936
1249
|
}
|
|
937
|
-
|
|
938
|
-
// hooks/useMetricsReporter.ts
|
|
939
|
-
import { useRef as useRef8, useCallback as useCallback3, useEffect as useEffect9, useMemo as useMemo4 } from "react";
|
|
940
1250
|
var SCROLL_THROTTLE_MS = 66;
|
|
941
1251
|
function useMetricsReporter({
|
|
942
1252
|
id,
|
|
@@ -945,9 +1255,9 @@ function useMetricsReporter({
|
|
|
945
1255
|
onScrollProgress,
|
|
946
1256
|
throttleMs = SCROLL_THROTTLE_MS
|
|
947
1257
|
}) {
|
|
948
|
-
const scrollRef =
|
|
1258
|
+
const scrollRef = useRef(null);
|
|
949
1259
|
const updateMetrics = useScrollStore((s) => s.updateViewMetrics);
|
|
950
|
-
const measureAndReport =
|
|
1260
|
+
const measureAndReport = useCallback(() => {
|
|
951
1261
|
const el = scrollRef.current;
|
|
952
1262
|
if (!el) return;
|
|
953
1263
|
const metrics = {
|
|
@@ -962,11 +1272,11 @@ function useMetricsReporter({
|
|
|
962
1272
|
onScrollProgress(progress);
|
|
963
1273
|
}
|
|
964
1274
|
}, [id, scrollDirection, updateMetrics, onScrollProgress]);
|
|
965
|
-
const throttledMeasure =
|
|
1275
|
+
const throttledMeasure = useMemo(
|
|
966
1276
|
() => throttle(measureAndReport, throttleMs),
|
|
967
1277
|
[measureAndReport, throttleMs]
|
|
968
1278
|
);
|
|
969
|
-
|
|
1279
|
+
useEffect(() => {
|
|
970
1280
|
const el = scrollRef.current;
|
|
971
1281
|
if (!el) return;
|
|
972
1282
|
const resizeObserver = new ResizeObserver(() => {
|
|
@@ -981,16 +1291,13 @@ function useMetricsReporter({
|
|
|
981
1291
|
el.removeEventListener("scroll", throttledMeasure);
|
|
982
1292
|
};
|
|
983
1293
|
}, [measureAndReport, throttledMeasure]);
|
|
984
|
-
|
|
1294
|
+
useEffect(() => {
|
|
985
1295
|
if (isActive) {
|
|
986
1296
|
measureAndReport();
|
|
987
1297
|
}
|
|
988
1298
|
}, [isActive, measureAndReport]);
|
|
989
1299
|
return { scrollRef, measureAndReport };
|
|
990
1300
|
}
|
|
991
|
-
|
|
992
|
-
// components/ScrollLockedView.tsx
|
|
993
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
994
1301
|
function ScrollLockedView({
|
|
995
1302
|
id,
|
|
996
1303
|
children,
|
|
@@ -1026,14 +1333,14 @@ function ScrollLockedView({
|
|
|
1026
1333
|
onScrollProgress
|
|
1027
1334
|
});
|
|
1028
1335
|
const scrollClasses = scrollDirection === "vertical" ? "overflow-y-auto overflow-x-hidden" : "overflow-x-auto overflow-y-hidden";
|
|
1029
|
-
return /* @__PURE__ */
|
|
1336
|
+
return /* @__PURE__ */ jsx(
|
|
1030
1337
|
"section",
|
|
1031
1338
|
{
|
|
1032
1339
|
id,
|
|
1033
1340
|
className: `relative w-full h-screen ${className}`,
|
|
1034
1341
|
"data-view-type": "scroll-locked",
|
|
1035
1342
|
"data-active": isActive,
|
|
1036
|
-
children: /* @__PURE__ */
|
|
1343
|
+
children: /* @__PURE__ */ jsx(
|
|
1037
1344
|
"div",
|
|
1038
1345
|
{
|
|
1039
1346
|
ref: scrollRef,
|
|
@@ -1045,10 +1352,6 @@ function ScrollLockedView({
|
|
|
1045
1352
|
}
|
|
1046
1353
|
);
|
|
1047
1354
|
}
|
|
1048
|
-
|
|
1049
|
-
// components/ControlledView.tsx
|
|
1050
|
-
import { useMemo as useMemo5, useEffect as useEffect10 } from "react";
|
|
1051
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1052
1355
|
function ControlledView({
|
|
1053
1356
|
id,
|
|
1054
1357
|
children,
|
|
@@ -1065,7 +1368,7 @@ function ControlledView({
|
|
|
1065
1368
|
onExitEnd
|
|
1066
1369
|
}) {
|
|
1067
1370
|
const setExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
|
|
1068
|
-
const config =
|
|
1371
|
+
const config = useMemo(
|
|
1069
1372
|
() => ({
|
|
1070
1373
|
id,
|
|
1071
1374
|
type: "controlled",
|
|
@@ -1089,11 +1392,11 @@ function ControlledView({
|
|
|
1089
1392
|
isActive,
|
|
1090
1393
|
scrollDirection: allowInternalScroll ? scrollDirection : "none"
|
|
1091
1394
|
});
|
|
1092
|
-
|
|
1395
|
+
useEffect(() => {
|
|
1093
1396
|
const lockState = canProceed ? "unlocked" : "locked";
|
|
1094
1397
|
setExplicitLock(id, lockState);
|
|
1095
1398
|
}, [id, canProceed, setExplicitLock]);
|
|
1096
|
-
const overflowClasses =
|
|
1399
|
+
const overflowClasses = useMemo(() => {
|
|
1097
1400
|
if (!allowInternalScroll) return "overflow-hidden";
|
|
1098
1401
|
switch (scrollDirection) {
|
|
1099
1402
|
case "vertical":
|
|
@@ -1104,14 +1407,14 @@ function ControlledView({
|
|
|
1104
1407
|
return "overflow-auto";
|
|
1105
1408
|
}
|
|
1106
1409
|
}, [allowInternalScroll, scrollDirection]);
|
|
1107
|
-
return /* @__PURE__ */
|
|
1410
|
+
return /* @__PURE__ */ jsx(
|
|
1108
1411
|
"section",
|
|
1109
1412
|
{
|
|
1110
1413
|
id,
|
|
1111
1414
|
className: `relative w-full h-screen ${isActive ? "z-10" : "z-0"} ${className}`,
|
|
1112
1415
|
"data-view-type": "controlled",
|
|
1113
1416
|
"data-active": isActive,
|
|
1114
|
-
children: /* @__PURE__ */
|
|
1417
|
+
children: /* @__PURE__ */ jsx(
|
|
1115
1418
|
"div",
|
|
1116
1419
|
{
|
|
1117
1420
|
ref: scrollRef,
|
|
@@ -1127,7 +1430,7 @@ function useViewControl(viewId) {
|
|
|
1127
1430
|
const goToNext = useScrollStore((s) => s.goToNext);
|
|
1128
1431
|
const goToPrevious = useScrollStore((s) => s.goToPrevious);
|
|
1129
1432
|
const goToView = useScrollStore((s) => s.goToView);
|
|
1130
|
-
return
|
|
1433
|
+
return useMemo(
|
|
1131
1434
|
() => ({
|
|
1132
1435
|
unlock: () => setExplicitLock(viewId, "unlocked"),
|
|
1133
1436
|
lock: () => setExplicitLock(viewId, "locked"),
|
|
@@ -1139,9 +1442,6 @@ function useViewControl(viewId) {
|
|
|
1139
1442
|
[viewId, setExplicitLock, goToNext, goToPrevious, goToView]
|
|
1140
1443
|
);
|
|
1141
1444
|
}
|
|
1142
|
-
|
|
1143
|
-
// components/ScrollDebugOverlay.tsx
|
|
1144
|
-
import { jsx as jsx5, jsxs } from "react/jsx-runtime";
|
|
1145
1445
|
function ScrollDebugOverlay({
|
|
1146
1446
|
position = "bottom-left",
|
|
1147
1447
|
visible = true
|
|
@@ -1167,29 +1467,29 @@ function ScrollDebugOverlay({
|
|
|
1167
1467
|
style: { pointerEvents: "none" },
|
|
1168
1468
|
children: [
|
|
1169
1469
|
/* @__PURE__ */ jsxs("div", { className: "text-green-300 font-bold mb-2 flex items-center gap-2", children: [
|
|
1170
|
-
/* @__PURE__ */
|
|
1470
|
+
/* @__PURE__ */ jsx("span", { className: "w-2 h-2 rounded-full bg-green-500 animate-pulse" }),
|
|
1171
1471
|
"ScrollSystem Debug"
|
|
1172
1472
|
] }),
|
|
1173
1473
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1 mb-3", children: [
|
|
1174
|
-
/* @__PURE__ */
|
|
1175
|
-
/* @__PURE__ */
|
|
1176
|
-
/* @__PURE__ */
|
|
1177
|
-
/* @__PURE__ */
|
|
1474
|
+
/* @__PURE__ */ jsx(Row, { label: "initialized", value: isInitialized }),
|
|
1475
|
+
/* @__PURE__ */ jsx(Row, { label: "activeIndex", value: `${activeIndex} / ${totalViews - 1}` }),
|
|
1476
|
+
/* @__PURE__ */ jsx(Row, { label: "transitioning", value: isTransitioning }),
|
|
1477
|
+
/* @__PURE__ */ jsx(Row, { label: "globalLocked", value: isGlobalLocked })
|
|
1178
1478
|
] }),
|
|
1179
1479
|
activeView && /* @__PURE__ */ jsxs("div", { className: "border-t border-green-500/20 pt-2 mt-2", children: [
|
|
1180
|
-
/* @__PURE__ */
|
|
1181
|
-
/* @__PURE__ */
|
|
1182
|
-
/* @__PURE__ */
|
|
1183
|
-
/* @__PURE__ */
|
|
1184
|
-
/* @__PURE__ */
|
|
1185
|
-
/* @__PURE__ */
|
|
1186
|
-
activeView.explicitLock && /* @__PURE__ */
|
|
1480
|
+
/* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Active View" }),
|
|
1481
|
+
/* @__PURE__ */ jsx(Row, { label: "id", value: activeView.id }),
|
|
1482
|
+
/* @__PURE__ */ jsx(Row, { label: "type", value: activeView.type }),
|
|
1483
|
+
/* @__PURE__ */ jsx(Row, { label: "capability", value: activeView.capability }),
|
|
1484
|
+
/* @__PURE__ */ jsx(Row, { label: "navigation", value: activeView.navigation }),
|
|
1485
|
+
/* @__PURE__ */ jsx(Row, { label: "progress", value: `${(activeView.progress * 100).toFixed(0)}%` }),
|
|
1486
|
+
activeView.explicitLock && /* @__PURE__ */ jsx(Row, { label: "explicitLock", value: activeView.explicitLock, highlight: true })
|
|
1187
1487
|
] }),
|
|
1188
1488
|
activeView?.metrics && /* @__PURE__ */ jsxs("div", { className: "border-t border-green-500/20 pt-2 mt-2", children: [
|
|
1189
|
-
/* @__PURE__ */
|
|
1190
|
-
/* @__PURE__ */
|
|
1191
|
-
/* @__PURE__ */
|
|
1192
|
-
/* @__PURE__ */
|
|
1489
|
+
/* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Metrics" }),
|
|
1490
|
+
/* @__PURE__ */ jsx(Row, { label: "scrollHeight", value: activeView.metrics.scrollHeight }),
|
|
1491
|
+
/* @__PURE__ */ jsx(Row, { label: "clientHeight", value: activeView.metrics.clientHeight }),
|
|
1492
|
+
/* @__PURE__ */ jsx(Row, { label: "scrollTop", value: Math.round(activeView.metrics.scrollTop) })
|
|
1193
1493
|
] })
|
|
1194
1494
|
]
|
|
1195
1495
|
}
|
|
@@ -1207,23 +1507,19 @@ function Row({
|
|
|
1207
1507
|
label,
|
|
1208
1508
|
":"
|
|
1209
1509
|
] }),
|
|
1210
|
-
/* @__PURE__ */
|
|
1510
|
+
/* @__PURE__ */ jsx("span", { className: valueColor, children: displayValue })
|
|
1211
1511
|
] });
|
|
1212
1512
|
}
|
|
1213
|
-
|
|
1214
|
-
// components/AriaLiveRegion.tsx
|
|
1215
|
-
import { useEffect as useEffect11, useState as useState3 } from "react";
|
|
1216
|
-
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1217
1513
|
function AriaLiveRegion({
|
|
1218
1514
|
template = "Navigated to section {viewIndex} of {totalViews}",
|
|
1219
1515
|
politeness = "polite"
|
|
1220
1516
|
}) {
|
|
1221
|
-
const [announcement, setAnnouncement] =
|
|
1517
|
+
const [announcement, setAnnouncement] = useState("");
|
|
1222
1518
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1223
1519
|
const activeId = useScrollStore((s) => s.activeId);
|
|
1224
1520
|
const totalViews = useScrollStore((s) => s.totalViews);
|
|
1225
1521
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
1226
|
-
|
|
1522
|
+
useEffect(() => {
|
|
1227
1523
|
if (!isTransitioning && activeId) {
|
|
1228
1524
|
const message = template.replace("{viewIndex}", String(activeIndex + 1)).replace("{viewId}", activeId).replace("{totalViews}", String(totalViews));
|
|
1229
1525
|
const timer = setTimeout(() => {
|
|
@@ -1232,7 +1528,7 @@ function AriaLiveRegion({
|
|
|
1232
1528
|
return () => clearTimeout(timer);
|
|
1233
1529
|
}
|
|
1234
1530
|
}, [activeIndex, activeId, totalViews, isTransitioning, template]);
|
|
1235
|
-
return /* @__PURE__ */
|
|
1531
|
+
return /* @__PURE__ */ jsx(
|
|
1236
1532
|
"div",
|
|
1237
1533
|
{
|
|
1238
1534
|
role: "status",
|
|
@@ -1254,10 +1550,6 @@ function AriaLiveRegion({
|
|
|
1254
1550
|
}
|
|
1255
1551
|
);
|
|
1256
1552
|
}
|
|
1257
|
-
|
|
1258
|
-
// components/LazyView.tsx
|
|
1259
|
-
import { useMemo as useMemo6 } from "react";
|
|
1260
|
-
import { Fragment, jsx as jsx7 } from "react/jsx-runtime";
|
|
1261
1553
|
function LazyView({
|
|
1262
1554
|
viewId,
|
|
1263
1555
|
buffer = 1,
|
|
@@ -1266,7 +1558,7 @@ function LazyView({
|
|
|
1266
1558
|
}) {
|
|
1267
1559
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1268
1560
|
const views = useScrollStore((s) => s.views);
|
|
1269
|
-
const shouldRender =
|
|
1561
|
+
const shouldRender = useMemo(() => {
|
|
1270
1562
|
const viewIndex = views.findIndex((v) => v.id === viewId);
|
|
1271
1563
|
if (viewIndex === -1) return false;
|
|
1272
1564
|
const minIndex = Math.max(0, activeIndex - buffer);
|
|
@@ -1274,20 +1566,149 @@ function LazyView({
|
|
|
1274
1566
|
return viewIndex >= minIndex && viewIndex <= maxIndex;
|
|
1275
1567
|
}, [viewId, views, activeIndex, buffer]);
|
|
1276
1568
|
if (!shouldRender) {
|
|
1277
|
-
return /* @__PURE__ */
|
|
1569
|
+
return /* @__PURE__ */ jsx(Fragment, { children: placeholder });
|
|
1278
1570
|
}
|
|
1279
|
-
return /* @__PURE__ */
|
|
1571
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1572
|
+
}
|
|
1573
|
+
function NestedScrollView({
|
|
1574
|
+
id,
|
|
1575
|
+
children,
|
|
1576
|
+
className = "",
|
|
1577
|
+
nestedDirection = "horizontal",
|
|
1578
|
+
enableSnap = true,
|
|
1579
|
+
onItemChange,
|
|
1580
|
+
onActivate,
|
|
1581
|
+
onDeactivate,
|
|
1582
|
+
onEnterStart,
|
|
1583
|
+
onEnterEnd,
|
|
1584
|
+
onExitStart,
|
|
1585
|
+
onExitEnd
|
|
1586
|
+
}) {
|
|
1587
|
+
const containerRef = useRef(null);
|
|
1588
|
+
const nestedContainerRef = useRef(null);
|
|
1589
|
+
const [activeNestedIndex, setActiveNestedIndex] = useState(0);
|
|
1590
|
+
const [isNestedScrolling, setIsNestedScrolling] = useState(false);
|
|
1591
|
+
useScrollStore((s) => s.activeIndex);
|
|
1592
|
+
const views = useScrollStore((s) => s.views);
|
|
1593
|
+
const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
|
|
1594
|
+
const view = views.find((v) => v.id === id);
|
|
1595
|
+
const isActive = view?.isActive ?? false;
|
|
1596
|
+
useViewRegistration({
|
|
1597
|
+
config: {
|
|
1598
|
+
id,
|
|
1599
|
+
type: "nested",
|
|
1600
|
+
nestedConfig: {
|
|
1601
|
+
direction: nestedDirection,
|
|
1602
|
+
enableSnap,
|
|
1603
|
+
onItemChange
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
onActivate,
|
|
1607
|
+
onDeactivate,
|
|
1608
|
+
onEnterStart,
|
|
1609
|
+
onEnterEnd,
|
|
1610
|
+
onExitStart,
|
|
1611
|
+
onExitEnd
|
|
1612
|
+
});
|
|
1613
|
+
useEffect(() => {
|
|
1614
|
+
if (isActive) {
|
|
1615
|
+
onActivate?.();
|
|
1616
|
+
} else {
|
|
1617
|
+
onDeactivate?.();
|
|
1618
|
+
}
|
|
1619
|
+
}, [isActive, onActivate, onDeactivate]);
|
|
1620
|
+
const handleNestedScroll = useCallback((e) => {
|
|
1621
|
+
if (!nestedContainerRef.current) return;
|
|
1622
|
+
const container = nestedContainerRef.current;
|
|
1623
|
+
const scrollPos = nestedDirection === "horizontal" ? container.scrollLeft : container.scrollTop;
|
|
1624
|
+
const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
|
|
1625
|
+
if (itemSize > 0) {
|
|
1626
|
+
const newIndex = Math.round(scrollPos / itemSize);
|
|
1627
|
+
if (newIndex !== activeNestedIndex) {
|
|
1628
|
+
setActiveNestedIndex(newIndex);
|
|
1629
|
+
onItemChange?.(newIndex);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}, [nestedDirection, activeNestedIndex, onItemChange]);
|
|
1633
|
+
const handleTouchStart = useCallback((e) => {
|
|
1634
|
+
setIsNestedScrolling(true);
|
|
1635
|
+
setGlobalLock(true);
|
|
1636
|
+
}, [setGlobalLock]);
|
|
1637
|
+
const handleTouchEnd = useCallback(() => {
|
|
1638
|
+
setTimeout(() => {
|
|
1639
|
+
setIsNestedScrolling(false);
|
|
1640
|
+
setGlobalLock(false);
|
|
1641
|
+
}, 100);
|
|
1642
|
+
}, [setGlobalLock]);
|
|
1643
|
+
useCallback((index) => {
|
|
1644
|
+
if (!nestedContainerRef.current) return;
|
|
1645
|
+
const container = nestedContainerRef.current;
|
|
1646
|
+
const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
|
|
1647
|
+
const scrollPos = index * itemSize;
|
|
1648
|
+
container.scrollTo({
|
|
1649
|
+
[nestedDirection === "horizontal" ? "left" : "top"]: scrollPos,
|
|
1650
|
+
behavior: "smooth"
|
|
1651
|
+
});
|
|
1652
|
+
}, [nestedDirection]);
|
|
1653
|
+
const nestedScrollStyle = {
|
|
1654
|
+
display: "flex",
|
|
1655
|
+
flexDirection: nestedDirection === "horizontal" ? "row" : "column",
|
|
1656
|
+
overflow: nestedDirection === "horizontal" ? "auto hidden" : "hidden auto",
|
|
1657
|
+
scrollSnapType: enableSnap ? `${nestedDirection === "horizontal" ? "x" : "y"} mandatory` : "none",
|
|
1658
|
+
scrollBehavior: "smooth",
|
|
1659
|
+
WebkitOverflowScrolling: "touch",
|
|
1660
|
+
width: "100%",
|
|
1661
|
+
height: "100%"
|
|
1662
|
+
};
|
|
1663
|
+
return /* @__PURE__ */ jsx(
|
|
1664
|
+
"div",
|
|
1665
|
+
{
|
|
1666
|
+
ref: containerRef,
|
|
1667
|
+
className: `scroll-view nested-scroll-view h-screen w-screen flex-shrink-0 relative ${className}`,
|
|
1668
|
+
"data-view-id": id,
|
|
1669
|
+
"data-view-type": "nested",
|
|
1670
|
+
"data-nested-direction": nestedDirection,
|
|
1671
|
+
role: "region",
|
|
1672
|
+
"aria-label": `Nested scroll view ${id}`,
|
|
1673
|
+
tabIndex: 0,
|
|
1674
|
+
children: /* @__PURE__ */ jsx(
|
|
1675
|
+
"div",
|
|
1676
|
+
{
|
|
1677
|
+
ref: nestedContainerRef,
|
|
1678
|
+
className: "nested-scroll-container",
|
|
1679
|
+
style: nestedScrollStyle,
|
|
1680
|
+
onScroll: handleNestedScroll,
|
|
1681
|
+
onTouchStart: handleTouchStart,
|
|
1682
|
+
onTouchEnd: handleTouchEnd,
|
|
1683
|
+
children
|
|
1684
|
+
}
|
|
1685
|
+
)
|
|
1686
|
+
}
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
function NestedScrollItem({
|
|
1690
|
+
children,
|
|
1691
|
+
className = ""
|
|
1692
|
+
}) {
|
|
1693
|
+
return /* @__PURE__ */ jsx(
|
|
1694
|
+
"div",
|
|
1695
|
+
{
|
|
1696
|
+
className: `nested-scroll-item flex-shrink-0 w-full h-full ${className}`,
|
|
1697
|
+
style: {
|
|
1698
|
+
scrollSnapAlign: "start",
|
|
1699
|
+
scrollSnapStop: "always"
|
|
1700
|
+
},
|
|
1701
|
+
children
|
|
1702
|
+
}
|
|
1703
|
+
);
|
|
1280
1704
|
}
|
|
1281
|
-
|
|
1282
|
-
// hooks/useNavigation.ts
|
|
1283
|
-
import { useCallback as useCallback4, useMemo as useMemo7 } from "react";
|
|
1284
1705
|
function useNavigation() {
|
|
1285
1706
|
const goToView = useScrollStore((s) => s.goToView);
|
|
1286
1707
|
const goToNextAction = useScrollStore((s) => s.goToNext);
|
|
1287
1708
|
const goToPreviousAction = useScrollStore((s) => s.goToPrevious);
|
|
1288
1709
|
const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
|
|
1289
|
-
const lockScroll =
|
|
1290
|
-
const unlockScroll =
|
|
1710
|
+
const lockScroll = useCallback(() => setGlobalLock(true), [setGlobalLock]);
|
|
1711
|
+
const unlockScroll = useCallback(() => setGlobalLock(false), [setGlobalLock]);
|
|
1291
1712
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1292
1713
|
const activeId = useScrollStore((s) => s.activeId);
|
|
1293
1714
|
const totalViews = useScrollStore((s) => s.totalViews);
|
|
@@ -1295,16 +1716,16 @@ function useNavigation() {
|
|
|
1295
1716
|
const isScrollLocked = useScrollStore((s) => s.isGlobalLocked);
|
|
1296
1717
|
const canNavigateNext = useScrollStore(selectCanNavigateNext);
|
|
1297
1718
|
const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
|
|
1298
|
-
const goToNext =
|
|
1719
|
+
const goToNext = useCallback(() => {
|
|
1299
1720
|
return goToNextAction();
|
|
1300
1721
|
}, [goToNextAction]);
|
|
1301
|
-
const goToPrevious =
|
|
1722
|
+
const goToPrevious = useCallback(() => {
|
|
1302
1723
|
return goToPreviousAction();
|
|
1303
1724
|
}, [goToPreviousAction]);
|
|
1304
1725
|
const isFirstView = activeIndex === 0;
|
|
1305
1726
|
const isLastView = activeIndex === totalViews - 1;
|
|
1306
1727
|
const progress = totalViews > 1 ? activeIndex / (totalViews - 1) : 0;
|
|
1307
|
-
return
|
|
1728
|
+
return useMemo(
|
|
1308
1729
|
() => ({
|
|
1309
1730
|
// Acciones de navegación
|
|
1310
1731
|
goToView,
|
|
@@ -1374,18 +1795,15 @@ function useActiveViewProgress() {
|
|
|
1374
1795
|
viewType: activeView?.type
|
|
1375
1796
|
};
|
|
1376
1797
|
}
|
|
1377
|
-
|
|
1378
|
-
// hooks/useScrollAnalytics.ts
|
|
1379
|
-
import { useEffect as useEffect12, useRef as useRef9, useCallback as useCallback5 } from "react";
|
|
1380
1798
|
function useScrollAnalytics(options = {}) {
|
|
1381
1799
|
const { onViewEnter, onViewExit, enabled = true } = options;
|
|
1382
1800
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1383
1801
|
const activeId = useScrollStore((s) => s.activeId);
|
|
1384
1802
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
1385
|
-
const enterTimeRef =
|
|
1386
|
-
const prevIndexRef =
|
|
1387
|
-
const prevIdRef =
|
|
1388
|
-
const createAnalytics =
|
|
1803
|
+
const enterTimeRef = useRef(Date.now());
|
|
1804
|
+
const prevIndexRef = useRef(activeIndex);
|
|
1805
|
+
const prevIdRef = useRef(activeId);
|
|
1806
|
+
const createAnalytics = useCallback((viewId, viewIndex, enterTime, isActive, exitTime = null) => ({
|
|
1389
1807
|
viewId,
|
|
1390
1808
|
viewIndex,
|
|
1391
1809
|
enterTime,
|
|
@@ -1393,7 +1811,7 @@ function useScrollAnalytics(options = {}) {
|
|
|
1393
1811
|
duration: exitTime ? (exitTime - enterTime) / 1e3 : 0,
|
|
1394
1812
|
isActive
|
|
1395
1813
|
}), []);
|
|
1396
|
-
|
|
1814
|
+
useEffect(() => {
|
|
1397
1815
|
if (!enabled) return;
|
|
1398
1816
|
if (!isTransitioning && (activeIndex !== prevIndexRef.current || activeId !== prevIdRef.current)) {
|
|
1399
1817
|
const now = Date.now();
|
|
@@ -1428,36 +1846,205 @@ function useScrollAnalytics(options = {}) {
|
|
|
1428
1846
|
getTimeInView: () => (Date.now() - enterTimeRef.current) / 1e3
|
|
1429
1847
|
};
|
|
1430
1848
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1849
|
+
function useScrollLock() {
|
|
1850
|
+
const isLocked = useScrollStore((s) => s.isGlobalLocked);
|
|
1851
|
+
const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
|
|
1852
|
+
const setViewExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
|
|
1853
|
+
const lock = useCallback(() => {
|
|
1854
|
+
setGlobalLock(true);
|
|
1855
|
+
}, [setGlobalLock]);
|
|
1856
|
+
const unlock = useCallback(() => {
|
|
1857
|
+
setGlobalLock(false);
|
|
1858
|
+
}, [setGlobalLock]);
|
|
1859
|
+
const toggle = useCallback(() => {
|
|
1860
|
+
setGlobalLock(!isLocked);
|
|
1861
|
+
}, [setGlobalLock, isLocked]);
|
|
1862
|
+
const lockView = useCallback((viewId) => {
|
|
1863
|
+
setViewExplicitLock(viewId, "locked");
|
|
1864
|
+
}, [setViewExplicitLock]);
|
|
1865
|
+
const unlockView = useCallback((viewId) => {
|
|
1866
|
+
setViewExplicitLock(viewId, "unlocked");
|
|
1867
|
+
}, [setViewExplicitLock]);
|
|
1868
|
+
return {
|
|
1869
|
+
isLocked,
|
|
1870
|
+
lock,
|
|
1871
|
+
unlock,
|
|
1872
|
+
toggle,
|
|
1873
|
+
lockView,
|
|
1874
|
+
unlockView
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
var DEFAULT_CONFIG4 = {
|
|
1878
|
+
speed: 0.5,
|
|
1879
|
+
direction: "vertical",
|
|
1880
|
+
offset: 0,
|
|
1881
|
+
easing: "linear"
|
|
1882
|
+
};
|
|
1883
|
+
var easingFunctions = {
|
|
1884
|
+
linear: (t) => t,
|
|
1885
|
+
easeOut: (t) => 1 - Math.pow(1 - t, 2),
|
|
1886
|
+
easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
|
|
1463
1887
|
};
|
|
1888
|
+
function useParallax(viewId, config = {}) {
|
|
1889
|
+
const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
|
|
1890
|
+
const { speed, direction, offset, easing } = mergedConfig;
|
|
1891
|
+
const views = useScrollStore((s) => s.views);
|
|
1892
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1893
|
+
useScrollStore((s) => s.globalProgress);
|
|
1894
|
+
const result = useMemo(() => {
|
|
1895
|
+
const view = views.find((v) => v.id === viewId);
|
|
1896
|
+
if (!view) {
|
|
1897
|
+
return {
|
|
1898
|
+
transform: 0,
|
|
1899
|
+
style: {},
|
|
1900
|
+
progress: 0
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
const viewOffset = view.index - activeIndex;
|
|
1904
|
+
const internalProgress = view.isActive ? view.progress : 0;
|
|
1905
|
+
const combinedProgress = viewOffset + internalProgress;
|
|
1906
|
+
const easingFn = easingFunctions[easing];
|
|
1907
|
+
const easedProgress = easingFn(Math.abs(combinedProgress)) * Math.sign(combinedProgress);
|
|
1908
|
+
const maxDistance = 100;
|
|
1909
|
+
const transformValue = easedProgress * maxDistance * speed + offset;
|
|
1910
|
+
const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
|
|
1911
|
+
const style = {
|
|
1912
|
+
transform: transformProperty,
|
|
1913
|
+
willChange: "transform"
|
|
1914
|
+
};
|
|
1915
|
+
return {
|
|
1916
|
+
transform: transformValue,
|
|
1917
|
+
style,
|
|
1918
|
+
progress: combinedProgress
|
|
1919
|
+
};
|
|
1920
|
+
}, [viewId, views, activeIndex, speed, direction, offset, easing]);
|
|
1921
|
+
return result;
|
|
1922
|
+
}
|
|
1923
|
+
function useActiveParallax(config = {}) {
|
|
1924
|
+
const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
|
|
1925
|
+
const { speed, direction, offset, easing } = mergedConfig;
|
|
1926
|
+
const globalProgress = useScrollStore((s) => s.globalProgress);
|
|
1927
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1928
|
+
const views = useScrollStore((s) => s.views);
|
|
1929
|
+
return useMemo(() => {
|
|
1930
|
+
const activeView = views[activeIndex];
|
|
1931
|
+
const viewProgress = activeView?.progress ?? 0;
|
|
1932
|
+
const easingFn = easingFunctions[easing];
|
|
1933
|
+
const easedProgress = easingFn(viewProgress);
|
|
1934
|
+
const maxDistance = 100;
|
|
1935
|
+
const transformValue = easedProgress * maxDistance * speed + offset;
|
|
1936
|
+
const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
|
|
1937
|
+
const style = {
|
|
1938
|
+
transform: transformProperty,
|
|
1939
|
+
willChange: "transform"
|
|
1940
|
+
};
|
|
1941
|
+
return {
|
|
1942
|
+
transform: transformValue,
|
|
1943
|
+
style,
|
|
1944
|
+
progress: viewProgress
|
|
1945
|
+
};
|
|
1946
|
+
}, [globalProgress, activeIndex, views, speed, direction, offset, easing]);
|
|
1947
|
+
}
|
|
1948
|
+
function useSnapPoints(options) {
|
|
1949
|
+
const { viewId, points, snapThreshold = 0.1, smoothScroll = true } = options;
|
|
1950
|
+
const scrollContainerRef = useRef(null);
|
|
1951
|
+
const lastActivePointRef = useRef(null);
|
|
1952
|
+
const views = useScrollStore((s) => s.views);
|
|
1953
|
+
const setActiveSnapPoint = useScrollStore((s) => s.setActiveSnapPoint);
|
|
1954
|
+
const view = useMemo(() => views.find((v) => v.id === viewId), [views, viewId]);
|
|
1955
|
+
const viewProgress = view?.progress ?? 0;
|
|
1956
|
+
view?.activeSnapPointId ?? null;
|
|
1957
|
+
const sortedPoints = useMemo(
|
|
1958
|
+
() => [...points].sort((a, b) => a.position - b.position),
|
|
1959
|
+
[points]
|
|
1960
|
+
);
|
|
1961
|
+
const activePointData = useMemo(() => {
|
|
1962
|
+
if (sortedPoints.length === 0) {
|
|
1963
|
+
return { point: null, index: -1 };
|
|
1964
|
+
}
|
|
1965
|
+
let closestIndex = 0;
|
|
1966
|
+
let closestDistance = Math.abs(viewProgress - sortedPoints[0].position);
|
|
1967
|
+
for (let i = 1; i < sortedPoints.length; i++) {
|
|
1968
|
+
const distance = Math.abs(viewProgress - sortedPoints[i].position);
|
|
1969
|
+
if (distance < closestDistance) {
|
|
1970
|
+
closestDistance = distance;
|
|
1971
|
+
closestIndex = i;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
if (closestDistance <= snapThreshold) {
|
|
1975
|
+
return { point: sortedPoints[closestIndex], index: closestIndex };
|
|
1976
|
+
}
|
|
1977
|
+
for (let i = sortedPoints.length - 1; i >= 0; i--) {
|
|
1978
|
+
if (viewProgress >= sortedPoints[i].position - snapThreshold) {
|
|
1979
|
+
return { point: sortedPoints[i], index: i };
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return { point: sortedPoints[0], index: 0 };
|
|
1983
|
+
}, [viewProgress, sortedPoints, snapThreshold]);
|
|
1984
|
+
useEffect(() => {
|
|
1985
|
+
const newPointId = activePointData.point?.id ?? null;
|
|
1986
|
+
if (newPointId !== lastActivePointRef.current) {
|
|
1987
|
+
if (lastActivePointRef.current) {
|
|
1988
|
+
const prevPoint = sortedPoints.find((p) => p.id === lastActivePointRef.current);
|
|
1989
|
+
prevPoint?.onLeave?.();
|
|
1990
|
+
}
|
|
1991
|
+
setActiveSnapPoint(viewId, newPointId);
|
|
1992
|
+
if (newPointId) {
|
|
1993
|
+
const newPoint = sortedPoints.find((p) => p.id === newPointId);
|
|
1994
|
+
newPoint?.onReach?.();
|
|
1995
|
+
}
|
|
1996
|
+
lastActivePointRef.current = newPointId;
|
|
1997
|
+
}
|
|
1998
|
+
}, [activePointData.point?.id, viewId, sortedPoints, setActiveSnapPoint]);
|
|
1999
|
+
const goToPoint = useCallback((pointId) => {
|
|
2000
|
+
const point = sortedPoints.find((p) => p.id === pointId);
|
|
2001
|
+
if (!point || !scrollContainerRef.current) return;
|
|
2002
|
+
const container = scrollContainerRef.current;
|
|
2003
|
+
const maxScroll = container.scrollHeight - container.clientHeight;
|
|
2004
|
+
const targetScroll = point.position * maxScroll;
|
|
2005
|
+
container.scrollTo({
|
|
2006
|
+
top: targetScroll,
|
|
2007
|
+
behavior: smoothScroll ? "smooth" : "auto"
|
|
2008
|
+
});
|
|
2009
|
+
}, [sortedPoints, smoothScroll]);
|
|
2010
|
+
const goToNextPoint = useCallback(() => {
|
|
2011
|
+
const nextIndex = Math.min(activePointData.index + 1, sortedPoints.length - 1);
|
|
2012
|
+
const nextPoint = sortedPoints[nextIndex];
|
|
2013
|
+
if (nextPoint) {
|
|
2014
|
+
goToPoint(nextPoint.id);
|
|
2015
|
+
}
|
|
2016
|
+
}, [activePointData.index, sortedPoints, goToPoint]);
|
|
2017
|
+
const goToPrevPoint = useCallback(() => {
|
|
2018
|
+
const prevIndex = Math.max(activePointData.index - 1, 0);
|
|
2019
|
+
const prevPoint = sortedPoints[prevIndex];
|
|
2020
|
+
if (prevPoint) {
|
|
2021
|
+
goToPoint(prevPoint.id);
|
|
2022
|
+
}
|
|
2023
|
+
}, [activePointData.index, sortedPoints, goToPoint]);
|
|
2024
|
+
const pointsWithState = useMemo(
|
|
2025
|
+
() => sortedPoints.map((point) => ({
|
|
2026
|
+
...point,
|
|
2027
|
+
isActive: point.id === activePointData.point?.id
|
|
2028
|
+
})),
|
|
2029
|
+
[sortedPoints, activePointData.point?.id]
|
|
2030
|
+
);
|
|
2031
|
+
return {
|
|
2032
|
+
activePoint: activePointData.point,
|
|
2033
|
+
activeIndex: activePointData.index,
|
|
2034
|
+
goToPoint,
|
|
2035
|
+
goToNextPoint,
|
|
2036
|
+
goToPrevPoint,
|
|
2037
|
+
points: pointsWithState,
|
|
2038
|
+
progress: viewProgress
|
|
2039
|
+
};
|
|
2040
|
+
}
|
|
2041
|
+
function createSnapPoints(positions, options) {
|
|
2042
|
+
const { prefix = "snap", labels } = options ?? {};
|
|
2043
|
+
return positions.map((position, index) => ({
|
|
2044
|
+
id: `${prefix}-${index}`,
|
|
2045
|
+
position,
|
|
2046
|
+
label: labels?.[index] ?? `Section ${index + 1}`
|
|
2047
|
+
}));
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
export { AriaLiveRegion, ControlledView, DEFAULT_PROGRESS_DEBOUNCE, DEFAULT_TRANSITION_DURATION, DEFAULT_TRANSITION_EASING, FullView, LazyView, NAVIGATION_COOLDOWN, NAV_THRESHOLDS, NestedScrollItem, NestedScrollView, ScrollContainer, ScrollDebugOverlay, ScrollLockedView, createSnapPoints, selectActiveView, selectActiveViewProgress, selectCanNavigateNext, selectCanNavigatePrevious, selectGlobalProgress, selectIsAutoScrolling, useActiveParallax, useActiveViewProgress, useAutoScroll, useDragHandler, useFocusManagement, useGestureConfig, useGlobalProgress, useHashSync, useInfiniteScroll, useKeyboardHandler, useMetricsReporter, useNavigation, useParallax, usePreload, useScrollAnalytics, useScrollLock, useScrollStore, useScrollSystem, useSnapPoints, useTouchHandler, useViewControl, useViewProgress, useViewRegistration, useWheelHandler };
|