react-view-transition-swiper 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.mts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +973 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +971 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import { forwardRef, useRef, useLayoutEffect, useState, useCallback, useEffect, useImperativeHandle } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/index.tsx
|
|
5
|
+
var logger = {
|
|
6
|
+
debug: (...args) => console.debug(...args),
|
|
7
|
+
warn: (...args) => console.warn(...args),
|
|
8
|
+
error: (...args) => console.error(...args)
|
|
9
|
+
};
|
|
10
|
+
var _instanceCounter = 0;
|
|
11
|
+
var STATIC_STYLE_ID = "swipe-deck-v6";
|
|
12
|
+
var STATIC_CSS = `
|
|
13
|
+
/*
|
|
14
|
+
SwipeDeck v6.2 \u2014 static styles
|
|
15
|
+
Import this file once in your app: import "swipe-deck/SwipeDeck.css"
|
|
16
|
+
All class names are prefixed with sd- to avoid collisions.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
.sd-root {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
height: 100%;
|
|
23
|
+
touch-action: pan-y;
|
|
24
|
+
user-select: none;
|
|
25
|
+
-webkit-user-select: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
No overflow:hidden on .sd-stage \u2014 it clips the live card but NOT
|
|
30
|
+
::view-transition-old which renders in a top-layer pseudo-element.
|
|
31
|
+
That mismatch makes VT snapshots look different from the live element.
|
|
32
|
+
*/
|
|
33
|
+
.sd-stage {
|
|
34
|
+
position: relative;
|
|
35
|
+
width: 100%;
|
|
36
|
+
flex: 1;
|
|
37
|
+
min-height: 0;
|
|
38
|
+
/* touch-action set via inline style \u2014 varies by navAxis */
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sd-sizer {
|
|
42
|
+
width: 100%;
|
|
43
|
+
visibility: hidden;
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.sd-card {
|
|
49
|
+
position: absolute;
|
|
50
|
+
inset: 0;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
z-index: 0;
|
|
53
|
+
visibility: hidden;
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
will-change: transform;
|
|
56
|
+
overflow-y: auto;
|
|
57
|
+
overflow-x: hidden;
|
|
58
|
+
/* touch-action set via inline style on SlideSlot \u2014 varies by navAxis */
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.sd-card.sd-current {
|
|
62
|
+
z-index: 2;
|
|
63
|
+
visibility: visible;
|
|
64
|
+
pointer-events: auto;
|
|
65
|
+
cursor: grab;
|
|
66
|
+
}
|
|
67
|
+
.sd-card.sd-current.sd-no-drag { cursor: default; }
|
|
68
|
+
.sd-card.sd-current.sd-grabbing { cursor: grabbing; }
|
|
69
|
+
.sd-card.sd-current.sd-drag-pending { cursor: wait; }
|
|
70
|
+
|
|
71
|
+
.sd-card.sd-peek {
|
|
72
|
+
z-index: 1;
|
|
73
|
+
visibility: visible;
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Consumer-hookable drag feedback classes \u2014 add your own styles */
|
|
78
|
+
.sd-card.sd-current.sd-pull-fwd {}
|
|
79
|
+
.sd-card.sd-current.sd-pull-back {}
|
|
80
|
+
|
|
81
|
+
/* \u2500\u2500 View transition exit keyframes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
82
|
+
/* X-axis */
|
|
83
|
+
@keyframes sd-out-left { to { transform: translateX(-100%); } }
|
|
84
|
+
@keyframes sd-out-right { to { transform: translateX( 100%); } }
|
|
85
|
+
@keyframes sd-out-left-fade { to { transform: translateX(-80%); opacity: 0; } }
|
|
86
|
+
@keyframes sd-out-right-fade { to { transform: translateX( 80%); opacity: 0; } }
|
|
87
|
+
|
|
88
|
+
/* Y-axis */
|
|
89
|
+
@keyframes sd-out-up { to { transform: translateY(-100%); } }
|
|
90
|
+
@keyframes sd-out-down { to { transform: translateY( 100%); } }
|
|
91
|
+
@keyframes sd-out-up-fade { to { transform: translateY(-80%); opacity: 0; } }
|
|
92
|
+
@keyframes sd-out-down-fade { to { transform: translateY( 80%); opacity: 0; } }
|
|
93
|
+
|
|
94
|
+
/* \u2500\u2500 Navigation dots \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
95
|
+
.sd-dots {
|
|
96
|
+
display: flex;
|
|
97
|
+
gap: 7px;
|
|
98
|
+
margin-top: 12px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sd-dot {
|
|
102
|
+
width: 6px;
|
|
103
|
+
height: 6px;
|
|
104
|
+
border-radius: 50%;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
transition: opacity 0.25s, transform 0.25s;
|
|
107
|
+
opacity: 0.3;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.sd-dot.sd-dot-active {
|
|
111
|
+
opacity: 1;
|
|
112
|
+
transform: scale(1.4);
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
function injectStaticStyles() {
|
|
116
|
+
if (typeof document === "undefined") return;
|
|
117
|
+
if (document.getElementById(STATIC_STYLE_ID)) return;
|
|
118
|
+
const style = document.createElement("style");
|
|
119
|
+
style.id = STATIC_STYLE_ID;
|
|
120
|
+
style.textContent = STATIC_CSS;
|
|
121
|
+
document.head.appendChild(style);
|
|
122
|
+
}
|
|
123
|
+
function injectVTStyles(instanceId, vtName, ms, axis) {
|
|
124
|
+
if (typeof document === "undefined") return;
|
|
125
|
+
const styleId = `sd-vt-${instanceId}`;
|
|
126
|
+
document.getElementById(styleId)?.remove();
|
|
127
|
+
const dur = `${ms}ms`;
|
|
128
|
+
const dir = `data-sd-dir-${instanceId}`;
|
|
129
|
+
const fade = `data-sd-fade-${instanceId}`;
|
|
130
|
+
const fwdAnim = axis === "y" ? "sd-out-up" : "sd-out-left";
|
|
131
|
+
const backAnim = axis === "y" ? "sd-out-down" : "sd-out-right";
|
|
132
|
+
const fwdFade = axis === "y" ? "sd-out-up-fade" : "sd-out-left-fade";
|
|
133
|
+
const backFade = axis === "y" ? "sd-out-down-fade" : "sd-out-right-fade";
|
|
134
|
+
const style = document.createElement("style");
|
|
135
|
+
style.id = styleId;
|
|
136
|
+
style.textContent = `
|
|
137
|
+
::view-transition-old(${vtName}),
|
|
138
|
+
::view-transition-new(${vtName}) { animation: none; }
|
|
139
|
+
::view-transition-new(${vtName}) { z-index: 0; }
|
|
140
|
+
::view-transition-old(${vtName}) { z-index: 1; }
|
|
141
|
+
html[${dir}="forward"]::view-transition-old(${vtName}) {
|
|
142
|
+
animation: ${fwdAnim} ${dur} cubic-bezier(0.4,0,0.2,1) forwards;
|
|
143
|
+
}
|
|
144
|
+
html[${dir}="back"]::view-transition-old(${vtName}) {
|
|
145
|
+
animation: ${backAnim} ${dur} cubic-bezier(0.4,0,0.2,1) forwards;
|
|
146
|
+
}
|
|
147
|
+
html[${dir}="forward"][${fade}]::view-transition-old(${vtName}) {
|
|
148
|
+
animation: ${fwdFade} ${dur} cubic-bezier(0.4,0,0.2,1) forwards;
|
|
149
|
+
}
|
|
150
|
+
html[${dir}="back"][${fade}]::view-transition-old(${vtName}) {
|
|
151
|
+
animation: ${backFade} ${dur} cubic-bezier(0.4,0,0.2,1) forwards;
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
document.head.appendChild(style);
|
|
155
|
+
}
|
|
156
|
+
function removeVTStyles(instanceId) {
|
|
157
|
+
document.getElementById(`sd-vt-${instanceId}`)?.remove();
|
|
158
|
+
}
|
|
159
|
+
var VELOCITY_COMMIT = 0.45;
|
|
160
|
+
var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
161
|
+
function rubberBand(x, dim) {
|
|
162
|
+
const sign = x > 0 ? 1 : -1;
|
|
163
|
+
const abs = Math.abs(x);
|
|
164
|
+
return sign * (1 - 1 / (abs / dim * 0.55 + 1)) * dim;
|
|
165
|
+
}
|
|
166
|
+
function layerFor(i, cur, peek) {
|
|
167
|
+
if (i === cur) return "current";
|
|
168
|
+
if (peek !== null && i === peek) return "peek";
|
|
169
|
+
return "hidden";
|
|
170
|
+
}
|
|
171
|
+
function SlideSlot({ slide, layer, keepDom, dragDisabled, navAxis, children, refCallback }) {
|
|
172
|
+
const cls = [
|
|
173
|
+
"sd-card",
|
|
174
|
+
layer === "current" && "sd-current",
|
|
175
|
+
layer === "peek" && "sd-peek",
|
|
176
|
+
layer === "current" && dragDisabled && "sd-no-drag"
|
|
177
|
+
].filter(Boolean).join(" ");
|
|
178
|
+
const cardTouchAction = navAxis === "y" ? "pan-x" : "pan-y";
|
|
179
|
+
return /* @__PURE__ */ jsx("div", { ref: refCallback, className: cls, "data-slide-id": slide.id, style: { touchAction: cardTouchAction }, children: keepDom || layer !== "hidden" ? children : null });
|
|
180
|
+
}
|
|
181
|
+
var SwipeDeck = forwardRef(
|
|
182
|
+
function SwipeDeck2({
|
|
183
|
+
width = 390,
|
|
184
|
+
showDots = true,
|
|
185
|
+
fade = false,
|
|
186
|
+
dimOnDrag = false,
|
|
187
|
+
maxDragRatio = 1,
|
|
188
|
+
dragDisabled = false,
|
|
189
|
+
keyboardDisabled = false,
|
|
190
|
+
dragDelayMs = 0,
|
|
191
|
+
transitionMs = 360,
|
|
192
|
+
navAxis = "x",
|
|
193
|
+
onSlideChange,
|
|
194
|
+
onError,
|
|
195
|
+
className = ""
|
|
196
|
+
}, externalRef) {
|
|
197
|
+
injectStaticStyles();
|
|
198
|
+
const instanceId = useRef(-1);
|
|
199
|
+
if (instanceId.current === -1) instanceId.current = ++_instanceCounter;
|
|
200
|
+
const vtName = `sd-card-${instanceId.current}`;
|
|
201
|
+
injectVTStyles(instanceId.current, vtName, transitionMs, navAxis);
|
|
202
|
+
useLayoutEffect(() => () => removeVTStyles(instanceId.current), []);
|
|
203
|
+
const [slides, setSlides] = useState([]);
|
|
204
|
+
const [current, setCurrent] = useState(0);
|
|
205
|
+
const [peekIndex, setPeekIndex] = useState(null);
|
|
206
|
+
const slidesRef = useRef([]);
|
|
207
|
+
const currentRef = useRef(0);
|
|
208
|
+
const peekRef = useRef(null);
|
|
209
|
+
const userPeekOverride = useRef(null);
|
|
210
|
+
const transitioningRef = useRef(false);
|
|
211
|
+
const lastShownAt = useRef([]);
|
|
212
|
+
const cardRefs = useRef(/* @__PURE__ */ new Map());
|
|
213
|
+
const keepDomIds = useRef(/* @__PURE__ */ new Set());
|
|
214
|
+
const dragPermissions = useRef(/* @__PURE__ */ new Map());
|
|
215
|
+
const stageRef = useRef(null);
|
|
216
|
+
const heightRef = useRef(0);
|
|
217
|
+
const dragRef = useRef(null);
|
|
218
|
+
const dragTimerRef = useRef(null);
|
|
219
|
+
const lastDragXRef = useRef(0);
|
|
220
|
+
const lastDragYRef = useRef(0);
|
|
221
|
+
const pendingRemoveNavTo = useRef(null);
|
|
222
|
+
const pendingRemoveIds = useRef(/* @__PURE__ */ new Set());
|
|
223
|
+
const pendingNavDelta = useRef(null);
|
|
224
|
+
const navWithVTRef = useRef(() => {
|
|
225
|
+
});
|
|
226
|
+
const navInstantRef = useRef(() => {
|
|
227
|
+
});
|
|
228
|
+
const onErrorRef = useRef(void 0);
|
|
229
|
+
onErrorRef.current = onError;
|
|
230
|
+
const reportError = (error) => {
|
|
231
|
+
logger.error(`[SD] ERROR ${error.code}: ${error.message}`, error.detail ?? "");
|
|
232
|
+
onErrorRef.current?.(error);
|
|
233
|
+
};
|
|
234
|
+
if (lastShownAt.current.length < slides.length) {
|
|
235
|
+
lastShownAt.current = [
|
|
236
|
+
...lastShownAt.current,
|
|
237
|
+
...Array(slides.length - lastShownAt.current.length).fill(0)
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
const resolvePeek = useCallback((cur, hint) => {
|
|
241
|
+
const total = slidesRef.current.length;
|
|
242
|
+
if (total <= 1) return null;
|
|
243
|
+
const ov = userPeekOverride.current;
|
|
244
|
+
if (ov !== null && ov >= 0 && ov < total && ov !== cur) return ov;
|
|
245
|
+
if (hint >= 0 && hint < total && hint !== cur) return hint;
|
|
246
|
+
const prev = cur - 1;
|
|
247
|
+
const next = cur + 1;
|
|
248
|
+
if (prev < 0 && next >= total) return null;
|
|
249
|
+
if (prev < 0) return next;
|
|
250
|
+
if (next >= total) return prev;
|
|
251
|
+
return (lastShownAt.current[next] ?? 0) >= (lastShownAt.current[prev] ?? 0) ? next : prev;
|
|
252
|
+
}, []);
|
|
253
|
+
const syncLayers = useCallback(
|
|
254
|
+
(nextCur, nextPeek, flyingEl) => {
|
|
255
|
+
const pool = slidesRef.current;
|
|
256
|
+
for (let i = 0; i < pool.length; i++) {
|
|
257
|
+
const el = cardRefs.current.get(pool[i].id);
|
|
258
|
+
if (!el || el === flyingEl) continue;
|
|
259
|
+
el.classList.remove("sd-current", "sd-peek");
|
|
260
|
+
const layer = layerFor(i, nextCur, nextPeek);
|
|
261
|
+
if (layer !== "hidden") el.classList.add(`sd-${layer}`);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
[]
|
|
265
|
+
);
|
|
266
|
+
const doRemoveByIndices = useCallback((sortedDescIndices) => {
|
|
267
|
+
if (sortedDescIndices.length === 0) return;
|
|
268
|
+
const snapCur = currentRef.current;
|
|
269
|
+
const snapPeek = peekRef.current;
|
|
270
|
+
const snapOverride = userPeekOverride.current;
|
|
271
|
+
setSlides((prev) => {
|
|
272
|
+
const valid = sortedDescIndices.filter((i) => i >= 0 && i < prev.length);
|
|
273
|
+
if (valid.length === 0) return prev;
|
|
274
|
+
const removeSet = new Set(valid);
|
|
275
|
+
const next = prev.filter((_, i) => !removeSet.has(i));
|
|
276
|
+
slidesRef.current = next;
|
|
277
|
+
for (const idx of valid) {
|
|
278
|
+
const id = prev[idx].id;
|
|
279
|
+
const el = cardRefs.current.get(id);
|
|
280
|
+
if (el) {
|
|
281
|
+
el.classList.remove("sd-current", "sd-peek");
|
|
282
|
+
el.style.visibility = "hidden";
|
|
283
|
+
el.style.zIndex = "0";
|
|
284
|
+
}
|
|
285
|
+
cardRefs.current.delete(id);
|
|
286
|
+
lastShownAt.current.splice(idx, 1);
|
|
287
|
+
}
|
|
288
|
+
const shift = valid.filter((i) => i < snapCur).length;
|
|
289
|
+
const newCur = Math.min(Math.max(snapCur - shift, 0), next.length - 1);
|
|
290
|
+
const shiftIdx = (idx) => {
|
|
291
|
+
if (idx === null) return null;
|
|
292
|
+
if (removeSet.has(idx)) return null;
|
|
293
|
+
return idx - valid.filter((r) => r < idx).length;
|
|
294
|
+
};
|
|
295
|
+
const newPeek = shiftIdx(snapPeek);
|
|
296
|
+
const newOverride = shiftIdx(snapOverride);
|
|
297
|
+
userPeekOverride.current = newOverride;
|
|
298
|
+
const resolvedPeek = newPeek !== null ? newPeek : resolvePeek(newCur, -1);
|
|
299
|
+
currentRef.current = newCur;
|
|
300
|
+
peekRef.current = resolvedPeek;
|
|
301
|
+
syncLayers(newCur, resolvedPeek);
|
|
302
|
+
setCurrent(newCur);
|
|
303
|
+
setPeekIndex(resolvedPeek);
|
|
304
|
+
return next;
|
|
305
|
+
});
|
|
306
|
+
}, [resolvePeek, syncLayers]);
|
|
307
|
+
const flushAfterNav = useCallback(() => {
|
|
308
|
+
logger.debug(`[SD] flushAfterNav cur=${currentRef.current} pendingRemoveNavTo=${pendingRemoveNavTo.current} pendingRemoveIds.size=${pendingRemoveIds.current.size} pendingNavDelta=${pendingNavDelta.current}`);
|
|
309
|
+
if (pendingRemoveNavTo.current !== null) {
|
|
310
|
+
const target = pendingRemoveNavTo.current;
|
|
311
|
+
pendingRemoveNavTo.current = null;
|
|
312
|
+
pendingNavDelta.current = null;
|
|
313
|
+
const delta = target - currentRef.current;
|
|
314
|
+
logger.debug(`[SD] flushAfterNav \u2192 pendingRemoveNavTo target=${target} delta=${delta}`);
|
|
315
|
+
if (delta !== 0) {
|
|
316
|
+
navWithVTRef.current(delta);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
logger.debug("[SD] flushAfterNav already at removeNavTo target, falling through");
|
|
320
|
+
}
|
|
321
|
+
if (pendingRemoveIds.current.size > 0) {
|
|
322
|
+
const ids = Array.from(pendingRemoveIds.current);
|
|
323
|
+
pendingRemoveIds.current = /* @__PURE__ */ new Set();
|
|
324
|
+
const indices = ids.map((id) => slidesRef.current.findIndex((s) => s.id === id)).filter((i) => i !== -1 && i !== currentRef.current).sort((a, b) => b - a);
|
|
325
|
+
logger.debug(`[SD] flushAfterNav \u2192 pendingRemoveIds ids=[${ids}] indices=[${indices}]`);
|
|
326
|
+
if (indices.length > 0) doRemoveByIndices(indices);
|
|
327
|
+
pendingNavDelta.current = null;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (pendingNavDelta.current !== null) {
|
|
331
|
+
const delta = pendingNavDelta.current;
|
|
332
|
+
pendingNavDelta.current = null;
|
|
333
|
+
logger.debug(`[SD] flushAfterNav \u2192 pendingNavDelta delta=${delta}`);
|
|
334
|
+
navWithVTRef.current(delta);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
logger.debug("[SD] flushAfterNav \u2192 nothing pending, done");
|
|
338
|
+
}, [doRemoveByIndices]);
|
|
339
|
+
const navInstant = useCallback(
|
|
340
|
+
(delta) => {
|
|
341
|
+
const cur = currentRef.current;
|
|
342
|
+
const total = slidesRef.current.length;
|
|
343
|
+
const next = cur + delta;
|
|
344
|
+
logger.debug(`[SD] navInstant delta=${delta} cur=${cur} next=${next} total=${total} transitioning=${transitioningRef.current}`);
|
|
345
|
+
if (next < 0 || next >= total) {
|
|
346
|
+
logger.warn(`[SD] navInstant SKIPPED: out of range cur=${cur} delta=${delta} next=${next} total=${total}`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (transitioningRef.current) {
|
|
350
|
+
pendingNavDelta.current = next - currentRef.current;
|
|
351
|
+
logger.debug("[SD] navInstant QUEUED pendingNavDelta=", pendingNavDelta.current);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
transitioningRef.current = true;
|
|
355
|
+
const afterPeek = resolvePeek(next, delta > 0 ? next + 1 : next - 1);
|
|
356
|
+
lastShownAt.current[cur] = performance.now() - 1;
|
|
357
|
+
lastShownAt.current[next] = performance.now();
|
|
358
|
+
currentRef.current = next;
|
|
359
|
+
peekRef.current = afterPeek;
|
|
360
|
+
setCurrent(next);
|
|
361
|
+
setPeekIndex(afterPeek);
|
|
362
|
+
syncLayers(next, afterPeek);
|
|
363
|
+
onSlideChange?.(next);
|
|
364
|
+
transitioningRef.current = false;
|
|
365
|
+
logger.debug(`[SD] navInstant settled cur=${currentRef.current}`);
|
|
366
|
+
flushAfterNav();
|
|
367
|
+
},
|
|
368
|
+
[resolvePeek, syncLayers, onSlideChange, flushAfterNav]
|
|
369
|
+
);
|
|
370
|
+
navInstantRef.current = navInstant;
|
|
371
|
+
const navWithVT = useCallback(
|
|
372
|
+
(delta) => {
|
|
373
|
+
const cur = currentRef.current;
|
|
374
|
+
const total = slidesRef.current.length;
|
|
375
|
+
const next = cur + delta;
|
|
376
|
+
logger.debug(`[SD] navWithVT delta=${delta} cur=${cur} next=${next} total=${total} transitioning=${transitioningRef.current}`);
|
|
377
|
+
if (next < 0 || next >= total) {
|
|
378
|
+
logger.warn(`[SD] navWithVT SKIPPED: out of range cur=${cur} delta=${delta} next=${next} total=${total}`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (transitioningRef.current) {
|
|
382
|
+
pendingNavDelta.current = next - currentRef.current;
|
|
383
|
+
logger.debug("[SD] navWithVT QUEUED pendingNavDelta=", pendingNavDelta.current);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
transitioningRef.current = true;
|
|
387
|
+
const oldEl = cardRefs.current.get(slidesRef.current[cur].id);
|
|
388
|
+
let newEl = cardRefs.current.get(slidesRef.current[next].id);
|
|
389
|
+
logger.debug(`[SD] navWithVT elements: oldEl=${!!oldEl} newEl=${!!newEl} cardRefs.size=${cardRefs.current.size}`);
|
|
390
|
+
if (!oldEl || !newEl) {
|
|
391
|
+
if (!oldEl) {
|
|
392
|
+
transitioningRef.current = false;
|
|
393
|
+
logger.debug("[SD] navWithVT BLOCKED: oldEl missing");
|
|
394
|
+
reportError({ code: "NAV_MISSING_ELEMENT", message: "DOM node for current slide not found", detail: { cur, curId: slidesRef.current[cur]?.id, cardRefsSize: cardRefs.current.size } });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
let retries = 0;
|
|
398
|
+
const retry = () => {
|
|
399
|
+
newEl = cardRefs.current.get(slidesRef.current[next]?.id ?? "") ?? null;
|
|
400
|
+
logger.debug(`[SD] navWithVT retry ${retries + 1} newEl=${!!newEl}`);
|
|
401
|
+
if (newEl) {
|
|
402
|
+
runNav(oldEl, newEl);
|
|
403
|
+
} else if (++retries < 5) {
|
|
404
|
+
requestAnimationFrame(retry);
|
|
405
|
+
} else {
|
|
406
|
+
transitioningRef.current = false;
|
|
407
|
+
logger.debug("[SD] navWithVT BLOCKED: newEl missing after retries");
|
|
408
|
+
reportError({ code: "NAV_MISSING_ELEMENT", message: "DOM node for destination slide not found after retries", detail: { next, nextId: slidesRef.current[next]?.id, retries, cardRefsSize: cardRefs.current.size } });
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
requestAnimationFrame(retry);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
runNav(oldEl, newEl);
|
|
415
|
+
function runNav(oldEl2, newEl2) {
|
|
416
|
+
const afterPeek = resolvePeek(next, delta > 0 ? next + 1 : next - 1);
|
|
417
|
+
const html = document.documentElement;
|
|
418
|
+
const dirAttr = `data-sd-dir-${instanceId.current}`;
|
|
419
|
+
const fadeAttr = `data-sd-fade-${instanceId.current}`;
|
|
420
|
+
html.setAttribute(dirAttr, delta > 0 ? "forward" : "back");
|
|
421
|
+
if (fade) html.setAttribute(fadeAttr, "1");
|
|
422
|
+
newEl2.classList.remove("sd-current");
|
|
423
|
+
newEl2.classList.add("sd-peek");
|
|
424
|
+
const isVisible = () => newEl2.classList.contains("sd-current") || newEl2.classList.contains("sd-peek");
|
|
425
|
+
logger.debug(`[SD] runNav newEl.className="${newEl2.className}" isVisible=${isVisible()}`);
|
|
426
|
+
const startNav = () => {
|
|
427
|
+
oldEl2.style.viewTransitionName = vtName;
|
|
428
|
+
const commit = () => {
|
|
429
|
+
oldEl2.style.viewTransitionName = "";
|
|
430
|
+
newEl2.style.viewTransitionName = vtName;
|
|
431
|
+
lastShownAt.current[cur] = performance.now() - 1;
|
|
432
|
+
lastShownAt.current[next] = performance.now();
|
|
433
|
+
currentRef.current = next;
|
|
434
|
+
peekRef.current = afterPeek;
|
|
435
|
+
setCurrent(next);
|
|
436
|
+
setPeekIndex(afterPeek);
|
|
437
|
+
syncLayers(next, afterPeek);
|
|
438
|
+
onSlideChange?.(next);
|
|
439
|
+
newEl2.style.viewTransitionName = "";
|
|
440
|
+
};
|
|
441
|
+
let cleanupFired = false;
|
|
442
|
+
const cleanup = () => {
|
|
443
|
+
if (cleanupFired) return;
|
|
444
|
+
cleanupFired = true;
|
|
445
|
+
html.removeAttribute(dirAttr);
|
|
446
|
+
html.removeAttribute(fadeAttr);
|
|
447
|
+
transitioningRef.current = false;
|
|
448
|
+
logger.debug(`[SD] navWithVT cleanup cur=${currentRef.current} pendingRemoveNavTo=${pendingRemoveNavTo.current} pendingRemoveIds.size=${pendingRemoveIds.current.size} pendingNavDelta=${pendingNavDelta.current}`);
|
|
449
|
+
flushAfterNav();
|
|
450
|
+
};
|
|
451
|
+
const doc = document;
|
|
452
|
+
if (!doc.startViewTransition) {
|
|
453
|
+
commit();
|
|
454
|
+
cleanup();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const vt = doc.startViewTransition(commit);
|
|
458
|
+
vt.finished.finally(cleanup);
|
|
459
|
+
};
|
|
460
|
+
let visRetries = 0;
|
|
461
|
+
const waitForVisible = () => {
|
|
462
|
+
if (isVisible()) {
|
|
463
|
+
logger.debug(`[SD] newEl visible after ${visRetries} retries`);
|
|
464
|
+
startNav();
|
|
465
|
+
} else if (visRetries++ < 3) {
|
|
466
|
+
logger.debug(`[SD] newEl not visible yet, retry ${visRetries}`);
|
|
467
|
+
requestAnimationFrame(waitForVisible);
|
|
468
|
+
} else {
|
|
469
|
+
logger.debug("[SD] newEl still not visible after retries, proceeding");
|
|
470
|
+
reportError({ code: "NAV_ELEMENT_NOT_VISIBLE", message: "Destination slide not visible after retries \u2014 animating anyway", detail: { next, newElClass: newEl2.className, retries: visRetries } });
|
|
471
|
+
startNav();
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
waitForVisible();
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
[fade, vtName, resolvePeek, syncLayers, onSlideChange, flushAfterNav]
|
|
478
|
+
);
|
|
479
|
+
navWithVTRef.current = navWithVT;
|
|
480
|
+
const navWithGesture = useCallback(
|
|
481
|
+
(delta, dragPos) => {
|
|
482
|
+
const cur = currentRef.current;
|
|
483
|
+
const total = slidesRef.current.length;
|
|
484
|
+
const next = cur + delta;
|
|
485
|
+
logger.debug(`[SD] navWithGesture delta=${delta} cur=${cur} next=${next} total=${total} transitioning=${transitioningRef.current}`);
|
|
486
|
+
if (next < 0 || next >= total) {
|
|
487
|
+
logger.warn(`[SD] navWithGesture SKIPPED: out of range cur=${cur} delta=${delta} next=${next} total=${total}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (transitioningRef.current) {
|
|
491
|
+
logger.debug("[SD] navWithGesture BLOCKED: transitioning");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
transitioningRef.current = true;
|
|
495
|
+
const flyingEl = cardRefs.current.get(slidesRef.current[cur].id);
|
|
496
|
+
if (!flyingEl) {
|
|
497
|
+
transitioningRef.current = false;
|
|
498
|
+
logger.debug("[SD] navWithGesture BLOCKED: flyingEl missing");
|
|
499
|
+
reportError({ code: "NAV_MISSING_ELEMENT", message: "DOM node for gesture fly-out not found", detail: { cur, curId: slidesRef.current[cur]?.id } });
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const dim = navAxis === "y" ? heightRef.current : width;
|
|
503
|
+
const exitPos = delta > 0 ? -dim * 1.05 : dim * 1.05;
|
|
504
|
+
const remaining = Math.abs(exitPos - dragPos);
|
|
505
|
+
const duration = Math.max(40, Math.min(transitionMs, remaining / dim * (transitionMs * 0.6)));
|
|
506
|
+
const translate = navAxis === "y" ? `translateY(${exitPos}px)` : `translateX(${exitPos}px)`;
|
|
507
|
+
flyingEl.style.transition = `transform ${duration}ms cubic-bezier(0.25,0,0.35,1)` + (dimOnDrag ? `, opacity ${duration * 0.7}ms ease` : "");
|
|
508
|
+
flyingEl.style.transform = translate;
|
|
509
|
+
if (dimOnDrag) flyingEl.style.opacity = "0";
|
|
510
|
+
const onDone = (e) => {
|
|
511
|
+
if (e.propertyName !== "transform") return;
|
|
512
|
+
flyingEl.removeEventListener("transitionend", onDone);
|
|
513
|
+
flyingEl.style.visibility = "hidden";
|
|
514
|
+
flyingEl.style.transition = "none";
|
|
515
|
+
flyingEl.style.transform = "translateX(0)";
|
|
516
|
+
flyingEl.style.opacity = "1";
|
|
517
|
+
flyingEl.classList.remove(
|
|
518
|
+
"sd-current",
|
|
519
|
+
"sd-pull-fwd",
|
|
520
|
+
"sd-pull-back",
|
|
521
|
+
"sd-grabbing",
|
|
522
|
+
"sd-drag-pending"
|
|
523
|
+
);
|
|
524
|
+
flyingEl.style.visibility = "";
|
|
525
|
+
const afterPeek = resolvePeek(next, delta > 0 ? next + 1 : next - 1);
|
|
526
|
+
lastShownAt.current[cur] = performance.now() - 1;
|
|
527
|
+
lastShownAt.current[next] = performance.now();
|
|
528
|
+
currentRef.current = next;
|
|
529
|
+
peekRef.current = afterPeek;
|
|
530
|
+
syncLayers(next, afterPeek, flyingEl);
|
|
531
|
+
setCurrent(next);
|
|
532
|
+
setPeekIndex(afterPeek);
|
|
533
|
+
onSlideChange?.(next);
|
|
534
|
+
transitioningRef.current = false;
|
|
535
|
+
logger.debug(`[SD] navWithGesture done cur=${currentRef.current}`);
|
|
536
|
+
flushAfterNav();
|
|
537
|
+
};
|
|
538
|
+
flyingEl.addEventListener("transitionend", onDone);
|
|
539
|
+
},
|
|
540
|
+
[width, dimOnDrag, transitionMs, navAxis, resolvePeek, syncLayers, onSlideChange, flushAfterNav]
|
|
541
|
+
);
|
|
542
|
+
const snapBack = useCallback(() => {
|
|
543
|
+
const el = cardRefs.current.get(slidesRef.current[currentRef.current]?.id ?? "");
|
|
544
|
+
if (!el) return;
|
|
545
|
+
el.style.transition = `transform ${transitionMs}ms cubic-bezier(0.34,1.56,0.64,1), opacity ${Math.round(transitionMs * 0.6)}ms ease`;
|
|
546
|
+
el.style.transform = navAxis === "y" ? "translateY(0)" : "translateX(0)";
|
|
547
|
+
el.style.opacity = "1";
|
|
548
|
+
el.classList.remove("sd-pull-fwd", "sd-pull-back", "sd-grabbing", "sd-drag-pending");
|
|
549
|
+
}, [transitionMs, navAxis]);
|
|
550
|
+
const cancelDragTimer = useCallback(() => {
|
|
551
|
+
if (dragTimerRef.current !== null) {
|
|
552
|
+
clearTimeout(dragTimerRef.current);
|
|
553
|
+
dragTimerRef.current = null;
|
|
554
|
+
}
|
|
555
|
+
}, []);
|
|
556
|
+
const maxTravel = useCallback(
|
|
557
|
+
() => width * maxDragRatio,
|
|
558
|
+
[width, maxDragRatio]
|
|
559
|
+
);
|
|
560
|
+
const onPointerDown = useCallback(
|
|
561
|
+
(e) => {
|
|
562
|
+
if (dragDisabled) return;
|
|
563
|
+
const cur = currentRef.current;
|
|
564
|
+
const el = cardRefs.current.get(slidesRef.current[cur]?.id ?? "");
|
|
565
|
+
if (!el) {
|
|
566
|
+
logger.debug("[SD] onPointerDown BLOCKED: no el for cur", cur);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (transitioningRef.current) {
|
|
570
|
+
logger.debug("[SD] onPointerDown BLOCKED: transitioning");
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (e.target.closest("button, a, input, select, textarea")) {
|
|
574
|
+
logger.debug("[SD] onPointerDown BLOCKED: interactive target");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (!el.contains(e.target) && e.target !== el) {
|
|
578
|
+
logger.debug("[SD] onPointerDown BLOCKED: target outside card");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const targetEl = e.target;
|
|
582
|
+
const nearestStage = targetEl.closest(".sd-stage");
|
|
583
|
+
if (nearestStage && nearestStage !== e.currentTarget) {
|
|
584
|
+
logger.debug("[SD] onPointerDown BLOCKED: target inside nested sd-stage");
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (navAxis === "x") {
|
|
588
|
+
try {
|
|
589
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
590
|
+
logger.debug("[SD] onPointerDown captured pointerId", e.pointerId, "cur", cur);
|
|
591
|
+
} catch (err) {
|
|
592
|
+
logger.warn("[SD] setPointerCapture failed:", err);
|
|
593
|
+
reportError({ code: "POINTER_CAPTURE_FAILED", message: "setPointerCapture threw \u2014 drag may not work correctly", detail: { pointerId: e.pointerId, error: String(err) } });
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
logger.debug("[SD] onPointerDown Y-axis: deferring capture, watching scroll boundary");
|
|
597
|
+
}
|
|
598
|
+
const scrollEl = navAxis === "y" ? el.querySelector("[data-scrollable]") ?? el : null;
|
|
599
|
+
const state = {
|
|
600
|
+
startX: e.clientX,
|
|
601
|
+
startY: e.clientY,
|
|
602
|
+
lastX: e.clientX,
|
|
603
|
+
lastY: e.clientY,
|
|
604
|
+
lastTime: performance.now(),
|
|
605
|
+
velX: 0,
|
|
606
|
+
velY: 0,
|
|
607
|
+
axis: null,
|
|
608
|
+
active: dragDelayMs <= 0,
|
|
609
|
+
pointerId: e.pointerId,
|
|
610
|
+
boundaryClaimed: false,
|
|
611
|
+
scrollEl
|
|
612
|
+
};
|
|
613
|
+
if (dragDelayMs > 0) {
|
|
614
|
+
el.classList.add("sd-drag-pending");
|
|
615
|
+
dragRef.current = state;
|
|
616
|
+
dragTimerRef.current = setTimeout(() => {
|
|
617
|
+
dragTimerRef.current = null;
|
|
618
|
+
if (dragRef.current) {
|
|
619
|
+
dragRef.current.active = true;
|
|
620
|
+
el.classList.remove("sd-drag-pending");
|
|
621
|
+
el.classList.add("sd-grabbing");
|
|
622
|
+
}
|
|
623
|
+
}, dragDelayMs);
|
|
624
|
+
} else {
|
|
625
|
+
dragRef.current = state;
|
|
626
|
+
el.classList.add("sd-grabbing");
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
}
|
|
629
|
+
lastDragXRef.current = 0;
|
|
630
|
+
lastDragYRef.current = 0;
|
|
631
|
+
},
|
|
632
|
+
[dragDisabled, dragDelayMs, navAxis]
|
|
633
|
+
);
|
|
634
|
+
const onPointerMove = useCallback(
|
|
635
|
+
(e) => {
|
|
636
|
+
if (!dragRef.current?.active) return;
|
|
637
|
+
const d = dragRef.current;
|
|
638
|
+
const dx = e.clientX - d.startX;
|
|
639
|
+
const dy = e.clientY - d.startY;
|
|
640
|
+
if (navAxis === "x") {
|
|
641
|
+
if (!d.axis) {
|
|
642
|
+
if (Math.hypot(dx, dy) < 6) return;
|
|
643
|
+
d.axis = Math.abs(dx) >= Math.abs(dy) ? "x" : "y";
|
|
644
|
+
}
|
|
645
|
+
if (d.axis !== "x") return;
|
|
646
|
+
e.preventDefault();
|
|
647
|
+
const now2 = performance.now();
|
|
648
|
+
const dt2 = Math.max(now2 - d.lastTime, 1);
|
|
649
|
+
d.velX = d.velX * 0.7 + (e.clientX - d.lastX) / dt2 * 0.3;
|
|
650
|
+
d.lastX = e.clientX;
|
|
651
|
+
d.lastTime = now2;
|
|
652
|
+
const cur2 = currentRef.current;
|
|
653
|
+
const total2 = slidesRef.current.length;
|
|
654
|
+
const id2 = slidesRef.current[cur2]?.id ?? "";
|
|
655
|
+
const el2 = cardRefs.current.get(id2);
|
|
656
|
+
if (!el2) return;
|
|
657
|
+
const perm2 = dragPermissions.current.get(id2) ?? "all";
|
|
658
|
+
if (perm2 === "left" && dx > 0) return;
|
|
659
|
+
if (perm2 === "right" && dx < 0) return;
|
|
660
|
+
let x = dx;
|
|
661
|
+
if (x > 0 && cur2 === 0 || x < 0 && cur2 === total2 - 1) {
|
|
662
|
+
x = rubberBand(x, width);
|
|
663
|
+
}
|
|
664
|
+
x = clamp(x, -maxTravel(), maxTravel());
|
|
665
|
+
lastDragXRef.current = x;
|
|
666
|
+
if (userPeekOverride.current === null) {
|
|
667
|
+
const wantedPeek = x < -8 ? cur2 + 1 < total2 ? cur2 + 1 : null : x > 8 ? cur2 - 1 >= 0 ? cur2 - 1 : null : peekRef.current;
|
|
668
|
+
if (wantedPeek !== peekRef.current) {
|
|
669
|
+
peekRef.current = wantedPeek;
|
|
670
|
+
setPeekIndex(wantedPeek);
|
|
671
|
+
syncLayers(cur2, wantedPeek, el2);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
el2.style.transition = "none";
|
|
675
|
+
el2.style.transform = `translateX(${x}px)`;
|
|
676
|
+
el2.style.opacity = dimOnDrag ? String(1 - clamp(Math.abs(x) / width * 0.28, 0, 0.28)) : "1";
|
|
677
|
+
el2.classList.toggle("sd-pull-fwd", x < -8);
|
|
678
|
+
el2.classList.toggle("sd-pull-back", x > 8);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (!d.boundaryClaimed) {
|
|
682
|
+
const sEl = d.scrollEl;
|
|
683
|
+
const atTop = !sEl || sEl.scrollTop <= 0;
|
|
684
|
+
const atBottom = !sEl || sEl.scrollTop >= sEl.scrollHeight - sEl.clientHeight - 1;
|
|
685
|
+
const pullingDown = dy > 12;
|
|
686
|
+
const pullingUp = dy < -12;
|
|
687
|
+
const canClaim = pullingDown && atTop || pullingUp && atBottom;
|
|
688
|
+
if (!canClaim) return;
|
|
689
|
+
d.boundaryClaimed = true;
|
|
690
|
+
d.startY = e.clientY;
|
|
691
|
+
d.lastY = e.clientY;
|
|
692
|
+
try {
|
|
693
|
+
e.currentTarget.setPointerCapture(d.pointerId);
|
|
694
|
+
logger.debug("[SD] Y boundary claimed, capture taken dy=", dy);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
logger.warn("[SD] Y setPointerCapture failed:", err);
|
|
697
|
+
reportError({ code: "POINTER_CAPTURE_FAILED", message: "Y-axis setPointerCapture failed", detail: { error: String(err) } });
|
|
698
|
+
d.boundaryClaimed = false;
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
e.preventDefault();
|
|
703
|
+
const now = performance.now();
|
|
704
|
+
const dt = Math.max(now - d.lastTime, 1);
|
|
705
|
+
const rawDy = e.clientY - d.startY;
|
|
706
|
+
d.velY = d.velY * 0.7 + (e.clientY - d.lastY) / dt * 0.3;
|
|
707
|
+
d.lastY = e.clientY;
|
|
708
|
+
d.lastTime = now;
|
|
709
|
+
const cur = currentRef.current;
|
|
710
|
+
const total = slidesRef.current.length;
|
|
711
|
+
const id = slidesRef.current[cur]?.id ?? "";
|
|
712
|
+
const el = cardRefs.current.get(id);
|
|
713
|
+
if (!el) return;
|
|
714
|
+
const perm = dragPermissions.current.get(id) ?? "all";
|
|
715
|
+
if (perm === "up" && rawDy > 0) return;
|
|
716
|
+
if (perm === "down" && rawDy < 0) return;
|
|
717
|
+
const dim = heightRef.current || 600;
|
|
718
|
+
let y = rawDy;
|
|
719
|
+
if (y > 0 && cur === 0 || y < 0 && cur === total - 1) {
|
|
720
|
+
y = rubberBand(y, dim);
|
|
721
|
+
}
|
|
722
|
+
y = clamp(y, -dim * maxDragRatio, dim * maxDragRatio);
|
|
723
|
+
lastDragYRef.current = y;
|
|
724
|
+
if (userPeekOverride.current === null) {
|
|
725
|
+
const wantedPeek = y < -8 ? cur + 1 < total ? cur + 1 : null : y > 8 ? cur - 1 >= 0 ? cur - 1 : null : peekRef.current;
|
|
726
|
+
if (wantedPeek !== peekRef.current) {
|
|
727
|
+
peekRef.current = wantedPeek;
|
|
728
|
+
setPeekIndex(wantedPeek);
|
|
729
|
+
syncLayers(cur, wantedPeek, el);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
el.style.transition = "none";
|
|
733
|
+
el.style.transform = `translateY(${y}px)`;
|
|
734
|
+
el.style.opacity = dimOnDrag ? String(1 - clamp(Math.abs(y) / dim * 0.28, 0, 0.28)) : "1";
|
|
735
|
+
el.classList.toggle("sd-pull-fwd", y < -8);
|
|
736
|
+
el.classList.toggle("sd-pull-back", y > 8);
|
|
737
|
+
},
|
|
738
|
+
[width, dimOnDrag, navAxis, syncLayers, maxTravel, maxDragRatio]
|
|
739
|
+
);
|
|
740
|
+
const onPointerUp = useCallback(
|
|
741
|
+
(e) => {
|
|
742
|
+
if (!dragRef.current) return;
|
|
743
|
+
const d = dragRef.current;
|
|
744
|
+
const dx = e.clientX - d.startX;
|
|
745
|
+
const vel = d.velX;
|
|
746
|
+
const wasActive = d.active;
|
|
747
|
+
dragRef.current = null;
|
|
748
|
+
cancelDragTimer();
|
|
749
|
+
try {
|
|
750
|
+
e.currentTarget.releasePointerCapture(d.pointerId);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
logger.warn("[SD] releasePointerCapture failed:", err);
|
|
753
|
+
reportError({ code: "POINTER_RELEASE_FAILED", message: "releasePointerCapture threw \u2014 future pointer events may be blocked", detail: { pointerId: d.pointerId, error: String(err) } });
|
|
754
|
+
}
|
|
755
|
+
const el = cardRefs.current.get(slidesRef.current[currentRef.current]?.id ?? "");
|
|
756
|
+
el?.classList.remove("sd-drag-pending", "sd-grabbing");
|
|
757
|
+
if (!wasActive) {
|
|
758
|
+
logger.debug("[SD] onPointerUp: drag never activated, no-op");
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (transitioningRef.current) {
|
|
762
|
+
logger.debug("[SD] onPointerUp: transitioning \u2014 snapBack");
|
|
763
|
+
snapBack();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const moved = Math.abs(dx) > 8;
|
|
767
|
+
const commit = moved && (Math.abs(lastDragXRef.current) >= maxTravel() - 1 || Math.abs(vel) > VELOCITY_COMMIT);
|
|
768
|
+
logger.debug(`[SD] onPointerUp dx=${dx.toFixed(1)} lastDragX=${lastDragXRef.current.toFixed(1)} vel=${vel.toFixed(3)} moved=${moved} commit=${commit}`);
|
|
769
|
+
if (commit) {
|
|
770
|
+
const delta = dx < 0 ? 1 : -1;
|
|
771
|
+
const next = currentRef.current + delta;
|
|
772
|
+
logger.debug(`[SD] onPointerUp committing delta=${delta} next=${next} total=${slidesRef.current.length}`);
|
|
773
|
+
if (next >= 0 && next < slidesRef.current.length) {
|
|
774
|
+
const flyingEl = cardRefs.current.get(slidesRef.current[currentRef.current]?.id ?? "");
|
|
775
|
+
if (flyingEl) {
|
|
776
|
+
flyingEl.style.transition = "none";
|
|
777
|
+
flyingEl.style.transform = `translateX(${lastDragXRef.current}px)`;
|
|
778
|
+
}
|
|
779
|
+
navWithGesture(delta, lastDragXRef.current);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
logger.debug("[SD] onPointerUp commit BLOCKED: next out of range");
|
|
783
|
+
}
|
|
784
|
+
logger.debug("[SD] onPointerUp snapBack");
|
|
785
|
+
snapBack();
|
|
786
|
+
},
|
|
787
|
+
[snapBack, navWithGesture, cancelDragTimer, maxTravel, maxDragRatio, navAxis, width, reportError]
|
|
788
|
+
);
|
|
789
|
+
useEffect(() => {
|
|
790
|
+
if (keyboardDisabled) return;
|
|
791
|
+
const h = (e) => {
|
|
792
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") navWithVT(1);
|
|
793
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowUp") navWithVT(-1);
|
|
794
|
+
};
|
|
795
|
+
window.addEventListener("keydown", h);
|
|
796
|
+
return () => window.removeEventListener("keydown", h);
|
|
797
|
+
}, [navWithVT, keyboardDisabled]);
|
|
798
|
+
useEffect(() => () => {
|
|
799
|
+
cancelDragTimer();
|
|
800
|
+
}, [cancelDragTimer]);
|
|
801
|
+
useEffect(() => {
|
|
802
|
+
if (!stageRef.current) return;
|
|
803
|
+
heightRef.current = stageRef.current.getBoundingClientRect().height;
|
|
804
|
+
const ro = new ResizeObserver((entries) => {
|
|
805
|
+
heightRef.current = entries[0]?.contentRect.height ?? heightRef.current;
|
|
806
|
+
});
|
|
807
|
+
ro.observe(stageRef.current);
|
|
808
|
+
return () => ro.disconnect();
|
|
809
|
+
}, []);
|
|
810
|
+
const execRemoveSlides = useCallback(
|
|
811
|
+
(ids, options = {}) => {
|
|
812
|
+
const { animate = true } = options;
|
|
813
|
+
if (ids.length === 0) return;
|
|
814
|
+
const list = slidesRef.current;
|
|
815
|
+
if (list.length <= 1) return;
|
|
816
|
+
const idSet = new Set(ids);
|
|
817
|
+
const allIdxs = Array.from(idSet).map((id) => list.findIndex((s) => s.id === id)).filter((i) => i !== -1);
|
|
818
|
+
if (allIdxs.length === 0) return;
|
|
819
|
+
let safeIdxs = allIdxs;
|
|
820
|
+
if (allIdxs.length >= list.length) {
|
|
821
|
+
const keepIdx = allIdxs.includes(currentRef.current) ? allIdxs.find((i) => i !== currentRef.current) ?? 0 : currentRef.current;
|
|
822
|
+
safeIdxs = allIdxs.filter((i) => i !== keepIdx);
|
|
823
|
+
if (safeIdxs.length === 0) return;
|
|
824
|
+
}
|
|
825
|
+
const safeSet = new Set(safeIdxs);
|
|
826
|
+
const curIdx = currentRef.current;
|
|
827
|
+
const curIsGone = safeSet.has(curIdx);
|
|
828
|
+
for (const i of safeIdxs) {
|
|
829
|
+
keepDomIds.current.delete(list[i].id);
|
|
830
|
+
dragPermissions.current.delete(list[i].id);
|
|
831
|
+
}
|
|
832
|
+
const nonCurIdxs = safeIdxs.filter((i) => i !== curIdx).sort((a, b) => b - a);
|
|
833
|
+
if (!curIsGone) {
|
|
834
|
+
doRemoveByIndices(nonCurIdxs);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
pendingRemoveIds.current.add(list[curIdx].id);
|
|
838
|
+
for (const i of nonCurIdxs) pendingRemoveIds.current.add(list[i].id);
|
|
839
|
+
if (nonCurIdxs.length > 0) doRemoveByIndices(nonCurIdxs);
|
|
840
|
+
const total = slidesRef.current.length;
|
|
841
|
+
const removeDelta = curIdx + 1 < total ? 1 : -1;
|
|
842
|
+
const removeTarget = curIdx + removeDelta;
|
|
843
|
+
logger.debug(`[SD] removeSlides curIdx=${curIdx} removeTarget=${removeTarget} transitioning=${transitioningRef.current} pendingRemoveIds.size=${pendingRemoveIds.current.size}`);
|
|
844
|
+
if (!transitioningRef.current) {
|
|
845
|
+
const nav = animate ? navWithVT : navInstant;
|
|
846
|
+
nav(removeDelta);
|
|
847
|
+
} else {
|
|
848
|
+
pendingRemoveNavTo.current = removeTarget;
|
|
849
|
+
pendingNavDelta.current = null;
|
|
850
|
+
logger.debug(`[SD] removeSlides QUEUED pendingRemoveNavTo=${removeTarget}`);
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
[navWithVT, navInstant, doRemoveByIndices, flushAfterNav]
|
|
854
|
+
);
|
|
855
|
+
useImperativeHandle(
|
|
856
|
+
externalRef,
|
|
857
|
+
() => ({
|
|
858
|
+
get currentIndex() {
|
|
859
|
+
return currentRef.current;
|
|
860
|
+
},
|
|
861
|
+
get currentSlides() {
|
|
862
|
+
return slidesRef.current;
|
|
863
|
+
},
|
|
864
|
+
addSlide(slide, options = {}) {
|
|
865
|
+
const { keepDom = false, animate = true, drag = "all" } = options;
|
|
866
|
+
if (keepDom) keepDomIds.current.add(slide.id);
|
|
867
|
+
if (drag !== "all") dragPermissions.current.set(slide.id, drag);
|
|
868
|
+
const newIdx = slidesRef.current.length;
|
|
869
|
+
setSlides((prev) => {
|
|
870
|
+
const next = [...prev, slide];
|
|
871
|
+
slidesRef.current = next;
|
|
872
|
+
lastShownAt.current.push(0);
|
|
873
|
+
return next;
|
|
874
|
+
});
|
|
875
|
+
requestAnimationFrame(() => {
|
|
876
|
+
const delta = newIdx - currentRef.current;
|
|
877
|
+
logger.debug(`[SD] addSlide rAF newIdx=${newIdx} cur=${currentRef.current} delta=${delta} animate=${animate} transitioning=${transitioningRef.current}`);
|
|
878
|
+
const nav = animate ? navWithVTRef.current : navInstantRef.current;
|
|
879
|
+
nav(delta);
|
|
880
|
+
});
|
|
881
|
+
},
|
|
882
|
+
removeSlide(id, options = {}) {
|
|
883
|
+
execRemoveSlides([id], options);
|
|
884
|
+
},
|
|
885
|
+
removeSlides(ids, options = {}) {
|
|
886
|
+
execRemoveSlides(ids, options);
|
|
887
|
+
},
|
|
888
|
+
goTo(index, options = {}) {
|
|
889
|
+
const delta = index - currentRef.current;
|
|
890
|
+
if (delta === 0) return;
|
|
891
|
+
const nav = options.instant ? navInstant : navWithVT;
|
|
892
|
+
nav(delta);
|
|
893
|
+
},
|
|
894
|
+
next(options = {}) {
|
|
895
|
+
const nav = options.instant ? navInstant : navWithVT;
|
|
896
|
+
nav(1);
|
|
897
|
+
},
|
|
898
|
+
prev(options = {}) {
|
|
899
|
+
const nav = options.instant ? navInstant : navWithVT;
|
|
900
|
+
nav(-1);
|
|
901
|
+
},
|
|
902
|
+
setPeek(index) {
|
|
903
|
+
const total = slidesRef.current.length;
|
|
904
|
+
const valid = index !== null && index >= 0 && index < total && index !== currentRef.current ? index : null;
|
|
905
|
+
userPeekOverride.current = valid;
|
|
906
|
+
if (!transitioningRef.current) {
|
|
907
|
+
const resolved = valid !== null ? valid : resolvePeek(currentRef.current, -1);
|
|
908
|
+
peekRef.current = resolved;
|
|
909
|
+
setPeekIndex(resolved);
|
|
910
|
+
syncLayers(currentRef.current, resolved);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}),
|
|
914
|
+
[navWithVT, navInstant, resolvePeek, syncLayers, doRemoveByIndices]
|
|
915
|
+
);
|
|
916
|
+
return /* @__PURE__ */ jsxs(
|
|
917
|
+
"div",
|
|
918
|
+
{
|
|
919
|
+
className: `sd-root${className ? " " + className : ""}`,
|
|
920
|
+
style: { width },
|
|
921
|
+
children: [
|
|
922
|
+
/* @__PURE__ */ jsxs(
|
|
923
|
+
"div",
|
|
924
|
+
{
|
|
925
|
+
ref: stageRef,
|
|
926
|
+
className: "sd-stage",
|
|
927
|
+
style: { touchAction: navAxis === "y" ? "pan-y" : "none" },
|
|
928
|
+
onPointerDown,
|
|
929
|
+
onPointerMove,
|
|
930
|
+
onPointerUp,
|
|
931
|
+
onPointerCancel: onPointerUp,
|
|
932
|
+
children: [
|
|
933
|
+
/* @__PURE__ */ jsx("div", { className: "sd-sizer", "aria-hidden": "true" }),
|
|
934
|
+
slides.map((slide, i) => /* @__PURE__ */ jsx(
|
|
935
|
+
SlideSlot,
|
|
936
|
+
{
|
|
937
|
+
slide,
|
|
938
|
+
layer: layerFor(i, current, peekIndex),
|
|
939
|
+
keepDom: keepDomIds.current.has(slide.id),
|
|
940
|
+
dragDisabled,
|
|
941
|
+
navAxis,
|
|
942
|
+
refCallback: (el) => {
|
|
943
|
+
if (el) cardRefs.current.set(slide.id, el);
|
|
944
|
+
},
|
|
945
|
+
children: slide.content
|
|
946
|
+
},
|
|
947
|
+
slide.id
|
|
948
|
+
))
|
|
949
|
+
]
|
|
950
|
+
}
|
|
951
|
+
),
|
|
952
|
+
showDots && slides.length > 0 && /* @__PURE__ */ jsx("div", { className: "sd-dots", role: "tablist", "aria-label": "Slides", children: slides.map((slide, i) => /* @__PURE__ */ jsx(
|
|
953
|
+
"div",
|
|
954
|
+
{
|
|
955
|
+
role: "tab",
|
|
956
|
+
"aria-selected": i === current,
|
|
957
|
+
className: `sd-dot${i === current ? " sd-dot-active" : ""}`,
|
|
958
|
+
onClick: () => navWithVT(i - current)
|
|
959
|
+
},
|
|
960
|
+
slide.id
|
|
961
|
+
)) })
|
|
962
|
+
]
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
);
|
|
967
|
+
var index_default = SwipeDeck;
|
|
968
|
+
|
|
969
|
+
export { index_default as default };
|
|
970
|
+
//# sourceMappingURL=index.mjs.map
|
|
971
|
+
//# sourceMappingURL=index.mjs.map
|