scroll-system 1.0.1 → 1.1.1
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 +878 -284
- package/dist/index.mjs +817 -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();
|
|
@@ -263,6 +322,24 @@ function useWheelHandler() {
|
|
|
263
322
|
return;
|
|
264
323
|
}
|
|
265
324
|
const normalized = normalizeWheel(event);
|
|
325
|
+
const activeView = state.views[state.activeIndex];
|
|
326
|
+
if (activeView?.capability === "internal") {
|
|
327
|
+
const scrollContainer = document.querySelector(
|
|
328
|
+
`[data-view-type="scroll-locked"][data-active="true"] > div`
|
|
329
|
+
);
|
|
330
|
+
if (scrollContainer) {
|
|
331
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
|
|
332
|
+
const maxScroll = scrollHeight - clientHeight;
|
|
333
|
+
const isScrollingDown = normalized.pixelY > 0;
|
|
334
|
+
const isScrollingUp = normalized.pixelY < 0;
|
|
335
|
+
const isAtBottom = scrollTop >= maxScroll - 1;
|
|
336
|
+
const isAtTop = scrollTop <= 1;
|
|
337
|
+
if (isScrollingDown && !isAtBottom || isScrollingUp && !isAtTop) {
|
|
338
|
+
scrollAccumulator.current = 0;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
266
343
|
const delta = normalized.pixelY;
|
|
267
344
|
scrollAccumulator.current += delta;
|
|
268
345
|
const now = Date.now();
|
|
@@ -282,7 +359,6 @@ function useWheelHandler() {
|
|
|
282
359
|
if (handled) {
|
|
283
360
|
scrollAccumulator.current = 0;
|
|
284
361
|
event.preventDefault();
|
|
285
|
-
} else {
|
|
286
362
|
}
|
|
287
363
|
}
|
|
288
364
|
};
|
|
@@ -290,14 +366,11 @@ function useWheelHandler() {
|
|
|
290
366
|
return () => window.removeEventListener("wheel", handleWheel);
|
|
291
367
|
}, []);
|
|
292
368
|
}
|
|
293
|
-
|
|
294
|
-
// hooks/useTouchHandler.ts
|
|
295
|
-
import { useRef as useRef2, useEffect as useEffect2 } from "react";
|
|
296
369
|
function useTouchHandler(options = {}) {
|
|
297
370
|
const { enabled = true } = options;
|
|
298
|
-
const touchStart =
|
|
299
|
-
const touchStartTime =
|
|
300
|
-
|
|
371
|
+
const touchStart = useRef(null);
|
|
372
|
+
const touchStartTime = useRef(0);
|
|
373
|
+
useEffect(() => {
|
|
301
374
|
if (!enabled) return;
|
|
302
375
|
const handleTouchStart = (e) => {
|
|
303
376
|
touchStart.current = {
|
|
@@ -338,12 +411,9 @@ function useTouchHandler(options = {}) {
|
|
|
338
411
|
};
|
|
339
412
|
}, [enabled]);
|
|
340
413
|
}
|
|
341
|
-
|
|
342
|
-
// hooks/useKeyboardHandler.ts
|
|
343
|
-
import { useEffect as useEffect3 } from "react";
|
|
344
414
|
function useKeyboardHandler(options = {}) {
|
|
345
415
|
const { enabled = true, preventDefault = true } = options;
|
|
346
|
-
|
|
416
|
+
useEffect(() => {
|
|
347
417
|
if (!enabled) return;
|
|
348
418
|
const handleKeyDown = (e) => {
|
|
349
419
|
const target = e.target;
|
|
@@ -401,13 +471,10 @@ function useKeyboardHandler(options = {}) {
|
|
|
401
471
|
};
|
|
402
472
|
}, [enabled, preventDefault]);
|
|
403
473
|
}
|
|
404
|
-
|
|
405
|
-
// hooks/useHashSync.ts
|
|
406
|
-
import { useEffect as useEffect4, useRef as useRef3 } from "react";
|
|
407
474
|
function useHashSync(options = {}) {
|
|
408
475
|
const { enabled = true, pushHistory = false, hashPrefix = "" } = options;
|
|
409
|
-
const hasInitialized =
|
|
410
|
-
|
|
476
|
+
const hasInitialized = useRef(false);
|
|
477
|
+
useEffect(() => {
|
|
411
478
|
if (!enabled) return;
|
|
412
479
|
const unsubscribe = useScrollStore.subscribe(
|
|
413
480
|
(state) => state.activeIndex,
|
|
@@ -428,7 +495,7 @@ function useHashSync(options = {}) {
|
|
|
428
495
|
);
|
|
429
496
|
return () => unsubscribe();
|
|
430
497
|
}, [enabled, pushHistory, hashPrefix]);
|
|
431
|
-
|
|
498
|
+
useEffect(() => {
|
|
432
499
|
if (!enabled) return;
|
|
433
500
|
const handlePopState = () => {
|
|
434
501
|
const hash = window.location.hash.slice(1);
|
|
@@ -443,7 +510,7 @@ function useHashSync(options = {}) {
|
|
|
443
510
|
window.addEventListener("popstate", handlePopState);
|
|
444
511
|
return () => window.removeEventListener("popstate", handlePopState);
|
|
445
512
|
}, [enabled, hashPrefix]);
|
|
446
|
-
|
|
513
|
+
useEffect(() => {
|
|
447
514
|
if (!enabled) return;
|
|
448
515
|
const checkAndNavigate = () => {
|
|
449
516
|
const state = useScrollStore.getState();
|
|
@@ -466,9 +533,6 @@ function useHashSync(options = {}) {
|
|
|
466
533
|
checkAndNavigate();
|
|
467
534
|
}, [enabled, hashPrefix]);
|
|
468
535
|
}
|
|
469
|
-
|
|
470
|
-
// hooks/useDragHandler.ts
|
|
471
|
-
import { useRef as useRef4, useEffect as useEffect5, useCallback, useState } from "react";
|
|
472
536
|
var DRAG_THRESHOLD = 50;
|
|
473
537
|
var VELOCITY_THRESHOLD = 0.5;
|
|
474
538
|
var RESISTANCE_FACTOR = 0.4;
|
|
@@ -479,9 +543,9 @@ function useDragHandler(options = {}) {
|
|
|
479
543
|
dragOffset: 0,
|
|
480
544
|
dragDirection: null
|
|
481
545
|
});
|
|
482
|
-
const touchStartRef =
|
|
483
|
-
const lastMoveRef =
|
|
484
|
-
const rafRef =
|
|
546
|
+
const touchStartRef = useRef(null);
|
|
547
|
+
const lastMoveRef = useRef(null);
|
|
548
|
+
const rafRef = useRef(null);
|
|
485
549
|
const updateDragState = useCallback((newState) => {
|
|
486
550
|
setDragState((prev) => {
|
|
487
551
|
const updated = { ...prev, ...newState };
|
|
@@ -489,7 +553,7 @@ function useDragHandler(options = {}) {
|
|
|
489
553
|
return updated;
|
|
490
554
|
});
|
|
491
555
|
}, [onDragUpdate]);
|
|
492
|
-
|
|
556
|
+
useEffect(() => {
|
|
493
557
|
if (!enabled) return;
|
|
494
558
|
const handleTouchStart = (e) => {
|
|
495
559
|
const target = e.target;
|
|
@@ -575,16 +639,13 @@ function useDragHandler(options = {}) {
|
|
|
575
639
|
}, [enabled, updateDragState, onDragEnd]);
|
|
576
640
|
return dragState;
|
|
577
641
|
}
|
|
578
|
-
|
|
579
|
-
// hooks/useFocusManagement.ts
|
|
580
|
-
import { useEffect as useEffect6, useRef as useRef5 } from "react";
|
|
581
642
|
function useFocusManagement(options = {}) {
|
|
582
643
|
const { enabled = true, focusDelay = 100 } = options;
|
|
583
644
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
584
645
|
const activeId = useScrollStore((s) => s.activeId);
|
|
585
646
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
586
|
-
const prevIndexRef =
|
|
587
|
-
|
|
647
|
+
const prevIndexRef = useRef(activeIndex);
|
|
648
|
+
useEffect(() => {
|
|
588
649
|
if (!enabled) return;
|
|
589
650
|
if (activeIndex !== prevIndexRef.current && !isTransitioning && activeId) {
|
|
590
651
|
const timer = setTimeout(() => {
|
|
@@ -601,9 +662,6 @@ function useFocusManagement(options = {}) {
|
|
|
601
662
|
}
|
|
602
663
|
}, [activeIndex, activeId, isTransitioning, enabled, focusDelay]);
|
|
603
664
|
}
|
|
604
|
-
|
|
605
|
-
// hooks/useScrollSystem.ts
|
|
606
|
-
import { useCallback as useCallback2, useMemo } from "react";
|
|
607
665
|
function useScrollSystem() {
|
|
608
666
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
609
667
|
const globalProgress = useScrollStore((s) => s.globalProgress);
|
|
@@ -623,24 +681,24 @@ function useScrollSystem() {
|
|
|
623
681
|
const activeViewProgress = activeView?.progress ?? 0;
|
|
624
682
|
const canNavigateNext = useScrollStore(selectCanNavigateNext);
|
|
625
683
|
const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
|
|
626
|
-
const goToNext =
|
|
684
|
+
const goToNext = useCallback(() => {
|
|
627
685
|
if (canNavigateNext) {
|
|
628
686
|
storeNext();
|
|
629
687
|
return true;
|
|
630
688
|
}
|
|
631
689
|
return false;
|
|
632
690
|
}, [canNavigateNext, storeNext]);
|
|
633
|
-
const goToPrev =
|
|
691
|
+
const goToPrev = useCallback(() => {
|
|
634
692
|
if (canNavigatePrevious) {
|
|
635
693
|
storePrev();
|
|
636
694
|
return true;
|
|
637
695
|
}
|
|
638
696
|
return false;
|
|
639
697
|
}, [canNavigatePrevious, storePrev]);
|
|
640
|
-
const getCurrentIndex =
|
|
641
|
-
const getProgress =
|
|
642
|
-
const getActiveViewProgress =
|
|
643
|
-
const isLocked =
|
|
698
|
+
const getCurrentIndex = useCallback(() => activeIndex, [activeIndex]);
|
|
699
|
+
const getProgress = useCallback(() => globalProgress, [globalProgress]);
|
|
700
|
+
const getActiveViewProgress = useCallback(() => activeViewProgress, [activeViewProgress]);
|
|
701
|
+
const isLocked = useCallback(() => isGlobalLocked || isTransitioning || !canNavigateNext, [isGlobalLocked, isTransitioning, canNavigateNext]);
|
|
644
702
|
return useMemo(() => ({
|
|
645
703
|
goToNext,
|
|
646
704
|
goToPrev,
|
|
@@ -677,6 +735,255 @@ function useScrollSystem() {
|
|
|
677
735
|
isTransitioning
|
|
678
736
|
]);
|
|
679
737
|
}
|
|
738
|
+
function useGlobalProgress(options = {}) {
|
|
739
|
+
const { onProgress, throttle: throttle2 = 16 } = options;
|
|
740
|
+
const progress = useScrollStore(selectGlobalProgress);
|
|
741
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
742
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
743
|
+
useEffect(() => {
|
|
744
|
+
if (!onProgress) return;
|
|
745
|
+
let lastCall = 0;
|
|
746
|
+
const now = Date.now();
|
|
747
|
+
if (now - lastCall >= throttle2) {
|
|
748
|
+
lastCall = now;
|
|
749
|
+
onProgress(progress);
|
|
750
|
+
}
|
|
751
|
+
}, [progress, onProgress, throttle2]);
|
|
752
|
+
return {
|
|
753
|
+
progress,
|
|
754
|
+
activeIndex,
|
|
755
|
+
totalViews,
|
|
756
|
+
percentage: Math.round(progress * 100)
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
var DEFAULT_CONFIG = {
|
|
760
|
+
enabled: false,
|
|
761
|
+
interval: 5e3,
|
|
762
|
+
pauseOnInteraction: true,
|
|
763
|
+
resumeDelay: 3e3,
|
|
764
|
+
direction: "forward",
|
|
765
|
+
stopAtEnd: false
|
|
766
|
+
};
|
|
767
|
+
function useAutoScroll(config) {
|
|
768
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
769
|
+
const {
|
|
770
|
+
enabled,
|
|
771
|
+
interval,
|
|
772
|
+
pauseOnInteraction,
|
|
773
|
+
resumeDelay,
|
|
774
|
+
direction,
|
|
775
|
+
stopAtEnd
|
|
776
|
+
} = mergedConfig;
|
|
777
|
+
const intervalRef = useRef(null);
|
|
778
|
+
const resumeTimeoutRef = useRef(null);
|
|
779
|
+
const isAutoScrolling = useScrollStore((s) => s.isAutoScrolling);
|
|
780
|
+
const isAutoScrollPaused = useScrollStore((s) => s.isAutoScrollPaused);
|
|
781
|
+
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
782
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
783
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
784
|
+
const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
|
|
785
|
+
const isDragging = useScrollStore((s) => s.isDragging);
|
|
786
|
+
const setAutoScrolling = useScrollStore((s) => s.setAutoScrolling);
|
|
787
|
+
const setAutoScrollPaused = useScrollStore((s) => s.setAutoScrollPaused);
|
|
788
|
+
const goToNext = useScrollStore((s) => s.goToNext);
|
|
789
|
+
const goToPrevious = useScrollStore((s) => s.goToPrevious);
|
|
790
|
+
const pause = useCallback(() => {
|
|
791
|
+
setAutoScrollPaused(true);
|
|
792
|
+
if (resumeTimeoutRef.current) {
|
|
793
|
+
clearTimeout(resumeTimeoutRef.current);
|
|
794
|
+
resumeTimeoutRef.current = null;
|
|
795
|
+
}
|
|
796
|
+
}, [setAutoScrollPaused]);
|
|
797
|
+
const resume = useCallback(() => {
|
|
798
|
+
setAutoScrollPaused(false);
|
|
799
|
+
}, [setAutoScrollPaused]);
|
|
800
|
+
const toggle = useCallback(() => {
|
|
801
|
+
if (isAutoScrollPaused) {
|
|
802
|
+
resume();
|
|
803
|
+
} else {
|
|
804
|
+
pause();
|
|
805
|
+
}
|
|
806
|
+
}, [isAutoScrollPaused, pause, resume]);
|
|
807
|
+
const reset = useCallback(() => {
|
|
808
|
+
if (intervalRef.current) {
|
|
809
|
+
clearInterval(intervalRef.current);
|
|
810
|
+
intervalRef.current = null;
|
|
811
|
+
}
|
|
812
|
+
}, []);
|
|
813
|
+
useEffect(() => {
|
|
814
|
+
if (!enabled || !pauseOnInteraction) return;
|
|
815
|
+
if (isDragging || isTransitioning) {
|
|
816
|
+
pause();
|
|
817
|
+
if (resumeTimeoutRef.current) {
|
|
818
|
+
clearTimeout(resumeTimeoutRef.current);
|
|
819
|
+
}
|
|
820
|
+
resumeTimeoutRef.current = setTimeout(() => {
|
|
821
|
+
if (enabled) {
|
|
822
|
+
resume();
|
|
823
|
+
}
|
|
824
|
+
}, resumeDelay);
|
|
825
|
+
}
|
|
826
|
+
return () => {
|
|
827
|
+
if (resumeTimeoutRef.current) {
|
|
828
|
+
clearTimeout(resumeTimeoutRef.current);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
}, [isDragging, isTransitioning, enabled, pauseOnInteraction, resumeDelay, pause, resume]);
|
|
832
|
+
useEffect(() => {
|
|
833
|
+
setAutoScrolling(enabled);
|
|
834
|
+
setAutoScrollPaused(false);
|
|
835
|
+
return () => {
|
|
836
|
+
setAutoScrolling(false);
|
|
837
|
+
};
|
|
838
|
+
}, [enabled, setAutoScrolling, setAutoScrollPaused]);
|
|
839
|
+
useEffect(() => {
|
|
840
|
+
if (!enabled || isAutoScrollPaused || isTransitioning) {
|
|
841
|
+
reset();
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (stopAtEnd && !infiniteScrollEnabled) {
|
|
845
|
+
if (direction === "forward" && activeIndex >= totalViews - 1) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (direction === "backward" && activeIndex <= 0) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
intervalRef.current = setInterval(() => {
|
|
853
|
+
if (direction === "forward") {
|
|
854
|
+
goToNext();
|
|
855
|
+
} else {
|
|
856
|
+
goToPrevious();
|
|
857
|
+
}
|
|
858
|
+
}, interval);
|
|
859
|
+
return () => {
|
|
860
|
+
reset();
|
|
861
|
+
};
|
|
862
|
+
}, [
|
|
863
|
+
enabled,
|
|
864
|
+
isAutoScrollPaused,
|
|
865
|
+
isTransitioning,
|
|
866
|
+
interval,
|
|
867
|
+
direction,
|
|
868
|
+
stopAtEnd,
|
|
869
|
+
activeIndex,
|
|
870
|
+
totalViews,
|
|
871
|
+
infiniteScrollEnabled,
|
|
872
|
+
goToNext,
|
|
873
|
+
goToPrevious,
|
|
874
|
+
reset
|
|
875
|
+
]);
|
|
876
|
+
return {
|
|
877
|
+
isPlaying: isAutoScrolling && !isAutoScrollPaused,
|
|
878
|
+
isPaused: isAutoScrollPaused,
|
|
879
|
+
pause,
|
|
880
|
+
resume,
|
|
881
|
+
toggle,
|
|
882
|
+
reset
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
var DEFAULT_CONFIG2 = {
|
|
886
|
+
enabled: false,
|
|
887
|
+
loopDirection: "both"
|
|
888
|
+
};
|
|
889
|
+
function useInfiniteScroll(config = false) {
|
|
890
|
+
const normalizedConfig = typeof config === "boolean" ? { ...DEFAULT_CONFIG2, enabled: config } : { ...DEFAULT_CONFIG2, ...config };
|
|
891
|
+
const { enabled, loopDirection } = normalizedConfig;
|
|
892
|
+
const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
|
|
893
|
+
const setInfiniteScrollEnabled = useScrollStore((s) => s.setInfiniteScrollEnabled);
|
|
894
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
895
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
896
|
+
useEffect(() => {
|
|
897
|
+
setInfiniteScrollEnabled(enabled);
|
|
898
|
+
return () => {
|
|
899
|
+
};
|
|
900
|
+
}, [enabled, setInfiniteScrollEnabled]);
|
|
901
|
+
const enable = () => setInfiniteScrollEnabled(true);
|
|
902
|
+
const disable = () => setInfiniteScrollEnabled(false);
|
|
903
|
+
const toggle = () => setInfiniteScrollEnabled(!infiniteScrollEnabled);
|
|
904
|
+
const canLoopForward = infiniteScrollEnabled && (loopDirection === "forward" || loopDirection === "both") && activeIndex === totalViews - 1;
|
|
905
|
+
const canLoopBackward = infiniteScrollEnabled && (loopDirection === "backward" || loopDirection === "both") && activeIndex === 0;
|
|
906
|
+
return {
|
|
907
|
+
isEnabled: infiniteScrollEnabled,
|
|
908
|
+
enable,
|
|
909
|
+
disable,
|
|
910
|
+
toggle,
|
|
911
|
+
canLoopForward,
|
|
912
|
+
canLoopBackward
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
var DEFAULT_CONFIG3 = {
|
|
916
|
+
ahead: 1,
|
|
917
|
+
behind: 1,
|
|
918
|
+
delay: 100
|
|
919
|
+
};
|
|
920
|
+
function usePreload(config = {}) {
|
|
921
|
+
const mergedConfig = { ...DEFAULT_CONFIG3, ...config };
|
|
922
|
+
const { ahead, behind, delay } = mergedConfig;
|
|
923
|
+
const views = useScrollStore((s) => s.views);
|
|
924
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
925
|
+
const totalViews = useScrollStore((s) => s.totalViews);
|
|
926
|
+
const infiniteScrollEnabled = useScrollStore((s) => s.infiniteScrollEnabled);
|
|
927
|
+
const setViewPreloaded = useScrollStore((s) => s.setViewPreloaded);
|
|
928
|
+
const preloadedViewIds = useMemo(() => {
|
|
929
|
+
const ids = [];
|
|
930
|
+
for (let i = -behind; i <= ahead; i++) {
|
|
931
|
+
let targetIndex = activeIndex + i;
|
|
932
|
+
if (infiniteScrollEnabled) {
|
|
933
|
+
if (targetIndex < 0) targetIndex = totalViews + targetIndex;
|
|
934
|
+
if (targetIndex >= totalViews) targetIndex = targetIndex - totalViews;
|
|
935
|
+
}
|
|
936
|
+
if (targetIndex >= 0 && targetIndex < totalViews) {
|
|
937
|
+
const view = views[targetIndex];
|
|
938
|
+
if (view) {
|
|
939
|
+
ids.push(view.id);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return ids;
|
|
944
|
+
}, [activeIndex, ahead, behind, views, totalViews, infiniteScrollEnabled]);
|
|
945
|
+
useEffect(() => {
|
|
946
|
+
const timer = setTimeout(() => {
|
|
947
|
+
views.forEach((view) => {
|
|
948
|
+
const shouldBePreloaded = preloadedViewIds.includes(view.id);
|
|
949
|
+
if (view.isPreloaded !== shouldBePreloaded) {
|
|
950
|
+
setViewPreloaded(view.id, shouldBePreloaded);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}, delay);
|
|
954
|
+
return () => clearTimeout(timer);
|
|
955
|
+
}, [preloadedViewIds, views, setViewPreloaded, delay]);
|
|
956
|
+
const shouldPreload = (viewId) => {
|
|
957
|
+
return preloadedViewIds.includes(viewId);
|
|
958
|
+
};
|
|
959
|
+
const isPreloaded = (viewId) => {
|
|
960
|
+
const view = views.find((v) => v.id === viewId);
|
|
961
|
+
return view?.isPreloaded ?? false;
|
|
962
|
+
};
|
|
963
|
+
return {
|
|
964
|
+
shouldPreload,
|
|
965
|
+
preloadedViewIds,
|
|
966
|
+
isPreloaded
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
var DEFAULT_GESTURE_CONFIG = {
|
|
970
|
+
swipeThreshold: 50,
|
|
971
|
+
swipeVelocity: 0.5,
|
|
972
|
+
dragResistance: 0.3,
|
|
973
|
+
enableWheel: true,
|
|
974
|
+
enableTouch: true,
|
|
975
|
+
enableKeyboard: true
|
|
976
|
+
};
|
|
977
|
+
var GestureConfigContext = createContext(DEFAULT_GESTURE_CONFIG);
|
|
978
|
+
function useGestureConfig() {
|
|
979
|
+
return useContext(GestureConfigContext);
|
|
980
|
+
}
|
|
981
|
+
function mergeGestureConfig(config) {
|
|
982
|
+
return useMemo(() => ({
|
|
983
|
+
...DEFAULT_GESTURE_CONFIG,
|
|
984
|
+
...config
|
|
985
|
+
}), [config]);
|
|
986
|
+
}
|
|
680
987
|
|
|
681
988
|
// utils/index.ts
|
|
682
989
|
function throttle(fn, limit) {
|
|
@@ -705,9 +1012,6 @@ function prefersReducedMotion() {
|
|
|
705
1012
|
if (typeof window === "undefined") return false;
|
|
706
1013
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
707
1014
|
}
|
|
708
|
-
|
|
709
|
-
// components/ScrollContainer.tsx
|
|
710
|
-
import { jsx } from "react/jsx-runtime";
|
|
711
1015
|
function ScrollContainer({
|
|
712
1016
|
children,
|
|
713
1017
|
className = "",
|
|
@@ -725,12 +1029,20 @@ function ScrollContainer({
|
|
|
725
1029
|
// Touch Physics
|
|
726
1030
|
enableDragPhysics = false,
|
|
727
1031
|
// Layout
|
|
728
|
-
orientation = "vertical"
|
|
1032
|
+
orientation = "vertical",
|
|
1033
|
+
// NEW: v1.1.0 Features
|
|
1034
|
+
skipInitialAnimation = false,
|
|
1035
|
+
onProgress,
|
|
1036
|
+
gestureConfig,
|
|
1037
|
+
autoScroll,
|
|
1038
|
+
infiniteScroll = false,
|
|
1039
|
+
preload = true
|
|
729
1040
|
}) {
|
|
730
|
-
const containerRef =
|
|
1041
|
+
const containerRef = useRef(null);
|
|
1042
|
+
const isFirstRender = useRef(true);
|
|
731
1043
|
const isBrowser = typeof window !== "undefined";
|
|
732
|
-
const [reducedMotion, setReducedMotion] =
|
|
733
|
-
|
|
1044
|
+
const [reducedMotion, setReducedMotion] = useState(false);
|
|
1045
|
+
useEffect(() => {
|
|
734
1046
|
if (!isBrowser || !respectReducedMotion) return;
|
|
735
1047
|
setReducedMotion(prefersReducedMotion());
|
|
736
1048
|
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
@@ -741,9 +1053,9 @@ function ScrollContainer({
|
|
|
741
1053
|
const effectiveDuration = reducedMotion ? 0 : transitionDuration;
|
|
742
1054
|
const { initialize, endTransition } = useScrollStore();
|
|
743
1055
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const prevIndexRef =
|
|
1056
|
+
useScrollStore((s) => s.totalViews);
|
|
1057
|
+
useScrollStore((s) => s.isInitialized);
|
|
1058
|
+
const prevIndexRef = useRef(activeIndex);
|
|
747
1059
|
useWheelHandler();
|
|
748
1060
|
useTouchHandler({ enabled: !enableDragPhysics });
|
|
749
1061
|
useKeyboardHandler();
|
|
@@ -756,14 +1068,39 @@ function ScrollContainer({
|
|
|
756
1068
|
hashPrefix
|
|
757
1069
|
});
|
|
758
1070
|
useFocusManagement({ enabled: enableFocusManagement });
|
|
759
|
-
|
|
1071
|
+
useGlobalProgress({ onProgress });
|
|
1072
|
+
useInfiniteScroll(infiniteScroll);
|
|
1073
|
+
const autoScrollState = useAutoScroll(
|
|
1074
|
+
autoScroll ?? { enabled: false, interval: 5e3 }
|
|
1075
|
+
);
|
|
1076
|
+
const preloadConfig = typeof preload === "boolean" ? preload ? { ahead: 1, behind: 1 } : void 0 : preload;
|
|
1077
|
+
usePreload(preloadConfig ?? {});
|
|
1078
|
+
const mergedGestureConfig = mergeGestureConfig(gestureConfig);
|
|
1079
|
+
useEffect(() => {
|
|
760
1080
|
const timer = setTimeout(() => {
|
|
761
1081
|
initialize();
|
|
762
1082
|
onInitialized?.();
|
|
1083
|
+
isFirstRender.current = false;
|
|
763
1084
|
}, 50);
|
|
764
1085
|
return () => clearTimeout(timer);
|
|
765
1086
|
}, [initialize, onInitialized]);
|
|
766
|
-
|
|
1087
|
+
useEffect(() => {
|
|
1088
|
+
if (!isBrowser) return;
|
|
1089
|
+
const html = document.documentElement;
|
|
1090
|
+
const body = document.body;
|
|
1091
|
+
const originalHtmlOverscroll = html.style.overscrollBehavior;
|
|
1092
|
+
const originalBodyOverscroll = body.style.overscrollBehavior;
|
|
1093
|
+
const originalTouchAction = body.style.touchAction;
|
|
1094
|
+
html.style.overscrollBehavior = "none";
|
|
1095
|
+
body.style.overscrollBehavior = "none";
|
|
1096
|
+
body.style.touchAction = "pan-x pan-y";
|
|
1097
|
+
return () => {
|
|
1098
|
+
html.style.overscrollBehavior = originalHtmlOverscroll;
|
|
1099
|
+
body.style.overscrollBehavior = originalBodyOverscroll;
|
|
1100
|
+
body.style.touchAction = originalTouchAction;
|
|
1101
|
+
};
|
|
1102
|
+
}, [isBrowser]);
|
|
1103
|
+
useEffect(() => {
|
|
767
1104
|
if (prevIndexRef.current !== activeIndex) {
|
|
768
1105
|
onViewChange?.(prevIndexRef.current, activeIndex);
|
|
769
1106
|
const timer = setTimeout(() => {
|
|
@@ -773,37 +1110,34 @@ function ScrollContainer({
|
|
|
773
1110
|
return () => clearTimeout(timer);
|
|
774
1111
|
}
|
|
775
1112
|
}, [activeIndex, effectiveDuration, onViewChange, endTransition]);
|
|
776
|
-
const wrapperStyle =
|
|
1113
|
+
const wrapperStyle = useMemo(() => {
|
|
777
1114
|
const baseOffset = activeIndex * 100;
|
|
778
1115
|
const dragOffset = dragState.isDragging ? dragState.dragOffset * 100 : 0;
|
|
779
1116
|
const transformAxis = orientation === "horizontal" ? "X" : "Y";
|
|
780
1117
|
const sizeUnit = orientation === "horizontal" ? "vw" : "vh";
|
|
1118
|
+
const shouldAnimate = !skipInitialAnimation || !isFirstRender.current;
|
|
1119
|
+
const transitionValue = dragState.isDragging ? "none" : shouldAnimate && effectiveDuration > 0 ? `transform ${effectiveDuration}ms ${transitionEasing}` : "none";
|
|
781
1120
|
return {
|
|
782
1121
|
transform: `translate${transformAxis}(-${baseOffset + dragOffset}${sizeUnit})`,
|
|
783
|
-
transition:
|
|
1122
|
+
transition: transitionValue,
|
|
784
1123
|
height: "100%",
|
|
785
1124
|
width: "100%",
|
|
786
1125
|
display: orientation === "horizontal" ? "flex" : "block",
|
|
787
1126
|
flexDirection: orientation === "horizontal" ? "row" : void 0
|
|
788
1127
|
};
|
|
789
|
-
}, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation]);
|
|
790
|
-
return /* @__PURE__ */ jsx(
|
|
1128
|
+
}, [activeIndex, effectiveDuration, transitionEasing, dragState, orientation, skipInitialAnimation]);
|
|
1129
|
+
return /* @__PURE__ */ jsx(GestureConfigContext.Provider, { value: mergedGestureConfig, children: /* @__PURE__ */ jsx(
|
|
791
1130
|
"div",
|
|
792
1131
|
{
|
|
793
1132
|
ref: containerRef,
|
|
794
1133
|
className: `scroll-container fixed inset-0 overflow-hidden w-screen h-screen ${className}`,
|
|
795
1134
|
role: "main",
|
|
796
1135
|
"aria-label": "Scroll container",
|
|
1136
|
+
"data-auto-scrolling": autoScrollState.isPlaying,
|
|
797
1137
|
children: /* @__PURE__ */ jsx("div", { className: "scroll-wrapper", style: wrapperStyle, children })
|
|
798
1138
|
}
|
|
799
|
-
);
|
|
1139
|
+
) });
|
|
800
1140
|
}
|
|
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
1141
|
function useViewRegistration({
|
|
808
1142
|
config,
|
|
809
1143
|
onActivate,
|
|
@@ -817,7 +1151,7 @@ function useViewRegistration({
|
|
|
817
1151
|
const unregisterView = useScrollStore((s) => s.unregisterView);
|
|
818
1152
|
const activeId = useScrollStore((s) => s.activeId);
|
|
819
1153
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
820
|
-
const callbacksRef =
|
|
1154
|
+
const callbacksRef = useRef({
|
|
821
1155
|
onActivate,
|
|
822
1156
|
onDeactivate,
|
|
823
1157
|
onEnterStart,
|
|
@@ -833,20 +1167,20 @@ function useViewRegistration({
|
|
|
833
1167
|
onExitStart,
|
|
834
1168
|
onExitEnd
|
|
835
1169
|
};
|
|
836
|
-
const wasActiveRef =
|
|
837
|
-
const wasTransitioningRef =
|
|
838
|
-
const configRef =
|
|
1170
|
+
const wasActiveRef = useRef(false);
|
|
1171
|
+
const wasTransitioningRef = useRef(false);
|
|
1172
|
+
const configRef = useRef(config);
|
|
839
1173
|
if (JSON.stringify(config) !== JSON.stringify(configRef.current)) {
|
|
840
1174
|
configRef.current = config;
|
|
841
1175
|
}
|
|
842
|
-
|
|
1176
|
+
useEffect(() => {
|
|
843
1177
|
const currentConfig = configRef.current;
|
|
844
1178
|
registerView(currentConfig);
|
|
845
1179
|
return () => unregisterView(currentConfig.id);
|
|
846
1180
|
}, [registerView, unregisterView, configRef.current]);
|
|
847
1181
|
const viewState = useScrollStore((s) => s.views.find((v) => v.id === config.id));
|
|
848
1182
|
const isActive = activeId === config.id;
|
|
849
|
-
|
|
1183
|
+
useEffect(() => {
|
|
850
1184
|
if (isActive && !wasActiveRef.current) {
|
|
851
1185
|
callbacksRef.current.onActivate?.();
|
|
852
1186
|
} else if (!isActive && wasActiveRef.current) {
|
|
@@ -854,7 +1188,7 @@ function useViewRegistration({
|
|
|
854
1188
|
}
|
|
855
1189
|
wasActiveRef.current = isActive;
|
|
856
1190
|
}, [isActive]);
|
|
857
|
-
|
|
1191
|
+
useEffect(() => {
|
|
858
1192
|
const callbacks = callbacksRef.current;
|
|
859
1193
|
const wasActive = wasActiveRef.current;
|
|
860
1194
|
const wasTransitioning = wasTransitioningRef.current;
|
|
@@ -882,9 +1216,6 @@ function useViewRegistration({
|
|
|
882
1216
|
navigation: viewState?.navigation ?? "unlocked"
|
|
883
1217
|
};
|
|
884
1218
|
}
|
|
885
|
-
|
|
886
|
-
// components/FullView.tsx
|
|
887
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
888
1219
|
function FullView({
|
|
889
1220
|
id,
|
|
890
1221
|
children,
|
|
@@ -897,7 +1228,7 @@ function FullView({
|
|
|
897
1228
|
onExitStart,
|
|
898
1229
|
onExitEnd
|
|
899
1230
|
}) {
|
|
900
|
-
const config =
|
|
1231
|
+
const config = useMemo(
|
|
901
1232
|
() => ({
|
|
902
1233
|
id,
|
|
903
1234
|
type: "full",
|
|
@@ -914,7 +1245,7 @@ function FullView({
|
|
|
914
1245
|
onExitStart,
|
|
915
1246
|
onExitEnd
|
|
916
1247
|
});
|
|
917
|
-
return /* @__PURE__ */
|
|
1248
|
+
return /* @__PURE__ */ jsx(
|
|
918
1249
|
"section",
|
|
919
1250
|
{
|
|
920
1251
|
id,
|
|
@@ -934,9 +1265,6 @@ function FullView({
|
|
|
934
1265
|
}
|
|
935
1266
|
);
|
|
936
1267
|
}
|
|
937
|
-
|
|
938
|
-
// hooks/useMetricsReporter.ts
|
|
939
|
-
import { useRef as useRef8, useCallback as useCallback3, useEffect as useEffect9, useMemo as useMemo4 } from "react";
|
|
940
1268
|
var SCROLL_THROTTLE_MS = 66;
|
|
941
1269
|
function useMetricsReporter({
|
|
942
1270
|
id,
|
|
@@ -945,9 +1273,9 @@ function useMetricsReporter({
|
|
|
945
1273
|
onScrollProgress,
|
|
946
1274
|
throttleMs = SCROLL_THROTTLE_MS
|
|
947
1275
|
}) {
|
|
948
|
-
const scrollRef =
|
|
1276
|
+
const scrollRef = useRef(null);
|
|
949
1277
|
const updateMetrics = useScrollStore((s) => s.updateViewMetrics);
|
|
950
|
-
const measureAndReport =
|
|
1278
|
+
const measureAndReport = useCallback(() => {
|
|
951
1279
|
const el = scrollRef.current;
|
|
952
1280
|
if (!el) return;
|
|
953
1281
|
const metrics = {
|
|
@@ -962,11 +1290,11 @@ function useMetricsReporter({
|
|
|
962
1290
|
onScrollProgress(progress);
|
|
963
1291
|
}
|
|
964
1292
|
}, [id, scrollDirection, updateMetrics, onScrollProgress]);
|
|
965
|
-
const throttledMeasure =
|
|
1293
|
+
const throttledMeasure = useMemo(
|
|
966
1294
|
() => throttle(measureAndReport, throttleMs),
|
|
967
1295
|
[measureAndReport, throttleMs]
|
|
968
1296
|
);
|
|
969
|
-
|
|
1297
|
+
useEffect(() => {
|
|
970
1298
|
const el = scrollRef.current;
|
|
971
1299
|
if (!el) return;
|
|
972
1300
|
const resizeObserver = new ResizeObserver(() => {
|
|
@@ -981,16 +1309,13 @@ function useMetricsReporter({
|
|
|
981
1309
|
el.removeEventListener("scroll", throttledMeasure);
|
|
982
1310
|
};
|
|
983
1311
|
}, [measureAndReport, throttledMeasure]);
|
|
984
|
-
|
|
1312
|
+
useEffect(() => {
|
|
985
1313
|
if (isActive) {
|
|
986
1314
|
measureAndReport();
|
|
987
1315
|
}
|
|
988
1316
|
}, [isActive, measureAndReport]);
|
|
989
1317
|
return { scrollRef, measureAndReport };
|
|
990
1318
|
}
|
|
991
|
-
|
|
992
|
-
// components/ScrollLockedView.tsx
|
|
993
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
994
1319
|
function ScrollLockedView({
|
|
995
1320
|
id,
|
|
996
1321
|
children,
|
|
@@ -1026,14 +1351,14 @@ function ScrollLockedView({
|
|
|
1026
1351
|
onScrollProgress
|
|
1027
1352
|
});
|
|
1028
1353
|
const scrollClasses = scrollDirection === "vertical" ? "overflow-y-auto overflow-x-hidden" : "overflow-x-auto overflow-y-hidden";
|
|
1029
|
-
return /* @__PURE__ */
|
|
1354
|
+
return /* @__PURE__ */ jsx(
|
|
1030
1355
|
"section",
|
|
1031
1356
|
{
|
|
1032
1357
|
id,
|
|
1033
1358
|
className: `relative w-full h-screen ${className}`,
|
|
1034
1359
|
"data-view-type": "scroll-locked",
|
|
1035
1360
|
"data-active": isActive,
|
|
1036
|
-
children: /* @__PURE__ */
|
|
1361
|
+
children: /* @__PURE__ */ jsx(
|
|
1037
1362
|
"div",
|
|
1038
1363
|
{
|
|
1039
1364
|
ref: scrollRef,
|
|
@@ -1045,10 +1370,6 @@ function ScrollLockedView({
|
|
|
1045
1370
|
}
|
|
1046
1371
|
);
|
|
1047
1372
|
}
|
|
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
1373
|
function ControlledView({
|
|
1053
1374
|
id,
|
|
1054
1375
|
children,
|
|
@@ -1065,7 +1386,7 @@ function ControlledView({
|
|
|
1065
1386
|
onExitEnd
|
|
1066
1387
|
}) {
|
|
1067
1388
|
const setExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
|
|
1068
|
-
const config =
|
|
1389
|
+
const config = useMemo(
|
|
1069
1390
|
() => ({
|
|
1070
1391
|
id,
|
|
1071
1392
|
type: "controlled",
|
|
@@ -1089,11 +1410,11 @@ function ControlledView({
|
|
|
1089
1410
|
isActive,
|
|
1090
1411
|
scrollDirection: allowInternalScroll ? scrollDirection : "none"
|
|
1091
1412
|
});
|
|
1092
|
-
|
|
1413
|
+
useEffect(() => {
|
|
1093
1414
|
const lockState = canProceed ? "unlocked" : "locked";
|
|
1094
1415
|
setExplicitLock(id, lockState);
|
|
1095
1416
|
}, [id, canProceed, setExplicitLock]);
|
|
1096
|
-
const overflowClasses =
|
|
1417
|
+
const overflowClasses = useMemo(() => {
|
|
1097
1418
|
if (!allowInternalScroll) return "overflow-hidden";
|
|
1098
1419
|
switch (scrollDirection) {
|
|
1099
1420
|
case "vertical":
|
|
@@ -1104,14 +1425,14 @@ function ControlledView({
|
|
|
1104
1425
|
return "overflow-auto";
|
|
1105
1426
|
}
|
|
1106
1427
|
}, [allowInternalScroll, scrollDirection]);
|
|
1107
|
-
return /* @__PURE__ */
|
|
1428
|
+
return /* @__PURE__ */ jsx(
|
|
1108
1429
|
"section",
|
|
1109
1430
|
{
|
|
1110
1431
|
id,
|
|
1111
1432
|
className: `relative w-full h-screen ${isActive ? "z-10" : "z-0"} ${className}`,
|
|
1112
1433
|
"data-view-type": "controlled",
|
|
1113
1434
|
"data-active": isActive,
|
|
1114
|
-
children: /* @__PURE__ */
|
|
1435
|
+
children: /* @__PURE__ */ jsx(
|
|
1115
1436
|
"div",
|
|
1116
1437
|
{
|
|
1117
1438
|
ref: scrollRef,
|
|
@@ -1127,7 +1448,7 @@ function useViewControl(viewId) {
|
|
|
1127
1448
|
const goToNext = useScrollStore((s) => s.goToNext);
|
|
1128
1449
|
const goToPrevious = useScrollStore((s) => s.goToPrevious);
|
|
1129
1450
|
const goToView = useScrollStore((s) => s.goToView);
|
|
1130
|
-
return
|
|
1451
|
+
return useMemo(
|
|
1131
1452
|
() => ({
|
|
1132
1453
|
unlock: () => setExplicitLock(viewId, "unlocked"),
|
|
1133
1454
|
lock: () => setExplicitLock(viewId, "locked"),
|
|
@@ -1139,9 +1460,6 @@ function useViewControl(viewId) {
|
|
|
1139
1460
|
[viewId, setExplicitLock, goToNext, goToPrevious, goToView]
|
|
1140
1461
|
);
|
|
1141
1462
|
}
|
|
1142
|
-
|
|
1143
|
-
// components/ScrollDebugOverlay.tsx
|
|
1144
|
-
import { jsx as jsx5, jsxs } from "react/jsx-runtime";
|
|
1145
1463
|
function ScrollDebugOverlay({
|
|
1146
1464
|
position = "bottom-left",
|
|
1147
1465
|
visible = true
|
|
@@ -1167,29 +1485,29 @@ function ScrollDebugOverlay({
|
|
|
1167
1485
|
style: { pointerEvents: "none" },
|
|
1168
1486
|
children: [
|
|
1169
1487
|
/* @__PURE__ */ jsxs("div", { className: "text-green-300 font-bold mb-2 flex items-center gap-2", children: [
|
|
1170
|
-
/* @__PURE__ */
|
|
1488
|
+
/* @__PURE__ */ jsx("span", { className: "w-2 h-2 rounded-full bg-green-500 animate-pulse" }),
|
|
1171
1489
|
"ScrollSystem Debug"
|
|
1172
1490
|
] }),
|
|
1173
1491
|
/* @__PURE__ */ jsxs("div", { className: "space-y-1 mb-3", children: [
|
|
1174
|
-
/* @__PURE__ */
|
|
1175
|
-
/* @__PURE__ */
|
|
1176
|
-
/* @__PURE__ */
|
|
1177
|
-
/* @__PURE__ */
|
|
1492
|
+
/* @__PURE__ */ jsx(Row, { label: "initialized", value: isInitialized }),
|
|
1493
|
+
/* @__PURE__ */ jsx(Row, { label: "activeIndex", value: `${activeIndex} / ${totalViews - 1}` }),
|
|
1494
|
+
/* @__PURE__ */ jsx(Row, { label: "transitioning", value: isTransitioning }),
|
|
1495
|
+
/* @__PURE__ */ jsx(Row, { label: "globalLocked", value: isGlobalLocked })
|
|
1178
1496
|
] }),
|
|
1179
1497
|
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__ */
|
|
1498
|
+
/* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Active View" }),
|
|
1499
|
+
/* @__PURE__ */ jsx(Row, { label: "id", value: activeView.id }),
|
|
1500
|
+
/* @__PURE__ */ jsx(Row, { label: "type", value: activeView.type }),
|
|
1501
|
+
/* @__PURE__ */ jsx(Row, { label: "capability", value: activeView.capability }),
|
|
1502
|
+
/* @__PURE__ */ jsx(Row, { label: "navigation", value: activeView.navigation }),
|
|
1503
|
+
/* @__PURE__ */ jsx(Row, { label: "progress", value: `${(activeView.progress * 100).toFixed(0)}%` }),
|
|
1504
|
+
activeView.explicitLock && /* @__PURE__ */ jsx(Row, { label: "explicitLock", value: activeView.explicitLock, highlight: true })
|
|
1187
1505
|
] }),
|
|
1188
1506
|
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__ */
|
|
1507
|
+
/* @__PURE__ */ jsx("div", { className: "text-green-300/70 mb-1", children: "Metrics" }),
|
|
1508
|
+
/* @__PURE__ */ jsx(Row, { label: "scrollHeight", value: activeView.metrics.scrollHeight }),
|
|
1509
|
+
/* @__PURE__ */ jsx(Row, { label: "clientHeight", value: activeView.metrics.clientHeight }),
|
|
1510
|
+
/* @__PURE__ */ jsx(Row, { label: "scrollTop", value: Math.round(activeView.metrics.scrollTop) })
|
|
1193
1511
|
] })
|
|
1194
1512
|
]
|
|
1195
1513
|
}
|
|
@@ -1207,23 +1525,19 @@ function Row({
|
|
|
1207
1525
|
label,
|
|
1208
1526
|
":"
|
|
1209
1527
|
] }),
|
|
1210
|
-
/* @__PURE__ */
|
|
1528
|
+
/* @__PURE__ */ jsx("span", { className: valueColor, children: displayValue })
|
|
1211
1529
|
] });
|
|
1212
1530
|
}
|
|
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
1531
|
function AriaLiveRegion({
|
|
1218
1532
|
template = "Navigated to section {viewIndex} of {totalViews}",
|
|
1219
1533
|
politeness = "polite"
|
|
1220
1534
|
}) {
|
|
1221
|
-
const [announcement, setAnnouncement] =
|
|
1535
|
+
const [announcement, setAnnouncement] = useState("");
|
|
1222
1536
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1223
1537
|
const activeId = useScrollStore((s) => s.activeId);
|
|
1224
1538
|
const totalViews = useScrollStore((s) => s.totalViews);
|
|
1225
1539
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
1226
|
-
|
|
1540
|
+
useEffect(() => {
|
|
1227
1541
|
if (!isTransitioning && activeId) {
|
|
1228
1542
|
const message = template.replace("{viewIndex}", String(activeIndex + 1)).replace("{viewId}", activeId).replace("{totalViews}", String(totalViews));
|
|
1229
1543
|
const timer = setTimeout(() => {
|
|
@@ -1232,7 +1546,7 @@ function AriaLiveRegion({
|
|
|
1232
1546
|
return () => clearTimeout(timer);
|
|
1233
1547
|
}
|
|
1234
1548
|
}, [activeIndex, activeId, totalViews, isTransitioning, template]);
|
|
1235
|
-
return /* @__PURE__ */
|
|
1549
|
+
return /* @__PURE__ */ jsx(
|
|
1236
1550
|
"div",
|
|
1237
1551
|
{
|
|
1238
1552
|
role: "status",
|
|
@@ -1254,10 +1568,6 @@ function AriaLiveRegion({
|
|
|
1254
1568
|
}
|
|
1255
1569
|
);
|
|
1256
1570
|
}
|
|
1257
|
-
|
|
1258
|
-
// components/LazyView.tsx
|
|
1259
|
-
import { useMemo as useMemo6 } from "react";
|
|
1260
|
-
import { Fragment, jsx as jsx7 } from "react/jsx-runtime";
|
|
1261
1571
|
function LazyView({
|
|
1262
1572
|
viewId,
|
|
1263
1573
|
buffer = 1,
|
|
@@ -1266,7 +1576,7 @@ function LazyView({
|
|
|
1266
1576
|
}) {
|
|
1267
1577
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1268
1578
|
const views = useScrollStore((s) => s.views);
|
|
1269
|
-
const shouldRender =
|
|
1579
|
+
const shouldRender = useMemo(() => {
|
|
1270
1580
|
const viewIndex = views.findIndex((v) => v.id === viewId);
|
|
1271
1581
|
if (viewIndex === -1) return false;
|
|
1272
1582
|
const minIndex = Math.max(0, activeIndex - buffer);
|
|
@@ -1274,20 +1584,149 @@ function LazyView({
|
|
|
1274
1584
|
return viewIndex >= minIndex && viewIndex <= maxIndex;
|
|
1275
1585
|
}, [viewId, views, activeIndex, buffer]);
|
|
1276
1586
|
if (!shouldRender) {
|
|
1277
|
-
return /* @__PURE__ */
|
|
1587
|
+
return /* @__PURE__ */ jsx(Fragment, { children: placeholder });
|
|
1278
1588
|
}
|
|
1279
|
-
return /* @__PURE__ */
|
|
1589
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1590
|
+
}
|
|
1591
|
+
function NestedScrollView({
|
|
1592
|
+
id,
|
|
1593
|
+
children,
|
|
1594
|
+
className = "",
|
|
1595
|
+
nestedDirection = "horizontal",
|
|
1596
|
+
enableSnap = true,
|
|
1597
|
+
onItemChange,
|
|
1598
|
+
onActivate,
|
|
1599
|
+
onDeactivate,
|
|
1600
|
+
onEnterStart,
|
|
1601
|
+
onEnterEnd,
|
|
1602
|
+
onExitStart,
|
|
1603
|
+
onExitEnd
|
|
1604
|
+
}) {
|
|
1605
|
+
const containerRef = useRef(null);
|
|
1606
|
+
const nestedContainerRef = useRef(null);
|
|
1607
|
+
const [activeNestedIndex, setActiveNestedIndex] = useState(0);
|
|
1608
|
+
const [isNestedScrolling, setIsNestedScrolling] = useState(false);
|
|
1609
|
+
useScrollStore((s) => s.activeIndex);
|
|
1610
|
+
const views = useScrollStore((s) => s.views);
|
|
1611
|
+
const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
|
|
1612
|
+
const view = views.find((v) => v.id === id);
|
|
1613
|
+
const isActive = view?.isActive ?? false;
|
|
1614
|
+
useViewRegistration({
|
|
1615
|
+
config: {
|
|
1616
|
+
id,
|
|
1617
|
+
type: "nested",
|
|
1618
|
+
nestedConfig: {
|
|
1619
|
+
direction: nestedDirection,
|
|
1620
|
+
enableSnap,
|
|
1621
|
+
onItemChange
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
onActivate,
|
|
1625
|
+
onDeactivate,
|
|
1626
|
+
onEnterStart,
|
|
1627
|
+
onEnterEnd,
|
|
1628
|
+
onExitStart,
|
|
1629
|
+
onExitEnd
|
|
1630
|
+
});
|
|
1631
|
+
useEffect(() => {
|
|
1632
|
+
if (isActive) {
|
|
1633
|
+
onActivate?.();
|
|
1634
|
+
} else {
|
|
1635
|
+
onDeactivate?.();
|
|
1636
|
+
}
|
|
1637
|
+
}, [isActive, onActivate, onDeactivate]);
|
|
1638
|
+
const handleNestedScroll = useCallback((e) => {
|
|
1639
|
+
if (!nestedContainerRef.current) return;
|
|
1640
|
+
const container = nestedContainerRef.current;
|
|
1641
|
+
const scrollPos = nestedDirection === "horizontal" ? container.scrollLeft : container.scrollTop;
|
|
1642
|
+
const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
|
|
1643
|
+
if (itemSize > 0) {
|
|
1644
|
+
const newIndex = Math.round(scrollPos / itemSize);
|
|
1645
|
+
if (newIndex !== activeNestedIndex) {
|
|
1646
|
+
setActiveNestedIndex(newIndex);
|
|
1647
|
+
onItemChange?.(newIndex);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}, [nestedDirection, activeNestedIndex, onItemChange]);
|
|
1651
|
+
const handleTouchStart = useCallback((e) => {
|
|
1652
|
+
setIsNestedScrolling(true);
|
|
1653
|
+
setGlobalLock(true);
|
|
1654
|
+
}, [setGlobalLock]);
|
|
1655
|
+
const handleTouchEnd = useCallback(() => {
|
|
1656
|
+
setTimeout(() => {
|
|
1657
|
+
setIsNestedScrolling(false);
|
|
1658
|
+
setGlobalLock(false);
|
|
1659
|
+
}, 100);
|
|
1660
|
+
}, [setGlobalLock]);
|
|
1661
|
+
useCallback((index) => {
|
|
1662
|
+
if (!nestedContainerRef.current) return;
|
|
1663
|
+
const container = nestedContainerRef.current;
|
|
1664
|
+
const itemSize = nestedDirection === "horizontal" ? container.clientWidth : container.clientHeight;
|
|
1665
|
+
const scrollPos = index * itemSize;
|
|
1666
|
+
container.scrollTo({
|
|
1667
|
+
[nestedDirection === "horizontal" ? "left" : "top"]: scrollPos,
|
|
1668
|
+
behavior: "smooth"
|
|
1669
|
+
});
|
|
1670
|
+
}, [nestedDirection]);
|
|
1671
|
+
const nestedScrollStyle = {
|
|
1672
|
+
display: "flex",
|
|
1673
|
+
flexDirection: nestedDirection === "horizontal" ? "row" : "column",
|
|
1674
|
+
overflow: nestedDirection === "horizontal" ? "auto hidden" : "hidden auto",
|
|
1675
|
+
scrollSnapType: enableSnap ? `${nestedDirection === "horizontal" ? "x" : "y"} mandatory` : "none",
|
|
1676
|
+
scrollBehavior: "smooth",
|
|
1677
|
+
WebkitOverflowScrolling: "touch",
|
|
1678
|
+
width: "100%",
|
|
1679
|
+
height: "100%"
|
|
1680
|
+
};
|
|
1681
|
+
return /* @__PURE__ */ jsx(
|
|
1682
|
+
"div",
|
|
1683
|
+
{
|
|
1684
|
+
ref: containerRef,
|
|
1685
|
+
className: `scroll-view nested-scroll-view h-screen w-screen flex-shrink-0 relative ${className}`,
|
|
1686
|
+
"data-view-id": id,
|
|
1687
|
+
"data-view-type": "nested",
|
|
1688
|
+
"data-nested-direction": nestedDirection,
|
|
1689
|
+
role: "region",
|
|
1690
|
+
"aria-label": `Nested scroll view ${id}`,
|
|
1691
|
+
tabIndex: 0,
|
|
1692
|
+
children: /* @__PURE__ */ jsx(
|
|
1693
|
+
"div",
|
|
1694
|
+
{
|
|
1695
|
+
ref: nestedContainerRef,
|
|
1696
|
+
className: "nested-scroll-container",
|
|
1697
|
+
style: nestedScrollStyle,
|
|
1698
|
+
onScroll: handleNestedScroll,
|
|
1699
|
+
onTouchStart: handleTouchStart,
|
|
1700
|
+
onTouchEnd: handleTouchEnd,
|
|
1701
|
+
children
|
|
1702
|
+
}
|
|
1703
|
+
)
|
|
1704
|
+
}
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
function NestedScrollItem({
|
|
1708
|
+
children,
|
|
1709
|
+
className = ""
|
|
1710
|
+
}) {
|
|
1711
|
+
return /* @__PURE__ */ jsx(
|
|
1712
|
+
"div",
|
|
1713
|
+
{
|
|
1714
|
+
className: `nested-scroll-item flex-shrink-0 w-full h-full ${className}`,
|
|
1715
|
+
style: {
|
|
1716
|
+
scrollSnapAlign: "start",
|
|
1717
|
+
scrollSnapStop: "always"
|
|
1718
|
+
},
|
|
1719
|
+
children
|
|
1720
|
+
}
|
|
1721
|
+
);
|
|
1280
1722
|
}
|
|
1281
|
-
|
|
1282
|
-
// hooks/useNavigation.ts
|
|
1283
|
-
import { useCallback as useCallback4, useMemo as useMemo7 } from "react";
|
|
1284
1723
|
function useNavigation() {
|
|
1285
1724
|
const goToView = useScrollStore((s) => s.goToView);
|
|
1286
1725
|
const goToNextAction = useScrollStore((s) => s.goToNext);
|
|
1287
1726
|
const goToPreviousAction = useScrollStore((s) => s.goToPrevious);
|
|
1288
1727
|
const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
|
|
1289
|
-
const lockScroll =
|
|
1290
|
-
const unlockScroll =
|
|
1728
|
+
const lockScroll = useCallback(() => setGlobalLock(true), [setGlobalLock]);
|
|
1729
|
+
const unlockScroll = useCallback(() => setGlobalLock(false), [setGlobalLock]);
|
|
1291
1730
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1292
1731
|
const activeId = useScrollStore((s) => s.activeId);
|
|
1293
1732
|
const totalViews = useScrollStore((s) => s.totalViews);
|
|
@@ -1295,16 +1734,16 @@ function useNavigation() {
|
|
|
1295
1734
|
const isScrollLocked = useScrollStore((s) => s.isGlobalLocked);
|
|
1296
1735
|
const canNavigateNext = useScrollStore(selectCanNavigateNext);
|
|
1297
1736
|
const canNavigatePrevious = useScrollStore(selectCanNavigatePrevious);
|
|
1298
|
-
const goToNext =
|
|
1737
|
+
const goToNext = useCallback(() => {
|
|
1299
1738
|
return goToNextAction();
|
|
1300
1739
|
}, [goToNextAction]);
|
|
1301
|
-
const goToPrevious =
|
|
1740
|
+
const goToPrevious = useCallback(() => {
|
|
1302
1741
|
return goToPreviousAction();
|
|
1303
1742
|
}, [goToPreviousAction]);
|
|
1304
1743
|
const isFirstView = activeIndex === 0;
|
|
1305
1744
|
const isLastView = activeIndex === totalViews - 1;
|
|
1306
1745
|
const progress = totalViews > 1 ? activeIndex / (totalViews - 1) : 0;
|
|
1307
|
-
return
|
|
1746
|
+
return useMemo(
|
|
1308
1747
|
() => ({
|
|
1309
1748
|
// Acciones de navegación
|
|
1310
1749
|
goToView,
|
|
@@ -1374,18 +1813,15 @@ function useActiveViewProgress() {
|
|
|
1374
1813
|
viewType: activeView?.type
|
|
1375
1814
|
};
|
|
1376
1815
|
}
|
|
1377
|
-
|
|
1378
|
-
// hooks/useScrollAnalytics.ts
|
|
1379
|
-
import { useEffect as useEffect12, useRef as useRef9, useCallback as useCallback5 } from "react";
|
|
1380
1816
|
function useScrollAnalytics(options = {}) {
|
|
1381
1817
|
const { onViewEnter, onViewExit, enabled = true } = options;
|
|
1382
1818
|
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1383
1819
|
const activeId = useScrollStore((s) => s.activeId);
|
|
1384
1820
|
const isTransitioning = useScrollStore((s) => s.isTransitioning);
|
|
1385
|
-
const enterTimeRef =
|
|
1386
|
-
const prevIndexRef =
|
|
1387
|
-
const prevIdRef =
|
|
1388
|
-
const createAnalytics =
|
|
1821
|
+
const enterTimeRef = useRef(Date.now());
|
|
1822
|
+
const prevIndexRef = useRef(activeIndex);
|
|
1823
|
+
const prevIdRef = useRef(activeId);
|
|
1824
|
+
const createAnalytics = useCallback((viewId, viewIndex, enterTime, isActive, exitTime = null) => ({
|
|
1389
1825
|
viewId,
|
|
1390
1826
|
viewIndex,
|
|
1391
1827
|
enterTime,
|
|
@@ -1393,7 +1829,7 @@ function useScrollAnalytics(options = {}) {
|
|
|
1393
1829
|
duration: exitTime ? (exitTime - enterTime) / 1e3 : 0,
|
|
1394
1830
|
isActive
|
|
1395
1831
|
}), []);
|
|
1396
|
-
|
|
1832
|
+
useEffect(() => {
|
|
1397
1833
|
if (!enabled) return;
|
|
1398
1834
|
if (!isTransitioning && (activeIndex !== prevIndexRef.current || activeId !== prevIdRef.current)) {
|
|
1399
1835
|
const now = Date.now();
|
|
@@ -1428,36 +1864,205 @@ function useScrollAnalytics(options = {}) {
|
|
|
1428
1864
|
getTimeInView: () => (Date.now() - enterTimeRef.current) / 1e3
|
|
1429
1865
|
};
|
|
1430
1866
|
}
|
|
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
|
-
|
|
1867
|
+
function useScrollLock() {
|
|
1868
|
+
const isLocked = useScrollStore((s) => s.isGlobalLocked);
|
|
1869
|
+
const setGlobalLock = useScrollStore((s) => s.setGlobalLock);
|
|
1870
|
+
const setViewExplicitLock = useScrollStore((s) => s.setViewExplicitLock);
|
|
1871
|
+
const lock = useCallback(() => {
|
|
1872
|
+
setGlobalLock(true);
|
|
1873
|
+
}, [setGlobalLock]);
|
|
1874
|
+
const unlock = useCallback(() => {
|
|
1875
|
+
setGlobalLock(false);
|
|
1876
|
+
}, [setGlobalLock]);
|
|
1877
|
+
const toggle = useCallback(() => {
|
|
1878
|
+
setGlobalLock(!isLocked);
|
|
1879
|
+
}, [setGlobalLock, isLocked]);
|
|
1880
|
+
const lockView = useCallback((viewId) => {
|
|
1881
|
+
setViewExplicitLock(viewId, "locked");
|
|
1882
|
+
}, [setViewExplicitLock]);
|
|
1883
|
+
const unlockView = useCallback((viewId) => {
|
|
1884
|
+
setViewExplicitLock(viewId, "unlocked");
|
|
1885
|
+
}, [setViewExplicitLock]);
|
|
1886
|
+
return {
|
|
1887
|
+
isLocked,
|
|
1888
|
+
lock,
|
|
1889
|
+
unlock,
|
|
1890
|
+
toggle,
|
|
1891
|
+
lockView,
|
|
1892
|
+
unlockView
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
var DEFAULT_CONFIG4 = {
|
|
1896
|
+
speed: 0.5,
|
|
1897
|
+
direction: "vertical",
|
|
1898
|
+
offset: 0,
|
|
1899
|
+
easing: "linear"
|
|
1900
|
+
};
|
|
1901
|
+
var easingFunctions = {
|
|
1902
|
+
linear: (t) => t,
|
|
1903
|
+
easeOut: (t) => 1 - Math.pow(1 - t, 2),
|
|
1904
|
+
easeInOut: (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2
|
|
1463
1905
|
};
|
|
1906
|
+
function useParallax(viewId, config = {}) {
|
|
1907
|
+
const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
|
|
1908
|
+
const { speed, direction, offset, easing } = mergedConfig;
|
|
1909
|
+
const views = useScrollStore((s) => s.views);
|
|
1910
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1911
|
+
useScrollStore((s) => s.globalProgress);
|
|
1912
|
+
const result = useMemo(() => {
|
|
1913
|
+
const view = views.find((v) => v.id === viewId);
|
|
1914
|
+
if (!view) {
|
|
1915
|
+
return {
|
|
1916
|
+
transform: 0,
|
|
1917
|
+
style: {},
|
|
1918
|
+
progress: 0
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
const viewOffset = view.index - activeIndex;
|
|
1922
|
+
const internalProgress = view.isActive ? view.progress : 0;
|
|
1923
|
+
const combinedProgress = viewOffset + internalProgress;
|
|
1924
|
+
const easingFn = easingFunctions[easing];
|
|
1925
|
+
const easedProgress = easingFn(Math.abs(combinedProgress)) * Math.sign(combinedProgress);
|
|
1926
|
+
const maxDistance = 100;
|
|
1927
|
+
const transformValue = easedProgress * maxDistance * speed + offset;
|
|
1928
|
+
const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
|
|
1929
|
+
const style = {
|
|
1930
|
+
transform: transformProperty,
|
|
1931
|
+
willChange: "transform"
|
|
1932
|
+
};
|
|
1933
|
+
return {
|
|
1934
|
+
transform: transformValue,
|
|
1935
|
+
style,
|
|
1936
|
+
progress: combinedProgress
|
|
1937
|
+
};
|
|
1938
|
+
}, [viewId, views, activeIndex, speed, direction, offset, easing]);
|
|
1939
|
+
return result;
|
|
1940
|
+
}
|
|
1941
|
+
function useActiveParallax(config = {}) {
|
|
1942
|
+
const mergedConfig = { ...DEFAULT_CONFIG4, ...config };
|
|
1943
|
+
const { speed, direction, offset, easing } = mergedConfig;
|
|
1944
|
+
const globalProgress = useScrollStore((s) => s.globalProgress);
|
|
1945
|
+
const activeIndex = useScrollStore((s) => s.activeIndex);
|
|
1946
|
+
const views = useScrollStore((s) => s.views);
|
|
1947
|
+
return useMemo(() => {
|
|
1948
|
+
const activeView = views[activeIndex];
|
|
1949
|
+
const viewProgress = activeView?.progress ?? 0;
|
|
1950
|
+
const easingFn = easingFunctions[easing];
|
|
1951
|
+
const easedProgress = easingFn(viewProgress);
|
|
1952
|
+
const maxDistance = 100;
|
|
1953
|
+
const transformValue = easedProgress * maxDistance * speed + offset;
|
|
1954
|
+
const transformProperty = direction === "vertical" ? `translateY(${transformValue}px)` : `translateX(${transformValue}px)`;
|
|
1955
|
+
const style = {
|
|
1956
|
+
transform: transformProperty,
|
|
1957
|
+
willChange: "transform"
|
|
1958
|
+
};
|
|
1959
|
+
return {
|
|
1960
|
+
transform: transformValue,
|
|
1961
|
+
style,
|
|
1962
|
+
progress: viewProgress
|
|
1963
|
+
};
|
|
1964
|
+
}, [globalProgress, activeIndex, views, speed, direction, offset, easing]);
|
|
1965
|
+
}
|
|
1966
|
+
function useSnapPoints(options) {
|
|
1967
|
+
const { viewId, points, snapThreshold = 0.1, smoothScroll = true } = options;
|
|
1968
|
+
const scrollContainerRef = useRef(null);
|
|
1969
|
+
const lastActivePointRef = useRef(null);
|
|
1970
|
+
const views = useScrollStore((s) => s.views);
|
|
1971
|
+
const setActiveSnapPoint = useScrollStore((s) => s.setActiveSnapPoint);
|
|
1972
|
+
const view = useMemo(() => views.find((v) => v.id === viewId), [views, viewId]);
|
|
1973
|
+
const viewProgress = view?.progress ?? 0;
|
|
1974
|
+
view?.activeSnapPointId ?? null;
|
|
1975
|
+
const sortedPoints = useMemo(
|
|
1976
|
+
() => [...points].sort((a, b) => a.position - b.position),
|
|
1977
|
+
[points]
|
|
1978
|
+
);
|
|
1979
|
+
const activePointData = useMemo(() => {
|
|
1980
|
+
if (sortedPoints.length === 0) {
|
|
1981
|
+
return { point: null, index: -1 };
|
|
1982
|
+
}
|
|
1983
|
+
let closestIndex = 0;
|
|
1984
|
+
let closestDistance = Math.abs(viewProgress - sortedPoints[0].position);
|
|
1985
|
+
for (let i = 1; i < sortedPoints.length; i++) {
|
|
1986
|
+
const distance = Math.abs(viewProgress - sortedPoints[i].position);
|
|
1987
|
+
if (distance < closestDistance) {
|
|
1988
|
+
closestDistance = distance;
|
|
1989
|
+
closestIndex = i;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (closestDistance <= snapThreshold) {
|
|
1993
|
+
return { point: sortedPoints[closestIndex], index: closestIndex };
|
|
1994
|
+
}
|
|
1995
|
+
for (let i = sortedPoints.length - 1; i >= 0; i--) {
|
|
1996
|
+
if (viewProgress >= sortedPoints[i].position - snapThreshold) {
|
|
1997
|
+
return { point: sortedPoints[i], index: i };
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return { point: sortedPoints[0], index: 0 };
|
|
2001
|
+
}, [viewProgress, sortedPoints, snapThreshold]);
|
|
2002
|
+
useEffect(() => {
|
|
2003
|
+
const newPointId = activePointData.point?.id ?? null;
|
|
2004
|
+
if (newPointId !== lastActivePointRef.current) {
|
|
2005
|
+
if (lastActivePointRef.current) {
|
|
2006
|
+
const prevPoint = sortedPoints.find((p) => p.id === lastActivePointRef.current);
|
|
2007
|
+
prevPoint?.onLeave?.();
|
|
2008
|
+
}
|
|
2009
|
+
setActiveSnapPoint(viewId, newPointId);
|
|
2010
|
+
if (newPointId) {
|
|
2011
|
+
const newPoint = sortedPoints.find((p) => p.id === newPointId);
|
|
2012
|
+
newPoint?.onReach?.();
|
|
2013
|
+
}
|
|
2014
|
+
lastActivePointRef.current = newPointId;
|
|
2015
|
+
}
|
|
2016
|
+
}, [activePointData.point?.id, viewId, sortedPoints, setActiveSnapPoint]);
|
|
2017
|
+
const goToPoint = useCallback((pointId) => {
|
|
2018
|
+
const point = sortedPoints.find((p) => p.id === pointId);
|
|
2019
|
+
if (!point || !scrollContainerRef.current) return;
|
|
2020
|
+
const container = scrollContainerRef.current;
|
|
2021
|
+
const maxScroll = container.scrollHeight - container.clientHeight;
|
|
2022
|
+
const targetScroll = point.position * maxScroll;
|
|
2023
|
+
container.scrollTo({
|
|
2024
|
+
top: targetScroll,
|
|
2025
|
+
behavior: smoothScroll ? "smooth" : "auto"
|
|
2026
|
+
});
|
|
2027
|
+
}, [sortedPoints, smoothScroll]);
|
|
2028
|
+
const goToNextPoint = useCallback(() => {
|
|
2029
|
+
const nextIndex = Math.min(activePointData.index + 1, sortedPoints.length - 1);
|
|
2030
|
+
const nextPoint = sortedPoints[nextIndex];
|
|
2031
|
+
if (nextPoint) {
|
|
2032
|
+
goToPoint(nextPoint.id);
|
|
2033
|
+
}
|
|
2034
|
+
}, [activePointData.index, sortedPoints, goToPoint]);
|
|
2035
|
+
const goToPrevPoint = useCallback(() => {
|
|
2036
|
+
const prevIndex = Math.max(activePointData.index - 1, 0);
|
|
2037
|
+
const prevPoint = sortedPoints[prevIndex];
|
|
2038
|
+
if (prevPoint) {
|
|
2039
|
+
goToPoint(prevPoint.id);
|
|
2040
|
+
}
|
|
2041
|
+
}, [activePointData.index, sortedPoints, goToPoint]);
|
|
2042
|
+
const pointsWithState = useMemo(
|
|
2043
|
+
() => sortedPoints.map((point) => ({
|
|
2044
|
+
...point,
|
|
2045
|
+
isActive: point.id === activePointData.point?.id
|
|
2046
|
+
})),
|
|
2047
|
+
[sortedPoints, activePointData.point?.id]
|
|
2048
|
+
);
|
|
2049
|
+
return {
|
|
2050
|
+
activePoint: activePointData.point,
|
|
2051
|
+
activeIndex: activePointData.index,
|
|
2052
|
+
goToPoint,
|
|
2053
|
+
goToNextPoint,
|
|
2054
|
+
goToPrevPoint,
|
|
2055
|
+
points: pointsWithState,
|
|
2056
|
+
progress: viewProgress
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
function createSnapPoints(positions, options) {
|
|
2060
|
+
const { prefix = "snap", labels } = options ?? {};
|
|
2061
|
+
return positions.map((position, index) => ({
|
|
2062
|
+
id: `${prefix}-${index}`,
|
|
2063
|
+
position,
|
|
2064
|
+
label: labels?.[index] ?? `Section ${index + 1}`
|
|
2065
|
+
}));
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
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 };
|