@leavepulse/ui 0.40.0 → 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.
- package/dist/assets/LpCommandPalette-DyT6aGdW.css +85 -0
- 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 +12 -13
- 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 +41 -2
- package/dist/composables/useHotkeys.js +57 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +13 -8
- package/package.json +1 -1
- package/src/components/LpCommandPalette.vue +48 -26
- 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 +107 -6
- package/src/index.ts +9 -3
- package/dist/assets/LpCommandPalette-B5eZBHox.css +0 -64
|
@@ -16,13 +16,40 @@ export interface HotkeyBinding {
|
|
|
16
16
|
* something there — Escape, and the palette the user reaches from anywhere.
|
|
17
17
|
*/
|
|
18
18
|
allowInInput?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* How the key reference lists this binding while it is mounted. Bindings
|
|
21
|
+
* without it still work, they just stay out of `activeShortcuts`.
|
|
22
|
+
*/
|
|
23
|
+
help?: HotkeyHelp;
|
|
19
24
|
/**
|
|
20
25
|
* Returning `false` declines the key: the event keeps its default and the
|
|
21
26
|
* search moves on. This is what lets a binding defer to a layer that owns the
|
|
22
27
|
* key better than it does — Escape belongs to whichever overlay is on top,
|
|
23
|
-
* and swallowing it here would close all of them.
|
|
28
|
+
* and swallowing it here would close all of them. The event is passed for
|
|
29
|
+
* the checks only it can answer: where the focus is, whether an inner
|
|
30
|
+
* control already took the key.
|
|
31
|
+
*/
|
|
32
|
+
handler: (event: KeyboardEvent) => void | boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface HotkeyHelp {
|
|
35
|
+
/** Section of the reference the entry goes under. */
|
|
36
|
+
group: string;
|
|
37
|
+
label: string;
|
|
38
|
+
/**
|
|
39
|
+
* Written the way it reads, "+"-separated. Defaults to one built from the
|
|
40
|
+
* binding; set it where the binding is not the whole story, such as the
|
|
41
|
+
* second key of a chord ("G+D") or a key that needs Shift to type ("?").
|
|
24
42
|
*/
|
|
25
|
-
|
|
43
|
+
combo?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ShortcutEntry {
|
|
46
|
+
/** Rendered one <kbd> per "+"-separated key, so write it the way it reads. */
|
|
47
|
+
combo: string;
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
50
|
+
export interface ShortcutGroup {
|
|
51
|
+
title: string;
|
|
52
|
+
entries: ShortcutEntry[];
|
|
26
53
|
}
|
|
27
54
|
/**
|
|
28
55
|
* Whether this machine prints ⌘ where the rest print Ctrl. Read once: the
|
|
@@ -58,3 +85,15 @@ export interface UseHotkeysOptions {
|
|
|
58
85
|
scope?: MaybeRefOrGetter<HTMLElement | null | undefined>;
|
|
59
86
|
}
|
|
60
87
|
export declare function useHotkeys(bindings: MaybeRefOrGetter<HotkeyBinding[]>, options?: UseHotkeysOptions): void;
|
|
88
|
+
/**
|
|
89
|
+
* The shortcuts that work right now, grouped for `LpShortcutsDialog`: every
|
|
90
|
+
* binding with `help` from every mounted `useHotkeys`. A screen that binds its
|
|
91
|
+
* own keys adds its section while it is open, so the reference never lists a
|
|
92
|
+
* key that does nothing here or misses one that does.
|
|
93
|
+
*/
|
|
94
|
+
export declare const activeShortcuts: import('vue').ComputedRef<ShortcutGroup[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Groups merged by title, entries kept once per combo: what a reference shows
|
|
97
|
+
* when it lists both its own entries and the live ones.
|
|
98
|
+
*/
|
|
99
|
+
export declare function mergeShortcutGroups(...lists: ShortcutGroup[][]): ShortcutGroup[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onMounted, onBeforeUnmount, toValue } from "vue";
|
|
1
|
+
import { shallowReactive, computed, onMounted, onBeforeUnmount, toValue } 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;
|
|
@@ -45,16 +45,69 @@ function useHotkeys(bindings, options = {}) {
|
|
|
45
45
|
for (const binding of toValue(bindings)) {
|
|
46
46
|
if (inTextEntry && !binding.allowInInput) continue;
|
|
47
47
|
if (!matches(event, binding)) continue;
|
|
48
|
-
if (binding.handler() === false) continue;
|
|
48
|
+
if (binding.handler(event) === false) continue;
|
|
49
49
|
event.preventDefault();
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const read = () => toValue(bindings);
|
|
54
|
+
onMounted(() => {
|
|
55
|
+
window.addEventListener("keydown", onKeydown);
|
|
56
|
+
mounted.add(read);
|
|
57
|
+
});
|
|
58
|
+
onBeforeUnmount(() => {
|
|
59
|
+
window.removeEventListener("keydown", onKeydown);
|
|
60
|
+
mounted.delete(read);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const mounted = shallowReactive(/* @__PURE__ */ new Set());
|
|
64
|
+
const KEY_NAMES = {
|
|
65
|
+
escape: "Esc",
|
|
66
|
+
enter: "Enter",
|
|
67
|
+
backspace: "Backspace",
|
|
68
|
+
delete: "Delete",
|
|
69
|
+
tab: "Tab",
|
|
70
|
+
" ": "Space",
|
|
71
|
+
arrowup: "↑",
|
|
72
|
+
arrowdown: "↓",
|
|
73
|
+
arrowleft: "←",
|
|
74
|
+
arrowright: "→"
|
|
75
|
+
};
|
|
76
|
+
function comboOf(binding) {
|
|
77
|
+
const key = binding.key.toLowerCase();
|
|
78
|
+
const name = KEY_NAMES[key] ?? (key.length === 1 || /^f\d+$/.test(key) ? key.toUpperCase() : binding.key);
|
|
79
|
+
return [binding.ctrl && "Ctrl", binding.alt && "Alt", binding.shift && "Shift", name].filter(Boolean).join("+");
|
|
80
|
+
}
|
|
81
|
+
const activeShortcuts = computed(() => {
|
|
82
|
+
const groups = /* @__PURE__ */ new Map();
|
|
83
|
+
for (const read of mounted) {
|
|
84
|
+
for (const binding of read()) {
|
|
85
|
+
if (!binding.help) continue;
|
|
86
|
+
const combo = binding.help.combo ?? comboOf(binding);
|
|
87
|
+
const entries = groups.get(binding.help.group) ?? [];
|
|
88
|
+
if (!entries.some((e) => e.combo === combo)) entries.push({ combo, label: binding.help.label });
|
|
89
|
+
groups.set(binding.help.group, entries);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...groups].map(([title, entries]) => ({ title, entries }));
|
|
93
|
+
});
|
|
94
|
+
function mergeShortcutGroups(...lists) {
|
|
95
|
+
const groups = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const list of lists) {
|
|
97
|
+
for (const group of list) {
|
|
98
|
+
const entries = groups.get(group.title) ?? [];
|
|
99
|
+
for (const entry of group.entries) {
|
|
100
|
+
if (!entries.some((e) => e.combo === entry.combo)) entries.push(entry);
|
|
101
|
+
}
|
|
102
|
+
groups.set(group.title, entries);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...groups].map(([title, entries]) => ({ title, entries }));
|
|
55
106
|
}
|
|
56
107
|
export {
|
|
108
|
+
activeShortcuts,
|
|
57
109
|
appleModifierLabel,
|
|
58
110
|
isAppleKeyboard,
|
|
111
|
+
mergeShortcutGroups,
|
|
59
112
|
useHotkeys
|
|
60
113
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -91,7 +91,6 @@ export { default as LpServiceNode } from './components/LpServiceNode.vue';
|
|
|
91
91
|
export type { ServiceNodeData } from './components/LpServiceNode.vue';
|
|
92
92
|
export { default as LpShift } from './components/LpShift.vue';
|
|
93
93
|
export { default as LpShortcutsDialog } from './components/LpShortcutsDialog.vue';
|
|
94
|
-
export type { ShortcutEntry, ShortcutGroup } from './components/LpShortcutsDialog.vue';
|
|
95
94
|
export { default as LpSidebar } from './components/LpSidebar.vue';
|
|
96
95
|
export type { SidebarItem, SidebarSection } from './components/LpSidebar.vue';
|
|
97
96
|
export { default as LpSkeleton } from './components/LpSkeleton.vue';
|
|
@@ -135,8 +134,9 @@ export { parseConfig, serializeConfig, UI_CONFIG_VERSION } from './theme/config'
|
|
|
135
134
|
export type { UiConfig } from './theme/config';
|
|
136
135
|
export { useClipboard } from './composables/useClipboard';
|
|
137
136
|
export type { CopyOptions, UseClipboard } from './composables/useClipboard';
|
|
138
|
-
export { useHotkeys, isAppleKeyboard, appleModifierLabel } from './composables/useHotkeys';
|
|
139
|
-
export type { HotkeyBinding } from './composables/useHotkeys';
|
|
137
|
+
export { useHotkeys, isAppleKeyboard, appleModifierLabel, activeShortcuts, mergeShortcutGroups, } from './composables/useHotkeys';
|
|
138
|
+
export type { HotkeyBinding, HotkeyHelp, ShortcutEntry, ShortcutGroup } from './composables/useHotkeys';
|
|
139
|
+
export { provideGlass, useGlass } from './composables/useGlass';
|
|
140
140
|
export { useMergedClass, useMergedAttrs, mergeClass } from './composables/useMergedClass';
|
|
141
141
|
export { useInputFilter } from './composables/useInputFilter';
|
|
142
142
|
export type { UseInputFilterOptions } from './composables/useInputFilter';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { _ } from "./chunks/LayoutCanvas-Dd20boUG.js";
|
|
2
|
-
import { _ as _2 } from "./chunks/LpAddressInput-
|
|
2
|
+
import { _ as _2 } from "./chunks/LpAddressInput-CMjaPQVw.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-
|
|
7
|
+
import { _ as _6 } from "./chunks/LpAppShell-Bcv6EhqU.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,12 @@ 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-
|
|
22
|
+
import { _ as _17 } from "./chunks/LpConfirmDialog-BNAvOR24.js";
|
|
23
23
|
import { _ as _18 } from "./chunks/LpContextMenu-CwW1QXe_.js";
|
|
24
24
|
import { _ as _19 } from "./chunks/LpDatePicker-CwOToxGq.js";
|
|
25
25
|
import { _ as _20 } from "./chunks/LpDisclosure-ChQYmjlR.js";
|
|
26
26
|
import { _ as _21 } from "./chunks/LpDivider-CUX46KR0.js";
|
|
27
|
-
import { _ as _22 } from "./chunks/LpDrawer-
|
|
27
|
+
import { _ as _22 } from "./chunks/LpDrawer-DgaS2s2i.js";
|
|
28
28
|
import { _ as _23 } from "./chunks/LpDropdownMenu-DJpoqRCb.js";
|
|
29
29
|
import { default as default4 } from "./components/LpDropzone.vue.js";
|
|
30
30
|
import { _ as _24 } from "./chunks/LpEmptyState-Dsdqz1Wj.js";
|
|
@@ -40,8 +40,8 @@ import { fileNameOf } from "./components/lightbox.js";
|
|
|
40
40
|
import { _ as _31 } from "./chunks/LpLink-DCND-zyu.js";
|
|
41
41
|
import { _ as _32 } from "./chunks/LpLogViewer-CaRy5MeS.js";
|
|
42
42
|
import { _ as _33 } from "./chunks/LpMediaGallery-Cid7PGYx.js";
|
|
43
|
-
import { _ as _34 } from "./chunks/LpModal-
|
|
44
|
-
import { _ as _35 } from "./chunks/LpModalNav-
|
|
43
|
+
import { _ as _34 } from "./chunks/LpModal-8bja5bWS.js";
|
|
44
|
+
import { _ as _35 } from "./chunks/LpModalNav-w9kfwHqO.js";
|
|
45
45
|
import { _ as _36 } from "./chunks/LpNotificationBell-DyGGsMAM.js";
|
|
46
46
|
import { _ as _37 } from "./chunks/LpNumberField-CWuX08iX.js";
|
|
47
47
|
import { _ as _38 } from "./chunks/LpTimeField-K6nAGboX.js";
|
|
@@ -64,7 +64,7 @@ import { _ as _53 } from "./chunks/LpSelect-ANIVtrpM.js";
|
|
|
64
64
|
import { _ as _54 } from "./chunks/LpServiceNode-D8uJ8sGk.js";
|
|
65
65
|
import { _ as _55 } from "./chunks/LpShift-DXNsauQZ.js";
|
|
66
66
|
import { default as default6 } from "./components/LpShortcutsDialog.vue.js";
|
|
67
|
-
import { _ as _56 } from "./chunks/LpSidebar-
|
|
67
|
+
import { _ as _56 } from "./chunks/LpSidebar-DlTn5nXc.js";
|
|
68
68
|
import { _ as _57 } from "./chunks/LpSkeleton-CTI-vBgW.js";
|
|
69
69
|
import { _ as _58 } from "./chunks/LpSlider-BwNIBkAK.js";
|
|
70
70
|
import { _ as _59 } from "./chunks/LpSparkline-C6xQ2dwv.js";
|
|
@@ -87,7 +87,8 @@ import { addLeaf, addTab, countBlocks, countLeaves, makeLayout, moveLeaf, remove
|
|
|
87
87
|
import { blockTitle, defineBlocks } from "./layout/registry.js";
|
|
88
88
|
import { useLayout } from "./layout/useLayout.js";
|
|
89
89
|
import { useClipboard } from "./composables/useClipboard.js";
|
|
90
|
-
import { appleModifierLabel, isAppleKeyboard, useHotkeys } from "./composables/useHotkeys.js";
|
|
90
|
+
import { activeShortcuts, appleModifierLabel, isAppleKeyboard, mergeShortcutGroups, useHotkeys } from "./composables/useHotkeys.js";
|
|
91
|
+
import { provideGlass, useGlass } from "./composables/useGlass.js";
|
|
91
92
|
import { mergeClass, useMergedAttrs, useMergedClass } from "./composables/useMergedClass.js";
|
|
92
93
|
import { useInputFilter } from "./composables/useInputFilter.js";
|
|
93
94
|
import { easeOut, easeTravel, easingToken, prefersReducedMotion } from "./composables/easing.js";
|
|
@@ -215,6 +216,7 @@ export {
|
|
|
215
216
|
OSM_TILES,
|
|
216
217
|
T as THEME_COOKIE,
|
|
217
218
|
UI_CONFIG_VERSION,
|
|
219
|
+
activeShortcuts,
|
|
218
220
|
addLeaf,
|
|
219
221
|
addTab,
|
|
220
222
|
b as amber,
|
|
@@ -261,6 +263,7 @@ export {
|
|
|
261
263
|
makeLayout,
|
|
262
264
|
matchCountryByValue,
|
|
263
265
|
mergeClass,
|
|
266
|
+
mergeShortcutGroups,
|
|
264
267
|
moveLeaf,
|
|
265
268
|
niceScale,
|
|
266
269
|
n as nord,
|
|
@@ -273,6 +276,7 @@ export {
|
|
|
273
276
|
prefersReducedMotion,
|
|
274
277
|
presets,
|
|
275
278
|
project,
|
|
279
|
+
provideGlass,
|
|
276
280
|
removeBlock,
|
|
277
281
|
removeLeaf,
|
|
278
282
|
reorderTab,
|
|
@@ -296,6 +300,7 @@ export {
|
|
|
296
300
|
useAsyncTask,
|
|
297
301
|
useClipboard,
|
|
298
302
|
useFormDraft,
|
|
303
|
+
useGlass,
|
|
299
304
|
useHotkeys,
|
|
300
305
|
useInputFilter,
|
|
301
306
|
useLayout,
|
package/package.json
CHANGED
|
@@ -40,6 +40,7 @@ import { computed, nextTick, 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
|
+
import { useGlass } from "../composables/useGlass"
|
|
43
44
|
import { useClipboard } from "../composables/useClipboard"
|
|
44
45
|
import { useHotkeys } from "../composables/useHotkeys"
|
|
45
46
|
|
|
@@ -210,7 +211,8 @@ const props = withDefaults(
|
|
|
210
211
|
/**
|
|
211
212
|
* Frosted glass instead of a solid panel: the page shows through,
|
|
212
213
|
* blurred, under a lighter scrim (.lp-glass-overlay, .lp-scrim--light).
|
|
213
|
-
* A look, not a behaviour, so it is the app's call
|
|
214
|
+
* A look, not a behaviour, so it is the app's call: unset follows
|
|
215
|
+
* `provideGlass`, and without one it is off.
|
|
214
216
|
*/
|
|
215
217
|
glass?: boolean
|
|
216
218
|
}>(),
|
|
@@ -227,10 +229,12 @@ const props = withDefaults(
|
|
|
227
229
|
groupLimit: 0,
|
|
228
230
|
layoutFallback: true,
|
|
229
231
|
previewPane: false,
|
|
230
|
-
glass:
|
|
232
|
+
glass: undefined,
|
|
231
233
|
},
|
|
232
234
|
)
|
|
233
235
|
|
|
236
|
+
const frosted = useGlass(() => props.glass)
|
|
237
|
+
|
|
234
238
|
const DEFAULT_LABELS: PaletteLabels = {
|
|
235
239
|
navigate: "navigate",
|
|
236
240
|
select: "select",
|
|
@@ -864,16 +868,13 @@ useHotkeys(() =>
|
|
|
864
868
|
<DialogRoot :open="open" @update:open="(v) => emit('update:open', v)">
|
|
865
869
|
<DialogPortal>
|
|
866
870
|
<DialogOverlay
|
|
867
|
-
:class="{ 'lp-scrim--light':
|
|
871
|
+
:class="{ 'lp-scrim--light': frosted }"
|
|
868
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]"
|
|
869
873
|
/>
|
|
870
874
|
<DialogContent
|
|
871
|
-
class="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]"
|
|
872
|
-
:class="[
|
|
873
|
-
:
|
|
874
|
-
width: paneOpen ? 'min(92vw, 64rem)' : 'min(92vw, 40rem)',
|
|
875
|
-
minHeight: paneOpen ? 'min(76vh, 36rem)' : '0px',
|
|
876
|
-
}"
|
|
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']"
|
|
877
|
+
:data-pane="paneOpen ? 'open' : undefined"
|
|
877
878
|
@open-auto-focus.prevent
|
|
878
879
|
>
|
|
879
880
|
<DialogTitle class="sr-only">Command palette</DialogTitle>
|
|
@@ -927,7 +928,7 @@ useHotkeys(() =>
|
|
|
927
928
|
|
|
928
929
|
<!-- Results -->
|
|
929
930
|
<div class="flex min-h-0 flex-1">
|
|
930
|
-
<LpScrollArea ref="scrollRef" class="min-h-0 min-w-0 flex-1" content-class="p-2">
|
|
931
|
+
<LpScrollArea ref="scrollRef" class="lp-cmd-list min-h-0 min-w-0 flex-1" content-class="p-2">
|
|
931
932
|
<!-- While a request is out there is nothing to say yet: "no results"
|
|
932
933
|
under a spinner reads as an answer, and it is not one. -->
|
|
933
934
|
<p v-if="flat.length === 0 && !failed" class="px-2 py-10 text-center text-sm text-muted">
|
|
@@ -1040,7 +1041,7 @@ useHotkeys(() =>
|
|
|
1040
1041
|
<Transition name="lp-cmd-pane">
|
|
1041
1042
|
<aside
|
|
1042
1043
|
v-if="paneOpen"
|
|
1043
|
-
class="hidden min-h-0
|
|
1044
|
+
class="lp-cmd-pane hidden min-h-0 shrink-0 flex-col border-l border-line md:flex"
|
|
1044
1045
|
>
|
|
1045
1046
|
<LpScrollArea ref="previewRef" class="min-h-0 flex-1" content-class="px-5 py-4">
|
|
1046
1047
|
<!-- Keyed by the row, so moving the highlight replays the
|
|
@@ -1107,28 +1108,49 @@ useHotkeys(() =>
|
|
|
1107
1108
|
|
|
1108
1109
|
<style scoped>
|
|
1109
1110
|
/*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1111
|
+
* Sizes for the split layout. Only the dialog's width animates: the list and
|
|
1112
|
+
* the pane keep fixed widths, and the dialog — overflow hidden — uncovers the
|
|
1113
|
+
* pane from its right edge as it grows. Animating the columns themselves made
|
|
1114
|
+
* every line of the list and the pane re-wrap on every frame.
|
|
1112
1115
|
*/
|
|
1113
|
-
.lp-cmd-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1116
|
+
.lp-cmd-dialog {
|
|
1117
|
+
--lp-cmd-list: min(92vw, 40rem);
|
|
1118
|
+
--lp-cmd-pane: max(0px, min(26rem, 96vw - var(--lp-cmd-list)));
|
|
1119
|
+
width: var(--lp-cmd-list);
|
|
1117
1120
|
}
|
|
1118
|
-
.lp-cmd-
|
|
1121
|
+
.lp-cmd-split {
|
|
1119
1122
|
transition:
|
|
1120
|
-
|
|
1121
|
-
|
|
1123
|
+
width 300ms var(--ease-emphasized, ease),
|
|
1124
|
+
min-height 300ms var(--ease-emphasized, ease);
|
|
1122
1125
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1126
|
+
@media (min-width: 768px) {
|
|
1127
|
+
.lp-cmd-split .lp-cmd-list {
|
|
1128
|
+
flex: 0 0 auto;
|
|
1129
|
+
/* the dialog's 1px border on each side */
|
|
1130
|
+
width: calc(var(--lp-cmd-list) - 2px);
|
|
1131
|
+
}
|
|
1132
|
+
.lp-cmd-dialog[data-pane="open"] {
|
|
1133
|
+
width: calc(var(--lp-cmd-list) + var(--lp-cmd-pane));
|
|
1134
|
+
min-height: min(76vh, 36rem);
|
|
1135
|
+
}
|
|
1125
1136
|
}
|
|
1126
|
-
.lp-cmd-pane
|
|
1127
|
-
|
|
1128
|
-
|
|
1137
|
+
.lp-cmd-pane {
|
|
1138
|
+
width: var(--lp-cmd-pane);
|
|
1139
|
+
}
|
|
1140
|
+
/*
|
|
1141
|
+
* The pane rides in from the right edge while the dialog opens up to it, and
|
|
1142
|
+
* leaves the same way, for as long as the dialog takes to close over it.
|
|
1143
|
+
*/
|
|
1144
|
+
.lp-cmd-pane-enter-active,
|
|
1145
|
+
.lp-cmd-pane-leave-active {
|
|
1146
|
+
transition:
|
|
1147
|
+
transform 300ms var(--ease-emphasized, ease),
|
|
1148
|
+
opacity 300ms ease;
|
|
1129
1149
|
}
|
|
1150
|
+
.lp-cmd-pane-enter-from,
|
|
1130
1151
|
.lp-cmd-pane-leave-to {
|
|
1131
1152
|
opacity: 0;
|
|
1153
|
+
transform: translateX(40%);
|
|
1132
1154
|
}
|
|
1133
1155
|
.lp-cmd-para {
|
|
1134
1156
|
animation: lp-cmd-para-in 260ms ease-out both;
|
|
@@ -1140,7 +1162,7 @@ useHotkeys(() =>
|
|
|
1140
1162
|
}
|
|
1141
1163
|
}
|
|
1142
1164
|
@media (prefers-reduced-motion: reduce) {
|
|
1143
|
-
.lp-cmd-
|
|
1165
|
+
.lp-cmd-split,
|
|
1144
1166
|
.lp-cmd-pane-enter-active,
|
|
1145
1167
|
.lp-cmd-pane-leave-active {
|
|
1146
1168
|
transition: none;
|
|
@@ -36,10 +36,10 @@ const props = withDefaults(
|
|
|
36
36
|
*/
|
|
37
37
|
loading?: boolean
|
|
38
38
|
loadingLabel?: string
|
|
39
|
-
/** Frosted glass panel, passed to LpModal.
|
|
39
|
+
/** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
|
|
40
40
|
glass?: boolean
|
|
41
41
|
}>(),
|
|
42
|
-
{ confirmLabel: "Confirm", cancelLabel: "Cancel", loadingLabel: "Working…" },
|
|
42
|
+
{ confirmLabel: "Confirm", cancelLabel: "Cancel", loadingLabel: "Working…", glass: undefined },
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
const emit = defineEmits<{
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "vaul-vue"
|
|
32
32
|
import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
|
|
33
33
|
import { CLOSE_ICON } from "./dropdown"
|
|
34
|
+
import { useGlass } from "../composables/useGlass"
|
|
34
35
|
import LpIcon from "./LpIcon.vue"
|
|
35
36
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
36
37
|
import { anyModalOpen, useModalLayer } from "./modalDepth"
|
|
@@ -104,7 +105,8 @@ const props = withDefaults(
|
|
|
104
105
|
/**
|
|
105
106
|
* Frosted glass instead of a solid panel: the page shows through,
|
|
106
107
|
* blurred, under a lighter scrim (.lp-glass-overlay, .lp-scrim--light).
|
|
107
|
-
* A look, not a behaviour, so it is the app's call
|
|
108
|
+
* A look, not a behaviour, so it is the app's call: unset follows
|
|
109
|
+
* `provideGlass`, and without one it is off.
|
|
108
110
|
*/
|
|
109
111
|
glass?: boolean
|
|
110
112
|
}>(),
|
|
@@ -117,10 +119,12 @@ const props = withDefaults(
|
|
|
117
119
|
edgeOpenThreshold: 0.3,
|
|
118
120
|
minSize: "22rem",
|
|
119
121
|
maxSize: "96vw",
|
|
120
|
-
glass:
|
|
122
|
+
glass: undefined,
|
|
121
123
|
},
|
|
122
124
|
)
|
|
123
125
|
|
|
126
|
+
const frosted = useGlass(() => props.glass)
|
|
127
|
+
|
|
124
128
|
const emit = defineEmits<{
|
|
125
129
|
(e: "update:open", value: boolean): void
|
|
126
130
|
}>()
|
|
@@ -149,7 +153,7 @@ watch(() => props.open, (open) => (open ? claim() : release()), { immediate: tru
|
|
|
149
153
|
onBeforeUnmount(release)
|
|
150
154
|
|
|
151
155
|
const contentClass = computed(() => {
|
|
152
|
-
const surface =
|
|
156
|
+
const surface = frosted.value ? "lp-glass-overlay" : "bg-surface-raised"
|
|
153
157
|
const base = `fixed z-(--z-modal) flex flex-col border-line ${surface} shadow-panel outline-none`
|
|
154
158
|
const map: Record<Direction, string> = {
|
|
155
159
|
left: "inset-y-0 left-0 border-r rounded-r-card",
|
|
@@ -439,7 +443,7 @@ const previewStyle = computed(() => {
|
|
|
439
443
|
<div
|
|
440
444
|
v-show="peeking || peek > 0"
|
|
441
445
|
class="lp-scrim pointer-events-none fixed inset-0"
|
|
442
|
-
:class="{ 'lp-scrim--light':
|
|
446
|
+
:class="{ 'lp-scrim--light': frosted }"
|
|
443
447
|
:style="{ zIndex: layer.scrim, opacity: peek, transition: peeking || reducedMotion() ? 'none' : 'opacity 0.18s ease-out' }"
|
|
444
448
|
/>
|
|
445
449
|
<div
|
|
@@ -472,7 +476,7 @@ const previewStyle = computed(() => {
|
|
|
472
476
|
<DrawerOverlay
|
|
473
477
|
:style="{ zIndex: layer.scrim }"
|
|
474
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]"
|
|
475
|
-
:class="{ 'lp-scrim--light':
|
|
479
|
+
:class="{ 'lp-scrim--light': frosted }"
|
|
476
480
|
/>
|
|
477
481
|
<DrawerContent
|
|
478
482
|
:class="contentClass"
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
DialogTitle,
|
|
10
10
|
} from "reka-ui"
|
|
11
11
|
import { computed, onBeforeUnmount, ref, useSlots, watch } from "vue"
|
|
12
|
+
import { useGlass } from "../composables/useGlass"
|
|
12
13
|
import { useShift } from "../composables/useShift"
|
|
13
14
|
import { useModalLayer } from "./modalDepth"
|
|
14
15
|
import { CLOSE_ICON } from "./dropdown"
|
|
@@ -65,13 +66,16 @@ const props = withDefaults(
|
|
|
65
66
|
/**
|
|
66
67
|
* Frosted glass instead of a solid panel: the page shows through,
|
|
67
68
|
* blurred, under a lighter scrim (.lp-glass-overlay, .lp-scrim--light).
|
|
68
|
-
* A look, not a behaviour, so it is the app's call
|
|
69
|
+
* A look, not a behaviour, so it is the app's call: unset follows
|
|
70
|
+
* `provideGlass`, and without one it is off.
|
|
69
71
|
*/
|
|
70
72
|
glass?: boolean
|
|
71
73
|
}>(),
|
|
72
|
-
{ size: "md", autoFocus: "auto", glass:
|
|
74
|
+
{ size: "md", autoFocus: "auto", glass: undefined },
|
|
73
75
|
)
|
|
74
76
|
|
|
77
|
+
const frosted = useGlass(() => props.glass)
|
|
78
|
+
|
|
75
79
|
defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
76
80
|
|
|
77
81
|
// Spread rather than a plain `:aria-describedby` binding: with a description
|
|
@@ -228,7 +232,7 @@ watch(
|
|
|
228
232
|
<DialogRoot :open="open" @update:open="(v) => $emit('update:open', v)">
|
|
229
233
|
<DialogPortal>
|
|
230
234
|
<DialogOverlay
|
|
231
|
-
:class="{ 'lp-scrim--light':
|
|
235
|
+
:class="{ 'lp-scrim--light': frosted }"
|
|
232
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]"
|
|
233
237
|
:style="{ zIndex: layer.scrim }"
|
|
234
238
|
/>
|
|
@@ -298,7 +302,7 @@ watch(
|
|
|
298
302
|
<div
|
|
299
303
|
ref="panelRef"
|
|
300
304
|
class="flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col overflow-hidden rounded-card border border-line shadow-panel"
|
|
301
|
-
:class="[
|
|
305
|
+
:class="[frosted ? 'lp-glass-overlay' : 'bg-surface-raised', widthClass, tweening ? 'transition-[height] duration-fast ease-[var(--ease-emphasized)] motion-reduce:transition-none' : '']"
|
|
302
306
|
:style="{ ...(width ? { width } : {}), ...(hasAside ? { alignSelf: 'stretch' } : {}) }"
|
|
303
307
|
>
|
|
304
308
|
<header v-if="title || $slots.title" class="flex shrink-0 items-start justify-between gap-4 p-5 pb-3">
|
|
@@ -55,7 +55,7 @@ const props = withDefaults(
|
|
|
55
55
|
* column inside the body, and the rail is no longer in the body.
|
|
56
56
|
*/
|
|
57
57
|
fillBody?: boolean
|
|
58
|
-
/** Frosted glass panel, passed to LpModal.
|
|
58
|
+
/** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
|
|
59
59
|
glass?: boolean
|
|
60
60
|
}>(),
|
|
61
61
|
{
|
|
@@ -64,6 +64,7 @@ const props = withDefaults(
|
|
|
64
64
|
mobileBreakpoint: "lg",
|
|
65
65
|
navLabel: "Sections",
|
|
66
66
|
navSide: "start",
|
|
67
|
+
glass: undefined,
|
|
67
68
|
},
|
|
68
69
|
)
|
|
69
70
|
|
|
@@ -2,40 +2,49 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* A keyboard reference, conventionally opened with F1.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Two sources, used together or apart. `groups` is the written list: keys
|
|
6
|
+
* that are not bindings at all (arrows inside a list, Backspace in a palette)
|
|
7
|
+
* and the ones worth knowing before their screen is open. `live` adds every
|
|
8
|
+
* mounted `useHotkeys` binding that carries `help`, so a screen with its own
|
|
9
|
+
* keys documents them while it is open and drops them when it closes. Groups
|
|
10
|
+
* of the same title merge, and a combo is listed once.
|
|
9
11
|
*
|
|
10
|
-
* Combos are written the way they read ("Ctrl+Shift+M", "←→↑↓") and
|
|
11
|
-
* "+" into one <kbd> each.
|
|
12
|
+
* Combos are written the way they read ("Ctrl+Shift+M", "Ctrl++", "←→↑↓") and
|
|
13
|
+
* split on "+" into one <kbd> each.
|
|
12
14
|
*/
|
|
15
|
+
import { computed } from "vue"
|
|
13
16
|
import LpModal from "./LpModal.vue"
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ShortcutGroup {
|
|
23
|
-
title: string
|
|
24
|
-
entries: ShortcutEntry[]
|
|
25
|
-
}
|
|
17
|
+
import {
|
|
18
|
+
activeShortcuts,
|
|
19
|
+
appleModifierLabel,
|
|
20
|
+
mergeShortcutGroups,
|
|
21
|
+
type ShortcutGroup,
|
|
22
|
+
} from "../composables/useHotkeys"
|
|
26
23
|
|
|
27
24
|
const props = withDefaults(
|
|
28
25
|
defineProps<{
|
|
29
26
|
open?: boolean
|
|
30
|
-
groups
|
|
27
|
+
groups?: ShortcutGroup[]
|
|
28
|
+
/** Also list the bindings mounted right now (`useHotkeys` with `help`). */
|
|
29
|
+
live?: boolean
|
|
31
30
|
title?: string
|
|
32
31
|
description?: string
|
|
33
|
-
/** Frosted glass panel, passed to LpModal.
|
|
32
|
+
/** Frosted glass panel, passed to LpModal. Follows `provideGlass` when unset. */
|
|
34
33
|
glass?: boolean
|
|
35
34
|
}>(),
|
|
36
|
-
{ open: false, title: "Keyboard shortcuts" },
|
|
35
|
+
{ open: false, groups: () => [], live: false, title: "Keyboard shortcuts", glass: undefined },
|
|
37
36
|
)
|
|
38
37
|
|
|
38
|
+
/*
|
|
39
|
+
* One key per "+", except a "+" that is the key itself: "Ctrl++" is Ctrl and
|
|
40
|
+
* plus, not Ctrl and two blanks.
|
|
41
|
+
*/
|
|
42
|
+
function keysOf(combo: string): string[] {
|
|
43
|
+
return combo.split(/\+(?!$)/)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const shown = computed(() => (props.live ? mergeShortcutGroups(props.groups, activeShortcuts.value) : props.groups))
|
|
47
|
+
|
|
39
48
|
const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
40
49
|
</script>
|
|
41
50
|
|
|
@@ -49,13 +58,13 @@ const emit = defineEmits<{ (e: "update:open", value: boolean): void }>()
|
|
|
49
58
|
@update:open="(value: boolean) => emit('update:open', value)"
|
|
50
59
|
>
|
|
51
60
|
<div class="shortcut-groups">
|
|
52
|
-
<section v-for="group in
|
|
61
|
+
<section v-for="group in shown" :key="group.title" class="shortcut-group">
|
|
53
62
|
<h3>{{ group.title }}</h3>
|
|
54
63
|
<dl>
|
|
55
64
|
<div v-for="entry in group.entries" :key="entry.combo" class="shortcut-row">
|
|
56
65
|
<dt>{{ entry.label }}</dt>
|
|
57
66
|
<dd>
|
|
58
|
-
<kbd v-for="(key, index) in entry.combo
|
|
67
|
+
<kbd v-for="(key, index) in keysOf(entry.combo)" :key="`${key}-${index}`">
|
|
59
68
|
{{ appleModifierLabel(key.trim()) }}
|
|
60
69
|
</kbd>
|
|
61
70
|
</dd>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { computed, inject, provide, toValue, type ComputedRef, type InjectionKey, type MaybeRefOrGetter } from "vue"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Frosted glass as an app-wide default for overlays (modals, drawers, dialogs,
|
|
5
|
+
* the command palette). An app that wants every overlay frosted says so once
|
|
6
|
+
* at its root instead of on each of dozens of call sites, where the next new
|
|
7
|
+
* dialog would be the one that forgets. An explicit `glass` prop on a single
|
|
8
|
+
* overlay still wins, either way.
|
|
9
|
+
*/
|
|
10
|
+
const GLASS: InjectionKey<MaybeRefOrGetter<boolean>> = Symbol("lp-glass")
|
|
11
|
+
|
|
12
|
+
/** Make `glass` the default for every overlay below the calling component. */
|
|
13
|
+
export function provideGlass(enabled: MaybeRefOrGetter<boolean> = true): void {
|
|
14
|
+
provide(GLASS, enabled)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The glass flag an overlay renders with: its own prop when set, otherwise the
|
|
19
|
+
* nearest `provideGlass`, otherwise off. The prop must default to `undefined`,
|
|
20
|
+
* not `false`, or an unset prop could not be told apart from an explicit off.
|
|
21
|
+
*/
|
|
22
|
+
export function useGlass(own: () => boolean | undefined): ComputedRef<boolean> {
|
|
23
|
+
const inherited = inject(GLASS, false)
|
|
24
|
+
return computed(() => own() ?? toValue(inherited))
|
|
25
|
+
}
|