@leavepulse/ui 0.40.1 → 0.42.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.
- package/dist/assets/{LpCommandPalette-DDGFwOom.css → LpCommandPalette-Esv0a-gS.css} +21 -21
- package/dist/assets/{LpShortcutsDialog-Cap_ALQM.css → LpShortcutsDialog-C6YoQ8_M.css} +7 -7
- package/dist/chunks/{LpAddressInput-H5NdhK6c.js → LpAddressInput-CMjaPQVw.js} +1 -1
- package/dist/chunks/{LpAppShell-BfqSA1ba.js → LpAppShell-Bcv6EhqU.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-BBRnMkV4.js → LpConfirmDialog-BNAvOR24.js} +2 -2
- package/dist/chunks/{LpDrawer-B8vBnCWF.js → LpDrawer-DgaS2s2i.js} +6 -4
- package/dist/chunks/{LpModal-EyEdRuqF.js → LpModal-8bja5bWS.js} +5 -3
- package/dist/chunks/{LpModalNav-BrDtP7b_.js → LpModalNav-w9kfwHqO.js} +3 -3
- package/dist/chunks/{LpSidebar-DZ20iBxY.js → LpSidebar-DlTn5nXc.js} +1 -1
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +2 -1
- package/dist/components/LpCommandPalette.vue.js +140 -76
- package/dist/components/LpConfirmDialog.vue.d.ts +2 -1
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpDrawer.vue.d.ts +4 -2
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +2 -1
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.d.ts +2 -1
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.d.ts +8 -11
- package/dist/components/LpShortcutsDialog.vue.js +14 -9
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/composables/useGlass.d.ts +9 -0
- package/dist/composables/useGlass.js +13 -0
- package/dist/composables/useHotkeys.d.ts +43 -2
- package/dist/composables/useHotkeys.js +89 -16
- package/dist/index.d.ts +3 -3
- package/dist/index.js +13 -8
- package/package.json +1 -1
- package/src/components/LpCommandPalette.vue +114 -36
- package/src/components/LpConfirmDialog.vue +2 -2
- package/src/components/LpDrawer.vue +9 -5
- package/src/components/LpModal.vue +8 -4
- package/src/components/LpModalNav.vue +2 -1
- package/src/components/LpShortcutsDialog.vue +32 -23
- package/src/composables/useGlass.ts +25 -0
- package/src/composables/useHotkeys.ts +146 -24
- 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.
|
|
54
|
+
*/
|
|
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 ("?").
|
|
47
66
|
*/
|
|
48
|
-
|
|
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
|
/**
|
|
@@ -112,6 +142,8 @@ function matches(event: KeyboardEvent, binding: HotkeyBinding): boolean {
|
|
|
112
142
|
* formality.
|
|
113
143
|
*/
|
|
114
144
|
export interface UseHotkeysOptions {
|
|
145
|
+
enabled?: MaybeRefOrGetter<boolean>
|
|
146
|
+
priority?: MaybeRefOrGetter<number>
|
|
115
147
|
/**
|
|
116
148
|
* Only act while the focus is inside this element.
|
|
117
149
|
*
|
|
@@ -127,34 +159,124 @@ export interface UseHotkeysOptions {
|
|
|
127
159
|
scope?: MaybeRefOrGetter<HTMLElement | null | undefined>
|
|
128
160
|
}
|
|
129
161
|
|
|
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
|
+
}
|
|
169
|
+
|
|
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()) {
|
|
190
|
+
if (inTextEntry && !binding.allowInInput) continue
|
|
191
|
+
if (!matches(event, binding)) continue
|
|
192
|
+
if (binding.handler(event) === false) continue
|
|
193
|
+
event.preventDefault()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
130
199
|
export function useHotkeys(
|
|
131
200
|
bindings: MaybeRefOrGetter<HotkeyBinding[]>,
|
|
132
201
|
options: UseHotkeysOptions = {},
|
|
133
202
|
) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return !!node && (el === node || el.contains(node))
|
|
203
|
+
const entry: MountedHotkeys = {
|
|
204
|
+
read: () => toValue(bindings),
|
|
205
|
+
scope: options.scope,
|
|
206
|
+
enabled: options.enabled,
|
|
207
|
+
priority: options.priority,
|
|
208
|
+
order: ++mountOrder,
|
|
141
209
|
}
|
|
210
|
+
onMounted(() => {
|
|
211
|
+
mounted.add(entry)
|
|
212
|
+
if (!listening) {
|
|
213
|
+
window.addEventListener("keydown", onKeydown)
|
|
214
|
+
listening = true
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
onBeforeUnmount(() => {
|
|
218
|
+
mounted.delete(entry)
|
|
219
|
+
if (!mounted.size && listening) {
|
|
220
|
+
window.removeEventListener("keydown", onKeydown)
|
|
221
|
+
listening = false
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
142
225
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
226
|
+
const KEY_NAMES: Record<string, string> = {
|
|
227
|
+
escape: "Esc",
|
|
228
|
+
enter: "Enter",
|
|
229
|
+
backspace: "Backspace",
|
|
230
|
+
delete: "Delete",
|
|
231
|
+
tab: "Tab",
|
|
232
|
+
" ": "Space",
|
|
233
|
+
arrowup: "↑",
|
|
234
|
+
arrowdown: "↓",
|
|
235
|
+
arrowleft: "←",
|
|
236
|
+
arrowright: "→",
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function comboOf(binding: HotkeyBinding): string {
|
|
240
|
+
const key = binding.key.toLowerCase()
|
|
241
|
+
const name = KEY_NAMES[key] ?? (key.length === 1 || /^f\d+$/.test(key) ? key.toUpperCase() : binding.key)
|
|
242
|
+
return [binding.ctrl && "Ctrl", binding.alt && "Alt", binding.shift && "Shift", name].filter(Boolean).join("+")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The shortcuts that work right now, grouped for `LpShortcutsDialog`: every
|
|
247
|
+
* binding with `help` from every mounted `useHotkeys`. A screen that binds its
|
|
248
|
+
* own keys adds its section while it is open, so the reference never lists a
|
|
249
|
+
* key that does nothing here or misses one that does.
|
|
250
|
+
*/
|
|
251
|
+
export const activeShortcuts = computed<ShortcutGroup[]>(() => {
|
|
252
|
+
const groups = new Map<string, ShortcutEntry[]>()
|
|
253
|
+
for (const entry of mounted) {
|
|
254
|
+
if (toValue(entry.enabled) === false) continue
|
|
255
|
+
for (const binding of entry.read()) {
|
|
256
|
+
if (!binding.help) continue
|
|
257
|
+
const combo = binding.help.combo ?? comboOf(binding)
|
|
258
|
+
const entries = groups.get(binding.help.group) ?? []
|
|
259
|
+
if (!entries.some((e) => e.combo === combo)) entries.push({ combo, label: binding.help.label })
|
|
260
|
+
groups.set(binding.help.group, entries)
|
|
155
261
|
}
|
|
156
262
|
}
|
|
263
|
+
return [...groups].map(([title, entries]) => ({ title, entries }))
|
|
264
|
+
})
|
|
157
265
|
|
|
158
|
-
|
|
159
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Groups merged by title, entries kept once per combo: what a reference shows
|
|
268
|
+
* when it lists both its own entries and the live ones.
|
|
269
|
+
*/
|
|
270
|
+
export function mergeShortcutGroups(...lists: ShortcutGroup[][]): ShortcutGroup[] {
|
|
271
|
+
const groups = new Map<string, ShortcutEntry[]>()
|
|
272
|
+
for (const list of lists) {
|
|
273
|
+
for (const group of list) {
|
|
274
|
+
const entries = groups.get(group.title) ?? []
|
|
275
|
+
for (const entry of group.entries) {
|
|
276
|
+
if (!entries.some((e) => e.combo === entry.combo)) entries.push(entry)
|
|
277
|
+
}
|
|
278
|
+
groups.set(group.title, entries)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return [...groups].map(([title, entries]) => ({ title, entries }))
|
|
160
282
|
}
|
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 {
|
|
232
|
-
|
|
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"
|