@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
@@ -0,0 +1,66 @@
1
+ const QUERY_TOKEN = /(-?)(?:(in):)?(?:"([^"]*)"|(\S+))/giu;
2
+ const LATIN = "qwertyuiop[]asdfghjkl;'zxcvbnm,.`";
3
+ const CYRILLIC = "йцукенгшщзхъфывапролджэячсмитьбюё";
4
+ const SWAP = /* @__PURE__ */ new Map();
5
+ for (let i = 0; i < LATIN.length; i++) {
6
+ SWAP.set(LATIN[i], CYRILLIC[i]);
7
+ SWAP.set(CYRILLIC[i], LATIN[i]);
8
+ }
9
+ function parseCommandQuery(value) {
10
+ const out = { include: [], exclude: [], groups: [] };
11
+ QUERY_TOKEN.lastIndex = 0;
12
+ for (let match; match = QUERY_TOKEN.exec(value); ) {
13
+ const text = (match[3] ?? match[4] ?? "").trim().toLowerCase();
14
+ if (!text) continue;
15
+ if (match[2] && !match[1]) out.groups.push(text);
16
+ else if (match[1]) out.exclude.push(text);
17
+ else out.include.push(text);
18
+ }
19
+ return out;
20
+ }
21
+ function swapCommandTerms(terms) {
22
+ const swap = (term) => [...term].map((ch) => SWAP.get(ch) ?? ch).join("");
23
+ return {
24
+ include: terms.include.map(swap),
25
+ exclude: terms.exclude.map(swap),
26
+ groups: terms.groups.map(swap)
27
+ };
28
+ }
29
+ function matches(command, term) {
30
+ return command.label.toLowerCase().includes(term) || (command.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(term)) || (command.description?.toLowerCase().includes(term) ?? false);
31
+ }
32
+ function withinOne(a, b) {
33
+ if (Math.abs(a.length - b.length) > 1) return false;
34
+ let i = 0;
35
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
36
+ if (i === a.length || i === b.length) return true;
37
+ const rest = (x, y) => a.slice(i + x) === b.slice(i + y);
38
+ return rest(1, 1) || rest(1, 0) || rest(0, 1) || a[i] === b[i + 1] && a[i + 1] === b[i] && rest(2, 2);
39
+ }
40
+ function nearWord(term, label) {
41
+ if (term.length < 5) return false;
42
+ return label.split(/[^\p{L}\p{N}]+/u).some((word) => withinOne(term, word) || word.length > term.length && withinOne(term, word.slice(0, term.length)));
43
+ }
44
+ function score(command, terms) {
45
+ const label = command.label.toLowerCase();
46
+ const keywords = (command.keywords ?? []).map((keyword) => keyword.toLowerCase());
47
+ const description = command.description?.toLowerCase() ?? "";
48
+ const group = command.group?.toLowerCase() ?? "";
49
+ if (terms.exclude.some((term) => matches(command, term)) || terms.groups.some((term) => !group.includes(term))) return 0;
50
+ let total = terms.include.length ? 0 : 1;
51
+ for (const term of terms.include) {
52
+ let hit = (label.includes(term) ? 4 : 0) + (keywords.some((keyword) => keyword.includes(term)) ? 3 : 0) + (description.includes(term) ? 2 : 0);
53
+ if (!hit && nearWord(term, label)) hit = 1;
54
+ if (!hit) return 0;
55
+ total += hit;
56
+ }
57
+ return total;
58
+ }
59
+ function rankCommandItems(items, terms) {
60
+ return items.map((command) => ({ command, score: score(command, terms) })).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.command.label.localeCompare(b.command.label)).map((entry) => entry.command);
61
+ }
62
+ export {
63
+ parseCommandQuery,
64
+ rankCommandItems,
65
+ swapCommandTerms
66
+ };
@@ -18,8 +18,9 @@ function useModalLayer() {
18
18
  layer: computed(() => {
19
19
  const drop = fromTop.value * STEP;
20
20
  return {
21
- scrim: `calc(var(--z-overlay) - ${drop})`,
22
- panel: `calc(var(--z-modal) - ${drop})`
21
+ scrim: `calc(var(--z-toast) - 20 - ${drop})`,
22
+ panel: `calc(var(--z-toast) - 10 - ${drop})`,
23
+ nested: stack.value.indexOf(id) > 0
23
24
  };
24
25
  })
25
26
  };
@@ -1,4 +1,4 @@
1
- import { shallowReactive, computed, onMounted, onBeforeUnmount, toValue } from "vue";
1
+ import { shallowReactive, computed, toValue, onMounted, onBeforeUnmount } from "vue";
2
2
  const isAppleKeyboard = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || "");
3
3
  function appleModifierLabel(key) {
4
4
  if (!isAppleKeyboard) return key;
@@ -31,18 +31,24 @@ function matches(event, binding) {
31
31
  if (event.altKey !== Boolean(binding.alt)) return false;
32
32
  return event.key.toLowerCase() === binding.key.toLowerCase() || binding.code !== void 0 && event.code === binding.code;
33
33
  }
34
- function useHotkeys(bindings, options = {}) {
35
- function inScope(target) {
36
- const el = toValue(options.scope);
37
- if (!el) return true;
38
- const node = target instanceof Node ? target : null;
39
- return !!node && (el === node || el.contains(node));
40
- }
41
- function onKeydown(event) {
42
- if (event.isComposing) return;
43
- if (!inScope(event.target)) return;
44
- const inTextEntry = isTextEntry(event.target);
45
- for (const binding of toValue(bindings)) {
34
+ const mounted = shallowReactive(/* @__PURE__ */ new Set());
35
+ let mountOrder = 0;
36
+ let listening = false;
37
+ function inScope(target, scope) {
38
+ const el = toValue(scope);
39
+ if (!el) return true;
40
+ const node = target instanceof Node ? target : null;
41
+ return !!node && (el === node || el.contains(node));
42
+ }
43
+ function onKeydown(event) {
44
+ if (event.isComposing) return;
45
+ const inTextEntry = isTextEntry(event.target);
46
+ const entries = [...mounted].sort(
47
+ (a, b) => Number(toValue(b.priority) || 0) - Number(toValue(a.priority) || 0) || b.order - a.order
48
+ );
49
+ for (const entry of entries) {
50
+ if (toValue(entry.enabled) === false || !inScope(event.target, entry.scope)) continue;
51
+ for (const binding of entry.read()) {
46
52
  if (inTextEntry && !binding.allowInInput) continue;
47
53
  if (!matches(event, binding)) continue;
48
54
  if (binding.handler(event) === false) continue;
@@ -50,17 +56,30 @@ function useHotkeys(bindings, options = {}) {
50
56
  return;
51
57
  }
52
58
  }
53
- const read = () => toValue(bindings);
59
+ }
60
+ function useHotkeys(bindings, options = {}) {
61
+ const entry = {
62
+ read: () => toValue(bindings),
63
+ scope: options.scope,
64
+ enabled: options.enabled,
65
+ priority: options.priority,
66
+ order: ++mountOrder
67
+ };
54
68
  onMounted(() => {
55
- window.addEventListener("keydown", onKeydown);
56
- mounted.add(read);
69
+ mounted.add(entry);
70
+ if (!listening) {
71
+ window.addEventListener("keydown", onKeydown);
72
+ listening = true;
73
+ }
57
74
  });
58
75
  onBeforeUnmount(() => {
59
- window.removeEventListener("keydown", onKeydown);
60
- mounted.delete(read);
76
+ mounted.delete(entry);
77
+ if (!mounted.size && listening) {
78
+ window.removeEventListener("keydown", onKeydown);
79
+ listening = false;
80
+ }
61
81
  });
62
82
  }
63
- const mounted = shallowReactive(/* @__PURE__ */ new Set());
64
83
  const KEY_NAMES = {
65
84
  escape: "Esc",
66
85
  enter: "Enter",
@@ -80,8 +99,9 @@ function comboOf(binding) {
80
99
  }
81
100
  const activeShortcuts = computed(() => {
82
101
  const groups = /* @__PURE__ */ new Map();
83
- for (const read of mounted) {
84
- for (const binding of read()) {
102
+ for (const entry of mounted) {
103
+ if (toValue(entry.enabled) === false) continue;
104
+ for (const binding of entry.read()) {
85
105
  if (!binding.help) continue;
86
106
  const combo = binding.help.combo ?? comboOf(binding);
87
107
  const entries = groups.get(binding.help.group) ?? [];
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
2
- import { _ as _2 } from "./chunks/LpAddressInput-CMjaPQVw.js";
2
+ import { _ as _2 } from "./chunks/LpAddressInput-B-Zvxsn7.js";
3
3
  import { _ as _3 } from "./chunks/LpMapPicker-ul4oYno6.js";
4
4
  import { OSM_TILES, createNominatimProvider, emptyAddress, formatAddress, project, tileUrl, unproject } from "./components/address.js";
5
5
  import { _ as _4 } from "./chunks/LpActionBar-BlvtGB2P.js";
6
6
  import { _ as _5 } from "./chunks/LpAlert-BAYHDSS1.js";
7
- import { _ as _6 } from "./chunks/LpAppShell-Bcv6EhqU.js";
7
+ import { _ as _6 } from "./chunks/LpAppShell-BGQVIY-3.js";
8
8
  import { _ as _7 } from "./chunks/LpAutocomplete-DExlIZe_.js";
9
9
  import { _ as _8 } from "./chunks/LpAvatar-B3-xAOH3.js";
10
10
  import { _ as _9 } from "./chunks/LpBadge-Fe0D1bYx.js";
@@ -19,12 +19,13 @@ import { _ as _15 } from "./chunks/LpCodeBlock-Bo0B4TFR.js";
19
19
  import { _ as _16 } from "./chunks/LpColorPicker-BlXea7LM.js";
20
20
  import { DEFAULT_SWATCHES, clampHsv, formatColor, hsvToHsl, hsvToRgb, isLight, normalizeHue, opaqueCss, parseColor, parseHex, rgbToHex, rgbToHsv } from "./components/color.js";
21
21
  import { default as default3 } from "./components/LpCommandPalette.vue.js";
22
- import { _ as _17 } from "./chunks/LpConfirmDialog-BNAvOR24.js";
22
+ import { parseCommandQuery, rankCommandItems, swapCommandTerms } from "./components/commandSearch.js";
23
+ import { _ as _17 } from "./chunks/LpConfirmDialog-Q-bxff8z.js";
23
24
  import { _ as _18 } from "./chunks/LpContextMenu-CwW1QXe_.js";
24
25
  import { _ as _19 } from "./chunks/LpDatePicker-CwOToxGq.js";
25
26
  import { _ as _20 } from "./chunks/LpDisclosure-ChQYmjlR.js";
26
27
  import { _ as _21 } from "./chunks/LpDivider-CUX46KR0.js";
27
- import { _ as _22 } from "./chunks/LpDrawer-DgaS2s2i.js";
28
+ import { _ as _22 } from "./chunks/LpDrawer-BZ6k3hvo.js";
28
29
  import { _ as _23 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
29
30
  import { default as default4 } from "./components/LpDropzone.vue.js";
30
31
  import { _ as _24 } from "./chunks/LpEmptyState-Dsdqz1Wj.js";
@@ -35,13 +36,13 @@ import { _ as _27 } from "./chunks/LpIcon-CCnX5_2j.js";
35
36
  import { default as default5 } from "./components/LpImageEditor.vue.js";
36
37
  import { _ as _28 } from "./chunks/LpInfraNode-Ds_8SZgN.js";
37
38
  import { _ as _29 } from "./chunks/LpInput-9EXWUM9B.js";
38
- import { _ as _30 } from "./chunks/LpLightbox-oLqyjOln.js";
39
+ import { _ as _30 } from "./chunks/LpLightbox-2sYVMY-S.js";
39
40
  import { fileNameOf } from "./components/lightbox.js";
40
41
  import { _ as _31 } from "./chunks/LpLink-DCND-zyu.js";
41
42
  import { _ as _32 } from "./chunks/LpLogViewer-CaRy5MeS.js";
42
- import { _ as _33 } from "./chunks/LpMediaGallery-Cid7PGYx.js";
43
- import { _ as _34 } from "./chunks/LpModal-8bja5bWS.js";
44
- import { _ as _35 } from "./chunks/LpModalNav-w9kfwHqO.js";
43
+ import { _ as _33 } from "./chunks/LpMediaGallery-FF-NquIo.js";
44
+ import { _ as _34 } from "./chunks/LpModal-Bt0pQCI_.js";
45
+ import { _ as _35 } from "./chunks/LpModalNav-YaifHrXC.js";
45
46
  import { _ as _36 } from "./chunks/LpNotificationBell-DyGGsMAM.js";
46
47
  import { _ as _37 } from "./chunks/LpNumberField-CWuX08iX.js";
47
48
  import { _ as _38 } from "./chunks/LpTimeField-K6nAGboX.js";
@@ -64,7 +65,7 @@ import { _ as _53 } from "./chunks/LpSelect-ANIVtrpM.js";
64
65
  import { _ as _54 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
65
66
  import { _ as _55 } from "./chunks/LpShift-DXNsauQZ.js";
66
67
  import { default as default6 } from "./components/LpShortcutsDialog.vue.js";
67
- import { _ as _56 } from "./chunks/LpSidebar-DlTn5nXc.js";
68
+ import { _ as _56 } from "./chunks/LpSidebar-ClBUwoEz.js";
68
69
  import { _ as _57 } from "./chunks/LpSkeleton-CTI-vBgW.js";
69
70
  import { _ as _58 } from "./chunks/LpSlider-BwNIBkAK.js";
70
71
  import { _ as _59 } from "./chunks/LpSparkline-C6xQ2dwv.js";
@@ -270,6 +271,7 @@ export {
270
271
  normalizeHue,
271
272
  opaqueCss,
272
273
  parseColor,
274
+ parseCommandQuery,
273
275
  parseConfig,
274
276
  parseHex,
275
277
  parseTheme,
@@ -277,6 +279,7 @@ export {
277
279
  presets,
278
280
  project,
279
281
  provideGlass,
282
+ rankCommandItems,
280
283
  removeBlock,
281
284
  removeLeaf,
282
285
  reorderTab,
@@ -292,6 +295,7 @@ export {
292
295
  sortNodes,
293
296
  subtreeIds,
294
297
  subtreeSize,
298
+ swapCommandTerms,
295
299
  t as themeFromCookie,
296
300
  k as themeToCssRule,
297
301
  m as themeToCssVars,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leavepulse/ui",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "type": "module",
5
5
  "description": "LeavePulse token-driven Vue component kit.",
6
6
  "repository": {
@@ -45,10 +45,11 @@
45
45
  "scripts": {
46
46
  "dev": "vite",
47
47
  "gen:names": "node scripts/gen-component-names.mjs",
48
- "build": "node scripts/gen-component-names.mjs && vue-tsc --noEmit -p tsconfig.build.json && vite build --config vite.lib.config.ts",
48
+ "build": "node scripts/gen-component-names.mjs && vue-tsc --noEmit -p tsconfig.build.json && vite build --config vite.lib.config.ts && vue-tsc --declaration --emitDeclarationOnly --noEmit false -p tsconfig.build.json --outDir dist",
49
49
  "dev:dist": "vite build --watch --config vite.lib.config.ts",
50
50
  "prepublishOnly": "node scripts/gen-component-names.mjs && vue-tsc --noEmit -p tsconfig.build.json && vite build --config vite.lib.config.ts",
51
51
  "typecheck": "vue-tsc --noEmit",
52
+ "test": "bun test",
52
53
  "release": "npm version patch -m \"chore: release v%s\" && git push --follow-tags",
53
54
  "release:minor": "npm version minor -m \"chore: release v%s\" && git push --follow-tags",
54
55
  "release:major": "npm version major -m \"chore: release v%s\" && git push --follow-tags"
@@ -83,7 +84,6 @@
83
84
  "tailwindcss": "^4.2.4",
84
85
  "typescript": "^5.6.3",
85
86
  "vite": "^7.0.0",
86
- "vite-plugin-dts": "^5.0.2",
87
87
  "vite-plugin-lib-inject-css": "^2.2.2",
88
88
  "vue": "3.5.31",
89
89
  "vue-tsc": "^3.0.0"
@@ -36,13 +36,15 @@ import {
36
36
  DialogRoot,
37
37
  DialogTitle,
38
38
  } from "reka-ui"
39
- import { computed, nextTick, ref, shallowRef, useId, watch } from "vue"
39
+ import { computed, nextTick, onBeforeUnmount, ref, shallowRef, useId, watch } from "vue"
40
40
  import LpIcon from "./LpIcon.vue"
41
41
  import LpScrollArea from "./LpScrollArea.vue"
42
42
  import { useAsyncTask } from "../composables/useAsyncTask"
43
43
  import { useGlass } from "../composables/useGlass"
44
44
  import { useClipboard } from "../composables/useClipboard"
45
45
  import { useHotkeys } from "../composables/useHotkeys"
46
+ import { useModalLayer } from "./modalDepth"
47
+ import { parseCommandQuery, rankCommandItems, swapCommandTerms } from "./commandSearch"
46
48
 
47
49
  export interface Command {
48
50
  id: string
@@ -234,6 +236,7 @@ const props = withDefaults(
234
236
  )
235
237
 
236
238
  const frosted = useGlass(() => props.glass)
239
+ const { claim, release, layer } = useModalLayer()
237
240
 
238
241
  const DEFAULT_LABELS: PaletteLabels = {
239
242
  navigate: "navigate",
@@ -276,6 +279,7 @@ function optionId(index: number): string {
276
279
  }
277
280
  const activeId = computed(() => (flat.value.length ? optionId(selectedIndex.value) : undefined))
278
281
  const selectedIndex = ref(0)
282
+ const selectedId = ref<string | null>(null)
279
283
  const inputRef = ref<HTMLInputElement | null>(null)
280
284
  const scrollRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
281
285
 
@@ -290,6 +294,7 @@ const scrollRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
290
294
  * a row of data), and deep-tracking them would walk it on every keystroke.
291
295
  */
292
296
  const trail = shallowRef<Command[]>([])
297
+ const trailState = shallowRef<{ query: string; selectedId: string | null }[]>([])
293
298
  const mode = computed<Command | null>(() => trail.value[trail.value.length - 1] ?? null)
294
299
 
295
300
  /*
@@ -332,21 +337,20 @@ const selectedActions = computed(() => actionsOf(flat.value[selectedIndex.value]
332
337
  * actions it keeps that default.
333
338
  */
334
339
  function onTab(event: KeyboardEvent) {
340
+ event.preventDefault()
341
+ event.stopPropagation()
335
342
  if (event.shiftKey) return
336
343
  const c = flat.value[selectedIndex.value]
337
344
  const actions = selectedActions.value
338
345
  if (!c || !actions.length) return
339
- // The dialog's focus trap handles Tab too, and would hand focus to the
340
- // mode chip this is about to render.
341
- event.preventDefault()
342
- event.stopPropagation()
343
346
  enter({ id: `${c.id}:actions`, label: c.label, icon: c.icon, children: actions })
344
347
  }
345
348
 
346
349
  function enter(c: Command) {
350
+ trailState.value = [...trailState.value, { query: query.value, selectedId: selectedId.value }]
347
351
  trail.value = [...trail.value, c]
348
352
  query.value = ""
349
- selectedIndex.value = 0
353
+ select(0)
350
354
  modeItems.value = Array.isArray(c.children) ? c.children : []
351
355
  refresh()
352
356
  // Entering by click leaves the focus on the button that no longer exists,
@@ -357,13 +361,19 @@ function enter(c: Command) {
357
361
 
358
362
  /** Unwind the trail to `depth` entries — 0 is the root. */
359
363
  function popTo(depth: number) {
364
+ const state = trailState.value[depth] || { query: "", selectedId: null }
360
365
  const next = trail.value.slice(0, Math.max(0, depth))
366
+ trailState.value = trailState.value.slice(0, Math.max(0, depth))
361
367
  trail.value = next
362
- query.value = ""
363
- selectedIndex.value = 0
364
368
  const parent = next[next.length - 1]
365
369
  modeItems.value = parent && Array.isArray(parent.children) ? parent.children : []
370
+ query.value = state.query
371
+ select(0)
366
372
  refresh()
373
+ void nextTick(() => {
374
+ const index = state.selectedId ? flat.value.findIndex((c) => c.id === state.selectedId) : -1
375
+ select(index >= 0 ? index : 0)
376
+ })
367
377
  inputRef.value?.focus()
368
378
  }
369
379
 
@@ -446,88 +456,23 @@ const source = computed<Command[]>(() => (mode.value ? modeItems.value : props.c
446
456
  * the one bilingual setup common enough to be worth a table — and it is a
447
457
  * fallback, tried only when the query as typed matches nothing.
448
458
  */
449
- const LATIN = "qwertyuiop[]asdfghjkl;'zxcvbnm,.`"
450
- const CYRILLIC = "йцукенгшщзхъфывапролджэячсмитьбюё"
451
- const SWAP = new Map<string, string>()
452
- for (let i = 0; i < LATIN.length; i++) {
453
- SWAP.set(LATIN[i]!, CYRILLIC[i]!)
454
- SWAP.set(CYRILLIC[i]!, LATIN[i]!)
455
- }
456
-
457
- function swapLayout(term: string): string {
458
- return [...term].map((ch) => SWAP.get(ch) ?? ch).join("")
459
- }
460
-
461
- /** Damerau-Levenshtein distance, giving up past 1: all a typo check needs. */
462
- function withinOne(a: string, b: string): boolean {
463
- if (Math.abs(a.length - b.length) > 1) return false
464
- let i = 0
465
- while (i < a.length && i < b.length && a[i] === b[i]) i++
466
- if (i === a.length || i === b.length) return true
467
- const rest = (x: number, y: number) => a.slice(i + x) === b.slice(i + y)
468
- return rest(1, 1) || rest(1, 0) || rest(0, 1) || (a[i] === b[i + 1] && a[i + 1] === b[i] && rest(2, 2))
469
- }
470
-
471
- /*
472
- * A typo in a word long enough to have one: a slip in a three-letter word
473
- * makes it a different word. Compared against the word and its start, so
474
- * "deplyo" finds "deploy" and "deployment" alike.
475
- */
476
- const TYPO_MIN = 5
477
- function nearWord(term: string, label: string): boolean {
478
- if (term.length < TYPO_MIN) return false
479
- return label
480
- .split(/[^\p{L}\p{N}]+/u)
481
- .some((word) => withinOne(term, word) || (word.length > term.length && withinOne(term, word.slice(0, term.length))))
482
- }
483
-
484
- /*
485
- * Every word of the query has to land somewhere, in any order: "branch deploy"
486
- * finds "Deploy current branch". Per word, label > keywords > description,
487
- * the same weights the palette always had; a typo match ranks below all of
488
- * them, so an exact hit is never outranked by a near one.
489
- */
490
- function score(c: Command, terms: string[]): number {
491
- const label = c.label.toLowerCase()
492
- const keywords = (c.keywords ?? []).map((k) => k.toLowerCase())
493
- const description = c.description?.toLowerCase() ?? ""
494
- let total = 0
495
- for (const term of terms) {
496
- let hit =
497
- (label.includes(term) ? 4 : 0) +
498
- (keywords.some((k) => k.includes(term)) ? 3 : 0) +
499
- (description.includes(term) ? 2 : 0)
500
- if (!hit && nearWord(term, label)) hit = 1
501
- if (!hit) return 0
502
- total += hit
503
- }
504
- return total
505
- }
506
-
507
- function rank(items: Command[], terms: string[]): Command[] {
508
- return items
509
- .map((c) => ({ c, score: score(c, terms) }))
510
- .filter((e) => e.score > 0)
511
- .sort((a, b) => b.score - a.score || a.c.label.localeCompare(b.c.label))
512
- .map((e) => e.c)
513
- }
514
-
515
- const typedTerms = computed(() => query.value.trim().toLowerCase().split(/\s+/).filter(Boolean))
516
-
517
459
  /*
518
460
  * The local match and the terms it matched with — the swapped ones when the
519
461
  * layout fallback is what found it, so the highlight marks what actually hit.
520
462
  */
521
463
  const local = computed<{ items: Command[]; terms: string[] }>(() => {
522
- const terms = typedTerms.value
464
+ const terms = parseCommandQuery(query.value)
465
+ const filtered = query.value.trim()
466
+ ? source.value
467
+ : source.value.filter((c) => !c.hidden)
523
468
  // Empty query: show everything EXCEPT hidden commands (they only surface once
524
469
  // typed). With a query, hidden commands rank like any other.
525
- if (!terms.length) return { items: source.value.filter((c) => !c.hidden), terms }
526
- const items = rank(source.value, terms)
527
- if (items.length || !props.layoutFallback) return { items, terms }
528
- const swapped = terms.map(swapLayout)
529
- const other = rank(source.value, swapped)
530
- return other.length ? { items: other, terms: swapped } : { items, terms }
470
+ if (!terms.include.length && !terms.exclude.length && !terms.groups.length) return { items: filtered, terms: [] }
471
+ const items = rankCommandItems(filtered, terms)
472
+ if (items.length || !props.layoutFallback) return { items, terms: terms.include }
473
+ const swapped = swapCommandTerms(terms)
474
+ const other = rankCommandItems(filtered, swapped)
475
+ return other.length ? { items: other, terms: swapped.include } : { items, terms: terms.include }
531
476
  })
532
477
 
533
478
  const ranked = computed<Command[]>(() => local.value.items)
@@ -706,16 +651,17 @@ function highlight(value: string, extra: string[] = []): Part[] {
706
651
 
707
652
  // ── selection + keyboard ─────────────────────────────────────
708
653
  watch(query, () => {
709
- selectedIndex.value = 0
654
+ select(0)
710
655
  refresh()
711
656
  })
712
657
  watch([query, trail, () => props.open], () => {
713
658
  expanded.value = new Set()
714
659
  })
715
660
  watch(
716
- () => flat.value.length,
717
- (len) => {
718
- if (selectedIndex.value >= len) selectedIndex.value = Math.max(0, len - 1)
661
+ () => flat.value.map((c) => c.id).join("|"),
662
+ () => {
663
+ const index = selectedId.value ? flat.value.findIndex((c) => c.id === selectedId.value) : -1
664
+ select(index >= 0 ? index : 0)
719
665
  },
720
666
  )
721
667
  watch([selectedIndex, () => flat.value.map((c) => c.id).join("|")], async () => {
@@ -730,13 +676,42 @@ function scrollSelectedIntoView() {
730
676
  el?.scrollIntoView({ block: "nearest" })
731
677
  }
732
678
 
679
+ const keyboardSelection = ref(false)
680
+
681
+ function select(index: number, keyboard = false) {
682
+ const len = flat.value.length
683
+ selectedIndex.value = len ? Math.max(0, Math.min(index, len - 1)) : 0
684
+ selectedId.value = flat.value[selectedIndex.value]?.id ?? null
685
+ if (keyboard) keyboardSelection.value = true
686
+ }
687
+
733
688
  function moveDown() {
734
689
  const len = flat.value.length
735
- if (len) selectedIndex.value = (selectedIndex.value + 1) % len
690
+ if (len) select((selectedIndex.value + 1) % len, true)
736
691
  }
737
692
  function moveUp() {
738
693
  const len = flat.value.length
739
- if (len) selectedIndex.value = (selectedIndex.value - 1 + len) % len
694
+ if (len) select((selectedIndex.value - 1 + len) % len, true)
695
+ }
696
+ function movePage(direction: -1 | 1) {
697
+ select(selectedIndex.value + direction * 8, true)
698
+ }
699
+ function onPaletteKeydown(event: KeyboardEvent) {
700
+ if (event.isComposing) return
701
+ if (!(["ArrowDown", "ArrowUp", "Home", "End", "PageDown", "PageUp"] as string[]).includes(event.key)) return
702
+ event.preventDefault()
703
+ event.stopPropagation()
704
+ if (event.key === "ArrowDown") moveDown()
705
+ else if (event.key === "ArrowUp") moveUp()
706
+ else if (event.key === "Home") select(0, true)
707
+ else if (event.key === "End") select(flat.value.length - 1, true)
708
+ else movePage(event.key === "PageDown" ? 1 : -1)
709
+ }
710
+ function onPointerMove() {
711
+ keyboardSelection.value = false
712
+ }
713
+ function onPointerEnter(c: Command) {
714
+ if (!keyboardSelection.value) select(flatIndexOf(c))
740
715
  }
741
716
  function runSelected(event?: KeyboardEvent) {
742
717
  const c = flat.value[selectedIndex.value]
@@ -789,7 +764,7 @@ function run(c: Command | undefined) {
789
764
  // Back to the top of a fresh list: the query that found this command is
790
765
  // spent, and leaving it would hide whatever the action just changed.
791
766
  query.value = ""
792
- selectedIndex.value = 0
767
+ select(0)
793
768
  inputRef.value?.focus()
794
769
  return
795
770
  }
@@ -831,10 +806,14 @@ function onEscape(event: KeyboardEvent) {
831
806
  watch(
832
807
  () => props.open,
833
808
  async (o) => {
809
+ if (o) claim()
810
+ else release()
834
811
  cancel()
835
812
  query.value = ""
836
- selectedIndex.value = 0
813
+ select(0)
814
+ selectedId.value = null
837
815
  trail.value = []
816
+ trailState.value = []
838
817
  modeItems.value = []
839
818
  found.value = []
840
819
  failed.value = false
@@ -846,6 +825,8 @@ watch(
846
825
  { immediate: true },
847
826
  )
848
827
 
828
+ onBeforeUnmount(release)
829
+
849
830
  // Through useHotkeys rather than a listener of its own: the palette is the one
850
831
  // shortcut that has to work from inside a text field, and `code` is what makes
851
832
  // it fire on a layout where Ctrl-K arrives as "л".
@@ -868,14 +849,17 @@ useHotkeys(() =>
868
849
  <DialogRoot :open="open" @update:open="(v) => emit('update:open', v)">
869
850
  <DialogPortal>
870
851
  <DialogOverlay
871
- :class="{ 'lp-scrim--light': frosted }"
872
- class="lp-scrim fixed inset-0 z-(--z-overlay) data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
852
+ :class="{ 'lp-scrim--light': frosted, 'lp-scrim--nested': layer.nested }"
853
+ class="lp-scrim fixed inset-0 data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
854
+ :style="{ zIndex: layer.scrim }"
873
855
  />
874
856
  <DialogContent
875
- class="lp-cmd-dialog fixed left-1/2 top-[12vh] z-(--z-modal) flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]"
876
- :class="[frosted ? 'lp-glass-overlay' : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
857
+ class="lp-cmd-dialog fixed left-1/2 top-[12vh] flex max-h-[76vh] -translate-x-1/2 flex-col overflow-hidden rounded-card border border-line shadow-panel outline-none data-[state=open]:animate-[pop-in_160ms_var(--ease-emphasized)] data-[state=closed]:animate-[pop-out_130ms_ease]"
858
+ :class="[frosted ? (layer.nested ? 'lp-glass-overlay--nested' : 'lp-glass-overlay') : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
859
+ :style="{ zIndex: layer.panel }"
877
860
  :data-pane="paneOpen ? 'open' : undefined"
878
861
  @open-auto-focus.prevent
862
+ @keydown.capture="onPaletteKeydown"
879
863
  >
880
864
  <DialogTitle class="sr-only">Command palette</DialogTitle>
881
865
  <DialogDescription class="sr-only">Search and run a command.</DialogDescription>
@@ -916,8 +900,6 @@ useHotkeys(() =>
916
900
  :aria-activedescendant="activeId"
917
901
  :placeholder="mode?.childPlaceholder ?? placeholder"
918
902
  class="h-12 min-w-0 flex-1 bg-transparent text-sm text-ink outline-none placeholder:text-muted"
919
- @keydown.down.prevent="moveDown"
920
- @keydown.up.prevent="moveUp"
921
903
  @keydown.enter.prevent="runSelected"
922
904
  @keydown.backspace="onBackspace"
923
905
  @keydown.esc="onEscape"
@@ -927,7 +909,7 @@ useHotkeys(() =>
927
909
  </div>
928
910
 
929
911
  <!-- Results -->
930
- <div class="flex min-h-0 flex-1">
912
+ <div class="flex min-h-0 flex-1" @pointermove="onPointerMove">
931
913
  <LpScrollArea ref="scrollRef" class="lp-cmd-list min-h-0 min-w-0 flex-1" content-class="p-2">
932
914
  <!-- While a request is out there is nothing to say yet: "no results"
933
915
  under a spinner reads as an answer, and it is not one. -->
@@ -974,7 +956,7 @@ useHotkeys(() =>
974
956
  : 'text-ink hover:bg-surface-soft/60',
975
957
  ]"
976
958
  @click="onRowClick($event, c)"
977
- @pointerenter="selectedIndex = flatIndexOf(c)"
959
+ @pointerenter="onPointerEnter(c)"
978
960
  >
979
961
  <LpIcon v-if="c.icon" :name="c.icon" :size="16" class="shrink-0" :class="{ 'mt-0.5': c.excerpt }" />
980
962
  <span class="min-w-0 flex-1">
@@ -153,7 +153,7 @@ watch(() => props.open, (open) => (open ? claim() : release()), { immediate: tru
153
153
  onBeforeUnmount(release)
154
154
 
155
155
  const contentClass = computed(() => {
156
- const surface = frosted.value ? "lp-glass-overlay" : "bg-surface-raised"
156
+ const surface = frosted.value ? (layer.value.nested ? "lp-glass-overlay--nested" : "lp-glass-overlay") : "bg-surface-raised"
157
157
  const base = `fixed z-(--z-modal) flex flex-col border-line ${surface} shadow-panel outline-none`
158
158
  const map: Record<Direction, string> = {
159
159
  left: "inset-y-0 left-0 border-r rounded-r-card",
@@ -443,7 +443,7 @@ const previewStyle = computed(() => {
443
443
  <div
444
444
  v-show="peeking || peek > 0"
445
445
  class="lp-scrim pointer-events-none fixed inset-0"
446
- :class="{ 'lp-scrim--light': frosted }"
446
+ :class="{ 'lp-scrim--light': frosted, 'lp-scrim--nested': layer.nested }"
447
447
  :style="{ zIndex: layer.scrim, opacity: peek, transition: peeking || reducedMotion() ? 'none' : 'opacity 0.18s ease-out' }"
448
448
  />
449
449
  <div
@@ -476,7 +476,7 @@ const previewStyle = computed(() => {
476
476
  <DrawerOverlay
477
477
  :style="{ zIndex: layer.scrim }"
478
478
  class="lp-scrim fixed inset-0 z-(--z-overlay) data-[state=open]:animate-[fade-in_150ms_ease] data-[state=closed]:animate-[fade-out_130ms_ease]"
479
- :class="{ 'lp-scrim--light': frosted }"
479
+ :class="{ 'lp-scrim--light': frosted, 'lp-scrim--nested': layer.nested }"
480
480
  />
481
481
  <DrawerContent
482
482
  :class="contentClass"