@waveso/ui 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -12
- package/dist/accordion.js +2 -2
- package/dist/action-bar.js +4 -4
- package/dist/alert-dialog.js +5 -5
- package/dist/alert.d.ts +8 -1
- package/dist/alert.js +13 -4
- package/dist/animate.d.ts +1 -1
- package/dist/animate.js +2 -9
- package/dist/autocomplete.js +3 -3
- package/dist/avatar.d.ts +1 -1
- package/dist/avatar.js +4 -4
- package/dist/badge.d.ts +1 -1
- package/dist/badge.js +6 -6
- package/dist/button-group.d.ts +4 -2
- package/dist/button-group.js +5 -4
- package/dist/button.d.ts +1 -1
- package/dist/button.js +6 -6
- package/dist/card.d.ts +11 -4
- package/dist/card.js +27 -14
- package/dist/checkbox.js +2 -2
- package/dist/combobox.js +4 -4
- package/dist/context-menu.d.ts +2 -1
- package/dist/context-menu.js +14 -14
- package/dist/dialog.js +5 -5
- package/dist/drawer.js +1 -1
- package/dist/encrypted-text.d.ts +15 -1
- package/dist/encrypted-text.js +205 -67
- package/dist/field.d.ts +21 -3
- package/dist/field.js +36 -8
- package/dist/fieldset.d.ts +14 -0
- package/dist/fieldset.js +26 -0
- package/dist/form.d.ts +14 -14
- package/dist/form.js +7 -4
- package/dist/glyph-matrix.d.ts +25 -0
- package/dist/glyph-matrix.js +266 -0
- package/dist/gradient-reveal-text.d.ts +3 -3
- package/dist/gradient-reveal-text.js +8 -8
- package/dist/input-group.js +1 -1
- package/dist/input-otp.d.ts +3 -3
- package/dist/input-otp.js +2 -2
- package/dist/input.js +1 -1
- package/dist/item.d.ts +1 -1
- package/dist/item.js +17 -11
- package/dist/kbd.d.ts +1 -1
- package/dist/kbd.js +2 -2
- package/dist/label.js +1 -1
- package/dist/lib/merge-refs.d.ts +9 -0
- package/dist/lib/merge-refs.js +16 -0
- package/dist/lib/utils.js +7 -0
- package/dist/masonry.d.ts +1 -1
- package/dist/masonry.js +4 -2
- package/dist/menu.d.ts +2 -1
- package/dist/menu.js +7 -7
- package/dist/menubar.js +1 -1
- package/dist/pagination.js +1 -1
- package/dist/popover.js +1 -1
- package/dist/preview-card.js +1 -1
- package/dist/progress.js +2 -2
- package/dist/radio-group.js +1 -1
- package/dist/radio.js +2 -2
- package/dist/resizable.d.ts +43 -0
- package/dist/resizable.js +53 -0
- package/dist/scroll-area.d.ts +13 -2
- package/dist/scroll-area.js +26 -7
- package/dist/select.js +4 -4
- package/dist/separator.js +1 -1
- package/dist/sidebar.js +12 -12
- package/dist/slider.js +3 -3
- package/dist/spinner.d.ts +7 -0
- package/dist/spinner.js +24 -6
- package/dist/styles.css +581 -194
- package/dist/swipe-deck.d.ts +98 -0
- package/dist/swipe-deck.js +931 -0
- package/dist/switch.js +6 -6
- package/dist/table.d.ts +7 -1
- package/dist/table.js +40 -9
- package/dist/tabs.js +3 -3
- package/dist/textarea.js +1 -1
- package/dist/toast.js +4 -4
- package/dist/toggle-group.js +3 -3
- package/dist/toggle.d.ts +1 -1
- package/dist/toggle.js +11 -6
- package/dist/tooltip.d.ts +3 -1
- package/dist/tooltip.js +10 -6
- package/dist/video-text.d.ts +25 -0
- package/dist/video-text.js +147 -0
- package/package.json +6 -1
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "./lib/utils.js";
|
|
3
|
+
import { Button } from "./button.js";
|
|
4
|
+
import { mergeRefs } from "./lib/merge-refs.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { animate, useMotionValue, useReducedMotion } from "motion/react";
|
|
8
|
+
//#region src/swipe-deck.tsx
|
|
9
|
+
const DEFAULT_DIRECTIONS = ["left", "right"];
|
|
10
|
+
const KEY_DIRECTIONS = {
|
|
11
|
+
ArrowLeft: "left",
|
|
12
|
+
ArrowRight: "right",
|
|
13
|
+
ArrowUp: "up",
|
|
14
|
+
ArrowDown: "down"
|
|
15
|
+
};
|
|
16
|
+
/** How far (px) a press moves before it becomes a drag, so taps stay clicks. */
|
|
17
|
+
const DRAG_SLOP = 6;
|
|
18
|
+
/** A card leaves when it comes to rest past this share of its width (or height). */
|
|
19
|
+
const LEAVE_SHARE = .25;
|
|
20
|
+
/** Only pointer samples this recent (ms) set the release speed. */
|
|
21
|
+
const VELOCITY_WINDOW = 100;
|
|
22
|
+
/**
|
|
23
|
+
* How much a throw counts: its release speed adds this many seconds' worth of
|
|
24
|
+
* travel to where the card is, to decide whether it leaves and which way.
|
|
25
|
+
*/
|
|
26
|
+
const THROW_TIME = .25;
|
|
27
|
+
/** A leaving card's exit point is this far (px) past the stack's edge, shadow and all. */
|
|
28
|
+
const CLEARANCE = 16;
|
|
29
|
+
const RETURN_SPRING = {
|
|
30
|
+
type: "spring",
|
|
31
|
+
stiffness: 400,
|
|
32
|
+
damping: 32
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Carries a leaving card to its exit point. A slow card slides out on it; a
|
|
36
|
+
* thrown one brings its speed and runs out against it, so the harder the
|
|
37
|
+
* throw, the harder the pull back: a limit, not a coast to anywhere.
|
|
38
|
+
*/
|
|
39
|
+
const EXIT_SPRING = {
|
|
40
|
+
type: "spring",
|
|
41
|
+
stiffness: 650,
|
|
42
|
+
damping: 42
|
|
43
|
+
};
|
|
44
|
+
/** A leaving card fades once clear of the stack and slowed below this (px/s). */
|
|
45
|
+
const CAUGHT_SPEED = 200;
|
|
46
|
+
/** Moves a card from one place in the pile to the next. */
|
|
47
|
+
const PLACE_SPRING = {
|
|
48
|
+
type: "spring",
|
|
49
|
+
stiffness: 260,
|
|
50
|
+
damping: 24
|
|
51
|
+
};
|
|
52
|
+
/** How far (in places) the cards behind lift toward the front as it's dragged out. */
|
|
53
|
+
const LIFT = .4;
|
|
54
|
+
/** On mount, the cards behind wait this long (s) for the front card to fade in. */
|
|
55
|
+
const ENTER_LEAD = .15;
|
|
56
|
+
/** Then each card behind comes this long (s) after the one before. */
|
|
57
|
+
const ENTER_STAGGER = .06;
|
|
58
|
+
/** And grows in from this size, the zoom recipes' md step (`--scale-md`). */
|
|
59
|
+
const ENTER_SCALE = .9;
|
|
60
|
+
/** Dash and gap (px) of `SwipeDeckEmpty`'s outline. */
|
|
61
|
+
const OUTLINE_DASH = 8;
|
|
62
|
+
const OUTLINE_GAP = 6;
|
|
63
|
+
/** Points drawn per corner of that outline. */
|
|
64
|
+
const OUTLINE_CORNER_STEPS = 24;
|
|
65
|
+
/** Presses on these inside a card stay theirs: a slider's thumb, a text box. */
|
|
66
|
+
const FIELD_SELECTOR = "input, textarea, select, [contenteditable]:not([contenteditable='false']), [role=slider]";
|
|
67
|
+
/** Keeps the browser's own panning on the axis the deck doesn't swipe along. */
|
|
68
|
+
function touchAction(directions) {
|
|
69
|
+
const horizontal = directions.some(isHorizontal);
|
|
70
|
+
const vertical = directions.some((direction) => !isHorizontal(direction));
|
|
71
|
+
if (horizontal && vertical) return "touch-none";
|
|
72
|
+
return horizontal ? "touch-pan-y" : "touch-pan-x";
|
|
73
|
+
}
|
|
74
|
+
function isHorizontal(direction) {
|
|
75
|
+
return direction === "left" || direction === "right";
|
|
76
|
+
}
|
|
77
|
+
/** Lets the cards behind settle back from a lift. */
|
|
78
|
+
function releaseLift(lift, isInstant) {
|
|
79
|
+
if (isInstant) lift.jump(0);
|
|
80
|
+
else animate(lift, 0, PLACE_SPRING);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolves once an element's running transitions and animations end. jsdom
|
|
84
|
+
* has no Web Animations, and an endless animation of your own would never
|
|
85
|
+
* end, so neither is waited for.
|
|
86
|
+
*/
|
|
87
|
+
function transitionsEnd(element) {
|
|
88
|
+
const animations = typeof element.getAnimations === "function" ? element.getAnimations().filter((animation) => animation.effect?.getComputedTiming().endTime !== Infinity) : [];
|
|
89
|
+
return Promise.allSettled(animations.map((animation) => animation.finished));
|
|
90
|
+
}
|
|
91
|
+
/** `value` kept within `min`–`max`, or `fallback` when it isn't a number. */
|
|
92
|
+
function within(value, min, max, fallback) {
|
|
93
|
+
return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Three steady numbers in [-0.5, 0.5] from a card's value: where it lies in a
|
|
97
|
+
* `pile`, the same on every render and server.
|
|
98
|
+
*/
|
|
99
|
+
function scatterOf(value) {
|
|
100
|
+
let hash = 2166136261;
|
|
101
|
+
for (let index = 0; index < value.length; index++) hash = Math.imul(hash ^ value.charCodeAt(index), 16777619);
|
|
102
|
+
const next = () => {
|
|
103
|
+
hash ^= hash << 13;
|
|
104
|
+
hash ^= hash >>> 17;
|
|
105
|
+
hash ^= hash << 5;
|
|
106
|
+
return (hash >>> 0) % 1e3 / 1e3 - .5;
|
|
107
|
+
};
|
|
108
|
+
return [
|
|
109
|
+
next(),
|
|
110
|
+
next(),
|
|
111
|
+
next()
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
/** Layout size, without the transform a moving card carries. */
|
|
115
|
+
function sizeOf(element) {
|
|
116
|
+
return {
|
|
117
|
+
width: element.offsetWidth,
|
|
118
|
+
height: element.offsetHeight
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Whether a card moved by (x, y) is off the stack. */
|
|
122
|
+
function isClear(x, y, size) {
|
|
123
|
+
return Math.abs(x) >= size.width || Math.abs(y) >= size.height;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Where a card at (x, y), heading along (dx, dy), first sits `CLEARANCE` past
|
|
127
|
+
* the stack's edge: the nearer of the side edges and the top or bottom edge.
|
|
128
|
+
*/
|
|
129
|
+
function exitPoint(x, y, [dx, dy], size) {
|
|
130
|
+
const length = Math.hypot(dx, dy);
|
|
131
|
+
const ux = dx / length;
|
|
132
|
+
const uy = dy / length;
|
|
133
|
+
const steps = [ux === 0 ? Infinity : (Math.sign(ux) * (size.width + CLEARANCE) - x) / ux, uy === 0 ? Infinity : (Math.sign(uy) * (size.height + CLEARANCE) - y) / uy];
|
|
134
|
+
const step = Math.max(0, Math.min(...steps));
|
|
135
|
+
return [x + step * ux, y + step * uy];
|
|
136
|
+
}
|
|
137
|
+
/** How far a movement goes toward a direction; negative when it goes away. */
|
|
138
|
+
function reach(direction, x, y) {
|
|
139
|
+
switch (direction) {
|
|
140
|
+
case "right": return x;
|
|
141
|
+
case "left": return -x;
|
|
142
|
+
case "down": return y;
|
|
143
|
+
case "up": return -y;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The allowed direction a movement leans furthest into, and how far it goes
|
|
148
|
+
* there as a share of the distance that makes a card leave (1 or more leaves).
|
|
149
|
+
*/
|
|
150
|
+
function lean(directions, x, y, size) {
|
|
151
|
+
let best = {
|
|
152
|
+
direction: void 0,
|
|
153
|
+
share: 0
|
|
154
|
+
};
|
|
155
|
+
for (const direction of directions) {
|
|
156
|
+
const length = isHorizontal(direction) ? size.width : size.height;
|
|
157
|
+
const share = reach(direction, x, y) / Math.max(length * LEAVE_SHARE, 1);
|
|
158
|
+
if (share > best.share) best = {
|
|
159
|
+
direction,
|
|
160
|
+
share
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return best;
|
|
164
|
+
}
|
|
165
|
+
/** Pointer speed (px/s) over the samples taken just before `time`. */
|
|
166
|
+
function releaseVelocity(samples, time) {
|
|
167
|
+
const recent = samples.filter((sample) => time - sample.time <= VELOCITY_WINDOW);
|
|
168
|
+
const first = recent[0];
|
|
169
|
+
const last = recent.at(-1);
|
|
170
|
+
if (!first || !last || last.time <= first.time) return [0, 0];
|
|
171
|
+
const elapsed = (last.time - first.time) / 1e3;
|
|
172
|
+
return [(last.x - first.x) / elapsed, (last.y - first.y) / elapsed];
|
|
173
|
+
}
|
|
174
|
+
const SwipeDeckContext = React.createContext(null);
|
|
175
|
+
function useSwipeDeckContext(part) {
|
|
176
|
+
const context = React.useContext(SwipeDeckContext);
|
|
177
|
+
if (!context) throw new Error(`${part} must be used within a SwipeDeck.`);
|
|
178
|
+
return context;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* A pile of cards people swipe away: drag or throw the front card, press an
|
|
182
|
+
* arrow key while the deck is focused, or use a `SwipeDeckAction`. A let-go
|
|
183
|
+
* card whose position and throw carry it past a quarter of its width leaves:
|
|
184
|
+
* it slides out the way it is heading until clear of the stack, held by a
|
|
185
|
+
* spring that a hard throw runs out against, then fades. Short of that, it
|
|
186
|
+
* springs back. Your list stays the source of truth — `onSwipe` tells you
|
|
187
|
+
* which card left and which way, and you remove it or move it to the back.
|
|
188
|
+
*
|
|
189
|
+
* End the deck with a `SwipeDeckEmpty`: it lies under the last card, so
|
|
190
|
+
* dragging that card shows what comes next. The deck carries `data-empty`
|
|
191
|
+
* once no card is in front (the last one leaving, or none left), for styles
|
|
192
|
+
* of your own.
|
|
193
|
+
*
|
|
194
|
+
* Give the deck an `aria-label`. Screen readers hear the next card after each
|
|
195
|
+
* swipe: its `aria-label` when it has one, otherwise all of its text.
|
|
196
|
+
*/
|
|
197
|
+
function SwipeDeck({ onSwipe, directions = DEFAULT_DIRECTIONS, disabled = false, layout = "stack", spread = .5, depth = .5, visible = 4, className, style, onKeyDown, children, ref, ...props }) {
|
|
198
|
+
const rootRef = React.useRef(null);
|
|
199
|
+
const mergedRef = React.useMemo(() => mergeRefs(rootRef, ref), [ref]);
|
|
200
|
+
const itemsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
201
|
+
const finishedRef = React.useRef(/* @__PURE__ */ new Map());
|
|
202
|
+
const statusRef = React.useRef(null);
|
|
203
|
+
const emptyRef = React.useRef(null);
|
|
204
|
+
const sizeObserverRef = React.useRef(null);
|
|
205
|
+
const lift = useMotionValue(0);
|
|
206
|
+
const [order, setOrder] = React.useState([]);
|
|
207
|
+
const [leavingList, setLeavingList] = React.useState([]);
|
|
208
|
+
const [isReady, setIsReady] = React.useState(false);
|
|
209
|
+
const onSwipeRef = React.useRef(onSwipe);
|
|
210
|
+
React.useEffect(() => {
|
|
211
|
+
onSwipeRef.current = onSwipe;
|
|
212
|
+
});
|
|
213
|
+
const readOrder = React.useCallback(() => [...itemsRef.current].sort(([, a], [, b]) => a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1).map(([value]) => value), []);
|
|
214
|
+
const syncOrder = React.useCallback(() => {
|
|
215
|
+
const items = itemsRef.current;
|
|
216
|
+
const next = readOrder();
|
|
217
|
+
setOrder((previous) => previous.length === next.length && previous.every((value, index) => value === next[index]) ? previous : next);
|
|
218
|
+
const answered = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const [value, followers] of finishedRef.current) {
|
|
220
|
+
const index = next.indexOf(value);
|
|
221
|
+
const hasMoved = followers.some((follower) => {
|
|
222
|
+
const followerIndex = next.indexOf(follower);
|
|
223
|
+
return followerIndex !== -1 && followerIndex < index;
|
|
224
|
+
});
|
|
225
|
+
if (index === -1 || hasMoved) {
|
|
226
|
+
answered.add(value);
|
|
227
|
+
finishedRef.current.delete(value);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
setLeavingList((previous) => {
|
|
231
|
+
const kept = previous.filter((value) => items.has(value) && !answered.has(value));
|
|
232
|
+
return kept.length === previous.length ? previous : kept;
|
|
233
|
+
});
|
|
234
|
+
}, [readOrder]);
|
|
235
|
+
React.useLayoutEffect(() => {
|
|
236
|
+
syncOrder();
|
|
237
|
+
});
|
|
238
|
+
React.useLayoutEffect(() => {
|
|
239
|
+
setIsReady(true);
|
|
240
|
+
}, []);
|
|
241
|
+
React.useEffect(() => {
|
|
242
|
+
const root = rootRef.current;
|
|
243
|
+
if (!root) return;
|
|
244
|
+
const observer = new MutationObserver(syncOrder);
|
|
245
|
+
observer.observe(root, { childList: true });
|
|
246
|
+
return () => observer.disconnect();
|
|
247
|
+
}, [syncOrder]);
|
|
248
|
+
const sizeObserver = React.useCallback(() => {
|
|
249
|
+
sizeObserverRef.current ??= new ResizeObserver(() => {
|
|
250
|
+
const root = rootRef.current;
|
|
251
|
+
const cards = [...itemsRef.current.values()];
|
|
252
|
+
if (!root || cards.length === 0) return;
|
|
253
|
+
const width = Math.max(...cards.map(({ element }) => element.offsetWidth));
|
|
254
|
+
const height = Math.max(...cards.map(({ element }) => element.offsetHeight));
|
|
255
|
+
root.style.setProperty("--swipe-deck-card-width", `${width}px`);
|
|
256
|
+
root.style.setProperty("--swipe-deck-card-height", `${height}px`);
|
|
257
|
+
});
|
|
258
|
+
return sizeObserverRef.current;
|
|
259
|
+
}, []);
|
|
260
|
+
React.useEffect(() => () => sizeObserverRef.current?.disconnect(), []);
|
|
261
|
+
const register = React.useCallback((value, record) => {
|
|
262
|
+
itemsRef.current.set(value, record);
|
|
263
|
+
sizeObserver().observe(record.element);
|
|
264
|
+
syncOrder();
|
|
265
|
+
return () => {
|
|
266
|
+
if (itemsRef.current.get(value) === record) {
|
|
267
|
+
itemsRef.current.delete(value);
|
|
268
|
+
sizeObserverRef.current?.unobserve(record.element);
|
|
269
|
+
syncOrder();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}, [sizeObserver, syncOrder]);
|
|
273
|
+
const focusDeck = React.useCallback(() => rootRef.current?.focus(), []);
|
|
274
|
+
const [settledFront, setSettledFront] = React.useState();
|
|
275
|
+
React.useEffect(() => lift.on("change", (latest) => {
|
|
276
|
+
rootRef.current?.style.setProperty("--swipe-deck-lift", String(latest));
|
|
277
|
+
}), [lift]);
|
|
278
|
+
const leave = React.useCallback((value) => {
|
|
279
|
+
setLeavingList((previous) => previous.includes(value) ? previous : [...previous, value]);
|
|
280
|
+
}, []);
|
|
281
|
+
const finish = React.useCallback((details) => {
|
|
282
|
+
const current = readOrder();
|
|
283
|
+
finishedRef.current.set(details.value, current.slice(current.indexOf(details.value) + 1));
|
|
284
|
+
onSwipeRef.current?.(details);
|
|
285
|
+
}, [readOrder]);
|
|
286
|
+
const leaving = React.useMemo(() => new Set(leavingList), [leavingList]);
|
|
287
|
+
const positions = React.useMemo(() => new Map(order.filter((value) => !leaving.has(value)).map((value, index) => [value, index])), [order, leaving]);
|
|
288
|
+
const front = order.find((value) => !leaving.has(value));
|
|
289
|
+
const swipeFront = React.useCallback((direction) => {
|
|
290
|
+
if (disabled || front === void 0 || !directions.includes(direction)) return;
|
|
291
|
+
itemsRef.current.get(front)?.swipe(direction);
|
|
292
|
+
}, [
|
|
293
|
+
directions,
|
|
294
|
+
disabled,
|
|
295
|
+
front
|
|
296
|
+
]);
|
|
297
|
+
const announcedRef = React.useRef(void 0);
|
|
298
|
+
React.useEffect(() => {
|
|
299
|
+
const previous = announcedRef.current;
|
|
300
|
+
announcedRef.current = front;
|
|
301
|
+
const status = statusRef.current;
|
|
302
|
+
if (previous === void 0 || previous === front || !status) return;
|
|
303
|
+
const element = front === void 0 ? emptyRef.current : itemsRef.current.get(front)?.element;
|
|
304
|
+
status.textContent = element ? element.getAttribute("aria-label") ?? (element.innerText || element.textContent || "") : "";
|
|
305
|
+
}, [front]);
|
|
306
|
+
const handleKeyDown = (event) => {
|
|
307
|
+
onKeyDown?.(event);
|
|
308
|
+
if (event.defaultPrevented || event.target !== event.currentTarget) return;
|
|
309
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
310
|
+
const direction = KEY_DIRECTIONS[event.key];
|
|
311
|
+
if (!direction || disabled || !directions.includes(direction)) return;
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
swipeFront(direction);
|
|
314
|
+
};
|
|
315
|
+
const context = React.useMemo(() => ({
|
|
316
|
+
directions,
|
|
317
|
+
disabled,
|
|
318
|
+
isReady,
|
|
319
|
+
front,
|
|
320
|
+
positions,
|
|
321
|
+
leaving,
|
|
322
|
+
count: order.length,
|
|
323
|
+
lift,
|
|
324
|
+
register,
|
|
325
|
+
leave,
|
|
326
|
+
finish,
|
|
327
|
+
swipeFront,
|
|
328
|
+
isEmpty: isReady && front === void 0,
|
|
329
|
+
emptyRef,
|
|
330
|
+
focusDeck,
|
|
331
|
+
settledFront,
|
|
332
|
+
settle: setSettledFront
|
|
333
|
+
}), [
|
|
334
|
+
directions,
|
|
335
|
+
disabled,
|
|
336
|
+
isReady,
|
|
337
|
+
front,
|
|
338
|
+
positions,
|
|
339
|
+
leaving,
|
|
340
|
+
order.length,
|
|
341
|
+
lift,
|
|
342
|
+
register,
|
|
343
|
+
leave,
|
|
344
|
+
finish,
|
|
345
|
+
swipeFront,
|
|
346
|
+
focusDeck,
|
|
347
|
+
settledFront
|
|
348
|
+
]);
|
|
349
|
+
return /* @__PURE__ */ jsx(SwipeDeckContext.Provider, {
|
|
350
|
+
value: context,
|
|
351
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
352
|
+
ref: mergedRef,
|
|
353
|
+
"data-slot": "swipe-deck",
|
|
354
|
+
"data-layout": layout,
|
|
355
|
+
"data-empty": context.isEmpty ? "" : void 0,
|
|
356
|
+
"data-disabled": disabled ? "" : void 0,
|
|
357
|
+
role: "group",
|
|
358
|
+
tabIndex: 0,
|
|
359
|
+
"aria-disabled": disabled || void 0,
|
|
360
|
+
className: cn("group/swipe-deck relative isolate grid outline-hidden", "ring-focus/50 rounded-lg focus-visible:not-has-data-front:ring-3", className),
|
|
361
|
+
style: {
|
|
362
|
+
"--swipe-deck-spread": within(spread, 0, 2, .5),
|
|
363
|
+
"--swipe-deck-depth": within(depth, 0, 1, .5),
|
|
364
|
+
"--swipe-deck-visible": Math.round(within(visible, 1, Infinity, 4)),
|
|
365
|
+
...style
|
|
366
|
+
},
|
|
367
|
+
onKeyDown: handleKeyDown,
|
|
368
|
+
...props,
|
|
369
|
+
children: [children, /* @__PURE__ */ jsx("span", {
|
|
370
|
+
ref: statusRef,
|
|
371
|
+
"data-slot": "swipe-deck-status",
|
|
372
|
+
role: "status",
|
|
373
|
+
className: "sr-only"
|
|
374
|
+
})]
|
|
375
|
+
})
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* One card in the deck. It sits in the deck's first grid cell with every other
|
|
380
|
+
* card, so the deck takes the size of its largest card.
|
|
381
|
+
*
|
|
382
|
+
* Styling hooks: `data-front` on the top card, `data-swiping` while it is
|
|
383
|
+
* dragged, `data-leaving` from the moment it leaves, `data-ending-style` once
|
|
384
|
+
* it is clear of the stack and fading (the `motion-fade-lg` recipe), and
|
|
385
|
+
* `data-swipe-direction` for the way it leans or leaves. CSS variables:
|
|
386
|
+
* `--swipe-deck-index` (its place in the pile, 0 at the front),
|
|
387
|
+
* `--swipe-deck-place` (that place as it springs from one to the next, less
|
|
388
|
+
* the lift while the front card is dragged out, as `--swipe-deck-slot`),
|
|
389
|
+
* `--swipe-deck-movement-x` / `-y` in px, and `--swipe-deck-progress` from 0
|
|
390
|
+
* at rest to 1 where letting go makes the card leave.
|
|
391
|
+
*
|
|
392
|
+
* The card owns its translate, rotate and scale, so its motion recipe can only
|
|
393
|
+
* be a `motion-fade-*` one: a recipe that transitions those would lag every
|
|
394
|
+
* drag.
|
|
395
|
+
*/
|
|
396
|
+
function SwipeDeckItem({ value, className, style, ref, onPointerDown, onClickCapture, onDragStart, ...props }) {
|
|
397
|
+
const deck = useSwipeDeckContext("SwipeDeckItem");
|
|
398
|
+
const { directions, register, leave, finish } = deck;
|
|
399
|
+
const prefersReducedMotion = useReducedMotion();
|
|
400
|
+
const elementRef = React.useRef(null);
|
|
401
|
+
const mergedRef = React.useMemo(() => mergeRefs(elementRef, ref), [ref]);
|
|
402
|
+
const x = useMotionValue(0);
|
|
403
|
+
const y = useMotionValue(0);
|
|
404
|
+
const place = useMotionValue(0);
|
|
405
|
+
const enterScale = useMotionValue(1);
|
|
406
|
+
const drivesLiftRef = React.useRef(false);
|
|
407
|
+
const scatter = React.useMemo(() => scatterOf(value), [value]);
|
|
408
|
+
const leftTowardRef = React.useRef(null);
|
|
409
|
+
const stopWatchingRef = React.useRef(null);
|
|
410
|
+
const dragRef = React.useRef(null);
|
|
411
|
+
const endDragRef = React.useRef(null);
|
|
412
|
+
const sizeRef = React.useRef({
|
|
413
|
+
width: 0,
|
|
414
|
+
height: 0
|
|
415
|
+
});
|
|
416
|
+
const draggedRef = React.useRef(false);
|
|
417
|
+
const [isSwiping, setIsSwiping] = React.useState(false);
|
|
418
|
+
const [isFading, setIsFading] = React.useState(false);
|
|
419
|
+
const [isStarting, setIsStarting] = React.useState(true);
|
|
420
|
+
const [direction, setDirection] = React.useState();
|
|
421
|
+
const position = deck.positions.get(value);
|
|
422
|
+
const isLeaving = deck.leaving.has(value);
|
|
423
|
+
const isFront = deck.front === value;
|
|
424
|
+
const directionsRef = React.useRef(directions);
|
|
425
|
+
const disabledRef = React.useRef(deck.disabled);
|
|
426
|
+
const reducedMotionRef = React.useRef(prefersReducedMotion);
|
|
427
|
+
React.useEffect(() => {
|
|
428
|
+
directionsRef.current = directions;
|
|
429
|
+
disabledRef.current = deck.disabled;
|
|
430
|
+
reducedMotionRef.current = prefersReducedMotion;
|
|
431
|
+
drivesLiftRef.current = isFront && !isLeaving;
|
|
432
|
+
});
|
|
433
|
+
React.useEffect(() => {
|
|
434
|
+
const write = () => {
|
|
435
|
+
const element = elementRef.current;
|
|
436
|
+
if (!element) return;
|
|
437
|
+
const { share } = lean(directionsRef.current, x.get(), y.get(), sizeRef.current);
|
|
438
|
+
element.style.setProperty("--swipe-deck-movement-x", `${x.get()}px`);
|
|
439
|
+
element.style.setProperty("--swipe-deck-movement-y", `${y.get()}px`);
|
|
440
|
+
element.style.setProperty("--swipe-deck-progress", String(Math.min(share, 1)));
|
|
441
|
+
if (drivesLiftRef.current) deck.lift.set(Math.min(share, 1) * LIFT);
|
|
442
|
+
};
|
|
443
|
+
const stopX = x.on("change", write);
|
|
444
|
+
const stopY = y.on("change", write);
|
|
445
|
+
const stopPlace = place.on("change", (latest) => {
|
|
446
|
+
elementRef.current?.style.setProperty("--swipe-deck-place", String(latest));
|
|
447
|
+
});
|
|
448
|
+
const stopEnterScale = enterScale.on("change", (latest) => {
|
|
449
|
+
elementRef.current?.style.setProperty("--swipe-deck-enter-scale", String(latest));
|
|
450
|
+
});
|
|
451
|
+
return () => {
|
|
452
|
+
stopX();
|
|
453
|
+
stopY();
|
|
454
|
+
stopPlace();
|
|
455
|
+
stopEnterScale();
|
|
456
|
+
if (dragRef.current) releaseLift(deck.lift, reducedMotionRef.current);
|
|
457
|
+
endDragRef.current?.();
|
|
458
|
+
stopWatchingRef.current?.();
|
|
459
|
+
x.stop();
|
|
460
|
+
y.stop();
|
|
461
|
+
place.stop();
|
|
462
|
+
enterScale.stop();
|
|
463
|
+
};
|
|
464
|
+
}, [
|
|
465
|
+
deck.lift,
|
|
466
|
+
enterScale,
|
|
467
|
+
place,
|
|
468
|
+
x,
|
|
469
|
+
y
|
|
470
|
+
]);
|
|
471
|
+
const snapBack = React.useCallback(([vx, vy] = [0, 0]) => {
|
|
472
|
+
if (prefersReducedMotion) {
|
|
473
|
+
x.set(0);
|
|
474
|
+
y.set(0);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
animate(x, 0, {
|
|
478
|
+
...RETURN_SPRING,
|
|
479
|
+
velocity: vx
|
|
480
|
+
});
|
|
481
|
+
animate(y, 0, {
|
|
482
|
+
...RETURN_SPRING,
|
|
483
|
+
velocity: vy
|
|
484
|
+
});
|
|
485
|
+
}, [
|
|
486
|
+
prefersReducedMotion,
|
|
487
|
+
x,
|
|
488
|
+
y
|
|
489
|
+
]);
|
|
490
|
+
const fling = React.useCallback((to, [vx, vy] = [0, 0]) => {
|
|
491
|
+
endDragRef.current?.();
|
|
492
|
+
setIsSwiping(false);
|
|
493
|
+
leftTowardRef.current = to;
|
|
494
|
+
leave(value);
|
|
495
|
+
setDirection(to);
|
|
496
|
+
drivesLiftRef.current = false;
|
|
497
|
+
releaseLift(deck.lift, prefersReducedMotion);
|
|
498
|
+
setIsStarting(false);
|
|
499
|
+
if (prefersReducedMotion) {
|
|
500
|
+
setIsFading(true);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const size = sizeRef.current;
|
|
504
|
+
const aheadX = x.get() + vx * THROW_TIME;
|
|
505
|
+
const aheadY = y.get() + vy * THROW_TIME;
|
|
506
|
+
const sign = to === "right" || to === "down" ? 1 : -1;
|
|
507
|
+
const heading = Math.hypot(aheadX, aheadY) >= 1 ? [aheadX, aheadY] : isHorizontal(to) ? [sign, 0] : [0, sign];
|
|
508
|
+
const [targetX, targetY] = exitPoint(x.get(), y.get(), heading, size);
|
|
509
|
+
const headingLength = Math.hypot(...heading);
|
|
510
|
+
const [ux, uy] = [heading[0] / headingLength, heading[1] / headingLength];
|
|
511
|
+
const fade = () => {
|
|
512
|
+
stopWatchingRef.current?.();
|
|
513
|
+
stopWatchingRef.current = null;
|
|
514
|
+
setIsFading(true);
|
|
515
|
+
};
|
|
516
|
+
const check = () => {
|
|
517
|
+
const outward = x.getVelocity() * ux + y.getVelocity() * uy;
|
|
518
|
+
if (isClear(x.get(), y.get(), size) && outward <= CAUGHT_SPEED) fade();
|
|
519
|
+
};
|
|
520
|
+
stopWatchingRef.current?.();
|
|
521
|
+
const stopX = x.on("change", check);
|
|
522
|
+
const stopY = y.on("change", check);
|
|
523
|
+
stopWatchingRef.current = () => {
|
|
524
|
+
stopX();
|
|
525
|
+
stopY();
|
|
526
|
+
};
|
|
527
|
+
const isMainX = Math.abs(targetX - x.get()) >= Math.abs(targetY - y.get());
|
|
528
|
+
animate(x, targetX, {
|
|
529
|
+
...EXIT_SPRING,
|
|
530
|
+
velocity: vx,
|
|
531
|
+
onComplete: isMainX ? fade : void 0
|
|
532
|
+
});
|
|
533
|
+
animate(y, targetY, {
|
|
534
|
+
...EXIT_SPRING,
|
|
535
|
+
velocity: vy,
|
|
536
|
+
onComplete: isMainX ? void 0 : fade
|
|
537
|
+
});
|
|
538
|
+
}, [
|
|
539
|
+
deck.lift,
|
|
540
|
+
leave,
|
|
541
|
+
prefersReducedMotion,
|
|
542
|
+
value,
|
|
543
|
+
x,
|
|
544
|
+
y
|
|
545
|
+
]);
|
|
546
|
+
React.useLayoutEffect(() => {
|
|
547
|
+
const element = elementRef.current;
|
|
548
|
+
if (!element) return;
|
|
549
|
+
return register(value, {
|
|
550
|
+
element,
|
|
551
|
+
swipe: (to) => {
|
|
552
|
+
sizeRef.current = sizeOf(element);
|
|
553
|
+
fling(to);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}, [
|
|
557
|
+
fling,
|
|
558
|
+
register,
|
|
559
|
+
value
|
|
560
|
+
]);
|
|
561
|
+
React.useEffect(() => {
|
|
562
|
+
const element = elementRef.current;
|
|
563
|
+
const to = leftTowardRef.current;
|
|
564
|
+
if (!isFading || !element || !to) return;
|
|
565
|
+
let isCancelled = false;
|
|
566
|
+
const frame = requestAnimationFrame(() => {
|
|
567
|
+
transitionsEnd(element).then(() => {
|
|
568
|
+
if (isCancelled) return;
|
|
569
|
+
x.stop();
|
|
570
|
+
y.stop();
|
|
571
|
+
finish({
|
|
572
|
+
value,
|
|
573
|
+
direction: to
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
return () => {
|
|
578
|
+
isCancelled = true;
|
|
579
|
+
cancelAnimationFrame(frame);
|
|
580
|
+
};
|
|
581
|
+
}, [
|
|
582
|
+
finish,
|
|
583
|
+
isFading,
|
|
584
|
+
value,
|
|
585
|
+
x,
|
|
586
|
+
y
|
|
587
|
+
]);
|
|
588
|
+
const wasLeavingRef = React.useRef(false);
|
|
589
|
+
const isPlacedRef = React.useRef(false);
|
|
590
|
+
React.useEffect(() => {
|
|
591
|
+
const target = isLeaving ? 0 : position;
|
|
592
|
+
if (target === void 0) return;
|
|
593
|
+
if (wasLeavingRef.current && !isLeaving) {
|
|
594
|
+
x.jump(0);
|
|
595
|
+
y.jump(0);
|
|
596
|
+
place.jump(target);
|
|
597
|
+
setIsFading(false);
|
|
598
|
+
} else if (prefersReducedMotion || !isPlacedRef.current) place.jump(target);
|
|
599
|
+
else animate(place, target, PLACE_SPRING);
|
|
600
|
+
isPlacedRef.current = true;
|
|
601
|
+
wasLeavingRef.current = isLeaving;
|
|
602
|
+
}, [
|
|
603
|
+
isLeaving,
|
|
604
|
+
place,
|
|
605
|
+
position,
|
|
606
|
+
prefersReducedMotion,
|
|
607
|
+
x,
|
|
608
|
+
y
|
|
609
|
+
]);
|
|
610
|
+
const enterAtRef = React.useRef(null);
|
|
611
|
+
React.useEffect(() => {
|
|
612
|
+
if (!isStarting || position === void 0) return;
|
|
613
|
+
if (enterAtRef.current === null) {
|
|
614
|
+
const delay = prefersReducedMotion || position === 0 ? 0 : ENTER_LEAD + (position - 1) * ENTER_STAGGER;
|
|
615
|
+
enterAtRef.current = performance.now() + delay * 1e3;
|
|
616
|
+
if (delay > 0) {
|
|
617
|
+
enterScale.jump(ENTER_SCALE);
|
|
618
|
+
animate(enterScale, 1, {
|
|
619
|
+
...PLACE_SPRING,
|
|
620
|
+
delay
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
} else if (position === 0) animate(enterScale, 1, PLACE_SPRING);
|
|
624
|
+
const wait = position === 0 ? 0 : Math.max(0, enterAtRef.current - performance.now());
|
|
625
|
+
let frame = 0;
|
|
626
|
+
const timer = window.setTimeout(() => {
|
|
627
|
+
frame = requestAnimationFrame(() => setIsStarting(false));
|
|
628
|
+
}, wait);
|
|
629
|
+
return () => {
|
|
630
|
+
window.clearTimeout(timer);
|
|
631
|
+
cancelAnimationFrame(frame);
|
|
632
|
+
};
|
|
633
|
+
}, [
|
|
634
|
+
enterScale,
|
|
635
|
+
isStarting,
|
|
636
|
+
position,
|
|
637
|
+
prefersReducedMotion
|
|
638
|
+
]);
|
|
639
|
+
const { settle } = deck;
|
|
640
|
+
React.useEffect(() => {
|
|
641
|
+
const element = elementRef.current;
|
|
642
|
+
if (!isFront || isStarting || !element) return;
|
|
643
|
+
let isCancelled = false;
|
|
644
|
+
const report = () => {
|
|
645
|
+
transitionsEnd(element).then(() => {
|
|
646
|
+
if (!isCancelled) settle(value);
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
const stopListening = place.on("animationComplete", report);
|
|
650
|
+
if (!place.isAnimating()) report();
|
|
651
|
+
return () => {
|
|
652
|
+
isCancelled = true;
|
|
653
|
+
stopListening();
|
|
654
|
+
};
|
|
655
|
+
}, [
|
|
656
|
+
isFront,
|
|
657
|
+
isStarting,
|
|
658
|
+
place,
|
|
659
|
+
settle,
|
|
660
|
+
value
|
|
661
|
+
]);
|
|
662
|
+
React.useEffect(() => {
|
|
663
|
+
if (isFront && !deck.disabled || !dragRef.current) return;
|
|
664
|
+
endDragRef.current?.();
|
|
665
|
+
setIsSwiping(false);
|
|
666
|
+
snapBack();
|
|
667
|
+
if (!isFront) releaseLift(deck.lift, prefersReducedMotion);
|
|
668
|
+
}, [
|
|
669
|
+
deck.disabled,
|
|
670
|
+
deck.lift,
|
|
671
|
+
isFront,
|
|
672
|
+
prefersReducedMotion,
|
|
673
|
+
snapBack
|
|
674
|
+
]);
|
|
675
|
+
const release = (drag, event) => {
|
|
676
|
+
if (!drag.moved) {
|
|
677
|
+
snapBack();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
setIsSwiping(false);
|
|
681
|
+
draggedRef.current = true;
|
|
682
|
+
window.setTimeout(() => {
|
|
683
|
+
draggedRef.current = false;
|
|
684
|
+
});
|
|
685
|
+
if (event.type === "pointerup") drag.samples.push({
|
|
686
|
+
x: event.clientX,
|
|
687
|
+
y: event.clientY,
|
|
688
|
+
time: event.timeStamp
|
|
689
|
+
});
|
|
690
|
+
const velocity = releaseVelocity(drag.samples, event.timeStamp);
|
|
691
|
+
if (event.type === "pointerup" && !disabledRef.current) {
|
|
692
|
+
const ahead = lean(directionsRef.current, x.get() + velocity[0] * THROW_TIME, y.get() + velocity[1] * THROW_TIME, sizeRef.current);
|
|
693
|
+
if (ahead.direction && ahead.share >= 1) {
|
|
694
|
+
fling(ahead.direction, velocity);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
snapBack(velocity);
|
|
699
|
+
};
|
|
700
|
+
const handlePointerDown = (event) => {
|
|
701
|
+
onPointerDown?.(event);
|
|
702
|
+
if (event.defaultPrevented || event.button !== 0 || !isFront || deck.disabled || dragRef.current) return;
|
|
703
|
+
const element = event.currentTarget;
|
|
704
|
+
const field = event.target instanceof Element ? event.target.closest(FIELD_SELECTOR) : null;
|
|
705
|
+
if (field && element.contains(field)) return;
|
|
706
|
+
deck.lift.stop();
|
|
707
|
+
x.stop();
|
|
708
|
+
y.stop();
|
|
709
|
+
sizeRef.current = sizeOf(element);
|
|
710
|
+
const drag = {
|
|
711
|
+
pointerId: event.pointerId,
|
|
712
|
+
startX: event.clientX,
|
|
713
|
+
startY: event.clientY,
|
|
714
|
+
originX: x.get(),
|
|
715
|
+
originY: y.get(),
|
|
716
|
+
moved: false,
|
|
717
|
+
samples: [{
|
|
718
|
+
x: event.clientX,
|
|
719
|
+
y: event.clientY,
|
|
720
|
+
time: event.timeStamp
|
|
721
|
+
}]
|
|
722
|
+
};
|
|
723
|
+
dragRef.current = drag;
|
|
724
|
+
const handleMove = (moveEvent) => {
|
|
725
|
+
if (moveEvent.pointerId !== drag.pointerId) return;
|
|
726
|
+
const dx = moveEvent.clientX - drag.startX;
|
|
727
|
+
const dy = moveEvent.clientY - drag.startY;
|
|
728
|
+
if (!drag.moved) {
|
|
729
|
+
if (Math.hypot(dx, dy) < DRAG_SLOP) return;
|
|
730
|
+
drag.moved = true;
|
|
731
|
+
element.setPointerCapture(drag.pointerId);
|
|
732
|
+
setIsSwiping(true);
|
|
733
|
+
}
|
|
734
|
+
x.set(drag.originX + dx);
|
|
735
|
+
y.set(drag.originY + dy);
|
|
736
|
+
drag.samples.push({
|
|
737
|
+
x: moveEvent.clientX,
|
|
738
|
+
y: moveEvent.clientY,
|
|
739
|
+
time: moveEvent.timeStamp
|
|
740
|
+
});
|
|
741
|
+
if (drag.samples.length > 20) drag.samples.shift();
|
|
742
|
+
setDirection(lean(directionsRef.current, x.get(), y.get(), sizeRef.current).direction);
|
|
743
|
+
};
|
|
744
|
+
const handleEnd = (endEvent) => {
|
|
745
|
+
if (endEvent.pointerId !== drag.pointerId) return;
|
|
746
|
+
endDrag();
|
|
747
|
+
release(drag, endEvent);
|
|
748
|
+
};
|
|
749
|
+
const endDrag = () => {
|
|
750
|
+
window.removeEventListener("pointermove", handleMove);
|
|
751
|
+
window.removeEventListener("pointerup", handleEnd);
|
|
752
|
+
window.removeEventListener("pointercancel", handleEnd);
|
|
753
|
+
dragRef.current = null;
|
|
754
|
+
endDragRef.current = null;
|
|
755
|
+
};
|
|
756
|
+
window.addEventListener("pointermove", handleMove);
|
|
757
|
+
window.addEventListener("pointerup", handleEnd);
|
|
758
|
+
window.addEventListener("pointercancel", handleEnd);
|
|
759
|
+
endDragRef.current = endDrag;
|
|
760
|
+
};
|
|
761
|
+
const handleClickCapture = (event) => {
|
|
762
|
+
if (draggedRef.current) {
|
|
763
|
+
draggedRef.current = false;
|
|
764
|
+
event.preventDefault();
|
|
765
|
+
event.stopPropagation();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
onClickCapture?.(event);
|
|
769
|
+
};
|
|
770
|
+
const zIndex = isLeaving ? deck.count + 1 : position === void 0 ? void 0 : deck.count - position;
|
|
771
|
+
return /* @__PURE__ */ jsx("div", {
|
|
772
|
+
ref: mergedRef,
|
|
773
|
+
"data-slot": "swipe-deck-item",
|
|
774
|
+
role: "group",
|
|
775
|
+
"data-front": isFront ? "" : void 0,
|
|
776
|
+
"data-swiping": isSwiping ? "" : void 0,
|
|
777
|
+
"data-leaving": isLeaving ? "" : void 0,
|
|
778
|
+
"data-starting-style": isStarting ? "" : void 0,
|
|
779
|
+
"data-ending-style": isFading ? "" : void 0,
|
|
780
|
+
"data-swipe-direction": isSwiping || isLeaving ? direction : void 0,
|
|
781
|
+
inert: isLeaving || position !== void 0 && position > 0,
|
|
782
|
+
className: cn("motion-fade-lg relative col-start-1 row-start-1 rounded-lg select-none", "[--swipe-deck-slot:calc(var(--swipe-deck-place,0)_-_min(var(--swipe-deck-place,0),1)_*_var(--swipe-deck-lift,0))]", "[--swipe-deck-layout-slot:min(var(--swipe-deck-slot),calc(var(--swipe-deck-visible,4)_-_1))]", "[translate:calc(var(--swipe-deck-movement-x,0px)_+_var(--swipe-deck-layout-x,0px))_calc(var(--swipe-deck-movement-y,0px)_+_var(--swipe-deck-layout-y,0px))]", "[rotate:var(--swipe-deck-layout-rotate,0deg)] [scale:calc(var(--swipe-deck-layout-scale,1)_*_var(--swipe-deck-enter-scale,1))]", "[opacity:clamp(0,calc(var(--swipe-deck-visible,4)_-_var(--swipe-deck-slot)),1)]", "group-data-[layout=stack]/swipe-deck:origin-top", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-32px)]", "group-data-[layout=stack]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.1))]", "group-data-[layout=fan]/swipe-deck:origin-[90%_90%]", "group-data-[layout=fan]/swipe-deck:[--swipe-deck-layout-rotate:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_8deg)]", "group-data-[layout=fan]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.12))]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-x:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-28px)]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-layout-slot)_*_var(--swipe-deck-spread,0.5)_*_-28px)]", "group-data-[layout=cascade]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.07))]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-x:calc(var(--swipe-deck-scatter-x)_*_var(--swipe-deck-spread,0.5)_*_56px)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-y:calc(var(--swipe-deck-scatter-y)_*_var(--swipe-deck-spread,0.5)_*_44px)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-rotate:calc(var(--swipe-deck-scatter-rotate)_*_var(--swipe-deck-spread,0.5)_*_40deg)]", "group-data-[layout=pile]/swipe-deck:[--swipe-deck-layout-scale:max(0,calc(1_-_var(--swipe-deck-layout-slot)_*_var(--swipe-deck-depth,0.5)_*_0.03))]", "ring-focus/50 group-focus-visible/swipe-deck:data-front:ring-3", "data-front:cursor-grab data-swiping:cursor-grabbing group-data-disabled/swipe-deck:data-front:cursor-auto", touchAction(directions), className),
|
|
783
|
+
style: {
|
|
784
|
+
zIndex,
|
|
785
|
+
"--swipe-deck-index": isLeaving ? 0 : position,
|
|
786
|
+
"--swipe-deck-scatter-x": scatter[0],
|
|
787
|
+
"--swipe-deck-scatter-y": scatter[1],
|
|
788
|
+
"--swipe-deck-scatter-rotate": scatter[2],
|
|
789
|
+
...style
|
|
790
|
+
},
|
|
791
|
+
onPointerDown: handlePointerDown,
|
|
792
|
+
onClickCapture: handleClickCapture,
|
|
793
|
+
onDragStart: (event) => {
|
|
794
|
+
onDragStart?.(event);
|
|
795
|
+
event.preventDefault();
|
|
796
|
+
},
|
|
797
|
+
...props
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* A rounded rectangle, inset half a pixel so a 1px line sits inside the box as
|
|
802
|
+
* a border would, with corners that are superellipses of `exponent` (2 is a
|
|
803
|
+
* circular arc, 4 a squircle). Returns the path and its length.
|
|
804
|
+
*/
|
|
805
|
+
function outlinePath(width, height, radius, exponent) {
|
|
806
|
+
const inset = .5;
|
|
807
|
+
const r = Math.max(0, Math.min(radius, width / 2, height / 2) - inset);
|
|
808
|
+
const [left, top] = [inset, inset];
|
|
809
|
+
const [right, bottom] = [width - inset, height - inset];
|
|
810
|
+
const corners = [
|
|
811
|
+
[
|
|
812
|
+
right - r,
|
|
813
|
+
top + r,
|
|
814
|
+
-Math.PI / 2
|
|
815
|
+
],
|
|
816
|
+
[
|
|
817
|
+
right - r,
|
|
818
|
+
bottom - r,
|
|
819
|
+
0
|
|
820
|
+
],
|
|
821
|
+
[
|
|
822
|
+
left + r,
|
|
823
|
+
bottom - r,
|
|
824
|
+
Math.PI / 2
|
|
825
|
+
],
|
|
826
|
+
[
|
|
827
|
+
left + r,
|
|
828
|
+
top + r,
|
|
829
|
+
Math.PI
|
|
830
|
+
]
|
|
831
|
+
];
|
|
832
|
+
const points = [];
|
|
833
|
+
for (const [cx, cy, start] of corners) for (let step = 0; step <= OUTLINE_CORNER_STEPS; step++) {
|
|
834
|
+
const angle = start + step / OUTLINE_CORNER_STEPS * (Math.PI / 2);
|
|
835
|
+
const [cos, sin] = [Math.cos(angle), Math.sin(angle)];
|
|
836
|
+
points.push([cx + r * Math.sign(cos) * Math.abs(cos) ** (2 / exponent), cy + r * Math.sign(sin) * Math.abs(sin) ** (2 / exponent)]);
|
|
837
|
+
}
|
|
838
|
+
const length = points.reduce((total, [x, y], index) => {
|
|
839
|
+
const [px, py] = points[(index + points.length - 1) % points.length];
|
|
840
|
+
return total + Math.hypot(x - px, y - py);
|
|
841
|
+
}, 0);
|
|
842
|
+
return {
|
|
843
|
+
d: `M ${points.map(([x, y]) => `${x.toFixed(2)} ${y.toFixed(2)}`).join(" L ")} Z`,
|
|
844
|
+
length
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* A 1px dashed outline with long dashes on the box it sits in. CSS ties a
|
|
849
|
+
* dashed border's dash length to its width, so this one is drawn, on the same
|
|
850
|
+
* corners as the cards: squircles where the browser draws them, arcs elsewhere.
|
|
851
|
+
*/
|
|
852
|
+
function DashedOutline() {
|
|
853
|
+
const svgRef = React.useRef(null);
|
|
854
|
+
const [outline, setOutline] = React.useState({
|
|
855
|
+
d: "",
|
|
856
|
+
length: 0
|
|
857
|
+
});
|
|
858
|
+
React.useLayoutEffect(() => {
|
|
859
|
+
const box = svgRef.current?.parentElement;
|
|
860
|
+
if (!box) return;
|
|
861
|
+
const exponent = CSS.supports("corner-shape", "squircle") ? 4 : 2;
|
|
862
|
+
const draw = () => setOutline(outlinePath(box.offsetWidth, box.offsetHeight, parseFloat(getComputedStyle(box).borderTopLeftRadius), exponent));
|
|
863
|
+
draw();
|
|
864
|
+
const observer = new ResizeObserver(draw);
|
|
865
|
+
observer.observe(box);
|
|
866
|
+
return () => observer.disconnect();
|
|
867
|
+
}, []);
|
|
868
|
+
const period = 14;
|
|
869
|
+
const pathLength = Math.max(1, Math.round(outline.length / period)) * period;
|
|
870
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
871
|
+
"data-slot": "swipe-deck-empty-outline",
|
|
872
|
+
ref: svgRef,
|
|
873
|
+
className: "pointer-events-none absolute inset-0 size-full overflow-visible",
|
|
874
|
+
"aria-hidden": true,
|
|
875
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
876
|
+
d: outline.d,
|
|
877
|
+
pathLength,
|
|
878
|
+
strokeDasharray: `${OUTLINE_DASH} ${OUTLINE_GAP}`,
|
|
879
|
+
className: "stroke-shade fill-none"
|
|
880
|
+
})
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* What the deck shows once its cards run out: a message, an action (start
|
|
885
|
+
* over, carry on), anything you put in it. It sits where the cards sit, at
|
|
886
|
+
* their size, inside a dashed outline of their shape (`data-slot`
|
|
887
|
+
* `swipe-deck-empty-outline`, to restyle or hide). It is drawn as soon as one
|
|
888
|
+
* card is left and has come to rest, under that card, so dragging it shows
|
|
889
|
+
* what comes next; it
|
|
890
|
+
* can be reached (Tab, clicks) and screen readers hear it (its `aria-label`,
|
|
891
|
+
* or its text) once that last card has gone too.
|
|
892
|
+
*/
|
|
893
|
+
function SwipeDeckEmpty({ className, children, ref, ...props }) {
|
|
894
|
+
const deck = useSwipeDeckContext("SwipeDeckEmpty");
|
|
895
|
+
const { emptyRef, focusDeck, isEmpty } = deck;
|
|
896
|
+
const isDrawn = deck.isReady && (deck.positions.size === 0 || deck.positions.size === 1 && deck.settledFront === deck.front);
|
|
897
|
+
const elementRef = React.useRef(null);
|
|
898
|
+
const mergedRef = React.useMemo(() => mergeRefs(elementRef, emptyRef, ref), [emptyRef, ref]);
|
|
899
|
+
React.useLayoutEffect(() => {
|
|
900
|
+
if (!isEmpty && elementRef.current?.contains(document.activeElement)) focusDeck();
|
|
901
|
+
}, [focusDeck, isEmpty]);
|
|
902
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
903
|
+
ref: mergedRef,
|
|
904
|
+
"data-slot": "swipe-deck-empty",
|
|
905
|
+
inert: !isEmpty,
|
|
906
|
+
className: cn("text-muted relative col-start-1 row-start-1 flex min-h-(--swipe-deck-card-height) min-w-(--swipe-deck-card-width) flex-col items-center justify-center gap-3 rounded-lg p-6 text-center text-sm", !isDrawn && "invisible", className),
|
|
907
|
+
...props,
|
|
908
|
+
children: [/* @__PURE__ */ jsx(DashedOutline, {}), children]
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* A button that sends the front card off in `direction`: the way to swipe
|
|
913
|
+
* without dragging. Disabled when the deck is empty or can't go that way.
|
|
914
|
+
*/
|
|
915
|
+
function SwipeDeckAction({ direction, disabled, onClick, ...props }) {
|
|
916
|
+
const deck = useSwipeDeckContext("SwipeDeckAction");
|
|
917
|
+
const unavailable = deck.disabled || deck.isReady && deck.front === void 0 || !deck.directions.includes(direction);
|
|
918
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
919
|
+
"data-slot": "swipe-deck-action",
|
|
920
|
+
"data-swipe-direction": direction,
|
|
921
|
+
disabled: disabled || unavailable,
|
|
922
|
+
focusableWhenDisabled: true,
|
|
923
|
+
onClick: (event) => {
|
|
924
|
+
onClick?.(event);
|
|
925
|
+
if (!event.defaultPrevented) deck.swipeFront(direction);
|
|
926
|
+
},
|
|
927
|
+
...props
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
//#endregion
|
|
931
|
+
export { SwipeDeck, SwipeDeckAction, SwipeDeckEmpty, SwipeDeckItem };
|