@leavepulse/ui 0.93.2 → 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.
Files changed (78) hide show
  1. package/README.md +53 -3
  2. package/dist/assets/LpTabs-D7EyG21l.css +18 -0
  3. package/dist/assets/LpVideoTrim-D8JDg9Qn.css +8 -0
  4. package/dist/chunks/AppShellContextNav-BoH4ffnC.js +54 -0
  5. package/dist/chunks/{LpAppShell-BWaVRsDB.js → LpAppShell-xEBmGV78.js} +12 -5
  6. package/dist/chunks/{LpDateRangePicker-CAn2yGbn.js → LpDateRangePicker-DRBB8z6t.js} +1 -1
  7. package/dist/chunks/{LpDrawer-iRHwJOGn.js → LpDrawer-BkWjFdV0.js} +81 -42
  8. package/dist/chunks/{LpLightbox-a24V_0Wk.js → LpLightbox-DPgz7IsT.js} +62 -49
  9. package/dist/chunks/{LpMapPicker-DVfiy3rR.js → LpMapPicker-V0Lybi10.js} +19 -52
  10. package/dist/chunks/{LpMediaGallery-B7DOV_aL.js → LpMediaGallery-CTejKF2-.js} +36 -15
  11. package/dist/chunks/{LpModalNav-DivqT1NV.js → LpModalNav-DVXAF-DG.js} +1 -1
  12. package/dist/chunks/{LpRangeCalendar-C9gkNMYw.js → LpRangeCalendar-9BvaLZji.js} +3 -4
  13. package/dist/chunks/LpScrollStrip-D4tsOfAQ.js +78 -0
  14. package/dist/chunks/{LpSidebar-DQtCYlT4.js → LpSidebar-BIhusppV.js} +1 -1
  15. package/dist/component-names.d.ts +1 -1
  16. package/dist/components/AppShellContextNav.vue.js +1 -1
  17. package/dist/components/LpAppShell.vue.js +1 -1
  18. package/dist/components/LpChart.vue.js +3 -3
  19. package/dist/components/LpDateRangePicker.vue.js +1 -1
  20. package/dist/components/LpDrawer.vue.d.ts +10 -2
  21. package/dist/components/LpDrawer.vue.js +1 -1
  22. package/dist/components/LpDropzone.vue.d.ts +1 -1
  23. package/dist/components/LpLightbox.vue.js +1 -1
  24. package/dist/components/LpMapPicker.vue.js +1 -1
  25. package/dist/components/LpMediaGallery.vue.d.ts +2 -0
  26. package/dist/components/LpMediaGallery.vue.js +1 -1
  27. package/dist/components/LpModalNav.vue.js +1 -1
  28. package/dist/components/LpRangeCalendar.vue.js +1 -1
  29. package/dist/components/LpRevealOnScroll.vue.d.ts +1 -1
  30. package/dist/components/LpScrollStrip.vue.d.ts +48 -0
  31. package/dist/components/LpScrollStrip.vue.js +4 -0
  32. package/dist/components/LpSidebar.vue.js +1 -1
  33. package/dist/components/LpTabs.vue.d.ts +4 -4
  34. package/dist/components/LpTabs.vue.js +104 -90
  35. package/dist/components/LpVideoTrim.vue.js +41 -54
  36. package/dist/composables/gesture.d.ts +141 -0
  37. package/dist/composables/gesture.js +176 -0
  38. package/dist/composables/scroll.d.ts +5 -1
  39. package/dist/composables/scroll.js +16 -9
  40. package/dist/composables/useAppUpdate.d.ts +26 -0
  41. package/dist/composables/useAppUpdate.js +91 -0
  42. package/dist/composables/useGesture.d.ts +107 -0
  43. package/dist/composables/useGesture.js +281 -0
  44. package/dist/composables/usePaintSelect.js +3 -4
  45. package/dist/composables/useScrollStrip.d.ts +27 -0
  46. package/dist/composables/useScrollStrip.js +194 -0
  47. package/dist/composables/useTouchHold.d.ts +3 -1
  48. package/dist/composables/useTouchHold.js +9 -51
  49. package/dist/composables/useZoomPan.js +4 -6
  50. package/dist/index.d.ts +9 -0
  51. package/dist/index.js +180 -152
  52. package/package.json +1 -1
  53. package/src/component-names.ts +1 -0
  54. package/src/components/AppShellContextNav.vue +3 -2
  55. package/src/components/LpAppShell.vue +10 -1
  56. package/src/components/LpChart.vue +2 -2
  57. package/src/components/LpDrawer.vue +121 -54
  58. package/src/components/LpLightbox.vue +33 -20
  59. package/src/components/LpMapPicker.vue +22 -64
  60. package/src/components/LpMediaGallery.vue +44 -11
  61. package/src/components/LpRangeCalendar.vue +4 -5
  62. package/src/components/LpScrollStrip.vue +96 -0
  63. package/src/components/LpSidebar.vue +2 -2
  64. package/src/components/LpTabs.vue +36 -17
  65. package/src/components/LpVideoTrim.vue +35 -46
  66. package/src/composables/gesture.ts +314 -0
  67. package/src/composables/scroll.ts +20 -9
  68. package/src/composables/useAppUpdate.ts +156 -0
  69. package/src/composables/useGesture.ts +451 -0
  70. package/src/composables/usePaintSelect.ts +4 -5
  71. package/src/composables/useScrollStrip.ts +249 -0
  72. package/src/composables/useTouchHold.ts +15 -73
  73. package/src/composables/useZoomPan.ts +6 -9
  74. package/src/index.ts +27 -0
  75. package/src/tokens/effects.css +40 -0
  76. package/dist/assets/LpTabs-Bh7a-Bdl.css +0 -18
  77. package/dist/assets/LpVideoTrim-CMoUw3dJ.css +0 -8
  78. 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 ?? HOLD_MS)
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) > SLOP_PX) cancelPending();
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?: PointerEvent) => void;
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
- const SLOP = 8;
3
- const CLICK_AFTER_RELEASE_MS = 350;
2
+ import { createGesture } from "./useGesture.js";
4
3
  function useTouchHold(options) {
5
- let timer = 0;
6
- let finger = null;
7
- let start = { x: 0, y: 0 };
8
- let held = false;
9
- function stop() {
10
- window.clearTimeout(timer);
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: () => onPointerCancel(),
55
- handlers: { onPointerdown: onPointerDown, onPointermove: onPointerMove, onPointerup: onPointerUp, onPointercancel: onPointerCancel, onClickCapture, onContextmenu: onContextMenu }
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 mid = { clientX: (a.x + b.x) / 2, clientY: (a.y + b.y) / 2 };
76
- const point = relative(mid);
77
- zoomAbout(pinchStartScale * (distance(a, b) / pinchStartDistance), point.x, point.y);
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";