@leavepulse/ui 0.33.1 → 0.34.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.
Files changed (39) hide show
  1. package/dist/assets/{LpCommandPalette-CEY-pajO.css → LpCommandPalette-BERCHiev.css} +4 -4
  2. package/dist/components/LpActionBar.vue.d.ts +1 -1
  3. package/dist/components/LpAddressInput.vue.d.ts +3 -3
  4. package/dist/components/LpAppShell.vue.d.ts +1 -4
  5. package/dist/components/LpAutocomplete.vue.d.ts +1 -1
  6. package/dist/components/LpBadge.vue.d.ts +2 -2
  7. package/dist/components/LpCodeBlock.vue.d.ts +1 -1
  8. package/dist/components/LpColorPicker.vue.d.ts +4 -9
  9. package/dist/components/LpCommandPalette.vue.d.ts +62 -0
  10. package/dist/components/LpCommandPalette.vue.js +237 -56
  11. package/dist/components/LpDropzone.vue.d.ts +4 -4
  12. package/dist/components/LpFileTree.vue.d.ts +4 -4
  13. package/dist/components/LpFormField.vue.d.ts +1 -1
  14. package/dist/components/LpLaneNode.vue.d.ts +0 -11
  15. package/dist/components/LpLightbox.vue.d.ts +2 -2
  16. package/dist/components/LpLogViewer.vue.d.ts +1 -1
  17. package/dist/components/LpMapPicker.vue.d.ts +1 -1
  18. package/dist/components/LpMediaGallery.vue.d.ts +1 -1
  19. package/dist/components/LpPie.vue.d.ts +4 -22
  20. package/dist/components/LpPopover.vue.d.ts +1 -1
  21. package/dist/components/LpProgress.vue.d.ts +2 -2
  22. package/dist/components/LpRollingText.vue.d.ts +3 -3
  23. package/dist/components/LpSelect.vue.d.ts +6 -6
  24. package/dist/components/LpShift.vue.d.ts +1 -1
  25. package/dist/components/LpShortcutsDialog.vue.d.ts +1 -1
  26. package/dist/components/LpSidebar.vue.d.ts +1 -7
  27. package/dist/components/LpSidebarNav.vue.d.ts +2 -2
  28. package/dist/components/LpSlider.vue.d.ts +2 -2
  29. package/dist/components/LpSparkline.vue.d.ts +2 -2
  30. package/dist/components/LpTable.vue.d.ts +9 -9
  31. package/dist/components/LpTabs.vue.d.ts +1 -1
  32. package/dist/components/LpThemeSwitcher.vue.d.ts +1 -1
  33. package/dist/components/LpTilt.vue.d.ts +1 -1
  34. package/dist/components/LpTooltip.vue.d.ts +1 -1
  35. package/dist/components/LpTopologyCanvas.vue.d.ts +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/package.json +1 -1
  38. package/src/components/LpCommandPalette.vue +369 -21
  39. package/src/index.ts +1 -1
@@ -9,6 +9,22 @@
9
9
  * composable can assemble them from routes/docs/API). Scoring ranks label >
10
10
  * keywords > description; matches are highlighted; the active row scrolls into
11
11
  * view. Open state is v-model:open; a global ⌘K/Ctrl-K toggle is on by default.
12
+ *
13
+ * Three things a palette outgrows the moment it stops being a list of routes,
14
+ * all of them here rather than in each app that hit the wall:
15
+ *
16
+ * search — `commands` is everything the palette knows, so anything living
17
+ * behind an API (people, files, tickets) had no way in. `search` is an
18
+ * async provider: it runs on the typed query, debounced, with the previous
19
+ * request aborted, and its results join the static ones.
20
+ *
21
+ * modes — a flat list cannot ask a second question. A command returning
22
+ * `children` (or a provider of them) pushes a mode: the query resets, a
23
+ * chip names where you are, and Backspace on an empty query pops back out.
24
+ *
25
+ * recency — the list is ordered the same on every open, so the command
26
+ * someone runs hourly sits wherever the alphabet put it. With `recent`
27
+ * bound, picks float to a group of their own.
12
28
  */
13
29
  import {
14
30
  DialogContent,
@@ -18,7 +34,7 @@ import {
18
34
  DialogRoot,
19
35
  DialogTitle,
20
36
  } from "reka-ui"
21
- import { computed, nextTick, ref, watch } from "vue"
37
+ import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from "vue"
22
38
  import LpIcon from "./LpIcon.vue"
23
39
  import LpScrollArea from "./LpScrollArea.vue"
24
40
  import { useHotkeys } from "../composables/useHotkeys"
@@ -42,9 +58,37 @@ export interface Command {
42
58
  * decommission, …) that shouldn't clutter the palette but stay reachable.
43
59
  */
44
60
  hidden?: boolean
61
+ /**
62
+ * Picking this opens a mode instead of closing: a nested list, either given
63
+ * outright or fetched from the query typed inside it. This is what keeps a
64
+ * palette from flattening "restart <service>" into one row per service —
65
+ * and what lets the second step search a list too big to enumerate.
66
+ *
67
+ * `onSelect` still runs when the mode opens, so a command can both act and
68
+ * drill in.
69
+ */
70
+ children?: Command[] | ((query: string) => Command[] | Promise<Command[]>)
71
+ /** Placeholder while inside this command's mode. */
72
+ childPlaceholder?: string
73
+ /**
74
+ * Keep the palette open after this runs. For a command that toggles
75
+ * something the user may want to toggle again (a theme, a filter) without
76
+ * reopening and retyping.
77
+ */
78
+ keepOpen?: boolean
45
79
  onSelect?: () => void
46
80
  }
47
81
 
82
+ /** Footer hints and the mode chip, spelled out so an app can translate them. */
83
+ export interface PaletteLabels {
84
+ navigate: string
85
+ select: string
86
+ close: string
87
+ /** Hint for leaving a mode, shown only while inside one. */
88
+ back: string
89
+ searching: string
90
+ }
91
+
48
92
  const props = withDefaults(
49
93
  defineProps<{
50
94
  open?: boolean
@@ -53,13 +97,66 @@ const props = withDefaults(
53
97
  emptyText?: string
54
98
  /** Bind a global ⌘K / Ctrl-K toggle. */
55
99
  hotkey?: boolean
100
+ /**
101
+ * Commands fetched from the typed query — what reaches the things too many
102
+ * (or too remote) to hand over as a list: people, files, tickets. Called
103
+ * debounced, never for a query shorter than `minChars`, and only its
104
+ * newest call counts: an earlier one that lands late is dropped, so a slow
105
+ * response can't overwrite a fresher list.
106
+ *
107
+ * Results join the static commands, so a palette can do both.
108
+ */
109
+ search?: (query: string, signal: AbortSignal) => Command[] | Promise<Command[]>
110
+ /** Wait after the last keystroke before `search` runs. */
111
+ debounce?: number
112
+ /** Shortest query `search` runs for. */
113
+ minChars?: number
114
+ /**
115
+ * Ids of recently picked commands, newest first — bind it to storage and
116
+ * the palette opens on what this person actually uses. It only reorders
117
+ * commands already in the list; an id that no longer resolves is skipped,
118
+ * so a stored history never resurrects a command that has gone away.
119
+ */
120
+ recent?: string[]
121
+ /** How many recent commands to surface. */
122
+ recentLimit?: number
123
+ /** Headings the palette adds itself. */
124
+ recentLabel?: string
125
+ resultsLabel?: string
126
+ /** Footer hints and the mode chip, spelled out for translation. */
127
+ labels?: Partial<PaletteLabels>
56
128
  }>(),
57
- { placeholder: "Type a command or search…", emptyText: "No results", hotkey: true },
129
+ {
130
+ placeholder: "Type a command or search…",
131
+ emptyText: "No results",
132
+ hotkey: true,
133
+ debounce: 250,
134
+ minChars: 1,
135
+ recentLimit: 5,
136
+ recentLabel: "Recent",
137
+ resultsLabel: "Results",
138
+ },
58
139
  )
59
140
 
141
+ const DEFAULT_LABELS: PaletteLabels = {
142
+ navigate: "navigate",
143
+ select: "select",
144
+ close: "close",
145
+ back: "back",
146
+ searching: "Searching…",
147
+ }
148
+
149
+ const text = computed<PaletteLabels>(() => ({ ...DEFAULT_LABELS, ...props.labels }))
150
+
60
151
  const emit = defineEmits<{
61
152
  (e: "update:open", value: boolean): void
62
153
  (e: "select", command: Command): void
154
+ /**
155
+ * A command ran — id first, because the common use is appending it to a
156
+ * stored recency list. Fires for leaf commands only: opening a mode is
157
+ * navigation, not a pick.
158
+ */
159
+ (e: "run", id: string, command: Command): void
63
160
  }>()
64
161
 
65
162
  const query = ref("")
@@ -67,15 +164,140 @@ const selectedIndex = ref(0)
67
164
  const inputRef = ref<HTMLInputElement | null>(null)
68
165
  const scrollRef = ref<{ viewportEl: HTMLElement | null } | null>(null)
69
166
 
167
+ // ── modes ────────────────────────────────────────────────────
168
+ /*
169
+ * The trail of commands drilled into, outermost first. Empty is the root list.
170
+ * A stack rather than one level because nesting costs nothing here and a
171
+ * two-step action ("container → restart → confirm") is the shape that shows up
172
+ * as soon as a palette does more than navigate.
173
+ *
174
+ * shallowRef: entries are consumer objects that may carry anything (a fetcher,
175
+ * a row of data), and deep-tracking them would walk it on every keystroke.
176
+ */
177
+ const trail = shallowRef<Command[]>([])
178
+ const mode = computed<Command | null>(() => trail.value[trail.value.length - 1] ?? null)
179
+
180
+ /*
181
+ * Commands of the open mode. A static `children` array is used as given; a
182
+ * function is a fetcher and goes through the same async path as `search`,
183
+ * which is what lets the second step be a lookup rather than a list.
184
+ */
185
+ const modeItems = shallowRef<Command[]>([])
186
+
187
+ function enter(c: Command) {
188
+ trail.value = [...trail.value, c]
189
+ query.value = ""
190
+ selectedIndex.value = 0
191
+ modeItems.value = Array.isArray(c.children) ? c.children : []
192
+ refresh()
193
+ // Entering by click leaves the focus on the button that no longer exists,
194
+ // and the dialog takes it — from there the next keystroke goes nowhere. The
195
+ // mode exists to be typed into, so the caret goes back in the box.
196
+ inputRef.value?.focus()
197
+ }
198
+
199
+ /** Unwind the trail to `depth` entries — 0 is the root. */
200
+ function popTo(depth: number) {
201
+ const next = trail.value.slice(0, Math.max(0, depth))
202
+ trail.value = next
203
+ query.value = ""
204
+ selectedIndex.value = 0
205
+ const parent = next[next.length - 1]
206
+ modeItems.value = parent && Array.isArray(parent.children) ? parent.children : []
207
+ refresh()
208
+ inputRef.value?.focus()
209
+ }
210
+
211
+ /** Leave the innermost mode. Returns false at the root, so Escape can close. */
212
+ function back(): boolean {
213
+ if (!trail.value.length) return false
214
+ popTo(trail.value.length - 1)
215
+ return true
216
+ }
217
+
218
+ // ── async results ────────────────────────────────────────────
219
+ /*
220
+ * One in-flight request, shared by `search` at the root and by a fetching
221
+ * `children` inside a mode — they differ only in which function to call.
222
+ *
223
+ * Two races to lose here, and both are lost silently: a stale response
224
+ * overwriting a fresher one, and a response arriving after the palette moved
225
+ * on (closed, or into another mode). The token guards both — the abort signal
226
+ * only asks the consumer to stop, it cannot promise they did.
227
+ */
228
+ const found = shallowRef<Command[]>([])
229
+ const searching = ref(false)
230
+
231
+ let timer: ReturnType<typeof setTimeout> | undefined
232
+ let controller: AbortController | undefined
233
+ let token = 0
234
+
235
+ function fetcher(): ((q: string, signal: AbortSignal) => Command[] | Promise<Command[]>) | null {
236
+ const current = mode.value
237
+ if (current) {
238
+ return typeof current.children === "function"
239
+ ? (q: string) => (current.children as (q: string) => Command[] | Promise<Command[]>)(q)
240
+ : null
241
+ }
242
+ return props.search ?? null
243
+ }
244
+
245
+ function cancel() {
246
+ if (timer) clearTimeout(timer)
247
+ timer = undefined
248
+ controller?.abort()
249
+ controller = undefined
250
+ token += 1
251
+ searching.value = false
252
+ }
253
+
254
+ function refresh() {
255
+ cancel()
256
+ found.value = []
257
+ const run = fetcher()
258
+ if (!run) return
259
+ const q = query.value.trim()
260
+ // A fetching mode runs on an empty query too: opening it should show the
261
+ // first page, not an empty screen waiting for a keystroke. Root search does
262
+ // not — ⌘K would fire a request at every open.
263
+ const inMode = mode.value !== null
264
+ if (!inMode && q.length < Math.max(1, props.minChars)) return
265
+ searching.value = true
266
+ const mine = ++token
267
+ timer = setTimeout(async () => {
268
+ controller = new AbortController()
269
+ try {
270
+ const hits = await run(q, controller.signal)
271
+ if (mine !== token) return
272
+ found.value = hits
273
+ } catch {
274
+ // Abort or a failing provider: the static commands still stand.
275
+ if (mine === token) found.value = []
276
+ } finally {
277
+ if (mine === token) searching.value = false
278
+ }
279
+ }, props.debounce)
280
+ }
281
+
282
+ onBeforeUnmount(cancel)
283
+
70
284
  // ── filtering + scoring ──────────────────────────────────────
285
+ /*
286
+ * What the query filters: the open mode's own children, or the root commands.
287
+ * A mode is a context, not a filter over the parent list — showing the root
288
+ * next to "which service?" is how a palette gets someone to restart the wrong
289
+ * thing.
290
+ */
291
+ const source = computed<Command[]>(() => (mode.value ? modeItems.value : props.commands))
292
+
71
293
  // No query → everything (grouped). With a query, score by where it hits:
72
294
  // label > keywords > description, then alphabetical. Mirrors the app palette.
73
295
  const ranked = computed<Command[]>(() => {
74
296
  const q = query.value.trim().toLowerCase()
75
297
  // Empty query: show everything EXCEPT hidden commands (they only surface once
76
298
  // typed). With a query, hidden commands rank like any other.
77
- if (!q) return props.commands.filter((c) => !c.hidden)
78
- return props.commands
299
+ if (!q) return source.value.filter((c) => !c.hidden)
300
+ return source.value
79
301
  .map((c) => {
80
302
  const label = c.label.toLowerCase().includes(q)
81
303
  const kw = (c.keywords ?? []).some((k) => k.toLowerCase().includes(q))
@@ -88,18 +310,56 @@ const ranked = computed<Command[]>(() => {
88
310
  .map((e) => e.c)
89
311
  })
90
312
 
313
+ /*
314
+ * Fetched results sit under their own heading unless they brought a group of
315
+ * their own, so they never look like they were part of the static list. An id
316
+ * already matched locally is dropped: the same command listed twice is a
317
+ * misclick waiting to happen.
318
+ */
319
+ const fetched = computed<Command[]>(() => {
320
+ if (!found.value.length) return []
321
+ const seen = new Set(ranked.value.map((c) => c.id))
322
+ return found.value
323
+ .filter((c) => !seen.has(c.id))
324
+ .map((c) => (c.group ? c : { ...c, group: props.resultsLabel }))
325
+ })
326
+
327
+ /*
328
+ * Recent picks, newest first, lifted out of the list they already belong to.
329
+ * Only on the root's empty query: once someone types, or drills into a mode,
330
+ * relevance is the better order and a duplicated row is just noise.
331
+ */
332
+ const recentItems = computed<Command[]>(() => {
333
+ if (mode.value || query.value.trim() || !props.recent?.length) return []
334
+ const byId = new Map(props.commands.map((c) => [c.id, c]))
335
+ const out: Command[] = []
336
+ for (const id of props.recent) {
337
+ const c = byId.get(id)
338
+ if (c && !c.disabled && !out.includes(c)) out.push(c)
339
+ if (out.length >= props.recentLimit) break
340
+ }
341
+ return out
342
+ })
343
+
91
344
  // Group preserving first-seen order; ungrouped commands sit under "".
345
+ // Recent first (it answers "the thing I always run"), then the matched list,
346
+ // then whatever the provider found.
92
347
  const grouped = computed<{ name: string; items: Command[] }[]>(() => {
93
348
  const order: string[] = []
94
349
  const map = new Map<string, Command[]>()
95
- for (const c of ranked.value) {
96
- const g = c.group ?? ""
97
- if (!map.has(g)) {
98
- map.set(g, [])
99
- order.push(g)
350
+ const push = (c: Command, group: string) => {
351
+ if (!map.has(group)) {
352
+ map.set(group, [])
353
+ order.push(group)
100
354
  }
101
- map.get(g)!.push(c)
355
+ map.get(group)!.push(c)
102
356
  }
357
+ const recent = recentItems.value
358
+ for (const c of recent) push(c, props.recentLabel)
359
+ // A recent entry is shown once: leaving the copy in its own group would put
360
+ // the same row on screen twice with nothing to tell them apart.
361
+ for (const c of ranked.value) if (!recent.includes(c)) push(c, c.group ?? "")
362
+ for (const c of fetched.value) push(c, c.group ?? "")
103
363
  return order.map((name) => ({ name, items: map.get(name)! }))
104
364
  })
105
365
 
@@ -146,7 +406,10 @@ function highlight(value: string): Part[] {
146
406
  }
147
407
 
148
408
  // ── selection + keyboard ─────────────────────────────────────
149
- watch(query, () => (selectedIndex.value = 0))
409
+ watch(query, () => {
410
+ selectedIndex.value = 0
411
+ refresh()
412
+ })
150
413
  watch(
151
414
  () => flat.value.length,
152
415
  (len) => {
@@ -181,16 +444,61 @@ function run(c: Command | undefined) {
181
444
  if (!c || c.disabled) return
182
445
  emit("select", c)
183
446
  c.onSelect?.()
447
+ // A command with children drills in instead of closing, and is navigation
448
+ // rather than a pick — so it stays out of the recency history.
449
+ if (c.children) {
450
+ enter(c)
451
+ return
452
+ }
453
+ emit("run", c.id, c)
454
+ if (c.keepOpen) {
455
+ // Back to the top of a fresh list: the query that found this command is
456
+ // spent, and leaving it would hide whatever the action just changed.
457
+ query.value = ""
458
+ selectedIndex.value = 0
459
+ inputRef.value?.focus()
460
+ return
461
+ }
184
462
  emit("update:open", false)
185
463
  }
186
464
 
187
- // Reset + focus when opening; clear on close.
465
+ /*
466
+ * Backspace on an empty query leaves the mode. Mirrors how the Slack and
467
+ * Linear palettes behave, and means the way out is the key already under the
468
+ * finger — Escape stays the way out of the palette itself.
469
+ */
470
+ function onBackspace(event: KeyboardEvent) {
471
+ if (query.value) return
472
+ if (back()) event.preventDefault()
473
+ }
474
+
475
+ /*
476
+ * Escape unwinds one mode at a time and only closes at the root, so drilling
477
+ * in three levels does not make leaving one an all-or-nothing choice. reka's
478
+ * Dialog would close on its own, so the key is handled here and declined
479
+ * (stopPropagation) only while there is a mode to pop.
480
+ */
481
+ function onEscape(event: KeyboardEvent) {
482
+ if (!trail.value.length) return
483
+ event.preventDefault()
484
+ event.stopPropagation()
485
+ back()
486
+ }
487
+
488
+ // Reset + focus when opening; clear on close. The trail resets too: reopening
489
+ // into the mode someone left three screens deep is never what they meant by
490
+ // ⌘K, and a pending request from last time is dropped rather than landing in
491
+ // the new list.
188
492
  watch(
189
493
  () => props.open,
190
494
  async (o) => {
495
+ cancel()
496
+ query.value = ""
497
+ selectedIndex.value = 0
498
+ trail.value = []
499
+ modeItems.value = []
500
+ found.value = []
191
501
  if (o) {
192
- query.value = ""
193
- selectedIndex.value = 0
194
502
  await nextTick()
195
503
  inputRef.value?.focus()
196
504
  }
@@ -228,26 +536,52 @@ useHotkeys(() =>
228
536
  <DialogTitle class="sr-only">Command palette</DialogTitle>
229
537
  <DialogDescription class="sr-only">Search and run a command.</DialogDescription>
230
538
 
231
- <!-- Filter -->
539
+ <!-- Filter. The trail sits inside the box as chips, the way a scoped
540
+ search reads everywhere else: what you type is filtered by what is
541
+ to its left. -->
232
542
  <div class="flex shrink-0 items-center gap-2.5 border-b border-line px-4">
233
- <LpIcon name="lucide:search" :size="18" class="shrink-0 text-muted" />
543
+ <LpIcon
544
+ v-if="!searching"
545
+ name="lucide:search"
546
+ :size="18"
547
+ class="shrink-0 text-muted"
548
+ />
549
+ <LpIcon v-else name="lucide:loader-circle" :size="18" class="shrink-0 animate-spin text-muted" />
550
+
551
+ <button
552
+ v-for="(step, i) in trail"
553
+ :key="step.id"
554
+ type="button"
555
+ class="flex shrink-0 items-center gap-1.5 rounded-md bg-brand-soft px-2 py-1 text-xs font-medium text-brand transition-opacity hover:opacity-80"
556
+ :title="text.back"
557
+ @click="popTo(i)"
558
+ >
559
+ <LpIcon v-if="step.icon" :name="step.icon" :size="13" />
560
+ {{ step.label }}
561
+ <LpIcon name="lucide:x" :size="12" class="opacity-60" />
562
+ </button>
563
+
234
564
  <input
235
565
  ref="inputRef"
236
566
  v-model="query"
237
567
  type="text"
238
- :placeholder="placeholder"
568
+ :placeholder="mode?.childPlaceholder ?? placeholder"
239
569
  class="h-12 min-w-0 flex-1 bg-transparent text-sm text-ink outline-none placeholder:text-muted"
240
570
  @keydown.down.prevent="moveDown"
241
571
  @keydown.up.prevent="moveUp"
242
572
  @keydown.enter.prevent="runSelected"
573
+ @keydown.backspace="onBackspace"
574
+ @keydown.esc="onEscape"
243
575
  />
244
576
  <kbd class="shrink-0 rounded border border-line px-1.5 py-0.5 text-[10px] text-muted">esc</kbd>
245
577
  </div>
246
578
 
247
579
  <!-- Results -->
248
580
  <LpScrollArea ref="scrollRef" class="min-h-0 flex-1" content-class="p-2">
581
+ <!-- While a request is out there is nothing to say yet: "no results"
582
+ under a spinner reads as an answer, and it is not one. -->
249
583
  <p v-if="flat.length === 0" class="px-2 py-10 text-center text-sm text-muted">
250
- {{ emptyText }}
584
+ {{ searching ? text.searching : emptyText }}
251
585
  </p>
252
586
 
253
587
  <template v-for="group in grouped" :key="group.name">
@@ -284,6 +618,14 @@ useHotkeys(() =>
284
618
  </span>
285
619
  </span>
286
620
  <kbd v-if="c.shortcut" class="shrink-0 text-xs text-muted">{{ c.shortcut }}</kbd>
621
+ <!-- Drills in rather than runs: worth saying before the click,
622
+ since the two look identical otherwise. -->
623
+ <LpIcon
624
+ v-else-if="c.children"
625
+ name="lucide:chevron-right"
626
+ :size="14"
627
+ class="shrink-0 text-muted"
628
+ />
287
629
  </button>
288
630
  </template>
289
631
  </LpScrollArea>
@@ -295,15 +637,21 @@ useHotkeys(() =>
295
637
  <span class="flex items-center gap-1">
296
638
  <kbd class="rounded border border-line px-1 text-[10px]">↑</kbd>
297
639
  <kbd class="rounded border border-line px-1 text-[10px]">↓</kbd>
298
- navigate
640
+ {{ text.navigate }}
299
641
  </span>
300
642
  <span class="flex items-center gap-1">
301
643
  <kbd class="rounded border border-line px-1 text-[10px]">↵</kbd>
302
- select
644
+ {{ text.select }}
645
+ </span>
646
+ <!-- Only inside a mode: at the root Backspace just edits the query,
647
+ and advertising it there would be a lie. -->
648
+ <span v-if="trail.length" class="flex items-center gap-1">
649
+ <kbd class="rounded border border-line px-1 text-[10px]">⌫</kbd>
650
+ {{ text.back }}
303
651
  </span>
304
652
  <span class="ml-auto flex items-center gap-1">
305
653
  <kbd class="rounded border border-line px-1 text-[10px]">esc</kbd>
306
- close
654
+ {{ text.close }}
307
655
  </span>
308
656
  </div>
309
657
  </DialogContent>
package/src/index.ts CHANGED
@@ -64,7 +64,7 @@ export {
64
64
  } from "./components/color"
65
65
  export type { ColorFormat, Hsv, Rgb } from "./components/color"
66
66
  export { default as LpCommandPalette } from "./components/LpCommandPalette.vue"
67
- export type { Command } from "./components/LpCommandPalette.vue"
67
+ export type { Command, PaletteLabels } from "./components/LpCommandPalette.vue"
68
68
  export { default as LpConfirmDialog } from "./components/LpConfirmDialog.vue"
69
69
  export { default as LpContextMenu } from "./components/LpContextMenu.vue"
70
70
  export type { ContextMenuItemDef } from "./components/LpContextMenu.vue"