yapin.js-client 0.1.0 → 0.1.4
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/docs/README.md +1 -0
- package/docs/about-page.md +19 -2
- package/docs/configuration.md +36 -2
- package/docs/getting-started.md +1 -1
- package/docs/installer.md +39 -12
- package/docs/ui.md +20 -3
- package/docs/welcome.md +63 -0
- package/package.json +1 -1
- package/src/build/build.js +34 -22
- package/src/build/nsis.js +763 -0
- package/src/config/defaults.js +30 -3
- package/src/config/load.js +9 -1
- package/src/desktop/preload.cjs +3 -1
- package/src/desktop/shim.cjs +60 -11
- package/src/installer/main.js +5 -0
- package/src/installer/nsis.js +75 -0
- package/src/installer/operations.js +23 -18
- package/src/installer/page.js +29 -0
- package/src/installer/processes.js +74 -19
- package/src/runtime/about.js +126 -36
- package/src/runtime/builtin-pages.js +2 -2
- package/src/runtime/client.js +21 -1
- package/src/runtime/onboarding.js +196 -0
- package/src/runtime/ui.js +52 -9
- package/src/shared/constants.js +1 -1
- package/src/shared/icons.js +52 -0
- package/types/index.d.ts +38 -1
- package/types/runtime.d.ts +26 -2
package/src/runtime/about.js
CHANGED
|
@@ -26,11 +26,11 @@ export class AboutPage {
|
|
|
26
26
|
const c = this.client;
|
|
27
27
|
const { build = {}, git = null } = c.manifest;
|
|
28
28
|
const env = c.environment;
|
|
29
|
-
this.section('app', { title: null, order: 0 });
|
|
29
|
+
this.section('app', { title: null, order: 0, layout: 'header' });
|
|
30
30
|
this.section('build', { title: 'Build', order: 10 });
|
|
31
31
|
this.section('git', { title: 'Source', order: 20 });
|
|
32
32
|
this.section('environment', { title: 'Environment', order: 30 });
|
|
33
|
-
this.section('links', { title: 'Links', order: 40 });
|
|
33
|
+
this.section('links', { title: 'Links', order: 40, layout: 'chips' });
|
|
34
34
|
|
|
35
35
|
this.set('app.name', { label: 'Name', value: c.name, order: 0 });
|
|
36
36
|
this.set('app.version', { label: 'Version', value: c.version, copy: true, order: 1 });
|
|
@@ -67,16 +67,20 @@ export class AboutPage {
|
|
|
67
67
|
|
|
68
68
|
this.action('copy', {
|
|
69
69
|
label: 'Copy debug info',
|
|
70
|
+
icon: 'copy',
|
|
70
71
|
order: 0,
|
|
71
72
|
run: async () => {
|
|
72
73
|
await navigator.clipboard.writeText(this.debugInfo());
|
|
73
74
|
c.ui.toast('Debug info copied', { type: 'success' });
|
|
74
75
|
},
|
|
75
76
|
});
|
|
77
|
+
if (c.welcome?.enabled) this.action('welcome', { label: 'Welcome screen', icon: 'sparkles', order: 3, variant: 'ghost', run: () => c.welcome.show() });
|
|
78
|
+
if (c.whatsNew?.enabled && c.whatsNew.config.entries?.length) this.action('whatsNew', { label: "What's new", icon: 'rocket', order: 4, variant: 'ghost', run: () => c.whatsNew.show() });
|
|
76
79
|
if (c.native) {
|
|
77
|
-
this.action('folder', { label: '
|
|
80
|
+
this.action('folder', { label: 'Data folder', icon: 'folder', order: 1, variant: 'ghost', run: () => c.native.openFolder('data') });
|
|
78
81
|
this.action('safeMode', {
|
|
79
|
-
label: c.safeMode ? 'Restart normally' : '
|
|
82
|
+
label: c.safeMode ? 'Restart normally' : 'Safe mode',
|
|
83
|
+
icon: 'shield',
|
|
80
84
|
order: 2,
|
|
81
85
|
variant: 'ghost',
|
|
82
86
|
run: async () => {
|
|
@@ -103,14 +107,29 @@ export class AboutPage {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Adds or changes a section: section('support', { title: 'Support', order: 35, layout: 'rows' }).
|
|
112
|
+
* Layouts: 'rows' (label and value, the default), 'chips' (compact pills, for links) and 'header'
|
|
113
|
+
* (the card at the top: its `name`, `version` and `description` fields become the title, a version
|
|
114
|
+
* tag and the line under it; any other field is shown small underneath).
|
|
115
|
+
*/
|
|
116
|
+
section(id, { title, order, layout } = {}) {
|
|
108
117
|
const existing = this.#sections.get(id);
|
|
109
|
-
this.#sections.set(id, {
|
|
118
|
+
this.#sections.set(id, {
|
|
119
|
+
id,
|
|
120
|
+
title: title === undefined ? existing?.title ?? id : title,
|
|
121
|
+
order: order ?? existing?.order ?? 100,
|
|
122
|
+
layout: LAYOUTS.has(layout) ? layout : existing?.layout ?? 'rows',
|
|
123
|
+
});
|
|
110
124
|
this.#changed();
|
|
111
125
|
return this;
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
/** The sections, in order: [{ id, title, order, layout }]. */
|
|
129
|
+
sections() {
|
|
130
|
+
return this.#sortedSections().map((s) => ({ ...s }));
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
removeSection(id) {
|
|
115
134
|
this.#sections.delete(id);
|
|
116
135
|
for (const key of [...this.#fields.keys()]) if (key.startsWith(`${id}.`)) this.#fields.delete(key);
|
|
@@ -148,10 +167,10 @@ export class AboutPage {
|
|
|
148
167
|
return [...this.#fields.values()].map((f) => ({ ...f }));
|
|
149
168
|
}
|
|
150
169
|
|
|
151
|
-
/** A button
|
|
152
|
-
action(id, { label, run, variant = 'primary', order = 100 } = {}) {
|
|
170
|
+
/** A button under the header: action('report', { label, icon, run, variant, order }). */
|
|
171
|
+
action(id, { label, icon = null, run, variant = 'primary', order = 100 } = {}) {
|
|
153
172
|
if (typeof run !== 'function') throw new ClientError(ErrorCodes.InvalidDefinition, `About action "${id}" needs a run function`);
|
|
154
|
-
this.#actions.set(id, { id, label: label ?? id, run, variant, order });
|
|
173
|
+
this.#actions.set(id, { id, label: label ?? id, icon, run, variant, order });
|
|
155
174
|
this.#changed();
|
|
156
175
|
return this;
|
|
157
176
|
}
|
|
@@ -196,16 +215,22 @@ export class AboutPage {
|
|
|
196
215
|
render(root, ctx) {
|
|
197
216
|
const { ui } = this.client;
|
|
198
217
|
const draw = () => {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
218
|
+
const sections = this.#sortedSections()
|
|
219
|
+
.map((section) => ({ ...section, fields: this.#sortedFields(section.id) }))
|
|
220
|
+
.filter((section) => section.fields.length);
|
|
221
|
+
const header = sections.find((s) => s.layout === 'header');
|
|
222
|
+
const cards = sections.filter((s) => s !== header);
|
|
223
|
+
root.replaceChildren(
|
|
224
|
+
ui.heading(this.config.title ?? 'About'),
|
|
225
|
+
h(
|
|
226
|
+
'div',
|
|
227
|
+
{ class: 'yc-about' },
|
|
228
|
+
header && this.#header(header),
|
|
229
|
+
this.config.intro && ui.hint(this.config.intro),
|
|
230
|
+
this.#actionBar(),
|
|
231
|
+
cards.length > 0 && h('div', { class: 'yc-about-grid' }, cards.map((s) => this.#card(s))),
|
|
232
|
+
),
|
|
233
|
+
);
|
|
209
234
|
};
|
|
210
235
|
this.onChange = draw;
|
|
211
236
|
draw();
|
|
@@ -214,29 +239,94 @@ export class AboutPage {
|
|
|
214
239
|
});
|
|
215
240
|
}
|
|
216
241
|
|
|
217
|
-
#
|
|
242
|
+
#header(section) {
|
|
218
243
|
const { ui } = this.client;
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const
|
|
244
|
+
const field = (name) => section.fields.find((f) => f.id === `${section.id}.${name}`);
|
|
245
|
+
const [name, version, description] = [field('name'), field('version'), field('description')];
|
|
246
|
+
const rest = section.fields.filter((f) => f !== name && f !== version && f !== description);
|
|
247
|
+
const title = name ? valueText(name) : this.client.name;
|
|
248
|
+
const logo = this.config.logo ? ui.icon(this.config.logo) : null;
|
|
223
249
|
return h(
|
|
224
|
-
'
|
|
225
|
-
{ class: 'yc-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
250
|
+
'div',
|
|
251
|
+
{ class: 'yc-about-header', dataset: { section: section.id } },
|
|
252
|
+
h('div', { class: 'yc-about-logo', attrs: { 'aria-hidden': 'true' } }, logo ?? title.trim().charAt(0).toUpperCase()),
|
|
253
|
+
h(
|
|
254
|
+
'div',
|
|
255
|
+
{ class: 'yc-grow' },
|
|
256
|
+
h('div', { class: 'yc-about-title' }, title, version && h('span', { class: 'yc-about-version' }, `v${valueText(version).replace(/^v/, '')}`, version.copy && this.#copyButton(version))),
|
|
257
|
+
description && h('div', { class: 'yc-about-desc' }, valueText(description)),
|
|
258
|
+
rest.length > 0 && h('div', { class: 'yc-about-meta' }, rest.map((f) => h('span', null, h('span', { class: 'yc-about-label' }, f.label), ' ', this.#value(f)))),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#actionBar() {
|
|
264
|
+
const { ui } = this.client;
|
|
265
|
+
const actions = [...this.#actions.values()].sort((a, b) => a.order - b.order);
|
|
266
|
+
if (!actions.length) return null;
|
|
267
|
+
return h('div', { class: 'yc-about-actions' }, actions.map((a) => ui.button({ label: a.label, icon: a.icon, variant: a.variant, small: true, onClick: () => a.run(this.client) })));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#card(section) {
|
|
271
|
+
return h(
|
|
272
|
+
'section',
|
|
273
|
+
{ class: 'yc-about-card', dataset: { section: section.id, layout: section.layout } },
|
|
274
|
+
section.title && h('div', { class: 'yc-section-title' }, section.title),
|
|
275
|
+
section.layout === 'chips' ? this.#chips(section.fields) : this.#rows(section.fields),
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#rows(fields) {
|
|
280
|
+
return h(
|
|
281
|
+
'dl',
|
|
282
|
+
{ class: 'yc-about-rows' },
|
|
283
|
+
fields.flatMap((f) => [h('dt', null, f.label), h('dd', null, this.#value(f))]),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#chips(fields) {
|
|
288
|
+
const { ui } = this.client;
|
|
289
|
+
return h(
|
|
290
|
+
'div',
|
|
291
|
+
{ class: 'yc-about-chips' },
|
|
292
|
+
fields.map((f) => {
|
|
293
|
+
if (f.href) return h(ui.link({ href: f.href, label: f.label, title: valueText(f) }), { class: 'yc-about-chip' }, ui.icon('external'));
|
|
294
|
+
return h('span', { class: 'yc-about-chip', title: f.label }, this.#value(f));
|
|
235
295
|
}),
|
|
236
296
|
);
|
|
237
297
|
}
|
|
298
|
+
|
|
299
|
+
#value(field) {
|
|
300
|
+
const { ui } = this.client;
|
|
301
|
+
const text = valueText(field);
|
|
302
|
+
const content = field.href ? ui.link({ href: field.href, label: text }) : field.code ? h('code', null, text) : text;
|
|
303
|
+
return h('span', { class: 'yc-about-value', title: text }, h('span', { class: 'yc-about-text' }, content), field.copy && this.#copyButton(field));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#copyButton(field) {
|
|
307
|
+
const { ui } = this.client;
|
|
308
|
+
const text = field.copy === true ? valueText(field) : String(field.copy);
|
|
309
|
+
const button = h('button', {
|
|
310
|
+
type: 'button',
|
|
311
|
+
class: 'yc-about-copy',
|
|
312
|
+
title: `Copy ${field.label.toLowerCase()}`,
|
|
313
|
+
attrs: { 'aria-label': `Copy ${field.label.toLowerCase()}` },
|
|
314
|
+
onClick: async () => {
|
|
315
|
+
try {
|
|
316
|
+
await navigator.clipboard.writeText(text);
|
|
317
|
+
button.replaceChildren(ui.icon('check'));
|
|
318
|
+
setTimeout(() => button.isConnected && button.replaceChildren(ui.icon('copy')), 1200);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
ui.toast(`Couldn't copy: ${error?.message ?? error}`, { type: 'error' });
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
}, ui.icon('copy'));
|
|
324
|
+
return button;
|
|
325
|
+
}
|
|
238
326
|
}
|
|
239
327
|
|
|
328
|
+
const LAYOUTS = new Set(['rows', 'chips', 'header']);
|
|
329
|
+
|
|
240
330
|
function valueText(field) {
|
|
241
331
|
try {
|
|
242
332
|
const value = typeof field.value === 'function' ? field.value() : field.value;
|
|
@@ -27,7 +27,7 @@ export function pluginsPage(client, config = {}) {
|
|
|
27
27
|
id: 'plugins',
|
|
28
28
|
title: config.title ?? 'Plugins',
|
|
29
29
|
order: config.order ?? 10,
|
|
30
|
-
icon: config.icon ??
|
|
30
|
+
icon: config.icon ?? 'puzzle',
|
|
31
31
|
render(root, ctx) {
|
|
32
32
|
const { ui } = client;
|
|
33
33
|
let query = '';
|
|
@@ -142,7 +142,7 @@ export function themesPage(client, config = {}) {
|
|
|
142
142
|
id: 'themes',
|
|
143
143
|
title: config.title ?? 'Themes',
|
|
144
144
|
order: config.order ?? 20,
|
|
145
|
-
icon: config.icon ??
|
|
145
|
+
icon: config.icon ?? 'palette',
|
|
146
146
|
render(root, ctx) {
|
|
147
147
|
const { ui } = client;
|
|
148
148
|
let query = '';
|
package/src/runtime/client.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Emitter } from './events.js';
|
|
|
7
7
|
import { Host } from './host.js';
|
|
8
8
|
import { Logger } from './logger.js';
|
|
9
9
|
import { ModuleFinder } from './modules.js';
|
|
10
|
+
import { Welcome, WhatsNew, onboardingCSS } from './onboarding.js';
|
|
10
11
|
import { PageManager } from './pages.js';
|
|
11
12
|
import { Patcher } from './patcher.js';
|
|
12
13
|
import { PluginManager } from './plugins.js';
|
|
@@ -63,6 +64,8 @@ export class Client {
|
|
|
63
64
|
this.plugins = new PluginManager(this, config.plugins ?? {}, { loadModule });
|
|
64
65
|
this.badges = new BadgeManager(this, config.badges ?? {});
|
|
65
66
|
this.userContent = new UserContent(this, { plugins: config.plugins ?? {}, themes: config.themes ?? {} });
|
|
67
|
+
this.welcome = config.welcome === false ? null : new Welcome(this, config.welcome ?? {});
|
|
68
|
+
this.whatsNew = config.whatsNew === false ? null : new WhatsNew(this, config.whatsNew ?? {});
|
|
66
69
|
this.about = config.about === false ? null : new AboutPage(this, config.about ?? {});
|
|
67
70
|
this.ready = new Promise((resolve) => {
|
|
68
71
|
this.#readyResolve = resolve;
|
|
@@ -99,6 +102,7 @@ export class Client {
|
|
|
99
102
|
async start(bundle = {}) {
|
|
100
103
|
const config = this.config;
|
|
101
104
|
this.styles.add(baseCSS, { id: 'framework', order: StyleOrder.framework });
|
|
105
|
+
this.styles.add(onboardingCSS, { id: 'framework-onboarding', order: StyleOrder.framework });
|
|
102
106
|
for (const theme of bundle.themes ?? []) {
|
|
103
107
|
try {
|
|
104
108
|
this.themes.add(this.themes.parse(theme.text, { file: theme.file, source: 'bundled' }), { source: 'bundled', file: theme.file });
|
|
@@ -146,6 +150,11 @@ export class Client {
|
|
|
146
150
|
this.badges.start();
|
|
147
151
|
await this.plugins.startAll();
|
|
148
152
|
if (this.safeMode) this.ui.toast(`${this.name} is in safe mode: plugins and themes are off.`, { type: 'warning', timeout: 8000 });
|
|
153
|
+
try {
|
|
154
|
+
this.#maybeShowOnboarding();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
this.reportError(error, { where: 'welcome/what\'s new' });
|
|
157
|
+
}
|
|
149
158
|
this.isReady = true;
|
|
150
159
|
this.#readyResolve(this);
|
|
151
160
|
this.events.emit('ready', this);
|
|
@@ -156,7 +165,7 @@ export class Client {
|
|
|
156
165
|
#addBuiltinPages() {
|
|
157
166
|
const pages = this.config.pages ?? {};
|
|
158
167
|
if (this.about) {
|
|
159
|
-
this.pages.add({ id: 'about', title: this.config.about?.title ?? 'About', order: this.config.about?.order ?? 0, icon: this.config.about?.icon ??
|
|
168
|
+
this.pages.add({ id: 'about', title: this.config.about?.title ?? 'About', order: this.config.about?.order ?? 0, icon: this.config.about?.icon ?? 'info', render: (root, ctx) => this.about.render(root, ctx) });
|
|
160
169
|
}
|
|
161
170
|
if (pages.plugins !== false) this.pages.add(pluginsPage(this, pages.plugins ?? {}));
|
|
162
171
|
if (pages.themes !== false) this.pages.add(themesPage(this, pages.themes ?? {}));
|
|
@@ -166,6 +175,7 @@ export class Client {
|
|
|
166
175
|
id: 'settings',
|
|
167
176
|
title: options.title ?? 'Settings',
|
|
168
177
|
order: options.order ?? 5,
|
|
178
|
+
icon: options.icon ?? 'sliders',
|
|
169
179
|
render: (root, ctx) => {
|
|
170
180
|
const form = this.ui.form(this.settings, { context: this });
|
|
171
181
|
root.append(this.ui.heading(options.title ?? 'Settings'), form.element);
|
|
@@ -175,6 +185,16 @@ export class Client {
|
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
|
|
188
|
+
/** First run shows the welcome screen (and skips what's new for that first run); otherwise an update shows what's new. */
|
|
189
|
+
#maybeShowOnboarding() {
|
|
190
|
+
if (this.welcome?.shouldShow()) {
|
|
191
|
+
this.welcome.show();
|
|
192
|
+
this.whatsNew?.markSeen();
|
|
193
|
+
} else {
|
|
194
|
+
this.whatsNew?.maybeShow();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
178
198
|
/** Stops plugins, removes styles and flushes storage. Mostly for tests and development reloads. */
|
|
179
199
|
stop() {
|
|
180
200
|
this.plugins.stopAll();
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { h } from './ui.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The rounded-corner "Welcome" screen a client app shows the first time it runs with no saved state,
|
|
5
|
+
* and the "What's new" screen it shows after an update. Both are entirely optional (`welcome: false` /
|
|
6
|
+
* `whatsNew: false`), entirely editable from the config (title, icon, steps/entries, labels), and can
|
|
7
|
+
* be replayed at any time with `client.welcome.show()` / `client.whatsNew.show()`.
|
|
8
|
+
*/
|
|
9
|
+
const SEEN_NAMESPACE = 'onboarding';
|
|
10
|
+
|
|
11
|
+
function fill(text, client) {
|
|
12
|
+
if (typeof text !== 'string') return text;
|
|
13
|
+
return text.replace(/\{name\}/g, client.name).replace(/\{version\}/g, client.version);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Builds the shared rounded, animated panel. Returns the backdrop element; caller appends it. */
|
|
17
|
+
function panel(client, { icon, title, subtitle, rows = [], actionLabel, dismissLabel, dismissable = true, onAction, onDismiss, onClose }) {
|
|
18
|
+
const { ui } = client;
|
|
19
|
+
const doc = globalThis.document;
|
|
20
|
+
let settled = false;
|
|
21
|
+
const finish = (fn) => {
|
|
22
|
+
if (settled) return;
|
|
23
|
+
settled = true;
|
|
24
|
+
backdrop.classList.add('yc-onboard-leave');
|
|
25
|
+
doc.removeEventListener('keydown', onKey, true);
|
|
26
|
+
setTimeout(() => backdrop.remove(), 180);
|
|
27
|
+
fn?.();
|
|
28
|
+
onClose?.();
|
|
29
|
+
};
|
|
30
|
+
const onKey = (event) => {
|
|
31
|
+
if (event.key === 'Escape' && dismissable) {
|
|
32
|
+
event.stopPropagation();
|
|
33
|
+
finish(onDismiss);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const rowsEl = rows.map((row, index) =>
|
|
37
|
+
h(
|
|
38
|
+
'div',
|
|
39
|
+
{ class: 'yc-onboard-row', style: { '--yc-i': String(index) } },
|
|
40
|
+
row.icon !== false && h('div', { class: 'yc-onboard-row-icon' }, ui.icon(row.icon ?? 'sparkles')),
|
|
41
|
+
h('div', { class: 'yc-onboard-row-body' }, h('div', { class: 'yc-onboard-row-title' }, row.title), row.description && h('div', { class: 'yc-onboard-row-desc' }, row.description)),
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
const buttons = [];
|
|
45
|
+
if (dismissable && dismissLabel) buttons.push(ui.button({ label: dismissLabel, variant: 'ghost', onClick: () => finish(onDismiss) }));
|
|
46
|
+
buttons.push(ui.button({ label: actionLabel, variant: 'primary', onClick: () => finish(onAction) }));
|
|
47
|
+
const dialog = h(
|
|
48
|
+
'div',
|
|
49
|
+
{ class: 'yc-onboard', attrs: { role: 'dialog', 'aria-modal': 'true' } },
|
|
50
|
+
dismissable && h('button', { type: 'button', class: 'yc-onboard-close', attrs: { 'aria-label': 'Dismiss' }, onClick: () => finish(onDismiss) }, ui.icon('x')),
|
|
51
|
+
h('div', { class: 'yc-onboard-hero' }, h('div', { class: 'yc-onboard-hero-glow' }), h('div', { class: 'yc-onboard-hero-icon' }, ui.icon(icon ?? 'sparkles'))),
|
|
52
|
+
h('h2', { class: 'yc-onboard-title' }, title),
|
|
53
|
+
subtitle && h('p', { class: 'yc-onboard-subtitle' }, subtitle),
|
|
54
|
+
rowsEl.length ? h('div', { class: 'yc-onboard-rows' }, rowsEl) : null,
|
|
55
|
+
h('div', { class: 'yc-onboard-actions' }, buttons),
|
|
56
|
+
);
|
|
57
|
+
const backdrop = h('div', { class: 'yc-onboard-backdrop', onMousedown: (event) => event.target === backdrop && dismissable && finish(onDismiss) }, dialog);
|
|
58
|
+
doc.addEventListener('keydown', onKey, true);
|
|
59
|
+
return backdrop;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class OnboardingScreen {
|
|
63
|
+
constructor(client, config, storageKey) {
|
|
64
|
+
this.client = client;
|
|
65
|
+
this.config = config ?? {};
|
|
66
|
+
this.storageKey = storageKey;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get enabled() {
|
|
70
|
+
return this.config !== false && this.config?.enabled !== false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#store() {
|
|
74
|
+
return this.client.storage.namespace(SEEN_NAMESPACE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Marks the screen as seen without showing it, so it won't appear next time either. */
|
|
78
|
+
markSeen() {
|
|
79
|
+
this.#store().set(this.storageKey, this.client.version);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Clears the seen flag so it shows again next time the client app starts (or call show() now). */
|
|
83
|
+
reset() {
|
|
84
|
+
this.#store().delete(this.storageKey);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Shown the first time the client app runs (nothing saved yet). `welcome: false` turns it off. */
|
|
89
|
+
export class Welcome extends OnboardingScreen {
|
|
90
|
+
constructor(client, config) {
|
|
91
|
+
super(client, config, 'welcomeSeenVersion');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
shouldShow() {
|
|
95
|
+
return this.enabled && !this.#store().has(this.storageKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#store() {
|
|
99
|
+
return this.client.storage.namespace(SEEN_NAMESPACE);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Shows the welcome screen right now, regardless of whether it's been seen. */
|
|
103
|
+
show() {
|
|
104
|
+
const doc = globalThis.document;
|
|
105
|
+
if (!doc?.body) return () => {};
|
|
106
|
+
const { client, config } = this;
|
|
107
|
+
const backdrop = panel(client, {
|
|
108
|
+
icon: config.icon ?? 'sparkles',
|
|
109
|
+
title: fill(config.title ?? 'Welcome to {name}', client),
|
|
110
|
+
subtitle: fill(config.subtitle ?? null, client),
|
|
111
|
+
rows: (config.steps ?? []).map((s) => ({ icon: s.icon, title: fill(s.title, client), description: fill(s.description, client) })),
|
|
112
|
+
actionLabel: config.actionLabel ?? 'Get started',
|
|
113
|
+
dismissLabel: config.dismissible === false ? null : config.dismissLabel ?? 'Skip',
|
|
114
|
+
dismissable: config.dismissible !== false,
|
|
115
|
+
onAction: () => this.markSeen(),
|
|
116
|
+
onDismiss: () => this.markSeen(),
|
|
117
|
+
});
|
|
118
|
+
doc.body.appendChild(backdrop);
|
|
119
|
+
return () => backdrop.remove();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
maybeShow() {
|
|
123
|
+
if (this.shouldShow()) this.show();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Shown after an update, once, when there are entries configured. `whatsNew: false` turns it off. */
|
|
128
|
+
export class WhatsNew extends OnboardingScreen {
|
|
129
|
+
constructor(client, config) {
|
|
130
|
+
super(client, config, 'whatsNewSeenVersion');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
shouldShow() {
|
|
134
|
+
if (!this.enabled || !(this.config.entries ?? []).length) return false;
|
|
135
|
+
return this.#store().get(this.storageKey) !== this.client.version;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#store() {
|
|
139
|
+
return this.client.storage.namespace(SEEN_NAMESPACE);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Shows the what's new screen right now, regardless of version. */
|
|
143
|
+
show() {
|
|
144
|
+
const doc = globalThis.document;
|
|
145
|
+
if (!doc?.body) return () => {};
|
|
146
|
+
const { client, config } = this;
|
|
147
|
+
const backdrop = panel(client, {
|
|
148
|
+
icon: config.icon ?? 'rocket',
|
|
149
|
+
title: fill(config.title ?? "What's new in {name} {version}", client),
|
|
150
|
+
subtitle: fill(config.subtitle ?? null, client),
|
|
151
|
+
rows: (config.entries ?? []).map((e) => ({ icon: e.icon, title: fill(e.title, client), description: fill(e.description, client) })),
|
|
152
|
+
actionLabel: config.actionLabel ?? 'Got it',
|
|
153
|
+
dismissLabel: config.dismissible === false ? null : config.dismissLabel ?? null,
|
|
154
|
+
dismissable: config.dismissible !== false,
|
|
155
|
+
onAction: () => this.markSeen(),
|
|
156
|
+
onDismiss: () => this.markSeen(),
|
|
157
|
+
});
|
|
158
|
+
doc.body.appendChild(backdrop);
|
|
159
|
+
return () => backdrop.remove();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
maybeShow() {
|
|
163
|
+
if (this.shouldShow()) this.show();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const onboardingCSS = `
|
|
168
|
+
.yc-onboard-backdrop{position:fixed;inset:0;z-index:2147483100;background:#0009;display:flex;align-items:center;justify-content:center;padding:20px;animation:yc-onboard-fade .18s ease-out}
|
|
169
|
+
.yc-onboard-backdrop.yc-onboard-leave{animation:yc-onboard-fade .18s ease-in reverse}
|
|
170
|
+
.yc-onboard-backdrop.yc-onboard-leave .yc-onboard{animation:yc-onboard-in .18s ease-in reverse}
|
|
171
|
+
.yc-onboard{position:relative;background:var(--bg-float,#1b1c21);color:var(--text,#ececf1);border:1px solid var(--border);border-radius:20px;box-shadow:var(--shadow,0 20px 60px #0008);width:min(440px,100%);max-height:90vh;overflow:auto;padding:28px 26px 24px;text-align:center;animation:yc-onboard-in .22s cubic-bezier(.2,.8,.2,1)}
|
|
172
|
+
.yc-onboard-close{position:absolute;top:14px;right:14px;width:28px;height:28px;border-radius:999px;border:none;background:var(--bg-active,#2a2b31);color:var(--text-muted);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0}
|
|
173
|
+
.yc-onboard-close:hover{background:var(--bg-hover,#34353c);color:var(--text)}
|
|
174
|
+
.yc-onboard-close .yc-icon{width:14px;height:14px}
|
|
175
|
+
.yc-onboard-hero{position:relative;width:64px;height:64px;margin:0 auto 14px;display:flex;align-items:center;justify-content:center}
|
|
176
|
+
.yc-onboard-hero-glow{position:absolute;inset:0;border-radius:999px;background:radial-gradient(circle,var(--accent) 0%,transparent 70%);opacity:.35;animation:yc-onboard-pulse 2.4s ease-in-out infinite}
|
|
177
|
+
.yc-onboard-hero-icon{position:relative;width:64px;height:64px;border-radius:999px;background:var(--bg-active,#2a2b31);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;color:var(--accent);animation:yc-onboard-pop .4s cubic-bezier(.2,.8,.2,1) .05s backwards}
|
|
178
|
+
.yc-onboard-hero-icon .yc-icon{width:28px;height:28px}
|
|
179
|
+
.yc-onboard-title{margin:0 0 6px;font-size:20px;font-weight:700}
|
|
180
|
+
.yc-onboard-subtitle{margin:0 0 18px;color:var(--text-muted);font-size:14px;line-height:1.5}
|
|
181
|
+
.yc-onboard-rows{display:flex;flex-direction:column;gap:12px;text-align:left;margin-bottom:20px}
|
|
182
|
+
.yc-onboard-row{display:flex;gap:12px;align-items:flex-start;opacity:0;animation:yc-onboard-row-in .35s ease-out forwards;animation-delay:calc(120ms + var(--yc-i) * 70ms)}
|
|
183
|
+
.yc-onboard-row-icon{flex-shrink:0;width:34px;height:34px;border-radius:10px;background:var(--bg-active,#2a2b31);display:flex;align-items:center;justify-content:center;color:var(--accent)}
|
|
184
|
+
.yc-onboard-row-icon .yc-icon{width:18px;height:18px}
|
|
185
|
+
.yc-onboard-row-title{font-weight:600;font-size:14px}
|
|
186
|
+
.yc-onboard-row-desc{color:var(--text-muted);font-size:13px;margin-top:2px;line-height:1.4}
|
|
187
|
+
.yc-onboard-actions{display:flex;justify-content:center;gap:10px}
|
|
188
|
+
@keyframes yc-onboard-fade{from{opacity:0}to{opacity:1}}
|
|
189
|
+
@keyframes yc-onboard-in{from{opacity:0;transform:translateY(14px) scale(.96)}to{opacity:1;transform:none}}
|
|
190
|
+
@keyframes yc-onboard-pop{from{opacity:0;transform:scale(.6)}to{opacity:1;transform:none}}
|
|
191
|
+
@keyframes yc-onboard-pulse{0%,100%{opacity:.25;transform:scale(1)}50%{opacity:.45;transform:scale(1.15)}}
|
|
192
|
+
@keyframes yc-onboard-row-in{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}
|
|
193
|
+
@media (prefers-reduced-motion:reduce){
|
|
194
|
+
.yc-onboard-backdrop,.yc-onboard,.yc-onboard-hero-icon,.yc-onboard-hero-glow,.yc-onboard-row{animation:none!important;opacity:1!important;transform:none!important}
|
|
195
|
+
}
|
|
196
|
+
`;
|
package/src/runtime/ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SettingTypes } from './settings.js';
|
|
2
|
+
import { builtinIcon } from '../shared/icons.js';
|
|
2
3
|
import { safeURL, toArray } from './util.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -57,7 +58,10 @@ const SVG_ALLOWED = new Set([
|
|
|
57
58
|
*/
|
|
58
59
|
export function svg(markup) {
|
|
59
60
|
if (typeof markup !== 'string' || !markup.includes('<svg')) return null;
|
|
60
|
-
|
|
61
|
+
// Parsed as XML, an <svg> without xmlns is a plain element that draws nothing; inline-HTML markup often leaves it out.
|
|
62
|
+
let source = /<svg\b[^>]*\sxmlns=/.test(markup) ? markup : markup.replace(/<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
63
|
+
if (source.includes('xlink:') && !/\sxmlns:xlink=/.test(source)) source = source.replace(/<svg\b/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
|
|
64
|
+
const parsed = new DOMParser().parseFromString(source, 'image/svg+xml');
|
|
61
65
|
const root = parsed.documentElement;
|
|
62
66
|
if (!root || root.nodeName.toLowerCase() !== 'svg' || parsed.querySelector('parsererror')) return null;
|
|
63
67
|
const clean = (node) => {
|
|
@@ -77,6 +81,8 @@ export function svg(markup) {
|
|
|
77
81
|
return globalThis.document.importNode(root, true);
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
const TOAST_ICONS = { info: 'info', success: 'check', warning: 'alertTriangle', error: 'alertCircle' };
|
|
85
|
+
|
|
80
86
|
export const baseCSS = `
|
|
81
87
|
.yc-page{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
82
88
|
.yc-page h2{margin:0 0 12px}
|
|
@@ -89,7 +95,33 @@ export const baseCSS = `
|
|
|
89
95
|
.yc-kv{display:grid;grid-template-columns:minmax(120px,max-content) 1fr;gap:8px 16px;align-items:baseline;padding:10px 0;border-bottom:1px solid var(--border)}
|
|
90
96
|
.yc-kv dt{color:var(--text-muted);font-size:13px}
|
|
91
97
|
.yc-kv dd{margin:0;word-break:break-word;font-size:14px}
|
|
92
|
-
.yc-
|
|
98
|
+
.yc-about{display:flex;flex-direction:column;gap:12px}
|
|
99
|
+
.yc-about-header{display:flex;gap:14px;align-items:center;background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius,10px);padding:14px 16px}
|
|
100
|
+
.yc-about-logo{width:48px;height:48px;border-radius:12px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--accent) 18%,transparent);color:var(--accent);font-size:22px;font-weight:700}
|
|
101
|
+
.yc-about-logo .yc-icon{width:26px;height:26px}
|
|
102
|
+
.yc-about-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:17px;font-weight:700}
|
|
103
|
+
.yc-about-version{display:inline-flex;align-items:center;gap:2px;font-size:11.5px;font-weight:600;padding:1px 4px 1px 8px;border-radius:999px;background:var(--bg-active);color:var(--text-muted)}
|
|
104
|
+
.yc-about-desc{color:var(--text-muted);font-size:13px;margin-top:2px}
|
|
105
|
+
.yc-about-meta{display:flex;flex-wrap:wrap;gap:2px 14px;font-size:12.5px;margin-top:6px}
|
|
106
|
+
.yc-about-label{color:var(--text-muted)}
|
|
107
|
+
.yc-about-actions{display:flex;flex-wrap:wrap;gap:6px}
|
|
108
|
+
.yc-about-actions .yc-icon{width:15px;height:15px;margin-right:4px}
|
|
109
|
+
.yc-about-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:10px}
|
|
110
|
+
.yc-about-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius,10px);padding:10px 14px;min-width:0}
|
|
111
|
+
.yc-about-rows{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:4px 12px;margin:0;font-size:13px;align-items:center}
|
|
112
|
+
.yc-about-rows dt{color:var(--text-muted)}
|
|
113
|
+
.yc-about-rows dd{margin:0;min-width:0}
|
|
114
|
+
.yc-about-value{display:inline-flex;align-items:center;gap:2px;max-width:100%;min-width:0;vertical-align:middle}
|
|
115
|
+
.yc-about-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
116
|
+
.yc-about-copy{display:inline-flex;flex-shrink:0;border:0;background:none;color:var(--text-muted);cursor:pointer;padding:2px;border-radius:5px}
|
|
117
|
+
.yc-about-copy:hover{background:var(--bg-hover);color:var(--text)}
|
|
118
|
+
.yc-about-copy .yc-icon{width:14px;height:14px}
|
|
119
|
+
.yc-about-chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
120
|
+
.yc-about-chip{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:3px 10px;border-radius:999px;background:var(--bg-active);font-size:12.5px;color:var(--text)}
|
|
121
|
+
.yc-page a.yc-about-chip{color:var(--text)}
|
|
122
|
+
.yc-page a.yc-about-chip:hover{text-decoration:none;background:var(--bg-hover)}
|
|
123
|
+
.yc-about-chip .yc-icon{width:13px;height:13px;color:var(--text-muted)}
|
|
124
|
+
.yc-kv code,.yc-about code,.yc-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12.5px;background:var(--bg-input);border-radius:6px;padding:1px 6px}
|
|
93
125
|
.yc-card{background:var(--bg-elev);border:1px solid var(--border);border-radius:var(--radius,10px);padding:12px 14px;margin:8px 0}
|
|
94
126
|
.yc-card-head{display:flex;align-items:center;gap:10px}
|
|
95
127
|
.yc-card-title{font-weight:600}
|
|
@@ -111,10 +143,18 @@ export const baseCSS = `
|
|
|
111
143
|
.yc-toasts{position:fixed;z-index:2147483000;bottom:20px;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;gap:8px;align-items:center;pointer-events:none}
|
|
112
144
|
.yc-toast{pointer-events:auto;background:var(--bg-float,#1b1c21);color:var(--text,#ececf1);border:1px solid var(--border);box-shadow:var(--shadow);border-radius:var(--radius,10px);padding:10px 14px;font-size:14px;max-width:min(520px,90vw);display:flex;gap:10px;align-items:center;animation:yc-in .15s ease-out}
|
|
113
145
|
.yc-toast[data-type=success]{border-left:3px solid var(--success,#3fcf82)}
|
|
146
|
+
.yc-toast[data-type=success] .yc-icon{color:var(--success,#3fcf82)}
|
|
114
147
|
.yc-toast[data-type=error]{border-left:3px solid var(--danger,#f2555a)}
|
|
148
|
+
.yc-toast[data-type=error] .yc-icon{color:var(--danger,#f2555a)}
|
|
115
149
|
.yc-toast[data-type=warning]{border-left:3px solid var(--warning,#f5b544)}
|
|
150
|
+
.yc-toast[data-type=warning] .yc-icon{color:var(--warning,#f5b544)}
|
|
151
|
+
.yc-toast[data-type=info] .yc-icon{color:var(--accent)}
|
|
116
152
|
.yc-backdrop{position:fixed;inset:0;z-index:2147482000;background:#0009;display:flex;align-items:center;justify-content:center;padding:16px;animation:yc-in .12s ease-out}
|
|
117
153
|
.yc-dialog{background:var(--bg-float,#1b1c21);color:var(--text,#ececf1);border:1px solid var(--border);border-radius:calc(var(--radius,10px) + 4px);box-shadow:var(--shadow);width:min(460px,100%);max-height:90vh;overflow:auto;padding:20px}
|
|
154
|
+
.yc-dialog-head{display:flex;align-items:center;gap:10px}
|
|
155
|
+
.yc-dialog-head .yc-icon{width:22px;height:22px;color:var(--accent);flex-shrink:0}
|
|
156
|
+
.yc-dialog-head.yc-danger .yc-icon{color:var(--danger,#f2555a)}
|
|
157
|
+
.yc-dialog-head h3{margin:0 0 8px;font-size:18px}
|
|
118
158
|
.yc-dialog h3{margin:0 0 8px;font-size:18px}
|
|
119
159
|
.yc-dialog p{margin:0 0 12px;color:var(--text-muted);white-space:pre-wrap}
|
|
120
160
|
.yc-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}
|
|
@@ -173,10 +213,11 @@ export class UI {
|
|
|
173
213
|
return h('div', { class: 'yc-empty' }, text);
|
|
174
214
|
}
|
|
175
215
|
|
|
176
|
-
/** An icon
|
|
216
|
+
/** An icon: a built-in name ('rocket', 'puzzle', …), SVG markup (sanitized), an https/data image URL, or an element. */
|
|
177
217
|
icon(source) {
|
|
178
218
|
if (!source) return null;
|
|
179
219
|
if (source instanceof Node) return h('span', { class: 'yc-icon' }, source);
|
|
220
|
+
if (typeof source === 'string' && !source.includes('<') && !/^(https?:|data:)/.test(source)) source = builtinIcon(source) ?? source;
|
|
180
221
|
const asSvg = svg(source);
|
|
181
222
|
if (asSvg) return h('span', { class: 'yc-icon', attrs: { 'aria-hidden': 'true' } }, asSvg);
|
|
182
223
|
const url = safeURL(source, { protocols: ['https:', 'data:'] });
|
|
@@ -392,22 +433,22 @@ export class UI {
|
|
|
392
433
|
}
|
|
393
434
|
|
|
394
435
|
/** Shows a short message at the bottom of the window. type: info | success | warning | error. */
|
|
395
|
-
toast(message, { type = 'info', timeout = 4000, action } = {}) {
|
|
436
|
+
toast(message, { type = 'info', timeout = 4000, action, icon = TOAST_ICONS[type] } = {}) {
|
|
396
437
|
const doc = globalThis.document;
|
|
397
438
|
if (!doc?.body) return () => {};
|
|
398
439
|
if (!this.#toastRoot?.isConnected) {
|
|
399
440
|
this.#toastRoot = h('div', { class: 'yc-toasts', attrs: { role: 'status', 'aria-live': 'polite' } });
|
|
400
441
|
doc.body.appendChild(this.#toastRoot);
|
|
401
442
|
}
|
|
402
|
-
const element = h('div', { class: 'yc-toast', dataset: { type } }, h('span', { class: 'yc-grow' }, message), action && this.button({ label: action.label, small: true, variant: 'ghost', onClick: () => (close(), action.onClick()) }));
|
|
443
|
+
const element = h('div', { class: 'yc-toast', dataset: { type } }, this.icon(icon), h('span', { class: 'yc-grow' }, message), action && this.button({ label: action.label, small: true, variant: 'ghost', onClick: () => (close(), action.onClick()) }));
|
|
403
444
|
const close = () => element.remove();
|
|
404
445
|
this.#toastRoot.appendChild(element);
|
|
405
446
|
if (timeout > 0) setTimeout(close, timeout);
|
|
406
447
|
return close;
|
|
407
448
|
}
|
|
408
449
|
|
|
409
|
-
/** A modal dialog. Resolves with the value of the button pressed, or null when dismissed. */
|
|
410
|
-
dialog({ title, message, content, buttons = [{ label: 'OK', value: true }], dismissable = true } = {}) {
|
|
450
|
+
/** A modal dialog. Resolves with the value of the button pressed, or null when dismissed. `icon` shows next to the title. */
|
|
451
|
+
dialog({ title, icon, message, content, buttons = [{ label: 'OK', value: true }], dismissable = true } = {}) {
|
|
411
452
|
const doc = globalThis.document;
|
|
412
453
|
if (!doc?.body) return Promise.resolve(null);
|
|
413
454
|
return new Promise((resolve) => {
|
|
@@ -426,7 +467,8 @@ export class UI {
|
|
|
426
467
|
}
|
|
427
468
|
};
|
|
428
469
|
const actions = buttons.map((b) => this.button({ label: b.label, variant: b.variant ?? 'primary', onClick: async () => finish(typeof b.value === 'function' ? await b.value() : b.value) }));
|
|
429
|
-
const
|
|
470
|
+
const head = title && (icon ? h('div', { class: ['yc-dialog-head', buttons.some((b) => b.variant === 'danger') && 'yc-danger'] }, this.icon(icon), h('h3', null, title)) : h('h3', null, title));
|
|
471
|
+
const dialog = h('div', { class: 'yc-dialog', attrs: { role: 'dialog', 'aria-modal': 'true' } }, head, message && h('p', null, message), content, h('div', { class: 'yc-dialog-actions' }, actions));
|
|
430
472
|
const backdrop = h('div', { class: 'yc-backdrop', onMousedown: (event) => event.target === backdrop && dismissable && finish(null) }, dialog);
|
|
431
473
|
doc.addEventListener('keydown', onKey, true);
|
|
432
474
|
doc.body.appendChild(backdrop);
|
|
@@ -434,9 +476,10 @@ export class UI {
|
|
|
434
476
|
});
|
|
435
477
|
}
|
|
436
478
|
|
|
437
|
-
async confirm({ title = 'Are you sure?', message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', danger = false, content } = {}) {
|
|
479
|
+
async confirm({ title = 'Are you sure?', message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', danger = false, icon = danger ? 'alertTriangle' : null, content } = {}) {
|
|
438
480
|
const value = await this.dialog({
|
|
439
481
|
title,
|
|
482
|
+
icon,
|
|
440
483
|
message,
|
|
441
484
|
content,
|
|
442
485
|
buttons: [
|