@leavepulse/ui 0.94.0 → 0.95.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 (96) hide show
  1. package/dist/assets/{LpTable-DxnthVz4.css → LpTable-BJjNEvgJ.css} +57 -30
  2. package/dist/assets/LpTabs-7yQjX2ih.css +18 -0
  3. package/dist/chunks/{LpAddressInput-BCqR0zmv.js → LpAddressInput-C5VIgRp0.js} +1 -1
  4. package/dist/chunks/{LpAppShell-xEBmGV78.js → LpAppShell-C4b3ySa_.js} +3 -3
  5. package/dist/chunks/{LpCodeBlock-BgWcg2mk.js → LpCodeBlock-DTU40x8P.js} +1 -1
  6. package/dist/chunks/{LpCodeDisplay-DBi1YPLp.js → LpCodeDisplay-F2hjLb0X.js} +2 -2
  7. package/dist/chunks/{LpConfirmDialog-y6k0ryWR.js → LpConfirmDialog-DrHcIxRn.js} +1 -1
  8. package/dist/chunks/{LpConfirmHost-D-X0tDym.js → LpConfirmHost-D0Cd8FA_.js} +1 -1
  9. package/dist/chunks/{LpCopyButton-BGzw95Xc.js → LpCopyButton-CiqUS4SX.js} +1 -1
  10. package/dist/chunks/{LpDataList-B56Gu6Yt.js → LpDataList-Cit9fOSp.js} +2 -2
  11. package/dist/chunks/{LpDrawer-BkWjFdV0.js → LpDrawer-BHlqi62K.js} +1 -1
  12. package/dist/chunks/{LpFileTree-BN_zybBN.js → LpFileTree-gVjF_b6r.js} +1 -1
  13. package/dist/chunks/{LpInspector-oTfkOdYd.js → LpInspector-DSL1lINx.js} +2 -2
  14. package/dist/chunks/{LpLightbox-DPgz7IsT.js → LpLightbox--EwvQGe-.js} +1 -1
  15. package/dist/chunks/{LpLogViewer-BuIzr_6h.js → LpLogViewer-DSq9e8mT.js} +1 -1
  16. package/dist/chunks/{LpMediaGallery-CTejKF2-.js → LpMediaGallery-DlYJf3DS.js} +1 -1
  17. package/dist/chunks/{LpModal-D1E7rQzw.js → LpModal-COMHrBKL.js} +1 -1
  18. package/dist/chunks/{LpModalNav-DVXAF-DG.js → LpModalNav-Cj02cfhL.js} +2 -2
  19. package/dist/chunks/{LpNotificationBell-KRgdRZs-.js → LpNotificationBell-DNGijtmW.js} +1 -1
  20. package/dist/chunks/{LpNotificationFeed-CMGYU8c-.js → LpNotificationFeed-BoovbtfL.js} +3 -3
  21. package/dist/chunks/{LpPageScrollbar-C9sTeU-2.js → LpPageScrollbar-BniFeL2U.js} +5 -1
  22. package/dist/chunks/{LpPaletteTheme-CLwO-SDf.js → LpPaletteTheme-_FwkJO2o.js} +1 -1
  23. package/dist/chunks/{LpPasswordInput-LPVI7D3v.js → LpPasswordInput-BBDz2zyU.js} +1 -1
  24. package/dist/chunks/{LpPie-CsiBXe-e.js → LpPie-CtC4XdhC.js} +1 -1
  25. package/dist/chunks/{LpPipeline-DVcoSs--.js → LpPipeline-aCALaUY8.js} +1 -1
  26. package/dist/chunks/{LpRelativeTime-CN5bWD73.js → LpRelativeTime-9MHlbq6F.js} +1 -1
  27. package/dist/chunks/{LpScrollArea-CqvwfZs2.js → LpScrollArea-B_1KZOSS.js} +11 -1
  28. package/dist/chunks/{LpSecretField-BwrZ4TfT.js → LpSecretField-C9vwMFQ6.js} +4 -4
  29. package/dist/chunks/{LpSidebar-BIhusppV.js → LpSidebar-DPri9uyz.js} +4 -4
  30. package/dist/chunks/{LpSidebarNav-CFcI7--7.js → LpSidebarNav-CV1rU6La.js} +34 -20
  31. package/dist/chunks/{LpThemeStudio-BPPctXgd.js → LpThemeStudio-yiEdFJQL.js} +1 -1
  32. package/dist/chunks/{LpThemeSwitcher-B3c44xFH.js → LpThemeSwitcher-DKkCFaFk.js} +1 -1
  33. package/dist/chunks/{LpToaster-Cdx1tnHB.js → LpToaster-C7hHkGOA.js} +1 -1
  34. package/dist/chunks/LpTooltip-Bt1ZFViJ.js +333 -0
  35. package/dist/chunks/{LpTruncate-C_C8eiIE.js → LpTruncate-DEHWL1OA.js} +2 -2
  36. package/dist/chunks/{LpUptimeBar-Dt6SY_-m.js → LpUptimeBar-CXgzU5ym.js} +1 -1
  37. package/dist/chunks/{LpWorkspaceSwitcher-DCIiOQt_.js → LpWorkspaceSwitcher-CrUVSI7x.js} +1 -1
  38. package/dist/chunks/{RevealToggle-BsYxY6Oz.js → RevealToggle-D9Vs88kM.js} +1 -1
  39. package/dist/components/LpAddressInput.vue.js +1 -1
  40. package/dist/components/LpAppShell.vue.js +1 -1
  41. package/dist/components/LpCodeBlock.vue.js +1 -1
  42. package/dist/components/LpCodeDisplay.vue.js +1 -1
  43. package/dist/components/LpCommandPalette.vue.js +1 -1
  44. package/dist/components/LpConfirmDialog.vue.js +1 -1
  45. package/dist/components/LpConfirmHost.vue.js +1 -1
  46. package/dist/components/LpCopyButton.vue.js +1 -1
  47. package/dist/components/LpDataList.vue.js +1 -1
  48. package/dist/components/LpDock.vue.js +1 -1
  49. package/dist/components/LpDrawer.vue.js +1 -1
  50. package/dist/components/LpFileTree.vue.js +1 -1
  51. package/dist/components/LpInspector.vue.js +1 -1
  52. package/dist/components/LpLightbox.vue.js +1 -1
  53. package/dist/components/LpLogViewer.vue.js +1 -1
  54. package/dist/components/LpMediaGallery.vue.js +1 -1
  55. package/dist/components/LpModal.vue.js +1 -1
  56. package/dist/components/LpModalNav.vue.js +1 -1
  57. package/dist/components/LpNotificationBell.vue.js +1 -1
  58. package/dist/components/LpNotificationFeed.vue.js +1 -1
  59. package/dist/components/LpPageScrollbar.vue.js +1 -1
  60. package/dist/components/LpPaletteTheme.vue.js +1 -1
  61. package/dist/components/LpPasswordInput.vue.js +1 -1
  62. package/dist/components/LpPie.vue.js +1 -1
  63. package/dist/components/LpPipeline.vue.js +1 -1
  64. package/dist/components/LpRelativeTime.vue.js +1 -1
  65. package/dist/components/LpScrollArea.vue.js +1 -1
  66. package/dist/components/LpSecretField.vue.js +1 -1
  67. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  68. package/dist/components/LpSidebar.vue.js +1 -1
  69. package/dist/components/LpSidebarNav.vue.js +1 -1
  70. package/dist/components/LpTable.vue.js +38 -12
  71. package/dist/components/LpTabs.vue.js +11 -6
  72. package/dist/components/LpThemeStudio.vue.js +1 -1
  73. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  74. package/dist/components/LpToaster.vue.js +1 -1
  75. package/dist/components/LpTooltip.vue.d.ts +3 -3
  76. package/dist/components/LpTooltip.vue.js +1 -1
  77. package/dist/components/LpTruncate.vue.js +1 -1
  78. package/dist/components/LpUptimeBar.vue.js +1 -1
  79. package/dist/components/LpWorkspaceSwitcher.vue.js +1 -1
  80. package/dist/components/RevealToggle.vue.js +1 -1
  81. package/dist/components/tableFeatures.d.ts +1 -0
  82. package/dist/components/tableFeatures.js +25 -0
  83. package/dist/composables/useShift.js +28 -11
  84. package/dist/index.js +34 -34
  85. package/package.json +1 -1
  86. package/src/components/LpPageScrollbar.vue +14 -1
  87. package/src/components/LpScrollArea.vue +18 -1
  88. package/src/components/LpSidebar.vue +1 -1
  89. package/src/components/LpSidebarNav.vue +18 -8
  90. package/src/components/LpTable.vue +76 -4
  91. package/src/components/LpTabs.vue +14 -5
  92. package/src/components/LpTooltip.vue +153 -14
  93. package/src/components/tableFeatures.ts +36 -0
  94. package/src/composables/useShift.ts +45 -15
  95. package/dist/assets/LpTabs-D7EyG21l.css +0 -18
  96. package/dist/chunks/LpTooltip-BKde8Xjv.js +0 -230
@@ -23,6 +23,15 @@
23
23
  * alone could not guarantee. reka opens the tip on keyboard focus as well as
24
24
  * hover and closes it on Escape, satisfying WCAG 1.4.13.
25
25
  *
26
+ * Until it is first wanted only the trigger is rendered: the reka tooltip (its
27
+ * provider, root, popper and portal) is stood up on the first pointer over the
28
+ * trigger, the first focus on it or a touch hold, and stays from then on. A
29
+ * table row carries two or three tips, nearly all of them never hovered, and
30
+ * fifty rows of full tooltips were most of the cost of showing the rows. The
31
+ * trigger's element is the same one before and after, so a ref to it, its
32
+ * focus and its hover survive the switch; the label is on it from the start,
33
+ * and the description arrives with the tip, as it always did.
34
+ *
26
35
  * Do NOT wrap the `#trigger` of LpDropdownMenu, LpPopover or LpContextMenu:
27
36
  * those bind their handlers to the slot's direct child, so a wrapper swallows
28
37
  * them and the control stops opening. Put `aria-label` on the trigger instead.
@@ -30,13 +39,28 @@
30
39
  import { clamp } from "../composables/clamp"
31
40
  import {
32
41
  injectTooltipProviderContext,
42
+ injectTooltipRootContext,
43
+ Slot,
33
44
  TooltipContent,
34
45
  TooltipPortal,
35
46
  TooltipProvider,
36
47
  TooltipRoot,
37
48
  TooltipTrigger,
38
49
  } from "reka-ui"
39
- import { computed, mergeProps, onBeforeUnmount, ref, useAttrs, watch, type FunctionalComponent } from "vue"
50
+ import {
51
+ computed,
52
+ defineComponent,
53
+ mergeProps,
54
+ onBeforeUnmount,
55
+ onMounted,
56
+ onUpdated,
57
+ ref,
58
+ shallowRef,
59
+ useAttrs,
60
+ watch,
61
+ type ComponentPublicInstance,
62
+ type FunctionalComponent,
63
+ } from "vue"
40
64
  import {
41
65
  springSettled,
42
66
  springStep,
@@ -263,6 +287,7 @@ const hold = useTouchHold({
263
287
  enabled: () => props.touch === "hold" && !props.followCursor && !props.disabled,
264
288
  delay: () => HOLD_MS,
265
289
  onHold: () => {
290
+ armed.value = true
266
291
  heldOpen.value = true
267
292
  wanted.value = true
268
293
  open.value = true
@@ -354,11 +379,132 @@ const guardOutside = useOutsideGuard()
354
379
  defineOptions({ inheritAttrs: false })
355
380
  // The caller's own listeners on the trigger run beside the hold's, not instead of them.
356
381
  const attrs = useAttrs()
357
- const triggerAttrs = computed(() => mergeProps(attrs, hold.handlers))
382
+
383
+ /*
384
+ * The tip is mounted on demand (see the header). The trigger is drawn by this
385
+ * component itself rather than by reka's TooltipTrigger, so it is the same
386
+ * element before and after: TooltipTrigger needs the root around it, and
387
+ * putting the root in later would have rebuilt the trigger under the pointer
388
+ * or the focus. What TooltipTrigger does with its events is done here instead,
389
+ * through the root's own context once it exists; reka's TooltipTrigger is
390
+ * still mounted with the tip, as the anchor it is placed against.
391
+ */
392
+ const armed = ref(false)
393
+ type RootContext = ReturnType<typeof injectTooltipRootContext>
394
+ type ProviderContext = ReturnType<typeof injectTooltipProviderContext>
395
+ const root = shallowRef<RootContext | null>(null)
396
+ let provider: ProviderContext | null = null
397
+
398
+ const triggerRef = ref<ComponentPublicInstance | null>(null)
399
+ const triggerEl = shallowRef<HTMLElement | undefined>()
400
+ function readTrigger() {
401
+ const el = triggerRef.value?.$el as Node | null | undefined
402
+ const next = el && el.nodeType === 1 ? (el as HTMLElement) : undefined
403
+ if (next === triggerEl.value) return
404
+ triggerEl.value = next
405
+ root.value?.onTriggerChange(next)
406
+ }
407
+ onMounted(readTrigger)
408
+ onUpdated(readTrigger)
409
+
410
+ /* What happened before the tip existed, replayed once it does. */
411
+ let hovering = false
412
+ let focusedBy: HTMLElement | null = null
413
+ let pointerDown = false
414
+ let pointerMoveOpened = false
415
+
416
+ const live = () => (root.value && !root.value.disabled.value ? root.value : null)
417
+
418
+ function enter() {
419
+ const tip = live()
420
+ if (!tip || pointerMoveOpened || provider?.isPointerInTransitRef.value) return
421
+ tip.onTriggerEnter()
422
+ pointerMoveOpened = true
423
+ }
424
+
425
+ function focusOpen(target: HTMLElement) {
426
+ const tip = live()
427
+ if (!tip || pointerDown) return
428
+ if (tip.ignoreNonKeyboardFocus.value && !target.matches?.(":focus-visible")) return
429
+ tip.onOpen()
430
+ }
431
+
432
+ const triggerEvents = {
433
+ onPointerenter(event: PointerEvent) {
434
+ if (event.pointerType === "touch") return
435
+ hovering = true
436
+ armed.value = true
437
+ },
438
+ onPointermove(event: PointerEvent) {
439
+ onPointerMove(event)
440
+ if (event.pointerType === "touch") return
441
+ hovering = true
442
+ armed.value = true
443
+ enter()
444
+ },
445
+ onPointerleave() {
446
+ hovering = false
447
+ const tip = live()
448
+ if (!tip) return
449
+ tip.onTriggerLeave()
450
+ pointerMoveOpened = false
451
+ },
452
+ onPointerdown(event: PointerEvent) {
453
+ const tip = live()
454
+ if (tip?.open.value && !tip.disableClosingTrigger.value) tip.onClose()
455
+ pointerDown = true
456
+ const doc = (event.currentTarget as Node | null)?.ownerDocument ?? document
457
+ doc.addEventListener("pointerup", () => setTimeout(() => (pointerDown = false), 1), { once: true })
458
+ },
459
+ onFocus(event: FocusEvent) {
460
+ focusedBy = event.currentTarget as HTMLElement
461
+ armed.value = true
462
+ focusOpen(focusedBy)
463
+ },
464
+ onBlur() {
465
+ focusedBy = null
466
+ live()?.onClose()
467
+ },
468
+ onClick() {
469
+ const tip = live()
470
+ if (tip && !tip.disableClosingTrigger.value) tip.onClose()
471
+ },
472
+ }
473
+
474
+ /* Hands the root's context out and replays what the tip was armed by. */
475
+ const RootBridge = defineComponent({
476
+ setup() {
477
+ root.value = injectTooltipRootContext()
478
+ provider = injectTooltipProviderContext()
479
+ onMounted(() => {
480
+ // After reka's TooltipTrigger, which reports the element it renders (none).
481
+ root.value?.onTriggerChange(triggerEl.value)
482
+ if (hovering) enter()
483
+ if (focusedBy && focusedBy.ownerDocument.activeElement === focusedBy) focusOpen(focusedBy)
484
+ })
485
+ onBeforeUnmount(() => {
486
+ root.value = null
487
+ provider = null
488
+ })
489
+ return () => null
490
+ },
491
+ })
492
+
493
+ const triggerProps = computed(() =>
494
+ mergeProps(attrs, hold.handlers, triggerEvents, {
495
+ "aria-label": props.label ? props.content : undefined,
496
+ "aria-describedby": root.value?.open.value ? root.value.contentId : undefined,
497
+ "data-state": root.value?.stateAttribute.value ?? "closed",
498
+ "data-grace-area-trigger": "",
499
+ class: props.touch === "hold" && !props.followCursor ? "[-webkit-touch-callout:none]" : undefined,
500
+ }),
501
+ )
358
502
  </script>
359
503
 
360
504
  <template>
361
- <component :is="shared ? PassThrough : TooltipProvider" v-bind="shared ? {} : { delayDuration: delay ?? 200 }">
505
+ <!-- The caller's own element, with the trigger's props merged onto it. -->
506
+ <Slot ref="triggerRef" v-bind="triggerProps"><slot /></Slot>
507
+ <component v-if="armed" :is="shared ? PassThrough : TooltipProvider" v-bind="shared ? {} : { delayDuration: delay ?? 200 }">
362
508
  <!--
363
509
  `disable-closing-trigger` in follow mode: reka dismisses a tip when its
364
510
  trigger is clicked, which is right for a button (the tip explained the
@@ -373,17 +519,10 @@ const triggerAttrs = computed(() => mergeProps(attrs, hold.handlers))
373
519
  :disable-closing-trigger="followCursor"
374
520
  @update:open="onOpenChange"
375
521
  >
376
- <!-- `as-child` merges these onto the caller's own element, so the label
377
- lands on the real button rather than on a wrapper. -->
378
- <TooltipTrigger
379
- as-child
380
- v-bind="triggerAttrs"
381
- :aria-label="label ? content : undefined"
382
- :class="touch === 'hold' && !followCursor ? '[-webkit-touch-callout:none]' : undefined"
383
- @pointermove="onPointerMove"
384
- >
385
- <slot />
386
- </TooltipTrigger>
522
+ <!-- Renders nothing: the anchor the tip is placed against, and the id
523
+ the trigger's aria-describedby points at. -->
524
+ <TooltipTrigger as-child :reference="triggerEl" />
525
+ <RootBridge />
387
526
  <TooltipPortal :to="portalTo">
388
527
  <TooltipContent
389
528
  @interact-outside="guardOutside"
@@ -157,3 +157,39 @@ export function presetHidden(
157
157
  * - `list`: one compact line per row with no header, for narrow places.
158
158
  */
159
159
  export type TableLayout = "table" | "cards" | "list"
160
+
161
+ /*
162
+ * How many rows a change of the shown list moves on screen: the ones that
163
+ * arrive, the ones that leave, and the ones that change their order among the
164
+ * rows that stay. Rows that only slide up or down to close a gap or make room
165
+ * are not counted; they move with the others and say nothing new.
166
+ *
167
+ * The order changes are the survivors outside the longest run that kept its
168
+ * order (a longest increasing subsequence of their old positions), which is the
169
+ * fewest rows that had to be picked up and put elsewhere.
170
+ */
171
+ export function rowChanges(before: readonly string[], after: readonly string[]): number {
172
+ const old = new Map<string, number>()
173
+ before.forEach((id, i) => old.set(id, i))
174
+ let entered = 0
175
+ const kept: number[] = []
176
+ for (const id of after) {
177
+ const at = old.get(id)
178
+ if (at === undefined) entered++
179
+ else kept.push(at)
180
+ }
181
+ const left = before.length - kept.length
182
+ // Patience sorting: tails[k] is the smallest end of an increasing run of k + 1.
183
+ const tails: number[] = []
184
+ for (const at of kept) {
185
+ let lo = 0
186
+ let hi = tails.length
187
+ while (lo < hi) {
188
+ const mid = (lo + hi) >> 1
189
+ if (tails[mid]! < at) lo = mid + 1
190
+ else hi = mid
191
+ }
192
+ tails[lo] = at
193
+ }
194
+ return entered + left + (kept.length - tails.length)
195
+ }
@@ -63,6 +63,33 @@ export interface UseShift {
63
63
  retune: () => void
64
64
  }
65
65
 
66
+ interface FirstRead {
67
+ read: () => void
68
+ write: () => void
69
+ }
70
+ const firstReads = new Map<Window, Set<FirstRead>>()
71
+
72
+ /*
73
+ * Runs in the next frame, every read of every box mounted meanwhile before any
74
+ * write: one layout for all of them, however many there are.
75
+ */
76
+ function firstRead(win: Window, pass: FirstRead): () => void {
77
+ let queue = firstReads.get(win)
78
+ if (!queue) {
79
+ const batch = new Set<FirstRead>()
80
+ queue = batch
81
+ firstReads.set(win, batch)
82
+ win.requestAnimationFrame(() => {
83
+ firstReads.delete(win)
84
+ for (const p of batch) p.read()
85
+ for (const p of batch) p.write()
86
+ })
87
+ }
88
+ queue.add(pass)
89
+ const batch = queue
90
+ return () => batch.delete(pass)
91
+ }
92
+
66
93
  /** Kept in step with the `duration-fast` token the transition utilities use. */
67
94
  const TWEEN_MS = 160
68
95
 
@@ -83,7 +110,7 @@ export function useShift(
83
110
 
84
111
  let ro: ResizeObserver | undefined
85
112
  let mo: MutationObserver | undefined
86
- let raf = 0
113
+ let cancelFirstRead: (() => void) | undefined
87
114
  let settleTimer: ReturnType<typeof setTimeout> | undefined
88
115
  /** Where the box last settled, so a retune has a size to ease FROM. */
89
116
  let lastHeight = 0
@@ -259,7 +286,8 @@ export function useShift(
259
286
  mo?.disconnect()
260
287
  ro = undefined
261
288
  mo = undefined
262
- cancelAnimationFrame(raf)
289
+ cancelFirstRead?.()
290
+ cancelFirstRead = undefined
263
291
  clearTimeout(settleTimer)
264
292
  tweening.value = false
265
293
  resizing.value = false
@@ -277,21 +305,23 @@ export function useShift(
277
305
  teardown()
278
306
  if (!enabled || !node) return
279
307
 
280
- lastHeight = node.offsetHeight
281
- lastWidth = node.offsetWidth
282
- laidOut = node.getClientRects().length > 0
283
308
  scrollHost = node
284
309
  node.addEventListener("scroll", rememberScroll, { capture: true, passive: true })
285
- if (wantsHeight()) node.style.height = `${lastHeight}px`
286
- if (wantsWidth()) node.style.width = `${lastWidth}px`
287
- // A box must not animate in from its pre-content size, so the transition
288
- // only arms once that first pin has painted. The pin is dropped in the
289
- // same frame: it exists to give the first `retune` something to start
290
- // from, and holding it would cap the box at its opening size.
291
- raf = requestAnimationFrame(() => {
292
- tweening.value = true
293
- if (wantsHeight()) node.style.height = ""
294
- if (wantsWidth()) node.style.width = ""
310
+ // The size a first `retune` eases from, read in the next frame together
311
+ // with every other box mounted in this render, and the transition armed
312
+ // then: a box must not animate in from its pre-content size. Read here,
313
+ // at mount, each box forced a layout of its own: fifty badges arriving
314
+ // in a table's rows laid the page out fifty times inside one click.
315
+ // Until the read, a retune finds no size to ease from and lets the box
316
+ // take its own, which is what a box that has just appeared should do.
317
+ cancelFirstRead = firstRead(nodeWindow(node), {
318
+ read: () => {
319
+ if (inFlight) return
320
+ lastHeight = node.offsetHeight
321
+ lastWidth = node.offsetWidth
322
+ laidOut = node.getClientRects().length > 0
323
+ },
324
+ write: () => (tweening.value = true),
295
325
  })
296
326
 
297
327
  // Two triggers, since either can move the size alone: the resize observer
@@ -1,18 +0,0 @@
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
- }
@@ -1,230 +0,0 @@
1
- import { defineComponent, ref, watch, onBeforeUnmount, useAttrs, computed, mergeProps, openBlock, createBlock, resolveDynamicComponent, unref, normalizeProps, guardReactiveProps, withCtx, createVNode, renderSlot, normalizeStyle, normalizeClass, createElementVNode, createTextVNode, toDisplayString } from "vue";
2
- import { clamp } from "../composables/clamp.js";
3
- import { injectTooltipProviderContext, TooltipProvider, TooltipRoot, TooltipTrigger, TooltipPortal, TooltipContent } from "reka-ui";
4
- import { springStep, springSettled } from "../composables/usePillTransition.js";
5
- import { PANEL_SURFACE, TOOLTIP_ANIM } from "../components/dropdown.js";
6
- import { useTouchHold } from "../composables/useTouchHold.js";
7
- import { prefersReducedMotion } from "../composables/easing.js";
8
- import { useView, viewWindow, usePortalTarget, useOutsideGuard } from "../composables/useView.js";
9
- import '../assets/LpTooltip-B61SYd4K.css';const HOLD_MS = 450;
10
- const EDGE = 8;
11
- const _sfc_main = /* @__PURE__ */ defineComponent({
12
- ...{ inheritAttrs: false },
13
- __name: "LpTooltip",
14
- props: {
15
- content: {},
16
- sideOffset: {},
17
- side: { default: "top" },
18
- label: { type: Boolean },
19
- delay: {},
20
- showOnPointerFocus: { type: Boolean },
21
- followCursor: { type: Boolean },
22
- disabled: { type: Boolean },
23
- touch: { default: "hold" }
24
- },
25
- setup(__props) {
26
- const props = __props;
27
- const shared = injectTooltipProviderContext(null);
28
- const PassThrough = (_, { slots }) => slots.default?.();
29
- const target = { x: 0, y: 0 };
30
- const offset = ref({ x: 0, y: 0 });
31
- const tipBody = ref(null);
32
- function inView(x, y) {
33
- const panel = tipBody.value?.parentElement;
34
- if (!panel) return { x, y };
35
- const rect = panel.getBoundingClientRect();
36
- const win = panel.ownerDocument.defaultView ?? window;
37
- return {
38
- x: withinRoom(x, rect.left - offset.value.x, rect.width, win.innerWidth),
39
- y: withinRoom(y, rect.top - offset.value.y, rect.height, win.innerHeight)
40
- };
41
- }
42
- function withinRoom(value, placed, size, room) {
43
- const min = EDGE - placed;
44
- const max = room - EDGE - size - placed;
45
- return max < min ? value : clamp(value, min, max);
46
- }
47
- let sx = { value: 0, velocity: 0 };
48
- let sy = { value: 0, velocity: 0 };
49
- let frame = 0;
50
- let lastFrame = 0;
51
- let pointer = null;
52
- function stopSpring() {
53
- if (frame) cancelAnimationFrame(frame);
54
- frame = 0;
55
- lastFrame = 0;
56
- }
57
- function jumpToTarget() {
58
- stopSpring();
59
- offset.value = { x: target.x, y: target.y };
60
- sx = { value: target.x, velocity: 0 };
61
- sy = { value: target.y, velocity: 0 };
62
- }
63
- const settle = (now) => {
64
- const dt = lastFrame ? (now - lastFrame) / 1e3 : 1 / 60;
65
- lastFrame = now;
66
- frame = 0;
67
- sx = springStep(sx, target.x, "float", dt);
68
- sy = springStep(sy, target.y, "float", dt);
69
- if (springSettled(sx, target.x) && springSettled(sy, target.y)) {
70
- sx = { value: target.x, velocity: 0 };
71
- sy = { value: target.y, velocity: 0 };
72
- offset.value = { x: target.x, y: target.y };
73
- lastFrame = 0;
74
- return;
75
- }
76
- offset.value = { x: sx.value, y: sy.value };
77
- frame = requestAnimationFrame(settle);
78
- };
79
- const onPointerMove = (e) => {
80
- if (!props.followCursor) return;
81
- const el = e.currentTarget;
82
- if (!el) return;
83
- pointer = { el, x: e.clientX, y: e.clientY };
84
- follow();
85
- };
86
- function follow() {
87
- if (!pointer) return;
88
- const { el, x: clientX, y: clientY } = pointer;
89
- const rect = el.getBoundingClientRect();
90
- const halfX = props.side === "left" || props.side === "right" ? rect.width / 2 : 0;
91
- const dirX = props.side === "right" ? -1 : 1;
92
- target.x = clientX - (rect.left + rect.width / 2) + halfX * dirX;
93
- const half = props.side === "top" || props.side === "bottom" ? rect.height / 2 : 0;
94
- const dir = props.side === "bottom" ? -1 : 1;
95
- target.y = clientY - (rect.top + rect.height / 2) + half * dir;
96
- Object.assign(target, inView(target.x, target.y));
97
- if (prefersReducedMotion()) {
98
- jumpToTarget();
99
- return;
100
- }
101
- if (!frame) frame = requestAnimationFrame(settle);
102
- }
103
- const open = ref(false);
104
- const wanted = ref(false);
105
- const heldOpen = ref(false);
106
- const hold = useTouchHold({
107
- enabled: () => props.touch === "hold" && !props.followCursor && !props.disabled,
108
- delay: () => HOLD_MS,
109
- onHold: () => {
110
- heldOpen.value = true;
111
- wanted.value = true;
112
- open.value = true;
113
- }
114
- });
115
- function release() {
116
- heldOpen.value = false;
117
- wanted.value = false;
118
- open.value = false;
119
- }
120
- const onOpenChange = (next) => {
121
- if (!next && heldOpen.value) return;
122
- wanted.value = next;
123
- open.value = next && !props.disabled;
124
- if (!open.value || !props.followCursor) return;
125
- jumpToTarget();
126
- };
127
- watch(
128
- () => props.disabled,
129
- (off) => {
130
- if (off) {
131
- hold.cancel();
132
- heldOpen.value = false;
133
- open.value = false;
134
- } else if (wanted.value) open.value = true;
135
- }
136
- );
137
- const view = useView();
138
- watch(
139
- () => open.value && props.followCursor,
140
- (following, _, onCleanup) => {
141
- if (!following) {
142
- stopSpring();
143
- return;
144
- }
145
- const win = viewWindow(view.value);
146
- win.addEventListener("resize", follow);
147
- onCleanup(() => win.removeEventListener("resize", follow));
148
- }
149
- );
150
- watch(heldOpen, (held, _, onCleanup) => {
151
- if (!held) return;
152
- const win = viewWindow(view.value);
153
- const onKey = (event) => event.key === "Escape" && release();
154
- win.addEventListener("pointerdown", release, true);
155
- win.addEventListener("keydown", onKey, true);
156
- onCleanup(() => {
157
- win.removeEventListener("pointerdown", release, true);
158
- win.removeEventListener("keydown", onKey, true);
159
- });
160
- });
161
- onBeforeUnmount(stopSpring);
162
- const portalTo = usePortalTarget();
163
- const guardOutside = useOutsideGuard();
164
- const attrs = useAttrs();
165
- const triggerAttrs = computed(() => mergeProps(attrs, hold.handlers));
166
- return (_ctx, _cache) => {
167
- return openBlock(), createBlock(resolveDynamicComponent(unref(shared) ? PassThrough : unref(TooltipProvider)), normalizeProps(guardReactiveProps(unref(shared) ? {} : { delayDuration: __props.delay ?? 200 })), {
168
- default: withCtx(() => [
169
- createVNode(unref(TooltipRoot), {
170
- open: open.value,
171
- "delay-duration": __props.delay,
172
- "ignore-non-keyboard-focus": !__props.showOnPointerFocus,
173
- "disable-hoverable-content": __props.followCursor,
174
- "disable-closing-trigger": __props.followCursor,
175
- "onUpdate:open": onOpenChange
176
- }, {
177
- default: withCtx(() => [
178
- createVNode(unref(TooltipTrigger), mergeProps({ "as-child": "" }, triggerAttrs.value, {
179
- "aria-label": __props.label ? __props.content : void 0,
180
- class: __props.touch === "hold" && !__props.followCursor ? "[-webkit-touch-callout:none]" : void 0,
181
- onPointermove: onPointerMove
182
- }), {
183
- default: withCtx(() => [
184
- renderSlot(_ctx.$slots, "default")
185
- ]),
186
- _: 3
187
- }, 16, ["aria-label", "class"]),
188
- createVNode(unref(TooltipPortal), { to: unref(portalTo) }, {
189
- default: withCtx(() => [
190
- createVNode(unref(TooltipContent), {
191
- onInteractOutside: unref(guardOutside),
192
- onEscapeKeyDown: unref(guardOutside),
193
- side: __props.side,
194
- "side-offset": __props.sideOffset ?? 6,
195
- "collision-padding": 8,
196
- "data-lp-follow": __props.followCursor ? "" : void 0,
197
- class: normalizeClass([
198
- unref(PANEL_SURFACE),
199
- unref(TOOLTIP_ANIM),
200
- "z-(--z-tooltip) max-w-[min(20rem,90vw)] px-2.5 py-1.5 text-xs text-ink"
201
- ]),
202
- style: normalizeStyle(__props.followCursor ? { translate: `${offset.value.x}px ${offset.value.y}px` } : void 0)
203
- }, {
204
- default: withCtx(() => [
205
- createElementVNode("div", {
206
- ref_key: "tipBody",
207
- ref: tipBody
208
- }, [
209
- renderSlot(_ctx.$slots, "content", {}, () => [
210
- createTextVNode(toDisplayString(__props.content), 1)
211
- ])
212
- ], 512)
213
- ]),
214
- _: 3
215
- }, 8, ["onInteractOutside", "onEscapeKeyDown", "side", "side-offset", "data-lp-follow", "class", "style"])
216
- ]),
217
- _: 3
218
- }, 8, ["to"])
219
- ]),
220
- _: 3
221
- }, 8, ["open", "delay-duration", "ignore-non-keyboard-focus", "disable-hoverable-content", "disable-closing-trigger"])
222
- ]),
223
- _: 3
224
- }, 16);
225
- };
226
- }
227
- });
228
- export {
229
- _sfc_main as _
230
- };