@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
|
@@ -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
|
|
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<{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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>
|