@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
package/README.md CHANGED
@@ -9,7 +9,7 @@ JSON token sets), and a composable block-layout canvas.
9
9
  - [Install](#install) and [Usage](#usage)
10
10
  - [Configuration](#configuration): theme, labels and languages, icons, Nuxt, overlays and Back, prop check, WebKitGTK glass, scrollbars
11
11
  - [Components](#components): every component by group, with what it is for
12
- - [Guides](#guides): linked views, workspaces, forms, tables, clipboard, video, command palette
12
+ - [Guides](#guides): linked views, workspaces, forms, tables, clipboard, video, gestures and sideways rows, command palette
13
13
  - [Develop](#develop) and [Releasing](#releasing)
14
14
 
15
15
  ## Install
@@ -289,7 +289,7 @@ from the comment that opens the component.
289
289
 
290
290
  <!-- catalogue:start -->
291
291
 
292
- 142 components. The preview (`bun run dev`) shows each one live, with its props, events and slots.
292
+ 143 components. The preview (`bun run dev`) shows each one live, with its props, events and slots.
293
293
 
294
294
  <details>
295
295
  <summary><b>Basics</b> (24)</summary>
@@ -389,7 +389,7 @@ from the comment that opens the component.
389
389
  </details>
390
390
 
391
391
  <details>
392
- <summary><b>Navigation</b> (15)</summary>
392
+ <summary><b>Navigation</b> (16)</summary>
393
393
 
394
394
  - `LpActionBar`: A bar that rises from the bottom of the window to ask for one decision, then leaves.
395
395
  - `LpBreadcrumbs`: Where the page sits in the app, as a trail of links.
@@ -402,6 +402,7 @@ from the comment that opens the component.
402
402
  - `LpPalettePreview`: The body of LpCommandPalette's `#preview` slot.
403
403
  - `LpPaletteStats`: A few figures in a palette preview, as tiles: mods and worlds in an instance, downloads and follows of a project, members online.
404
404
  - `LpPaletteSwatches`: A theme's colours as a strip of swatches on its own surface, for a palette preview of a theme picker: the row says the name, this shows what picking it would look like.
405
+ - `LpScrollStrip`: A row that scrolls sideways and can be reached with any input: tabs, chips, branch pills, a filmstrip.
405
406
  - `LpSidebar`: App-shell side navigation.
406
407
  - `LpStepper`: The steps of a flow, with where the person is and which are done.
407
408
  - `LpTableOfContents`: On-this-page table of contents with scroll-spy.
@@ -621,6 +622,55 @@ if (await canTranscodeVideo()) {
621
622
  Re-encoding never raises the bitrate above the source's, and the sound is kept
622
623
  (AAC, or Opus where the browser has no AAC encoder).
623
624
 
625
+ ### Gestures and sideways rows
626
+
627
+ Every drag, swipe, pinch and long press in the kit runs on one engine, so they
628
+ draw the line in the same place: a press becomes a drag past the slop (8 px for
629
+ a finger, 4 for a mouse), nothing is captured or prevented before that (a click
630
+ on what is under it still lands), a drag swallows the click it ends with,
631
+ velocity is read over the last 100 ms, and a pointer the browser takes back, a
632
+ second finger or Escape ends it as `cancelled`.
633
+
634
+ ```ts
635
+ import { useGesture, shouldDismiss } from "@leavepulse/ui"
636
+
637
+ const card = ref<HTMLElement | null>(null)
638
+ const x = ref(0)
639
+ useGesture(card, {
640
+ axis: "x", // a press that starts vertical is let go, so the page scrolls
641
+ onDrag: ({ movement }) => (x.value = movement.x),
642
+ onDragEnd: ({ movement, velocity, cancelled }) => {
643
+ const away = !cancelled && shouldDismiss({ offset: movement.x, velocity: velocity.x, size: 320 })
644
+ x.value = away ? 400 : 0
645
+ },
646
+ })
647
+ ```
648
+
649
+ - `createGesture(options)` returns the handlers to bind yourself, for an element
650
+ that composes them with others. `onPinch`, `longPress`, `onTap` and `onSwipe`
651
+ sit beside the drag callbacks.
652
+ - `pageAfterSwipe` and `rubberClamp` make "swipe between items", `glide` coasts
653
+ after a fling, and `createDragSession` makes the same decisions from touch
654
+ events where pointer events cannot be used (a pull from the screen edge must
655
+ stop the page scrolling mid-gesture).
656
+ - An element dragged by finger needs `touch-action` that leaves it the gesture
657
+ (`touch-pan-y` for a sideways one inside a page that scrolls), or the browser
658
+ takes the touch and the drag ends cancelled.
659
+ - Side drawers use it both ways: a pull from within `edgeSize` (72 px) of the
660
+ edge opens one, with the panel's edge under the finger; a swipe on the panel or
661
+ on the scrim closes it. Both complete past a share of the panel or on a flick
662
+ and spring back otherwise.
663
+
664
+ `LpScrollStrip` is the row that scrolls sideways (tabs, chips, branch pills, a
665
+ filmstrip) with its scrollbar hidden and still reachable with a mouse: a
666
+ vertical wheel scrolls it while it can move that way and hands back to the page
667
+ at either end, a mouse or pen drags it, the clipped edges fade, a hover shows
668
+ previous and next buttons, and the focused item is scrolled into view within
669
+ the row only. Touch keeps native scrolling and scroll snap. `useScrollStrip(el)`
670
+ gives an existing scroller the same behaviour. LpTabs uses it for a row wider
671
+ than its box; a mouse there picks a tab on click rather than on press, so a
672
+ drag across the row does not pick the tab it started on.
673
+
624
674
  ### Contextual command palette
625
675
 
626
676
  Wrap the application (or a page and its palette) in `LpCommandScope`. Nested
@@ -0,0 +1,18 @@
1
+
2
+ .lp-tab-badge-enter-active[data-v-43a45129],
3
+ .lp-tab-badge-leave-active[data-v-43a45129] {
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-43a45129],
9
+ .lp-tab-badge-leave-to[data-v-43a45129] {
10
+ opacity: 0;
11
+ transform: scale(0.6);
12
+ }
13
+ @media (prefers-reduced-motion: reduce) {
14
+ .lp-tab-badge-enter-active[data-v-43a45129],
15
+ .lp-tab-badge-leave-active[data-v-43a45129] {
16
+ transition: none;
17
+ }
18
+ }
@@ -0,0 +1,8 @@
1
+
2
+ .lp-trim-still-in[data-v-5ee84a84] > canvas:not([data-lp-stand-in]) {
3
+ animation: lp-trim-still-5ee84a84 var(--duration-medium) var(--ease-emphasized);
4
+ }
5
+ @keyframes lp-trim-still-5ee84a84 {
6
+ from { opacity: 0;
7
+ }
8
+ }
@@ -0,0 +1,54 @@
1
+ import { defineComponent, openBlock, createBlock, withCtx, createElementBlock, Fragment, renderList, resolveDynamicComponent, mergeProps, createTextVNode, toDisplayString } from "vue";
2
+ import { useLinkComponent } from "../composables/useLpConfig.js";
3
+ import { resolveLink } from "../components/link.js";
4
+ import { _ as _sfc_main$1 } from "./LpScrollStrip-D4tsOfAQ.js";
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(), createBlock(_sfc_main$1, {
23
+ as: "nav",
24
+ "aria-label": props.label,
25
+ class: "shrink-0 border-b border-line bg-surface",
26
+ "content-class": "gap-1 px-4 py-1.5"
27
+ }, {
28
+ default: withCtx(() => [
29
+ (openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item) => {
30
+ return openBlock(), createBlock(resolveDynamicComponent(rowOf(item).is), mergeProps({
31
+ key: item.id
32
+ }, { ref_for: true }, rowOf(item).props, {
33
+ "aria-current": __props.isActive(item) ? "page" : void 0,
34
+ class: [
35
+ "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",
36
+ item.disabled ? "cursor-not-allowed opacity-45" : __props.isActive(item) ? "bg-surface-soft text-ink" : "text-muted hover:text-ink"
37
+ ],
38
+ onClick: ($event) => emit("select", item)
39
+ }), {
40
+ default: withCtx(() => [
41
+ createTextVNode(toDisplayString(item.label), 1)
42
+ ]),
43
+ _: 2
44
+ }, 1040, ["aria-current", "class", "onClick"]);
45
+ }), 128))
46
+ ]),
47
+ _: 1
48
+ }, 8, ["aria-label"]);
49
+ };
50
+ }
51
+ });
52
+ export {
53
+ _sfc_main as _
54
+ };
@@ -1,10 +1,11 @@
1
- import { defineComponent, computed, useModel, ref, useId, onMounted, watch, nextTick, onBeforeUnmount, useSlots, openBlock, createElementBlock, mergeProps, unref, createElementVNode, withModifiers, toDisplayString, normalizeClass, createVNode, createSlots, withCtx, renderSlot, normalizeProps, guardReactiveProps, normalizeStyle, createCommentVNode, withDirectives, vShow, createBlock, Teleport, mergeModels } from "vue";
1
+ import { defineComponent, ref, computed, useModel, useId, onMounted, watch, nextTick, onBeforeUnmount, useSlots, openBlock, createElementBlock, mergeProps, unref, createElementVNode, withModifiers, toDisplayString, normalizeClass, createVNode, createSlots, withCtx, renderSlot, normalizeProps, guardReactiveProps, normalizeStyle, createCommentVNode, withDirectives, vShow, createBlock, Teleport, mergeModels } from "vue";
2
2
  import { prefersReducedMotion, easingToken, durationToken } from "../composables/easing.js";
3
3
  import { useLabels } from "../composables/useLpConfig.js";
4
4
  import { useResizeObserver } from "../composables/useResizeObserver.js";
5
5
  import { useScrollMemory } from "../composables/scrollMemory.js";
6
+ import { useScrollStrip } from "../composables/useScrollStrip.js";
6
7
  import { useView, viewWindow } from "../composables/useView.js";
7
- import { _ as _sfc_main$6 } from "./AppShellContextNav-C6Ho2mof.js";
8
+ import { _ as _sfc_main$6 } from "./AppShellContextNav-BoH4ffnC.js";
8
9
  import { DENSITY_CLASS, resolveNavMode, NAV_BREAKPOINT_PX, contextNavItems, headerCollapsedAfter } from "../components/appShell.js";
9
10
  import { _ as _sfc_main$9 } from "./LpBottomNav-9AqvZlRW.js";
10
11
  import { _ as _sfc_main$2 } from "./LpContextSwitcher-COw6GHPW.js";
@@ -13,7 +14,7 @@ import { _ as _sfc_main$4 } from "./LpIcon-CxHFPwKt.js";
13
14
  import { _ as _sfc_main$8 } from "./LpPageScrollbar-C9sTeU-2.js";
14
15
  import { _ as _sfc_main$7 } from "./LpScrollArea-CqvwfZs2.js";
15
16
  import { _ as _sfc_main$5 } from "./LpShift-DAgS_d2x.js";
16
- import { _ as _sfc_main$1 } from "./LpSidebar-DQtCYlT4.js";
17
+ import { _ as _sfc_main$1 } from "./LpSidebar-BIhusppV.js";
17
18
  import { mergeClass, useMergedAttrs } from "../composables/useMergedClass.js";
18
19
  import { nodeWindow, isHTMLElement } from "../composables/realm.js";
19
20
  const _hoisted_1 = ["href"];
@@ -69,6 +70,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
69
70
  emits: /* @__PURE__ */ mergeModels(["update:modelValue", "update:collapsed", "select", "update:compact", "update:pinned", "update:context", "nav-mode"], ["update:open"]),
70
71
  setup(__props, { expose: __expose, emit: __emit }) {
71
72
  const BACK_TO_TOP_AFTER_PX = 600;
73
+ const headerActions = ref(null);
74
+ const actionStrip = useScrollStrip(() => headerActions.value?.$el);
72
75
  const props = __props;
73
76
  const emit = __emit;
74
77
  const switching = computed(() => (props.contexts?.length ?? 0) > 1);
@@ -478,16 +481,20 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
478
481
  _: 3
479
482
  }),
480
483
  createVNode(_sfc_main$5, {
484
+ ref_key: "headerActions",
485
+ ref: headerActions,
481
486
  axis: "width",
482
487
  anchor: "end",
483
488
  presence: "",
484
- class: "ml-auto flex max-w-[66%] shrink items-center gap-2 overflow-x-auto lp-scrollbar-none md:max-w-none md:shrink-0 md:overflow-visible"
489
+ class: "lp-strip-fade ml-auto flex max-w-[66%] shrink items-center gap-2 overflow-x-auto lp-scrollbar-none md:max-w-none md:shrink-0 md:overflow-visible",
490
+ "data-lp-more-start": unref(actionStrip).overflowing.value && !unref(actionStrip).atStart.value ? "" : void 0,
491
+ "data-lp-more-end": unref(actionStrip).overflowing.value && !unref(actionStrip).atEnd.value ? "" : void 0
485
492
  }, {
486
493
  default: withCtx(() => [
487
494
  renderSlot(_ctx.$slots, "header-actions")
488
495
  ]),
489
496
  _: 3
490
- })
497
+ }, 8, ["data-lp-more-start", "data-lp-more-end"])
491
498
  ], 38),
492
499
  contextVisible.value ? renderSlot(_ctx.$slots, "context-nav", {
493
500
  key: 0,
@@ -6,7 +6,7 @@ import { useLabels } from "../composables/useLpConfig.js";
6
6
  import { useMediaQuery } from "../composables/useMediaQuery.js";
7
7
  import { normalizeRange, formatRange, defaultRangePresets, sameRange, allowedRange } from "../components/dateRange.js";
8
8
  import { _ as _sfc_main$2 } from "./PickerField-DzF3UVnX.js";
9
- import { _ as _sfc_main$3 } from "./LpRangeCalendar-C9gkNMYw.js";
9
+ import { _ as _sfc_main$3 } from "./LpRangeCalendar-9BvaLZji.js";
10
10
  const __default__ = { inheritAttrs: false };
11
11
  const _sfc_main = /* @__PURE__ */ defineComponent({
12
12
  ...__default__,
@@ -11,6 +11,8 @@ import { _ as _sfc_main$1 } from "./LpIcon-CxHFPwKt.js";
11
11
  import { _ as _sfc_main$2 } from "./LpScrollArea-CqvwfZs2.js";
12
12
  import { useModalLayer, anyModalOpen } from "../components/modalDepth.js";
13
13
  import { startPointerDrag } from "../composables/pointerDrag.js";
14
+ import { createDragSession, TOUCH_SLOP, shouldDismiss, rubberBand } from "../composables/gesture.js";
15
+ import { createGesture } from "../composables/useGesture.js";
14
16
  import { useView, viewDocument, usePortalTarget, useOutsideGuard, viewWindow } from "../composables/useView.js";
15
17
  import { clamp } from "../composables/clamp.js";
16
18
  import { nodeWindow, isElement } from "../composables/realm.js";
@@ -43,7 +45,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
43
45
  noDragContent: { type: Boolean },
44
46
  edgeOpen: { type: Boolean },
45
47
  edgeBreakpoint: {},
46
- edgeSize: { default: 44 },
48
+ edgeSize: { default: 72 },
47
49
  edgeOpenThreshold: { default: 0.3 },
48
50
  resizable: { type: Boolean },
49
51
  minSize: { default: "22rem" },
@@ -168,10 +170,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
168
170
  mounted: tagNoDrag,
169
171
  updated: tagNoDrag
170
172
  };
171
- const EDGE_SLOP = 10;
172
- const FLICK_SPEED = 0.45;
173
173
  const BREAKPOINT_PX = { sm: 640, md: 768, lg: 1024, xl: 1280 };
174
- const EDGE_IGNORE = "input, textarea, select, [contenteditable=''], [contenteditable='true'], [data-vaul-no-drag], [data-lp-no-edge-pull]";
174
+ const EDGE_IGNORE = "input, textarea, select, [contenteditable=''], [contenteditable='true'], div[data-vaul-no-drag], [data-lp-no-edge-pull]";
175
175
  const peek = ref(0);
176
176
  const peeking = ref(false);
177
177
  const handingOff = ref(false);
@@ -179,7 +179,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
179
179
  watch(
180
180
  () => props.open,
181
181
  (open) => {
182
- if (open) return;
182
+ if (open) {
183
+ shut.value = 0;
184
+ return;
185
+ }
183
186
  skipOpenSlide.value = false;
184
187
  endResize?.();
185
188
  }
@@ -200,16 +203,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
200
203
  }
201
204
  let track = null;
202
205
  const owner = /* @__PURE__ */ Symbol("edge-pull");
203
- function scrollsWithPull(target) {
206
+ function scrollsWithPull(target, towardOpen) {
204
207
  let el = isElement(target) ? target : null;
205
- const pullsRight = dir.value === "left";
208
+ const right = dir.value === "left" === towardOpen;
206
209
  const body = el?.ownerDocument.body;
207
210
  while (el && el !== body) {
208
211
  if (el.closest(EDGE_IGNORE) === el) return true;
209
212
  if (el.scrollWidth > el.clientWidth) {
210
213
  const overflow = nodeWindow(el).getComputedStyle(el).overflowX;
211
214
  if (overflow === "auto" || overflow === "scroll") {
212
- if (pullsRight ? el.scrollLeft > 0 : el.scrollLeft + el.clientWidth < el.scrollWidth) return true;
215
+ if (right ? el.scrollLeft > 0 : el.scrollLeft + el.clientWidth < el.scrollWidth) return true;
213
216
  }
214
217
  }
215
218
  el = el.parentElement;
@@ -219,26 +222,25 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
219
222
  function inward(x) {
220
223
  return dir.value === "right" ? edgeWindow().innerWidth - x : x;
221
224
  }
225
+ const opening = (x) => dir.value === "right" ? -x : x;
222
226
  function onTouchStart(e) {
223
227
  if (track) dropTrack();
224
228
  if (e.touches.length !== 1 || !edgeEnabled() || anyModalOpen() || edgeOwner.value) return;
225
229
  const t = e.touches[0];
226
- if (inward(t.clientX) > props.edgeSize || scrollsWithPull(e.target)) return;
227
- track = { id: t.identifier, x: t.clientX, y: t.clientY, t: e.timeStamp, lastX: t.clientX, lastT: e.timeStamp, speed: 0 };
230
+ if (inward(t.clientX) > props.edgeSize || scrollsWithPull(e.target, true)) return;
231
+ track = { id: t.identifier, session: createDragSession({ x: t.clientX, y: t.clientY }, e.timeStamp, { slop: TOUCH_SLOP, axis: "x" }) };
228
232
  }
229
233
  function onTouchMove(e) {
230
234
  if (!track) return;
231
235
  const t = Array.from(e.changedTouches).find((x) => x.identifier === track.id);
232
236
  if (!t) return;
233
- const sign = dir.value === "right" ? -1 : 1;
234
- const dx = (t.clientX - track.x) * sign;
235
- const dy = t.clientY - track.y;
236
- if (!peeking.value) {
237
- if (Math.abs(dy) > EDGE_SLOP && Math.abs(dy) > Math.abs(dx)) {
238
- track = null;
239
- return;
240
- }
241
- if (dx < EDGE_SLOP || Math.abs(dx) < Math.abs(dy)) return;
237
+ const frame = track.session.move({ x: t.clientX, y: t.clientY }, e.timeStamp);
238
+ if (frame.phase === "rejected" || frame.first && opening(frame.movement.x) <= 0) {
239
+ dropTrack();
240
+ return;
241
+ }
242
+ if (frame.phase !== "drag") return;
243
+ if (frame.first) {
242
244
  if (edgeOwner.value && edgeOwner.value !== owner) {
243
245
  track = null;
244
246
  return;
@@ -247,10 +249,6 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
247
249
  peeking.value = true;
248
250
  }
249
251
  if (e.cancelable) e.preventDefault();
250
- const dt = e.timeStamp - track.lastT;
251
- if (dt > 0) track.speed = (t.clientX - track.lastX) * sign / dt * 0.6 + track.speed * 0.4;
252
- track.lastX = t.clientX;
253
- track.lastT = e.timeStamp;
254
252
  peek.value = clamp(inward(t.clientX) / panelPx(), 0, 1);
255
253
  }
256
254
  function dropTrack() {
@@ -263,12 +261,15 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
263
261
  peeking.value = false;
264
262
  peek.value = 0;
265
263
  }
266
- function settle() {
267
- const flick = track !== null && track.speed > FLICK_SPEED && peek.value > 0.05;
264
+ function settle(e) {
265
+ const t = track && Array.from(e.changedTouches).find((x) => x.identifier === track.id);
266
+ const end = track && t ? track.session.end({ x: t.clientX, y: t.clientY }, e.timeStamp) : null;
268
267
  dropTrack();
269
268
  if (!peeking.value) return;
270
269
  peeking.value = false;
271
- if (peek.value >= props.edgeOpenThreshold || flick) {
270
+ const size = panelPx();
271
+ const opens = !!end && shouldDismiss({ offset: peek.value * size, velocity: opening(end.velocity.x), size, ratio: props.edgeOpenThreshold });
272
+ if (opens) {
272
273
  peek.value = 1;
273
274
  handingOff.value = true;
274
275
  skipOpenSlide.value = true;
@@ -287,6 +288,44 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
287
288
  peek.value = 0;
288
289
  }
289
290
  }
291
+ const shut = ref(0);
292
+ const shutting = ref(false);
293
+ const closeSwipe = {
294
+ pointerTypes: ["touch", "pen"],
295
+ axis: "x",
296
+ ignore: EDGE_IGNORE,
297
+ enabled: () => !!props.open && isHorizontal.value && props.dismissible && !guarded.value && !handingOff.value,
298
+ onPress: (e) => !scrollsWithPull(e.target, false),
299
+ onDragStart: () => shutting.value = true,
300
+ onDrag: ({ movement }) => {
301
+ const toward = -opening(movement.x);
302
+ shut.value = toward >= 0 ? toward : rubberBand(toward, panelPx(), 0.2);
303
+ },
304
+ onDragEnd: ({ velocity, cancelled }) => {
305
+ shutting.value = false;
306
+ const size = panelPx();
307
+ if (!cancelled && shouldDismiss({ offset: shut.value, velocity: -opening(velocity.x), size, ratio: props.closeThreshold ?? 0.25 })) {
308
+ emit("update:open", false);
309
+ return;
310
+ }
311
+ shut.value = 0;
312
+ }
313
+ };
314
+ const panelSwipe = createGesture(closeSwipe);
315
+ const scrimSwipe = createGesture(closeSwipe);
316
+ onBeforeUnmount(() => {
317
+ panelSwipe.dispose();
318
+ scrimSwipe.dispose();
319
+ });
320
+ const shutStyle = computed(() => {
321
+ if (!shut.value && !shutting.value) return void 0;
322
+ const px = dir.value === "right" ? shut.value : -shut.value;
323
+ return { transform: `translate3d(${px}px, 0, 0)`, transition: shutting.value ? "none" : void 0 };
324
+ });
325
+ const scrimStyle = computed(() => {
326
+ if (!shut.value && !shutting.value) return void 0;
327
+ return { opacity: clamp(1 - shut.value / Math.max(1, panelPx()), 0, 1), transition: shutting.value ? "none" : "opacity 0.5s cubic-bezier(0.32, 0.72, 0, 1)" };
328
+ });
290
329
  const timers = [];
291
330
  onBeforeUnmount(() => {
292
331
  for (const id of timers) window.clearTimeout(id);
@@ -294,7 +333,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
294
333
  });
295
334
  function onTouchEnd(e) {
296
335
  if (track && !Array.from(e.changedTouches).some((x) => x.identifier === track.id)) return;
297
- settle();
336
+ settle(e);
298
337
  }
299
338
  if (typeof document !== "undefined") {
300
339
  watch(
@@ -302,16 +341,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
302
341
  (on, _, onCleanup) => {
303
342
  if (!on) return;
304
343
  const doc = viewDocument(view.value);
305
- const passive = { passive: true };
344
+ const passive = { passive: true, capture: true };
306
345
  doc.addEventListener("touchstart", onTouchStart, passive);
307
- doc.addEventListener("touchmove", onTouchMove, { passive: false });
346
+ doc.addEventListener("touchmove", onTouchMove, { passive: false, capture: true });
308
347
  doc.addEventListener("touchend", onTouchEnd, passive);
309
348
  doc.addEventListener("touchcancel", cancelPull, passive);
310
349
  onCleanup(() => {
311
- doc.removeEventListener("touchstart", onTouchStart);
312
- doc.removeEventListener("touchmove", onTouchMove);
313
- doc.removeEventListener("touchend", onTouchEnd);
314
- doc.removeEventListener("touchcancel", cancelPull);
350
+ doc.removeEventListener("touchstart", onTouchStart, true);
351
+ doc.removeEventListener("touchmove", onTouchMove, true);
352
+ doc.removeEventListener("touchend", onTouchEnd, true);
353
+ doc.removeEventListener("touchcancel", cancelPull, true);
315
354
  cancelPull();
316
355
  });
317
356
  },
@@ -365,7 +404,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
365
404
  direction: dir.value,
366
405
  dismissible: __props.dismissible && !guarded.value,
367
406
  "snap-points": __props.snapPoints,
368
- "handle-only": __props.handleOnly,
407
+ "handle-only": __props.handleOnly || isHorizontal.value,
369
408
  "should-scale-background": __props.scaleBackground,
370
409
  "close-threshold": __props.closeThreshold,
371
410
  "onUpdate:open": _cache[1] || (_cache[1] = (v) => emit("update:open", v))
@@ -373,19 +412,19 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
373
412
  default: withCtx(() => [
374
413
  createVNode(unref(DrawerPortal), { to: unref(portalTo) }, {
375
414
  default: withCtx(() => [
376
- createVNode(unref(DrawerOverlay), {
377
- style: normalizeStyle({ zIndex: unref(layer).scrim }),
415
+ createVNode(unref(DrawerOverlay), mergeProps(unref(scrimSwipe).handlers, {
416
+ style: [{ zIndex: unref(layer).scrim }, scrimStyle.value],
378
417
  onClick: _cache[0] || (_cache[0] = ($event) => guarded.value && requestClose()),
379
- class: normalizeClass(["lp-scrim fixed inset-0 z-(--z-overlay) data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]", { "lp-scrim--light": unref(frosted), "lp-scrim--nested": unref(layer).nested, "lp-scrim--covered": unref(layer).covered }])
380
- }, null, 8, ["style", "class"]),
418
+ class: ["lp-scrim fixed inset-0 z-(--z-overlay) touch-none data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]", { "lp-scrim--light": unref(frosted), "lp-scrim--nested": unref(layer).nested, "lp-scrim--covered": unref(layer).covered }]
419
+ }), null, 16, ["style", "class"]),
381
420
  createVNode(unref(DrawerContent), mergeProps({
382
421
  onInteractOutside: unref(guardOutside),
383
422
  onEscapeKeyDown: unref(guardOutside),
384
423
  ref_key: "contentRef",
385
424
  ref: contentRef
386
- }, __props.description ? {} : { "aria-describedby": void 0 }, {
425
+ }, { ...__props.description ? {} : { "aria-describedby": void 0 }, ...unref(panelSwipe).handlers }, {
387
426
  class: contentClass.value,
388
- style: [sizeStyle.value, { zIndex: unref(layer).panel }],
427
+ style: [sizeStyle.value, { zIndex: unref(layer).panel }, shutStyle.value],
389
428
  "data-vaul-animate": handingOff.value || skipOpenSlide.value ? "false" : void 0,
390
429
  onKeydown: withKeys(guardEscape, ["escape"])
391
430
  }), {
@@ -452,7 +491,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
452
491
  }, 8, ["aria-label"])
453
492
  ], 2)) : createCommentVNode("", true),
454
493
  withDirectives((openBlock(), createElementBlock("div", {
455
- class: "flex min-h-0 min-w-0 flex-1 flex-col [-webkit-user-select:text]! [user-select:text]!",
494
+ class: normalizeClass(["flex min-h-0 min-w-0 flex-1 flex-col [-webkit-user-select:text]! [user-select:text]!", isHorizontal.value ? "[&_[data-reka-scroll-area-viewport]]:touch-pan-y" : void 0]),
456
495
  "data-vaul-no-drag": __props.noDragContent ? "" : void 0
457
496
  }, [
458
497
  createVNode(_sfc_main$2, {
@@ -464,7 +503,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
464
503
  ]),
465
504
  _: 3
466
505
  }, 8, ["content-class"])
467
- ], 8, _hoisted_3)), [
506
+ ], 10, _hoisted_3)), [
468
507
  [vNoDragControls, __props.noDragControls]
469
508
  ]),
470
509
  _ctx.$slots.footer ? (openBlock(), createElementBlock("footer", _hoisted_4, [