@leavepulse/ui 0.45.1 → 0.47.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/HighlightText-BwC5xbqn.css +6 -0
- package/dist/assets/{LpCommandPalette-HzAA4gri.css → LpCommandPalette-DmQUsJqA.css} +21 -21
- package/dist/assets/{LpDropzone-i4BdMi1H.css → LpDropzone-Ev6X03zj.css} +12 -12
- package/dist/assets/LpKeys-BxRhfQda.css +36 -0
- package/dist/assets/LpPaletteFacts-B9FdVmIO.css +93 -0
- package/dist/assets/LpPaletteHeader-BVOu1UX5.css +70 -0
- package/dist/assets/LpPalettePreview-CJmAKgYW.css +43 -0
- package/dist/assets/LpPaletteStats-ClE-37MK.css +33 -0
- package/dist/assets/LpPaletteSwatches-CA0B8y2E.css +21 -0
- package/dist/assets/{LpShortcutsDialog-CqYX3yy6.css → LpShortcutsDialog-BUp_BX4s.css} +7 -28
- package/dist/assets/{LpTable-CPwWxTJS.css → LpTable-xWwI4KPZ.css} +17 -17
- package/dist/chunks/HighlightText-BvnYZkhi.js +27 -0
- package/dist/chunks/{LpAddressInput-CJOYGZlI.js → LpAddressInput-iOJhmRzy.js} +1 -1
- package/dist/chunks/{LpAppShell-B07Lf48e.js → LpAppShell-BmMYcEVx.js} +2 -2
- package/dist/chunks/{LpAvatar-C1XStdOo.js → LpAvatar-Cb1jtOVv.js} +22 -4
- package/dist/chunks/{LpCard-C2IvucmX.js → LpCard-BdxkMXjo.js} +1 -1
- package/dist/chunks/{LpColorPicker-zz4VGkG4.js → LpColorPicker-BLqPX5Vw.js} +1 -1
- package/dist/chunks/{LpConfirmDialog-BTVAR4tF.js → LpConfirmDialog-Ca-CTlxT.js} +1 -1
- package/dist/chunks/{LpConfirmHost-Ci_uCHgQ.js → LpConfirmHost-BAaSaT2b.js} +1 -1
- package/dist/chunks/{LpContextMenu-BqkAYa9X.js → LpContextMenu-1cKew_Dc.js} +17 -4
- package/dist/chunks/{LpDrawer-CfRhHwmE.js → LpDrawer-CBFVD624.js} +1 -1
- package/dist/chunks/{LpDropdownMenu-DdJGQzPn.js → LpDropdownMenu-DrpJJBp0.js} +22 -8
- package/dist/chunks/{LpFileTree-CNurOvkV.js → LpFileTree-bk5VjBey.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-D_qRRdsH.js → LpFileTreeNode-BvMSBM7X.js} +1 -1
- package/dist/chunks/{LpLightbox-DSDahF23.js → LpLightbox-EVXzAq62.js} +3 -3
- package/dist/chunks/{LpLogViewer-KypbgOyo.js → LpLogViewer-BDFXzvcl.js} +2 -2
- package/dist/chunks/{LpMediaGallery-YKr-cQPN.js → LpMediaGallery-BnlkN_1f.js} +1 -1
- package/dist/chunks/{LpModal-BzTVeNqe.js → LpModal-JNa3B_Lr.js} +12 -5
- package/dist/chunks/{LpModalNav-CjABSGCu.js → LpModalNav-BAxC9et7.js} +10 -3
- package/dist/chunks/{LpNotificationBell-CCwyJP5C.js → LpNotificationBell-DsqSyMwS.js} +2 -2
- package/dist/chunks/LpPalettePreview-CFav_7co.js +19 -0
- package/dist/chunks/{LpPie-BEJ2SfI6.js → LpPie-BdSnta11.js} +1 -1
- package/dist/chunks/{LpScrollArea-Bry1fdOM.js → LpScrollArea-Hz3-JWUG.js} +23 -5
- package/dist/chunks/{LpSidebar-Bwws_X29.js → LpSidebar-DpEiGi8L.js} +2 -2
- package/dist/chunks/{LpSidebarNav-kx-LdqNU.js → LpSidebarNav-BUuxipX-.js} +2 -2
- package/dist/chunks/{LpThemeSwitcher-B4aVPbNC.js → LpThemeSwitcher-BbYFFT5w.js} +1 -1
- package/dist/chunks/MenuTitle-J3ZDYjpV.js +56 -0
- package/dist/component-names.d.ts +1 -1
- package/dist/components/HighlightText.vue.d.ts +7 -0
- package/dist/components/HighlightText.vue.js +4 -0
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAppShell.vue.js +1 -1
- package/dist/components/LpAvatar.vue.d.ts +12 -0
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpCard.vue.js +1 -1
- package/dist/components/LpChart.vue.js +1 -1
- package/dist/components/LpColorPicker.vue.js +1 -1
- package/dist/components/LpCommandPalette.vue.d.ts +152 -15
- package/dist/components/LpCommandPalette.vue.js +181 -92
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpConfirmHost.vue.js +1 -1
- package/dist/components/LpContextMenu.vue.d.ts +11 -0
- package/dist/components/LpContextMenu.vue.js +1 -1
- package/dist/components/LpDrawer.vue.js +1 -1
- package/dist/components/LpDropdownMenu.vue.d.ts +6 -0
- package/dist/components/LpDropdownMenu.vue.js +1 -1
- package/dist/components/LpDropzone.vue.d.ts +14 -0
- package/dist/components/LpDropzone.vue.js +9 -7
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpKeys.vue.d.ts +22 -0
- package/dist/components/LpKeys.vue.js +52 -0
- package/dist/components/LpLightbox.vue.js +1 -1
- package/dist/components/LpLogViewer.vue.js +1 -1
- package/dist/components/LpMediaGallery.vue.js +1 -1
- package/dist/components/LpModal.vue.d.ts +7 -5
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/LpModalNav.vue.d.ts +5 -3
- package/dist/components/LpModalNav.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpPaletteFacts.vue.d.ts +25 -0
- package/dist/components/LpPaletteFacts.vue.js +85 -0
- package/dist/components/LpPaletteHeader.vue.d.ts +32 -0
- package/dist/components/LpPaletteHeader.vue.js +103 -0
- package/dist/components/LpPalettePreview.vue.d.ts +19 -0
- package/dist/components/LpPalettePreview.vue.js +4 -0
- package/dist/components/LpPaletteStats.vue.d.ts +11 -0
- package/dist/components/LpPaletteStats.vue.js +47 -0
- package/dist/components/LpPaletteSwatches.vue.d.ts +10 -0
- package/dist/components/LpPaletteSwatches.vue.js +34 -0
- package/dist/components/LpPie.vue.js +1 -1
- package/dist/components/LpScrollArea.vue.d.ts +2 -2
- package/dist/components/LpScrollArea.vue.js +1 -1
- package/dist/components/LpShortcutsDialog.vue.js +9 -29
- package/dist/components/LpSidebar.vue.js +1 -1
- package/dist/components/LpSidebarNav.vue.js +1 -1
- package/dist/components/LpTable.vue.js +8 -6
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/components/MenuTitle.vue.d.ts +9 -0
- package/dist/components/MenuTitle.vue.js +4 -0
- package/dist/components/commandPalette.d.ts +157 -0
- package/dist/components/commandPalette.js +103 -0
- package/dist/components/commandSearch.d.ts +32 -0
- package/dist/components/commandSearch.js +48 -0
- package/dist/components/keys.d.ts +2 -0
- package/dist/components/keys.js +11 -0
- package/dist/composables/useClipboard.js +35 -2
- package/dist/index.d.ts +12 -3
- package/dist/index.js +161 -138
- package/package.json +1 -1
- package/src/component-names.ts +6 -0
- package/src/components/HighlightText.vue +28 -0
- package/src/components/LpAvatar.vue +21 -2
- package/src/components/LpCommandPalette.vue +392 -86
- package/src/components/LpContextMenu.vue +19 -1
- package/src/components/LpDropdownMenu.vue +14 -2
- package/src/components/LpDropzone.vue +13 -5
- package/src/components/LpKeys.vue +79 -0
- package/src/components/LpLightbox.vue +4 -1
- package/src/components/LpModal.vue +7 -1
- package/src/components/LpModalNav.vue +4 -0
- package/src/components/LpPaletteFacts.vue +165 -0
- package/src/components/LpPaletteHeader.vue +155 -0
- package/src/components/LpPalettePreview.vue +77 -0
- package/src/components/LpPaletteStats.vue +81 -0
- package/src/components/LpPaletteSwatches.vue +56 -0
- package/src/components/LpScrollArea.vue +30 -2
- package/src/components/LpShortcutsDialog.vue +2 -45
- package/src/components/LpTable.vue +7 -2
- package/src/components/MenuTitle.vue +34 -0
- package/src/components/commandPalette.ts +264 -0
- package/src/components/commandSearch.ts +79 -0
- package/src/components/keys.ts +13 -0
- package/src/composables/useClipboard.ts +49 -7
- package/src/index.ts +12 -3
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
DialogTitle,
|
|
38
38
|
} from "reka-ui"
|
|
39
39
|
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, useId, watch } from "vue"
|
|
40
|
+
import LpAvatar from "./LpAvatar.vue"
|
|
40
41
|
import LpIcon from "./LpIcon.vue"
|
|
41
42
|
import LpScrollArea from "./LpScrollArea.vue"
|
|
42
43
|
import { useAsyncTask } from "../composables/useAsyncTask"
|
|
@@ -45,7 +46,44 @@ import { useClipboard } from "../composables/useClipboard"
|
|
|
45
46
|
import { useHotkeys } from "../composables/useHotkeys"
|
|
46
47
|
import { useModalLayer } from "./modalDepth"
|
|
47
48
|
import { useLabels } from "../composables/useLpConfig"
|
|
48
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
applyQueryFilters,
|
|
51
|
+
highlightParts,
|
|
52
|
+
parseCommandQuery,
|
|
53
|
+
parsePaletteQuery,
|
|
54
|
+
rankCommandItems,
|
|
55
|
+
type HighlightPart,
|
|
56
|
+
type PaletteQuery,
|
|
57
|
+
} from "./commandSearch"
|
|
58
|
+
import {
|
|
59
|
+
commandHasPreview,
|
|
60
|
+
fallbackRows,
|
|
61
|
+
matchModePrefix,
|
|
62
|
+
modeChipLabel,
|
|
63
|
+
pushRecent,
|
|
64
|
+
readRecent,
|
|
65
|
+
recentVisible,
|
|
66
|
+
resolveRecentItems,
|
|
67
|
+
toRecentItem,
|
|
68
|
+
type RecentEntry,
|
|
69
|
+
type RecentItem,
|
|
70
|
+
} from "./commandPalette"
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A picture in a row's icon slot: an avatar, a project logo, a world's
|
|
74
|
+
* thumbnail. While it loads, and for good if it fails, the row shows
|
|
75
|
+
* `fallback` (initials) or else `icon`, so a dead link never leaves a hole.
|
|
76
|
+
*/
|
|
77
|
+
export interface CommandImage {
|
|
78
|
+
src?: string | null
|
|
79
|
+
alt?: string
|
|
80
|
+
/** Text shown while there is no picture, usually initials. */
|
|
81
|
+
fallback?: string
|
|
82
|
+
/** Icon shown while there is no picture; the command's `icon` when unset. */
|
|
83
|
+
icon?: string
|
|
84
|
+
/** circle for people, soft or square for things. Defaults to soft. */
|
|
85
|
+
shape?: "circle" | "soft" | "square"
|
|
86
|
+
}
|
|
49
87
|
|
|
50
88
|
export interface Command {
|
|
51
89
|
id: string
|
|
@@ -66,6 +104,12 @@ export interface Command {
|
|
|
66
104
|
*/
|
|
67
105
|
highlight?: string[]
|
|
68
106
|
icon?: string
|
|
107
|
+
/**
|
|
108
|
+
* A picture in place of the icon: a URL, or a `CommandImage` with a
|
|
109
|
+
* fallback. Sized like the icon and faded in once loaded; until then, and
|
|
110
|
+
* when it fails, the fallback (or `icon`) holds the slot.
|
|
111
|
+
*/
|
|
112
|
+
image?: string | CommandImage
|
|
69
113
|
/** Right-aligned hint, e.g. "⌘P". */
|
|
70
114
|
shortcut?: string
|
|
71
115
|
/** Extra search terms not shown but matched against the query. */
|
|
@@ -86,11 +130,60 @@ export interface Command {
|
|
|
86
130
|
* and what lets the second step search a list too big to enumerate.
|
|
87
131
|
*
|
|
88
132
|
* `onSelect` still runs when the mode opens, so a command can both act and
|
|
89
|
-
* drill in.
|
|
133
|
+
* drill in. A fetcher is called like `search` (see CommandFetcher): its
|
|
134
|
+
* abort signal may never reach the transport, and an answer that lands
|
|
135
|
+
* after the query moved on or the mode was left is discarded.
|
|
90
136
|
*/
|
|
91
|
-
children?: Command[] |
|
|
137
|
+
children?: Command[] | CommandFetcher
|
|
92
138
|
/** Placeholder while inside this command's mode. */
|
|
93
139
|
childPlaceholder?: string
|
|
140
|
+
/**
|
|
141
|
+
* The mode's name in the chip beside the box, when the row's label is a
|
|
142
|
+
* sentence: "Mods" for a row reading `Search mods for "{query}"`. Without
|
|
143
|
+
* it a `{query}` placeholder and its quotes are dropped from the label.
|
|
144
|
+
*/
|
|
145
|
+
modeLabel?: string
|
|
146
|
+
/**
|
|
147
|
+
* Typed at the start of the box, enters this command's mode directly, and
|
|
148
|
+
* whatever follows the prefix becomes the query inside it: ">" for
|
|
149
|
+
* commands, "mr " for a mod search. Only for a command with `children`,
|
|
150
|
+
* matched against the list on screen (the root, or the open mode's own
|
|
151
|
+
* static children), longest prefix first, letters in any case. Entering
|
|
152
|
+
* this way does not run `onSelect`. The row shows the prefix as its hint
|
|
153
|
+
* when it has no `shortcut`.
|
|
154
|
+
*/
|
|
155
|
+
prefix?: string
|
|
156
|
+
/**
|
|
157
|
+
* Opening this mode takes the query typed so far along, operators and
|
|
158
|
+
* all, so typing "sodium", arrowing to "Search mods" and pressing Enter
|
|
159
|
+
* opens the mod search already searching for sodium instead of starting
|
|
160
|
+
* it empty. Backspace out of the mode restores the query as it was.
|
|
161
|
+
*/
|
|
162
|
+
carryQuery?: boolean
|
|
163
|
+
/**
|
|
164
|
+
* A "search somewhere else" row: listed only while words are typed, for
|
|
165
|
+
* any words (it is not matched against them), after local and fetched
|
|
166
|
+
* results: at the end of the list, or of its group when that group is
|
|
167
|
+
* already listed. `{query}` in the label becomes the
|
|
168
|
+
* typed words without the operators, `Search Modrinth for "{query}"`;
|
|
169
|
+
* `-word` and `in:group` still apply to the row's own label, keywords and
|
|
170
|
+
* group. Usually a mode with `carryQuery`, so picking it runs the search.
|
|
171
|
+
*/
|
|
172
|
+
fallback?: boolean
|
|
173
|
+
/**
|
|
174
|
+
* Keep this out of the recent history. Picks are remembered wherever they
|
|
175
|
+
* are made, inside modes and among fetched results too; a pick that is a
|
|
176
|
+
* setting rather than a destination (a language, a zoom step) can opt out.
|
|
177
|
+
* Rows offered by Tab (`actions`) are never remembered.
|
|
178
|
+
*/
|
|
179
|
+
remember?: boolean
|
|
180
|
+
/**
|
|
181
|
+
* A JSON-safe value stored with this command in the recent history and
|
|
182
|
+
* handed back to `resolveRecent`: what an app needs to rebuild a result
|
|
183
|
+
* that a search fetched (a project id, a hit), since the command's
|
|
184
|
+
* functions cannot be stored.
|
|
185
|
+
*/
|
|
186
|
+
recentData?: unknown
|
|
94
187
|
/**
|
|
95
188
|
* Keep the palette open after this runs. For a command that toggles
|
|
96
189
|
* something the user may want to toggle again (a theme, a filter) without
|
|
@@ -118,9 +211,41 @@ export interface Command {
|
|
|
118
211
|
* breaks are kept. Without it the pane shows the `excerpt`.
|
|
119
212
|
*/
|
|
120
213
|
preview?: string
|
|
214
|
+
/**
|
|
215
|
+
* This row has nothing for the pane, even with a `#preview` slot: the pane
|
|
216
|
+
* folds away while it is highlighted instead of the slot having to draw a
|
|
217
|
+
* placeholder. For a whole class of rows, `hasPreview` says it once.
|
|
218
|
+
*/
|
|
219
|
+
noPreview?: boolean
|
|
121
220
|
onSelect?: () => void
|
|
122
221
|
}
|
|
123
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Commands fetched for a query: `search` at the root, a function `children`
|
|
225
|
+
* inside a mode.
|
|
226
|
+
*
|
|
227
|
+
* `query` is the box's positive words only, in the case they were typed:
|
|
228
|
+
* `sodium -forge in:mods` arrives as "sodium", because a server would treat
|
|
229
|
+
* the operators as words to find. They come in `parsed` instead (a
|
|
230
|
+
* PaletteQuery: `raw`, `text`, lowercased `include`, `exclude` and `groups`)
|
|
231
|
+
* for a provider that can scope or exclude on its side; either way the
|
|
232
|
+
* palette drops returned rows that match an exclusion or miss an `in:` scope.
|
|
233
|
+
*
|
|
234
|
+
* `signal` aborts when the query moves on, the mode is left or the palette
|
|
235
|
+
* closes. It is a courtesy, not a guarantee: hand it to `fetch` and the
|
|
236
|
+
* request is cancelled, but a transport that cannot be cancelled (a Tauri
|
|
237
|
+
* `invoke`, a worker message, most SDKs) never sees it and runs to the end.
|
|
238
|
+
* That is safe: an answer that lands after a newer request was made, or
|
|
239
|
+
* after the palette closed, is discarded and never reaches the list, so a
|
|
240
|
+
* fetcher does not need to check the signal itself. Checking it before an
|
|
241
|
+
* expensive step is only a saving.
|
|
242
|
+
*/
|
|
243
|
+
export type CommandFetcher = (
|
|
244
|
+
query: string,
|
|
245
|
+
signal: AbortSignal,
|
|
246
|
+
parsed: PaletteQuery,
|
|
247
|
+
) => Command[] | Promise<Command[]>
|
|
248
|
+
|
|
124
249
|
/** Footer hints and the mode chip, spelled out so an app can translate them. */
|
|
125
250
|
export type PaletteLabels = {
|
|
126
251
|
navigate: string
|
|
@@ -165,28 +290,46 @@ const props = withDefaults(
|
|
|
165
290
|
* (or too remote) to hand over as a list: people, files, tickets. Called
|
|
166
291
|
* debounced, never for a query shorter than `minChars`, and only its
|
|
167
292
|
* newest call counts: an earlier one that lands late is dropped, so a slow
|
|
168
|
-
* response can't overwrite a fresher list.
|
|
293
|
+
* response can't overwrite a fresher list. The abort signal may never
|
|
294
|
+
* reach the transport (a Tauri invoke cannot be cancelled); the late
|
|
295
|
+
* answer is discarded all the same. See CommandFetcher for what
|
|
296
|
+
* `query` and `parsed` hold: the operators are stripped from the first,
|
|
297
|
+
* and the palette applies them to the results itself.
|
|
169
298
|
*
|
|
170
299
|
* Results join the static commands, so a palette can do both.
|
|
171
300
|
*/
|
|
172
|
-
search?:
|
|
301
|
+
search?: CommandFetcher
|
|
173
302
|
/** Wait after the last keystroke before `search` runs. */
|
|
174
303
|
debounce?: number
|
|
175
|
-
/** Shortest query `search` runs for. */
|
|
304
|
+
/** Shortest query `search` runs for, counted on its positive words. */
|
|
176
305
|
minChars?: number
|
|
177
306
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
307
|
+
* Recently picked commands, newest first — bind it to storage and the
|
|
308
|
+
* palette opens on what this person actually uses. An item is a root
|
|
309
|
+
* command's id, or a RecentEntry for a pick made inside a mode or among
|
|
310
|
+
* fetched results (the `remember` event hands over exactly what to
|
|
311
|
+
* store; `pushRecent` adds it to a list). A root id only reorders
|
|
312
|
+
* commands already in the list. An entry is found again down its path
|
|
313
|
+
* of static children, then through `resolveRecent`, then from its own
|
|
314
|
+
* `href`; what none of them finds is skipped, so a stored history never
|
|
315
|
+
* resurrects a command that has gone away.
|
|
182
316
|
*/
|
|
183
|
-
recent?:
|
|
317
|
+
recent?: RecentItem[]
|
|
184
318
|
/**
|
|
185
319
|
* Let the palette keep the history itself, in localStorage under this key:
|
|
186
|
-
* every
|
|
187
|
-
* `recent` is bound, which
|
|
320
|
+
* every pick is remembered, newest first, root ones as bare ids as before
|
|
321
|
+
* and deeper ones as entries. Ignored while `recent` is bound, which
|
|
322
|
+
* stays the way to store it somewhere else.
|
|
188
323
|
*/
|
|
189
324
|
recentKey?: string
|
|
325
|
+
/**
|
|
326
|
+
* Finds the live command for a remembered pick the palette cannot find
|
|
327
|
+
* itself: a result a fetcher returned, or one inside a mode whose
|
|
328
|
+
* children are fetched. Gets the stored entry, with the command's
|
|
329
|
+
* `recentData` as `data`; returns the command to show and run, or
|
|
330
|
+
* nothing to drop it.
|
|
331
|
+
*/
|
|
332
|
+
resolveRecent?: (entry: RecentEntry) => Command | null | undefined
|
|
190
333
|
/** How many recent commands to surface. */
|
|
191
334
|
recentLimit?: number
|
|
192
335
|
/** Headings the palette adds itself. */
|
|
@@ -207,8 +350,10 @@ const props = withDefaults(
|
|
|
207
350
|
* `preview` text, or the `preview` slot. For palettes that search
|
|
208
351
|
* content rather than commands: the excerpt says why a row matched, the
|
|
209
352
|
* pane lets it be read without leaving the page. It opens only for a row
|
|
210
|
-
* with `preview` or `excerpt` (any row, with the slot
|
|
211
|
-
*
|
|
353
|
+
* with `preview` or `excerpt` (any row, with the slot, unless `hasPreview`
|
|
354
|
+
* or the row's `noPreview` says otherwise): the dialog eases wider, the
|
|
355
|
+
* pane slides in, and the text arrives paragraph by paragraph. A slot
|
|
356
|
+
* that renders nothing for a row falls back to that built-in text.
|
|
212
357
|
* Hidden below the md breakpoint, where there is no room beside the list.
|
|
213
358
|
*
|
|
214
359
|
* The width is an inline style rather than a pair of classes: a base
|
|
@@ -216,6 +361,14 @@ const props = withDefaults(
|
|
|
216
361
|
* build in an order where the base won, and the pane opened squeezed.
|
|
217
362
|
*/
|
|
218
363
|
previewPane?: boolean
|
|
364
|
+
/**
|
|
365
|
+
* Which rows the pane opens for. With a `#preview` slot every row would
|
|
366
|
+
* otherwise qualify, and an app whose previews cover only some kinds of
|
|
367
|
+
* row had to draw a placeholder for the rest. Return false and the pane
|
|
368
|
+
* folds away for that row, as it does for a row without text. A row's
|
|
369
|
+
* own `noPreview` wins over this.
|
|
370
|
+
*/
|
|
371
|
+
hasPreview?: (command: Command) => boolean
|
|
219
372
|
/**
|
|
220
373
|
* Frosted glass instead of a solid panel: the page shows through,
|
|
221
374
|
* blurred, under a lighter scrim (.lp-glass-overlay, .lp-scrim--light).
|
|
@@ -274,6 +427,13 @@ const emit = defineEmits<{
|
|
|
274
427
|
* navigation, not a pick.
|
|
275
428
|
*/
|
|
276
429
|
(e: "run", id: string, command: Command): void
|
|
430
|
+
/**
|
|
431
|
+
* A pick the recent history keeps, as the item to store: a bare id for a
|
|
432
|
+
* root command, a RecentEntry for anything deeper. For apps that bind
|
|
433
|
+
* `recent` and keep the history themselves: `recent = pushRecent(recent,
|
|
434
|
+
* item, 20)`.
|
|
435
|
+
*/
|
|
436
|
+
(e: "remember", item: RecentItem, command: Command): void
|
|
277
437
|
}>()
|
|
278
438
|
|
|
279
439
|
const query = ref("")
|
|
@@ -354,7 +514,9 @@ function onTab(event: KeyboardEvent) {
|
|
|
354
514
|
if (!c || !actions.length) return
|
|
355
515
|
event.preventDefault()
|
|
356
516
|
event.stopPropagation()
|
|
357
|
-
|
|
517
|
+
const actionsMode: Command = { id: `${c.id}:actions`, label: c.label, icon: c.icon, children: actions }
|
|
518
|
+
syntheticModes.add(actionsMode)
|
|
519
|
+
enter(actionsMode)
|
|
358
520
|
}
|
|
359
521
|
|
|
360
522
|
/*
|
|
@@ -382,8 +544,8 @@ watch(
|
|
|
382
544
|
},
|
|
383
545
|
)
|
|
384
546
|
|
|
385
|
-
function enter(c: Command) {
|
|
386
|
-
trailState.value = [...trailState.value, { query:
|
|
547
|
+
function enter(c: Command, parentQuery = query.value) {
|
|
548
|
+
trailState.value = [...trailState.value, { query: parentQuery, selectedId: selectedId.value }]
|
|
387
549
|
trail.value = [...trail.value, c]
|
|
388
550
|
query.value = ""
|
|
389
551
|
select(0)
|
|
@@ -431,7 +593,7 @@ const found = shallowRef<Command[]>([])
|
|
|
431
593
|
const task = useAsyncTask<Command[]>()
|
|
432
594
|
const searching = task.pending
|
|
433
595
|
|
|
434
|
-
function fetcher():
|
|
596
|
+
function fetcher(): CommandFetcher | null {
|
|
435
597
|
const current = mode.value
|
|
436
598
|
if (current) {
|
|
437
599
|
return typeof current.children === "function"
|
|
@@ -459,7 +621,8 @@ function refresh() {
|
|
|
459
621
|
failed.value = false
|
|
460
622
|
const run = fetcher()
|
|
461
623
|
if (!run) return
|
|
462
|
-
const
|
|
624
|
+
const parsed = parsePaletteQuery(query.value.trim())
|
|
625
|
+
const q = parsed.text
|
|
463
626
|
// A fetching mode runs on an empty query too: opening it should show the
|
|
464
627
|
// first page, not an empty screen waiting for a keystroke. Root search does
|
|
465
628
|
// not — ⌘K would fire a request at every open.
|
|
@@ -467,7 +630,7 @@ function refresh() {
|
|
|
467
630
|
if (!inMode && q.length < Math.max(1, props.minChars)) return
|
|
468
631
|
// A failing provider: the static commands still stand, with a line saying
|
|
469
632
|
// that the rest could not be fetched.
|
|
470
|
-
task.run((signal) => run(q, signal), {
|
|
633
|
+
task.run((signal) => run(q, signal, parsed), {
|
|
471
634
|
delay: props.debounce,
|
|
472
635
|
apply: (hits) => (found.value = hits),
|
|
473
636
|
fail: () => {
|
|
@@ -498,9 +661,10 @@ const source = computed<Command[]>(() => (mode.value ? modeItems.value : props.c
|
|
|
498
661
|
*/
|
|
499
662
|
const local = computed<{ items: Command[]; terms: string[] }>(() => {
|
|
500
663
|
const terms = parseCommandQuery(query.value)
|
|
664
|
+
// Fallback rows are not matched against the query; they get their own pass.
|
|
501
665
|
const filtered = query.value.trim()
|
|
502
|
-
? source.value
|
|
503
|
-
: source.value.filter((c) => !c.hidden)
|
|
666
|
+
? source.value.filter((c) => !c.fallback)
|
|
667
|
+
: source.value.filter((c) => !c.hidden && !c.fallback)
|
|
504
668
|
// Empty query: show everything EXCEPT hidden commands (they only surface once
|
|
505
669
|
// typed). With a query, hidden commands rank like any other.
|
|
506
670
|
if (!terms.include.length && !terms.exclude.length && !terms.groups.length) return { items: filtered, terms: [] }
|
|
@@ -519,48 +683,91 @@ const ranked = computed<Command[]>(() => local.value.items)
|
|
|
519
683
|
const fetched = computed<Command[]>(() => {
|
|
520
684
|
if (!found.value.length) return []
|
|
521
685
|
const seen = new Set(ranked.value.map((c) => c.id))
|
|
522
|
-
|
|
686
|
+
const rows = found.value
|
|
523
687
|
.filter((c) => !seen.has(c.id))
|
|
524
688
|
.map((c) => (c.group ? c : { ...c, group: props.resultsLabel }))
|
|
689
|
+
return applyQueryFilters(rows, parseCommandQuery(query.value))
|
|
525
690
|
})
|
|
526
691
|
|
|
692
|
+
/*
|
|
693
|
+
* "Search elsewhere" rows for the typed words, labelled with them. The row
|
|
694
|
+
* on screen is a copy with the label filled in; the command it stands for
|
|
695
|
+
* is what runs and what a mode chip names.
|
|
696
|
+
*/
|
|
697
|
+
const fallbackOf = new WeakMap<Command, Command>()
|
|
698
|
+
const fallbacks = computed<Command[]>(() =>
|
|
699
|
+
fallbackRows(source.value, query.value).map(({ command, label }) => {
|
|
700
|
+
const row = { ...command, label }
|
|
701
|
+
fallbackOf.set(row, command)
|
|
702
|
+
return row
|
|
703
|
+
}),
|
|
704
|
+
)
|
|
705
|
+
|
|
527
706
|
/*
|
|
528
707
|
* Recent picks, newest first, lifted out of the list they already belong to.
|
|
529
708
|
* Only on the root's empty query: once someone types, or drills into a mode,
|
|
530
709
|
* relevance is the better order and a duplicated row is just noise.
|
|
531
710
|
*/
|
|
532
711
|
const RECENT_KEEP = 20
|
|
533
|
-
const ownRecent = ref<
|
|
712
|
+
const ownRecent = ref<RecentItem[]>([])
|
|
534
713
|
watch(
|
|
535
714
|
() => props.recentKey,
|
|
536
715
|
(key) => {
|
|
537
716
|
if (!key || typeof localStorage === "undefined") return
|
|
538
717
|
try {
|
|
539
|
-
|
|
540
|
-
ownRecent.value = Array.isArray(stored) ? stored.filter((v): v is string => typeof v === "string") : []
|
|
718
|
+
ownRecent.value = readRecent(JSON.parse(localStorage.getItem(key) ?? "[]"))
|
|
541
719
|
} catch {
|
|
542
720
|
ownRecent.value = []
|
|
543
721
|
}
|
|
544
722
|
},
|
|
545
723
|
{ immediate: true },
|
|
546
724
|
)
|
|
547
|
-
|
|
725
|
+
|
|
726
|
+
/*
|
|
727
|
+
* Where each recent row was found, so running it again remembers it under
|
|
728
|
+
* the same path rather than as a root pick. Modes the palette makes up
|
|
729
|
+
* itself (Tab's actions) are not destinations, and nothing picked in them
|
|
730
|
+
* is remembered.
|
|
731
|
+
*/
|
|
732
|
+
const recentPath = new WeakMap<Command, string[]>()
|
|
733
|
+
const syntheticModes = new WeakSet<Command>()
|
|
734
|
+
|
|
735
|
+
function remember(c: Command) {
|
|
736
|
+
if (c.remember === false || trail.value.some((step) => syntheticModes.has(step))) return
|
|
737
|
+
const path = recentPath.get(c) ?? trail.value.map((step) => step.id)
|
|
738
|
+
const item = toRecentItem(c, path)
|
|
739
|
+
emit("remember", item, c)
|
|
548
740
|
const key = props.recentKey
|
|
549
741
|
if (!key || props.recent) return
|
|
550
|
-
ownRecent.value =
|
|
742
|
+
ownRecent.value = pushRecent(ownRecent.value, item, RECENT_KEEP)
|
|
551
743
|
try {
|
|
552
744
|
localStorage.setItem(key, JSON.stringify(ownRecent.value))
|
|
553
745
|
} catch {}
|
|
554
746
|
}
|
|
555
|
-
const
|
|
747
|
+
const recentList = computed(() => props.recent ?? (props.recentKey ? ownRecent.value : []))
|
|
748
|
+
|
|
749
|
+
function fromEntry(entry: RecentEntry): Command {
|
|
750
|
+
return { id: entry.id, label: entry.label, description: entry.description, icon: entry.icon, image: entry.image, href: entry.href }
|
|
751
|
+
}
|
|
556
752
|
|
|
753
|
+
/*
|
|
754
|
+
* Shown while no words are typed: at an empty box, and under operators
|
|
755
|
+
* alone, which filter them like any row ("-theme", "in:recent").
|
|
756
|
+
*/
|
|
557
757
|
const recentItems = computed<Command[]>(() => {
|
|
558
|
-
if (mode.value ||
|
|
559
|
-
const
|
|
758
|
+
if (mode.value || !recentList.value.length) return []
|
|
759
|
+
const terms = parseCommandQuery(query.value)
|
|
760
|
+
if (terms.include.length) return []
|
|
761
|
+
const found = resolveRecentItems(recentList.value, props.commands, {
|
|
762
|
+
limit: Number.POSITIVE_INFINITY,
|
|
763
|
+
resolve: props.resolveRecent,
|
|
764
|
+
fromEntry,
|
|
765
|
+
})
|
|
560
766
|
const out: Command[] = []
|
|
561
|
-
for (const
|
|
562
|
-
|
|
563
|
-
|
|
767
|
+
for (const { command, path } of found) {
|
|
768
|
+
if (out.includes(command) || !recentVisible(command, terms, props.recentLabel)) continue
|
|
769
|
+
recentPath.set(command, path)
|
|
770
|
+
out.push(command)
|
|
564
771
|
if (out.length >= props.recentLimit) break
|
|
565
772
|
}
|
|
566
773
|
return out
|
|
@@ -585,6 +792,17 @@ const grouped = computed<{ name: string; items: Command[] }[]>(() => {
|
|
|
585
792
|
// the same row on screen twice with nothing to tell them apart.
|
|
586
793
|
for (const c of ranked.value) if (!recent.includes(c)) push(c, c.group ?? "")
|
|
587
794
|
for (const c of fetched.value) push(c, c.group ?? "")
|
|
795
|
+
// Last, whatever their group: they are the way out when nothing above is it.
|
|
796
|
+
const tail = new Map<string, Command[]>()
|
|
797
|
+
for (const c of fallbacks.value) {
|
|
798
|
+
const group = c.group ?? ""
|
|
799
|
+
if (map.has(group)) map.get(group)!.push(c)
|
|
800
|
+
else tail.set(group, [...(tail.get(group) ?? []), c])
|
|
801
|
+
}
|
|
802
|
+
for (const [group, items] of tail) {
|
|
803
|
+
map.set(group, items)
|
|
804
|
+
order.push(group)
|
|
805
|
+
}
|
|
588
806
|
return order.map((name) => ({ name, items: folded(name, map.get(name)!) }))
|
|
589
807
|
})
|
|
590
808
|
|
|
@@ -594,6 +812,10 @@ const grouped = computed<{ name: string; items: Command[] }[]>(() => {
|
|
|
594
812
|
*/
|
|
595
813
|
const expanded = ref(new Set<string>())
|
|
596
814
|
const moreOf = new WeakMap<Command, string>()
|
|
815
|
+
// The first row each "Show more" hides. Unfolding removes the "Show more" row
|
|
816
|
+
// itself, so the highlight has to be handed to this one: an id that is gone
|
|
817
|
+
// from the list sends the selection back to the top, and the list with it.
|
|
818
|
+
const firstHidden = new WeakMap<Command, string>()
|
|
597
819
|
|
|
598
820
|
/*
|
|
599
821
|
* Folding hides at least two rows: a "Show 1 more" row takes the same space
|
|
@@ -609,6 +831,7 @@ function folded(name: string, items: Command[]): Command[] {
|
|
|
609
831
|
keepOpen: true,
|
|
610
832
|
}
|
|
611
833
|
moreOf.set(more, name)
|
|
834
|
+
firstHidden.set(more, items[limit].id)
|
|
612
835
|
return [...items.slice(0, limit), more]
|
|
613
836
|
}
|
|
614
837
|
|
|
@@ -616,8 +839,14 @@ function folded(name: string, items: Command[]): Command[] {
|
|
|
616
839
|
const flat = computed(() => grouped.value.flatMap((g) => g.items))
|
|
617
840
|
|
|
618
841
|
const slots = defineSlots<{
|
|
619
|
-
/**
|
|
620
|
-
|
|
842
|
+
/**
|
|
843
|
+
* Body of the preview pane for the highlighted command. `highlight(text)`
|
|
844
|
+
* splits any text around the words typed (positive terms only, plus the
|
|
845
|
+
* command's own `highlight` words), the same matching the rows use; the
|
|
846
|
+
* LpPalette* blocks take its result wherever they take text, and
|
|
847
|
+
* `query` is the parsed box for anything else.
|
|
848
|
+
*/
|
|
849
|
+
preview?: (props: { command: Command; highlight: (text: string) => HighlightPart[]; query: PaletteQuery }) => unknown
|
|
621
850
|
}>()
|
|
622
851
|
|
|
623
852
|
const active = computed<Command | undefined>(() => {
|
|
@@ -631,16 +860,55 @@ const previewRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
|
|
|
631
860
|
* beside a list of commands is a column of dead space. With the slot the app
|
|
632
861
|
* decides what to show, so any highlighted row opens it.
|
|
633
862
|
*/
|
|
634
|
-
const
|
|
635
|
-
|
|
863
|
+
const previewable = computed(() =>
|
|
864
|
+
props.previewPane && commandHasPreview(active.value, { slot: !!slots.preview, hasPreview: props.hasPreview }),
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
/*
|
|
868
|
+
* The row the pane shows. It follows the highlight while the highlight has a
|
|
869
|
+
* preview, and lingers for a moment when it lands on a row without one:
|
|
870
|
+
* arrowing through a list where every other row has a preview would
|
|
871
|
+
* otherwise widen and narrow the dialog on every keystroke. Only when the
|
|
872
|
+
* highlight stays on such rows does the pane fold.
|
|
873
|
+
*/
|
|
874
|
+
const PANE_LINGER_MS = 160
|
|
875
|
+
const paneCommand = shallowRef<Command | undefined>()
|
|
876
|
+
let lingerTimer: ReturnType<typeof setTimeout> | undefined
|
|
877
|
+
watch(
|
|
878
|
+
[active, previewable],
|
|
879
|
+
([command, show]) => {
|
|
880
|
+
clearTimeout(lingerTimer)
|
|
881
|
+
if (show) {
|
|
882
|
+
paneCommand.value = command
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
if (!paneCommand.value) return
|
|
886
|
+
lingerTimer = setTimeout(() => (paneCommand.value = undefined), PANE_LINGER_MS)
|
|
887
|
+
},
|
|
888
|
+
{ immediate: true },
|
|
636
889
|
)
|
|
890
|
+
onBeforeUnmount(() => clearTimeout(lingerTimer))
|
|
891
|
+
|
|
892
|
+
const paneOpen = computed(() => props.previewPane && !!paneCommand.value)
|
|
893
|
+
|
|
894
|
+
function paneHighlight(text: string): HighlightPart[] {
|
|
895
|
+
return highlight(text, paneBody.value?.highlight)
|
|
896
|
+
}
|
|
897
|
+
const paneQuery = computed(() => parsePaletteQuery(query.value.trim()))
|
|
898
|
+
|
|
899
|
+
// What the pane draws: the last row it showed, kept while the pane slides out
|
|
900
|
+
// so it leaves with its content instead of emptying first.
|
|
901
|
+
const paneBody = shallowRef<Command | undefined>()
|
|
902
|
+
watch(paneCommand, (command) => {
|
|
903
|
+
if (command) paneBody.value = command
|
|
904
|
+
})
|
|
637
905
|
|
|
638
906
|
/*
|
|
639
907
|
* The text in paragraphs, so they can arrive one after another. A blank line
|
|
640
908
|
* separates them, as in the `preview` the consumer wrote.
|
|
641
909
|
*/
|
|
642
910
|
const paragraphs = computed(() =>
|
|
643
|
-
(
|
|
911
|
+
(paneBody.value?.preview ?? paneBody.value?.excerpt ?? "")
|
|
644
912
|
.split(/\n{2,}/)
|
|
645
913
|
.map((part) => part.trim())
|
|
646
914
|
.filter(Boolean),
|
|
@@ -652,7 +920,7 @@ const paragraphs = computed(() =>
|
|
|
652
920
|
* would otherwise print the same sentences twice, once small and once large.
|
|
653
921
|
*/
|
|
654
922
|
const paneDescription = computed(() => {
|
|
655
|
-
const description =
|
|
923
|
+
const description = paneBody.value?.description?.trim()
|
|
656
924
|
if (!description) return undefined
|
|
657
925
|
const body = paragraphs.value.join(" ")
|
|
658
926
|
return body.startsWith(description) ? undefined : description
|
|
@@ -668,7 +936,7 @@ const STAGGER_MAX = 10
|
|
|
668
936
|
* from the top rather than wherever the previous result was scrolled to.
|
|
669
937
|
*/
|
|
670
938
|
watch(
|
|
671
|
-
() =>
|
|
939
|
+
() => paneCommand.value?.id,
|
|
672
940
|
async () => {
|
|
673
941
|
if (!paneOpen.value) return
|
|
674
942
|
await nextTick()
|
|
@@ -687,46 +955,48 @@ function flatIndexOf(c: Command): number {
|
|
|
687
955
|
}
|
|
688
956
|
|
|
689
957
|
// ── highlight ────────────────────────────────────────────────
|
|
690
|
-
// Split text around query terms (
|
|
691
|
-
// without v-html.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
hit: boolean
|
|
958
|
+
// Split text around the positive query terms (and a row's own `highlight`
|
|
959
|
+
// words) for <mark> wrapping, without v-html. Exclusions are never marked.
|
|
960
|
+
function highlight(value: string, extra: string[] = []): HighlightPart[] {
|
|
961
|
+
return highlightParts(value, [...local.value.terms, ...extra])
|
|
695
962
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
term = t
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
if (at === -1) {
|
|
715
|
-
parts.push({ text: value.slice(i), hit: false })
|
|
716
|
-
break
|
|
717
|
-
}
|
|
718
|
-
if (at > i) parts.push({ text: value.slice(i, at), hit: false })
|
|
719
|
-
parts.push({ text: value.slice(at, at + term.length), hit: true })
|
|
720
|
-
i = at + term.length
|
|
721
|
-
}
|
|
722
|
-
return parts
|
|
963
|
+
|
|
964
|
+
// ── row picture ──────────────────────────────────────────────
|
|
965
|
+
/*
|
|
966
|
+
* A string is shorthand for the src. A picture with nothing to fall back on
|
|
967
|
+
* borrows the command's icon, so a row that had an icon keeps one while the
|
|
968
|
+
* picture loads or after it fails.
|
|
969
|
+
*/
|
|
970
|
+
// The avatar's own radii are drawn for 36px and up; at the icon's size a card
|
|
971
|
+
// radius rounds a square into a blob. Whole literals so Tailwind sees them.
|
|
972
|
+
const IMAGE_RADIUS = { circle: "rounded-pill", soft: "rounded-[6px]", square: "rounded-[3px]" } as const
|
|
973
|
+
|
|
974
|
+
function imageOf(c: Command): CommandImage | null {
|
|
975
|
+
if (!c.image) return null
|
|
976
|
+
const image = typeof c.image === "string" ? { src: c.image } : c.image
|
|
977
|
+
return { ...image, icon: image.icon ?? c.icon }
|
|
723
978
|
}
|
|
724
979
|
|
|
725
980
|
// ── selection + keyboard ─────────────────────────────────────
|
|
726
|
-
watch(query, () => {
|
|
981
|
+
watch(query, (value) => {
|
|
982
|
+
if (enterByPrefix(value)) return
|
|
727
983
|
select(0)
|
|
728
984
|
refresh()
|
|
729
985
|
})
|
|
986
|
+
|
|
987
|
+
/*
|
|
988
|
+
* A prefix typed (or pasted) at the start of the box opens its mode with the
|
|
989
|
+
* rest of the text as the query there. The list it leaves is remembered with
|
|
990
|
+
* an empty query: Backspace out of the mode should land on the plain list,
|
|
991
|
+
* not on the prefix, which would open the mode again on the spot.
|
|
992
|
+
*/
|
|
993
|
+
function enterByPrefix(value: string): boolean {
|
|
994
|
+
const hit = matchModePrefix(value, source.value)
|
|
995
|
+
if (!hit) return false
|
|
996
|
+
enter(hit.command, "")
|
|
997
|
+
query.value = hit.rest
|
|
998
|
+
return true
|
|
999
|
+
}
|
|
730
1000
|
watch([query, trail, () => props.open], () => {
|
|
731
1001
|
expanded.value = new Set()
|
|
732
1002
|
})
|
|
@@ -802,6 +1072,7 @@ function openInNewTab(c: Command) {
|
|
|
802
1072
|
if (!c.href) return
|
|
803
1073
|
window.open(c.href, "_blank", "noopener")
|
|
804
1074
|
emit("run", c.id, c)
|
|
1075
|
+
remember(c)
|
|
805
1076
|
emit("update:open", false)
|
|
806
1077
|
}
|
|
807
1078
|
|
|
@@ -816,12 +1087,14 @@ function onRowClick(event: MouseEvent, c: Command) {
|
|
|
816
1087
|
run(c)
|
|
817
1088
|
}
|
|
818
1089
|
|
|
819
|
-
function run(
|
|
820
|
-
if (!
|
|
1090
|
+
function run(row: Command | undefined) {
|
|
1091
|
+
if (!row || row.disabled) return
|
|
1092
|
+
const c = fallbackOf.get(row) ?? row
|
|
821
1093
|
// The highlight stays on the same index, which is now the first row that
|
|
822
1094
|
// was hidden: the arrows carry on from where the fold was.
|
|
823
|
-
const group = moreOf.get(
|
|
1095
|
+
const group = moreOf.get(row)
|
|
824
1096
|
if (group !== undefined) {
|
|
1097
|
+
selectedId.value = firstHidden.get(row) ?? null
|
|
825
1098
|
expanded.value = new Set([...expanded.value, group])
|
|
826
1099
|
inputRef.value?.focus()
|
|
827
1100
|
return
|
|
@@ -832,11 +1105,13 @@ function run(c: Command | undefined) {
|
|
|
832
1105
|
// A command with children drills in instead of closing, and is navigation
|
|
833
1106
|
// rather than a pick — so it stays out of the recency history.
|
|
834
1107
|
if (c.children) {
|
|
1108
|
+
const carried = query.value
|
|
835
1109
|
enter(c)
|
|
1110
|
+
if (c.carryQuery) query.value = carried
|
|
836
1111
|
return
|
|
837
1112
|
}
|
|
838
1113
|
emit("run", c.id, c)
|
|
839
|
-
|
|
1114
|
+
remember(c)
|
|
840
1115
|
if (c.keepOpen) {
|
|
841
1116
|
// Back to the top of a fresh list: the query that found this command is
|
|
842
1117
|
// spent, and leaving it would hide whatever the action just changed.
|
|
@@ -969,7 +1244,7 @@ useHotkeys(() =>
|
|
|
969
1244
|
@click="popTo(i)"
|
|
970
1245
|
>
|
|
971
1246
|
<LpIcon v-if="step.icon" :name="step.icon" :size="13" />
|
|
972
|
-
{{ step
|
|
1247
|
+
{{ modeChipLabel(step) }}
|
|
973
1248
|
<LpIcon name="lucide:x" :size="12" class="opacity-60" />
|
|
974
1249
|
</button>
|
|
975
1250
|
|
|
@@ -1042,7 +1317,21 @@ useHotkeys(() =>
|
|
|
1042
1317
|
@click="onRowClick($event, c)"
|
|
1043
1318
|
@pointerenter="onPointerEnter(c)"
|
|
1044
1319
|
>
|
|
1045
|
-
|
|
1320
|
+
<!-- A picture takes the icon's slot at the icon's footprint, with
|
|
1321
|
+
a negative margin so a 20px avatar does not make its row
|
|
1322
|
+
taller than its neighbours. -->
|
|
1323
|
+
<LpAvatar
|
|
1324
|
+
v-if="imageOf(c)"
|
|
1325
|
+
:src="imageOf(c)!.src ?? undefined"
|
|
1326
|
+
:alt="imageOf(c)!.alt ?? ''"
|
|
1327
|
+
:fallback="imageOf(c)!.fallback"
|
|
1328
|
+
:icon="imageOf(c)!.icon"
|
|
1329
|
+
:shape="imageOf(c)!.shape ?? 'soft'"
|
|
1330
|
+
:pixel-size="20"
|
|
1331
|
+
class="-my-0.5 -ml-0.5 shrink-0 text-[9px]"
|
|
1332
|
+
:class="IMAGE_RADIUS[imageOf(c)!.shape ?? 'soft']"
|
|
1333
|
+
/>
|
|
1334
|
+
<LpIcon v-else-if="c.icon" :name="c.icon" :size="16" class="shrink-0" :class="{ 'mt-0.5': c.excerpt }" />
|
|
1046
1335
|
<span class="min-w-0 flex-1">
|
|
1047
1336
|
<span class="block truncate font-medium">
|
|
1048
1337
|
<template v-for="(p, pi) in highlight(c.label, c.highlight)" :key="`l${pi}`">
|
|
@@ -1072,6 +1361,12 @@ useHotkeys(() =>
|
|
|
1072
1361
|
</span>
|
|
1073
1362
|
</span>
|
|
1074
1363
|
<kbd v-if="c.shortcut" class="shrink-0 text-xs text-muted">{{ c.shortcut }}</kbd>
|
|
1364
|
+
<!-- The prefix that opens this mode from the box, so the
|
|
1365
|
+
shortcut is learned by seeing it rather than reading docs. -->
|
|
1366
|
+
<kbd
|
|
1367
|
+
v-else-if="c.prefix && c.children"
|
|
1368
|
+
class="shrink-0 rounded border border-line px-1.5 font-mono text-[11px] whitespace-pre text-muted"
|
|
1369
|
+
>{{ c.prefix }}</kbd>
|
|
1075
1370
|
<!-- Drills in rather than runs: worth saying before the click,
|
|
1076
1371
|
since the two look identical otherwise. -->
|
|
1077
1372
|
<LpIcon
|
|
@@ -1112,11 +1407,22 @@ useHotkeys(() =>
|
|
|
1112
1407
|
<LpScrollArea ref="previewRef" class="min-h-0 flex-1" content-class="px-5 py-4">
|
|
1113
1408
|
<!-- Keyed by the row, so moving the highlight replays the
|
|
1114
1409
|
entrance rather than swapping text in place. -->
|
|
1115
|
-
<div v-if="
|
|
1116
|
-
<slot name="preview" :command="
|
|
1410
|
+
<div v-if="paneBody" :key="paneBody.id">
|
|
1411
|
+
<slot name="preview" :command="paneBody" :highlight="paneHighlight" :query="paneQuery">
|
|
1117
1412
|
<div class="lp-cmd-para flex items-center gap-2 text-ink">
|
|
1118
|
-
<
|
|
1119
|
-
|
|
1413
|
+
<LpAvatar
|
|
1414
|
+
v-if="imageOf(paneBody)"
|
|
1415
|
+
:src="imageOf(paneBody)!.src ?? undefined"
|
|
1416
|
+
:alt="imageOf(paneBody)!.alt ?? ''"
|
|
1417
|
+
:fallback="imageOf(paneBody)!.fallback"
|
|
1418
|
+
:icon="imageOf(paneBody)!.icon"
|
|
1419
|
+
:shape="imageOf(paneBody)!.shape ?? 'soft'"
|
|
1420
|
+
:pixel-size="28"
|
|
1421
|
+
class="shrink-0"
|
|
1422
|
+
:class="IMAGE_RADIUS[imageOf(paneBody)!.shape ?? 'soft']"
|
|
1423
|
+
/>
|
|
1424
|
+
<LpIcon v-else-if="paneBody.icon" :name="paneBody.icon" :size="16" class="shrink-0 text-muted" />
|
|
1425
|
+
<h3 class="min-w-0 text-base font-semibold">{{ paneBody.label }}</h3>
|
|
1120
1426
|
</div>
|
|
1121
1427
|
<p v-if="paneDescription" class="lp-cmd-para mt-0.5 text-xs text-muted">
|
|
1122
1428
|
{{ paneDescription }}
|
|
@@ -1127,7 +1433,7 @@ useHotkeys(() =>
|
|
|
1127
1433
|
class="lp-cmd-para mt-3 whitespace-pre-line text-sm leading-relaxed text-ink/90"
|
|
1128
1434
|
:style="{ animationDelay: `${Math.min(i + 1, STAGGER_MAX) * STAGGER_MS}ms` }"
|
|
1129
1435
|
>
|
|
1130
|
-
<template v-for="(p, pi) in highlight(para,
|
|
1436
|
+
<template v-for="(p, pi) in highlight(para, paneBody.highlight)" :key="`p${pi}`">
|
|
1131
1437
|
<mark v-if="p.hit" class="rounded-xs bg-brand/25 text-ink">{{ p.text }}</mark>
|
|
1132
1438
|
<template v-else>{{ p.text }}</template>
|
|
1133
1439
|
</template>
|