@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.
- package/dist/assets/{LpCommandPalette-DDGFwOom.css → LpCommandPalette-DyT6aGdW.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 +7 -5
- 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 +8 -4
- 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
|
@@ -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,12 +868,12 @@ 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
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]"
|
|
872
|
-
:class="[
|
|
876
|
+
:class="[frosted ? 'lp-glass-overlay' : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
|
|
873
877
|
:data-pane="paneOpen ? 'open' : undefined"
|
|
874
878
|
@open-auto-focus.prevent
|
|
875
879
|
>
|
|
@@ -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
|
+
}
|