@leavepulse/ui 0.93.3 → 0.94.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -3
- package/dist/assets/LpTabs-7yQjX2ih.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-DlpQierx.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-BFjiHWbr.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-BKhVDsMt.js} +3 -3
- package/dist/chunks/{LpSidebarNav-CFcI7--7.js → LpSidebarNav-DbCap8z1.js} +33 -19
- 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/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +4 -4
- package/dist/components/LpTabs.vue.js +111 -92
- 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 +3 -3
- package/src/components/LpSidebarNav.vue +18 -8
- package/src/components/LpTabs.vue +47 -19
- 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,141 @@
|
|
|
1
|
+
import type { Direction } from "./spatialNav";
|
|
2
|
+
export interface GesturePoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
export type GestureAxis = "x" | "y";
|
|
7
|
+
/** How far a touch may drift before a press stops being a tap or a hold. */
|
|
8
|
+
export declare const TOUCH_SLOP = 8;
|
|
9
|
+
/** A mouse is steadier: a press that moves this far is a drag. */
|
|
10
|
+
export declare const MOUSE_SLOP = 4;
|
|
11
|
+
/** Px/ms at release past which a short swipe still counts (a flick). */
|
|
12
|
+
export declare const FLICK_VELOCITY = 0.45;
|
|
13
|
+
/** How long a finger holds still before it drags (selects, paints) rather than scrolls the page. */
|
|
14
|
+
export declare const HOLD_TO_DRAG_MS = 320;
|
|
15
|
+
/** How long a press must stay for a long press, in ms. */
|
|
16
|
+
export declare const LONG_PRESS_MS = 500;
|
|
17
|
+
/** The slop for a pointer type: a finger gets more room than a mouse. */
|
|
18
|
+
export declare function slopFor(pointerType: string): number;
|
|
19
|
+
export interface VelocityTracker {
|
|
20
|
+
/** Record where the pointer is at `time` (ms, an event's timeStamp). */
|
|
21
|
+
add: (point: GesturePoint, time: number) => void;
|
|
22
|
+
/** Px/ms over the last moments, zero when the pointer rested before `now`. */
|
|
23
|
+
velocity: (now?: number) => GesturePoint;
|
|
24
|
+
reset: () => void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Velocity from the samples in the last VELOCITY_WINDOW_MS rather than from
|
|
28
|
+
* the last two events: a jittery final frame (two events a millisecond apart)
|
|
29
|
+
* would otherwise decide how far a flick travels.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createVelocityTracker(): VelocityTracker;
|
|
32
|
+
/**
|
|
33
|
+
* Which axis a movement has committed to, or null while it is still inside
|
|
34
|
+
* the slop or too diagonal to call. `ratio` is how much the winning axis must
|
|
35
|
+
* lead by: 1 takes any lead, higher waits for a clearer one.
|
|
36
|
+
*/
|
|
37
|
+
export declare function lockAxis(movement: GesturePoint, slop: number, ratio?: number): GestureAxis | null;
|
|
38
|
+
export interface SwipeOptions {
|
|
39
|
+
/** Distance in px that is a swipe at any speed. Default 60. */
|
|
40
|
+
distance?: number;
|
|
41
|
+
/** Px/ms that turns a shorter move into a swipe. Default FLICK_VELOCITY. */
|
|
42
|
+
velocity?: number;
|
|
43
|
+
/** A flick must still travel this far, so a twitch is not one. Default 16. */
|
|
44
|
+
minDistance?: number;
|
|
45
|
+
/** Only these axes count. Default both. */
|
|
46
|
+
axis?: GestureAxis;
|
|
47
|
+
/** How much the swipe's axis must lead the other. Default 1.5. */
|
|
48
|
+
ratio?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Which way a released movement swiped, or null when it was not a swipe. */
|
|
51
|
+
export declare function swipeDirection(movement: GesturePoint, velocity: GesturePoint, options?: SwipeOptions): Direction | null;
|
|
52
|
+
/**
|
|
53
|
+
* How far an edge moves when pulled `overshoot` px past it: the further the
|
|
54
|
+
* pull, the less each pixel gives, approaching `size` and never passing it.
|
|
55
|
+
* The sign follows the overshoot. `c` is the stiffness, iOS uses 0.55.
|
|
56
|
+
*/
|
|
57
|
+
export declare function rubberBand(overshoot: number, size: number, c?: number): number;
|
|
58
|
+
/** Clamp `value` into [min, max], letting it stretch past either end by rubberBand. */
|
|
59
|
+
export declare function rubberClamp(value: number, min: number, max: number, size: number, c?: number): number;
|
|
60
|
+
export interface DismissOptions {
|
|
61
|
+
/** Offset toward the dismissing side, in px (positive is toward it). */
|
|
62
|
+
offset: number;
|
|
63
|
+
/** Velocity toward the dismissing side, px/ms. */
|
|
64
|
+
velocity: number;
|
|
65
|
+
/** The size of what is being swiped away along the swipe, in px. */
|
|
66
|
+
size: number;
|
|
67
|
+
/** Fraction of `size` past which a slow release dismisses. Default 0.5. */
|
|
68
|
+
ratio?: number;
|
|
69
|
+
/** Px/ms past which a release dismisses from any distance. Default FLICK_VELOCITY. */
|
|
70
|
+
flick?: number;
|
|
71
|
+
}
|
|
72
|
+
/** Swipe to dismiss: whether a release should carry it away or let it spring back. */
|
|
73
|
+
export declare function shouldDismiss({ offset, velocity, size, ratio, flick }: DismissOptions): boolean;
|
|
74
|
+
export interface PageSwipeOptions {
|
|
75
|
+
index: number;
|
|
76
|
+
count: number;
|
|
77
|
+
/** Drag offset along the strip in px, positive toward the previous item. */
|
|
78
|
+
offset: number;
|
|
79
|
+
/** Velocity in px/ms, same sign as `offset`. */
|
|
80
|
+
velocity: number;
|
|
81
|
+
/** Width of one item in px. */
|
|
82
|
+
size: number;
|
|
83
|
+
/** Fraction of `size` that pages at any speed. Default 0.25. */
|
|
84
|
+
ratio?: number;
|
|
85
|
+
flick?: number;
|
|
86
|
+
/** Wrap past the ends instead of stopping. */
|
|
87
|
+
loop?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/** Swipe between items: the index a release lands on, one step at most. */
|
|
90
|
+
export declare function pageAfterSwipe({ index, count, offset, velocity, size, ratio, flick, loop }: PageSwipeOptions): number;
|
|
91
|
+
/** Where a fling at `velocity` px/ms comes to rest with exponential friction of time constant `tau` ms. */
|
|
92
|
+
export declare function momentumDistance(velocity: number, tau?: number): number;
|
|
93
|
+
export interface PinchFrame {
|
|
94
|
+
/** Distance between the two pointers now, over the distance at the start. */
|
|
95
|
+
scale: number;
|
|
96
|
+
/** Midpoint of the two pointers. */
|
|
97
|
+
origin: GesturePoint;
|
|
98
|
+
distance: number;
|
|
99
|
+
}
|
|
100
|
+
export declare function distance(a: GesturePoint, b: GesturePoint): number;
|
|
101
|
+
export declare function midpoint(a: GesturePoint, b: GesturePoint): GesturePoint;
|
|
102
|
+
/** The pinch between two pointers, measured against the distance it started at. */
|
|
103
|
+
export declare function pinchFrame(a: GesturePoint, b: GesturePoint, startDistance: number): PinchFrame;
|
|
104
|
+
/** The wheel's delta in pixels, whatever unit the device reported it in. */
|
|
105
|
+
export declare function wheelPixels(delta: number, mode: number, page: number): number;
|
|
106
|
+
export interface DragSessionOptions {
|
|
107
|
+
/** Px of travel before a press becomes a drag. */
|
|
108
|
+
slop: number;
|
|
109
|
+
/** Only a drag along this axis counts; one along the other is rejected. `lock` takes whichever comes first. */
|
|
110
|
+
axis?: GestureAxis | "lock";
|
|
111
|
+
}
|
|
112
|
+
export interface DragFrame {
|
|
113
|
+
/** `pending` inside the slop, `drag` once committed, `rejected` when it went the other way. */
|
|
114
|
+
phase: "pending" | "drag" | "rejected";
|
|
115
|
+
/** True on the frame the drag committed. */
|
|
116
|
+
first: boolean;
|
|
117
|
+
start: GesturePoint;
|
|
118
|
+
point: GesturePoint;
|
|
119
|
+
movement: GesturePoint;
|
|
120
|
+
delta: GesturePoint;
|
|
121
|
+
velocity: GesturePoint;
|
|
122
|
+
axis: GestureAxis | null;
|
|
123
|
+
elapsed: number;
|
|
124
|
+
}
|
|
125
|
+
export interface DragSession {
|
|
126
|
+
readonly phase: DragFrame["phase"];
|
|
127
|
+
/** Feed the pointer's position; returns where the gesture stands. */
|
|
128
|
+
move: (point: GesturePoint, time: number) => DragFrame;
|
|
129
|
+
/** The final frame at release, with the swipe it made (if it was a drag). */
|
|
130
|
+
end: (point: GesturePoint, time: number, swipe?: SwipeOptions) => DragFrame & {
|
|
131
|
+
swipe: Direction | null;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* One press followed from down to up, whatever events carry it: useGesture
|
|
136
|
+
* feeds it pointer events, a pull from the screen edge feeds it touches. It
|
|
137
|
+
* decides when the press becomes a drag, along which axis, how fast it moves
|
|
138
|
+
* and whether the release was a swipe, so every gesture in the kit draws the
|
|
139
|
+
* line in the same place.
|
|
140
|
+
*/
|
|
141
|
+
export declare function createDragSession(start: GesturePoint, time: number, options: DragSessionOptions): DragSession;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { clamp } from "./clamp.js";
|
|
2
|
+
const TOUCH_SLOP = 8;
|
|
3
|
+
const MOUSE_SLOP = 4;
|
|
4
|
+
const FLICK_VELOCITY = 0.45;
|
|
5
|
+
const HOLD_TO_DRAG_MS = 320;
|
|
6
|
+
const LONG_PRESS_MS = 500;
|
|
7
|
+
const VELOCITY_WINDOW_MS = 100;
|
|
8
|
+
const REST_MS = 90;
|
|
9
|
+
function slopFor(pointerType) {
|
|
10
|
+
return pointerType === "mouse" ? MOUSE_SLOP : TOUCH_SLOP;
|
|
11
|
+
}
|
|
12
|
+
function createVelocityTracker() {
|
|
13
|
+
let samples = [];
|
|
14
|
+
return {
|
|
15
|
+
add(point, time) {
|
|
16
|
+
samples.push({ x: point.x, y: point.y, t: time });
|
|
17
|
+
const cut = time - VELOCITY_WINDOW_MS;
|
|
18
|
+
while (samples.length > 2 && samples[0].t < cut) samples.shift();
|
|
19
|
+
},
|
|
20
|
+
velocity(now) {
|
|
21
|
+
if (samples.length < 2) return { x: 0, y: 0 };
|
|
22
|
+
const last = samples[samples.length - 1];
|
|
23
|
+
if (now !== void 0 && now - last.t > REST_MS) return { x: 0, y: 0 };
|
|
24
|
+
const first = samples[0];
|
|
25
|
+
const dt = last.t - first.t;
|
|
26
|
+
if (dt <= 0) return { x: 0, y: 0 };
|
|
27
|
+
return { x: (last.x - first.x) / dt, y: (last.y - first.y) / dt };
|
|
28
|
+
},
|
|
29
|
+
reset() {
|
|
30
|
+
samples = [];
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function lockAxis(movement, slop, ratio = 1) {
|
|
35
|
+
const ax = Math.abs(movement.x);
|
|
36
|
+
const ay = Math.abs(movement.y);
|
|
37
|
+
if (Math.hypot(ax, ay) < slop) return null;
|
|
38
|
+
if (ax > ay * ratio) return "x";
|
|
39
|
+
if (ay > ax * ratio) return "y";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function swipeDirection(movement, velocity, options = {}) {
|
|
43
|
+
const { distance: distance2 = 60, velocity: flick = FLICK_VELOCITY, minDistance = 16, ratio = 1.5 } = options;
|
|
44
|
+
const ax = Math.abs(movement.x);
|
|
45
|
+
const ay = Math.abs(movement.y);
|
|
46
|
+
const axis = options.axis ?? (ax > ay * ratio ? "x" : ay > ax * ratio ? "y" : null);
|
|
47
|
+
if (!axis) return null;
|
|
48
|
+
const along = axis === "x" ? movement.x : movement.y;
|
|
49
|
+
const across = axis === "x" ? ay : ax;
|
|
50
|
+
if (Math.abs(along) <= across * ratio) return null;
|
|
51
|
+
const speed = axis === "x" ? velocity.x : velocity.y;
|
|
52
|
+
const far = Math.abs(along) >= distance2;
|
|
53
|
+
const flung = Math.abs(along) >= minDistance && Math.abs(speed) >= flick && Math.sign(speed) === Math.sign(along);
|
|
54
|
+
if (!far && !flung) return null;
|
|
55
|
+
if (axis === "x") return along < 0 ? "left" : "right";
|
|
56
|
+
return along < 0 ? "up" : "down";
|
|
57
|
+
}
|
|
58
|
+
function rubberBand(overshoot, size, c = 0.55) {
|
|
59
|
+
if (!overshoot || size <= 0) return 0;
|
|
60
|
+
const distance2 = Math.abs(overshoot);
|
|
61
|
+
return Math.sign(overshoot) * (1 - 1 / (distance2 * c / size + 1)) * size;
|
|
62
|
+
}
|
|
63
|
+
function rubberClamp(value, min, max, size, c) {
|
|
64
|
+
if (value < min) return min + rubberBand(value - min, size, c);
|
|
65
|
+
if (value > max) return max + rubberBand(value - max, size, c);
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function shouldDismiss({ offset, velocity, size, ratio = 0.5, flick = FLICK_VELOCITY }) {
|
|
69
|
+
if (offset <= 0) return false;
|
|
70
|
+
if (velocity >= flick && offset > 4) return true;
|
|
71
|
+
return offset >= size * ratio && velocity > -flick;
|
|
72
|
+
}
|
|
73
|
+
function pageAfterSwipe({ index, count, offset, velocity, size, ratio = 0.25, flick = FLICK_VELOCITY, loop = false }) {
|
|
74
|
+
if (count <= 0) return 0;
|
|
75
|
+
const far = Math.abs(offset) >= size * ratio;
|
|
76
|
+
const flung = Math.abs(velocity) >= flick && Math.sign(velocity) === Math.sign(offset) && Math.abs(offset) > 8;
|
|
77
|
+
if (!far && !flung) return index;
|
|
78
|
+
const next = index + (offset < 0 ? 1 : -1);
|
|
79
|
+
if (loop) return (next + count) % count;
|
|
80
|
+
return clamp(next, 0, count - 1);
|
|
81
|
+
}
|
|
82
|
+
function momentumDistance(velocity, tau = 240) {
|
|
83
|
+
return velocity * tau;
|
|
84
|
+
}
|
|
85
|
+
function distance(a, b) {
|
|
86
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
87
|
+
}
|
|
88
|
+
function midpoint(a, b) {
|
|
89
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
90
|
+
}
|
|
91
|
+
function pinchFrame(a, b, startDistance) {
|
|
92
|
+
const now = Math.max(1, distance(a, b));
|
|
93
|
+
return { scale: now / Math.max(1, startDistance), origin: midpoint(a, b), distance: now };
|
|
94
|
+
}
|
|
95
|
+
function wheelPixels(delta, mode, page) {
|
|
96
|
+
if (mode === 1) return delta * 16;
|
|
97
|
+
if (mode === 2) return delta * page;
|
|
98
|
+
return delta;
|
|
99
|
+
}
|
|
100
|
+
function createDragSession(start, time, options) {
|
|
101
|
+
const tracker = createVelocityTracker();
|
|
102
|
+
tracker.add(start, time);
|
|
103
|
+
let phase = "pending";
|
|
104
|
+
let axis = null;
|
|
105
|
+
let last = start;
|
|
106
|
+
function frame(point, at, first) {
|
|
107
|
+
const f = {
|
|
108
|
+
phase,
|
|
109
|
+
first,
|
|
110
|
+
start,
|
|
111
|
+
point,
|
|
112
|
+
movement: { x: point.x - start.x, y: point.y - start.y },
|
|
113
|
+
delta: { x: point.x - last.x, y: point.y - last.y },
|
|
114
|
+
velocity: tracker.velocity(at),
|
|
115
|
+
axis,
|
|
116
|
+
elapsed: at - time
|
|
117
|
+
};
|
|
118
|
+
last = point;
|
|
119
|
+
return f;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
get phase() {
|
|
123
|
+
return phase;
|
|
124
|
+
},
|
|
125
|
+
move(point, at) {
|
|
126
|
+
tracker.add(point, at);
|
|
127
|
+
let first = false;
|
|
128
|
+
if (phase === "pending") {
|
|
129
|
+
const movement = { x: point.x - start.x, y: point.y - start.y };
|
|
130
|
+
if (Math.hypot(movement.x, movement.y) >= options.slop) {
|
|
131
|
+
if (!options.axis) {
|
|
132
|
+
phase = "drag";
|
|
133
|
+
first = true;
|
|
134
|
+
} else {
|
|
135
|
+
const taken = lockAxis(movement, options.slop);
|
|
136
|
+
if (taken && options.axis !== "lock" && taken !== options.axis) phase = "rejected";
|
|
137
|
+
else if (taken) {
|
|
138
|
+
axis = taken;
|
|
139
|
+
phase = "drag";
|
|
140
|
+
first = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (phase !== "drag") return frame(point, at, false);
|
|
146
|
+
return frame(point, at, first);
|
|
147
|
+
},
|
|
148
|
+
end(point, at, swipe) {
|
|
149
|
+
tracker.add(point, at);
|
|
150
|
+
const f = frame(point, at, false);
|
|
151
|
+
const direction = phase === "drag" ? swipeDirection(f.movement, f.velocity, { axis: axis ?? void 0, ...swipe }) : null;
|
|
152
|
+
return { ...f, swipe: direction };
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
FLICK_VELOCITY,
|
|
158
|
+
HOLD_TO_DRAG_MS,
|
|
159
|
+
LONG_PRESS_MS,
|
|
160
|
+
MOUSE_SLOP,
|
|
161
|
+
TOUCH_SLOP,
|
|
162
|
+
createDragSession,
|
|
163
|
+
createVelocityTracker,
|
|
164
|
+
distance,
|
|
165
|
+
lockAxis,
|
|
166
|
+
midpoint,
|
|
167
|
+
momentumDistance,
|
|
168
|
+
pageAfterSwipe,
|
|
169
|
+
pinchFrame,
|
|
170
|
+
rubberBand,
|
|
171
|
+
rubberClamp,
|
|
172
|
+
shouldDismiss,
|
|
173
|
+
slopFor,
|
|
174
|
+
swipeDirection,
|
|
175
|
+
wheelPixels
|
|
176
|
+
};
|
|
@@ -17,8 +17,12 @@ export interface ScrollToElementOptions {
|
|
|
17
17
|
behavior?: ScrollBehavior;
|
|
18
18
|
/** Room kept between the element and the scroller's edge. */
|
|
19
19
|
margin?: number;
|
|
20
|
+
/** Which way to scroll: vertical (default) or horizontal, for a strip. */
|
|
21
|
+
axis?: "y" | "x";
|
|
22
|
+
/** Scroll this element rather than the nearest scroller along `axis`. */
|
|
23
|
+
container?: HTMLElement | null;
|
|
20
24
|
}
|
|
21
25
|
/** Scroll the nearest scroller, and only it, so that `el` is in view. */
|
|
22
26
|
/** The element's own `scroll-margin-top`, the gap it asks to keep above it when scrolled to. */
|
|
23
27
|
export declare function scrollMarginTop(el: Element): number;
|
|
24
|
-
export declare function scrollToElement(el: Element | null | undefined, { block, behavior, margin }?: ScrollToElementOptions): void;
|
|
28
|
+
export declare function scrollToElement(el: Element | null | undefined, { block, behavior, margin, axis, container }?: ScrollToElementOptions): void;
|
|
@@ -20,21 +20,28 @@ function atScrollEnd(el, slack = 2) {
|
|
|
20
20
|
function scrollMarginTop(el) {
|
|
21
21
|
return Number.parseFloat(nodeWindow(el).getComputedStyle(el).scrollMarginTop) || 0;
|
|
22
22
|
}
|
|
23
|
-
function scrollToElement(el, { block = "nearest", behavior = "auto", margin = 0 } = {}) {
|
|
23
|
+
function scrollToElement(el, { block = "nearest", behavior = "auto", margin = 0, axis = "y", container } = {}) {
|
|
24
24
|
if (!el) return;
|
|
25
25
|
const doc = el.ownerDocument;
|
|
26
26
|
const win = doc.defaultView ?? window;
|
|
27
|
-
const box = scrollParent(el) ?? doc.scrollingElement;
|
|
27
|
+
const box = container ?? scrollParent(el, { axis }) ?? doc.scrollingElement;
|
|
28
28
|
if (!box) return;
|
|
29
29
|
const outer = box === doc.scrollingElement ? new DOMRect(0, 0, win.innerWidth, win.innerHeight) : box.getBoundingClientRect();
|
|
30
30
|
const inner = el.getBoundingClientRect();
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
const x = axis === "x";
|
|
32
|
+
const from = x ? box.scrollLeft : box.scrollTop;
|
|
33
|
+
const view = x ? box.clientWidth : box.clientHeight;
|
|
34
|
+
const size = x ? inner.width : inner.height;
|
|
35
|
+
const lo = x ? inner.left - outer.left : inner.top - outer.top;
|
|
36
|
+
const hi = x ? inner.right - outer.right : inner.bottom - outer.bottom;
|
|
37
|
+
const at = lo + from;
|
|
38
|
+
let target = from;
|
|
39
|
+
if (block === "start") target = at - margin;
|
|
40
|
+
else if (block === "center") target = at - (view - size) / 2;
|
|
41
|
+
else if (lo < margin) target = at - margin;
|
|
42
|
+
else if (hi > -margin) target = at - view + size + margin;
|
|
43
|
+
if (target === from) return;
|
|
44
|
+
box.scrollTo(x ? { left: target, behavior } : { top: target, behavior });
|
|
38
45
|
}
|
|
39
46
|
export {
|
|
40
47
|
atScrollEnd,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Ref } from "vue";
|
|
2
|
+
export interface AppUpdateOptions {
|
|
3
|
+
/** The build this page is running. */
|
|
4
|
+
current: string;
|
|
5
|
+
/** The build that is live now, or null when it cannot be told. */
|
|
6
|
+
latest: () => Promise<string | null>;
|
|
7
|
+
/** Unsaved work the kit cannot see, such as a dirty form outside a dialog. */
|
|
8
|
+
busy?: () => boolean;
|
|
9
|
+
/** Ms without input before a visible tab may reload. */
|
|
10
|
+
idleMs?: number;
|
|
11
|
+
/** Ms between background checks; 0 turns the timer off. */
|
|
12
|
+
everyMs?: number;
|
|
13
|
+
/** sessionStorage key for the scroll position carried over the reload. */
|
|
14
|
+
storageKey?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface AppUpdate {
|
|
17
|
+
/** A newer build is live and the tab is waiting for a safe moment. */
|
|
18
|
+
pending: Readonly<Ref<boolean>>;
|
|
19
|
+
/** This page load is the reload of an update. */
|
|
20
|
+
restored: Readonly<Ref<boolean>>;
|
|
21
|
+
/** Ask the server now. */
|
|
22
|
+
check: () => Promise<void>;
|
|
23
|
+
/** Reload at once, keeping the scroll position. */
|
|
24
|
+
reloadNow: () => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function useAppUpdate(options: AppUpdateOptions): AppUpdate;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ref, onScopeDispose, readonly } from "vue";
|
|
2
|
+
const OVERLAY = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"], [data-state="open"][data-side]';
|
|
3
|
+
const INPUT_EVENTS = ["pointerdown", "keydown", "wheel", "touchstart", "input"];
|
|
4
|
+
const CARRY_MS = 6e4;
|
|
5
|
+
function editableFocused() {
|
|
6
|
+
const el = document.activeElement;
|
|
7
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
8
|
+
if (el.isContentEditable) return true;
|
|
9
|
+
if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;
|
|
10
|
+
return el instanceof HTMLInputElement && !["button", "checkbox", "radio", "range", "submit", "reset"].includes(el.type);
|
|
11
|
+
}
|
|
12
|
+
function overlayOpen() {
|
|
13
|
+
return [...document.querySelectorAll(OVERLAY)].some((el) => el.getClientRects().length > 0);
|
|
14
|
+
}
|
|
15
|
+
function readCarried(key) {
|
|
16
|
+
try {
|
|
17
|
+
const raw = sessionStorage.getItem(key);
|
|
18
|
+
sessionStorage.removeItem(key);
|
|
19
|
+
if (!raw) return null;
|
|
20
|
+
const value = JSON.parse(raw);
|
|
21
|
+
if (typeof value.href !== "string" || typeof value.top !== "number" || typeof value.at !== "number") return null;
|
|
22
|
+
return value;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function useAppUpdate(options) {
|
|
28
|
+
const key = options.storageKey ?? "lp:app-update";
|
|
29
|
+
const idleMs = options.idleMs ?? 3e4;
|
|
30
|
+
const pending = ref(false);
|
|
31
|
+
const restored = ref(false);
|
|
32
|
+
let lastInput = Date.now();
|
|
33
|
+
let timer;
|
|
34
|
+
const carried = readCarried(key);
|
|
35
|
+
if (carried && carried.href === location.href && Date.now() - carried.at < CARRY_MS) {
|
|
36
|
+
restored.value = true;
|
|
37
|
+
const top = carried.top;
|
|
38
|
+
const restore = () => window.scrollTo({ top, behavior: "instant" });
|
|
39
|
+
requestAnimationFrame(() => requestAnimationFrame(restore));
|
|
40
|
+
window.addEventListener("load", restore, { once: true });
|
|
41
|
+
}
|
|
42
|
+
function reloadNow() {
|
|
43
|
+
try {
|
|
44
|
+
sessionStorage.setItem(key, JSON.stringify({ href: location.href, top: window.scrollY, at: Date.now() }));
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
location.reload();
|
|
48
|
+
}
|
|
49
|
+
function safe() {
|
|
50
|
+
if (document.hidden) return true;
|
|
51
|
+
if (Date.now() - lastInput < idleMs) return false;
|
|
52
|
+
return !overlayOpen() && !editableFocused() && !options.busy?.();
|
|
53
|
+
}
|
|
54
|
+
function attempt() {
|
|
55
|
+
if (pending.value && safe()) reloadNow();
|
|
56
|
+
}
|
|
57
|
+
async function check() {
|
|
58
|
+
if (pending.value) return attempt();
|
|
59
|
+
let live = null;
|
|
60
|
+
try {
|
|
61
|
+
live = await options.latest();
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!live || live === options.current) return;
|
|
66
|
+
pending.value = true;
|
|
67
|
+
attempt();
|
|
68
|
+
}
|
|
69
|
+
const onInput = () => {
|
|
70
|
+
lastInput = Date.now();
|
|
71
|
+
};
|
|
72
|
+
const onVisibility = () => {
|
|
73
|
+
if (document.hidden) attempt();
|
|
74
|
+
else void check();
|
|
75
|
+
};
|
|
76
|
+
for (const name of INPUT_EVENTS) window.addEventListener(name, onInput, { passive: true, capture: true });
|
|
77
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
78
|
+
const every = options.everyMs ?? 5 * 6e4;
|
|
79
|
+
const tick = setInterval(attempt, 5e3);
|
|
80
|
+
if (every > 0) timer = setInterval(() => void check(), every);
|
|
81
|
+
onScopeDispose(() => {
|
|
82
|
+
for (const name of INPUT_EVENTS) window.removeEventListener(name, onInput, { capture: true });
|
|
83
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
84
|
+
clearInterval(tick);
|
|
85
|
+
if (timer) clearInterval(timer);
|
|
86
|
+
});
|
|
87
|
+
return { pending: readonly(pending), restored: readonly(restored), check, reloadNow };
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
useAppUpdate
|
|
91
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type MaybeRefOrGetter, type Ref } from "vue";
|
|
2
|
+
import { type GestureAxis, type GesturePoint, type PinchFrame, type SwipeOptions } from "./gesture";
|
|
3
|
+
import type { Direction } from "./spatialNav";
|
|
4
|
+
export interface GestureState {
|
|
5
|
+
event: PointerEvent;
|
|
6
|
+
pointerType: string;
|
|
7
|
+
/** Where the press started, client coordinates. */
|
|
8
|
+
start: GesturePoint;
|
|
9
|
+
/** Where the pointer is now. */
|
|
10
|
+
point: GesturePoint;
|
|
11
|
+
/** Travel since the press. */
|
|
12
|
+
movement: GesturePoint;
|
|
13
|
+
/** Travel since the previous report. */
|
|
14
|
+
delta: GesturePoint;
|
|
15
|
+
/** Px/ms over the last moments. */
|
|
16
|
+
velocity: GesturePoint;
|
|
17
|
+
/** The axis the drag committed to, with `axis` set. */
|
|
18
|
+
axis: GestureAxis | null;
|
|
19
|
+
/** Ms since the press. */
|
|
20
|
+
elapsed: number;
|
|
21
|
+
/** A drag that ended without being completed: do not commit it. */
|
|
22
|
+
cancelled: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface GestureEnd extends GestureState {
|
|
25
|
+
/** The swipe this release made, or null. */
|
|
26
|
+
swipe: Direction | null;
|
|
27
|
+
}
|
|
28
|
+
export interface GesturePinch extends PinchFrame {
|
|
29
|
+
event: PointerEvent;
|
|
30
|
+
}
|
|
31
|
+
export interface GestureOptions {
|
|
32
|
+
/** Off while false; read on every press. */
|
|
33
|
+
enabled?: MaybeRefOrGetter<boolean | undefined>;
|
|
34
|
+
/** Pointer types that may start a gesture. Default all. */
|
|
35
|
+
pointerTypes?: readonly ("mouse" | "pen" | "touch")[];
|
|
36
|
+
/** Restrict drags to an axis, or `lock` to the first one taken. */
|
|
37
|
+
axis?: GestureAxis | "lock";
|
|
38
|
+
/** Override the slop in px (default by pointer type). */
|
|
39
|
+
threshold?: number;
|
|
40
|
+
/** Presses starting inside an element matching this selector are ignored. */
|
|
41
|
+
ignore?: string;
|
|
42
|
+
/** Capture the pointer once dragging. Default true. */
|
|
43
|
+
capture?: boolean;
|
|
44
|
+
/** Swallow the click a drag ends with. Default true. */
|
|
45
|
+
preventClick?: boolean;
|
|
46
|
+
/** Options for the swipe reported on release. */
|
|
47
|
+
swipe?: SwipeOptions;
|
|
48
|
+
/** A press landed (before any threshold). Return false to ignore this press. */
|
|
49
|
+
onPress?: (event: PointerEvent) => boolean | void;
|
|
50
|
+
onDragStart?: (state: GestureState) => void;
|
|
51
|
+
onDrag?: (state: GestureState) => void;
|
|
52
|
+
onDragEnd?: (state: GestureEnd) => void;
|
|
53
|
+
/** Released after a swipe; also reported on onDragEnd as `swipe`. */
|
|
54
|
+
onSwipe?: (direction: Direction, state: GestureEnd) => void;
|
|
55
|
+
/** A press that never became a drag, released. */
|
|
56
|
+
onTap?: (event: PointerEvent) => void;
|
|
57
|
+
onPinchStart?: (pinch: GesturePinch) => void;
|
|
58
|
+
onPinch?: (pinch: GesturePinch) => void;
|
|
59
|
+
onPinchEnd?: (pinch: GesturePinch & {
|
|
60
|
+
cancelled: boolean;
|
|
61
|
+
}) => void;
|
|
62
|
+
/** A press held still for `delay` ms (default LONG_PRESS_MS); the click its release makes is swallowed. */
|
|
63
|
+
longPress?: {
|
|
64
|
+
delay?: MaybeRefOrGetter<number | undefined>;
|
|
65
|
+
pointerTypes?: readonly ("mouse" | "pen" | "touch")[];
|
|
66
|
+
handler: (event: PointerEvent) => void;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface Gesture {
|
|
70
|
+
/** True while a drag (past the slop) is in flight. */
|
|
71
|
+
dragging: Ref<boolean>;
|
|
72
|
+
/** True while two pointers are pinching. */
|
|
73
|
+
pinching: Ref<boolean>;
|
|
74
|
+
/** End whatever is in flight as cancelled. */
|
|
75
|
+
cancel: () => void;
|
|
76
|
+
/** Bind these to the element when not using useGesture. */
|
|
77
|
+
handlers: {
|
|
78
|
+
onPointerdown: (event: PointerEvent) => void;
|
|
79
|
+
onPointermove: (event: PointerEvent) => void;
|
|
80
|
+
onPointerup: (event: PointerEvent) => void;
|
|
81
|
+
onPointercancel: (event: PointerEvent) => void;
|
|
82
|
+
onLostpointercapture: (event: PointerEvent) => void;
|
|
83
|
+
onClickCapture: (event: MouseEvent) => void;
|
|
84
|
+
onDragstart: (event: DragEvent) => void;
|
|
85
|
+
onContextmenu: (event: Event) => void;
|
|
86
|
+
};
|
|
87
|
+
/** Detach window listeners and timers; useGesture calls it on unmount. */
|
|
88
|
+
dispose: () => void;
|
|
89
|
+
}
|
|
90
|
+
/** The engine without an element: bind `handlers` yourself (to compose with other handlers). */
|
|
91
|
+
export declare function createGesture(options: GestureOptions): Gesture;
|
|
92
|
+
/** The engine bound to `target`: listeners follow the element and leave on unmount. */
|
|
93
|
+
export declare function useGesture(target: MaybeRefOrGetter<HTMLElement | null | undefined>, options: GestureOptions): Gesture;
|
|
94
|
+
export interface Glide {
|
|
95
|
+
stop: () => void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Momentum after a fling: keep moving at `velocity` px/ms and bleed the speed
|
|
99
|
+
* off exponentially (time constant `tau` ms), so letting go drifts to a stop.
|
|
100
|
+
* Time-based, so it feels the same at 60 and 144 Hz. `step` gets the px to
|
|
101
|
+
* move this frame and returns false to stop early (an edge was hit). Under
|
|
102
|
+
* reduced motion nothing glides.
|
|
103
|
+
*/
|
|
104
|
+
export declare function glide(velocity: GesturePoint, step: (dx: number, dy: number) => boolean | void, { tau, win }?: {
|
|
105
|
+
tau?: number;
|
|
106
|
+
win?: Window;
|
|
107
|
+
}): Glide;
|