@leavepulse/ui 0.41.0 → 0.43.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 (157) hide show
  1. package/dist/assets/{LpCommandPalette-DyT6aGdW.css → LpCommandPalette-s_kKuCRf.css} +21 -21
  2. package/dist/chunks/{LpAddressInput-CMjaPQVw.js → LpAddressInput-B-Zvxsn7.js} +1 -1
  3. package/dist/chunks/{LpAppShell-Bcv6EhqU.js → LpAppShell-BGQVIY-3.js} +1 -1
  4. package/dist/chunks/{LpConfirmDialog-BNAvOR24.js → LpConfirmDialog-Q-bxff8z.js} +1 -1
  5. package/dist/chunks/{LpDrawer-DgaS2s2i.js → LpDrawer-BZ6k3hvo.js} +3 -3
  6. package/dist/chunks/{LpLightbox-oLqyjOln.js → LpLightbox-2sYVMY-S.js} +12 -4
  7. package/dist/chunks/{LpMediaGallery-Cid7PGYx.js → LpMediaGallery-FF-NquIo.js} +1 -1
  8. package/dist/chunks/{LpModal-8bja5bWS.js → LpModal-Bt0pQCI_.js} +2 -2
  9. package/dist/chunks/{LpModalNav-w9kfwHqO.js → LpModalNav-YaifHrXC.js} +2 -2
  10. package/dist/chunks/{LpSidebar-DlTn5nXc.js → LpSidebar-ClBUwoEz.js} +1 -1
  11. package/dist/components/LpAddressInput.vue.js +1 -1
  12. package/dist/components/LpAppShell.vue.js +1 -1
  13. package/dist/components/LpCommandPalette.vue.js +124 -115
  14. package/dist/components/LpConfirmDialog.vue.js +1 -1
  15. package/dist/components/LpDrawer.vue.js +1 -1
  16. package/dist/components/LpLightbox.vue.js +1 -1
  17. package/dist/components/LpMediaGallery.vue.js +1 -1
  18. package/dist/components/LpModal.vue.js +1 -1
  19. package/dist/components/LpModalNav.vue.js +1 -1
  20. package/dist/components/LpShortcutsDialog.vue.js +1 -1
  21. package/dist/components/LpSidebar.vue.js +1 -1
  22. package/dist/components/commandSearch.js +66 -0
  23. package/dist/components/modalDepth.js +3 -2
  24. package/dist/composables/useHotkeys.js +41 -21
  25. package/dist/index.js +13 -9
  26. package/package.json +3 -3
  27. package/src/components/LpCommandPalette.vue +81 -99
  28. package/src/components/LpDrawer.vue +3 -3
  29. package/src/components/LpLightbox.vue +11 -5
  30. package/src/components/LpModal.vue +2 -2
  31. package/src/components/commandSearch.ts +92 -0
  32. package/src/components/modalDepth.ts +4 -48
  33. package/src/composables/useHotkeys.ts +54 -33
  34. package/src/index.ts +2 -0
  35. package/src/tokens/tokens.css +13 -19
  36. package/dist/component-names.d.ts +0 -1
  37. package/dist/components/LayoutCanvas.vue.d.ts +0 -28
  38. package/dist/components/LayoutNode.vue.d.ts +0 -40
  39. package/dist/components/LpActionBar.vue.d.ts +0 -84
  40. package/dist/components/LpAddressInput.vue.d.ts +0 -70
  41. package/dist/components/LpAlert.vue.d.ts +0 -44
  42. package/dist/components/LpAppShell.vue.d.ts +0 -123
  43. package/dist/components/LpAutocomplete.vue.d.ts +0 -50
  44. package/dist/components/LpAvatar.vue.d.ts +0 -34
  45. package/dist/components/LpBadge.vue.d.ts +0 -20
  46. package/dist/components/LpBreadcrumbs.vue.d.ts +0 -32
  47. package/dist/components/LpButton.vue.d.ts +0 -113
  48. package/dist/components/LpCalendar.vue.d.ts +0 -29
  49. package/dist/components/LpCard.vue.d.ts +0 -60
  50. package/dist/components/LpChart.vue.d.ts +0 -130
  51. package/dist/components/LpCheckbox.vue.d.ts +0 -32
  52. package/dist/components/LpCodeBlock.vue.d.ts +0 -58
  53. package/dist/components/LpColorPicker.vue.d.ts +0 -124
  54. package/dist/components/LpCommandPalette.vue.d.ts +0 -206
  55. package/dist/components/LpConfirmDialog.vue.d.ts +0 -59
  56. package/dist/components/LpContextMenu.vue.d.ts +0 -44
  57. package/dist/components/LpDatePicker.vue.d.ts +0 -33
  58. package/dist/components/LpDisclosure.vue.d.ts +0 -47
  59. package/dist/components/LpDivider.vue.d.ts +0 -17
  60. package/dist/components/LpDrawer.vue.d.ts +0 -170
  61. package/dist/components/LpDropdownMenu.vue.d.ts +0 -23
  62. package/dist/components/LpDropzone.vue.d.ts +0 -90
  63. package/dist/components/LpEmptyState.vue.d.ts +0 -28
  64. package/dist/components/LpFileTree.vue.d.ts +0 -126
  65. package/dist/components/LpFileTreeNode.vue.d.ts +0 -62
  66. package/dist/components/LpFormField.vue.d.ts +0 -24
  67. package/dist/components/LpIcon.vue.d.ts +0 -8
  68. package/dist/components/LpImageEditor.vue.d.ts +0 -106
  69. package/dist/components/LpInfraNode.vue.d.ts +0 -65
  70. package/dist/components/LpInput.vue.d.ts +0 -80
  71. package/dist/components/LpLaneNode.vue.d.ts +0 -24
  72. package/dist/components/LpLightbox.vue.d.ts +0 -62
  73. package/dist/components/LpLink.vue.d.ts +0 -93
  74. package/dist/components/LpLogViewer.vue.d.ts +0 -153
  75. package/dist/components/LpMapPicker.vue.d.ts +0 -59
  76. package/dist/components/LpMediaGallery.vue.d.ts +0 -80
  77. package/dist/components/LpModal.vue.d.ts +0 -66
  78. package/dist/components/LpModalNav.vue.d.ts +0 -66
  79. package/dist/components/LpNotificationBell.vue.d.ts +0 -85
  80. package/dist/components/LpNumberField.vue.d.ts +0 -21
  81. package/dist/components/LpNumberFlow.vue.d.ts +0 -35
  82. package/dist/components/LpOtpInput.vue.d.ts +0 -39
  83. package/dist/components/LpPagination.vue.d.ts +0 -35
  84. package/dist/components/LpPasswordInput.vue.d.ts +0 -36
  85. package/dist/components/LpPhoneInput.vue.d.ts +0 -49
  86. package/dist/components/LpPie.vue.d.ts +0 -196
  87. package/dist/components/LpPopover.vue.d.ts +0 -34
  88. package/dist/components/LpProgress.vue.d.ts +0 -66
  89. package/dist/components/LpRadio.vue.d.ts +0 -18
  90. package/dist/components/LpRadioGroup.vue.d.ts +0 -26
  91. package/dist/components/LpRollingText.vue.d.ts +0 -33
  92. package/dist/components/LpScrollArea.vue.d.ts +0 -28
  93. package/dist/components/LpSection.vue.d.ts +0 -120
  94. package/dist/components/LpSegmented.vue.d.ts +0 -30
  95. package/dist/components/LpSelect.vue.d.ts +0 -47
  96. package/dist/components/LpServiceNode.vue.d.ts +0 -14
  97. package/dist/components/LpShift.vue.d.ts +0 -36
  98. package/dist/components/LpShortcutsDialog.vue.d.ts +0 -24
  99. package/dist/components/LpSidebar.vue.d.ts +0 -146
  100. package/dist/components/LpSidebarNav.vue.d.ts +0 -54
  101. package/dist/components/LpSkeleton.vue.d.ts +0 -16
  102. package/dist/components/LpSlider.vue.d.ts +0 -30
  103. package/dist/components/LpSparkline.vue.d.ts +0 -48
  104. package/dist/components/LpStat.vue.d.ts +0 -69
  105. package/dist/components/LpStepper.vue.d.ts +0 -36
  106. package/dist/components/LpSwitch.vue.d.ts +0 -42
  107. package/dist/components/LpTable.vue.d.ts +0 -124
  108. package/dist/components/LpTableOfContents.vue.d.ts +0 -19
  109. package/dist/components/LpTabs.vue.d.ts +0 -47
  110. package/dist/components/LpTextarea.vue.d.ts +0 -22
  111. package/dist/components/LpThemeSwitcher.vue.d.ts +0 -47
  112. package/dist/components/LpTilt.vue.d.ts +0 -31
  113. package/dist/components/LpTimeField.vue.d.ts +0 -24
  114. package/dist/components/LpToaster.vue.d.ts +0 -3
  115. package/dist/components/LpTooltip.vue.d.ts +0 -58
  116. package/dist/components/LpTopologyCanvas.vue.d.ts +0 -205
  117. package/dist/components/LpUptimeBar.vue.d.ts +0 -46
  118. package/dist/components/LpWaypointEdge.vue.d.ts +0 -21
  119. package/dist/components/actionBar.d.ts +0 -19
  120. package/dist/components/address.d.ts +0 -107
  121. package/dist/components/chart.d.ts +0 -180
  122. package/dist/components/codeHighlight.d.ts +0 -8
  123. package/dist/components/color.d.ts +0 -65
  124. package/dist/components/countries-data.d.ts +0 -2
  125. package/dist/components/countries.d.ts +0 -20
  126. package/dist/components/dropdown.d.ts +0 -29
  127. package/dist/components/fileTree.d.ts +0 -79
  128. package/dist/components/lightbox.d.ts +0 -24
  129. package/dist/components/modalDepth.d.ts +0 -15
  130. package/dist/components/sidebar.d.ts +0 -42
  131. package/dist/composables/easing.d.ts +0 -46
  132. package/dist/composables/useAsyncTask.d.ts +0 -18
  133. package/dist/composables/useClipboard.d.ts +0 -46
  134. package/dist/composables/useFormDraft.d.ts +0 -67
  135. package/dist/composables/useGlass.d.ts +0 -9
  136. package/dist/composables/useHotkeys.d.ts +0 -99
  137. package/dist/composables/useInputFilter.d.ts +0 -14
  138. package/dist/composables/useMergedClass.d.ts +0 -42
  139. package/dist/composables/usePillTransition.d.ts +0 -21
  140. package/dist/composables/useReveal.d.ts +0 -26
  141. package/dist/composables/useRovingFocus.d.ts +0 -49
  142. package/dist/composables/useSettled.d.ts +0 -26
  143. package/dist/composables/useShift.d.ts +0 -35
  144. package/dist/composables/useTilt.d.ts +0 -23
  145. package/dist/composables/useToast.d.ts +0 -41
  146. package/dist/composables/useZoomPan.d.ts +0 -58
  147. package/dist/icons.d.ts +0 -1
  148. package/dist/index.d.ts +0 -160
  149. package/dist/layout/registry.d.ts +0 -26
  150. package/dist/layout/tree.d.ts +0 -75
  151. package/dist/layout/useLayout.d.ts +0 -39
  152. package/dist/theme/backdrop.d.ts +0 -9
  153. package/dist/theme/config.d.ts +0 -16
  154. package/dist/theme/define.d.ts +0 -12
  155. package/dist/theme/presets.d.ts +0 -20
  156. package/dist/theme/tokens.d.ts +0 -75
  157. package/dist/theme/useTheme.d.ts +0 -46
@@ -21,10 +21,11 @@ import {
21
21
  DialogRoot,
22
22
  DialogTitle,
23
23
  } from "reka-ui"
24
- import { computed, nextTick, ref, watch } from "vue"
24
+ import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue"
25
25
  import { useZoomPan } from "../composables/useZoomPan"
26
26
  import { fileNameOf, type LightboxItem } from "./lightbox"
27
27
  import LpIcon from "./LpIcon.vue"
28
+ import { useModalLayer } from "./modalDepth"
28
29
 
29
30
  const props = withDefaults(
30
31
  defineProps<{
@@ -67,6 +68,10 @@ const emit = defineEmits<{
67
68
  (e: "copy", item: LightboxItem, ok: boolean): void
68
69
  }>()
69
70
 
71
+ const { claim, release, layer } = useModalLayer()
72
+ watch(() => props.open, (open) => (open ? claim() : release()), { immediate: true })
73
+ onBeforeUnmount(release)
74
+
70
75
  defineSlots<{
71
76
  /** Replace the caption block under the image. */
72
77
  caption(props: { item: LightboxItem; index: number; total: number }): unknown
@@ -335,13 +340,14 @@ const ARROW =
335
340
  <template>
336
341
  <DialogRoot :open="open" @update:open="(v) => emit('update:open', v)">
337
342
  <DialogPortal>
338
- <!-- Sits on the lightbox rung, not the shared modal one: opening this
339
- from inside a dialog must cover that dialog, not tie with it. -->
340
343
  <DialogOverlay
341
- class="lp-scrim lp-scrim--deep fixed inset-0 z-(--z-lightbox-overlay) data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
344
+ class="lp-scrim fixed inset-0 data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
345
+ :class="{ 'lp-scrim--nested': layer.nested }"
346
+ :style="{ zIndex: layer.scrim }"
342
347
  />
343
348
  <DialogContent
344
- class="fixed inset-0 z-(--z-lightbox) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
349
+ class="fixed inset-0 flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none"
350
+ :style="{ zIndex: layer.panel }"
345
351
  @keydown="onKeydown"
346
352
  >
347
353
  <!-- reka wants both for a labelled dialog; the caption is the title. -->
@@ -232,7 +232,7 @@ watch(
232
232
  <DialogRoot :open="open" @update:open="(v) => $emit('update:open', v)">
233
233
  <DialogPortal>
234
234
  <DialogOverlay
235
- :class="{ 'lp-scrim--light': frosted }"
235
+ :class="{ 'lp-scrim--light': frosted, 'lp-scrim--nested': layer.nested }"
236
236
  class="lp-scrim fixed inset-0 data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
237
237
  :style="{ zIndex: layer.scrim }"
238
238
  />
@@ -302,7 +302,7 @@ watch(
302
302
  <div
303
303
  ref="panelRef"
304
304
  class="flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
305
- :class="[frosted ? 'lp-glass-overlay' : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
305
+ :class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
306
306
  :style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
307
307
  >
308
308
  <header v-if="title || $slots.title" class="flex shrink-0 items-start justify-between gap-4 p-5 pb-3">
@@ -0,0 +1,92 @@
1
+ export interface SearchableCommand {
2
+ label: string
3
+ description?: string
4
+ keywords?: string[]
5
+ group?: string
6
+ }
7
+
8
+ export interface CommandSearchTerms {
9
+ include: string[]
10
+ exclude: string[]
11
+ groups: string[]
12
+ }
13
+
14
+ const QUERY_TOKEN = /(-?)(?:(in):)?(?:"([^"]*)"|(\S+))/giu
15
+ const LATIN = "qwertyuiop[]asdfghjkl;'zxcvbnm,.`"
16
+ const CYRILLIC = "йцукенгшщзхъфывапролджэячсмитьбюё"
17
+ const SWAP = new Map<string, string>()
18
+
19
+ for (let i = 0; i < LATIN.length; i++) {
20
+ SWAP.set(LATIN[i]!, CYRILLIC[i]!)
21
+ SWAP.set(CYRILLIC[i]!, LATIN[i]!)
22
+ }
23
+
24
+ export function parseCommandQuery(value: string): CommandSearchTerms {
25
+ const out: CommandSearchTerms = { include: [], exclude: [], groups: [] }
26
+ QUERY_TOKEN.lastIndex = 0
27
+ for (let match; (match = QUERY_TOKEN.exec(value)); ) {
28
+ const text = (match[3] ?? match[4] ?? "").trim().toLowerCase()
29
+ if (!text) continue
30
+ if (match[2] && !match[1]) out.groups.push(text)
31
+ else if (match[1]) out.exclude.push(text)
32
+ else out.include.push(text)
33
+ }
34
+ return out
35
+ }
36
+
37
+ export function swapCommandTerms(terms: CommandSearchTerms): CommandSearchTerms {
38
+ const swap = (term: string) => [...term].map((ch) => SWAP.get(ch) ?? ch).join("")
39
+ return {
40
+ include: terms.include.map(swap),
41
+ exclude: terms.exclude.map(swap),
42
+ groups: terms.groups.map(swap),
43
+ }
44
+ }
45
+
46
+ function matches(command: SearchableCommand, term: string): boolean {
47
+ return (
48
+ command.label.toLowerCase().includes(term)
49
+ || (command.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(term))
50
+ || (command.description?.toLowerCase().includes(term) ?? false)
51
+ )
52
+ }
53
+
54
+ function withinOne(a: string, b: string): boolean {
55
+ if (Math.abs(a.length - b.length) > 1) return false
56
+ let i = 0
57
+ while (i < a.length && i < b.length && a[i] === b[i]) i++
58
+ if (i === a.length || i === b.length) return true
59
+ const rest = (x: number, y: number) => a.slice(i + x) === b.slice(i + y)
60
+ return rest(1, 1) || rest(1, 0) || rest(0, 1) || (a[i] === b[i + 1] && a[i + 1] === b[i] && rest(2, 2))
61
+ }
62
+
63
+ function nearWord(term: string, label: string): boolean {
64
+ if (term.length < 5) return false
65
+ return label
66
+ .split(/[^\p{L}\p{N}]+/u)
67
+ .some((word) => withinOne(term, word) || (word.length > term.length && withinOne(term, word.slice(0, term.length))))
68
+ }
69
+
70
+ function score(command: SearchableCommand, terms: CommandSearchTerms): number {
71
+ const label = command.label.toLowerCase()
72
+ const keywords = (command.keywords ?? []).map((keyword) => keyword.toLowerCase())
73
+ const description = command.description?.toLowerCase() ?? ""
74
+ const group = command.group?.toLowerCase() ?? ""
75
+ if (terms.exclude.some((term) => matches(command, term)) || terms.groups.some((term) => !group.includes(term))) return 0
76
+ let total = terms.include.length ? 0 : 1
77
+ for (const term of terms.include) {
78
+ let hit = (label.includes(term) ? 4 : 0) + (keywords.some((keyword) => keyword.includes(term)) ? 3 : 0) + (description.includes(term) ? 2 : 0)
79
+ if (!hit && nearWord(term, label)) hit = 1
80
+ if (!hit) return 0
81
+ total += hit
82
+ }
83
+ return total
84
+ }
85
+
86
+ export function rankCommandItems<T extends SearchableCommand>(items: T[], terms: CommandSearchTerms): T[] {
87
+ return items
88
+ .map((command) => ({ command, score: score(command, terms) }))
89
+ .filter((entry) => entry.score > 0)
90
+ .sort((a, b) => b.score - a.score || a.command.label.localeCompare(b.command.label))
91
+ .map((entry) => entry.command)
92
+ }
@@ -1,61 +1,17 @@
1
- /*
2
- * Stacking order for dialogs that are open at the same time.
3
- *
4
- * Not provide/inject, which was the first shape tried: it only works when the
5
- * second dialog is rendered INSIDE the first one's subtree, and the ordinary
6
- * way to write a dialog opened from a dialog is to put both at the top level of
7
- * the same component and toggle them with separate flags. Those two are
8
- * siblings, so the inner one injects nothing and lands on the same rungs as the
9
- * outer — which is the bug.
10
- *
11
- * What decides the order is the sequence of OPENING: a dialog opened while
12
- * another is up belongs above it, wherever it sits in the markup.
13
- *
14
- * The newest dialog keeps the scale's own --z-overlay/--z-modal and the ones
15
- * beneath are pushed DOWN, rather than each new dialog climbing higher. Climbing
16
- * runs out of room immediately: --z-popover is only 10 above --z-modal, so the
17
- * second nested dialog would tie with every popover and the third with every
18
- * tooltip — and a select inside a dialog is ordinary. Descending has no such
19
- * ceiling, and it keeps the frontmost dialog exactly where a single one has
20
- * always been.
21
- */
22
1
  import { computed, ref, type ComputedRef } from "vue"
23
2
 
24
- /*
25
- * Rungs per level. Has to clear a whole level, not just its scrim: --z-overlay
26
- * is 100 and --z-modal 110, so a gap of 2 left the upper scrim under the lower
27
- * PANEL — it darkened that panel and could not blur it, a backdrop-filter
28
- * having nothing of it underneath to work on.
29
- *
30
- * Clearing it means strictly more than the 10 between the two rungs. A step of
31
- * exactly 10 put the lower panel at 100, level with the upper scrim, and a tie
32
- * in z-index is settled by DOM order. Siblings in one component happened to
33
- * come out right, but a dialog mounted once at the app root — opened from a
34
- * page dialog that teleported in later — lost the tie, and its scrim darkened
35
- * the page behind the first dialog instead of the first dialog itself.
36
- */
37
3
  const STEP = 20
38
4
 
39
- /** Levels currently claimed, oldest first. */
40
5
  const stack = ref<number[]>([])
41
6
  let nextId = 0
42
7
 
43
- /**
44
- * Register a dialog as open and get back the z-indexes it should paint with.
45
- * The result is reactive: opening another dialog moves this one down a step.
46
- */
47
8
  export function useModalLayer(): {
48
9
  claim: () => void
49
10
  release: () => void
50
- layer: ComputedRef<{ scrim: string; panel: string }>
11
+ layer: ComputedRef<{ scrim: string; panel: string; nested: boolean }>
51
12
  } {
52
13
  const id = nextId++
53
14
 
54
- /*
55
- * How far from the TOP of the stack, so the newest dialog is 0 and keeps the
56
- * unmodified rungs. A dialog that is not in the stack reads 0 too: it is
57
- * either closed, or opening this very frame and about to claim.
58
- */
59
15
  const fromTop = computed(() => {
60
16
  const at = stack.value.indexOf(id)
61
17
  return at === -1 ? 0 : stack.value.length - 1 - at
@@ -71,14 +27,14 @@ export function useModalLayer(): {
71
27
  layer: computed(() => {
72
28
  const drop = fromTop.value * STEP
73
29
  return {
74
- scrim: `calc(var(--z-overlay) - ${drop})`,
75
- panel: `calc(var(--z-modal) - ${drop})`,
30
+ scrim: `calc(var(--z-toast) - 20 - ${drop})`,
31
+ panel: `calc(var(--z-toast) - 10 - ${drop})`,
32
+ nested: stack.value.indexOf(id) > 0,
76
33
  }
77
34
  }),
78
35
  }
79
36
  }
80
37
 
81
- /** True while any dialog or drawer holds a level — e.g. to keep a gesture from opening another one over it. */
82
38
  export function anyModalOpen(): boolean {
83
39
  return stack.value.length > 0
84
40
  }
@@ -142,6 +142,8 @@ function matches(event: KeyboardEvent, binding: HotkeyBinding): boolean {
142
142
  * formality.
143
143
  */
144
144
  export interface UseHotkeysOptions {
145
+ enabled?: MaybeRefOrGetter<boolean>
146
+ priority?: MaybeRefOrGetter<number>
145
147
  /**
146
148
  * Only act while the focus is inside this element.
147
149
  *
@@ -157,52 +159,70 @@ export interface UseHotkeysOptions {
157
159
  scope?: MaybeRefOrGetter<HTMLElement | null | undefined>
158
160
  }
159
161
 
160
- export function useHotkeys(
161
- bindings: MaybeRefOrGetter<HotkeyBinding[]>,
162
- options: UseHotkeysOptions = {},
163
- ) {
164
- function inScope(target: EventTarget | null): boolean {
165
- const el = toValue(options.scope)
166
- if (!el) return true
167
- const node = target instanceof Node ? target : null
168
- // The element itself counts: a focusable canvas holds the focus without
169
- // containing anything that could.
170
- return !!node && (el === node || el.contains(node))
171
- }
162
+ interface MountedHotkeys {
163
+ read: () => HotkeyBinding[]
164
+ scope: MaybeRefOrGetter<HTMLElement | null | undefined> | undefined
165
+ enabled: MaybeRefOrGetter<boolean> | undefined
166
+ priority: MaybeRefOrGetter<number> | undefined
167
+ order: number
168
+ }
172
169
 
173
- function onKeydown(event: KeyboardEvent) {
174
- if (event.isComposing) return
175
- if (!inScope(event.target)) return
176
- const inTextEntry = isTextEntry(event.target)
177
- for (const binding of toValue(bindings)) {
170
+ const mounted = shallowReactive(new Set<MountedHotkeys>())
171
+ let mountOrder = 0
172
+ let listening = false
173
+
174
+ function inScope(target: EventTarget | null, scope: MountedHotkeys["scope"]): boolean {
175
+ const el = toValue(scope)
176
+ if (!el) return true
177
+ const node = target instanceof Node ? target : null
178
+ return !!node && (el === node || el.contains(node))
179
+ }
180
+
181
+ function onKeydown(event: KeyboardEvent) {
182
+ if (event.isComposing) return
183
+ const inTextEntry = isTextEntry(event.target)
184
+ const entries = [...mounted].sort(
185
+ (a, b) => Number(toValue(b.priority) || 0) - Number(toValue(a.priority) || 0) || b.order - a.order,
186
+ )
187
+ for (const entry of entries) {
188
+ if (toValue(entry.enabled) === false || !inScope(event.target, entry.scope)) continue
189
+ for (const binding of entry.read()) {
178
190
  if (inTextEntry && !binding.allowInInput) continue
179
191
  if (!matches(event, binding)) continue
180
- // Claimed only once the handler has run and not declined, so a binding
181
- // that stands down leaves the key untouched for whoever handles it next.
182
192
  if (binding.handler(event) === false) continue
183
193
  event.preventDefault()
184
194
  return
185
195
  }
186
196
  }
197
+ }
187
198
 
188
- const read = () => toValue(bindings)
199
+ export function useHotkeys(
200
+ bindings: MaybeRefOrGetter<HotkeyBinding[]>,
201
+ options: UseHotkeysOptions = {},
202
+ ) {
203
+ const entry: MountedHotkeys = {
204
+ read: () => toValue(bindings),
205
+ scope: options.scope,
206
+ enabled: options.enabled,
207
+ priority: options.priority,
208
+ order: ++mountOrder,
209
+ }
189
210
  onMounted(() => {
190
- window.addEventListener("keydown", onKeydown)
191
- mounted.add(read)
211
+ mounted.add(entry)
212
+ if (!listening) {
213
+ window.addEventListener("keydown", onKeydown)
214
+ listening = true
215
+ }
192
216
  })
193
217
  onBeforeUnmount(() => {
194
- window.removeEventListener("keydown", onKeydown)
195
- mounted.delete(read)
218
+ mounted.delete(entry)
219
+ if (!mounted.size && listening) {
220
+ window.removeEventListener("keydown", onKeydown)
221
+ listening = false
222
+ }
196
223
  })
197
224
  }
198
225
 
199
- /*
200
- * Every mounted binding list, in mount order. The lists are getters, so the
201
- * reference below follows the state behind them too: a binding that only
202
- * exists while a mode is on shows up and goes away with that mode.
203
- */
204
- const mounted = shallowReactive(new Set<() => HotkeyBinding[]>())
205
-
206
226
  const KEY_NAMES: Record<string, string> = {
207
227
  escape: "Esc",
208
228
  enter: "Enter",
@@ -230,8 +250,9 @@ function comboOf(binding: HotkeyBinding): string {
230
250
  */
231
251
  export const activeShortcuts = computed<ShortcutGroup[]>(() => {
232
252
  const groups = new Map<string, ShortcutEntry[]>()
233
- for (const read of mounted) {
234
- for (const binding of read()) {
253
+ for (const entry of mounted) {
254
+ if (toValue(entry.enabled) === false) continue
255
+ for (const binding of entry.read()) {
235
256
  if (!binding.help) continue
236
257
  const combo = binding.help.combo ?? comboOf(binding)
237
258
  const entries = groups.get(binding.help.group) ?? []
package/src/index.ts CHANGED
@@ -65,6 +65,8 @@ export {
65
65
  export type { ColorFormat, Hsv, Rgb } from "./components/color"
66
66
  export { default as LpCommandPalette } from "./components/LpCommandPalette.vue"
67
67
  export type { Command, PaletteLabels } from "./components/LpCommandPalette.vue"
68
+ export { parseCommandQuery, rankCommandItems, swapCommandTerms } from "./components/commandSearch"
69
+ export type { CommandSearchTerms, SearchableCommand } from "./components/commandSearch"
68
70
  export { default as LpConfirmDialog } from "./components/LpConfirmDialog.vue"
69
71
  export { default as LpContextMenu } from "./components/LpContextMenu.vue"
70
72
  export type { ContextMenuItemDef } from "./components/LpContextMenu.vue"
@@ -171,14 +171,6 @@
171
171
  --z-tooltip: 130;
172
172
  --z-toast: 200;
173
173
 
174
- /* A lightbox is usually opened FROM a dialog — click a thumbnail in a modal
175
- and the full image comes up over it. Sharing --z-overlay/--z-modal left
176
- that order to DOM insertion alone, so the lightbox's own scrim landed on
177
- top of the modal panel and swallowed it while double-darkening the page
178
- behind. Its own rung above the modal (but below tooltip/toast, which must
179
- outrank everything) makes the stack explicit. */
180
- --z-lightbox-overlay: 140;
181
- --z-lightbox: 150;
182
174
  }
183
175
 
184
176
  /* ── overlay enter/exit animations ───────────────
@@ -511,17 +503,6 @@
511
503
  --scrim-opacity: 0.62;
512
504
  }
513
505
 
514
- /*
515
- * A lightbox is the one scrim that hides a whole page behind a photo, and it is
516
- * usually opened FROM a dialog — so it is the layer the mild shared tint serves
517
- * worst: without the blur, the content behind stayed readable around the image.
518
- * It gets its own value instead of raising the shared one, which would darken
519
- * every stacked pair.
520
- */
521
- :root[data-lp-no-backdrop] .lp-scrim--deep {
522
- --scrim-opacity: 0.78;
523
- }
524
-
525
506
  /*
526
507
  * Under a glass panel (.lp-glass-overlay) the usual tint leaves nothing but
527
508
  * grey to see through it, so the scrim dims less and the panel's own blur
@@ -532,6 +513,12 @@
532
513
  --scrim-opacity: 0.32;
533
514
  }
534
515
 
516
+ .lp-scrim--nested {
517
+ --scrim-opacity: 0.24;
518
+ -webkit-backdrop-filter: none;
519
+ backdrop-filter: none;
520
+ }
521
+
535
522
  /*
536
523
  * Frosted overlay: a floating panel that lets the page show through, softened.
537
524
  * For surfaces that float over everything and are read briefly (the command
@@ -550,6 +537,13 @@
550
537
  backdrop-filter: blur(var(--glass-overlay-blur, 28px)) saturate(170%);
551
538
  }
552
539
 
540
+ .lp-glass-overlay--nested {
541
+ background-color: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
542
+ background-image: none;
543
+ -webkit-backdrop-filter: none;
544
+ backdrop-filter: none;
545
+ }
546
+
553
547
  :root[data-lp-no-backdrop] .lp-glass-overlay {
554
548
  background-color: var(--color-surface-raised);
555
549
  background-image: none;
@@ -1 +0,0 @@
1
- export declare const COMPONENT_NAMES: readonly ["LayoutCanvas", "LayoutNode", "LpActionBar", "LpAddressInput", "LpAlert", "LpAppShell", "LpAutocomplete", "LpAvatar", "LpBadge", "LpBreadcrumbs", "LpButton", "LpCalendar", "LpCard", "LpChart", "LpCheckbox", "LpCodeBlock", "LpColorPicker", "LpCommandPalette", "LpConfirmDialog", "LpContextMenu", "LpDatePicker", "LpDisclosure", "LpDivider", "LpDrawer", "LpDropdownMenu", "LpDropzone", "LpEmptyState", "LpFileTree", "LpFileTreeNode", "LpFormField", "LpIcon", "LpImageEditor", "LpInfraNode", "LpInput", "LpLaneNode", "LpLightbox", "LpLink", "LpLogViewer", "LpMapPicker", "LpMediaGallery", "LpModal", "LpModalNav", "LpNotificationBell", "LpNumberField", "LpNumberFlow", "LpOtpInput", "LpPagination", "LpPasswordInput", "LpPhoneInput", "LpPie", "LpPopover", "LpProgress", "LpRadio", "LpRadioGroup", "LpRollingText", "LpScrollArea", "LpSection", "LpSegmented", "LpSelect", "LpServiceNode", "LpShift", "LpShortcutsDialog", "LpSidebar", "LpSidebarNav", "LpSkeleton", "LpSlider", "LpSparkline", "LpStat", "LpStepper", "LpSwitch", "LpTable", "LpTableOfContents", "LpTabs", "LpTextarea", "LpThemeSwitcher", "LpTilt", "LpTimeField", "LpToaster", "LpTooltip", "LpTopologyCanvas", "LpUptimeBar", "LpWaypointEdge"];
@@ -1,28 +0,0 @@
1
- import { Split } from '../layout/tree';
2
- import { BlockRegistry } from '../layout/registry';
3
- type __VLS_Props = {
4
- modelValue: Split;
5
- edit?: boolean;
6
- /** Catalogue mapping block ids to titles/icons for the tab bars. */
7
- registry?: BlockRegistry;
8
- };
9
- type __VLS_Slots = {
10
- block(props: {
11
- block: string;
12
- leafId: string;
13
- edit: boolean;
14
- }): unknown;
15
- };
16
- declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
17
- "update:modelValue": (value: Split) => any;
18
- }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
19
- "onUpdate:modelValue"?: ((value: Split) => any) | undefined;
20
- }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
21
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
22
- declare const _default: typeof __VLS_export;
23
- export default _default;
24
- type __VLS_WithSlots<T, S> = T & {
25
- new (): {
26
- $slots: S;
27
- };
28
- };
@@ -1,40 +0,0 @@
1
- import { LayoutNode, Side, Split } from '../layout/tree';
2
- import { BlockRegistry } from '../layout/registry';
3
- type __VLS_Props = {
4
- node: LayoutNode;
5
- dragId: string | null;
6
- edit: boolean;
7
- registry?: BlockRegistry;
8
- };
9
- type __VLS_Slots = {
10
- block(props: {
11
- block: string;
12
- leafId: string;
13
- edit: boolean;
14
- }): unknown;
15
- };
16
- declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
17
- dragstart: (id: string) => any;
18
- dragend: () => any;
19
- drop: (targetId: string, side: Side) => any;
20
- "set-active": (leafId: string, index: number) => any;
21
- "remove-block": (leafId: string, index: number) => any;
22
- "reorder-tab": (leafId: string, from: number, to: number) => any;
23
- resize: (parent: Split, index: number, deltaFraction: number) => any;
24
- }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
25
- onDragstart?: ((id: string) => any) | undefined;
26
- onDragend?: (() => any) | undefined;
27
- onDrop?: ((targetId: string, side: Side) => any) | undefined;
28
- "onSet-active"?: ((leafId: string, index: number) => any) | undefined;
29
- "onRemove-block"?: ((leafId: string, index: number) => any) | undefined;
30
- "onReorder-tab"?: ((leafId: string, from: number, to: number) => any) | undefined;
31
- onResize?: ((parent: Split, index: number, deltaFraction: number) => any) | undefined;
32
- }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
33
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
34
- declare const _default: typeof __VLS_export;
35
- export default _default;
36
- type __VLS_WithSlots<T, S> = T & {
37
- new (): {
38
- $slots: S;
39
- };
40
- };
@@ -1,84 +0,0 @@
1
- export interface ActionBarAction {
2
- label: string;
3
- /** Leading icon (any name LpIcon takes). */
4
- icon?: string;
5
- /** Mirrors LpButton's variants; the default reads as the primary action. */
6
- variant?: "solid" | "action" | "outline" | "soft" | "ghost" | "muted" | "light" | "danger";
7
- disabled?: boolean;
8
- /** Shows a spinner in place of the icon and blocks repeat clicks. */
9
- loading?: boolean;
10
- onClick?: () => void | Promise<void>;
11
- }
12
- type __VLS_Props = {
13
- /** Shown or not (v-model:open, or a plain :open). */
14
- open?: boolean;
15
- /** Lead line — "You have unsaved changes". */
16
- title?: string;
17
- /** Second line, quieter. Long explanations belong here, not in the title. */
18
- description?: string;
19
- /** Leading icon. Omit for a bar that is purely text + buttons. */
20
- icon?: string;
21
- /**
22
- * Buttons on the right, in reading order. The LAST one is the primary and
23
- * gets the solid treatment unless it says otherwise — the confirming action
24
- * sits closest to the edge the thumb and the eye finish on.
25
- */
26
- actions?: ActionBarAction[];
27
- /** Adds a "✕" that emits `dismiss`. For notices, not for decisions. */
28
- dismissible?: boolean;
29
- /**
30
- * Tone. `danger` for a destructive pending action; the default stays neutral
31
- * because most of these bars are routine and a page edged in red for every
32
- * unsaved field trains people to ignore red.
33
- */
34
- variant?: "default" | "danger";
35
- /**
36
- * How wide the bar runs.
37
- *
38
- * "contained" (the default) keeps it to the width of its own content, up to
39
- * a cap, and centres it: a bar carrying six words and two buttons has no
40
- * business spanning a 1920px display, and stretching it puts the buttons a
41
- * screen away from the field you were editing.
42
- *
43
- * "full" spans the viewport edge to edge — for a cookie notice, which reads
44
- * as part of the page frame rather than as a message about one thing on it.
45
- * Its CONTENTS are still capped and centred, so the buttons stay beside the
46
- * text instead of being flung to the far corner.
47
- */
48
- width?: "contained" | "full";
49
- /**
50
- * Cap for the bar (or, when `width="full"`, for the row inside it). Any CSS
51
- * length. Defaults to a comfortable measure for a line of text and two or
52
- * three buttons — wider for `full`, whose job is to span the page and whose
53
- * contents therefore sit on the page's own measure, not a card's.
54
- */
55
- maxWidth?: string;
56
- /** Accessible name for the landmark. Falls back to `title`. */
57
- ariaLabel?: string;
58
- };
59
- declare var __VLS_18: {}, __VLS_20: {};
60
- type __VLS_Slots = {} & {
61
- default?: (props: typeof __VLS_18) => any;
62
- } & {
63
- actions?: (props: typeof __VLS_20) => any;
64
- };
65
- declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
66
- "update:open": (value: boolean) => any;
67
- dismiss: () => any;
68
- }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
69
- "onUpdate:open"?: ((value: boolean) => any) | undefined;
70
- onDismiss?: (() => any) | undefined;
71
- }>, {
72
- width: "contained" | "full";
73
- open: boolean;
74
- variant: "default" | "danger";
75
- actions: ActionBarAction[];
76
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
77
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
78
- declare const _default: typeof __VLS_export;
79
- export default _default;
80
- type __VLS_WithSlots<T, S> = T & {
81
- new (): {
82
- $slots: S;
83
- };
84
- };
@@ -1,70 +0,0 @@
1
- import { AddressLabels, AddressValue, GeocodeProvider, LatLon, TileSource } from './address';
2
- declare const _default: typeof __VLS_export;
3
- export default _default;
4
- declare const __VLS_export: __VLS_WithSlots<import('vue').DefineComponent<{
5
- /** The address. Bind with v-model. */
6
- modelValue?: AddressValue;
7
- /** Geocoder for suggestions and for the map's reverse lookup. */
8
- provider?: GeocodeProvider;
9
- /** Tile server handed to the built-in picker. */
10
- tiles?: TileSource;
11
- /** Restrict the country list (ISO2). Omit for every country. */
12
- only?: string[];
13
- /** Show the "apartment, suite" line. */
14
- secondLine?: boolean;
15
- /** Suggestions start after this many characters. */
16
- minChars?: number;
17
- /** Debounce before a suggestion request goes out. */
18
- debounce?: number;
19
- /** Accept-Language for the geocoder. */
20
- language?: string;
21
- disabled?: boolean;
22
- size?: "sm" | "md" | "lg";
23
- /** Hide the map button when picking a point makes no sense. */
24
- hideMap?: boolean;
25
- labels?: Partial<AddressLabels>;
26
- }, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
27
- "update:modelValue": (value: AddressValue) => any;
28
- pick: (value: LatLon) => any;
29
- }, string, import('vue').PublicProps, Readonly<{
30
- /** The address. Bind with v-model. */
31
- modelValue?: AddressValue;
32
- /** Geocoder for suggestions and for the map's reverse lookup. */
33
- provider?: GeocodeProvider;
34
- /** Tile server handed to the built-in picker. */
35
- tiles?: TileSource;
36
- /** Restrict the country list (ISO2). Omit for every country. */
37
- only?: string[];
38
- /** Show the "apartment, suite" line. */
39
- secondLine?: boolean;
40
- /** Suggestions start after this many characters. */
41
- minChars?: number;
42
- /** Debounce before a suggestion request goes out. */
43
- debounce?: number;
44
- /** Accept-Language for the geocoder. */
45
- language?: string;
46
- disabled?: boolean;
47
- size?: "sm" | "md" | "lg";
48
- /** Hide the map button when picking a point makes no sense. */
49
- hideMap?: boolean;
50
- labels?: Partial<AddressLabels>;
51
- }> & Readonly<{
52
- "onUpdate:modelValue"?: ((value: AddressValue) => any) | undefined;
53
- onPick?: ((value: LatLon) => any) | undefined;
54
- }>, {
55
- size: "sm" | "md" | "lg";
56
- minChars: number;
57
- tiles: TileSource;
58
- debounce: number;
59
- }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>, {
60
- map?: (props: {
61
- point: LatLon | undefined;
62
- setPoint: (next: LatLon) => Promise<void>;
63
- tiles: TileSource;
64
- }) => any;
65
- }>;
66
- type __VLS_WithSlots<T, S> = T & {
67
- new (): {
68
- $slots: S;
69
- };
70
- };