@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,249 @@
|
|
|
1
|
+
import { onBeforeUnmount, ref, toValue, watch, type MaybeRefOrGetter, type Ref } from "vue"
|
|
2
|
+
import { prefersReducedMotion } from "./easing"
|
|
3
|
+
import { momentumDistance, wheelPixels } from "./gesture"
|
|
4
|
+
import { isHTMLElement, nodeWindow } from "./realm"
|
|
5
|
+
import { scrollToElement } from "./scroll"
|
|
6
|
+
import { useGesture } from "./useGesture"
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* A row that scrolls sideways, made reachable without a touch screen. A
|
|
10
|
+
* hidden scrollbar leaves a mouse with no way to the items off to the right,
|
|
11
|
+
* so:
|
|
12
|
+
*
|
|
13
|
+
* - The wheel scrolls the row while the row can still move that way. At
|
|
14
|
+
* either end it does nothing, and the page scrolls on as usual; a sideways
|
|
15
|
+
* wheel or touchpad is left to the browser. Notches ease on a frame loop of
|
|
16
|
+
* our own, so quick notches add up instead of restarting.
|
|
17
|
+
* - A mouse or pen drags the row. A press that stays within a few pixels is
|
|
18
|
+
* an ordinary click on the item under it; a drag past that swallows the
|
|
19
|
+
* click, and a release while moving coasts on.
|
|
20
|
+
* - The focused item is kept in view, scrolling the row and nothing else
|
|
21
|
+
* (scrollIntoView would move every scrollable ancestor too).
|
|
22
|
+
* - Touch keeps the browser's own scrolling and scroll snap; a snapping row
|
|
23
|
+
* lets the browser snap the wheel and the end of a drag.
|
|
24
|
+
*
|
|
25
|
+
* `atStart`/`atEnd`/`overflowing` drive the edge fades and the buttons.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface ScrollStripOptions {
|
|
29
|
+
/** Map a vertical wheel to sideways scrolling. Default true. */
|
|
30
|
+
wheel?: MaybeRefOrGetter<boolean | undefined>
|
|
31
|
+
/** Drag to scroll with a mouse or pen. Default true. */
|
|
32
|
+
drag?: MaybeRefOrGetter<boolean | undefined>
|
|
33
|
+
/** Room kept beside a focused item, so it clears the fade. Default 24. */
|
|
34
|
+
focusMargin?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScrollStrip {
|
|
38
|
+
overflowing: Ref<boolean>
|
|
39
|
+
atStart: Ref<boolean>
|
|
40
|
+
atEnd: Ref<boolean>
|
|
41
|
+
dragging: Ref<boolean>
|
|
42
|
+
/** Scroll most of a view's width toward the start (-1) or the end (1). */
|
|
43
|
+
page: (direction: -1 | 1) => void
|
|
44
|
+
/** Scroll by `px`, eased unless reduced motion asks otherwise. */
|
|
45
|
+
scrollBy: (px: number) => void
|
|
46
|
+
/** Bring `el` into view within the strip only (`center` puts it in the middle). */
|
|
47
|
+
reveal: (el: Element | null | undefined, options?: { smooth?: boolean; block?: "nearest" | "center" }) => void
|
|
48
|
+
/** Measure again (after content changed in a way nothing observed). */
|
|
49
|
+
measure: () => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PAGE_SHARE = 0.8
|
|
53
|
+
|
|
54
|
+
export function useScrollStrip(target: MaybeRefOrGetter<HTMLElement | null | undefined>, options: ScrollStripOptions = {}): ScrollStrip {
|
|
55
|
+
const overflowing = ref(false)
|
|
56
|
+
const atStart = ref(true)
|
|
57
|
+
const atEnd = ref(true)
|
|
58
|
+
const el = () => {
|
|
59
|
+
const box = toValue(target)
|
|
60
|
+
return isHTMLElement(box) ? box : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function measure() {
|
|
64
|
+
const box = el()
|
|
65
|
+
if (!box) return
|
|
66
|
+
const max = box.scrollWidth - box.clientWidth
|
|
67
|
+
const left = Math.abs(box.scrollLeft)
|
|
68
|
+
overflowing.value = max > 1
|
|
69
|
+
atStart.value = left <= 1
|
|
70
|
+
atEnd.value = left >= max - 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const maxScroll = (box: HTMLElement) => Math.max(0, box.scrollWidth - box.clientWidth)
|
|
74
|
+
const snaps = (box: HTMLElement) => {
|
|
75
|
+
const type = nodeWindow(box).getComputedStyle(box).scrollSnapType
|
|
76
|
+
return !!type && type !== "none"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let frame = 0
|
|
80
|
+
let goal: number | null = null
|
|
81
|
+
let restoreBehavior: (() => void) | null = null
|
|
82
|
+
|
|
83
|
+
function stopEase() {
|
|
84
|
+
const box = el()
|
|
85
|
+
if (frame && box) nodeWindow(box).cancelAnimationFrame(frame)
|
|
86
|
+
frame = 0
|
|
87
|
+
goal = null
|
|
88
|
+
restoreBehavior?.()
|
|
89
|
+
restoreBehavior = null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function easeTo(box: HTMLElement, to: number) {
|
|
93
|
+
const win = nodeWindow(box)
|
|
94
|
+
goal = Math.max(0, Math.min(maxScroll(box), to))
|
|
95
|
+
if (prefersReducedMotion(win)) {
|
|
96
|
+
box.scrollLeft = goal
|
|
97
|
+
goal = null
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (frame) return
|
|
101
|
+
let previous = win.performance.now()
|
|
102
|
+
let position = box.scrollLeft
|
|
103
|
+
const behavior = box.style.scrollBehavior
|
|
104
|
+
box.style.scrollBehavior = "auto"
|
|
105
|
+
restoreBehavior = () => (box.style.scrollBehavior = behavior)
|
|
106
|
+
const done = () => {
|
|
107
|
+
frame = 0
|
|
108
|
+
goal = null
|
|
109
|
+
restoreBehavior?.()
|
|
110
|
+
restoreBehavior = null
|
|
111
|
+
}
|
|
112
|
+
const tick = (now: number) => {
|
|
113
|
+
if (goal === null) {
|
|
114
|
+
done()
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const dt = Math.min(now - previous, 32)
|
|
118
|
+
previous = now
|
|
119
|
+
position += (goal - position) * (1 - Math.exp(-dt / 70))
|
|
120
|
+
if (Math.abs(goal - position) < 0.5) position = goal
|
|
121
|
+
box.scrollLeft = position
|
|
122
|
+
if (position === goal) {
|
|
123
|
+
done()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
frame = win.requestAnimationFrame(tick)
|
|
127
|
+
}
|
|
128
|
+
frame = win.requestAnimationFrame(tick)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function scrollBy(px: number) {
|
|
132
|
+
const box = el()
|
|
133
|
+
if (!box) return
|
|
134
|
+
if (snaps(box)) {
|
|
135
|
+
box.scrollBy({ left: px, behavior: prefersReducedMotion(nodeWindow(box)) ? "auto" : "smooth" })
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
easeTo(box, (goal ?? box.scrollLeft) + px)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function page(direction: -1 | 1) {
|
|
142
|
+
const box = el()
|
|
143
|
+
if (box) scrollBy(direction * box.clientWidth * PAGE_SHARE)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function reveal(item: Element | null | undefined, { smooth = true, block = "nearest" }: { smooth?: boolean; block?: "nearest" | "center" } = {}) {
|
|
147
|
+
const box = el()
|
|
148
|
+
if (!box || !item || !box.contains(item)) return
|
|
149
|
+
stopEase()
|
|
150
|
+
const behavior = smooth && !prefersReducedMotion(nodeWindow(box)) ? "smooth" : "auto"
|
|
151
|
+
scrollToElement(item, { axis: "x", container: box, margin: options.focusMargin ?? 24, behavior, block })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onWheel(e: WheelEvent) {
|
|
155
|
+
const box = el()
|
|
156
|
+
if (!box || toValue(options.wheel) === false || e.ctrlKey || e.defaultPrevented) return
|
|
157
|
+
if (Math.abs(e.deltaX) >= Math.abs(e.deltaY)) return
|
|
158
|
+
const max = maxScroll(box)
|
|
159
|
+
if (max <= 1) return
|
|
160
|
+
const px = wheelPixels(e.shiftKey ? e.deltaX || e.deltaY : e.deltaY, e.deltaMode, box.clientWidth)
|
|
161
|
+
const from = goal ?? box.scrollLeft
|
|
162
|
+
if ((px > 0 && from >= max - 1) || (px < 0 && from <= 1)) return
|
|
163
|
+
e.preventDefault()
|
|
164
|
+
const notch = e.deltaMode !== 0 || Math.abs(px) >= 50
|
|
165
|
+
if (notch) scrollBy(px)
|
|
166
|
+
else {
|
|
167
|
+
stopEase()
|
|
168
|
+
box.scrollLeft += px
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function onFocusin(e: FocusEvent) {
|
|
173
|
+
const item = e.target
|
|
174
|
+
if (!isHTMLElement(item) || gesture.dragging.value) return
|
|
175
|
+
try {
|
|
176
|
+
if (!item.matches(":focus-visible")) return
|
|
177
|
+
} catch {}
|
|
178
|
+
reveal(item)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let snapType: string | null = null
|
|
182
|
+
const gesture = useGesture(el, {
|
|
183
|
+
pointerTypes: ["mouse", "pen"],
|
|
184
|
+
enabled: () => toValue(options.drag) !== false && overflowing.value,
|
|
185
|
+
onDragStart() {
|
|
186
|
+
const box = el()
|
|
187
|
+
if (!box) return
|
|
188
|
+
stopEase()
|
|
189
|
+
if (snaps(box)) {
|
|
190
|
+
snapType = box.style.scrollSnapType
|
|
191
|
+
box.style.scrollSnapType = "none"
|
|
192
|
+
}
|
|
193
|
+
box.style.scrollBehavior = "auto"
|
|
194
|
+
},
|
|
195
|
+
onDrag({ delta }) {
|
|
196
|
+
const box = el()
|
|
197
|
+
if (box) box.scrollLeft -= delta.x
|
|
198
|
+
},
|
|
199
|
+
onDragEnd({ velocity, cancelled }) {
|
|
200
|
+
const box = el()
|
|
201
|
+
if (!box) return
|
|
202
|
+
box.style.scrollBehavior = ""
|
|
203
|
+
const travel = cancelled ? 0 : -momentumDistance(velocity.x)
|
|
204
|
+
if (snapType !== null) {
|
|
205
|
+
box.style.scrollSnapType = snapType
|
|
206
|
+
snapType = null
|
|
207
|
+
box.scrollBy({ left: travel, behavior: prefersReducedMotion(nodeWindow(box)) ? "auto" : "smooth" })
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
if (Math.abs(travel) > 4) easeTo(box, box.scrollLeft + travel)
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
watch(
|
|
215
|
+
() => el(),
|
|
216
|
+
(box, _, onCleanup) => {
|
|
217
|
+
if (!box) return
|
|
218
|
+
const win = nodeWindow(box)
|
|
219
|
+
const resize = new win.ResizeObserver(measure)
|
|
220
|
+
const observeAll = () => {
|
|
221
|
+
resize.disconnect()
|
|
222
|
+
resize.observe(box)
|
|
223
|
+
for (const child of Array.from(box.children)) resize.observe(child)
|
|
224
|
+
}
|
|
225
|
+
const mutations = new win.MutationObserver(() => {
|
|
226
|
+
observeAll()
|
|
227
|
+
measure()
|
|
228
|
+
})
|
|
229
|
+
observeAll()
|
|
230
|
+
mutations.observe(box, { childList: true })
|
|
231
|
+
box.addEventListener("scroll", measure, { passive: true })
|
|
232
|
+
box.addEventListener("wheel", onWheel, { passive: false })
|
|
233
|
+
box.addEventListener("focusin", onFocusin)
|
|
234
|
+
measure()
|
|
235
|
+
onCleanup(() => {
|
|
236
|
+
resize.disconnect()
|
|
237
|
+
mutations.disconnect()
|
|
238
|
+
box.removeEventListener("scroll", measure)
|
|
239
|
+
box.removeEventListener("wheel", onWheel)
|
|
240
|
+
box.removeEventListener("focusin", onFocusin)
|
|
241
|
+
stopEase()
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
{ immediate: true, flush: "post" },
|
|
245
|
+
)
|
|
246
|
+
onBeforeUnmount(stopEase)
|
|
247
|
+
|
|
248
|
+
return { overflowing, atStart, atEnd, dragging: gesture.dragging, page, scrollBy, reveal, measure }
|
|
249
|
+
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { onBeforeUnmount } from "vue"
|
|
2
|
+
import { createGesture } from "./useGesture"
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
* Press-and-hold on a touch screen, for things a pointer reaches by hovering
|
|
5
|
-
* (a hover card, a tooltip).
|
|
6
|
-
*
|
|
7
|
-
* produces on lifting is swallowed
|
|
8
|
-
* the link or button under it
|
|
6
|
+
* (a hover card, a tooltip). The kit's gesture engine's long press, limited to
|
|
7
|
+
* touch: a hold that drifts past the slop is a scroll, not a hold; once it has
|
|
8
|
+
* fired, the click the finger produces on lifting is swallowed (the reader
|
|
9
|
+
* asked for the preview, not for the link or button under it), and the
|
|
10
|
+
* system's own long-press menu is kept away. A mouse or pen is left alone.
|
|
9
11
|
*
|
|
10
|
-
* The handlers go on the trigger
|
|
11
|
-
* pointercancel, click (capture) and contextmenu.
|
|
12
|
+
* The handlers go on the trigger.
|
|
12
13
|
*/
|
|
13
|
-
const SLOP = 8
|
|
14
|
-
/* A long press may produce no click at all; past this the next click is the user's own. */
|
|
15
|
-
const CLICK_AFTER_RELEASE_MS = 350
|
|
16
|
-
|
|
17
14
|
export interface TouchHoldOptions {
|
|
18
15
|
/** Whether a touch press may start a hold now. */
|
|
19
16
|
enabled: () => boolean
|
|
@@ -23,70 +20,15 @@ export interface TouchHoldOptions {
|
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
export function useTouchHold(options: TouchHoldOptions) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
function stop() {
|
|
33
|
-
window.clearTimeout(timer)
|
|
34
|
-
timer = 0
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function onPointerDown(event: PointerEvent) {
|
|
38
|
-
// Any new press starts over: on a hybrid laptop the mouse click after a
|
|
39
|
-
// hold must not be taken for that hold's own click.
|
|
40
|
-
if (timer && event.pointerId !== finger) return
|
|
41
|
-
held = false
|
|
42
|
-
if (event.pointerType !== "touch" || !options.enabled()) return
|
|
43
|
-
finger = event.pointerId
|
|
44
|
-
start = { x: event.clientX, y: event.clientY }
|
|
45
|
-
stop()
|
|
46
|
-
timer = window.setTimeout(() => {
|
|
47
|
-
timer = 0
|
|
48
|
-
held = true
|
|
49
|
-
options.onHold()
|
|
50
|
-
}, options.delay())
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function onPointerMove(event: PointerEvent) {
|
|
54
|
-
if (!timer || event.pointerId !== finger) return
|
|
55
|
-
if (Math.hypot(event.clientX - start.x, event.clientY - start.y) > SLOP) stop()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function onPointerUp(event: PointerEvent) {
|
|
59
|
-
if (finger !== null && event.pointerId !== finger) return
|
|
60
|
-
finger = null
|
|
61
|
-
stop()
|
|
62
|
-
if (held) window.setTimeout(() => (held = false), CLICK_AFTER_RELEASE_MS)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// A cancelled press produces no click, so there is nothing left to swallow.
|
|
66
|
-
function onPointerCancel(event?: PointerEvent) {
|
|
67
|
-
if (event && finger !== null && event.pointerId !== finger) return
|
|
68
|
-
finger = null
|
|
69
|
-
stop()
|
|
70
|
-
held = false
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function onClickCapture(event: MouseEvent) {
|
|
74
|
-
if (!held) return
|
|
75
|
-
held = false
|
|
76
|
-
event.preventDefault()
|
|
77
|
-
event.stopImmediatePropagation()
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// A long press would otherwise also bring up the system's own menu.
|
|
81
|
-
function onContextMenu(event: Event) {
|
|
82
|
-
if (timer || held) event.preventDefault()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
onBeforeUnmount(stop)
|
|
86
|
-
|
|
23
|
+
const gesture = createGesture({
|
|
24
|
+
enabled: options.enabled,
|
|
25
|
+
pointerTypes: ["touch"],
|
|
26
|
+
longPress: { delay: options.delay, handler: () => options.onHold() },
|
|
27
|
+
})
|
|
28
|
+
onBeforeUnmount(gesture.dispose)
|
|
87
29
|
return {
|
|
88
30
|
/** Drops a hold in progress, as if the finger had been lifted off the screen. */
|
|
89
|
-
cancel:
|
|
90
|
-
handlers:
|
|
31
|
+
cancel: gesture.cancel,
|
|
32
|
+
handlers: gesture.handlers,
|
|
91
33
|
}
|
|
92
34
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { clamp } from "./clamp"
|
|
2
|
+
import { distance, pinchFrame } from "./gesture"
|
|
2
3
|
import { computed, ref, toValue, type ComputedRef, type CSSProperties, type MaybeRefOrGetter, type Ref } from "vue"
|
|
3
4
|
|
|
4
5
|
/*
|
|
@@ -130,15 +131,11 @@ export function useZoomPan(options: UseZoomPanOptions = {}): UseZoomPan {
|
|
|
130
131
|
zoomAbout(scale.value * (e.deltaY < 0 ? step : 1 / step), point.x, point.y)
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
function distance(a: { x: number; y: number }, b: { x: number; y: number }) {
|
|
134
|
-
return Math.hypot(a.x - b.x, a.y - b.y)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
134
|
function onPointerDown(e: PointerEvent) {
|
|
138
135
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
|
139
136
|
|
|
140
137
|
if (pointers.size === 2) {
|
|
141
|
-
const [a, b] = [...pointers.values()]
|
|
138
|
+
const [a, b] = [...pointers.values()] as [{ x: number; y: number }, { x: number; y: number }]
|
|
142
139
|
pinchStartDistance = distance(a, b)
|
|
143
140
|
pinchStartScale = scale.value
|
|
144
141
|
panning.value = false
|
|
@@ -159,10 +156,10 @@ export function useZoomPan(options: UseZoomPanOptions = {}): UseZoomPan {
|
|
|
159
156
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
|
|
160
157
|
|
|
161
158
|
if (pointers.size === 2 && pinchStartDistance > 0) {
|
|
162
|
-
const [a, b] = [...pointers.values()]
|
|
163
|
-
const
|
|
164
|
-
const point = relative(
|
|
165
|
-
zoomAbout(pinchStartScale *
|
|
159
|
+
const [a, b] = [...pointers.values()] as [{ x: number; y: number }, { x: number; y: number }]
|
|
160
|
+
const pinch = pinchFrame(a, b, pinchStartDistance)
|
|
161
|
+
const point = relative({ clientX: pinch.origin.x, clientY: pinch.origin.y })
|
|
162
|
+
zoomAbout(pinchStartScale * pinch.scale, point.x, point.y)
|
|
166
163
|
return
|
|
167
164
|
}
|
|
168
165
|
|
package/src/index.ts
CHANGED
|
@@ -216,6 +216,7 @@ export { default as LpRadioGroup } from "./components/LpRadioGroup.vue"
|
|
|
216
216
|
export type { RadioOption } from "./components/LpRadioGroup.vue"
|
|
217
217
|
export { default as LpRollingText } from "./components/LpRollingText.vue"
|
|
218
218
|
export { default as LpScrollArea } from "./components/LpScrollArea.vue"
|
|
219
|
+
export { default as LpScrollStrip } from "./components/LpScrollStrip.vue"
|
|
219
220
|
export { default as LpPageScrollbar } from "./components/LpPageScrollbar.vue"
|
|
220
221
|
export { default as LpSegmented } from "./components/LpSegmented.vue"
|
|
221
222
|
export type { SegmentedOption } from "./components/LpSegmented.vue"
|
|
@@ -496,6 +497,30 @@ export { useTilt } from "./composables/useTilt"
|
|
|
496
497
|
export type { UseTilt, UseTiltOptions } from "./composables/useTilt"
|
|
497
498
|
export { useZoomPan } from "./composables/useZoomPan"
|
|
498
499
|
export type { UseZoomPan, UseZoomPanOptions } from "./composables/useZoomPan"
|
|
500
|
+
export { createGesture, glide, useGesture } from "./composables/useGesture"
|
|
501
|
+
export type { Gesture, GestureEnd, GestureOptions, GesturePinch, GestureState, Glide } from "./composables/useGesture"
|
|
502
|
+
export {
|
|
503
|
+
createDragSession,
|
|
504
|
+
createVelocityTracker,
|
|
505
|
+
FLICK_VELOCITY,
|
|
506
|
+
HOLD_TO_DRAG_MS,
|
|
507
|
+
LONG_PRESS_MS,
|
|
508
|
+
lockAxis,
|
|
509
|
+
MOUSE_SLOP,
|
|
510
|
+
momentumDistance,
|
|
511
|
+
pageAfterSwipe,
|
|
512
|
+
pinchFrame,
|
|
513
|
+
rubberBand,
|
|
514
|
+
rubberClamp,
|
|
515
|
+
shouldDismiss,
|
|
516
|
+
slopFor,
|
|
517
|
+
swipeDirection,
|
|
518
|
+
TOUCH_SLOP,
|
|
519
|
+
wheelPixels,
|
|
520
|
+
} from "./composables/gesture"
|
|
521
|
+
export type { DismissOptions, DragFrame, DragSession, DragSessionOptions, GestureAxis, GesturePoint, PageSwipeOptions, PinchFrame, SwipeOptions, VelocityTracker } from "./composables/gesture"
|
|
522
|
+
export { useScrollStrip } from "./composables/useScrollStrip"
|
|
523
|
+
export type { ScrollStrip, ScrollStripOptions } from "./composables/useScrollStrip"
|
|
499
524
|
export { useToast } from "./composables/useToast"
|
|
500
525
|
export type {
|
|
501
526
|
ToastAction,
|
|
@@ -572,6 +597,8 @@ export type { PipelineStep, StepStatus } from "./components/pipeline"
|
|
|
572
597
|
export { default as LpLoader } from "./components/LpLoader.vue"
|
|
573
598
|
export type { LoaderLabels, LoaderSize, LoaderTone, LoaderVariant } from "./components/LpLoader.vue"
|
|
574
599
|
export { useLoadingGate } from "./composables/useLoadingGate"
|
|
600
|
+
export { useAppUpdate } from "./composables/useAppUpdate"
|
|
601
|
+
export type { AppUpdate, AppUpdateOptions } from "./composables/useAppUpdate"
|
|
575
602
|
export type { LoadingGateOptions } from "./composables/useLoadingGate"
|
|
576
603
|
export { useNow } from "./composables/useNow"
|
|
577
604
|
export type { UseNowOptions } from "./composables/useNow"
|
package/src/tokens/effects.css
CHANGED
|
@@ -149,3 +149,43 @@
|
|
|
149
149
|
display: none;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* Edge fades for a sideways strip (LpScrollStrip, useScrollStrip): an edge
|
|
155
|
+
* with items beyond it fades out, an edge at the end of the row stays crisp.
|
|
156
|
+
* The widths are registered so a fade grows and leaves rather than switching;
|
|
157
|
+
* without @property it switches.
|
|
158
|
+
*/
|
|
159
|
+
@property --lp-strip-fade-start {
|
|
160
|
+
syntax: "<length>";
|
|
161
|
+
inherits: false;
|
|
162
|
+
initial-value: 0px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@property --lp-strip-fade-end {
|
|
166
|
+
syntax: "<length>";
|
|
167
|
+
inherits: false;
|
|
168
|
+
initial-value: 0px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.lp-strip-fade {
|
|
172
|
+
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--lp-strip-fade-start), #000 calc(100% - var(--lp-strip-fade-end)), transparent 100%);
|
|
173
|
+
mask-image: linear-gradient(to right, transparent 0, #000 var(--lp-strip-fade-start), #000 calc(100% - var(--lp-strip-fade-end)), transparent 100%);
|
|
174
|
+
transition:
|
|
175
|
+
--lp-strip-fade-start var(--duration-fast) var(--ease-emphasized),
|
|
176
|
+
--lp-strip-fade-end var(--duration-fast) var(--ease-emphasized);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.lp-strip-fade[data-lp-more-start] {
|
|
180
|
+
--lp-strip-fade-start: var(--lp-strip-fade, 28px);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.lp-strip-fade[data-lp-more-end] {
|
|
184
|
+
--lp-strip-fade-end: var(--lp-strip-fade, 28px);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@media (prefers-reduced-motion: reduce) {
|
|
188
|
+
.lp-strip-fade {
|
|
189
|
+
transition: none;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
.lp-tab-badge-enter-active[data-v-ef828291],
|
|
3
|
-
.lp-tab-badge-leave-active[data-v-ef828291] {
|
|
4
|
-
transition:
|
|
5
|
-
opacity var(--duration-fast) var(--ease-emphasized),
|
|
6
|
-
transform var(--duration-fast) var(--ease-emphasized);
|
|
7
|
-
}
|
|
8
|
-
.lp-tab-badge-enter-from[data-v-ef828291],
|
|
9
|
-
.lp-tab-badge-leave-to[data-v-ef828291] {
|
|
10
|
-
opacity: 0;
|
|
11
|
-
transform: scale(0.6);
|
|
12
|
-
}
|
|
13
|
-
@media (prefers-reduced-motion: reduce) {
|
|
14
|
-
.lp-tab-badge-enter-active[data-v-ef828291],
|
|
15
|
-
.lp-tab-badge-leave-active[data-v-ef828291] {
|
|
16
|
-
transition: none;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { defineComponent, openBlock, createElementBlock, Fragment, renderList, createBlock, resolveDynamicComponent, mergeProps, withCtx, createTextVNode, toDisplayString } from "vue";
|
|
2
|
-
import { useLinkComponent } from "../composables/useLpConfig.js";
|
|
3
|
-
import { resolveLink } from "../components/link.js";
|
|
4
|
-
const _hoisted_1 = ["aria-label"];
|
|
5
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
6
|
-
__name: "AppShellContextNav",
|
|
7
|
-
props: {
|
|
8
|
-
items: {},
|
|
9
|
-
isActive: { type: Function },
|
|
10
|
-
label: {}
|
|
11
|
-
},
|
|
12
|
-
emits: ["select"],
|
|
13
|
-
setup(__props, { emit: __emit }) {
|
|
14
|
-
const props = __props;
|
|
15
|
-
const emit = __emit;
|
|
16
|
-
const linkComponent = useLinkComponent();
|
|
17
|
-
function rowOf(item) {
|
|
18
|
-
const link = item.disabled ? null : resolveLink(linkComponent.value, { to: item.to, href: item.href });
|
|
19
|
-
return link ?? { is: "button", props: { type: "button", disabled: item.disabled || void 0 } };
|
|
20
|
-
}
|
|
21
|
-
return (_ctx, _cache) => {
|
|
22
|
-
return openBlock(), createElementBlock("nav", {
|
|
23
|
-
"aria-label": props.label,
|
|
24
|
-
class: "flex shrink-0 gap-1 overflow-x-auto border-b border-line bg-surface px-4 py-1.5 lp-scrollbar-none"
|
|
25
|
-
}, [
|
|
26
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
|
|
27
|
-
return openBlock(), createBlock(resolveDynamicComponent(rowOf(item).is), mergeProps({
|
|
28
|
-
key: item.id
|
|
29
|
-
}, { ref_for: true }, rowOf(item).props, {
|
|
30
|
-
"aria-current": __props.isActive(item) ? "page" : void 0,
|
|
31
|
-
class: [
|
|
32
|
-
"shrink-0 rounded-control px-2.5 py-1 text-sm font-medium outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring",
|
|
33
|
-
item.disabled ? "cursor-not-allowed opacity-45" : __props.isActive(item) ? "bg-surface-soft text-ink" : "text-muted hover:text-ink"
|
|
34
|
-
],
|
|
35
|
-
onClick: ($event) => emit("select", item)
|
|
36
|
-
}), {
|
|
37
|
-
default: withCtx(() => [
|
|
38
|
-
createTextVNode(toDisplayString(item.label), 1)
|
|
39
|
-
]),
|
|
40
|
-
_: 2
|
|
41
|
-
}, 1040, ["aria-current", "class", "onClick"]);
|
|
42
|
-
}), 128))
|
|
43
|
-
], 8, _hoisted_1);
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
export {
|
|
48
|
-
_sfc_main as _
|
|
49
|
-
};
|