@leavepulse/ui 0.41.0 → 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-DyT6aGdW.css → LpCommandPalette-Esv0a-gS.css} +21 -21
- package/dist/components/LpCommandPalette.vue.js +135 -73
- package/dist/composables/useHotkeys.d.ts +2 -0
- package/dist/composables/useHotkeys.js +41 -21
- package/package.json +1 -1
- package/src/components/LpCommandPalette.vue +106 -32
- package/src/composables/useHotkeys.ts +54 -33
|
@@ -5,81 +5,81 @@
|
|
|
5
5
|
* pane from its right edge as it grows. Animating the columns themselves made
|
|
6
6
|
* every line of the list and the pane re-wrap on every frame.
|
|
7
7
|
*/
|
|
8
|
-
.lp-cmd-dialog[data-v-
|
|
8
|
+
.lp-cmd-dialog[data-v-7bcd1140] {
|
|
9
9
|
--lp-cmd-list: min(92vw, 40rem);
|
|
10
10
|
--lp-cmd-pane: max(0px, min(26rem, 96vw - var(--lp-cmd-list)));
|
|
11
11
|
width: var(--lp-cmd-list);
|
|
12
12
|
}
|
|
13
|
-
.lp-cmd-split[data-v-
|
|
13
|
+
.lp-cmd-split[data-v-7bcd1140] {
|
|
14
14
|
transition:
|
|
15
15
|
width 300ms var(--ease-emphasized, ease),
|
|
16
16
|
min-height 300ms var(--ease-emphasized, ease);
|
|
17
17
|
}
|
|
18
18
|
@media (min-width: 768px) {
|
|
19
|
-
.lp-cmd-split .lp-cmd-list[data-v-
|
|
19
|
+
.lp-cmd-split .lp-cmd-list[data-v-7bcd1140] {
|
|
20
20
|
flex: 0 0 auto;
|
|
21
21
|
/* the dialog's 1px border on each side */
|
|
22
22
|
width: calc(var(--lp-cmd-list) - 2px);
|
|
23
23
|
}
|
|
24
|
-
.lp-cmd-dialog[data-pane="open"][data-v-
|
|
24
|
+
.lp-cmd-dialog[data-pane="open"][data-v-7bcd1140] {
|
|
25
25
|
width: calc(var(--lp-cmd-list) + var(--lp-cmd-pane));
|
|
26
26
|
min-height: min(76vh, 36rem);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
.lp-cmd-pane[data-v-
|
|
29
|
+
.lp-cmd-pane[data-v-7bcd1140] {
|
|
30
30
|
width: var(--lp-cmd-pane);
|
|
31
31
|
}
|
|
32
32
|
/*
|
|
33
33
|
* The pane rides in from the right edge while the dialog opens up to it, and
|
|
34
34
|
* leaves the same way, for as long as the dialog takes to close over it.
|
|
35
35
|
*/
|
|
36
|
-
.lp-cmd-pane-enter-active[data-v-
|
|
37
|
-
.lp-cmd-pane-leave-active[data-v-
|
|
36
|
+
.lp-cmd-pane-enter-active[data-v-7bcd1140],
|
|
37
|
+
.lp-cmd-pane-leave-active[data-v-7bcd1140] {
|
|
38
38
|
transition:
|
|
39
39
|
transform 300ms var(--ease-emphasized, ease),
|
|
40
40
|
opacity 300ms ease;
|
|
41
41
|
}
|
|
42
|
-
.lp-cmd-pane-enter-from[data-v-
|
|
43
|
-
.lp-cmd-pane-leave-to[data-v-
|
|
42
|
+
.lp-cmd-pane-enter-from[data-v-7bcd1140],
|
|
43
|
+
.lp-cmd-pane-leave-to[data-v-7bcd1140] {
|
|
44
44
|
opacity: 0;
|
|
45
45
|
transform: translateX(40%);
|
|
46
46
|
}
|
|
47
|
-
.lp-cmd-para[data-v-
|
|
48
|
-
animation: lp-cmd-para-in-
|
|
47
|
+
.lp-cmd-para[data-v-7bcd1140] {
|
|
48
|
+
animation: lp-cmd-para-in-7bcd1140 260ms ease-out both;
|
|
49
49
|
}
|
|
50
|
-
@keyframes lp-cmd-para-in-
|
|
50
|
+
@keyframes lp-cmd-para-in-7bcd1140 {
|
|
51
51
|
from {
|
|
52
52
|
opacity: 0;
|
|
53
53
|
transform: translateY(4px);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
@media (prefers-reduced-motion: reduce) {
|
|
57
|
-
.lp-cmd-split[data-v-
|
|
58
|
-
.lp-cmd-pane-enter-active[data-v-
|
|
59
|
-
.lp-cmd-pane-leave-active[data-v-
|
|
57
|
+
.lp-cmd-split[data-v-7bcd1140],
|
|
58
|
+
.lp-cmd-pane-enter-active[data-v-7bcd1140],
|
|
59
|
+
.lp-cmd-pane-leave-active[data-v-7bcd1140] {
|
|
60
60
|
transition: none;
|
|
61
61
|
}
|
|
62
|
-
.lp-cmd-para[data-v-
|
|
62
|
+
.lp-cmd-para[data-v-7bcd1140] {
|
|
63
63
|
animation: none;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
.lp-cmd-excerpt[data-v-
|
|
66
|
+
.lp-cmd-excerpt[data-v-7bcd1140] {
|
|
67
67
|
display: -webkit-box;
|
|
68
68
|
-webkit-box-orient: vertical;
|
|
69
69
|
overflow: hidden;
|
|
70
70
|
}
|
|
71
71
|
/* Each result eases in; honours reduced motion. */
|
|
72
|
-
.lp-cmd-item[data-v-
|
|
73
|
-
animation: lp-cmd-in-
|
|
72
|
+
.lp-cmd-item[data-v-7bcd1140] {
|
|
73
|
+
animation: lp-cmd-in-7bcd1140 140ms ease-out both;
|
|
74
74
|
}
|
|
75
|
-
@keyframes lp-cmd-in-
|
|
75
|
+
@keyframes lp-cmd-in-7bcd1140 {
|
|
76
76
|
from {
|
|
77
77
|
opacity: 0;
|
|
78
78
|
transform: translateY(3px);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
@media (prefers-reduced-motion: reduce) {
|
|
82
|
-
.lp-cmd-item[data-v-
|
|
82
|
+
.lp-cmd-item[data-v-7bcd1140] {
|
|
83
83
|
animation: none;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
@@ -7,64 +7,63 @@ import { useGlass } from "../composables/useGlass.js";
|
|
|
7
7
|
import { useClipboard } from "../composables/useClipboard.js";
|
|
8
8
|
import { useHotkeys } from "../composables/useHotkeys.js";
|
|
9
9
|
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
|
|
10
|
-
import '../assets/LpCommandPalette-
|
|
10
|
+
import '../assets/LpCommandPalette-Esv0a-gS.css';const _hoisted_1 = { class: "flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
|
|
11
11
|
const _hoisted_2 = ["title", "onClick"];
|
|
12
12
|
const _hoisted_3 = ["aria-activedescendant", "placeholder", "onKeydown"];
|
|
13
|
-
const _hoisted_4 = {
|
|
14
|
-
const _hoisted_5 = {
|
|
13
|
+
const _hoisted_4 = {
|
|
15
14
|
key: 0,
|
|
16
15
|
class: "px-2 py-10 text-center text-sm text-muted"
|
|
17
16
|
};
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
17
|
+
const _hoisted_5 = ["aria-labelledby"];
|
|
18
|
+
const _hoisted_6 = ["id"];
|
|
19
|
+
const _hoisted_7 = { class: "min-w-0 flex-1" };
|
|
20
|
+
const _hoisted_8 = { class: "block truncate font-medium" };
|
|
21
|
+
const _hoisted_9 = {
|
|
23
22
|
key: 0,
|
|
24
23
|
class: "rounded-xs bg-brand/25 text-ink"
|
|
25
24
|
};
|
|
26
|
-
const
|
|
25
|
+
const _hoisted_10 = {
|
|
27
26
|
key: 0,
|
|
28
27
|
class: "mt-0.5 block truncate text-xs text-muted"
|
|
29
28
|
};
|
|
30
|
-
const
|
|
29
|
+
const _hoisted_11 = {
|
|
31
30
|
key: 0,
|
|
32
31
|
class: "rounded-xs bg-brand/20 text-ink"
|
|
33
32
|
};
|
|
34
|
-
const
|
|
33
|
+
const _hoisted_12 = {
|
|
35
34
|
key: 0,
|
|
36
35
|
class: "rounded-xs bg-brand/20 text-ink"
|
|
37
36
|
};
|
|
38
|
-
const
|
|
37
|
+
const _hoisted_13 = {
|
|
39
38
|
key: 1,
|
|
40
39
|
class: "shrink-0 text-xs text-muted"
|
|
41
40
|
};
|
|
42
|
-
const
|
|
41
|
+
const _hoisted_14 = {
|
|
43
42
|
key: 0,
|
|
44
43
|
class: "lp-cmd-pane hidden min-h-0 shrink-0 flex-col border-l border-line md:flex"
|
|
45
44
|
};
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
45
|
+
const _hoisted_15 = { class: "lp-cmd-para flex items-center gap-2 text-ink" };
|
|
46
|
+
const _hoisted_16 = { class: "min-w-0 text-base font-semibold" };
|
|
47
|
+
const _hoisted_17 = {
|
|
49
48
|
key: 0,
|
|
50
49
|
class: "lp-cmd-para mt-0.5 text-xs text-muted"
|
|
51
50
|
};
|
|
52
|
-
const
|
|
51
|
+
const _hoisted_18 = {
|
|
53
52
|
key: 0,
|
|
54
53
|
class: "rounded-xs bg-brand/25 text-ink"
|
|
55
54
|
};
|
|
56
|
-
const
|
|
55
|
+
const _hoisted_19 = { class: "flex shrink-0 items-center gap-4 border-t border-line px-4 py-2.5 text-[11px] text-muted" };
|
|
56
|
+
const _hoisted_20 = { class: "flex items-center gap-1" };
|
|
57
57
|
const _hoisted_21 = { class: "flex items-center gap-1" };
|
|
58
|
-
const _hoisted_22 = {
|
|
59
|
-
const _hoisted_23 = {
|
|
58
|
+
const _hoisted_22 = {
|
|
60
59
|
key: 0,
|
|
61
60
|
class: "flex items-center gap-1"
|
|
62
61
|
};
|
|
63
|
-
const
|
|
62
|
+
const _hoisted_23 = {
|
|
64
63
|
key: 1,
|
|
65
64
|
class: "flex items-center gap-1"
|
|
66
65
|
};
|
|
67
|
-
const
|
|
66
|
+
const _hoisted_24 = { class: "ml-auto flex items-center gap-1" };
|
|
68
67
|
const LATIN = "qwertyuiop[]asdfghjkl;'zxcvbnm,.`";
|
|
69
68
|
const CYRILLIC = "йцукенгшщзхъфывапролджэячсмитьбюё";
|
|
70
69
|
const TYPO_MIN = 5;
|
|
@@ -120,9 +119,11 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
120
119
|
}
|
|
121
120
|
const activeId = computed(() => flat.value.length ? optionId(selectedIndex.value) : void 0);
|
|
122
121
|
const selectedIndex = ref(0);
|
|
122
|
+
const selectedId = ref(null);
|
|
123
123
|
const inputRef = ref(null);
|
|
124
124
|
const scrollRef = ref(null);
|
|
125
125
|
const trail = shallowRef([]);
|
|
126
|
+
const trailState = shallowRef([]);
|
|
126
127
|
const mode = computed(() => trail.value[trail.value.length - 1] ?? null);
|
|
127
128
|
const modeItems = shallowRef([]);
|
|
128
129
|
const { copy } = useClipboard();
|
|
@@ -150,30 +151,37 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
150
151
|
}
|
|
151
152
|
const selectedActions = computed(() => actionsOf(flat.value[selectedIndex.value]));
|
|
152
153
|
function onTab(event) {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
event.stopPropagation();
|
|
153
156
|
if (event.shiftKey) return;
|
|
154
157
|
const c = flat.value[selectedIndex.value];
|
|
155
158
|
const actions = selectedActions.value;
|
|
156
159
|
if (!c || !actions.length) return;
|
|
157
|
-
event.preventDefault();
|
|
158
|
-
event.stopPropagation();
|
|
159
160
|
enter({ id: `${c.id}:actions`, label: c.label, icon: c.icon, children: actions });
|
|
160
161
|
}
|
|
161
162
|
function enter(c) {
|
|
163
|
+
trailState.value = [...trailState.value, { query: query.value, selectedId: selectedId.value }];
|
|
162
164
|
trail.value = [...trail.value, c];
|
|
163
165
|
query.value = "";
|
|
164
|
-
|
|
166
|
+
select(0);
|
|
165
167
|
modeItems.value = Array.isArray(c.children) ? c.children : [];
|
|
166
168
|
refresh();
|
|
167
169
|
inputRef.value?.focus();
|
|
168
170
|
}
|
|
169
171
|
function popTo(depth) {
|
|
172
|
+
const state = trailState.value[depth] || { query: "", selectedId: null };
|
|
170
173
|
const next = trail.value.slice(0, Math.max(0, depth));
|
|
174
|
+
trailState.value = trailState.value.slice(0, Math.max(0, depth));
|
|
171
175
|
trail.value = next;
|
|
172
|
-
query.value = "";
|
|
173
|
-
selectedIndex.value = 0;
|
|
174
176
|
const parent = next[next.length - 1];
|
|
175
177
|
modeItems.value = parent && Array.isArray(parent.children) ? parent.children : [];
|
|
178
|
+
query.value = state.query;
|
|
179
|
+
select(0);
|
|
176
180
|
refresh();
|
|
181
|
+
void nextTick(() => {
|
|
182
|
+
const index = state.selectedId ? flat.value.findIndex((c) => c.id === state.selectedId) : -1;
|
|
183
|
+
select(index >= 0 ? index : 0);
|
|
184
|
+
});
|
|
177
185
|
inputRef.value?.focus();
|
|
178
186
|
}
|
|
179
187
|
function back() {
|
|
@@ -234,12 +242,30 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
234
242
|
if (term.length < TYPO_MIN) return false;
|
|
235
243
|
return label.split(/[^\p{L}\p{N}]+/u).some((word) => withinOne(term, word) || word.length > term.length && withinOne(term, word.slice(0, term.length)));
|
|
236
244
|
}
|
|
245
|
+
const QUERY_TOKEN = /(-?)(?:(in):)?(?:"([^"]*)"|(\S+))/giu;
|
|
246
|
+
function searchTerms(value) {
|
|
247
|
+
const out = { include: [], exclude: [], groups: [] };
|
|
248
|
+
QUERY_TOKEN.lastIndex = 0;
|
|
249
|
+
for (let match; match = QUERY_TOKEN.exec(value); ) {
|
|
250
|
+
const text2 = (match[3] ?? match[4] ?? "").trim().toLowerCase();
|
|
251
|
+
if (!text2) continue;
|
|
252
|
+
if (match[2] && !match[1]) out.groups.push(text2);
|
|
253
|
+
else if (match[1]) out.exclude.push(text2);
|
|
254
|
+
else out.include.push(text2);
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function matches(c, term) {
|
|
259
|
+
return c.label.toLowerCase().includes(term) || (c.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(term)) || (c.description?.toLowerCase().includes(term) ?? false);
|
|
260
|
+
}
|
|
237
261
|
function score(c, terms) {
|
|
238
262
|
const label = c.label.toLowerCase();
|
|
239
263
|
const keywords = (c.keywords ?? []).map((k) => k.toLowerCase());
|
|
240
264
|
const description = c.description?.toLowerCase() ?? "";
|
|
241
|
-
|
|
242
|
-
|
|
265
|
+
const group = c.group?.toLowerCase() ?? "";
|
|
266
|
+
if (terms.exclude.some((term) => matches(c, term)) || terms.groups.some((term) => !group.includes(term))) return 0;
|
|
267
|
+
let total = terms.include.length ? 0 : 1;
|
|
268
|
+
for (const term of terms.include) {
|
|
243
269
|
let hit = (label.includes(term) ? 4 : 0) + (keywords.some((k) => k.includes(term)) ? 3 : 0) + (description.includes(term) ? 2 : 0);
|
|
244
270
|
if (!hit && nearWord(term, label)) hit = 1;
|
|
245
271
|
if (!hit) return 0;
|
|
@@ -250,15 +276,19 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
250
276
|
function rank(items, terms) {
|
|
251
277
|
return items.map((c) => ({ c, score: score(c, terms) })).filter((e) => e.score > 0).sort((a, b) => b.score - a.score || a.c.label.localeCompare(b.c.label)).map((e) => e.c);
|
|
252
278
|
}
|
|
253
|
-
const typedTerms = computed(() => query.value.trim().toLowerCase().split(/\s+/).filter(Boolean));
|
|
254
279
|
const local = computed(() => {
|
|
255
|
-
const terms =
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
280
|
+
const terms = searchTerms(query.value);
|
|
281
|
+
const filtered = query.value.trim() ? source.value : source.value.filter((c) => !c.hidden);
|
|
282
|
+
if (!terms.include.length && !terms.exclude.length && !terms.groups.length) return { items: filtered, terms: [] };
|
|
283
|
+
const items = rank(filtered, terms);
|
|
284
|
+
if (items.length || !props.layoutFallback) return { items, terms: terms.include };
|
|
285
|
+
const swapped = {
|
|
286
|
+
include: terms.include.map(swapLayout),
|
|
287
|
+
exclude: terms.exclude.map(swapLayout),
|
|
288
|
+
groups: terms.groups.map(swapLayout)
|
|
289
|
+
};
|
|
290
|
+
const other = rank(filtered, swapped);
|
|
291
|
+
return other.length ? { items: other, terms: swapped.include } : { items, terms: terms.include };
|
|
262
292
|
});
|
|
263
293
|
const ranked = computed(() => local.value.items);
|
|
264
294
|
const fetched = computed(() => {
|
|
@@ -364,16 +394,17 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
364
394
|
return parts;
|
|
365
395
|
}
|
|
366
396
|
watch(query, () => {
|
|
367
|
-
|
|
397
|
+
select(0);
|
|
368
398
|
refresh();
|
|
369
399
|
});
|
|
370
400
|
watch([query, trail, () => props.open], () => {
|
|
371
401
|
expanded.value = /* @__PURE__ */ new Set();
|
|
372
402
|
});
|
|
373
403
|
watch(
|
|
374
|
-
() => flat.value.
|
|
375
|
-
(
|
|
376
|
-
|
|
404
|
+
() => flat.value.map((c) => c.id).join("|"),
|
|
405
|
+
() => {
|
|
406
|
+
const index = selectedId.value ? flat.value.findIndex((c) => c.id === selectedId.value) : -1;
|
|
407
|
+
select(index >= 0 ? index : 0);
|
|
377
408
|
}
|
|
378
409
|
);
|
|
379
410
|
watch([selectedIndex, () => flat.value.map((c) => c.id).join("|")], async () => {
|
|
@@ -386,13 +417,40 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
386
417
|
);
|
|
387
418
|
el?.scrollIntoView({ block: "nearest" });
|
|
388
419
|
}
|
|
420
|
+
const keyboardSelection = ref(false);
|
|
421
|
+
function select(index, keyboard = false) {
|
|
422
|
+
const len = flat.value.length;
|
|
423
|
+
selectedIndex.value = len ? Math.max(0, Math.min(index, len - 1)) : 0;
|
|
424
|
+
selectedId.value = flat.value[selectedIndex.value]?.id ?? null;
|
|
425
|
+
if (keyboard) keyboardSelection.value = true;
|
|
426
|
+
}
|
|
389
427
|
function moveDown() {
|
|
390
428
|
const len = flat.value.length;
|
|
391
|
-
if (len)
|
|
429
|
+
if (len) select((selectedIndex.value + 1) % len, true);
|
|
392
430
|
}
|
|
393
431
|
function moveUp() {
|
|
394
432
|
const len = flat.value.length;
|
|
395
|
-
if (len)
|
|
433
|
+
if (len) select((selectedIndex.value - 1 + len) % len, true);
|
|
434
|
+
}
|
|
435
|
+
function movePage(direction) {
|
|
436
|
+
select(selectedIndex.value + direction * 8, true);
|
|
437
|
+
}
|
|
438
|
+
function onPaletteKeydown(event) {
|
|
439
|
+
if (event.isComposing) return;
|
|
440
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End", "PageDown", "PageUp"].includes(event.key)) return;
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
event.stopPropagation();
|
|
443
|
+
if (event.key === "ArrowDown") moveDown();
|
|
444
|
+
else if (event.key === "ArrowUp") moveUp();
|
|
445
|
+
else if (event.key === "Home") select(0, true);
|
|
446
|
+
else if (event.key === "End") select(flat.value.length - 1, true);
|
|
447
|
+
else movePage(event.key === "PageDown" ? 1 : -1);
|
|
448
|
+
}
|
|
449
|
+
function onPointerMove() {
|
|
450
|
+
keyboardSelection.value = false;
|
|
451
|
+
}
|
|
452
|
+
function onPointerEnter(c) {
|
|
453
|
+
if (!keyboardSelection.value) select(flatIndexOf(c));
|
|
396
454
|
}
|
|
397
455
|
function runSelected(event) {
|
|
398
456
|
const c = flat.value[selectedIndex.value];
|
|
@@ -431,7 +489,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
431
489
|
emit("run", c.id, c);
|
|
432
490
|
if (c.keepOpen) {
|
|
433
491
|
query.value = "";
|
|
434
|
-
|
|
492
|
+
select(0);
|
|
435
493
|
inputRef.value?.focus();
|
|
436
494
|
return;
|
|
437
495
|
}
|
|
@@ -452,8 +510,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
452
510
|
async (o) => {
|
|
453
511
|
cancel();
|
|
454
512
|
query.value = "";
|
|
455
|
-
|
|
513
|
+
select(0);
|
|
514
|
+
selectedId.value = null;
|
|
456
515
|
trail.value = [];
|
|
516
|
+
trailState.value = [];
|
|
457
517
|
modeItems.value = [];
|
|
458
518
|
found.value = [];
|
|
459
519
|
failed.value = false;
|
|
@@ -490,7 +550,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
490
550
|
class: normalizeClass(["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]", [unref(frosted) ? "lp-glass-overlay" : "bg-surface-raised", __props.previewPane && "lp-cmd-split"]]),
|
|
491
551
|
"data-pane": paneOpen.value ? "open" : void 0,
|
|
492
552
|
onOpenAutoFocus: _cache[2] || (_cache[2] = withModifiers(() => {
|
|
493
|
-
}, ["prevent"]))
|
|
553
|
+
}, ["prevent"])),
|
|
554
|
+
onKeydownCapture: onPaletteKeydown
|
|
494
555
|
}, {
|
|
495
556
|
default: withCtx(() => [
|
|
496
557
|
createVNode(unref(DialogTitle), { class: "sr-only" }, {
|
|
@@ -551,8 +612,6 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
551
612
|
placeholder: mode.value?.childPlaceholder ?? __props.placeholder,
|
|
552
613
|
class: "h-12 min-w-0 flex-1 bg-transparent text-sm text-ink outline-none placeholder:text-muted",
|
|
553
614
|
onKeydown: [
|
|
554
|
-
withKeys(withModifiers(moveDown, ["prevent"]), ["down"]),
|
|
555
|
-
withKeys(withModifiers(moveUp, ["prevent"]), ["up"]),
|
|
556
615
|
withKeys(withModifiers(runSelected, ["prevent"]), ["enter"]),
|
|
557
616
|
withKeys(onBackspace, ["backspace"]),
|
|
558
617
|
withKeys(onEscape, ["esc"]),
|
|
@@ -563,7 +622,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
563
622
|
]),
|
|
564
623
|
_cache[6] || (_cache[6] = createElementVNode("kbd", { class: "shrink-0 rounded border border-line px-1.5 py-0.5 text-[10px] text-muted" }, "esc", -1))
|
|
565
624
|
]),
|
|
566
|
-
createElementVNode("div",
|
|
625
|
+
createElementVNode("div", {
|
|
626
|
+
class: "flex min-h-0 flex-1",
|
|
627
|
+
onPointermove: onPointerMove
|
|
628
|
+
}, [
|
|
567
629
|
createVNode(_sfc_main$2, {
|
|
568
630
|
ref_key: "scrollRef",
|
|
569
631
|
ref: scrollRef,
|
|
@@ -571,7 +633,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
571
633
|
"content-class": "p-2"
|
|
572
634
|
}, {
|
|
573
635
|
default: withCtx(() => [
|
|
574
|
-
flat.value.length === 0 && !failed.value ? (openBlock(), createElementBlock("p",
|
|
636
|
+
flat.value.length === 0 && !failed.value ? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString(unref(searching) ? text.value.searching : __props.emptyText), 1)) : createCommentVNode("", true),
|
|
575
637
|
createElementVNode("div", {
|
|
576
638
|
id: listId,
|
|
577
639
|
role: "listbox"
|
|
@@ -586,7 +648,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
586
648
|
key: 0,
|
|
587
649
|
id: `${listId}-g${gi}`,
|
|
588
650
|
class: "px-2 pb-1 pt-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-muted"
|
|
589
|
-
}, toDisplayString(group.name), 9,
|
|
651
|
+
}, toDisplayString(group.name), 9, _hoisted_6)) : createCommentVNode("", true),
|
|
590
652
|
(openBlock(true), createElementBlock(Fragment, null, renderList(group.items, (c) => {
|
|
591
653
|
return openBlock(), createBlock(resolveDynamicComponent(c.href ? "a" : "button"), {
|
|
592
654
|
id: optionId(flatIndexOf(c)),
|
|
@@ -604,7 +666,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
604
666
|
flatIndexOf(c) === selectedIndex.value ? "bg-brand-soft text-brand" : unref(moreOf).has(c) ? "text-muted hover:bg-surface-soft/60" : "text-ink hover:bg-surface-soft/60"
|
|
605
667
|
]]),
|
|
606
668
|
onClick: ($event) => onRowClick($event, c),
|
|
607
|
-
onPointerenter: ($event) =>
|
|
669
|
+
onPointerenter: ($event) => onPointerEnter(c)
|
|
608
670
|
}, {
|
|
609
671
|
default: withCtx(() => [
|
|
610
672
|
c.icon ? (openBlock(), createBlock(_sfc_main$1, {
|
|
@@ -613,24 +675,24 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
613
675
|
size: 16,
|
|
614
676
|
class: normalizeClass(["shrink-0", { "mt-0.5": c.excerpt }])
|
|
615
677
|
}, null, 8, ["name", "class"])) : createCommentVNode("", true),
|
|
616
|
-
createElementVNode("span",
|
|
617
|
-
createElementVNode("span",
|
|
678
|
+
createElementVNode("span", _hoisted_7, [
|
|
679
|
+
createElementVNode("span", _hoisted_8, [
|
|
618
680
|
(openBlock(true), createElementBlock(Fragment, null, renderList(highlight(c.label, c.highlight), (p, pi) => {
|
|
619
681
|
return openBlock(), createElementBlock(Fragment, {
|
|
620
682
|
key: `l${pi}`
|
|
621
683
|
}, [
|
|
622
|
-
p.hit ? (openBlock(), createElementBlock("mark",
|
|
684
|
+
p.hit ? (openBlock(), createElementBlock("mark", _hoisted_9, toDisplayString(p.text), 1)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
623
685
|
createTextVNode(toDisplayString(p.text), 1)
|
|
624
686
|
], 64))
|
|
625
687
|
], 64);
|
|
626
688
|
}), 128))
|
|
627
689
|
]),
|
|
628
|
-
c.description ? (openBlock(), createElementBlock("span",
|
|
690
|
+
c.description ? (openBlock(), createElementBlock("span", _hoisted_10, [
|
|
629
691
|
(openBlock(true), createElementBlock(Fragment, null, renderList(highlight(c.description, c.highlight), (p, pi) => {
|
|
630
692
|
return openBlock(), createElementBlock(Fragment, {
|
|
631
693
|
key: `d${pi}`
|
|
632
694
|
}, [
|
|
633
|
-
p.hit ? (openBlock(), createElementBlock("mark",
|
|
695
|
+
p.hit ? (openBlock(), createElementBlock("mark", _hoisted_11, toDisplayString(p.text), 1)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
634
696
|
createTextVNode(toDisplayString(p.text), 1)
|
|
635
697
|
], 64))
|
|
636
698
|
], 64);
|
|
@@ -645,14 +707,14 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
645
707
|
return openBlock(), createElementBlock(Fragment, {
|
|
646
708
|
key: `e${pi}`
|
|
647
709
|
}, [
|
|
648
|
-
p.hit ? (openBlock(), createElementBlock("mark",
|
|
710
|
+
p.hit ? (openBlock(), createElementBlock("mark", _hoisted_12, toDisplayString(p.text), 1)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
649
711
|
createTextVNode(toDisplayString(p.text), 1)
|
|
650
712
|
], 64))
|
|
651
713
|
], 64);
|
|
652
714
|
}), 128))
|
|
653
715
|
], 4)) : createCommentVNode("", true)
|
|
654
716
|
]),
|
|
655
|
-
c.shortcut ? (openBlock(), createElementBlock("kbd",
|
|
717
|
+
c.shortcut ? (openBlock(), createElementBlock("kbd", _hoisted_13, toDisplayString(c.shortcut), 1)) : c.children ? (openBlock(), createBlock(_sfc_main$1, {
|
|
656
718
|
key: 2,
|
|
657
719
|
name: "lucide:chevron-right",
|
|
658
720
|
size: 14,
|
|
@@ -662,7 +724,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
662
724
|
_: 2
|
|
663
725
|
}, 1064, ["id", "href", "type", "aria-selected", "aria-disabled", "data-cmd-index", "disabled", "class", "onClick", "onPointerenter"]);
|
|
664
726
|
}), 128))
|
|
665
|
-
], 8,
|
|
727
|
+
], 8, _hoisted_5);
|
|
666
728
|
}), 128))
|
|
667
729
|
]),
|
|
668
730
|
failed.value ? (openBlock(), createElementBlock("div", {
|
|
@@ -690,7 +752,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
690
752
|
}, 512),
|
|
691
753
|
createVNode(Transition, { name: "lp-cmd-pane" }, {
|
|
692
754
|
default: withCtx(() => [
|
|
693
|
-
paneOpen.value ? (openBlock(), createElementBlock("aside",
|
|
755
|
+
paneOpen.value ? (openBlock(), createElementBlock("aside", _hoisted_14, [
|
|
694
756
|
createVNode(_sfc_main$2, {
|
|
695
757
|
ref_key: "previewRef",
|
|
696
758
|
ref: previewRef,
|
|
@@ -702,16 +764,16 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
702
764
|
key: active.value.id
|
|
703
765
|
}, [
|
|
704
766
|
renderSlot(_ctx.$slots, "preview", { command: active.value }, () => [
|
|
705
|
-
createElementVNode("div",
|
|
767
|
+
createElementVNode("div", _hoisted_15, [
|
|
706
768
|
active.value.icon ? (openBlock(), createBlock(_sfc_main$1, {
|
|
707
769
|
key: 0,
|
|
708
770
|
name: active.value.icon,
|
|
709
771
|
size: 16,
|
|
710
772
|
class: "shrink-0 text-muted"
|
|
711
773
|
}, null, 8, ["name"])) : createCommentVNode("", true),
|
|
712
|
-
createElementVNode("h3",
|
|
774
|
+
createElementVNode("h3", _hoisted_16, toDisplayString(active.value.label), 1)
|
|
713
775
|
]),
|
|
714
|
-
active.value.description ? (openBlock(), createElementBlock("p",
|
|
776
|
+
active.value.description ? (openBlock(), createElementBlock("p", _hoisted_17, toDisplayString(active.value.description), 1)) : createCommentVNode("", true),
|
|
715
777
|
(openBlock(true), createElementBlock(Fragment, null, renderList(paragraphs.value, (para, i) => {
|
|
716
778
|
return openBlock(), createElementBlock("p", {
|
|
717
779
|
key: i,
|
|
@@ -722,7 +784,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
722
784
|
return openBlock(), createElementBlock(Fragment, {
|
|
723
785
|
key: `p${pi}`
|
|
724
786
|
}, [
|
|
725
|
-
p.hit ? (openBlock(), createElementBlock("mark",
|
|
787
|
+
p.hit ? (openBlock(), createElementBlock("mark", _hoisted_18, toDisplayString(p.text), 1)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
|
|
726
788
|
createTextVNode(toDisplayString(p.text), 1)
|
|
727
789
|
], 64))
|
|
728
790
|
], 64);
|
|
@@ -738,26 +800,26 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
738
800
|
]),
|
|
739
801
|
_: 3
|
|
740
802
|
})
|
|
741
|
-
]),
|
|
742
|
-
createElementVNode("div",
|
|
743
|
-
createElementVNode("span",
|
|
803
|
+
], 32),
|
|
804
|
+
createElementVNode("div", _hoisted_19, [
|
|
805
|
+
createElementVNode("span", _hoisted_20, [
|
|
744
806
|
_cache[7] || (_cache[7] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "↑", -1)),
|
|
745
807
|
_cache[8] || (_cache[8] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "↓", -1)),
|
|
746
808
|
createTextVNode(" " + toDisplayString(text.value.navigate), 1)
|
|
747
809
|
]),
|
|
748
|
-
createElementVNode("span",
|
|
810
|
+
createElementVNode("span", _hoisted_21, [
|
|
749
811
|
_cache[9] || (_cache[9] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "↵", -1)),
|
|
750
812
|
createTextVNode(" " + toDisplayString(text.value.select), 1)
|
|
751
813
|
]),
|
|
752
|
-
selectedActions.value.length ? (openBlock(), createElementBlock("span",
|
|
814
|
+
selectedActions.value.length ? (openBlock(), createElementBlock("span", _hoisted_22, [
|
|
753
815
|
_cache[10] || (_cache[10] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "tab", -1)),
|
|
754
816
|
createTextVNode(" " + toDisplayString(text.value.actions), 1)
|
|
755
817
|
])) : createCommentVNode("", true),
|
|
756
|
-
trail.value.length ? (openBlock(), createElementBlock("span",
|
|
818
|
+
trail.value.length ? (openBlock(), createElementBlock("span", _hoisted_23, [
|
|
757
819
|
_cache[11] || (_cache[11] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "⌫", -1)),
|
|
758
820
|
createTextVNode(" " + toDisplayString(text.value.back), 1)
|
|
759
821
|
])) : createCommentVNode("", true),
|
|
760
|
-
createElementVNode("span",
|
|
822
|
+
createElementVNode("span", _hoisted_24, [
|
|
761
823
|
_cache[12] || (_cache[12] = createElementVNode("kbd", { class: "rounded border border-line px-1 text-[10px]" }, "esc", -1)),
|
|
762
824
|
createTextVNode(" " + toDisplayString(text.value.close), 1)
|
|
763
825
|
])
|
|
@@ -774,7 +836,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
774
836
|
};
|
|
775
837
|
}
|
|
776
838
|
});
|
|
777
|
-
const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
839
|
+
const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-7bcd1140"]]);
|
|
778
840
|
export {
|
|
779
841
|
LpCommandPalette as default
|
|
780
842
|
};
|
|
@@ -70,6 +70,8 @@ export declare function appleModifierLabel(key: string): string;
|
|
|
70
70
|
* formality.
|
|
71
71
|
*/
|
|
72
72
|
export interface UseHotkeysOptions {
|
|
73
|
+
enabled?: MaybeRefOrGetter<boolean>;
|
|
74
|
+
priority?: MaybeRefOrGetter<number>;
|
|
73
75
|
/**
|
|
74
76
|
* Only act while the focus is inside this element.
|
|
75
77
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shallowReactive, computed, onMounted, onBeforeUnmount
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
69
|
+
mounted.add(entry);
|
|
70
|
+
if (!listening) {
|
|
71
|
+
window.addEventListener("keydown", onKeydown);
|
|
72
|
+
listening = true;
|
|
73
|
+
}
|
|
57
74
|
});
|
|
58
75
|
onBeforeUnmount(() => {
|
|
59
|
-
|
|
60
|
-
mounted.
|
|
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
|
|
84
|
-
|
|
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/package.json
CHANGED
|
@@ -276,6 +276,7 @@ function optionId(index: number): string {
|
|
|
276
276
|
}
|
|
277
277
|
const activeId = computed(() => (flat.value.length ? optionId(selectedIndex.value) : undefined))
|
|
278
278
|
const selectedIndex = ref(0)
|
|
279
|
+
const selectedId = ref<string | null>(null)
|
|
279
280
|
const inputRef = ref<HTMLInputElement | null>(null)
|
|
280
281
|
const scrollRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
|
|
281
282
|
|
|
@@ -290,6 +291,7 @@ const scrollRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
|
|
|
290
291
|
* a row of data), and deep-tracking them would walk it on every keystroke.
|
|
291
292
|
*/
|
|
292
293
|
const trail = shallowRef<Command[]>([])
|
|
294
|
+
const trailState = shallowRef<{ query: string; selectedId: string | null }[]>([])
|
|
293
295
|
const mode = computed<Command | null>(() => trail.value[trail.value.length - 1] ?? null)
|
|
294
296
|
|
|
295
297
|
/*
|
|
@@ -332,21 +334,20 @@ const selectedActions = computed(() => actionsOf(flat.value[selectedIndex.value]
|
|
|
332
334
|
* actions it keeps that default.
|
|
333
335
|
*/
|
|
334
336
|
function onTab(event: KeyboardEvent) {
|
|
337
|
+
event.preventDefault()
|
|
338
|
+
event.stopPropagation()
|
|
335
339
|
if (event.shiftKey) return
|
|
336
340
|
const c = flat.value[selectedIndex.value]
|
|
337
341
|
const actions = selectedActions.value
|
|
338
342
|
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
343
|
enter({ id: `${c.id}:actions`, label: c.label, icon: c.icon, children: actions })
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
function enter(c: Command) {
|
|
347
|
+
trailState.value = [...trailState.value, { query: query.value, selectedId: selectedId.value }]
|
|
347
348
|
trail.value = [...trail.value, c]
|
|
348
349
|
query.value = ""
|
|
349
|
-
|
|
350
|
+
select(0)
|
|
350
351
|
modeItems.value = Array.isArray(c.children) ? c.children : []
|
|
351
352
|
refresh()
|
|
352
353
|
// Entering by click leaves the focus on the button that no longer exists,
|
|
@@ -357,13 +358,19 @@ function enter(c: Command) {
|
|
|
357
358
|
|
|
358
359
|
/** Unwind the trail to `depth` entries — 0 is the root. */
|
|
359
360
|
function popTo(depth: number) {
|
|
361
|
+
const state = trailState.value[depth] || { query: "", selectedId: null }
|
|
360
362
|
const next = trail.value.slice(0, Math.max(0, depth))
|
|
363
|
+
trailState.value = trailState.value.slice(0, Math.max(0, depth))
|
|
361
364
|
trail.value = next
|
|
362
|
-
query.value = ""
|
|
363
|
-
selectedIndex.value = 0
|
|
364
365
|
const parent = next[next.length - 1]
|
|
365
366
|
modeItems.value = parent && Array.isArray(parent.children) ? parent.children : []
|
|
367
|
+
query.value = state.query
|
|
368
|
+
select(0)
|
|
366
369
|
refresh()
|
|
370
|
+
void nextTick(() => {
|
|
371
|
+
const index = state.selectedId ? flat.value.findIndex((c) => c.id === state.selectedId) : -1
|
|
372
|
+
select(index >= 0 ? index : 0)
|
|
373
|
+
})
|
|
367
374
|
inputRef.value?.focus()
|
|
368
375
|
}
|
|
369
376
|
|
|
@@ -487,12 +494,43 @@ function nearWord(term: string, label: string): boolean {
|
|
|
487
494
|
* the same weights the palette always had; a typo match ranks below all of
|
|
488
495
|
* them, so an exact hit is never outranked by a near one.
|
|
489
496
|
*/
|
|
490
|
-
|
|
497
|
+
interface SearchTerms {
|
|
498
|
+
include: string[]
|
|
499
|
+
exclude: string[]
|
|
500
|
+
groups: string[]
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const QUERY_TOKEN = /(-?)(?:(in):)?(?:"([^"]*)"|(\S+))/giu
|
|
504
|
+
|
|
505
|
+
function searchTerms(value: string): SearchTerms {
|
|
506
|
+
const out: SearchTerms = { include: [], exclude: [], groups: [] }
|
|
507
|
+
QUERY_TOKEN.lastIndex = 0
|
|
508
|
+
for (let match; (match = QUERY_TOKEN.exec(value)); ) {
|
|
509
|
+
const text = (match[3] ?? match[4] ?? "").trim().toLowerCase()
|
|
510
|
+
if (!text) continue
|
|
511
|
+
if (match[2] && !match[1]) out.groups.push(text)
|
|
512
|
+
else if (match[1]) out.exclude.push(text)
|
|
513
|
+
else out.include.push(text)
|
|
514
|
+
}
|
|
515
|
+
return out
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function matches(c: Command, term: string): boolean {
|
|
519
|
+
return (
|
|
520
|
+
c.label.toLowerCase().includes(term) ||
|
|
521
|
+
(c.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(term)) ||
|
|
522
|
+
(c.description?.toLowerCase().includes(term) ?? false)
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function score(c: Command, terms: SearchTerms): number {
|
|
491
527
|
const label = c.label.toLowerCase()
|
|
492
528
|
const keywords = (c.keywords ?? []).map((k) => k.toLowerCase())
|
|
493
529
|
const description = c.description?.toLowerCase() ?? ""
|
|
494
|
-
|
|
495
|
-
|
|
530
|
+
const group = c.group?.toLowerCase() ?? ""
|
|
531
|
+
if (terms.exclude.some((term) => matches(c, term)) || terms.groups.some((term) => !group.includes(term))) return 0
|
|
532
|
+
let total = terms.include.length ? 0 : 1
|
|
533
|
+
for (const term of terms.include) {
|
|
496
534
|
let hit =
|
|
497
535
|
(label.includes(term) ? 4 : 0) +
|
|
498
536
|
(keywords.some((k) => k.includes(term)) ? 3 : 0) +
|
|
@@ -504,7 +542,7 @@ function score(c: Command, terms: string[]): number {
|
|
|
504
542
|
return total
|
|
505
543
|
}
|
|
506
544
|
|
|
507
|
-
function rank(items: Command[], terms:
|
|
545
|
+
function rank(items: Command[], terms: SearchTerms): Command[] {
|
|
508
546
|
return items
|
|
509
547
|
.map((c) => ({ c, score: score(c, terms) }))
|
|
510
548
|
.filter((e) => e.score > 0)
|
|
@@ -512,22 +550,27 @@ function rank(items: Command[], terms: string[]): Command[] {
|
|
|
512
550
|
.map((e) => e.c)
|
|
513
551
|
}
|
|
514
552
|
|
|
515
|
-
const typedTerms = computed(() => query.value.trim().toLowerCase().split(/\s+/).filter(Boolean))
|
|
516
|
-
|
|
517
553
|
/*
|
|
518
554
|
* The local match and the terms it matched with — the swapped ones when the
|
|
519
555
|
* layout fallback is what found it, so the highlight marks what actually hit.
|
|
520
556
|
*/
|
|
521
557
|
const local = computed<{ items: Command[]; terms: string[] }>(() => {
|
|
522
|
-
const terms =
|
|
558
|
+
const terms = searchTerms(query.value)
|
|
559
|
+
const filtered = query.value.trim()
|
|
560
|
+
? source.value
|
|
561
|
+
: source.value.filter((c) => !c.hidden)
|
|
523
562
|
// Empty query: show everything EXCEPT hidden commands (they only surface once
|
|
524
563
|
// typed). With a query, hidden commands rank like any other.
|
|
525
|
-
if (!terms.length) return { items:
|
|
526
|
-
const items = rank(
|
|
527
|
-
if (items.length || !props.layoutFallback) return { items, terms }
|
|
528
|
-
const swapped =
|
|
529
|
-
|
|
530
|
-
|
|
564
|
+
if (!terms.include.length && !terms.exclude.length && !terms.groups.length) return { items: filtered, terms: [] }
|
|
565
|
+
const items = rank(filtered, terms)
|
|
566
|
+
if (items.length || !props.layoutFallback) return { items, terms: terms.include }
|
|
567
|
+
const swapped: SearchTerms = {
|
|
568
|
+
include: terms.include.map(swapLayout),
|
|
569
|
+
exclude: terms.exclude.map(swapLayout),
|
|
570
|
+
groups: terms.groups.map(swapLayout),
|
|
571
|
+
}
|
|
572
|
+
const other = rank(filtered, swapped)
|
|
573
|
+
return other.length ? { items: other, terms: swapped.include } : { items, terms: terms.include }
|
|
531
574
|
})
|
|
532
575
|
|
|
533
576
|
const ranked = computed<Command[]>(() => local.value.items)
|
|
@@ -706,16 +749,17 @@ function highlight(value: string, extra: string[] = []): Part[] {
|
|
|
706
749
|
|
|
707
750
|
// ── selection + keyboard ─────────────────────────────────────
|
|
708
751
|
watch(query, () => {
|
|
709
|
-
|
|
752
|
+
select(0)
|
|
710
753
|
refresh()
|
|
711
754
|
})
|
|
712
755
|
watch([query, trail, () => props.open], () => {
|
|
713
756
|
expanded.value = new Set()
|
|
714
757
|
})
|
|
715
758
|
watch(
|
|
716
|
-
() => flat.value.
|
|
717
|
-
(
|
|
718
|
-
|
|
759
|
+
() => flat.value.map((c) => c.id).join("|"),
|
|
760
|
+
() => {
|
|
761
|
+
const index = selectedId.value ? flat.value.findIndex((c) => c.id === selectedId.value) : -1
|
|
762
|
+
select(index >= 0 ? index : 0)
|
|
719
763
|
},
|
|
720
764
|
)
|
|
721
765
|
watch([selectedIndex, () => flat.value.map((c) => c.id).join("|")], async () => {
|
|
@@ -730,13 +774,42 @@ function scrollSelectedIntoView() {
|
|
|
730
774
|
el?.scrollIntoView({ block: "nearest" })
|
|
731
775
|
}
|
|
732
776
|
|
|
777
|
+
const keyboardSelection = ref(false)
|
|
778
|
+
|
|
779
|
+
function select(index: number, keyboard = false) {
|
|
780
|
+
const len = flat.value.length
|
|
781
|
+
selectedIndex.value = len ? Math.max(0, Math.min(index, len - 1)) : 0
|
|
782
|
+
selectedId.value = flat.value[selectedIndex.value]?.id ?? null
|
|
783
|
+
if (keyboard) keyboardSelection.value = true
|
|
784
|
+
}
|
|
785
|
+
|
|
733
786
|
function moveDown() {
|
|
734
787
|
const len = flat.value.length
|
|
735
|
-
if (len)
|
|
788
|
+
if (len) select((selectedIndex.value + 1) % len, true)
|
|
736
789
|
}
|
|
737
790
|
function moveUp() {
|
|
738
791
|
const len = flat.value.length
|
|
739
|
-
if (len)
|
|
792
|
+
if (len) select((selectedIndex.value - 1 + len) % len, true)
|
|
793
|
+
}
|
|
794
|
+
function movePage(direction: -1 | 1) {
|
|
795
|
+
select(selectedIndex.value + direction * 8, true)
|
|
796
|
+
}
|
|
797
|
+
function onPaletteKeydown(event: KeyboardEvent) {
|
|
798
|
+
if (event.isComposing) return
|
|
799
|
+
if (!(["ArrowDown", "ArrowUp", "Home", "End", "PageDown", "PageUp"] as string[]).includes(event.key)) return
|
|
800
|
+
event.preventDefault()
|
|
801
|
+
event.stopPropagation()
|
|
802
|
+
if (event.key === "ArrowDown") moveDown()
|
|
803
|
+
else if (event.key === "ArrowUp") moveUp()
|
|
804
|
+
else if (event.key === "Home") select(0, true)
|
|
805
|
+
else if (event.key === "End") select(flat.value.length - 1, true)
|
|
806
|
+
else movePage(event.key === "PageDown" ? 1 : -1)
|
|
807
|
+
}
|
|
808
|
+
function onPointerMove() {
|
|
809
|
+
keyboardSelection.value = false
|
|
810
|
+
}
|
|
811
|
+
function onPointerEnter(c: Command) {
|
|
812
|
+
if (!keyboardSelection.value) select(flatIndexOf(c))
|
|
740
813
|
}
|
|
741
814
|
function runSelected(event?: KeyboardEvent) {
|
|
742
815
|
const c = flat.value[selectedIndex.value]
|
|
@@ -789,7 +862,7 @@ function run(c: Command | undefined) {
|
|
|
789
862
|
// Back to the top of a fresh list: the query that found this command is
|
|
790
863
|
// spent, and leaving it would hide whatever the action just changed.
|
|
791
864
|
query.value = ""
|
|
792
|
-
|
|
865
|
+
select(0)
|
|
793
866
|
inputRef.value?.focus()
|
|
794
867
|
return
|
|
795
868
|
}
|
|
@@ -833,8 +906,10 @@ watch(
|
|
|
833
906
|
async (o) => {
|
|
834
907
|
cancel()
|
|
835
908
|
query.value = ""
|
|
836
|
-
|
|
909
|
+
select(0)
|
|
910
|
+
selectedId.value = null
|
|
837
911
|
trail.value = []
|
|
912
|
+
trailState.value = []
|
|
838
913
|
modeItems.value = []
|
|
839
914
|
found.value = []
|
|
840
915
|
failed.value = false
|
|
@@ -876,6 +951,7 @@ useHotkeys(() =>
|
|
|
876
951
|
:class="[frosted ? 'lp-glass-overlay' : 'bg-surface-raised', previewPane && 'lp-cmd-split']"
|
|
877
952
|
:data-pane="paneOpen ? 'open' : undefined"
|
|
878
953
|
@open-auto-focus.prevent
|
|
954
|
+
@keydown.capture="onPaletteKeydown"
|
|
879
955
|
>
|
|
880
956
|
<DialogTitle class="sr-only">Command palette</DialogTitle>
|
|
881
957
|
<DialogDescription class="sr-only">Search and run a command.</DialogDescription>
|
|
@@ -916,8 +992,6 @@ useHotkeys(() =>
|
|
|
916
992
|
:aria-activedescendant="activeId"
|
|
917
993
|
:placeholder="mode?.childPlaceholder ?? placeholder"
|
|
918
994
|
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
995
|
@keydown.enter.prevent="runSelected"
|
|
922
996
|
@keydown.backspace="onBackspace"
|
|
923
997
|
@keydown.esc="onEscape"
|
|
@@ -927,7 +1001,7 @@ useHotkeys(() =>
|
|
|
927
1001
|
</div>
|
|
928
1002
|
|
|
929
1003
|
<!-- Results -->
|
|
930
|
-
<div class="flex min-h-0 flex-1">
|
|
1004
|
+
<div class="flex min-h-0 flex-1" @pointermove="onPointerMove">
|
|
931
1005
|
<LpScrollArea ref="scrollRef" class="lp-cmd-list min-h-0 min-w-0 flex-1" content-class="p-2">
|
|
932
1006
|
<!-- While a request is out there is nothing to say yet: "no results"
|
|
933
1007
|
under a spinner reads as an answer, and it is not one. -->
|
|
@@ -974,7 +1048,7 @@ useHotkeys(() =>
|
|
|
974
1048
|
: 'text-ink hover:bg-surface-soft/60',
|
|
975
1049
|
]"
|
|
976
1050
|
@click="onRowClick($event, c)"
|
|
977
|
-
@pointerenter="
|
|
1051
|
+
@pointerenter="onPointerEnter(c)"
|
|
978
1052
|
>
|
|
979
1053
|
<LpIcon v-if="c.icon" :name="c.icon" :size="16" class="shrink-0" :class="{ 'mt-0.5': c.excerpt }" />
|
|
980
1054
|
<span class="min-w-0 flex-1">
|
|
@@ -142,6 +142,8 @@ function matches(event: KeyboardEvent, binding: HotkeyBinding): boolean {
|
|
|
142
142
|
* formality.
|
|
143
143
|
*/
|
|
144
144
|
export interface UseHotkeysOptions {
|
|
145
|
+
enabled?: MaybeRefOrGetter<boolean>
|
|
146
|
+
priority?: MaybeRefOrGetter<number>
|
|
145
147
|
/**
|
|
146
148
|
* Only act while the focus is inside this element.
|
|
147
149
|
*
|
|
@@ -157,52 +159,70 @@ export interface UseHotkeysOptions {
|
|
|
157
159
|
scope?: MaybeRefOrGetter<HTMLElement | null | undefined>
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const node = target instanceof Node ? target : null
|
|
168
|
-
// The element itself counts: a focusable canvas holds the focus without
|
|
169
|
-
// containing anything that could.
|
|
170
|
-
return !!node && (el === node || el.contains(node))
|
|
171
|
-
}
|
|
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
|
+
}
|
|
172
169
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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()) {
|
|
178
190
|
if (inTextEntry && !binding.allowInInput) continue
|
|
179
191
|
if (!matches(event, binding)) continue
|
|
180
|
-
// Claimed only once the handler has run and not declined, so a binding
|
|
181
|
-
// that stands down leaves the key untouched for whoever handles it next.
|
|
182
192
|
if (binding.handler(event) === false) continue
|
|
183
193
|
event.preventDefault()
|
|
184
194
|
return
|
|
185
195
|
}
|
|
186
196
|
}
|
|
197
|
+
}
|
|
187
198
|
|
|
188
|
-
|
|
199
|
+
export function useHotkeys(
|
|
200
|
+
bindings: MaybeRefOrGetter<HotkeyBinding[]>,
|
|
201
|
+
options: UseHotkeysOptions = {},
|
|
202
|
+
) {
|
|
203
|
+
const entry: MountedHotkeys = {
|
|
204
|
+
read: () => toValue(bindings),
|
|
205
|
+
scope: options.scope,
|
|
206
|
+
enabled: options.enabled,
|
|
207
|
+
priority: options.priority,
|
|
208
|
+
order: ++mountOrder,
|
|
209
|
+
}
|
|
189
210
|
onMounted(() => {
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
mounted.add(entry)
|
|
212
|
+
if (!listening) {
|
|
213
|
+
window.addEventListener("keydown", onKeydown)
|
|
214
|
+
listening = true
|
|
215
|
+
}
|
|
192
216
|
})
|
|
193
217
|
onBeforeUnmount(() => {
|
|
194
|
-
|
|
195
|
-
mounted.
|
|
218
|
+
mounted.delete(entry)
|
|
219
|
+
if (!mounted.size && listening) {
|
|
220
|
+
window.removeEventListener("keydown", onKeydown)
|
|
221
|
+
listening = false
|
|
222
|
+
}
|
|
196
223
|
})
|
|
197
224
|
}
|
|
198
225
|
|
|
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
226
|
const KEY_NAMES: Record<string, string> = {
|
|
207
227
|
escape: "Esc",
|
|
208
228
|
enter: "Enter",
|
|
@@ -230,8 +250,9 @@ function comboOf(binding: HotkeyBinding): string {
|
|
|
230
250
|
*/
|
|
231
251
|
export const activeShortcuts = computed<ShortcutGroup[]>(() => {
|
|
232
252
|
const groups = new Map<string, ShortcutEntry[]>()
|
|
233
|
-
for (const
|
|
234
|
-
|
|
253
|
+
for (const entry of mounted) {
|
|
254
|
+
if (toValue(entry.enabled) === false) continue
|
|
255
|
+
for (const binding of entry.read()) {
|
|
235
256
|
if (!binding.help) continue
|
|
236
257
|
const combo = binding.help.combo ?? comboOf(binding)
|
|
237
258
|
const entries = groups.get(binding.help.group) ?? []
|