@leavepulse/ui 0.93.3 → 0.94.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 +53 -3
- package/dist/assets/LpTabs-D7EyG21l.css +18 -0
- package/dist/assets/LpVideoTrim-D8JDg9Qn.css +8 -0
- package/dist/chunks/AppShellContextNav-BoH4ffnC.js +54 -0
- package/dist/chunks/{LpAppShell-BWaVRsDB.js → LpAppShell-xEBmGV78.js} +12 -5
- package/dist/chunks/{LpDateRangePicker-CAn2yGbn.js → LpDateRangePicker-DRBB8z6t.js} +1 -1
- package/dist/chunks/{LpDrawer-iRHwJOGn.js → LpDrawer-BkWjFdV0.js} +81 -42
- package/dist/chunks/{LpLightbox-a24V_0Wk.js → LpLightbox-DPgz7IsT.js} +62 -49
- package/dist/chunks/{LpMapPicker-DVfiy3rR.js → LpMapPicker-V0Lybi10.js} +19 -52
- package/dist/chunks/{LpMediaGallery-CtWA5Uab.js → LpMediaGallery-CTejKF2-.js} +1 -1
- package/dist/chunks/{LpModalNav-DivqT1NV.js → LpModalNav-DVXAF-DG.js} +1 -1
- package/dist/chunks/{LpRangeCalendar-C9gkNMYw.js → LpRangeCalendar-9BvaLZji.js} +3 -4
- package/dist/chunks/LpScrollStrip-D4tsOfAQ.js +78 -0
- package/dist/chunks/{LpSidebar-DQtCYlT4.js → LpSidebar-BIhusppV.js} +1 -1
- package/dist/component-names.d.ts +1 -1
- package/dist/components/AppShellContextNav.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpChart.vue.js +3 -3
- package/dist/components/LpDateRangePicker.vue.js +1 -1
- package/dist/components/LpDrawer.vue.d.ts +10 -2
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropzone.vue.d.ts +1 -1
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpMapPicker.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpRangeCalendar.vue.js +1 -1
- package/dist/components/LpRevealOnScroll.vue.d.ts +1 -1
- package/dist/components/LpScrollStrip.vue.d.ts +48 -0
- package/dist/components/LpScrollStrip.vue.js +4 -0
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +4 -4
- package/dist/components/LpTabs.vue.js +104 -90
- package/dist/components/LpVideoTrim.vue.js +41 -54
- package/dist/composables/gesture.d.ts +141 -0
- package/dist/composables/gesture.js +176 -0
- package/dist/composables/scroll.d.ts +5 -1
- package/dist/composables/scroll.js +16 -9
- package/dist/composables/useAppUpdate.d.ts +26 -0
- package/dist/composables/useAppUpdate.js +91 -0
- package/dist/composables/useGesture.d.ts +107 -0
- package/dist/composables/useGesture.js +281 -0
- package/dist/composables/usePaintSelect.js +3 -4
- package/dist/composables/useScrollStrip.d.ts +27 -0
- package/dist/composables/useScrollStrip.js +194 -0
- package/dist/composables/useTouchHold.d.ts +3 -1
- package/dist/composables/useTouchHold.js +9 -51
- package/dist/composables/useZoomPan.js +4 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +180 -152
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/AppShellContextNav.vue +3 -2
- package/src/components/LpAppShell.vue +10 -1
- package/src/components/LpChart.vue +2 -2
- package/src/components/LpDrawer.vue +121 -54
- package/src/components/LpLightbox.vue +33 -20
- package/src/components/LpMapPicker.vue +22 -64
- package/src/components/LpRangeCalendar.vue +4 -5
- package/src/components/LpScrollStrip.vue +96 -0
- package/src/components/LpSidebar.vue +2 -2
- package/src/components/LpTabs.vue +36 -17
- package/src/components/LpVideoTrim.vue +35 -46
- package/src/composables/gesture.ts +314 -0
- package/src/composables/scroll.ts +20 -9
- package/src/composables/useAppUpdate.ts +156 -0
- package/src/composables/useGesture.ts +451 -0
- package/src/composables/usePaintSelect.ts +4 -5
- package/src/composables/useScrollStrip.ts +249 -0
- package/src/composables/useTouchHold.ts +15 -73
- package/src/composables/useZoomPan.ts +6 -9
- package/src/index.ts +27 -0
- package/src/tokens/effects.css +40 -0
- package/dist/assets/LpTabs-Bh7a-Bdl.css +0 -18
- package/dist/assets/LpVideoTrim-CMoUw3dJ.css +0 -8
- package/dist/chunks/AppShellContextNav-C6Ho2mof.js +0 -49
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { ref, toValue, watch, onBeforeUnmount } from "vue";
|
|
2
|
+
import { prefersReducedMotion } from "./easing.js";
|
|
3
|
+
import { pinchFrame, distance, createDragSession, slopFor, LONG_PRESS_MS } from "./gesture.js";
|
|
4
|
+
import { isElement, nodeWindow } from "./realm.js";
|
|
5
|
+
const CLICK_AFTER_HOLD_MS = 350;
|
|
6
|
+
function createGesture(options) {
|
|
7
|
+
const dragging = ref(false);
|
|
8
|
+
const pinching = ref(false);
|
|
9
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
10
|
+
let press = null;
|
|
11
|
+
let pinchStart = 0;
|
|
12
|
+
let pinchLast = null;
|
|
13
|
+
let swallow = false;
|
|
14
|
+
let swallowTimer = 0;
|
|
15
|
+
let holdTimer = 0;
|
|
16
|
+
let held = false;
|
|
17
|
+
let bodyStyle = null;
|
|
18
|
+
let keyWindow = null;
|
|
19
|
+
const pt = (e) => ({ x: e.clientX, y: e.clientY });
|
|
20
|
+
function state(e, p, f, cancelled = false) {
|
|
21
|
+
return { event: e, pointerType: p.type, start: f.start, point: f.point, movement: f.movement, delta: f.delta, velocity: f.velocity, axis: f.axis, elapsed: f.elapsed, cancelled };
|
|
22
|
+
}
|
|
23
|
+
function clickSoon(win, within = 0) {
|
|
24
|
+
swallow = true;
|
|
25
|
+
win.clearTimeout(swallowTimer);
|
|
26
|
+
swallowTimer = win.setTimeout(() => swallow = false, within);
|
|
27
|
+
}
|
|
28
|
+
function stopHold() {
|
|
29
|
+
if (holdTimer) window.clearTimeout(holdTimer);
|
|
30
|
+
holdTimer = 0;
|
|
31
|
+
}
|
|
32
|
+
function onKey(e) {
|
|
33
|
+
if (e.key === "Escape" && dragging.value) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
cancel();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function beginDrag(e, p, f) {
|
|
39
|
+
p.dragging = true;
|
|
40
|
+
dragging.value = true;
|
|
41
|
+
stopHold();
|
|
42
|
+
if (options.capture !== false && p.type !== "touch") {
|
|
43
|
+
try {
|
|
44
|
+
p.el.setPointerCapture(p.id);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const doc = p.el.ownerDocument;
|
|
49
|
+
bodyStyle = { style: doc.body.style, userSelect: doc.body.style.userSelect };
|
|
50
|
+
doc.body.style.userSelect = "none";
|
|
51
|
+
doc.getSelection()?.removeAllRanges();
|
|
52
|
+
keyWindow = nodeWindow(p.el);
|
|
53
|
+
keyWindow.addEventListener("keydown", onKey, true);
|
|
54
|
+
options.onDragStart?.(state(e, p, f));
|
|
55
|
+
}
|
|
56
|
+
function release() {
|
|
57
|
+
if (bodyStyle) bodyStyle.style.userSelect = bodyStyle.userSelect;
|
|
58
|
+
bodyStyle = null;
|
|
59
|
+
keyWindow?.removeEventListener("keydown", onKey, true);
|
|
60
|
+
keyWindow = null;
|
|
61
|
+
dragging.value = false;
|
|
62
|
+
}
|
|
63
|
+
function endPress(e, cancelled) {
|
|
64
|
+
const p = press;
|
|
65
|
+
if (!p) return;
|
|
66
|
+
press = null;
|
|
67
|
+
stopHold();
|
|
68
|
+
if (p.el.hasPointerCapture?.(p.id)) {
|
|
69
|
+
try {
|
|
70
|
+
p.el.releasePointerCapture(p.id);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!p.dragging) {
|
|
75
|
+
release();
|
|
76
|
+
if (held) {
|
|
77
|
+
if (cancelled) swallow = false;
|
|
78
|
+
else clickSoon(nodeWindow(p.el), CLICK_AFTER_HOLD_MS);
|
|
79
|
+
held = false;
|
|
80
|
+
} else if (!cancelled) options.onTap?.(e);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const f = p.session.end(pt(e), e.timeStamp, options.swipe);
|
|
84
|
+
const s = state(e, p, f, cancelled);
|
|
85
|
+
const swipe = cancelled ? null : f.swipe;
|
|
86
|
+
if (!cancelled && options.preventClick !== false) clickSoon(nodeWindow(p.el));
|
|
87
|
+
release();
|
|
88
|
+
const end = { ...s, swipe };
|
|
89
|
+
options.onDragEnd?.(end);
|
|
90
|
+
if (swipe) options.onSwipe?.(swipe, end);
|
|
91
|
+
}
|
|
92
|
+
function endPinch(e, cancelled) {
|
|
93
|
+
if (!pinching.value) return;
|
|
94
|
+
pinching.value = false;
|
|
95
|
+
pinchStart = 0;
|
|
96
|
+
if (pinchLast) options.onPinchEnd?.({ ...pinchLast, event: e, cancelled });
|
|
97
|
+
pinchLast = null;
|
|
98
|
+
}
|
|
99
|
+
function allowed(e) {
|
|
100
|
+
if (toValue(options.enabled) === false) return false;
|
|
101
|
+
if (options.pointerTypes && !options.pointerTypes.includes(e.pointerType)) return false;
|
|
102
|
+
if (e.pointerType === "mouse" && e.button !== 0) return false;
|
|
103
|
+
if (options.ignore && isElement(e.target) && e.target.closest(options.ignore)) return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function onPointerdown(e) {
|
|
107
|
+
if (!allowed(e)) return;
|
|
108
|
+
pointers.set(e.pointerId, pt(e));
|
|
109
|
+
if (pointers.size === 2 && (options.onPinch || options.onPinchStart)) {
|
|
110
|
+
if (press) endPress(e, true);
|
|
111
|
+
const [a, b] = [...pointers.values()];
|
|
112
|
+
pinchStart = distance(a, b);
|
|
113
|
+
pinching.value = true;
|
|
114
|
+
pinchLast = { ...pinchFrame(a, b, pinchStart), event: e };
|
|
115
|
+
try {
|
|
116
|
+
;
|
|
117
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
options.onPinchStart?.(pinchLast);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (pointers.size > 1 || press) return;
|
|
124
|
+
if (options.onPress?.(e) === false) {
|
|
125
|
+
pointers.delete(e.pointerId);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const el = e.currentTarget;
|
|
129
|
+
held = false;
|
|
130
|
+
const session = createDragSession(pt(e), e.timeStamp, { slop: options.threshold ?? slopFor(e.pointerType), axis: options.axis });
|
|
131
|
+
press = { id: e.pointerId, type: e.pointerType, el, session, dragging: false, first: e };
|
|
132
|
+
const lp = options.longPress;
|
|
133
|
+
if (lp && (!lp.pointerTypes || lp.pointerTypes.includes(e.pointerType))) {
|
|
134
|
+
holdTimer = window.setTimeout(() => {
|
|
135
|
+
holdTimer = 0;
|
|
136
|
+
if (!press || press.dragging) return;
|
|
137
|
+
held = true;
|
|
138
|
+
lp.handler(e);
|
|
139
|
+
}, toValue(lp.delay) ?? LONG_PRESS_MS);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function onPointermove(e) {
|
|
143
|
+
if (!pointers.has(e.pointerId)) return;
|
|
144
|
+
pointers.set(e.pointerId, pt(e));
|
|
145
|
+
if (pinching.value && pointers.size >= 2) {
|
|
146
|
+
const [a, b] = [...pointers.values()];
|
|
147
|
+
pinchLast = { ...pinchFrame(a, b, pinchStart), event: e };
|
|
148
|
+
options.onPinch?.(pinchLast);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const p = press;
|
|
152
|
+
if (!p || e.pointerId !== p.id) return;
|
|
153
|
+
const f = p.session.move(pt(e), e.timeStamp);
|
|
154
|
+
if (f.phase === "rejected") {
|
|
155
|
+
press = null;
|
|
156
|
+
pointers.delete(e.pointerId);
|
|
157
|
+
stopHold();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (f.phase === "pending") return;
|
|
161
|
+
if (!p.dragging) {
|
|
162
|
+
stopHold();
|
|
163
|
+
if (held) return;
|
|
164
|
+
if (!options.onDrag && !options.onDragStart && !options.onDragEnd && !options.onSwipe) return;
|
|
165
|
+
beginDrag(e, p, f);
|
|
166
|
+
}
|
|
167
|
+
options.onDrag?.(state(e, p, f));
|
|
168
|
+
}
|
|
169
|
+
function onPointerup(e) {
|
|
170
|
+
if (!pointers.delete(e.pointerId)) return;
|
|
171
|
+
if (pinching.value) {
|
|
172
|
+
if (pointers.size < 2) endPinch(e, false);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (press?.id === e.pointerId) endPress(e, false);
|
|
176
|
+
}
|
|
177
|
+
function onPointercancel(e) {
|
|
178
|
+
if (!pointers.delete(e.pointerId)) return;
|
|
179
|
+
if (pinching.value) {
|
|
180
|
+
endPinch(e, true);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (press?.id === e.pointerId) endPress(e, true);
|
|
184
|
+
}
|
|
185
|
+
function onLostpointercapture(e) {
|
|
186
|
+
if (press?.dragging && press.id === e.pointerId) {
|
|
187
|
+
pointers.delete(e.pointerId);
|
|
188
|
+
endPress(e, true);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function onClickCapture(e) {
|
|
192
|
+
if (!swallow) return;
|
|
193
|
+
swallow = false;
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
e.stopImmediatePropagation();
|
|
196
|
+
}
|
|
197
|
+
function onDragstart(e) {
|
|
198
|
+
if (press) e.preventDefault();
|
|
199
|
+
}
|
|
200
|
+
function onContextmenu(e) {
|
|
201
|
+
if (holdTimer || held || dragging.value) e.preventDefault();
|
|
202
|
+
}
|
|
203
|
+
function cancel() {
|
|
204
|
+
const p = press;
|
|
205
|
+
if (p) {
|
|
206
|
+
pointers.delete(p.id);
|
|
207
|
+
endPress(p.first, true);
|
|
208
|
+
}
|
|
209
|
+
if (pinching.value && pinchLast) endPinch(pinchLast.event, true);
|
|
210
|
+
pointers.clear();
|
|
211
|
+
}
|
|
212
|
+
function dispose() {
|
|
213
|
+
stopHold();
|
|
214
|
+
if (press?.dragging) release();
|
|
215
|
+
press = null;
|
|
216
|
+
pointers.clear();
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
dragging,
|
|
220
|
+
pinching,
|
|
221
|
+
cancel,
|
|
222
|
+
dispose,
|
|
223
|
+
handlers: { onPointerdown, onPointermove, onPointerup, onPointercancel, onLostpointercapture, onClickCapture, onDragstart, onContextmenu }
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function useGesture(target, options) {
|
|
227
|
+
const gesture = createGesture(options);
|
|
228
|
+
const h = gesture.handlers;
|
|
229
|
+
const bindings = [
|
|
230
|
+
["pointerdown", h.onPointerdown],
|
|
231
|
+
["pointermove", h.onPointermove],
|
|
232
|
+
["pointerup", h.onPointerup],
|
|
233
|
+
["pointercancel", h.onPointercancel],
|
|
234
|
+
["lostpointercapture", h.onLostpointercapture],
|
|
235
|
+
["click", h.onClickCapture, { capture: true }],
|
|
236
|
+
["dragstart", h.onDragstart],
|
|
237
|
+
["contextmenu", h.onContextmenu]
|
|
238
|
+
];
|
|
239
|
+
watch(
|
|
240
|
+
() => toValue(target),
|
|
241
|
+
(el, _, onCleanup) => {
|
|
242
|
+
if (!el) return;
|
|
243
|
+
for (const [name, fn, opts] of bindings) el.addEventListener(name, fn, opts);
|
|
244
|
+
onCleanup(() => {
|
|
245
|
+
for (const [name, fn, opts] of bindings) el.removeEventListener(name, fn, opts);
|
|
246
|
+
gesture.cancel();
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
{ immediate: true, flush: "post" }
|
|
250
|
+
);
|
|
251
|
+
onBeforeUnmount(gesture.dispose);
|
|
252
|
+
return gesture;
|
|
253
|
+
}
|
|
254
|
+
function glide(velocity, step, { tau = 240, win = window } = {}) {
|
|
255
|
+
let v = { ...velocity };
|
|
256
|
+
let frame = 0;
|
|
257
|
+
const stop = () => {
|
|
258
|
+
if (frame) win.cancelAnimationFrame(frame);
|
|
259
|
+
frame = 0;
|
|
260
|
+
};
|
|
261
|
+
if (prefersReducedMotion(win) || Math.hypot(v.x, v.y) < 0.05) return { stop };
|
|
262
|
+
let previous = win.performance.now();
|
|
263
|
+
const tick = (now) => {
|
|
264
|
+
const dt = Math.min(now - previous, 32);
|
|
265
|
+
previous = now;
|
|
266
|
+
const friction = Math.exp(-dt / tau);
|
|
267
|
+
v = { x: v.x * friction, y: v.y * friction };
|
|
268
|
+
if (step(v.x * dt, v.y * dt) === false || Math.hypot(v.x, v.y) <= 0.015) {
|
|
269
|
+
frame = 0;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
frame = win.requestAnimationFrame(tick);
|
|
273
|
+
};
|
|
274
|
+
frame = win.requestAnimationFrame(tick);
|
|
275
|
+
return { stop };
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
createGesture,
|
|
279
|
+
glide,
|
|
280
|
+
useGesture
|
|
281
|
+
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { TOUCH_SLOP, HOLD_TO_DRAG_MS } from "./gesture.js";
|
|
1
2
|
import { ref, toValue, onMounted, onBeforeUnmount, readonly } from "vue";
|
|
2
3
|
import { useHotkeys } from "./useHotkeys.js";
|
|
3
4
|
import { scrollParent } from "./scroll.js";
|
|
4
5
|
import { nodeWindow } from "./realm.js";
|
|
5
|
-
const HOLD_MS = 320;
|
|
6
|
-
const SLOP_PX = 8;
|
|
7
6
|
const EDGE_PX = 40;
|
|
8
7
|
const SCROLL_STEP = 14;
|
|
9
8
|
function usePaintSelect(options) {
|
|
@@ -90,12 +89,12 @@ function usePaintSelect(options) {
|
|
|
90
89
|
pending = null;
|
|
91
90
|
begin(key, x, y);
|
|
92
91
|
navigator.vibrate?.(8);
|
|
93
|
-
}, behaviour.hold ??
|
|
92
|
+
}, behaviour.hold ?? HOLD_TO_DRAG_MS)
|
|
94
93
|
};
|
|
95
94
|
}
|
|
96
95
|
function onMove(event) {
|
|
97
96
|
if (pending && event.pointerId === pending.pointerId) {
|
|
98
|
-
if (Math.hypot(event.clientX - pending.x, event.clientY - pending.y) >
|
|
97
|
+
if (Math.hypot(event.clientX - pending.x, event.clientY - pending.y) > TOUCH_SLOP) cancelPending();
|
|
99
98
|
return;
|
|
100
99
|
}
|
|
101
100
|
if (!stroke) return;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
|
+
export interface ScrollStripOptions {
|
|
3
|
+
/** Map a vertical wheel to sideways scrolling. Default true. */
|
|
4
|
+
wheel?: MaybeRefOrGetter<boolean | undefined>;
|
|
5
|
+
/** Drag to scroll with a mouse or pen. Default true. */
|
|
6
|
+
drag?: MaybeRefOrGetter<boolean | undefined>;
|
|
7
|
+
/** Room kept beside a focused item, so it clears the fade. Default 24. */
|
|
8
|
+
focusMargin?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ScrollStrip {
|
|
11
|
+
overflowing: Ref<boolean>;
|
|
12
|
+
atStart: Ref<boolean>;
|
|
13
|
+
atEnd: Ref<boolean>;
|
|
14
|
+
dragging: Ref<boolean>;
|
|
15
|
+
/** Scroll most of a view's width toward the start (-1) or the end (1). */
|
|
16
|
+
page: (direction: -1 | 1) => void;
|
|
17
|
+
/** Scroll by `px`, eased unless reduced motion asks otherwise. */
|
|
18
|
+
scrollBy: (px: number) => void;
|
|
19
|
+
/** Bring `el` into view within the strip only (`center` puts it in the middle). */
|
|
20
|
+
reveal: (el: Element | null | undefined, options?: {
|
|
21
|
+
smooth?: boolean;
|
|
22
|
+
block?: "nearest" | "center";
|
|
23
|
+
}) => void;
|
|
24
|
+
/** Measure again (after content changed in a way nothing observed). */
|
|
25
|
+
measure: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function useScrollStrip(target: MaybeRefOrGetter<HTMLElement | null | undefined>, options?: ScrollStripOptions): ScrollStrip;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { ref, watch, onBeforeUnmount, toValue } from "vue";
|
|
2
|
+
import { prefersReducedMotion } from "./easing.js";
|
|
3
|
+
import { wheelPixels, momentumDistance } from "./gesture.js";
|
|
4
|
+
import { nodeWindow, isHTMLElement } from "./realm.js";
|
|
5
|
+
import { scrollToElement } from "./scroll.js";
|
|
6
|
+
import { useGesture } from "./useGesture.js";
|
|
7
|
+
const PAGE_SHARE = 0.8;
|
|
8
|
+
function useScrollStrip(target, options = {}) {
|
|
9
|
+
const overflowing = ref(false);
|
|
10
|
+
const atStart = ref(true);
|
|
11
|
+
const atEnd = ref(true);
|
|
12
|
+
const el = () => {
|
|
13
|
+
const box = toValue(target);
|
|
14
|
+
return isHTMLElement(box) ? box : null;
|
|
15
|
+
};
|
|
16
|
+
function measure() {
|
|
17
|
+
const box = el();
|
|
18
|
+
if (!box) return;
|
|
19
|
+
const max = box.scrollWidth - box.clientWidth;
|
|
20
|
+
const left = Math.abs(box.scrollLeft);
|
|
21
|
+
overflowing.value = max > 1;
|
|
22
|
+
atStart.value = left <= 1;
|
|
23
|
+
atEnd.value = left >= max - 1;
|
|
24
|
+
}
|
|
25
|
+
const maxScroll = (box) => Math.max(0, box.scrollWidth - box.clientWidth);
|
|
26
|
+
const snaps = (box) => {
|
|
27
|
+
const type = nodeWindow(box).getComputedStyle(box).scrollSnapType;
|
|
28
|
+
return !!type && type !== "none";
|
|
29
|
+
};
|
|
30
|
+
let frame = 0;
|
|
31
|
+
let goal = null;
|
|
32
|
+
let restoreBehavior = null;
|
|
33
|
+
function stopEase() {
|
|
34
|
+
const box = el();
|
|
35
|
+
if (frame && box) nodeWindow(box).cancelAnimationFrame(frame);
|
|
36
|
+
frame = 0;
|
|
37
|
+
goal = null;
|
|
38
|
+
restoreBehavior?.();
|
|
39
|
+
restoreBehavior = null;
|
|
40
|
+
}
|
|
41
|
+
function easeTo(box, to) {
|
|
42
|
+
const win = nodeWindow(box);
|
|
43
|
+
goal = Math.max(0, Math.min(maxScroll(box), to));
|
|
44
|
+
if (prefersReducedMotion(win)) {
|
|
45
|
+
box.scrollLeft = goal;
|
|
46
|
+
goal = null;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (frame) return;
|
|
50
|
+
let previous = win.performance.now();
|
|
51
|
+
let position = box.scrollLeft;
|
|
52
|
+
const behavior = box.style.scrollBehavior;
|
|
53
|
+
box.style.scrollBehavior = "auto";
|
|
54
|
+
restoreBehavior = () => box.style.scrollBehavior = behavior;
|
|
55
|
+
const done = () => {
|
|
56
|
+
frame = 0;
|
|
57
|
+
goal = null;
|
|
58
|
+
restoreBehavior?.();
|
|
59
|
+
restoreBehavior = null;
|
|
60
|
+
};
|
|
61
|
+
const tick = (now) => {
|
|
62
|
+
if (goal === null) {
|
|
63
|
+
done();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const dt = Math.min(now - previous, 32);
|
|
67
|
+
previous = now;
|
|
68
|
+
position += (goal - position) * (1 - Math.exp(-dt / 70));
|
|
69
|
+
if (Math.abs(goal - position) < 0.5) position = goal;
|
|
70
|
+
box.scrollLeft = position;
|
|
71
|
+
if (position === goal) {
|
|
72
|
+
done();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
frame = win.requestAnimationFrame(tick);
|
|
76
|
+
};
|
|
77
|
+
frame = win.requestAnimationFrame(tick);
|
|
78
|
+
}
|
|
79
|
+
function scrollBy(px) {
|
|
80
|
+
const box = el();
|
|
81
|
+
if (!box) return;
|
|
82
|
+
if (snaps(box)) {
|
|
83
|
+
box.scrollBy({ left: px, behavior: prefersReducedMotion(nodeWindow(box)) ? "auto" : "smooth" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
easeTo(box, (goal ?? box.scrollLeft) + px);
|
|
87
|
+
}
|
|
88
|
+
function page(direction) {
|
|
89
|
+
const box = el();
|
|
90
|
+
if (box) scrollBy(direction * box.clientWidth * PAGE_SHARE);
|
|
91
|
+
}
|
|
92
|
+
function reveal(item, { smooth = true, block = "nearest" } = {}) {
|
|
93
|
+
const box = el();
|
|
94
|
+
if (!box || !item || !box.contains(item)) return;
|
|
95
|
+
stopEase();
|
|
96
|
+
const behavior = smooth && !prefersReducedMotion(nodeWindow(box)) ? "smooth" : "auto";
|
|
97
|
+
scrollToElement(item, { axis: "x", container: box, margin: options.focusMargin ?? 24, behavior, block });
|
|
98
|
+
}
|
|
99
|
+
function onWheel(e) {
|
|
100
|
+
const box = el();
|
|
101
|
+
if (!box || toValue(options.wheel) === false || e.ctrlKey || e.defaultPrevented) return;
|
|
102
|
+
if (Math.abs(e.deltaX) >= Math.abs(e.deltaY)) return;
|
|
103
|
+
const max = maxScroll(box);
|
|
104
|
+
if (max <= 1) return;
|
|
105
|
+
const px = wheelPixels(e.shiftKey ? e.deltaX || e.deltaY : e.deltaY, e.deltaMode, box.clientWidth);
|
|
106
|
+
const from = goal ?? box.scrollLeft;
|
|
107
|
+
if (px > 0 && from >= max - 1 || px < 0 && from <= 1) return;
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
const notch = e.deltaMode !== 0 || Math.abs(px) >= 50;
|
|
110
|
+
if (notch) scrollBy(px);
|
|
111
|
+
else {
|
|
112
|
+
stopEase();
|
|
113
|
+
box.scrollLeft += px;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function onFocusin(e) {
|
|
117
|
+
const item = e.target;
|
|
118
|
+
if (!isHTMLElement(item) || gesture.dragging.value) return;
|
|
119
|
+
try {
|
|
120
|
+
if (!item.matches(":focus-visible")) return;
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
reveal(item);
|
|
124
|
+
}
|
|
125
|
+
let snapType = null;
|
|
126
|
+
const gesture = useGesture(el, {
|
|
127
|
+
pointerTypes: ["mouse", "pen"],
|
|
128
|
+
enabled: () => toValue(options.drag) !== false && overflowing.value,
|
|
129
|
+
onDragStart() {
|
|
130
|
+
const box = el();
|
|
131
|
+
if (!box) return;
|
|
132
|
+
stopEase();
|
|
133
|
+
if (snaps(box)) {
|
|
134
|
+
snapType = box.style.scrollSnapType;
|
|
135
|
+
box.style.scrollSnapType = "none";
|
|
136
|
+
}
|
|
137
|
+
box.style.scrollBehavior = "auto";
|
|
138
|
+
},
|
|
139
|
+
onDrag({ delta }) {
|
|
140
|
+
const box = el();
|
|
141
|
+
if (box) box.scrollLeft -= delta.x;
|
|
142
|
+
},
|
|
143
|
+
onDragEnd({ velocity, cancelled }) {
|
|
144
|
+
const box = el();
|
|
145
|
+
if (!box) return;
|
|
146
|
+
box.style.scrollBehavior = "";
|
|
147
|
+
const travel = cancelled ? 0 : -momentumDistance(velocity.x);
|
|
148
|
+
if (snapType !== null) {
|
|
149
|
+
box.style.scrollSnapType = snapType;
|
|
150
|
+
snapType = null;
|
|
151
|
+
box.scrollBy({ left: travel, behavior: prefersReducedMotion(nodeWindow(box)) ? "auto" : "smooth" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (Math.abs(travel) > 4) easeTo(box, box.scrollLeft + travel);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
watch(
|
|
158
|
+
() => el(),
|
|
159
|
+
(box, _, onCleanup) => {
|
|
160
|
+
if (!box) return;
|
|
161
|
+
const win = nodeWindow(box);
|
|
162
|
+
const resize = new win.ResizeObserver(measure);
|
|
163
|
+
const observeAll = () => {
|
|
164
|
+
resize.disconnect();
|
|
165
|
+
resize.observe(box);
|
|
166
|
+
for (const child of Array.from(box.children)) resize.observe(child);
|
|
167
|
+
};
|
|
168
|
+
const mutations = new win.MutationObserver(() => {
|
|
169
|
+
observeAll();
|
|
170
|
+
measure();
|
|
171
|
+
});
|
|
172
|
+
observeAll();
|
|
173
|
+
mutations.observe(box, { childList: true });
|
|
174
|
+
box.addEventListener("scroll", measure, { passive: true });
|
|
175
|
+
box.addEventListener("wheel", onWheel, { passive: false });
|
|
176
|
+
box.addEventListener("focusin", onFocusin);
|
|
177
|
+
measure();
|
|
178
|
+
onCleanup(() => {
|
|
179
|
+
resize.disconnect();
|
|
180
|
+
mutations.disconnect();
|
|
181
|
+
box.removeEventListener("scroll", measure);
|
|
182
|
+
box.removeEventListener("wheel", onWheel);
|
|
183
|
+
box.removeEventListener("focusin", onFocusin);
|
|
184
|
+
stopEase();
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
{ immediate: true, flush: "post" }
|
|
188
|
+
);
|
|
189
|
+
onBeforeUnmount(stopEase);
|
|
190
|
+
return { overflowing, atStart, atEnd, dragging: gesture.dragging, page, scrollBy, reveal, measure };
|
|
191
|
+
}
|
|
192
|
+
export {
|
|
193
|
+
useScrollStrip
|
|
194
|
+
};
|
|
@@ -12,8 +12,10 @@ export declare function useTouchHold(options: TouchHoldOptions): {
|
|
|
12
12
|
onPointerdown: (event: PointerEvent) => void;
|
|
13
13
|
onPointermove: (event: PointerEvent) => void;
|
|
14
14
|
onPointerup: (event: PointerEvent) => void;
|
|
15
|
-
onPointercancel: (event
|
|
15
|
+
onPointercancel: (event: PointerEvent) => void;
|
|
16
|
+
onLostpointercapture: (event: PointerEvent) => void;
|
|
16
17
|
onClickCapture: (event: MouseEvent) => void;
|
|
18
|
+
onDragstart: (event: DragEvent) => void;
|
|
17
19
|
onContextmenu: (event: Event) => void;
|
|
18
20
|
};
|
|
19
21
|
};
|
|
@@ -1,58 +1,16 @@
|
|
|
1
1
|
import { onBeforeUnmount } from "vue";
|
|
2
|
-
|
|
3
|
-
const CLICK_AFTER_RELEASE_MS = 350;
|
|
2
|
+
import { createGesture } from "./useGesture.js";
|
|
4
3
|
function useTouchHold(options) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
timer = 0;
|
|
12
|
-
}
|
|
13
|
-
function onPointerDown(event) {
|
|
14
|
-
if (timer && event.pointerId !== finger) return;
|
|
15
|
-
held = false;
|
|
16
|
-
if (event.pointerType !== "touch" || !options.enabled()) return;
|
|
17
|
-
finger = event.pointerId;
|
|
18
|
-
start = { x: event.clientX, y: event.clientY };
|
|
19
|
-
stop();
|
|
20
|
-
timer = window.setTimeout(() => {
|
|
21
|
-
timer = 0;
|
|
22
|
-
held = true;
|
|
23
|
-
options.onHold();
|
|
24
|
-
}, options.delay());
|
|
25
|
-
}
|
|
26
|
-
function onPointerMove(event) {
|
|
27
|
-
if (!timer || event.pointerId !== finger) return;
|
|
28
|
-
if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > SLOP) stop();
|
|
29
|
-
}
|
|
30
|
-
function onPointerUp(event) {
|
|
31
|
-
if (finger !== null && event.pointerId !== finger) return;
|
|
32
|
-
finger = null;
|
|
33
|
-
stop();
|
|
34
|
-
if (held) window.setTimeout(() => held = false, CLICK_AFTER_RELEASE_MS);
|
|
35
|
-
}
|
|
36
|
-
function onPointerCancel(event) {
|
|
37
|
-
if (event && finger !== null && event.pointerId !== finger) return;
|
|
38
|
-
finger = null;
|
|
39
|
-
stop();
|
|
40
|
-
held = false;
|
|
41
|
-
}
|
|
42
|
-
function onClickCapture(event) {
|
|
43
|
-
if (!held) return;
|
|
44
|
-
held = false;
|
|
45
|
-
event.preventDefault();
|
|
46
|
-
event.stopImmediatePropagation();
|
|
47
|
-
}
|
|
48
|
-
function onContextMenu(event) {
|
|
49
|
-
if (timer || held) event.preventDefault();
|
|
50
|
-
}
|
|
51
|
-
onBeforeUnmount(stop);
|
|
4
|
+
const gesture = createGesture({
|
|
5
|
+
enabled: options.enabled,
|
|
6
|
+
pointerTypes: ["touch"],
|
|
7
|
+
longPress: { delay: options.delay, handler: () => options.onHold() }
|
|
8
|
+
});
|
|
9
|
+
onBeforeUnmount(gesture.dispose);
|
|
52
10
|
return {
|
|
53
11
|
/** Drops a hold in progress, as if the finger had been lifted off the screen. */
|
|
54
|
-
cancel:
|
|
55
|
-
handlers:
|
|
12
|
+
cancel: gesture.cancel,
|
|
13
|
+
handlers: gesture.handlers
|
|
56
14
|
};
|
|
57
15
|
}
|
|
58
16
|
export {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clamp } from "./clamp.js";
|
|
2
|
+
import { pinchFrame, distance } from "./gesture.js";
|
|
2
3
|
import { ref, computed, toValue } from "vue";
|
|
3
4
|
function useZoomPan(options = {}) {
|
|
4
5
|
const min = options.min ?? 1;
|
|
@@ -49,9 +50,6 @@ function useZoomPan(options = {}) {
|
|
|
49
50
|
const point = relative(e);
|
|
50
51
|
zoomAbout(scale.value * (e.deltaY < 0 ? step : 1 / step), point.x, point.y);
|
|
51
52
|
}
|
|
52
|
-
function distance(a, b) {
|
|
53
|
-
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
54
|
-
}
|
|
55
53
|
function onPointerDown(e) {
|
|
56
54
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
57
55
|
if (pointers.size === 2) {
|
|
@@ -72,9 +70,9 @@ function useZoomPan(options = {}) {
|
|
|
72
70
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
73
71
|
if (pointers.size === 2 && pinchStartDistance > 0) {
|
|
74
72
|
const [a, b] = [...pointers.values()];
|
|
75
|
-
const
|
|
76
|
-
const point = relative(
|
|
77
|
-
zoomAbout(pinchStartScale *
|
|
73
|
+
const pinch = pinchFrame(a, b, pinchStartDistance);
|
|
74
|
+
const point = relative({ clientX: pinch.origin.x, clientY: pinch.origin.y });
|
|
75
|
+
zoomAbout(pinchStartScale * pinch.scale, point.x, point.y);
|
|
78
76
|
return;
|
|
79
77
|
}
|
|
80
78
|
if (!panning.value) return;
|
package/dist/index.d.ts
CHANGED
|
@@ -147,6 +147,7 @@ export { default as LpRadioGroup } from "./components/LpRadioGroup.vue";
|
|
|
147
147
|
export type { RadioOption } from "./components/LpRadioGroup.vue";
|
|
148
148
|
export { default as LpRollingText } from "./components/LpRollingText.vue";
|
|
149
149
|
export { default as LpScrollArea } from "./components/LpScrollArea.vue";
|
|
150
|
+
export { default as LpScrollStrip } from "./components/LpScrollStrip.vue";
|
|
150
151
|
export { default as LpPageScrollbar } from "./components/LpPageScrollbar.vue";
|
|
151
152
|
export { default as LpSegmented } from "./components/LpSegmented.vue";
|
|
152
153
|
export type { SegmentedOption } from "./components/LpSegmented.vue";
|
|
@@ -333,6 +334,12 @@ export { useTilt } from "./composables/useTilt";
|
|
|
333
334
|
export type { UseTilt, UseTiltOptions } from "./composables/useTilt";
|
|
334
335
|
export { useZoomPan } from "./composables/useZoomPan";
|
|
335
336
|
export type { UseZoomPan, UseZoomPanOptions } from "./composables/useZoomPan";
|
|
337
|
+
export { createGesture, glide, useGesture } from "./composables/useGesture";
|
|
338
|
+
export type { Gesture, GestureEnd, GestureOptions, GesturePinch, GestureState, Glide } from "./composables/useGesture";
|
|
339
|
+
export { createDragSession, createVelocityTracker, FLICK_VELOCITY, HOLD_TO_DRAG_MS, LONG_PRESS_MS, lockAxis, MOUSE_SLOP, momentumDistance, pageAfterSwipe, pinchFrame, rubberBand, rubberClamp, shouldDismiss, slopFor, swipeDirection, TOUCH_SLOP, wheelPixels, } from "./composables/gesture";
|
|
340
|
+
export type { DismissOptions, DragFrame, DragSession, DragSessionOptions, GestureAxis, GesturePoint, PageSwipeOptions, PinchFrame, SwipeOptions, VelocityTracker } from "./composables/gesture";
|
|
341
|
+
export { useScrollStrip } from "./composables/useScrollStrip";
|
|
342
|
+
export type { ScrollStrip, ScrollStripOptions } from "./composables/useScrollStrip";
|
|
336
343
|
export { useToast } from "./composables/useToast";
|
|
337
344
|
export type { ToastAction, ToastItem, ToastMessage, ToastOptions, ToastVariant, } from "./composables/useToast";
|
|
338
345
|
export type { ToastPromiseMessages } from "./composables/useToast";
|
|
@@ -379,6 +386,8 @@ export type { PipelineStep, StepStatus } from "./components/pipeline";
|
|
|
379
386
|
export { default as LpLoader } from "./components/LpLoader.vue";
|
|
380
387
|
export type { LoaderLabels, LoaderSize, LoaderTone, LoaderVariant } from "./components/LpLoader.vue";
|
|
381
388
|
export { useLoadingGate } from "./composables/useLoadingGate";
|
|
389
|
+
export { useAppUpdate } from "./composables/useAppUpdate";
|
|
390
|
+
export type { AppUpdate, AppUpdateOptions } from "./composables/useAppUpdate";
|
|
382
391
|
export type { LoadingGateOptions } from "./composables/useLoadingGate";
|
|
383
392
|
export { useNow } from "./composables/useNow";
|
|
384
393
|
export type { UseNowOptions } from "./composables/useNow";
|