@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.
Files changed (125) hide show
  1. package/dist/assets/HighlightText-BwC5xbqn.css +6 -0
  2. package/dist/assets/{LpCommandPalette-HzAA4gri.css → LpCommandPalette-DmQUsJqA.css} +21 -21
  3. package/dist/assets/{LpDropzone-i4BdMi1H.css → LpDropzone-Ev6X03zj.css} +12 -12
  4. package/dist/assets/LpKeys-BxRhfQda.css +36 -0
  5. package/dist/assets/LpPaletteFacts-B9FdVmIO.css +93 -0
  6. package/dist/assets/LpPaletteHeader-BVOu1UX5.css +70 -0
  7. package/dist/assets/LpPalettePreview-CJmAKgYW.css +43 -0
  8. package/dist/assets/LpPaletteStats-ClE-37MK.css +33 -0
  9. package/dist/assets/LpPaletteSwatches-CA0B8y2E.css +21 -0
  10. package/dist/assets/{LpShortcutsDialog-CqYX3yy6.css → LpShortcutsDialog-BUp_BX4s.css} +7 -28
  11. package/dist/assets/{LpTable-CPwWxTJS.css → LpTable-xWwI4KPZ.css} +17 -17
  12. package/dist/chunks/HighlightText-BvnYZkhi.js +27 -0
  13. package/dist/chunks/{LpAddressInput-CJOYGZlI.js → LpAddressInput-iOJhmRzy.js} +1 -1
  14. package/dist/chunks/{LpAppShell-B07Lf48e.js → LpAppShell-BmMYcEVx.js} +2 -2
  15. package/dist/chunks/{LpAvatar-C1XStdOo.js → LpAvatar-Cb1jtOVv.js} +22 -4
  16. package/dist/chunks/{LpCard-C2IvucmX.js → LpCard-BdxkMXjo.js} +1 -1
  17. package/dist/chunks/{LpColorPicker-zz4VGkG4.js → LpColorPicker-BLqPX5Vw.js} +1 -1
  18. package/dist/chunks/{LpConfirmDialog-BTVAR4tF.js → LpConfirmDialog-Ca-CTlxT.js} +1 -1
  19. package/dist/chunks/{LpConfirmHost-Ci_uCHgQ.js → LpConfirmHost-BAaSaT2b.js} +1 -1
  20. package/dist/chunks/{LpContextMenu-BqkAYa9X.js → LpContextMenu-1cKew_Dc.js} +17 -4
  21. package/dist/chunks/{LpDrawer-CfRhHwmE.js → LpDrawer-CBFVD624.js} +1 -1
  22. package/dist/chunks/{LpDropdownMenu-DdJGQzPn.js → LpDropdownMenu-DrpJJBp0.js} +22 -8
  23. package/dist/chunks/{LpFileTree-CNurOvkV.js → LpFileTree-bk5VjBey.js} +2 -2
  24. package/dist/chunks/{LpFileTreeNode-D_qRRdsH.js → LpFileTreeNode-BvMSBM7X.js} +1 -1
  25. package/dist/chunks/{LpLightbox-DSDahF23.js → LpLightbox-EVXzAq62.js} +3 -3
  26. package/dist/chunks/{LpLogViewer-KypbgOyo.js → LpLogViewer-BDFXzvcl.js} +2 -2
  27. package/dist/chunks/{LpMediaGallery-YKr-cQPN.js → LpMediaGallery-BnlkN_1f.js} +1 -1
  28. package/dist/chunks/{LpModal-BzTVeNqe.js → LpModal-JNa3B_Lr.js} +12 -5
  29. package/dist/chunks/{LpModalNav-CjABSGCu.js → LpModalNav-BAxC9et7.js} +10 -3
  30. package/dist/chunks/{LpNotificationBell-CCwyJP5C.js → LpNotificationBell-DsqSyMwS.js} +2 -2
  31. package/dist/chunks/LpPalettePreview-CFav_7co.js +19 -0
  32. package/dist/chunks/{LpPie-BEJ2SfI6.js → LpPie-BdSnta11.js} +1 -1
  33. package/dist/chunks/{LpScrollArea-Bry1fdOM.js → LpScrollArea-Hz3-JWUG.js} +23 -5
  34. package/dist/chunks/{LpSidebar-Bwws_X29.js → LpSidebar-DpEiGi8L.js} +2 -2
  35. package/dist/chunks/{LpSidebarNav-kx-LdqNU.js → LpSidebarNav-BUuxipX-.js} +2 -2
  36. package/dist/chunks/{LpThemeSwitcher-B4aVPbNC.js → LpThemeSwitcher-BbYFFT5w.js} +1 -1
  37. package/dist/chunks/MenuTitle-J3ZDYjpV.js +56 -0
  38. package/dist/component-names.d.ts +1 -1
  39. package/dist/components/HighlightText.vue.d.ts +7 -0
  40. package/dist/components/HighlightText.vue.js +4 -0
  41. package/dist/components/LpAddressInput.vue.js +1 -1
  42. package/dist/components/LpAppShell.vue.js +1 -1
  43. package/dist/components/LpAvatar.vue.d.ts +12 -0
  44. package/dist/components/LpAvatar.vue.js +1 -1
  45. package/dist/components/LpCard.vue.js +1 -1
  46. package/dist/components/LpChart.vue.js +1 -1
  47. package/dist/components/LpColorPicker.vue.js +1 -1
  48. package/dist/components/LpCommandPalette.vue.d.ts +152 -15
  49. package/dist/components/LpCommandPalette.vue.js +181 -92
  50. package/dist/components/LpConfirmDialog.vue.js +1 -1
  51. package/dist/components/LpConfirmHost.vue.js +1 -1
  52. package/dist/components/LpContextMenu.vue.d.ts +11 -0
  53. package/dist/components/LpContextMenu.vue.js +1 -1
  54. package/dist/components/LpDrawer.vue.js +1 -1
  55. package/dist/components/LpDropdownMenu.vue.d.ts +6 -0
  56. package/dist/components/LpDropdownMenu.vue.js +1 -1
  57. package/dist/components/LpDropzone.vue.d.ts +14 -0
  58. package/dist/components/LpDropzone.vue.js +9 -7
  59. package/dist/components/LpFileTree.vue.js +1 -1
  60. package/dist/components/LpFileTreeNode.vue.js +1 -1
  61. package/dist/components/LpKeys.vue.d.ts +22 -0
  62. package/dist/components/LpKeys.vue.js +52 -0
  63. package/dist/components/LpLightbox.vue.js +1 -1
  64. package/dist/components/LpLogViewer.vue.js +1 -1
  65. package/dist/components/LpMediaGallery.vue.js +1 -1
  66. package/dist/components/LpModal.vue.d.ts +7 -5
  67. package/dist/components/LpModal.vue.js +1 -1
  68. package/dist/components/LpModalNav.vue.d.ts +5 -3
  69. package/dist/components/LpModalNav.vue.js +1 -1
  70. package/dist/components/LpNotificationBell.vue.js +1 -1
  71. package/dist/components/LpPaletteFacts.vue.d.ts +25 -0
  72. package/dist/components/LpPaletteFacts.vue.js +85 -0
  73. package/dist/components/LpPaletteHeader.vue.d.ts +32 -0
  74. package/dist/components/LpPaletteHeader.vue.js +103 -0
  75. package/dist/components/LpPalettePreview.vue.d.ts +19 -0
  76. package/dist/components/LpPalettePreview.vue.js +4 -0
  77. package/dist/components/LpPaletteStats.vue.d.ts +11 -0
  78. package/dist/components/LpPaletteStats.vue.js +47 -0
  79. package/dist/components/LpPaletteSwatches.vue.d.ts +10 -0
  80. package/dist/components/LpPaletteSwatches.vue.js +34 -0
  81. package/dist/components/LpPie.vue.js +1 -1
  82. package/dist/components/LpScrollArea.vue.d.ts +2 -2
  83. package/dist/components/LpScrollArea.vue.js +1 -1
  84. package/dist/components/LpShortcutsDialog.vue.js +9 -29
  85. package/dist/components/LpSidebar.vue.js +1 -1
  86. package/dist/components/LpSidebarNav.vue.js +1 -1
  87. package/dist/components/LpTable.vue.js +8 -6
  88. package/dist/components/LpThemeSwitcher.vue.js +1 -1
  89. package/dist/components/MenuTitle.vue.d.ts +9 -0
  90. package/dist/components/MenuTitle.vue.js +4 -0
  91. package/dist/components/commandPalette.d.ts +157 -0
  92. package/dist/components/commandPalette.js +103 -0
  93. package/dist/components/commandSearch.d.ts +32 -0
  94. package/dist/components/commandSearch.js +48 -0
  95. package/dist/components/keys.d.ts +2 -0
  96. package/dist/components/keys.js +11 -0
  97. package/dist/composables/useClipboard.js +35 -2
  98. package/dist/index.d.ts +12 -3
  99. package/dist/index.js +161 -138
  100. package/package.json +1 -1
  101. package/src/component-names.ts +6 -0
  102. package/src/components/HighlightText.vue +28 -0
  103. package/src/components/LpAvatar.vue +21 -2
  104. package/src/components/LpCommandPalette.vue +392 -86
  105. package/src/components/LpContextMenu.vue +19 -1
  106. package/src/components/LpDropdownMenu.vue +14 -2
  107. package/src/components/LpDropzone.vue +13 -5
  108. package/src/components/LpKeys.vue +79 -0
  109. package/src/components/LpLightbox.vue +4 -1
  110. package/src/components/LpModal.vue +7 -1
  111. package/src/components/LpModalNav.vue +4 -0
  112. package/src/components/LpPaletteFacts.vue +165 -0
  113. package/src/components/LpPaletteHeader.vue +155 -0
  114. package/src/components/LpPalettePreview.vue +77 -0
  115. package/src/components/LpPaletteStats.vue +81 -0
  116. package/src/components/LpPaletteSwatches.vue +56 -0
  117. package/src/components/LpScrollArea.vue +30 -2
  118. package/src/components/LpShortcutsDialog.vue +2 -45
  119. package/src/components/LpTable.vue +7 -2
  120. package/src/components/MenuTitle.vue +34 -0
  121. package/src/components/commandPalette.ts +264 -0
  122. package/src/components/commandSearch.ts +79 -0
  123. package/src/components/keys.ts +13 -0
  124. package/src/composables/useClipboard.ts +49 -7
  125. package/src/index.ts +12 -3
@@ -9,6 +9,7 @@
9
9
  import { ContextMenuContent, ContextMenuPortal, ContextMenuRoot, ContextMenuTrigger } from "reka-ui"
10
10
  import { computed } from "vue"
11
11
  import ContextMenuItems from "./ContextMenuItems.vue"
12
+ import MenuTitle from "./MenuTitle.vue"
12
13
  import { POPOVER_PANEL } from "./dropdown"
13
14
 
14
15
  export interface ContextMenuItemDef {
@@ -40,6 +41,17 @@ const props = defineProps<{
40
41
  * native-menu mode on that first click and swallow it.
41
42
  */
42
43
  always?: boolean
44
+ /**
45
+ * What the menu acts on, shown as a heading above the items: the row's name
46
+ * in a list of similar rows, so the user sees which one the right-click hit.
47
+ * Also names the menu for assistive tech.
48
+ */
49
+ title?: string
50
+ /** A muted second line under `title`, e.g. a version or a path. */
51
+ subtitle?: string
52
+ /** An icon beside the title, or `titleImage` for a picture (an avatar, a logo). */
53
+ titleIcon?: string
54
+ titleImage?: string
43
55
  }>()
44
56
 
45
57
  // With no items the trigger is disabled, so the element keeps the browser's
@@ -87,7 +99,13 @@ function returnFocus(event: Event) {
87
99
  <slot />
88
100
  </ContextMenuTrigger>
89
101
  <ContextMenuPortal v-if="items.length">
90
- <ContextMenuContent :collision-padding="8" :class="PANEL" @close-auto-focus="returnFocus">
102
+ <ContextMenuContent
103
+ :collision-padding="8"
104
+ :class="PANEL"
105
+ :aria-label="title"
106
+ @close-auto-focus="returnFocus"
107
+ >
108
+ <MenuTitle v-if="title" :title="title" :subtitle="subtitle" :icon="titleIcon" :image="titleImage" />
91
109
  <ContextMenuItems :items="items" :panel="PANEL" />
92
110
  </ContextMenuContent>
93
111
  </ContextMenuPortal>
@@ -9,16 +9,25 @@ import {
9
9
  } from "reka-ui"
10
10
  import { POPOVER_PANEL } from "./dropdown"
11
11
  import LpIcon from "./LpIcon.vue"
12
+ import MenuTitle from "./MenuTitle.vue"
12
13
 
13
14
  export interface MenuItem {
14
15
  label: string
15
16
  icon?: string
16
17
  danger?: boolean
17
18
  separatorBefore?: boolean
19
+ disabled?: boolean
18
20
  onSelect?: () => void
19
21
  }
20
22
 
21
- defineProps<{ items: MenuItem[] }>()
23
+ defineProps<{
24
+ items: MenuItem[]
25
+ /** A heading naming what the menu acts on; see LpContextMenu's `title`. */
26
+ title?: string
27
+ subtitle?: string
28
+ titleIcon?: string
29
+ titleImage?: string
30
+ }>()
22
31
  </script>
23
32
 
24
33
  <template>
@@ -32,12 +41,15 @@ defineProps<{ items: MenuItem[] }>()
32
41
  :side-offset="6"
33
42
  align="end"
34
43
  :class="[POPOVER_PANEL, 'z-(--z-popover) min-w-44 p-1 outline-none']"
44
+ :aria-label="title"
35
45
  >
46
+ <MenuTitle v-if="title" :title="title" :subtitle="subtitle" :icon="titleIcon" :image="titleImage" />
36
47
  <template v-for="(item, i) in items" :key="i">
37
48
  <DropdownMenuSeparator v-if="item.separatorBefore" class="my-1 h-px bg-line" />
38
49
  <DropdownMenuItem
39
- class="group/item flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-sm outline-none transition-[background-color,color] duration-[var(--duration-fast)] data-[highlighted]:bg-brand-soft"
50
+ class="group/item flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-sm outline-none transition-[background-color,color] duration-[var(--duration-fast)] data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[highlighted]:bg-brand-soft"
40
51
  :class="item.danger ? 'text-danger data-[highlighted]:bg-danger-soft data-[highlighted]:text-danger' : 'text-ink data-[highlighted]:text-brand'"
52
+ :disabled="item.disabled"
41
53
  @select="item.onSelect?.()"
42
54
  >
43
55
  <LpIcon
@@ -23,7 +23,7 @@ export default { inheritAttrs: false }
23
23
  * invisible to a keyboard: nothing focusable, nothing that Enter activates, and
24
24
  * the file dialog unreachable without a mouse.
25
25
  */
26
- import { onBeforeUnmount, onMounted, ref, watch } from "vue"
26
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
27
27
  import { useMergedAttrs } from "../composables/useMergedClass"
28
28
  import { useReveal } from "../composables/useReveal"
29
29
  import LpIcon from "./LpIcon.vue"
@@ -35,6 +35,13 @@ const props = withDefaults(
35
35
  /** Take more than one at a time. */
36
36
  multiple?: boolean
37
37
  disabled?: boolean
38
+ /**
39
+ * A drag the page itself cannot see is over the zone. Desktop shells
40
+ * (Tauri, Electron) often take file drags natively, and the page then gets
41
+ * no dragenter or dragover at all, only the shell's own events; the app
42
+ * hit-tests those and passes the answer here so the zone lights up the same.
43
+ */
44
+ dragOver?: boolean
38
45
  /**
39
46
  * Also accept a paste anywhere on the page, not only while focused.
40
47
  * Off by default: two zones both listening would each take the same
@@ -95,11 +102,12 @@ const input = ref<HTMLInputElement | null>(null)
95
102
  * may not — one stuck increment and the zone stays lit after the drag is long
96
103
  * gone, which is exactly what it did.
97
104
  */
98
- const over = ref(false)
105
+ const pointerOver = ref(false)
106
+ const over = computed(() => pointerOver.value || (props.dragOver && !props.disabled))
99
107
 
100
108
  function onDragOver(event: DragEvent) {
101
109
  if (props.disabled) return
102
- over.value = true
110
+ pointerOver.value = true
103
111
  if (event.dataTransfer) event.dataTransfer.dropEffect = "copy"
104
112
  }
105
113
 
@@ -108,7 +116,7 @@ function onDragLeave(event: DragEvent) {
108
116
  // drag merely crossed a child, and the highlight stays.
109
117
  const to = event.relatedTarget as Node | null
110
118
  if (to && (event.currentTarget as HTMLElement).contains(to)) return
111
- over.value = false
119
+ pointerOver.value = false
112
120
  }
113
121
 
114
122
  /** Does the file match `accept`? Mirrors the input's own matching so a drop
@@ -146,7 +154,7 @@ function take(list: FileList | File[] | null | undefined) {
146
154
  }
147
155
 
148
156
  function onDrop(event: DragEvent) {
149
- over.value = false
157
+ pointerOver.value = false
150
158
  take(event.dataTransfer?.files)
151
159
  }
152
160
 
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * A key combination drawn as keycaps: "Ctrl+K" is two caps, "G I" is G then
4
+ * I. Written the way LpShortcutsDialog reads combos, and drawn the same way,
5
+ * so a hint in a palette preview, a tooltip or a menu matches the reference.
6
+ *
7
+ * <LpKeys combo="Ctrl+Shift+P" label="from anywhere" />
8
+ *
9
+ * On an Apple keyboard Ctrl and Alt read as the symbols printed on its keys.
10
+ */
11
+ import { computed } from "vue"
12
+ import { appleModifierLabel, useAppleKeyboard } from "../composables/useHotkeys"
13
+ import { useLabels } from "../composables/useLpConfig"
14
+ import { comboSteps } from "./keys"
15
+
16
+ const props = withDefaults(
17
+ defineProps<{
18
+ /** "Ctrl+K", "Ctrl++", or a sequence "G I". Or keys already split. */
19
+ combo: string | string[]
20
+ /** Text after the caps, e.g. what the keys do. */
21
+ label?: string
22
+ size?: "sm" | "md"
23
+ }>(),
24
+ { size: "md" },
25
+ )
26
+
27
+ const text = useLabels("shortcuts", { then: "then" })
28
+ const apple = useAppleKeyboard()
29
+
30
+ const steps = computed(() => (Array.isArray(props.combo) ? [props.combo] : comboSteps(props.combo)))
31
+ </script>
32
+
33
+ <template>
34
+ <span class="lp-keys" :class="`lp-keys--${size}`">
35
+ <template v-for="(step, stepIndex) in steps" :key="stepIndex">
36
+ <span v-if="stepIndex" class="lp-keys__then">{{ text.then }}</span>
37
+ <kbd v-for="(key, index) in step" :key="`${key}-${index}`">{{ appleModifierLabel(key.trim(), apple) }}</kbd>
38
+ </template>
39
+ <span v-if="label || $slots.default" class="lp-keys__label"><slot>{{ label }}</slot></span>
40
+ </span>
41
+ </template>
42
+
43
+ <style scoped>
44
+ .lp-keys {
45
+ display: inline-flex;
46
+ flex-wrap: wrap;
47
+ align-items: center;
48
+ gap: 3px;
49
+ color: var(--color-muted);
50
+ }
51
+ .lp-keys__then {
52
+ padding: 0 2px;
53
+ font-size: 0.68rem;
54
+ }
55
+ .lp-keys__label {
56
+ margin-left: 5px;
57
+ font-size: 0.75rem;
58
+ }
59
+ kbd {
60
+ display: inline-flex;
61
+ min-width: 20px;
62
+ align-items: center;
63
+ justify-content: center;
64
+ padding: 2px 6px;
65
+ border: 1px solid var(--color-line-strong);
66
+ border-bottom-width: 2px;
67
+ border-radius: 5px;
68
+ background: color-mix(in srgb, var(--color-ink) 6%, transparent);
69
+ color: var(--color-muted-strong);
70
+ font-family: inherit;
71
+ font-size: 0.7rem;
72
+ line-height: 1.5;
73
+ }
74
+ .lp-keys--sm kbd {
75
+ min-width: 16px;
76
+ padding: 0 4px;
77
+ font-size: 0.65rem;
78
+ }
79
+ </style>
@@ -435,9 +435,12 @@ const ARROW =
435
435
  <template>
436
436
  <DialogRoot :open="open" @update:open="(v) => emit('update:open', v)">
437
437
  <DialogPortal>
438
+ <!-- Not `lp-scrim--nested` even over another layer: that variant drops
439
+ the scrim's blur because a nested dialog brings its own glass panel,
440
+ and a lightbox has none, so the drawer or dialog it opened from read
441
+ sharp around the picture. -->
438
442
  <DialogOverlay
439
443
  class="lp-scrim fixed inset-0 data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]"
440
- :class="{ 'lp-scrim--nested': layer.nested }"
441
444
  :style="{ zIndex: layer.scrim }"
442
445
  />
443
446
  <DialogContent
@@ -332,7 +332,7 @@ watch(
332
332
  {{ ariaLabel }}
333
333
  </DialogTitle>
334
334
  <header v-if="title || $slots.title" class="flex shrink-0 items-start justify-between gap-4 p-5 pb-3">
335
- <div class="flex flex-col gap-1">
335
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
336
336
  <DialogTitle class="text-base font-semibold text-ink">
337
337
  <slot name="title">{{ title }}</slot>
338
338
  </DialogTitle>
@@ -340,6 +340,12 @@ watch(
340
340
  {{ description }}
341
341
  </DialogDescription>
342
342
  </div>
343
+ <!-- Controls that act on the whole dialog (a period, a filter) sit
344
+ beside the close button, not inside the title: the title is a
345
+ heading, and a button in it would be read out as part of it. -->
346
+ <div v-if="$slots.actions" class="flex shrink-0 flex-wrap items-center justify-end gap-2">
347
+ <slot name="actions" />
348
+ </div>
343
349
  <!-- Marked so the auto-focus pass skips it: it is first in the DOM
344
350
  and last in usefulness. Still reachable by Tab and Escape. -->
345
351
  <DialogClose
@@ -211,6 +211,10 @@ const railPanel = computed(() => [
211
211
  </div>
212
212
  </template>
213
213
 
214
+ <template v-if="$slots.actions" #actions>
215
+ <slot name="actions" />
216
+ </template>
217
+
214
218
  <slot />
215
219
 
216
220
  <template v-if="$slots.footer" #footer>
@@ -0,0 +1,165 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * A short list of facts in a palette preview: a label, and what it is set to
4
+ * now. The tabs of a page, the fields of a settings section, a person's
5
+ * standing, the instance a world belongs to. An icon or a colour dot leads
6
+ * each row; `current` marks the one in effect.
7
+ *
8
+ * <LpPaletteFacts
9
+ * title="Now"
10
+ * :items="[
11
+ * { icon: 'lucide:server', label: 'Instance', value: 'Survival' },
12
+ * { color: '#5865f2', label: 'Role colour', value: '#5865F2', mono: true },
13
+ * ]"
14
+ * />
15
+ *
16
+ * Values roll when they change while the row is on screen (a theme picked
17
+ * with keepOpen, a count that refreshed), through LpRollingText. The `value`
18
+ * slot draws a value of the app's own (chips, a switch) instead.
19
+ */
20
+ import HighlightText from "./HighlightText.vue"
21
+ import LpIcon from "./LpIcon.vue"
22
+ import LpRollingText from "./LpRollingText.vue"
23
+ import type { PaletteFact } from "./commandPalette"
24
+
25
+ withDefaults(
26
+ defineProps<{
27
+ items: PaletteFact[]
28
+ /** Small heading over the list. */
29
+ title?: string
30
+ /** Plain rows instead of soft tiles, for a list read as text. */
31
+ plain?: boolean
32
+ }>(),
33
+ { plain: false },
34
+ )
35
+
36
+ defineSlots<{
37
+ value?: (props: { item: PaletteFact; index: number }) => unknown
38
+ }>()
39
+ </script>
40
+
41
+ <template>
42
+ <section class="lp-palette-facts" :class="{ 'lp-palette-facts--plain': plain }">
43
+ <p v-if="title" class="lp-palette-facts__title">{{ title }}</p>
44
+ <ul class="lp-palette-facts__list">
45
+ <li
46
+ v-for="(item, index) in items"
47
+ :key="index"
48
+ class="lp-palette-facts__row"
49
+ :class="{ 'is-current': item.current }"
50
+ :aria-current="item.current || undefined"
51
+ >
52
+ <span v-if="item.color" class="lp-palette-facts__dot" :style="{ background: item.color }" />
53
+ <LpIcon v-else-if="item.icon" :name="item.icon" :size="15" class="lp-palette-facts__icon" />
54
+ <span class="lp-palette-facts__label">
55
+ <span class="lp-palette-facts__name"><HighlightText :text="item.label" /></span>
56
+ <span v-if="item.hint?.length" class="lp-palette-facts__hint"><HighlightText :text="item.hint" /></span>
57
+ </span>
58
+ <span
59
+ v-if="item.value !== undefined && item.value !== '' || $slots.value"
60
+ class="lp-palette-facts__value"
61
+ :class="{ 'lp-palette-facts__value--mono': item.mono }"
62
+ >
63
+ <slot name="value" :item="item" :index="index">
64
+ <LpRollingText v-if="item.value !== undefined" :value="item.value" align="end" />
65
+ </slot>
66
+ </span>
67
+ </li>
68
+ </ul>
69
+ </section>
70
+ </template>
71
+
72
+ <style scoped>
73
+ .lp-palette-facts {
74
+ min-width: 0;
75
+ }
76
+ .lp-palette-facts__title {
77
+ margin: 0 0 0.4rem;
78
+ color: var(--color-muted);
79
+ font-size: 11px;
80
+ font-weight: 600;
81
+ letter-spacing: 0.08em;
82
+ text-transform: uppercase;
83
+ }
84
+ .lp-palette-facts__list {
85
+ display: grid;
86
+ gap: 2px;
87
+ margin: 0;
88
+ padding: 0;
89
+ list-style: none;
90
+ }
91
+ .lp-palette-facts__row {
92
+ display: flex;
93
+ min-width: 0;
94
+ align-items: center;
95
+ gap: 0.6rem;
96
+ padding: 0.4rem 0.6rem;
97
+ border-radius: var(--radius-control);
98
+ background: color-mix(in srgb, var(--color-ink) 3%, transparent);
99
+ color: var(--color-ink);
100
+ font-size: 0.8125rem;
101
+ transition: background-color var(--duration-fast, 150ms) ease;
102
+ }
103
+ .lp-palette-facts--plain .lp-palette-facts__row {
104
+ padding: 0.15rem 0;
105
+ background: transparent;
106
+ }
107
+ .lp-palette-facts__row.is-current {
108
+ background: var(--color-brand-soft);
109
+ }
110
+ .lp-palette-facts--plain .lp-palette-facts__row.is-current {
111
+ background: transparent;
112
+ color: var(--color-brand);
113
+ }
114
+ .lp-palette-facts__icon {
115
+ flex-shrink: 0;
116
+ color: var(--color-muted);
117
+ transition: color var(--duration-fast, 150ms) ease;
118
+ }
119
+ .is-current .lp-palette-facts__icon {
120
+ color: var(--color-brand);
121
+ }
122
+ .lp-palette-facts__dot {
123
+ width: 0.55rem;
124
+ height: 0.55rem;
125
+ flex-shrink: 0;
126
+ margin: 0 0.15rem;
127
+ border-radius: 9999px;
128
+ box-shadow: 0 0 0 1px var(--color-line);
129
+ }
130
+ .lp-palette-facts__label {
131
+ display: flex;
132
+ min-width: 0;
133
+ flex: 1;
134
+ flex-direction: column;
135
+ }
136
+ .lp-palette-facts__name {
137
+ overflow: hidden;
138
+ text-overflow: ellipsis;
139
+ white-space: nowrap;
140
+ }
141
+ .lp-palette-facts__hint {
142
+ margin-top: 1px;
143
+ color: var(--color-muted);
144
+ font-size: 0.75rem;
145
+ line-height: 1.45;
146
+ }
147
+ .lp-palette-facts__value {
148
+ display: inline-flex;
149
+ min-width: 0;
150
+ max-width: 55%;
151
+ flex-shrink: 0;
152
+ justify-content: flex-end;
153
+ overflow: hidden;
154
+ color: var(--color-muted);
155
+ font-size: 0.75rem;
156
+ font-variant-numeric: tabular-nums;
157
+ white-space: nowrap;
158
+ }
159
+ .lp-palette-facts__value :deep(.lp-roll) {
160
+ max-width: 100%;
161
+ }
162
+ .lp-palette-facts__value--mono {
163
+ font-family: var(--font-mono, ui-monospace, monospace);
164
+ }
165
+ </style>
@@ -0,0 +1,155 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * The head of a palette preview: what kind of thing this is (eyebrow), its
4
+ * name, a line under the name, and a lead paragraph. A picture or an icon
5
+ * sits beside the name; badges and other extras go in the default slot,
6
+ * between the name and the lead.
7
+ *
8
+ * <LpPaletteHeader
9
+ * eyebrow="Modrinth"
10
+ * :title="hit.title"
11
+ * :subtitle="hit.author"
12
+ * :image="{ src: hit.iconUrl, fallback: initials }"
13
+ * :lead="hit.description"
14
+ * >
15
+ * <LpBadge>{{ hit.version }}</LpBadge>
16
+ * </LpPaletteHeader>
17
+ *
18
+ * Title, subtitle and lead take the preview slot's `highlight(text)` as well
19
+ * as a string, to mark what was typed:
20
+ *
21
+ * <LpPaletteHeader :title="highlight(command.label)" />
22
+ *
23
+ * The picture goes through LpAvatar: it fades in once loaded and falls back
24
+ * to the initials or the icon when the link is dead.
25
+ */
26
+ import { computed } from "vue"
27
+ import LpAvatar from "./LpAvatar.vue"
28
+ import LpIcon from "./LpIcon.vue"
29
+ import HighlightText from "./HighlightText.vue"
30
+ import type { HighlightPart } from "./commandSearch"
31
+ import type { CommandImage } from "./LpCommandPalette.vue"
32
+
33
+ const props = defineProps<{
34
+ eyebrow?: string
35
+ /** Text, or the preview slot's `highlight(text)` to mark the query. */
36
+ title?: string | HighlightPart[]
37
+ subtitle?: string | HighlightPart[]
38
+ lead?: string | HighlightPart[]
39
+ /** Icon beside the title, on a soft tile. */
40
+ icon?: string
41
+ /** Picture beside the title; wins over `icon`, which becomes its fallback. */
42
+ image?: string | CommandImage | null
43
+ }>()
44
+
45
+ const picture = computed<CommandImage | null>(() => {
46
+ if (!props.image) return null
47
+ const image = typeof props.image === "string" ? { src: props.image } : props.image
48
+ return { ...image, icon: image.icon ?? props.icon }
49
+ })
50
+ </script>
51
+
52
+ <template>
53
+ <header class="lp-palette-header">
54
+ <div class="lp-palette-header__row">
55
+ <slot name="media">
56
+ <LpAvatar
57
+ v-if="picture"
58
+ :src="picture.src ?? undefined"
59
+ :alt="picture.alt ?? ''"
60
+ :fallback="picture.fallback"
61
+ :icon="picture.icon"
62
+ :shape="picture.shape ?? 'soft'"
63
+ :pixel-size="44"
64
+ class="lp-palette-header__media"
65
+ />
66
+ <span v-else-if="icon" class="lp-palette-header__tile">
67
+ <LpIcon :name="icon" :size="18" />
68
+ </span>
69
+ </slot>
70
+ <div class="lp-palette-header__text">
71
+ <p v-if="eyebrow" class="lp-palette-header__eyebrow">{{ eyebrow }}</p>
72
+ <h3 v-if="title?.length || $slots.title" class="lp-palette-header__title">
73
+ <slot name="title"><HighlightText v-if="title" :text="title" /></slot>
74
+ </h3>
75
+ <p v-if="subtitle?.length" class="lp-palette-header__subtitle"><HighlightText :text="subtitle" /></p>
76
+ </div>
77
+ </div>
78
+ <div v-if="$slots.default" class="lp-palette-header__extra"><slot /></div>
79
+ <p v-if="lead?.length || $slots.lead" class="lp-palette-header__lead">
80
+ <slot name="lead"><HighlightText v-if="lead" :text="lead" /></slot>
81
+ </p>
82
+ </header>
83
+ </template>
84
+
85
+ <style scoped>
86
+ .lp-palette-header {
87
+ display: flex;
88
+ min-width: 0;
89
+ flex-direction: column;
90
+ gap: 0.6rem;
91
+ }
92
+ .lp-palette-header__row {
93
+ display: flex;
94
+ min-width: 0;
95
+ align-items: center;
96
+ gap: 0.75rem;
97
+ }
98
+ .lp-palette-header__media {
99
+ flex-shrink: 0;
100
+ }
101
+ .lp-palette-header__tile {
102
+ display: grid;
103
+ width: 2.25rem;
104
+ height: 2.25rem;
105
+ flex-shrink: 0;
106
+ place-items: center;
107
+ border-radius: var(--radius-control);
108
+ background: var(--color-brand-soft);
109
+ color: var(--color-brand);
110
+ }
111
+ .lp-palette-header__text {
112
+ min-width: 0;
113
+ flex: 1;
114
+ }
115
+ .lp-palette-header__eyebrow {
116
+ margin: 0 0 2px;
117
+ color: var(--color-muted);
118
+ font-size: 11px;
119
+ font-weight: 600;
120
+ letter-spacing: 0.08em;
121
+ text-transform: uppercase;
122
+ }
123
+ .lp-palette-header__title {
124
+ display: -webkit-box;
125
+ margin: 0;
126
+ overflow: hidden;
127
+ -webkit-box-orient: vertical;
128
+ -webkit-line-clamp: 2;
129
+ color: var(--color-ink);
130
+ font-size: 1rem;
131
+ font-weight: 600;
132
+ line-height: 1.3;
133
+ overflow-wrap: anywhere;
134
+ }
135
+ .lp-palette-header__subtitle {
136
+ margin: 2px 0 0;
137
+ overflow: hidden;
138
+ color: var(--color-muted);
139
+ font-size: 0.75rem;
140
+ text-overflow: ellipsis;
141
+ white-space: nowrap;
142
+ }
143
+ .lp-palette-header__extra {
144
+ display: flex;
145
+ flex-wrap: wrap;
146
+ gap: 0.375rem;
147
+ }
148
+ .lp-palette-header__lead {
149
+ margin: 0;
150
+ color: var(--color-muted-strong);
151
+ font-size: 0.8125rem;
152
+ line-height: 1.55;
153
+ white-space: pre-line;
154
+ }
155
+ </style>
@@ -0,0 +1,77 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * The body of LpCommandPalette's `#preview` slot. It spaces the blocks inside
4
+ * it (LpPaletteHeader, LpPaletteFacts, LpPaletteStats, LpKeys, anything of the
5
+ * app's own) and brings them in one after another, the way the palette's own
6
+ * pane brings in paragraphs. Every app that filled the slot wrote this same
7
+ * wrapper with the same stagger; it is one element here.
8
+ *
9
+ * <template #preview="{ command, highlight }">
10
+ * <LpPalettePreview>
11
+ * <LpPaletteHeader :eyebrow="command.group" :title="highlight(command.label)" />
12
+ * <LpPaletteFacts :items="rows" />
13
+ * </LpPalettePreview>
14
+ * </template>
15
+ *
16
+ * The pane remounts its body for every highlighted row, so the entrance plays
17
+ * per row; reduced motion shows the blocks at once.
18
+ */
19
+ withDefaults(
20
+ defineProps<{
21
+ /** Space between blocks. */
22
+ gap?: "sm" | "md" | "lg"
23
+ }>(),
24
+ { gap: "md" },
25
+ )
26
+ </script>
27
+
28
+ <template>
29
+ <div class="lp-palette-preview" :class="`lp-palette-preview--${gap}`">
30
+ <slot />
31
+ </div>
32
+ </template>
33
+
34
+ <style>
35
+ .lp-palette-preview {
36
+ display: flex;
37
+ min-width: 0;
38
+ flex-direction: column;
39
+ color: var(--color-ink);
40
+ }
41
+ .lp-palette-preview--sm {
42
+ gap: 0.6rem;
43
+ }
44
+ .lp-palette-preview--md {
45
+ gap: 0.9rem;
46
+ }
47
+ .lp-palette-preview--lg {
48
+ gap: 1.25rem;
49
+ }
50
+ .lp-palette-preview > * {
51
+ min-width: 0;
52
+ animation: lp-palette-in 260ms var(--ease-emphasized, ease-out) both;
53
+ }
54
+ .lp-palette-preview > :nth-child(2) {
55
+ animation-delay: 40ms;
56
+ }
57
+ .lp-palette-preview > :nth-child(3) {
58
+ animation-delay: 80ms;
59
+ }
60
+ .lp-palette-preview > :nth-child(4) {
61
+ animation-delay: 120ms;
62
+ }
63
+ .lp-palette-preview > :nth-child(n + 5) {
64
+ animation-delay: 160ms;
65
+ }
66
+ @keyframes lp-palette-in {
67
+ from {
68
+ opacity: 0;
69
+ transform: translateY(4px);
70
+ }
71
+ }
72
+ @media (prefers-reduced-motion: reduce) {
73
+ .lp-palette-preview > * {
74
+ animation: none;
75
+ }
76
+ }
77
+ </style>