@leavepulse/ui 0.46.0 → 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 (64) hide show
  1. package/dist/assets/HighlightText-BwC5xbqn.css +6 -0
  2. package/dist/assets/{LpCommandPalette-5miKYsb-.css → LpCommandPalette-DmQUsJqA.css} +21 -21
  3. package/dist/assets/LpKeys-BxRhfQda.css +36 -0
  4. package/dist/assets/LpPaletteFacts-B9FdVmIO.css +93 -0
  5. package/dist/assets/LpPaletteHeader-BVOu1UX5.css +70 -0
  6. package/dist/assets/LpPalettePreview-CJmAKgYW.css +43 -0
  7. package/dist/assets/LpPaletteStats-ClE-37MK.css +33 -0
  8. package/dist/assets/LpPaletteSwatches-CA0B8y2E.css +21 -0
  9. package/dist/assets/{LpShortcutsDialog-CqYX3yy6.css → LpShortcutsDialog-BUp_BX4s.css} +7 -28
  10. package/dist/assets/{LpTable-BoeA_TgK.css → LpTable-xWwI4KPZ.css} +17 -17
  11. package/dist/chunks/HighlightText-BvnYZkhi.js +27 -0
  12. package/dist/chunks/{LpAvatar-DeSb6H0c.js → LpAvatar-Cb1jtOVv.js} +21 -3
  13. package/dist/chunks/{LpLightbox-DSDahF23.js → LpLightbox-EVXzAq62.js} +3 -3
  14. package/dist/chunks/{LpMediaGallery-YKr-cQPN.js → LpMediaGallery-BnlkN_1f.js} +1 -1
  15. package/dist/chunks/LpPalettePreview-CFav_7co.js +19 -0
  16. package/dist/component-names.d.ts +1 -1
  17. package/dist/components/HighlightText.vue.d.ts +7 -0
  18. package/dist/components/HighlightText.vue.js +4 -0
  19. package/dist/components/LpAvatar.vue.d.ts +12 -0
  20. package/dist/components/LpAvatar.vue.js +1 -1
  21. package/dist/components/LpCommandPalette.vue.d.ts +152 -15
  22. package/dist/components/LpCommandPalette.vue.js +178 -92
  23. package/dist/components/LpKeys.vue.d.ts +22 -0
  24. package/dist/components/LpKeys.vue.js +52 -0
  25. package/dist/components/LpLightbox.vue.js +1 -1
  26. package/dist/components/LpMediaGallery.vue.js +1 -1
  27. package/dist/components/LpPaletteFacts.vue.d.ts +25 -0
  28. package/dist/components/LpPaletteFacts.vue.js +85 -0
  29. package/dist/components/LpPaletteHeader.vue.d.ts +32 -0
  30. package/dist/components/LpPaletteHeader.vue.js +103 -0
  31. package/dist/components/LpPalettePreview.vue.d.ts +19 -0
  32. package/dist/components/LpPalettePreview.vue.js +4 -0
  33. package/dist/components/LpPaletteStats.vue.d.ts +11 -0
  34. package/dist/components/LpPaletteStats.vue.js +47 -0
  35. package/dist/components/LpPaletteSwatches.vue.d.ts +10 -0
  36. package/dist/components/LpPaletteSwatches.vue.js +34 -0
  37. package/dist/components/LpShortcutsDialog.vue.js +8 -28
  38. package/dist/components/LpTable.vue.js +5 -3
  39. package/dist/components/commandPalette.d.ts +157 -0
  40. package/dist/components/commandPalette.js +103 -0
  41. package/dist/components/commandSearch.d.ts +32 -0
  42. package/dist/components/commandSearch.js +48 -0
  43. package/dist/components/keys.d.ts +2 -0
  44. package/dist/components/keys.js +11 -0
  45. package/dist/index.d.ts +12 -3
  46. package/dist/index.js +157 -134
  47. package/package.json +1 -1
  48. package/src/component-names.ts +6 -0
  49. package/src/components/HighlightText.vue +28 -0
  50. package/src/components/LpAvatar.vue +21 -2
  51. package/src/components/LpCommandPalette.vue +387 -87
  52. package/src/components/LpKeys.vue +79 -0
  53. package/src/components/LpLightbox.vue +4 -1
  54. package/src/components/LpPaletteFacts.vue +165 -0
  55. package/src/components/LpPaletteHeader.vue +155 -0
  56. package/src/components/LpPalettePreview.vue +77 -0
  57. package/src/components/LpPaletteStats.vue +81 -0
  58. package/src/components/LpPaletteSwatches.vue +56 -0
  59. package/src/components/LpShortcutsDialog.vue +2 -45
  60. package/src/components/LpTable.vue +6 -1
  61. package/src/components/commandPalette.ts +264 -0
  62. package/src/components/commandSearch.ts +79 -0
  63. package/src/components/keys.ts +13 -0
  64. package/src/index.ts +12 -3
@@ -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
@@ -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>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * A few figures in a palette preview, as tiles: mods and worlds in an
4
+ * instance, downloads and follows of a project, members online. A number
5
+ * turns to a new value through LpNumberFlow, text rolls through
6
+ * LpRollingText, so a figure that refreshes while the row is highlighted
7
+ * moves instead of blinking.
8
+ *
9
+ * <LpPaletteStats :items="[
10
+ * { label: 'Downloads', value: 1250000, compact: true },
11
+ * { label: 'Latest', value: '1.21.4' },
12
+ * ]" />
13
+ */
14
+ import LpNumberFlow from "./LpNumberFlow.vue"
15
+ import LpRollingText from "./LpRollingText.vue"
16
+ import type { PaletteStat } from "./commandPalette"
17
+
18
+ withDefaults(
19
+ defineProps<{
20
+ items: PaletteStat[]
21
+ /** Tiles per row. */
22
+ columns?: 2 | 3 | 4
23
+ }>(),
24
+ { columns: 2 },
25
+ )
26
+ </script>
27
+
28
+ <template>
29
+ <dl class="lp-palette-stats" :style="{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }">
30
+ <div v-for="(item, index) in items" :key="`${item.label}-${index}`" class="lp-palette-stats__tile">
31
+ <dt>{{ item.label }}</dt>
32
+ <dd>
33
+ <LpNumberFlow
34
+ v-if="typeof item.value === 'number'"
35
+ :value="item.value"
36
+ :compact="item.compact"
37
+ :suffix="item.suffix"
38
+ :format="item.format"
39
+ :animate-on-mount="false"
40
+ grouped
41
+ />
42
+ <LpRollingText v-else :value="item.value" align="start" />
43
+ </dd>
44
+ </div>
45
+ </dl>
46
+ </template>
47
+
48
+ <style scoped>
49
+ .lp-palette-stats {
50
+ display: grid;
51
+ gap: 0.5rem;
52
+ margin: 0;
53
+ }
54
+ .lp-palette-stats__tile {
55
+ min-width: 0;
56
+ padding: 0.5rem 0.65rem;
57
+ border: 1px solid var(--color-line);
58
+ border-radius: var(--radius-control);
59
+ background: color-mix(in srgb, var(--color-surface-soft) 55%, transparent);
60
+ }
61
+ dt {
62
+ overflow: hidden;
63
+ color: var(--color-muted);
64
+ font-size: 11px;
65
+ text-overflow: ellipsis;
66
+ white-space: nowrap;
67
+ }
68
+ dd {
69
+ display: flex;
70
+ min-width: 0;
71
+ margin: 2px 0 0;
72
+ color: var(--color-ink);
73
+ font-size: 1.125rem;
74
+ font-weight: 650;
75
+ font-variant-numeric: tabular-nums;
76
+ line-height: 1.3;
77
+ }
78
+ dd :deep(.lp-roll) {
79
+ max-width: 100%;
80
+ }
81
+ </style>
@@ -0,0 +1,56 @@
1
+ <script setup lang="ts">
2
+ /*
3
+ * A theme's colours as a strip of swatches on its own surface, for a palette
4
+ * preview of a theme picker: the row says the name, this shows what picking
5
+ * it would look like. Any CSS colour works.
6
+ *
7
+ * <LpPaletteSwatches
8
+ * :background="theme.colors.surface"
9
+ * :colors="[theme.colors.ink, theme.colors.brand, theme.colors.accent]"
10
+ * />
11
+ *
12
+ * A swatch that changes (the same row after a keepOpen pick) eases to its
13
+ * new colour.
14
+ */
15
+ defineProps<{
16
+ colors: string[]
17
+ /** The surface the swatches sit on, usually the theme's own. */
18
+ background?: string
19
+ /** Accessible name of the strip, e.g. the theme's name. */
20
+ label?: string
21
+ }>()
22
+ </script>
23
+
24
+ <template>
25
+ <div
26
+ class="lp-palette-swatches"
27
+ role="img"
28
+ :aria-label="label"
29
+ :style="{ background, gridTemplateColumns: `repeat(${Math.max(colors.length, 1)}, minmax(0, 1fr))` }"
30
+ >
31
+ <span v-for="(color, index) in colors" :key="index" :title="color" :style="{ background: color }" />
32
+ </div>
33
+ </template>
34
+
35
+ <style scoped>
36
+ .lp-palette-swatches {
37
+ display: grid;
38
+ gap: 0.4rem;
39
+ padding: 0.7rem;
40
+ border: 1px solid var(--color-line);
41
+ border-radius: var(--radius-card);
42
+ transition: background-color var(--duration-medium, 240ms) ease;
43
+ }
44
+ .lp-palette-swatches span {
45
+ height: 2rem;
46
+ border-radius: var(--radius-control);
47
+ box-shadow: inset 0 0 0 1px rgb(128 128 128 / 0.25);
48
+ transition: background-color var(--duration-medium, 240ms) ease;
49
+ }
50
+ @media (prefers-reduced-motion: reduce) {
51
+ .lp-palette-swatches,
52
+ .lp-palette-swatches span {
53
+ transition: none;
54
+ }
55
+ }
56
+ </style>