yapin.js-client 0.1.4 → 0.1.5
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/LICENSE +46 -0
- package/docs/configuration.md +11 -0
- package/docs/ui.md +30 -0
- package/package.json +1 -1
- package/src/build/build.js +29 -2
- package/src/config/defaults.js +7 -0
- package/src/config/load.js +2 -2
- package/src/runtime/client.js +1 -1
- package/src/runtime/ui.js +36 -4
- package/src/shared/constants.js +1 -1
- package/src/shared/icons.js +54 -26
- package/templates/init/icons/.gitkeep +0 -0
- package/templates/init/yapin-client.config.js +6 -0
- package/types/index.d.ts +9 -0
- package/types/runtime.d.ts +4 -0
package/LICENSE
CHANGED
|
@@ -19,3 +19,49 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Third-party notices
|
|
26
|
+
|
|
27
|
+
The built-in icon set in src/shared/icons.js is taken from Lucide
|
|
28
|
+
(https://lucide.dev). Lucide is ISC licensed, and the icons it inherits from
|
|
29
|
+
Feather are MIT licensed. Both notices follow, and are reproduced as required.
|
|
30
|
+
|
|
31
|
+
ISC License
|
|
32
|
+
|
|
33
|
+
Copyright (c) 2026 Lucide Icons and Contributors.
|
|
34
|
+
|
|
35
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
36
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
37
|
+
copyright notice and this permission notice appear in all copies.
|
|
38
|
+
|
|
39
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
40
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
41
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
42
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
43
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
44
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
45
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
46
|
+
|
|
47
|
+
MIT License
|
|
48
|
+
|
|
49
|
+
Copyright (c) 2013-present Cole Bemis
|
|
50
|
+
|
|
51
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
52
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
53
|
+
in the Software without restriction, including without limitation the rights
|
|
54
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
55
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
56
|
+
furnished to do so, subject to the following conditions:
|
|
57
|
+
|
|
58
|
+
The above copyright notice and this permission notice shall be included in all
|
|
59
|
+
copies or substantial portions of the Software.
|
|
60
|
+
|
|
61
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
62
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
63
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
64
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
65
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
66
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
67
|
+
SOFTWARE.
|
package/docs/configuration.md
CHANGED
|
@@ -95,6 +95,17 @@ settings: {
|
|
|
95
95
|
| `customCSS.title`, `customCSS.description` | | Its heading and hint. |
|
|
96
96
|
| `user` | | Like `plugins.user`, with `maxBytes` 2000000. |
|
|
97
97
|
|
|
98
|
+
## `icons`
|
|
99
|
+
|
|
100
|
+
Icons your client app adds to the built-in set, usable by name anywhere an `icon` option is accepted. See [ui.md](ui.md#your-own-icons).
|
|
101
|
+
|
|
102
|
+
| Option | Default | |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `dirs` | `['icons']` | Folders with `.svg` icons. Each is registered under its file name, so `icons/brand.svg` is `icon: 'brand'`. Names starting with `_` or `.` are skipped. |
|
|
105
|
+
| `set` | `{}` | Icons given inline as `{ name: '<svg…>' }`. Wins over a folder file of the same name. |
|
|
106
|
+
|
|
107
|
+
A name that matches a built-in replaces it. Markup is inlined into the payload at build time and sanitized when drawn; draw on a 24×24 `viewBox` with `stroke="currentColor"` and no `width`/`height` on the root `<svg>`.
|
|
108
|
+
|
|
98
109
|
## `pages`
|
|
99
110
|
|
|
100
111
|
| Option | Default | |
|
package/docs/ui.md
CHANGED
|
@@ -44,6 +44,7 @@ ui.icon(svgMarkupOrImageURL)
|
|
|
44
44
|
`ui.icon()`, and anywhere else an `icon` option is accepted (page icons, About fields, `ui.card`, `ui.button`, toasts, dialogs, the [welcome and what's new screens](welcome.md)), takes:
|
|
45
45
|
|
|
46
46
|
- A built-in name: `'sparkles'`, `'rocket'`, `'puzzle'`, `'palette'`, `'sliders'`, `'info'`, `'check'`, `'x'`, `'alertTriangle'`, `'alertCircle'`, `'chevronRight'`, `'megaphone'`, `'bell'`, `'external'`, `'heart'`, `'copy'`, `'folder'`, `'shield'`.
|
|
47
|
+
- A name your client app registered itself (see below).
|
|
47
48
|
- SVG markup, sanitized (no scripts, event handlers, `foreignObject` or external links).
|
|
48
49
|
- An https or `data:image/…` URL.
|
|
49
50
|
- An element.
|
|
@@ -56,6 +57,35 @@ client.pages.add({ id: 'tools', title: 'Tools', icon: 'sliders', render })
|
|
|
56
57
|
|
|
57
58
|
Every icon is a single colour that follows `currentColor`, so it matches whatever text colour it sits in, in any theme.
|
|
58
59
|
|
|
60
|
+
The built-ins are [Lucide](https://lucide.dev), so Lucide's own names work too: `'triangle-alert'` is the same icon as `'alertTriangle'`, and names match in either kebab-case or camelCase. Lucide draws at a 2 px stroke; to re-weight the whole set, set `stroke-width` on `.yc-icon svg` in your `themes.css` rather than editing the markup.
|
|
61
|
+
|
|
62
|
+
### Your own icons
|
|
63
|
+
|
|
64
|
+
The built-in set is only a default: a client app can add to it, or replace any of it. The simplest way is a folder — every `.svg` in [`icons.dirs`](configuration.md#icons) is registered under its file name, so `icons/brand.svg` becomes `icon: 'brand'`. Give an icon the same name as a built-in and yours replaces it, which is how you swap the whole look without touching the framework:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
icons/
|
|
68
|
+
brand.svg → icon: 'brand'
|
|
69
|
+
rocket.svg → replaces the built-in 'rocket'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Files starting with `_` or `.` are skipped. The markup is inlined into the payload at build time, so there's no loading to wait for, and it's sanitized when it's drawn.
|
|
73
|
+
|
|
74
|
+
Icons can also be given inline in the config, which wins over a folder file of the same name:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
icons: { set: { brand: '<svg viewBox="0 0 24 24">…</svg>' } }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A plugin can register its own at runtime:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
client.ui.registerIcons({ sparkle: '<svg viewBox="0 0 24 24">…</svg>' });
|
|
84
|
+
client.ui.iconNames(); // every name available, built-in and registered
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Write icons on a 24×24 `viewBox` with `stroke="currentColor"` and no `width`/`height` on the root `<svg>` (CSS sizes them, from 13 px in About chips up to 26 px in the About logo tile), so they theme and scale like the built-ins. Markup that isn't SVG is reported through the client app's error handler rather than drawn.
|
|
88
|
+
|
|
59
89
|
## Controls
|
|
60
90
|
|
|
61
91
|
```js
|
package/package.json
CHANGED
package/src/build/build.js
CHANGED
|
@@ -54,16 +54,42 @@ export function findThemes(dirs) {
|
|
|
54
54
|
return out;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Icons in a folder: every `.svg` (not starting with _ or .), registered under its file name, so
|
|
59
|
+
* `icons/brand.svg` becomes `icon: 'brand'`. The markup is inlined into the payload; `width` and
|
|
60
|
+
* `height` are dropped because CSS sizes icons, and it is sanitized when drawn.
|
|
61
|
+
*/
|
|
62
|
+
export function findIcons(dirs) {
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const dir of dirs) {
|
|
65
|
+
if (!existsSync(dir)) continue;
|
|
66
|
+
for (const name of readdirSync(dir).sort()) {
|
|
67
|
+
if (!/\.svg$/i.test(name) || /^[_.]/.test(name)) continue;
|
|
68
|
+
const markup = readFileSync(join(dir, name), 'utf8').trim();
|
|
69
|
+
if (!markup.includes('<svg')) throw new Error(`Icon ${join(dir, name)} is not an SVG`);
|
|
70
|
+
// Exports are pretty-printed; collapse them so the payload stays small. Only the root <svg> loses
|
|
71
|
+
// width/height: the same attributes on a shape (a <rect>, say) are geometry and must stay.
|
|
72
|
+
out[basename(name, extname(name))] = markup
|
|
73
|
+
.replace(/<svg\b[^>]*>/, (open) => open.replace(/\s+(width|height)="[^"]*"/g, '').replace(/\s+/g, ' ').replace(/ (\/?)>$/, '$1>'))
|
|
74
|
+
.replace(/>\s+</g, '><');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
/** The part of the config the running client app gets (plain data, no build paths). */
|
|
58
|
-
export function runtimeConfig(config) {
|
|
81
|
+
export function runtimeConfig(config, { icons } = {}) {
|
|
59
82
|
const { dirs: _pd, ...plugins } = config.plugins;
|
|
60
83
|
const { dirs: _td, css: _css, ...themes } = config.themes;
|
|
84
|
+
const { dirs: _id, set, ...iconOptions } = config.icons ?? {};
|
|
61
85
|
return {
|
|
62
86
|
origins: config.origins,
|
|
63
87
|
settings: config.settings,
|
|
64
88
|
runtime: config.runtime,
|
|
65
89
|
plugins,
|
|
66
90
|
themes,
|
|
91
|
+
// Folder icons first, so an inline `icons.set` entry wins on a name clash.
|
|
92
|
+
icons: { ...iconOptions, set: { ...icons, ...set } },
|
|
67
93
|
pages: config.pages,
|
|
68
94
|
about: config.about,
|
|
69
95
|
welcome: config.welcome,
|
|
@@ -223,6 +249,7 @@ export async function build(config, { root, mode = 'production', onStep = () =>
|
|
|
223
249
|
onStep('Collecting plugins and themes');
|
|
224
250
|
const pluginFiles = findPlugins(config.plugins.dirs);
|
|
225
251
|
const themeFiles = findThemes(config.themes.dirs);
|
|
252
|
+
const iconFiles = findIcons(config.icons?.dirs ?? []);
|
|
226
253
|
const baseCSS = config.themes.css ? readFileSync(config.themes.css, 'utf8') : '';
|
|
227
254
|
|
|
228
255
|
const imports = [`import { boot } from ${JSON.stringify(toPosix(join(SRC, 'runtime', 'boot.js')))};`];
|
|
@@ -260,7 +287,7 @@ export default (args) => boot(args, { entry, plugins, themes });
|
|
|
260
287
|
build: { time: new Date().toISOString(), mode },
|
|
261
288
|
git,
|
|
262
289
|
baseCSS,
|
|
263
|
-
config: runtimeConfig(config),
|
|
290
|
+
config: runtimeConfig(config, { icons: iconFiles }),
|
|
264
291
|
files: {},
|
|
265
292
|
};
|
|
266
293
|
if (mode === 'development') {
|
package/src/config/defaults.js
CHANGED
|
@@ -52,6 +52,13 @@ export const defaults = Object.freeze({
|
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
|
|
55
|
+
icons: {
|
|
56
|
+
// Every .svg in these folders is registered under its file name, so `icons/brand.svg` is `icon: 'brand'`.
|
|
57
|
+
dirs: ['icons'],
|
|
58
|
+
// Icons given inline as `{ name: '<svg…>' }`. A name that matches a built-in replaces it.
|
|
59
|
+
set: {},
|
|
60
|
+
},
|
|
61
|
+
|
|
55
62
|
pages: {
|
|
56
63
|
section: null,
|
|
57
64
|
position: 'end',
|
package/src/config/load.js
CHANGED
|
@@ -8,7 +8,7 @@ import { idPattern } from '../shared/constants.js';
|
|
|
8
8
|
export const CONFIG_FILES = ['yapin-client.config.js', 'yapin-client.config.mjs', 'yapin-client.config.json'];
|
|
9
9
|
|
|
10
10
|
/** Sections that reach the running client app, so they must be JSON. */
|
|
11
|
-
const RUNTIME_SECTIONS = ['settings', 'runtime', 'plugins', 'themes', 'pages', 'about', 'welcome', 'whatsNew', 'badges', 'api', 'host', 'desktop', 'origins', 'links'];
|
|
11
|
+
const RUNTIME_SECTIONS = ['settings', 'runtime', 'plugins', 'themes', 'icons', 'pages', 'about', 'welcome', 'whatsNew', 'badges', 'api', 'host', 'desktop', 'origins', 'links'];
|
|
12
12
|
|
|
13
13
|
export class ConfigError extends Error {
|
|
14
14
|
constructor(problems, file) {
|
|
@@ -141,7 +141,7 @@ export function validateConfig(input, { root = process.cwd(), file = null } = {}
|
|
|
141
141
|
});
|
|
142
142
|
config.api.allowedOrigins = [...allowed];
|
|
143
143
|
|
|
144
|
-
for (const kind of ['plugins', 'themes']) {
|
|
144
|
+
for (const kind of ['plugins', 'themes', 'icons']) {
|
|
145
145
|
config[kind].dirs = (config[kind].dirs ?? []).map((d) => (isAbsolute(d) ? d : resolve(root, d)));
|
|
146
146
|
}
|
|
147
147
|
if (config.entry) {
|
package/src/runtime/client.js
CHANGED
|
@@ -46,7 +46,7 @@ export class Client {
|
|
|
46
46
|
this.host = new Host(config.host ?? {}, { win });
|
|
47
47
|
this.dom = new DomWatcher({ onError: (error) => this.reportError(error, { where: 'dom' }) });
|
|
48
48
|
this.styles = new StyleManager(win?.document);
|
|
49
|
-
this.ui = new UI({ host: this.host, openExternal: (url) => this.openExternal(url), onError: (error) => this.reportError(error, { where: 'ui' }) });
|
|
49
|
+
this.ui = new UI({ host: this.host, openExternal: (url) => this.openExternal(url), onError: (error) => this.reportError(error, { where: 'ui' }), icons: config.icons?.set });
|
|
50
50
|
this.react = {
|
|
51
51
|
fiberOf,
|
|
52
52
|
propsOf,
|
package/src/runtime/ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SettingTypes } from './settings.js';
|
|
2
|
-
import { builtinIcon } from '../shared/icons.js';
|
|
2
|
+
import { builtinIcon, ICONS } from '../shared/icons.js';
|
|
3
3
|
import { safeURL, toArray } from './util.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -171,13 +171,45 @@ export const baseCSS = `
|
|
|
171
171
|
*/
|
|
172
172
|
export class UI {
|
|
173
173
|
#toastRoot = null;
|
|
174
|
+
#icons = new Map();
|
|
174
175
|
|
|
175
|
-
constructor({ host, openExternal, onError } = {}) {
|
|
176
|
+
constructor({ host, openExternal, onError, icons } = {}) {
|
|
176
177
|
this.host = host;
|
|
177
178
|
this.openExternal = openExternal ?? ((url) => globalThis.open?.(url, '_blank', 'noopener,noreferrer'));
|
|
178
179
|
this.onError = onError ?? ((error) => console.error(error));
|
|
179
180
|
this.h = h;
|
|
180
181
|
this.svg = svg;
|
|
182
|
+
if (icons) this.registerIcons(icons);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Adds icons to this client app's set, as `{ name: '<svg…>' }`. They can be used by name anywhere an
|
|
187
|
+
* icon is accepted, exactly like the built-ins, and a name that matches a built-in replaces it, so a
|
|
188
|
+
* client app can swap the whole look without touching the framework. Markup is sanitized when drawn.
|
|
189
|
+
*
|
|
190
|
+
* The build registers `icons.dirs` and `icons.set` from the config automatically; plugins can call
|
|
191
|
+
* this to add their own. Names are matched case-insensitively in kebab-case or camelCase.
|
|
192
|
+
*/
|
|
193
|
+
registerIcons(icons) {
|
|
194
|
+
for (const [name, markup] of Object.entries(icons ?? {})) {
|
|
195
|
+
if (typeof name !== 'string' || !name) continue;
|
|
196
|
+
if (typeof markup !== 'string' || !markup.includes('<svg')) {
|
|
197
|
+
this.onError(new TypeError(`Icon ${JSON.stringify(name)} must be SVG markup`));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
this.#icons.set(name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()), markup);
|
|
201
|
+
}
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Every icon name this client app can use: the built-ins plus any registered ones. */
|
|
206
|
+
iconNames() {
|
|
207
|
+
return [...new Set([...Object.keys(ICONS), ...this.#icons.keys()])].sort();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Looks up an icon name: this client app's own icons first, then the built-ins. */
|
|
211
|
+
#lookupIcon(name) {
|
|
212
|
+
return this.#icons.get(name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())) ?? builtinIcon(name);
|
|
181
213
|
}
|
|
182
214
|
|
|
183
215
|
#cls(...keys) {
|
|
@@ -213,11 +245,11 @@ export class UI {
|
|
|
213
245
|
return h('div', { class: 'yc-empty' }, text);
|
|
214
246
|
}
|
|
215
247
|
|
|
216
|
-
/** An icon: a
|
|
248
|
+
/** An icon: a name ('rocket', 'puzzle', … or one registered by this client app), SVG markup (sanitized), an https/data image URL, or an element. */
|
|
217
249
|
icon(source) {
|
|
218
250
|
if (!source) return null;
|
|
219
251
|
if (source instanceof Node) return h('span', { class: 'yc-icon' }, source);
|
|
220
|
-
if (typeof source === 'string' && !source.includes('<') && !/^(https?:|data:)/.test(source)) source =
|
|
252
|
+
if (typeof source === 'string' && !source.includes('<') && !/^(https?:|data:)/.test(source)) source = this.#lookupIcon(source) ?? source;
|
|
221
253
|
const asSvg = svg(source);
|
|
222
254
|
if (asSvg) return h('span', { class: 'yc-icon', attrs: { 'aria-hidden': 'true' } }, asSvg);
|
|
223
255
|
const url = safeURL(source, { protocols: ['https:', 'data:'] });
|
package/src/shared/constants.js
CHANGED
package/src/shared/icons.js
CHANGED
|
@@ -6,47 +6,75 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Anywhere that takes an icon also still takes raw SVG markup, an https/data image URL, or an element;
|
|
8
8
|
* names here are just a shortcut so client apps don't have to carry their own SVGs for common things.
|
|
9
|
+
*
|
|
10
|
+
* The icons are Lucide (https://lucide.dev), kept byte-for-byte as upstream draws them apart from the
|
|
11
|
+
* `width`/`height` attributes, which are dropped because CSS sizes them (`.yc-icon`). Lucide draws at
|
|
12
|
+
* a 2px stroke; to re-weight the whole set, set `stroke-width` on `.yc-icon svg` in CSS rather than
|
|
13
|
+
* editing the paths here, so the set stays easy to update from upstream.
|
|
14
|
+
*
|
|
15
|
+
* Lucide is ISC licensed, and the icons it inherits from Feather are MIT licensed. Both notices are
|
|
16
|
+
* reproduced in LICENSE, under "Third-party notices", and must be kept with any copy of this file.
|
|
17
|
+
*
|
|
18
|
+
* ISC: Copyright (c) 2026 Lucide Icons and Contributors.
|
|
19
|
+
* MIT: Copyright (c) 2013-present Cole Bemis (Feather).
|
|
20
|
+
*
|
|
21
|
+
* Generated from the upstream SVGs; see LICENSE for the full notices.
|
|
9
22
|
*/
|
|
10
23
|
export const ICONS = Object.freeze({
|
|
11
24
|
sparkles:
|
|
12
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
25
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg>',
|
|
13
26
|
rocket:
|
|
14
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
27
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09"/><path d="M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05"/></svg>',
|
|
15
28
|
puzzle:
|
|
16
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
29
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z"/></svg>',
|
|
17
30
|
palette:
|
|
18
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
19
|
-
sliders:
|
|
20
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
31
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z"/><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/></svg>',
|
|
32
|
+
sliders: // lucide: sliders-horizontal
|
|
33
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 5H3"/><path d="M12 19H3"/><path d="M14 3v4"/><path d="M16 17v4"/><path d="M21 12h-9"/><path d="M21 19h-5"/><path d="M21 5h-7"/><path d="M8 10v4"/><path d="M8 12H3"/></svg>',
|
|
21
34
|
info:
|
|
22
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
35
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
|
|
23
36
|
check:
|
|
24
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
25
|
-
x:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
|
|
38
|
+
x:
|
|
39
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
|
|
40
|
+
alertTriangle: // lucide: triangle-alert
|
|
41
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>',
|
|
42
|
+
alertCircle: // lucide: circle-alert
|
|
43
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>',
|
|
44
|
+
chevronRight: // lucide: chevron-right
|
|
45
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>',
|
|
32
46
|
megaphone:
|
|
33
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
47
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/><path d="M8 6v8"/></svg>',
|
|
34
48
|
bell:
|
|
35
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
36
|
-
external:
|
|
37
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
49
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>',
|
|
50
|
+
external: // lucide: external-link
|
|
51
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>',
|
|
38
52
|
heart:
|
|
39
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
53
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"/></svg>',
|
|
40
54
|
copy:
|
|
41
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
55
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
|
|
42
56
|
folder:
|
|
43
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
44
|
-
shield:
|
|
45
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="
|
|
57
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>',
|
|
58
|
+
shield: // lucide: shield-check
|
|
59
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lucide's own names for icons whose key here differs, so `icon: 'triangle-alert'` works as well as
|
|
64
|
+
* `icon: 'alertTriangle'`. Keys are camelCase because names are camelised before lookup.
|
|
65
|
+
*/
|
|
66
|
+
const ALIASES = Object.freeze({
|
|
67
|
+
slidersHorizontal: 'sliders',
|
|
68
|
+
triangleAlert: 'alertTriangle',
|
|
69
|
+
circleAlert: 'alertCircle',
|
|
70
|
+
externalLink: 'external',
|
|
71
|
+
shieldCheck: 'shield',
|
|
72
|
+
close: 'x',
|
|
46
73
|
});
|
|
47
74
|
|
|
48
|
-
/** Looks up a built-in icon by name (case-insensitive). Returns raw SVG markup, or null. */
|
|
75
|
+
/** Looks up a built-in icon by name (case-insensitive, kebab-case or camelCase). Returns raw SVG markup, or null. */
|
|
49
76
|
export function builtinIcon(name) {
|
|
50
77
|
if (typeof name !== 'string') return null;
|
|
51
|
-
|
|
78
|
+
const camel = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
79
|
+
return ICONS[name] ?? ICONS[camel] ?? ICONS[ALIASES[camel]] ?? null;
|
|
52
80
|
}
|
|
File without changes
|
|
@@ -22,6 +22,12 @@ export default defineConfig({
|
|
|
22
22
|
dirs: ['./themes'],
|
|
23
23
|
},
|
|
24
24
|
|
|
25
|
+
// Drop .svg files in ./icons to use them by name (icons/brand.svg -> icon: 'brand').
|
|
26
|
+
// A name matching a built-in replaces it. See docs/ui.md.
|
|
27
|
+
icons: {
|
|
28
|
+
dirs: ['./icons'],
|
|
29
|
+
},
|
|
30
|
+
|
|
25
31
|
installer: {
|
|
26
32
|
preset: 'default',
|
|
27
33
|
marketing: './installer/marketing.json',
|
package/types/index.d.ts
CHANGED
|
@@ -96,6 +96,13 @@ export interface Config {
|
|
|
96
96
|
customCSS?: { enabled?: boolean; title?: string; description?: string | null };
|
|
97
97
|
user?: UserInstallConfig;
|
|
98
98
|
};
|
|
99
|
+
/** Icons this client app adds to the built-in set, usable by name anywhere an `icon` is accepted. */
|
|
100
|
+
icons?: {
|
|
101
|
+
/** Folders with `.svg` icons; each is registered under its file name (`icons/brand.svg` -> `'brand'`). */
|
|
102
|
+
dirs?: string[];
|
|
103
|
+
/** Icons given inline as `{ name: '<svg…>' }`. Wins over a folder file of the same name, and a name matching a built-in replaces it. */
|
|
104
|
+
set?: Record<string, string>;
|
|
105
|
+
};
|
|
99
106
|
pages?: {
|
|
100
107
|
/** The nav label above the client app's pages (default: its name). */
|
|
101
108
|
section?: string | null;
|
|
@@ -220,6 +227,8 @@ export interface BuildResult {
|
|
|
220
227
|
export declare function build(config: Record<string, any>, options: { root: string; mode?: 'production' | 'development'; installer?: boolean; onStep?: (step: string) => void }): Promise<BuildResult>;
|
|
221
228
|
export declare function findPlugins(dirs: string[]): string[];
|
|
222
229
|
export declare function findThemes(dirs: string[]): string[];
|
|
230
|
+
/** Every `.svg` in the folders, keyed by file name, with the root `<svg>` size stripped. */
|
|
231
|
+
export declare function findIcons(dirs: string[]): Record<string, string>;
|
|
223
232
|
export declare function runtimeConfig(config: Record<string, any>): Record<string, any>;
|
|
224
233
|
|
|
225
234
|
export declare function pack(options: {
|
package/types/runtime.d.ts
CHANGED
|
@@ -84,6 +84,10 @@ export declare class UI {
|
|
|
84
84
|
tag(text: string, variant?: 'warn' | 'danger'): HTMLElement;
|
|
85
85
|
empty(text: string): HTMLElement;
|
|
86
86
|
icon(source: string | Node): HTMLElement | null;
|
|
87
|
+
/** Adds icons to this client app's set as `{ name: '<svg…>' }`; a name matching a built-in replaces it. */
|
|
88
|
+
registerIcons(icons: Record<string, string>): this;
|
|
89
|
+
/** Every icon name available: the built-ins plus any registered. */
|
|
90
|
+
iconNames(): string[];
|
|
87
91
|
button(options: { label: string; variant?: 'primary' | 'ghost' | 'danger'; small?: boolean; disabled?: boolean; title?: string; icon?: string; onClick?: (event: MouseEvent) => unknown }): HTMLButtonElement;
|
|
88
92
|
link(options: { href: string; label?: string; title?: string }): HTMLElement;
|
|
89
93
|
switch(options: { checked?: boolean; disabled?: boolean; label?: string; onChange?: (checked: boolean, event: Event) => void }): HTMLInputElement;
|