@leavepulse/ui 0.89.1 → 0.91.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/README.md +1 -1
- package/dist/assets/{LpCommandPalette-CO_5TFOQ.css → LpCommandPalette-D9ZuNQmc.css} +20 -20
- package/dist/components/LpCommandPalette.vue.d.ts +11 -3
- package/dist/components/LpCommandPalette.vue.js +2 -2
- package/dist/components/commandSearch.d.ts +2 -0
- package/dist/components/commandSearch.js +9 -1
- package/package.json +1 -1
- package/src/components/LpCommandPalette.vue +13 -5
- package/src/components/LpSection.vue +3 -2
- package/src/components/commandSearch.ts +23 -1
- package/src/iconSubset.d.mts +2 -0
- package/src/iconSubset.mjs +31 -8
- package/src/nuxt.ts +52 -13
package/README.md
CHANGED
|
@@ -312,7 +312,7 @@ from the comment that opens the component.
|
|
|
312
312
|
- `LpPopover`: A floating panel anchored to a trigger, for content that is more than a tip.
|
|
313
313
|
- `LpRelativeTime`: A date said the way people say it ("3 min ago", "in 2 h"), kept true as time passes, with the exact date one hover away.
|
|
314
314
|
- `LpScrollArea`: Drawn overlay scrollbar (reka ScrollArea).
|
|
315
|
-
- `LpSection`: A section heading: a title, an optional line under it
|
|
315
|
+
- `LpSection`: A section heading with no default slot: a title, an optional line under it and actions on the right; the section's content goes after it, not inside.
|
|
316
316
|
- `LpSkeleton`: Loading placeholder, in two shapes.
|
|
317
317
|
- `LpStatus`: What state something is in, as a light and a word: a server online, a bot starting, a region degraded.
|
|
318
318
|
- `LpTooltip`: Tooltip for controls whose meaning is not written on them — an icon button, a truncated value, a disabled action that owes an explanation.
|
|
@@ -5,28 +5,28 @@
|
|
|
5
5
|
* pane from its right edge as it grows. Animating the columns themselves made
|
|
6
6
|
* every line of the list and the pane re-wrap on every frame.
|
|
7
7
|
*/
|
|
8
|
-
.lp-cmd-dialog[data-v-
|
|
8
|
+
.lp-cmd-dialog[data-v-d51dc98f] {
|
|
9
9
|
--lp-cmd-list: min(92vw, 40rem);
|
|
10
10
|
--lp-cmd-pane: max(0px, min(26rem, 96vw - var(--lp-cmd-list)));
|
|
11
11
|
width: var(--lp-cmd-list);
|
|
12
12
|
}
|
|
13
|
-
.lp-cmd-split[data-v-
|
|
13
|
+
.lp-cmd-split[data-v-d51dc98f] {
|
|
14
14
|
transition:
|
|
15
15
|
width 300ms var(--ease-emphasized, ease),
|
|
16
16
|
min-height 300ms var(--ease-emphasized, ease);
|
|
17
17
|
}
|
|
18
18
|
@media (min-width: 768px) {
|
|
19
|
-
.lp-cmd-split .lp-cmd-list[data-v-
|
|
19
|
+
.lp-cmd-split .lp-cmd-list[data-v-d51dc98f] {
|
|
20
20
|
flex: 0 0 auto;
|
|
21
21
|
/* the dialog's 1px border on each side */
|
|
22
22
|
width: calc(var(--lp-cmd-list) - 2px);
|
|
23
23
|
}
|
|
24
|
-
.lp-cmd-dialog[data-pane="open"][data-v-
|
|
24
|
+
.lp-cmd-dialog[data-pane="open"][data-v-d51dc98f] {
|
|
25
25
|
width: calc(var(--lp-cmd-list) + var(--lp-cmd-pane));
|
|
26
26
|
min-height: min(76vh, 36rem);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
.lp-cmd-pane[data-v-
|
|
29
|
+
.lp-cmd-pane[data-v-d51dc98f] {
|
|
30
30
|
width: var(--lp-cmd-pane);
|
|
31
31
|
}
|
|
32
32
|
/*
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* desktop window keeps the card.
|
|
37
37
|
*/
|
|
38
38
|
@media (hover: none) and (pointer: coarse) and (max-width: 639px) {
|
|
39
|
-
.lp-cmd-dialog[data-v-
|
|
39
|
+
.lp-cmd-dialog[data-v-d51dc98f] {
|
|
40
40
|
inset: var(--lp-cmd-vy, 0) auto auto var(--lp-cmd-vx, 0);
|
|
41
41
|
translate: none;
|
|
42
42
|
width: var(--lp-cmd-vw, 100%);
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
padding-bottom: env(safe-area-inset-bottom);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
.lp-cmd-dialog[data-editing] .lp-cmd-pane[data-v-
|
|
51
|
+
.lp-cmd-dialog[data-editing] .lp-cmd-pane[data-v-d51dc98f] {
|
|
52
52
|
width: 100%;
|
|
53
53
|
}
|
|
54
|
-
.lp-cmd-dialog[data-editing][data-v-
|
|
54
|
+
.lp-cmd-dialog[data-editing][data-v-d51dc98f] {
|
|
55
55
|
width: var(--lp-cmd-list);
|
|
56
56
|
min-height: 0;
|
|
57
57
|
}
|
|
@@ -59,43 +59,43 @@
|
|
|
59
59
|
* The pane rides in from the right edge while the dialog opens up to it, and
|
|
60
60
|
* leaves the same way, for as long as the dialog takes to close over it.
|
|
61
61
|
*/
|
|
62
|
-
.lp-cmd-pane-enter-active[data-v-
|
|
63
|
-
.lp-cmd-pane-leave-active[data-v-
|
|
62
|
+
.lp-cmd-pane-enter-active[data-v-d51dc98f],
|
|
63
|
+
.lp-cmd-pane-leave-active[data-v-d51dc98f] {
|
|
64
64
|
transition:
|
|
65
65
|
transform 300ms var(--ease-emphasized, ease),
|
|
66
66
|
opacity 300ms ease;
|
|
67
67
|
}
|
|
68
|
-
.lp-cmd-pane-enter-from[data-v-
|
|
69
|
-
.lp-cmd-pane-leave-to[data-v-
|
|
68
|
+
.lp-cmd-pane-enter-from[data-v-d51dc98f],
|
|
69
|
+
.lp-cmd-pane-leave-to[data-v-d51dc98f] {
|
|
70
70
|
opacity: 0;
|
|
71
71
|
transform: translateX(40%);
|
|
72
72
|
}
|
|
73
|
-
.lp-cmd-para[data-v-
|
|
73
|
+
.lp-cmd-para[data-v-d51dc98f] {
|
|
74
74
|
--rise: 4px;
|
|
75
75
|
animation: rise-in 260ms ease-out both;
|
|
76
76
|
}
|
|
77
77
|
@media (prefers-reduced-motion: reduce) {
|
|
78
|
-
.lp-cmd-split[data-v-
|
|
79
|
-
.lp-cmd-pane-enter-active[data-v-
|
|
80
|
-
.lp-cmd-pane-leave-active[data-v-
|
|
78
|
+
.lp-cmd-split[data-v-d51dc98f],
|
|
79
|
+
.lp-cmd-pane-enter-active[data-v-d51dc98f],
|
|
80
|
+
.lp-cmd-pane-leave-active[data-v-d51dc98f] {
|
|
81
81
|
transition: none;
|
|
82
82
|
}
|
|
83
|
-
.lp-cmd-para[data-v-
|
|
83
|
+
.lp-cmd-para[data-v-d51dc98f] {
|
|
84
84
|
animation: none;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
.lp-cmd-excerpt[data-v-
|
|
87
|
+
.lp-cmd-excerpt[data-v-d51dc98f] {
|
|
88
88
|
display: -webkit-box;
|
|
89
89
|
-webkit-box-orient: vertical;
|
|
90
90
|
overflow: hidden;
|
|
91
91
|
}
|
|
92
92
|
/* Each result eases in; honours reduced motion. */
|
|
93
|
-
.lp-cmd-item[data-v-
|
|
93
|
+
.lp-cmd-item[data-v-d51dc98f] {
|
|
94
94
|
--rise: 3px;
|
|
95
95
|
animation: rise-in 140ms ease-out both;
|
|
96
96
|
}
|
|
97
97
|
@media (prefers-reduced-motion: reduce) {
|
|
98
|
-
.lp-cmd-item[data-v-
|
|
98
|
+
.lp-cmd-item[data-v-d51dc98f] {
|
|
99
99
|
animation: none;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -53,11 +53,19 @@ export interface Command {
|
|
|
53
53
|
group?: string;
|
|
54
54
|
disabled?: boolean;
|
|
55
55
|
/**
|
|
56
|
-
* Hidden from the default (empty-query) list
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Hidden from the default (empty-query) list and surfaced only when the query
|
|
57
|
+
* names it: every typed word starts a word of its label or keywords. A word
|
|
58
|
+
* inside another word, the description or a near miss does not reveal it.
|
|
59
|
+
* For rarely-used or destructive actions (prune, decommission, …) that
|
|
60
|
+
* shouldn't clutter the palette but stay reachable.
|
|
59
61
|
*/
|
|
60
62
|
hidden?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* For a hidden command: the words that reveal it, instead of its label and
|
|
65
|
+
* keywords. A variant whose label repeats its parent's word ("Vacation: sick
|
|
66
|
+
* leave…") then stays out of a search for the parent and shows up for "sick".
|
|
67
|
+
*/
|
|
68
|
+
revealOn?: string[];
|
|
61
69
|
/**
|
|
62
70
|
* Picking this opens a mode instead of closing: a nested list, either given
|
|
63
71
|
* outright or fetched from the query typed inside it. This is what keeps a
|
|
@@ -26,7 +26,7 @@ import { parsePaletteQuery, parseCommandQuery, searchCommandItems, applyQueryFil
|
|
|
26
26
|
import { PALETTE_MODES, modeChipLabel, settleFetched, fallbackRows, readRecent, resolveRecentItems, recentVisible, commandHasPreview, matchModePrefix, isSafeHref, absoluteHref, commandKey, recentKey, pushRecent, moreRowId, toRecentItem } from "./commandPalette.js";
|
|
27
27
|
import { useView, usePortalTarget, useOutsideGuard, viewWindow } from "../composables/useView.js";
|
|
28
28
|
import { _ as _export_sfc } from "../chunks/_plugin-vue_export-helper-1tPrXgE0.js";
|
|
29
|
-
import '../assets/LpCommandPalette-
|
|
29
|
+
import '../assets/LpCommandPalette-D9ZuNQmc.css';const _hoisted_1 = { class: "relative flex shrink-0 items-center gap-2.5 border-b border-line px-4" };
|
|
30
30
|
const _hoisted_2 = ["title", "onClick"];
|
|
31
31
|
const _hoisted_3 = ["readonly", "aria-label", "aria-expanded", "aria-controls", "aria-activedescendant", "placeholder", "onKeydown"];
|
|
32
32
|
const _hoisted_4 = { class: "shrink-0 rounded-small border border-line px-1.5 py-0.5 text-[10px] text-muted [@media(hover:none)_and_(pointer:coarse)]:hidden" };
|
|
@@ -1400,7 +1400,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
1400
1400
|
};
|
|
1401
1401
|
}
|
|
1402
1402
|
});
|
|
1403
|
-
const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-
|
|
1403
|
+
const LpCommandPalette = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d51dc98f"]]);
|
|
1404
1404
|
export {
|
|
1405
1405
|
LpCommandPalette as default
|
|
1406
1406
|
};
|
|
@@ -26,9 +26,17 @@ function nearWord(term, label) {
|
|
|
26
26
|
if (term.length < 5) return false;
|
|
27
27
|
return label.split(/[^\p{L}\p{N}]+/u).some((word) => withinOne(term, word) || word.length > term.length && withinOne(term, word.slice(0, term.length)));
|
|
28
28
|
}
|
|
29
|
+
function wordStarts(text, term) {
|
|
30
|
+
return text.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(term));
|
|
31
|
+
}
|
|
32
|
+
function named(command, terms) {
|
|
33
|
+
const words = command.revealOn ? command.revealOn.map((word) => word.toLowerCase()) : [command.label.toLowerCase(), ...(command.keywords ?? []).map((keyword) => keyword.toLowerCase())];
|
|
34
|
+
return terms.include.length > 0 && terms.include.every((term) => words.some((text) => wordStarts(text, term)));
|
|
35
|
+
}
|
|
29
36
|
function score(command, terms) {
|
|
37
|
+
if (command.hidden && !named(command, terms)) return 0;
|
|
30
38
|
const label = command.label.toLowerCase();
|
|
31
|
-
const keywords =
|
|
39
|
+
const keywords = [...command.keywords ?? [], ...command.revealOn ?? []].map((keyword) => keyword.toLowerCase());
|
|
32
40
|
const description = command.description?.toLowerCase() ?? "";
|
|
33
41
|
const group = command.group?.toLowerCase() ?? "";
|
|
34
42
|
if (terms.exclude.some((term) => matches(command, term)) || terms.groups.some((term) => !group.includes(term))) return 0;
|
package/package.json
CHANGED
|
@@ -143,11 +143,19 @@ export interface Command {
|
|
|
143
143
|
group?: string
|
|
144
144
|
disabled?: boolean
|
|
145
145
|
/**
|
|
146
|
-
* Hidden from the default (empty-query) list
|
|
147
|
-
*
|
|
148
|
-
*
|
|
146
|
+
* Hidden from the default (empty-query) list and surfaced only when the query
|
|
147
|
+
* names it: every typed word starts a word of its label or keywords. A word
|
|
148
|
+
* inside another word, the description or a near miss does not reveal it.
|
|
149
|
+
* For rarely-used or destructive actions (prune, decommission, …) that
|
|
150
|
+
* shouldn't clutter the palette but stay reachable.
|
|
149
151
|
*/
|
|
150
152
|
hidden?: boolean
|
|
153
|
+
/**
|
|
154
|
+
* For a hidden command: the words that reveal it, instead of its label and
|
|
155
|
+
* keywords. A variant whose label repeats its parent's word ("Vacation: sick
|
|
156
|
+
* leave…") then stays out of a search for the parent and shows up for "sick".
|
|
157
|
+
*/
|
|
158
|
+
revealOn?: string[]
|
|
151
159
|
/**
|
|
152
160
|
* Picking this opens a mode instead of closing: a nested list, either given
|
|
153
161
|
* outright or fetched from the query typed inside it. This is what keeps a
|
|
@@ -902,8 +910,8 @@ const local = computed<{ items: Command[]; terms: string[] }>(() => {
|
|
|
902
910
|
const filtered = query.value.trim()
|
|
903
911
|
? source.value.filter((c) => !c.fallback)
|
|
904
912
|
: source.value.filter((c) => !c.hidden && !c.fallback)
|
|
905
|
-
// Empty query: show everything EXCEPT hidden commands
|
|
906
|
-
//
|
|
913
|
+
// Empty query: show everything EXCEPT hidden commands. With a query, a hidden
|
|
914
|
+
// command ranks only when the query names it (see commandSearch).
|
|
907
915
|
if (!terms.include.length && !terms.exclude.length && !terms.groups.length) return { items: filtered, terms: [] }
|
|
908
916
|
return searchCommandItems(filtered, terms)
|
|
909
917
|
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/*
|
|
3
|
-
* A section heading: a title, an optional line under
|
|
4
|
-
* right
|
|
3
|
+
* A section heading with no default slot: a title, an optional line under
|
|
4
|
+
* it and actions on the right; the section's content goes after it, not
|
|
5
|
+
* inside.
|
|
5
6
|
*
|
|
6
7
|
* <LpSection title="Recent invoices">
|
|
7
8
|
* <template #actions><LpButton size="sm">Export</LpButton></template>
|
|
@@ -3,6 +3,8 @@ export interface SearchableCommand {
|
|
|
3
3
|
description?: string
|
|
4
4
|
keywords?: string[]
|
|
5
5
|
group?: string
|
|
6
|
+
hidden?: boolean
|
|
7
|
+
revealOn?: string[]
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export interface CommandSearchTerms {
|
|
@@ -50,9 +52,29 @@ function nearWord(term: string, label: string): boolean {
|
|
|
50
52
|
.some((word) => withinOne(term, word) || (word.length > term.length && withinOne(term, word.slice(0, term.length))))
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
function wordStarts(text: string, term: string): boolean {
|
|
56
|
+
return text.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(term))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* A hidden command is there for whoever names it, not for whoever types a word
|
|
61
|
+
* it happens to contain. Every term has to start a word of its label or of a
|
|
62
|
+
* keyword (or of `revealOn`, which then replaces both): "пт" no longer digs a
|
|
63
|
+
* settings key out of "Транскрипты", and "vacation" no longer spills every
|
|
64
|
+
* hidden variant of it. Descriptions, mid-word hits and near misses do not
|
|
65
|
+
* reveal; the visible list keeps all of those.
|
|
66
|
+
*/
|
|
67
|
+
function named(command: SearchableCommand, terms: CommandSearchTerms): boolean {
|
|
68
|
+
const words = command.revealOn
|
|
69
|
+
? command.revealOn.map((word) => word.toLowerCase())
|
|
70
|
+
: [command.label.toLowerCase(), ...(command.keywords ?? []).map((keyword) => keyword.toLowerCase())]
|
|
71
|
+
return terms.include.length > 0 && terms.include.every((term) => words.some((text) => wordStarts(text, term)))
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
function score(command: SearchableCommand, terms: CommandSearchTerms): number {
|
|
75
|
+
if (command.hidden && !named(command, terms)) return 0
|
|
54
76
|
const label = command.label.toLowerCase()
|
|
55
|
-
const keywords = (command.keywords ?? []).map((keyword) => keyword.toLowerCase())
|
|
77
|
+
const keywords = [...(command.keywords ?? []), ...(command.revealOn ?? [])].map((keyword) => keyword.toLowerCase())
|
|
56
78
|
const description = command.description?.toLowerCase() ?? ""
|
|
57
79
|
const group = command.group?.toLowerCase() ?? ""
|
|
58
80
|
if (terms.exclude.some((term) => matches(command, term)) || terms.groups.some((term) => !group.includes(term))) return 0
|
package/src/iconSubset.d.mts
CHANGED
|
@@ -5,6 +5,8 @@ export interface IconCollection {
|
|
|
5
5
|
width?: number
|
|
6
6
|
height?: number
|
|
7
7
|
}
|
|
8
|
+
export function refsInText(text: string): [prefix: string, name: string][]
|
|
9
|
+
export function refsInDirs(dirs: string[]): Map<string, Set<string>>
|
|
8
10
|
export function namesInText(text: string): string[]
|
|
9
11
|
export function namesInDirs(dirs: string[]): Set<string>
|
|
10
12
|
export function subset(collection: IconCollection, names: Iterable<string>): IconCollection
|
package/src/iconSubset.mjs
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// Shared by the kit's own icon generator and the Nuxt module's app scan: find
|
|
2
|
-
// "
|
|
3
|
-
// just those icons (aliases keep their parent).
|
|
2
|
+
// "<prefix>:<name>" literals in source text and cut an iconify collection down
|
|
3
|
+
// to just those icons (aliases keep their parent). Any prefix is collected;
|
|
4
|
+
// whoever reads the result decides which prefixes are icon sets (the module
|
|
5
|
+
// keeps those the app has an @iconify-json package for), so "node:fs" in a
|
|
6
|
+
// source file costs nothing.
|
|
4
7
|
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
5
8
|
import { join } from "node:path"
|
|
6
9
|
|
|
7
|
-
const
|
|
10
|
+
const PART = "[a-z0-9]+(?:-[a-z0-9]+)*"
|
|
11
|
+
const REF = new RegExp(`(?<![a-z0-9-])(${PART}):(${PART})`, "g")
|
|
8
12
|
const SOURCE = /\.(vue|ts|tsx|js|jsx|mjs|md|html)$/
|
|
9
13
|
const SKIP = new Set(["node_modules", "dist", ".nuxt", ".output", ".git"])
|
|
10
14
|
|
|
15
|
+
/** Every "<prefix>:<name>" literal, as [prefix, name] pairs. */
|
|
16
|
+
export function refsInText(text) {
|
|
17
|
+
return [...text.matchAll(REF)].map((m) => [m[1], m[2]])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The lucide names in a text. */
|
|
11
21
|
export function namesInText(text) {
|
|
12
|
-
return
|
|
22
|
+
return refsInText(text)
|
|
23
|
+
.filter(([prefix]) => prefix === "lucide")
|
|
24
|
+
.map(([, name]) => name)
|
|
13
25
|
}
|
|
14
26
|
|
|
15
|
-
|
|
16
|
-
|
|
27
|
+
/** Every "<prefix>:<name>" literal under the dirs, as prefix -> names. */
|
|
28
|
+
export function refsInDirs(dirs) {
|
|
29
|
+
const refs = new Map()
|
|
17
30
|
const walk = (dir) => {
|
|
18
31
|
let entries
|
|
19
32
|
try {
|
|
@@ -25,11 +38,21 @@ export function namesInDirs(dirs) {
|
|
|
25
38
|
if (SKIP.has(entry)) continue
|
|
26
39
|
const path = join(dir, entry)
|
|
27
40
|
if (statSync(path).isDirectory()) walk(path)
|
|
28
|
-
else if (SOURCE.test(entry))
|
|
41
|
+
else if (SOURCE.test(entry)) {
|
|
42
|
+
for (const [prefix, name] of refsInText(readFileSync(path, "utf8"))) {
|
|
43
|
+
if (!refs.has(prefix)) refs.set(prefix, new Set())
|
|
44
|
+
refs.get(prefix).add(name)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
29
47
|
}
|
|
30
48
|
}
|
|
31
49
|
for (const dir of dirs) walk(dir)
|
|
32
|
-
return
|
|
50
|
+
return refs
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The lucide names under the dirs. */
|
|
54
|
+
export function namesInDirs(dirs) {
|
|
55
|
+
return refsInDirs(dirs).get("lucide") ?? new Set()
|
|
33
56
|
}
|
|
34
57
|
|
|
35
58
|
export function subset(collection, names) {
|
package/src/nuxt.ts
CHANGED
|
@@ -18,11 +18,16 @@
|
|
|
18
18
|
* link false to skip providing NuxtLink as the kit's link component
|
|
19
19
|
* exclude component names not to auto-register (e.g. ["LayoutCanvas"]
|
|
20
20
|
* when the app has its own component of that name)
|
|
21
|
-
* icons which
|
|
22
|
-
*
|
|
23
|
-
* app's source, "all" registers the whole set, "kit"
|
|
24
|
-
* kit's own, or a list of names. Anything else
|
|
25
|
-
* from a lazily loaded chunk of the
|
|
21
|
+
* icons which icons render in the server pass and ship in the main
|
|
22
|
+
* bundle: "scan" (default) finds "lucide:*" literals in the
|
|
23
|
+
* app's source, "all" registers the whole lucide set, "kit"
|
|
24
|
+
* only the kit's own, or a list of names. Anything else in
|
|
25
|
+
* lucide still renders, from a lazily loaded chunk of the
|
|
26
|
+
* bundled collection. Other sets ("logos:google-icon") are
|
|
27
|
+
* found the same way, by "scan" or in the list, for every
|
|
28
|
+
* prefix the app has an @iconify-json/<prefix> package for:
|
|
29
|
+
* registered offline from that package, never fetched from
|
|
30
|
+
* the iconify API.
|
|
26
31
|
* propCheck false to skip the dev warning for an unknown prop on a kit
|
|
27
32
|
* component (default: on in `nuxt dev`, never in a build)
|
|
28
33
|
*/
|
|
@@ -30,7 +35,7 @@ import { readFileSync } from "node:fs"
|
|
|
30
35
|
import { createRequire } from "node:module"
|
|
31
36
|
import { addComponent, addPlugin, addPluginTemplate, addTemplate, createResolver, defineNuxtModule } from "@nuxt/kit"
|
|
32
37
|
import { COMPONENT_NAMES } from "./component-names"
|
|
33
|
-
import {
|
|
38
|
+
import { type IconCollection, refsInDirs, subset } from "./iconSubset.mjs"
|
|
34
39
|
|
|
35
40
|
export interface LeavepulseUiOptions {
|
|
36
41
|
/** Register the SSR theme plugin. Disable to wire theming yourself. */
|
|
@@ -43,18 +48,52 @@ export interface LeavepulseUiOptions {
|
|
|
43
48
|
link?: boolean
|
|
44
49
|
/** Component names to leave unregistered. */
|
|
45
50
|
exclude?: string[]
|
|
46
|
-
/**
|
|
51
|
+
/** Icons registered up front (default "scan"). */
|
|
47
52
|
icons?: "scan" | "all" | "kit" | string[]
|
|
48
53
|
/** Warn in `nuxt dev` about an unknown prop on a kit component (default true). */
|
|
49
54
|
propCheck?: boolean
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
/** The names asked for, as prefix -> names; a bare name is lucide. */
|
|
58
|
+
function iconRefs(mode: LeavepulseUiOptions["icons"], srcDir: string): Map<string, Set<string>> {
|
|
59
|
+
if (!Array.isArray(mode)) return refsInDirs([srcDir])
|
|
60
|
+
const refs = new Map<string, Set<string>>()
|
|
61
|
+
for (const ref of mode) {
|
|
62
|
+
const [prefix, name] = ref.includes(":") ? ref.split(":", 2) : ["lucide", ref]
|
|
63
|
+
if (!refs.has(prefix!)) refs.set(prefix!, new Set())
|
|
64
|
+
refs.get(prefix!)!.add(name!)
|
|
65
|
+
}
|
|
66
|
+
return refs
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An installed iconify set, read from the app's own dependencies; null if it has none. */
|
|
70
|
+
function collectionOf(prefix: string, rootDir: string): IconCollection | null {
|
|
71
|
+
try {
|
|
72
|
+
const path = createRequire(`${rootDir}/package.json`).resolve(`@iconify-json/${prefix}/icons.json`)
|
|
73
|
+
return JSON.parse(readFileSync(path, "utf8")) as IconCollection
|
|
74
|
+
} catch {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function iconsPlugin(mode: LeavepulseUiOptions["icons"], srcDir: string, rootDir: string): string {
|
|
53
80
|
if (mode === "kit") return ""
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
const refs = iconRefs(mode, srcDir)
|
|
82
|
+
const sets: IconCollection[] = []
|
|
83
|
+
if (mode !== "all") {
|
|
84
|
+
const lucide = JSON.parse(readFileSync(createRequire(import.meta.url).resolve("@iconify-json/lucide/icons.json"), "utf8"))
|
|
85
|
+
sets.push(subset(lucide, refs.get("lucide") ?? []))
|
|
86
|
+
}
|
|
87
|
+
for (const [prefix, names] of refs) {
|
|
88
|
+
if (prefix === "lucide") continue
|
|
89
|
+
const collection = collectionOf(prefix, rootDir)
|
|
90
|
+
if (!collection) continue
|
|
91
|
+
const cut = subset(collection, names)
|
|
92
|
+
if (Object.keys(cut.icons).length) sets.push(cut)
|
|
93
|
+
}
|
|
94
|
+
const head = mode === "all" ? 'import "@leavepulse/ui/icons-all"\n' : ""
|
|
95
|
+
if (!sets.length) return head
|
|
96
|
+
return `${head}import { registerIconCollection } from "@leavepulse/ui"\n${sets.map((set) => `registerIconCollection(${JSON.stringify(set)})\n`).join("")}`
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
export default defineNuxtModule<LeavepulseUiOptions>({
|
|
@@ -74,7 +113,7 @@ export default defineNuxtModule<LeavepulseUiOptions>({
|
|
|
74
113
|
})
|
|
75
114
|
}
|
|
76
115
|
|
|
77
|
-
const icons = iconsPlugin(options.icons, nuxt.options.srcDir)
|
|
116
|
+
const icons = iconsPlugin(options.icons, nuxt.options.srcDir, nuxt.options.rootDir)
|
|
78
117
|
if (icons) {
|
|
79
118
|
addPluginTemplate({
|
|
80
119
|
filename: "leavepulse-ui-icons.mjs",
|