@leavepulse/ui 0.40.1 → 0.41.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 (40) hide show
  1. package/dist/assets/{LpCommandPalette-DDGFwOom.css → LpCommandPalette-DyT6aGdW.css} +21 -21
  2. package/dist/assets/{LpShortcutsDialog-Cap_ALQM.css → LpShortcutsDialog-C6YoQ8_M.css} +7 -7
  3. package/dist/chunks/{LpAddressInput-H5NdhK6c.js → LpAddressInput-CMjaPQVw.js} +1 -1
  4. package/dist/chunks/{LpAppShell-BfqSA1ba.js → LpAppShell-Bcv6EhqU.js} +1 -1
  5. package/dist/chunks/{LpConfirmDialog-BBRnMkV4.js → LpConfirmDialog-BNAvOR24.js} +2 -2
  6. package/dist/chunks/{LpDrawer-B8vBnCWF.js → LpDrawer-DgaS2s2i.js} +6 -4
  7. package/dist/chunks/{LpModal-EyEdRuqF.js → LpModal-8bja5bWS.js} +5 -3
  8. package/dist/chunks/{LpModalNav-BrDtP7b_.js → LpModalNav-w9kfwHqO.js} +3 -3
  9. package/dist/chunks/{LpSidebar-DZ20iBxY.js → LpSidebar-DlTn5nXc.js} +1 -1
  10. package/dist/components/LpAddressInput.vue.js +1 -1
  11. package/dist/components/LpAppShell.vue.js +1 -1
  12. package/dist/components/LpCommandPalette.vue.d.ts +2 -1
  13. package/dist/components/LpCommandPalette.vue.js +7 -5
  14. package/dist/components/LpConfirmDialog.vue.d.ts +2 -1
  15. package/dist/components/LpConfirmDialog.vue.js +1 -1
  16. package/dist/components/LpDrawer.vue.d.ts +4 -2
  17. package/dist/components/LpDrawer.vue.js +1 -1
  18. package/dist/components/LpModal.vue.d.ts +2 -1
  19. package/dist/components/LpModal.vue.js +1 -1
  20. package/dist/components/LpModalNav.vue.d.ts +2 -1
  21. package/dist/components/LpModalNav.vue.js +1 -1
  22. package/dist/components/LpShortcutsDialog.vue.d.ts +8 -11
  23. package/dist/components/LpShortcutsDialog.vue.js +14 -9
  24. package/dist/components/LpSidebar.vue.js +1 -1
  25. package/dist/composables/useGlass.d.ts +9 -0
  26. package/dist/composables/useGlass.js +13 -0
  27. package/dist/composables/useHotkeys.d.ts +41 -2
  28. package/dist/composables/useHotkeys.js +57 -4
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +13 -8
  31. package/package.json +1 -1
  32. package/src/components/LpCommandPalette.vue +8 -4
  33. package/src/components/LpConfirmDialog.vue +2 -2
  34. package/src/components/LpDrawer.vue +9 -5
  35. package/src/components/LpModal.vue +8 -4
  36. package/src/components/LpModalNav.vue +2 -1
  37. package/src/components/LpShortcutsDialog.vue +32 -23
  38. package/src/composables/useGlass.ts +25 -0
  39. package/src/composables/useHotkeys.ts +107 -6
  40. package/src/index.ts +9 -3
@@ -1,4 +1,4 @@
1
- import { onBeforeUnmount, onMounted, type MaybeRefOrGetter, toValue } from "vue"
1
+ import { computed, onBeforeUnmount, onMounted, shallowReactive, type MaybeRefOrGetter, toValue } from "vue"
2
2
 
3
3
  /*
4
4
  * One keydown listener over a registry of bindings, so a shortcut is declared
@@ -39,13 +39,43 @@ export interface HotkeyBinding {
39
39
  * something there — Escape, and the palette the user reaches from anywhere.
40
40
  */
41
41
  allowInInput?: boolean
42
+ /**
43
+ * How the key reference lists this binding while it is mounted. Bindings
44
+ * without it still work, they just stay out of `activeShortcuts`.
45
+ */
46
+ help?: HotkeyHelp
42
47
  /**
43
48
  * Returning `false` declines the key: the event keeps its default and the
44
49
  * search moves on. This is what lets a binding defer to a layer that owns the
45
50
  * key better than it does — Escape belongs to whichever overlay is on top,
46
- * and swallowing it here would close all of them.
51
+ * and swallowing it here would close all of them. The event is passed for
52
+ * the checks only it can answer: where the focus is, whether an inner
53
+ * control already took the key.
47
54
  */
48
- handler: () => void | boolean
55
+ handler: (event: KeyboardEvent) => void | boolean
56
+ }
57
+
58
+ export interface HotkeyHelp {
59
+ /** Section of the reference the entry goes under. */
60
+ group: string
61
+ label: string
62
+ /**
63
+ * Written the way it reads, "+"-separated. Defaults to one built from the
64
+ * binding; set it where the binding is not the whole story, such as the
65
+ * second key of a chord ("G+D") or a key that needs Shift to type ("?").
66
+ */
67
+ combo?: string
68
+ }
69
+
70
+ export interface ShortcutEntry {
71
+ /** Rendered one <kbd> per "+"-separated key, so write it the way it reads. */
72
+ combo: string
73
+ label: string
74
+ }
75
+
76
+ export interface ShortcutGroup {
77
+ title: string
78
+ entries: ShortcutEntry[]
49
79
  }
50
80
 
51
81
  /**
@@ -149,12 +179,83 @@ export function useHotkeys(
149
179
  if (!matches(event, binding)) continue
150
180
  // Claimed only once the handler has run and not declined, so a binding
151
181
  // that stands down leaves the key untouched for whoever handles it next.
152
- if (binding.handler() === false) continue
182
+ if (binding.handler(event) === false) continue
153
183
  event.preventDefault()
154
184
  return
155
185
  }
156
186
  }
157
187
 
158
- onMounted(() => window.addEventListener("keydown", onKeydown))
159
- onBeforeUnmount(() => window.removeEventListener("keydown", onKeydown))
188
+ const read = () => toValue(bindings)
189
+ onMounted(() => {
190
+ window.addEventListener("keydown", onKeydown)
191
+ mounted.add(read)
192
+ })
193
+ onBeforeUnmount(() => {
194
+ window.removeEventListener("keydown", onKeydown)
195
+ mounted.delete(read)
196
+ })
197
+ }
198
+
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
+ const KEY_NAMES: Record<string, string> = {
207
+ escape: "Esc",
208
+ enter: "Enter",
209
+ backspace: "Backspace",
210
+ delete: "Delete",
211
+ tab: "Tab",
212
+ " ": "Space",
213
+ arrowup: "↑",
214
+ arrowdown: "↓",
215
+ arrowleft: "←",
216
+ arrowright: "→",
217
+ }
218
+
219
+ function comboOf(binding: HotkeyBinding): string {
220
+ const key = binding.key.toLowerCase()
221
+ const name = KEY_NAMES[key] ?? (key.length === 1 || /^f\d+$/.test(key) ? key.toUpperCase() : binding.key)
222
+ return [binding.ctrl && "Ctrl", binding.alt && "Alt", binding.shift && "Shift", name].filter(Boolean).join("+")
223
+ }
224
+
225
+ /**
226
+ * The shortcuts that work right now, grouped for `LpShortcutsDialog`: every
227
+ * binding with `help` from every mounted `useHotkeys`. A screen that binds its
228
+ * own keys adds its section while it is open, so the reference never lists a
229
+ * key that does nothing here or misses one that does.
230
+ */
231
+ export const activeShortcuts = computed<ShortcutGroup[]>(() => {
232
+ const groups = new Map<string, ShortcutEntry[]>()
233
+ for (const read of mounted) {
234
+ for (const binding of read()) {
235
+ if (!binding.help) continue
236
+ const combo = binding.help.combo ?? comboOf(binding)
237
+ const entries = groups.get(binding.help.group) ?? []
238
+ if (!entries.some((e) => e.combo === combo)) entries.push({ combo, label: binding.help.label })
239
+ groups.set(binding.help.group, entries)
240
+ }
241
+ }
242
+ return [...groups].map(([title, entries]) => ({ title, entries }))
243
+ })
244
+
245
+ /**
246
+ * Groups merged by title, entries kept once per combo: what a reference shows
247
+ * when it lists both its own entries and the live ones.
248
+ */
249
+ export function mergeShortcutGroups(...lists: ShortcutGroup[][]): ShortcutGroup[] {
250
+ const groups = new Map<string, ShortcutEntry[]>()
251
+ for (const list of lists) {
252
+ for (const group of list) {
253
+ const entries = groups.get(group.title) ?? []
254
+ for (const entry of group.entries) {
255
+ if (!entries.some((e) => e.combo === entry.combo)) entries.push(entry)
256
+ }
257
+ groups.set(group.title, entries)
258
+ }
259
+ }
260
+ return [...groups].map(([title, entries]) => ({ title, entries }))
160
261
  }
package/src/index.ts CHANGED
@@ -141,7 +141,6 @@ export { default as LpServiceNode } from "./components/LpServiceNode.vue"
141
141
  export type { ServiceNodeData } from "./components/LpServiceNode.vue"
142
142
  export { default as LpShift } from "./components/LpShift.vue"
143
143
  export { default as LpShortcutsDialog } from "./components/LpShortcutsDialog.vue"
144
- export type { ShortcutEntry, ShortcutGroup } from "./components/LpShortcutsDialog.vue"
145
144
  export { default as LpSidebar } from "./components/LpSidebar.vue"
146
145
  export type { SidebarItem, SidebarSection } from "./components/LpSidebar.vue"
147
146
  export { default as LpSkeleton } from "./components/LpSkeleton.vue"
@@ -228,8 +227,15 @@ export type { UiConfig } from "./theme/config"
228
227
 
229
228
  export { useClipboard } from "./composables/useClipboard"
230
229
  export type { CopyOptions, UseClipboard } from "./composables/useClipboard"
231
- export { useHotkeys, isAppleKeyboard, appleModifierLabel } from "./composables/useHotkeys"
232
- export type { HotkeyBinding } from "./composables/useHotkeys"
230
+ export {
231
+ useHotkeys,
232
+ isAppleKeyboard,
233
+ appleModifierLabel,
234
+ activeShortcuts,
235
+ mergeShortcutGroups,
236
+ } from "./composables/useHotkeys"
237
+ export type { HotkeyBinding, HotkeyHelp, ShortcutEntry, ShortcutGroup } from "./composables/useHotkeys"
238
+ export { provideGlass, useGlass } from "./composables/useGlass"
233
239
  export { useMergedClass, useMergedAttrs, mergeClass } from "./composables/useMergedClass"
234
240
  export { useInputFilter } from "./composables/useInputFilter"
235
241
  export type { UseInputFilterOptions } from "./composables/useInputFilter"