yapin.js-client 0.1.0 → 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 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/README.md CHANGED
@@ -10,7 +10,7 @@ It's a framework, not a finished client. It provides the plumbing, so your clien
10
10
  - **An About page.** Version, build, git commit and environment, out of the box. Every row can be edited, hidden or removed, and so can the whole page.
11
11
  - **Badges.** Next to names on profile cards and messages, from a JSON file, your API, or code. Every badge is sanitised.
12
12
  - **API access.** Named services with retries, caching and secrets stored in the operating system's key store. On desktop, requests go through the main process, checked against an allowlist that's fixed at build time.
13
- - **A branded installer.** A single `setup.exe` on Windows (your icon, name and version, signable), a zip for macOS and Linux. Configured from a JSON file with three presets. It runs on the Node built into the desktop app, so the people installing need nothing else. It handles install, update, repair after desktop updates, and uninstall.
13
+ - **A branded installer.** A setup wizard on Windows, built with NSIS: one signable `setup.exe` with your logo, icon, name and version, a zip for macOS and Linux. Configured from a JSON file with three presets. It runs on the Node built into the desktop app, so the people installing need nothing else. It handles install, update, repair after desktop updates, and uninstall.
14
14
  - **Zero dependencies.** It includes its own bundler, asar reader and writer, zip writer and test suite.
15
15
 
16
16
  > Client apps modify Yapin’. Check Yapin’s terms before you publish one, and make clear that yours is unofficial (the installer says so by default).
package/docs/README.md CHANGED
@@ -19,6 +19,7 @@
19
19
  | [Themes](themes.md) | Theme files, variables, base styles, custom CSS |
20
20
  | [Pages](pages.md) | Adding pages to Yapin’s settings menu |
21
21
  | [The About page](about-page.md) | Editing, extending and removing it |
22
+ | [Welcome and what's new](welcome.md) | The first-run and update screens: editing, disabling, replaying |
22
23
  | [Badges](badges.md) | Badge providers, the JSON format, where badges appear |
23
24
  | [API access](api.md) | Services, the allowlist, secrets |
24
25
  | [UI kit](ui.md) | Native-looking controls, toasts and dialogs |
@@ -10,7 +10,20 @@ Every client app gets an About page in settings, filled in by the build:
10
10
  | `environment` (Environment) | `environment.platform`, `environment.desktop`, `environment.electron`, `environment.chrome`, `environment.node`, `environment.os` | Where it's running. |
11
11
  | `links` (Links) | `links.<key>` | One per entry in your config's `links`. |
12
12
 
13
- Under the rows are buttons: **Copy debug info** (`copy`), and on desktop **Open data folder** (`folder`) and **Restart in safe mode** (`safeMode`).
13
+ ## How it's laid out
14
+
15
+ - **A header card** for the `app` section: a logo tile, the name with a version tag (click the copy icon to copy it), and the description. Any other `app.*` field shows small underneath.
16
+ - **A row of buttons**: **Copy debug info** (`copy`), **Welcome screen** (`welcome`) and **What's new** (`whatsNew`) when those are on, and on desktop **Data folder** (`folder`) and **Safe mode** (`safeMode`).
17
+ - **A grid of compact cards**, one per other section, with labels and values side by side. Long values are cut short with the full text on hover, and copyable values get a copy icon.
18
+ - **Links** as small chips that open in the browser.
19
+
20
+ Each section's look is its `layout`, so any of them can be changed:
21
+
22
+ | Layout | |
23
+ | --- | --- |
24
+ | `rows` | Label and value side by side. The default for new sections. |
25
+ | `chips` | Compact pills: links show their label, other fields their value. The `links` section uses it. |
26
+ | `header` | The card at the top. Its `name`, `version` and `description` fields become the title, the version tag and the line under it. The `app` section uses it. Only the first `header` section is drawn as the header; give it to another section to change what's at the top. |
14
27
 
15
28
  **Copy debug info** copies every row as text, plus enabled plugins and themes (with versions and sources) and the last 20 warnings and errors. It's what you want in bug reports.
16
29
 
@@ -19,10 +32,12 @@ Under the rows are buttons: **Copy debug info** (`copy`), and on desktop **Open
19
32
  ```js
20
33
  about: {
21
34
  title: 'About Nebula',
35
+ logo: 'sparkles', // the header tile: an icon name, SVG, or image URL (default: the first letter)
22
36
  intro: 'Thanks for using Nebula!',
23
37
  sections: {
24
38
  environment: false, // hide a whole section
25
39
  support: { title: 'Support', order: 35 }, // add one (built-in orders: 0, 10, 20, 30, 40)
40
+ links: { layout: 'rows' }, // show links as rows instead of chips
26
41
  },
27
42
  fields: {
28
43
  'git.remote': false, // hide a row
@@ -46,8 +61,10 @@ client.about.set('app.version', { value: `${client.version} (beta)` });
46
61
  client.about.set('stats.plugins', { label: 'Plugins running', value: () => String(client.plugins.list().filter((p) => p.status === 'running').length) });
47
62
  client.about.remove('git.dirty');
48
63
  client.about.section('stats', { title: 'Stats', order: 25 });
64
+ client.about.section('community', { title: 'Community', layout: 'chips' });
65
+ client.about.sections(); // [{ id, title, order, layout }], in order
49
66
  client.about.removeSection('environment');
50
- client.about.action('report', { label: 'Report a bug', run: (client) => client.openExternal('https://github.com/ada/nebula/issues/new') });
67
+ client.about.action('report', { label: 'Report a bug', icon: 'alertCircle', run: (client) => client.openExternal('https://github.com/ada/nebula/issues/new') });
51
68
  client.about.removeAction('folder');
52
69
  client.about.debugInfo(); // the text Copy debug info copies
53
70
  ```
@@ -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 | |
@@ -113,13 +124,47 @@ settings: {
113
124
  | --- | --- | --- |
114
125
  | `title` | `About` | |
115
126
  | `order` | `0` | |
116
- | `intro` | none | A line under the heading. |
117
- | `sections` | | `{ git: false }` hides a section; `{ support: { title: 'Support', order: 35 } }` adds one. |
127
+ | `logo` | the name's first letter | The tile in the header card: an icon name, SVG, or https/data image URL. |
128
+ | `intro` | none | A line under the header. |
129
+ | `sections` | | `{ git: false }` hides a section; `{ support: { title: 'Support', order: 35 } }` adds one; `{ links: { layout: 'rows' } }` changes how one looks (`rows`, `chips` or `header`). |
118
130
  | `fields` | | `{ 'git.remote': false }` hides a row; `{ 'support.email': { label: 'Email', value: 'help@example.com' } }` adds one. A string is a value. |
119
131
  | `actions` | | `{ safeMode: false }` removes a button (`copy`, `folder`, `safeMode`). |
120
132
 
121
133
  See [The About page](about-page.md) for every field id.
122
134
 
135
+ ## `welcome` and `whatsNew`
136
+
137
+ A rounded, animated screen shown the first time the client app runs (`welcome`), and once after an update when there's something to say about it (`whatsNew`). Both are off with `enabled: false` or `welcome: false` / `whatsNew: false`, and both can be replayed at any time with `client.welcome.show()` / `client.whatsNew.show()`.
138
+
139
+ | Option | Default | |
140
+ | --- | --- | --- |
141
+ | `enabled` | `true` | |
142
+ | `icon` | `sparkles` / `rocket` | A built-in name, SVG markup, or an https/data image URL. See [UI kit](ui.md#icons). |
143
+ | `title` | `Welcome to {name}` / `What's new in {name} {version}` | `{name}` and `{version}` are replaced. |
144
+ | `subtitle` | none | A line under the title. |
145
+ | `steps` (welcome) / `entries` (whatsNew) | `[]` | `{ icon, title, description }` rows. `whatsNew` only shows once it has at least one entry. |
146
+ | `actionLabel` | `Get started` / `Got it` | The primary button. |
147
+ | `dismissible` | `true` | `false` removes the Escape/backdrop/Skip ways out, leaving only the primary button. |
148
+ | `dismissLabel` | `Skip` / none | The secondary button, when dismissible. |
149
+
150
+ ```js
151
+ welcome: {
152
+ title: 'Welcome to {name}',
153
+ subtitle: "Here's what you get out of the box.",
154
+ steps: [
155
+ { icon: 'puzzle', title: 'Plugins', description: 'Turn features on and off from Settings.' },
156
+ { icon: 'palette', title: 'Themes', description: 'Apply a theme or write your own CSS.' },
157
+ ],
158
+ },
159
+ whatsNew: {
160
+ entries: [
161
+ { icon: 'sparkles', title: 'New in 1.2.0', description: 'A redesigned Settings page.' },
162
+ ],
163
+ },
164
+ ```
165
+
166
+ See [Welcome and what's new](welcome.md).
167
+
123
168
  ## `badges`
124
169
 
125
170
  | Option | Default | |
@@ -110,7 +110,7 @@ npm run build
110
110
 
111
111
  `dist/` now holds:
112
112
 
113
- - `nebula-1.0.0-setup.exe`: share this with Windows users. They double-click it and the installer opens in their browser.
113
+ - `nebula-1.0.0-setup.exe`: share this with Windows users. They double-click it and a setup wizard walks them through it ([NSIS](installer.md#building-it) must be installed where you build).
114
114
  - `nebula-1.0.0-installer.zip`: for macOS (`Install Nebula.command`) and Linux (`./install.sh`).
115
115
  - `nebula.user.js`: the web version, for userscript managers ([The web version](web.md)).
116
116
  - `payload/`: what gets installed (used by `yapin-client install`).
package/docs/installer.md CHANGED
@@ -23,13 +23,41 @@ Every launcher runs `installer.mjs` with the Node that's built into the Yapin’
23
23
 
24
24
  ## The Windows .exe
25
25
 
26
- `<id>-<version>-setup.exe` is a small launcher (about 13 kB, written in C#) with the whole installer folder compressed onto its end. When someone runs it, it:
26
+ `<id>-<version>-setup.exe` is a setup wizard, the kind people know from other Windows programs. It's built with [NSIS](https://nsis.sourceforge.io) (the Nullsoft Scriptable Install System) and has its pages:
27
27
 
28
- 1. unpacks the installer into a new private folder in their temp directory;
29
- 2. finds the desktop app (the places in [Targets](#targets), plus any you add) and runs the installer with its Node, with no console window;
30
- 3. waits, then deletes the folder. If the installer fails, the error is shown in a message box.
28
+ 1. **Welcome**, with your logo down the side, your description and the "unofficial modification" notice.
29
+ 2. **License**, when `marketing.terms` has text. With `requireAccept`, Next stays disabled until the box is ticked.
30
+ 3. **Choose an option**: the desktop install it found (a list, when there are several) and what's there, then Install, Update, Reinstall or Repair, and Uninstall (with "Also delete my installed plugins, themes and settings"). Next is labelled with what it will do. If Yapin’ is open it says so, and asks before closing it (see `installer.closeApp`).
31
+ 4. **Installing**, with a progress bar and **Show details** for each step. If something goes wrong, the error is shown and nothing is left half-done.
32
+ 5. **Finish**, with **Open Yapin’** (ticked unless `installer.launchAfter` is false).
31
33
 
32
- It runs as the current user (no administrator prompt), like the per-user desktop app it installs into.
34
+ The wizard does the work with the same installer as everywhere else: it unpacks the installer folder to a temporary folder, runs it with the desktop app's Node (the places in [Targets](#targets), plus any you add) with no console window, and removes the folder when it closes. It runs as the current user (no administrator prompt), like the per-user desktop app it installs into.
35
+
36
+ | Run as | Does |
37
+ | --- | --- |
38
+ | `nebula-setup.exe` | Opens the wizard. |
39
+ | `nebula-setup.exe /S` | Silent: does what Next would (install, update, or repair) with no window. Exits with 0 when it worked. |
40
+ | `nebula-setup.exe /S /UNINSTALL [/PURGE]` | Silent uninstall; `/PURGE` also deletes installed plugins, themes and settings. |
41
+
42
+ The texts are in `marketing.strings` (the keys starting with `setup`, plus `install`, `launch`, `purge`, `accept` and the messages shared with the page), so they can be changed or translated.
43
+
44
+ ### Pictures, name, version and icon
45
+
46
+ The side picture (164×314) is your `accent` colour with your logo on it, and the top-right picture (150×57) is your logo on white. The logo is `marketing.logo`, or the config's `icon`, as a PNG. To use your own pictures instead, set `installer.windows.sidebar` and `installer.windows.header` to `.bmp` files of those sizes.
47
+
48
+ The .exe shows your name, description, version and authors in its file properties, and your icon (`installer.windows.icon`, or the config's `icon`: a PNG of at most 256×256 pixels, or an `.ico`) in Explorer and the taskbar.
49
+
50
+ ### Building it
51
+
52
+ Building the wizard needs NSIS 3 on the machine that runs `yapin-client build`. It's found in the usual install folders, on the `PATH`, in the `MAKENSIS` environment variable, or at `installer.windows.nsis` (a path to `makensis`).
53
+
54
+ | | |
55
+ | --- | --- |
56
+ | Windows | The installer from [nsis.sourceforge.io](https://nsis.sourceforge.io/Download), or `winget install NSIS.NSIS` |
57
+ | macOS | `brew install makensis` |
58
+ | Linux | `sudo apt install nsis` (or your distribution's `nsis` package) |
59
+
60
+ Without NSIS, the build still makes a `setup.exe`, the simpler kind: a small launcher (written in C#) that opens the [installer page](#what-it-shows) in the browser. The build warns when that happens. Set `installer.windows.nsis` to `false` to always make that kind. It takes commands too:
33
61
 
34
62
  | Run as | Does |
35
63
  | --- | --- |
@@ -37,11 +65,7 @@ It runs as the current user (no administrator prompt), like the per-user desktop
37
65
  | `nebula-setup.exe install --yes` (or any command) | Runs that installer command with no window ([options](#in-the-terminal)). |
38
66
  | `nebula-setup.exe --extract [--to <dir>]` | Unpacks the installer folder (next to the .exe by default), for running `installer.mjs --cli` yourself. |
39
67
 
40
- ### Name, version and icon
41
-
42
- Built on Windows, the launcher is compiled for your client app with `csc.exe`, the C# compiler that comes with the .NET Framework on every Windows 10 and 11 machine. The .exe then shows your name, description, version and authors in its file properties, and your icon in Explorer and the taskbar. The icon is `installer.windows.icon`, or the config's `icon`: a PNG of at most 256×256 pixels, or an `.ico`.
43
-
44
- Built on macOS or Linux, the .exe uses a prebuilt generic launcher instead (same behaviour, generic icon and details), and the build says so.
68
+ Built on Windows, that launcher is compiled for your client app with `csc.exe`, the C# compiler that comes with the .NET Framework on every Windows 10 and 11 machine, so it has your name and icon. Built on macOS or Linux, it's a prebuilt generic launcher (same behaviour, generic icon and details).
45
69
 
46
70
  ### Signing
47
71
 
@@ -55,21 +79,24 @@ installer: {
55
79
  },
56
80
  ```
57
81
 
58
- `{file}` is replaced with the .exe's path. Any signing tool works. The launcher finds its bundle with or without a signature.
82
+ `{file}` is replaced with the .exe's path. Any signing tool works, for either kind of .exe.
59
83
 
60
84
  ### Options
61
85
 
62
86
  | `installer.windows` | Default | |
63
87
  | --- | --- | --- |
64
88
  | `exe` | `true` | Build the .exe (it's skipped when `installer.gui` is false). |
89
+ | `nsis` | `true` | Build the wizard when NSIS is found. `false` for the browser launcher, or the path to `makensis`. |
65
90
  | `fileName` | `<id>-<version>-setup.exe` | |
66
91
  | `icon` | the config's `icon` | PNG (at most 256 px) or `.ico`. |
92
+ | `sidebar` | generated | A 164×314 `.bmp` for the Welcome and Finish pages. |
93
+ | `header` | generated | A 150×57 `.bmp` for the top of the other pages. |
67
94
  | `company` | your authors | Shown in the file's properties. |
68
95
  | `sign` | none | A signing command containing `{file}`. |
69
96
 
70
97
  ## What it shows
71
98
 
72
- By default the installer opens a page in the browser, served from the person's own computer, built from your marketing config. It lists the desktop installs it found, with each one's state:
99
+ On macOS and Linux (and from the Windows browser launcher), the installer opens a page in the browser, served from the person's own computer, built from your marketing config. It lists the desktop installs it found, with each one's state:
73
100
 
74
101
  | State | Shown as | Buttons |
75
102
  | --- | --- | --- |
package/docs/ui.md CHANGED
@@ -39,6 +39,53 @@ ui.keyValue([{ label: 'Version', value: '1.0.0' }, { label: 'Link', value: ui.li
39
39
  ui.icon(svgMarkupOrImageURL)
40
40
  ```
41
41
 
42
+ ## Icons
43
+
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
+
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).
48
+ - SVG markup, sanitized (no scripts, event handlers, `foreignObject` or external links).
49
+ - An https or `data:image/…` URL.
50
+ - An element.
51
+
52
+ ```js
53
+ ui.icon('rocket')
54
+ ui.button({ label: 'Install', icon: 'puzzle' })
55
+ client.pages.add({ id: 'tools', title: 'Tools', icon: 'sliders', render })
56
+ ```
57
+
58
+ Every icon is a single colour that follows `currentColor`, so it matches whatever text colour it sits in, in any theme.
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
+
42
89
  ## Controls
43
90
 
44
91
  ```js
@@ -72,10 +119,10 @@ One control per setting, saved as the person edits, with errors shown under the
72
119
  ## Toasts and dialogs
73
120
 
74
121
  ```js
75
- ui.toast('Saved', { type: 'success' }); // info | success | warning | error
76
- ui.toast('Update available', { timeout: 0, action: { label: 'Restart', onClick: restart } });
122
+ ui.toast('Saved', { type: 'success' }); // info | success | warning | error — each type gets a matching icon
123
+ ui.toast('Update available', { timeout: 0, action: { label: 'Restart', onClick: restart }, icon: 'rocket' });
77
124
 
78
- if (await ui.confirm({ title: 'Delete everything?', message: 'This can’t be undone.', confirmLabel: 'Delete', danger: true })) { … }
125
+ if (await ui.confirm({ title: 'Delete everything?', message: 'This can’t be undone.', confirmLabel: 'Delete', danger: true })) { … } // danger confirms get a warning icon automatically
79
126
 
80
127
  const name = await ui.prompt({ title: 'Rename', value: current, validate: (v) => v.trim() !== '' || 'Can’t be empty' });
81
128
 
@@ -0,0 +1,63 @@
1
+ # Welcome and what's new
2
+
3
+ Two rounded, animated screens the framework can show on top of the app:
4
+
5
+ - **Welcome**, the first time the client app runs with nothing saved yet — a rounded-corner hero icon, a title and subtitle, and a short list of steps with their own icons.
6
+ - **What's new**, once after an update, when `whatsNew.entries` has something for the new version.
7
+
8
+ Both fade and scale in, with the hero icon settling into place and each row sliding in in sequence; everything respects `prefers-reduced-motion`. Both are entirely optional and entirely yours to edit.
9
+
10
+ ## Turning them off
11
+
12
+ ```js
13
+ welcome: false, // no welcome screen, ever
14
+ whatsNew: false, // no what's new screen, ever
15
+ ```
16
+
17
+ Or just for one: `welcome: { enabled: false }`. `whatsNew` also stays quiet on its own whenever `entries` is empty — ship a release with nothing to say and nothing shows.
18
+
19
+ ## Editing them
20
+
21
+ ```js
22
+ welcome: {
23
+ icon: 'sparkles', // a built-in icon, SVG, or an image URL — see UI kit → Icons
24
+ title: 'Welcome to {name}', // {name} and {version} are replaced
25
+ subtitle: "Here's what you get out of the box.",
26
+ steps: [
27
+ { icon: 'puzzle', title: 'Plugins', description: 'Turn features on and off from Settings.' },
28
+ { icon: 'palette', title: 'Themes', description: 'Apply a theme, or write your own CSS.' },
29
+ ],
30
+ actionLabel: 'Get started',
31
+ dismissible: true, // false removes Escape, the backdrop click, and the Skip button
32
+ dismissLabel: 'Skip',
33
+ },
34
+ whatsNew: {
35
+ icon: 'rocket',
36
+ title: "What's new in {name} {version}",
37
+ entries: [
38
+ { icon: 'sparkles', title: 'A redesigned Settings page', description: 'Find things faster.' },
39
+ { icon: 'puzzle', title: 'Three new plugins', description: 'Enable them from the Plugins page.' },
40
+ ],
41
+ actionLabel: 'Got it',
42
+ },
43
+ ```
44
+
45
+ A client app that only wants the hero and a message can leave `steps` / `entries` out (welcome still shows; what's new needs at least one entry to show at all).
46
+
47
+ ## Replaying them from code
48
+
49
+ ```js
50
+ client.welcome.show(); // right now, regardless of whether it's been seen
51
+ client.whatsNew.show();
52
+
53
+ client.welcome.reset(); // clears the "seen" flag, so it shows again next start
54
+ client.welcome.markSeen(); // marks it seen without showing it
55
+ ```
56
+
57
+ A **Show welcome screen** / **Show what's new** button is added to the [About page](about-page.md) automatically whenever the screen is enabled (remove with `about: { actions: { welcome: false, whatsNew: false } }`).
58
+
59
+ `client.welcome` and `client.whatsNew` are `null` when turned off with `welcome: false` / `whatsNew: false`.
60
+
61
+ ## How "seen" is tracked
62
+
63
+ Each screen stores one value — the version it was last seen at — in the client app's own storage, under `onboarding`. Welcome only needs to know whether it's ever run; what's new compares the stored version against `client.version`, so it shows again after every update that has entries (and only once per version).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yapin.js-client",
3
- "version": "0.1.0",
3
+ "version": "0.1.5",
4
4
  "description": "A zero-dependency framework for building Yapin’ client mods: plugins, themes, settings pages, badges, API access and a branded installer.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,6 +7,7 @@ import { pack } from './pack.js';
7
7
  import { readGitInfo } from './git.js';
8
8
  import { zipDirectory } from './zip.js';
9
9
  import { compileLauncher, createSetupExe, findCsc, PREBUILT_LAUNCHER, signFile } from './windows.js';
10
+ import { buildNsisExe, findMakensis } from './nsis.js';
10
11
  import { defaultTarget } from '../installer/paths.js';
11
12
  import { deepMerge } from '../runtime/util.js';
12
13
  import { apiVersion, version as frameworkVersion } from '../shared/constants.js';
@@ -53,18 +54,46 @@ export function findThemes(dirs) {
53
54
  return out;
54
55
  }
55
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
+
56
80
  /** The part of the config the running client app gets (plain data, no build paths). */
57
- export function runtimeConfig(config) {
81
+ export function runtimeConfig(config, { icons } = {}) {
58
82
  const { dirs: _pd, ...plugins } = config.plugins;
59
83
  const { dirs: _td, css: _css, ...themes } = config.themes;
84
+ const { dirs: _id, set, ...iconOptions } = config.icons ?? {};
60
85
  return {
61
86
  origins: config.origins,
62
87
  settings: config.settings,
63
88
  runtime: config.runtime,
64
89
  plugins,
65
90
  themes,
91
+ // Folder icons first, so an inline `icons.set` entry wins on a name clash.
92
+ icons: { ...iconOptions, set: { ...icons, ...set } },
66
93
  pages: config.pages,
67
94
  about: config.about,
95
+ welcome: config.welcome,
96
+ whatsNew: config.whatsNew,
68
97
  badges: config.badges,
69
98
  api: config.api,
70
99
  host: config.host,
@@ -121,32 +150,38 @@ exit 1
121
150
  }
122
151
 
123
152
  /**
124
- * The Windows installer: one .exe with the installer folder inside. On Windows the launcher is compiled
125
- * for this client app (its name, version and icon); elsewhere the prebuilt generic one is used.
153
+ * The Windows installer: one .exe with the installer folder inside. With NSIS available it's a setup
154
+ * wizard (see nsis.js). Otherwise it's the small launcher that opens the installer page in the browser,
155
+ * compiled for this client app on Windows (its name, version and icon), or the prebuilt generic one.
126
156
  */
127
- function buildSetupExe(config, { installerDir, outDir }) {
157
+ function buildSetupExe(config, { settings, installerDir, outDir }) {
128
158
  const windows = config.installer.windows ?? {};
129
159
  const file = join(outDir, windows.fileName ?? `${config.id}-${config.version}-setup.exe`);
160
+ const icon = windows.icon ?? config.icon ?? null;
161
+ const exeIcon = icon && /\.(png|ico)$/i.test(icon) ? icon : null;
162
+ const company = windows.company ?? config.authors?.map((a) => a.name).join(', ') ?? '';
163
+ const target = deepMerge(structuredClone(defaultTarget), config.installer.target ?? {});
164
+ const exeName = target.executable?.win32 ?? 'Yapper.exe';
165
+ const exe = (target.locations?.win32 ?? []).map((location) => `${location.replace(/[\\/]+$/, '')}\\${exeName}`);
166
+
167
+ const makensis = windows.nsis === false ? null : findMakensis({ configured: windows.nsis });
168
+ if (makensis) {
169
+ buildNsisExe({ makensis, installer: settings, source: installerDir, out: file, icon: exeIcon, sidebar: windows.sidebar, header: windows.header, company, description: config.description, exeLocations: exe });
170
+ if (windows.sign) signFile(windows.sign, file);
171
+ return { file, kind: 'nsis', warning: null };
172
+ }
173
+
174
+ const warnings = [];
175
+ if (typeof windows.nsis === 'string') warnings.push(`installer.windows.nsis: ${windows.nsis} didn't run, so the Windows .exe opens the installer in the browser instead of the setup wizard.`);
176
+ else if (windows.nsis !== false) warnings.push("NSIS isn't installed, so the Windows .exe opens the installer in the browser instead of the setup wizard. Install NSIS (https://nsis.sourceforge.io, apt install nsis, or brew install makensis) for the wizard.");
130
177
  let launcher = PREBUILT_LAUNCHER;
131
- let warning = null;
132
178
  const work = mkdtempSync(join(tmpdir(), 'yc-setup-'));
133
179
  try {
134
180
  if (findCsc()) {
135
- const icon = windows.icon ?? config.icon ?? null;
136
- launcher = compileLauncher({
137
- out: join(work, 'launcher.exe'),
138
- name: config.name,
139
- version: config.version,
140
- description: config.description,
141
- company: windows.company ?? config.authors?.map((a) => a.name).join(', ') ?? '',
142
- icon: icon && /\.(png|ico)$/i.test(icon) ? icon : null,
143
- });
181
+ launcher = compileLauncher({ out: join(work, 'launcher.exe'), name: config.name, version: config.version, description: config.description, company, icon: exeIcon });
144
182
  } else {
145
- warning = 'Built the Windows .exe with the generic launcher (no csc.exe here), so it has no custom icon or file details. Build on Windows for those.';
183
+ warnings.push('Built the Windows .exe with the generic launcher (no csc.exe here), so it has no custom icon or file details. Build on Windows for those.');
146
184
  }
147
- const target = deepMerge(structuredClone(defaultTarget), config.installer.target ?? {});
148
- const exeName = target.executable?.win32 ?? 'Yapper.exe';
149
- const exe = (target.locations?.win32 ?? []).map((location) => `${location.replace(/[\\/]+$/, '')}\\${exeName}`);
150
185
  createSetupExe({
151
186
  launcher,
152
187
  installerDir,
@@ -158,7 +193,7 @@ function buildSetupExe(config, { installerDir, outDir }) {
158
193
  rmSync(work, { recursive: true, force: true });
159
194
  }
160
195
  if (windows.sign) signFile(windows.sign, file);
161
- return { file, warning };
196
+ return { file, kind: 'launcher', warning: warnings.join(' ') || null };
162
197
  }
163
198
 
164
199
  function userscript(config, manifest, runtime) {
@@ -214,6 +249,7 @@ export async function build(config, { root, mode = 'production', onStep = () =>
214
249
  onStep('Collecting plugins and themes');
215
250
  const pluginFiles = findPlugins(config.plugins.dirs);
216
251
  const themeFiles = findThemes(config.themes.dirs);
252
+ const iconFiles = findIcons(config.icons?.dirs ?? []);
217
253
  const baseCSS = config.themes.css ? readFileSync(config.themes.css, 'utf8') : '';
218
254
 
219
255
  const imports = [`import { boot } from ${JSON.stringify(toPosix(join(SRC, 'runtime', 'boot.js')))};`];
@@ -251,7 +287,7 @@ export default (args) => boot(args, { entry, plugins, themes });
251
287
  build: { time: new Date().toISOString(), mode },
252
288
  git,
253
289
  baseCSS,
254
- config: runtimeConfig(config),
290
+ config: runtimeConfig(config, { icons: iconFiles }),
255
291
  files: {},
256
292
  };
257
293
  if (mode === 'development') {
@@ -286,13 +322,15 @@ export default (args) => boot(args, { entry, plugins, themes });
286
322
  let installerDir = null;
287
323
  let zipFile = null;
288
324
  let exeFile = null;
325
+ let exeKind = null;
289
326
  if (installer) {
290
327
  onStep('Building the installer');
291
328
  installerDir = join(outDir, 'installer', config.id);
292
329
  mkdirSync(installerDir, { recursive: true });
293
330
  const installerBundle = pack({ entry: join(SRC, 'installer', 'main.js'), format: 'module', root: SRC });
294
331
  writeFileSync(join(installerDir, 'installer.mjs'), installerBundle.code);
295
- writeFileSync(join(installerDir, 'installer.json'), JSON.stringify(installerConfig(config, manifest), null, 2));
332
+ const settings = installerConfig(config, manifest);
333
+ writeFileSync(join(installerDir, 'installer.json'), JSON.stringify(settings, null, 2));
296
334
  cpSync(payloadDir, join(installerDir, 'payload'), { recursive: true });
297
335
  const sh = launcherScript();
298
336
  const safeName = config.name.replace(/[^\w .-]+/g, '').trim() || config.id;
@@ -307,7 +345,7 @@ export default (args) => boot(args, { entry, plugins, themes });
307
345
  }
308
346
  writeFileSync(
309
347
  join(installerDir, 'README.txt'),
310
- `${config.name} ${config.version}\r\n\r\nWindows: run ${config.id}-${config.version}-setup.exe instead of this folder.\r\nmacOS: double-click "Install ${safeName}.command".\r\nLinux: run ./install.sh\r\nAnywhere with Node: node installer.mjs (add --cli for the terminal version).\r\n\r\nThe installer opens in your browser. Close the Yapin' desktop app first, or the installer will offer to.\r\nTo remove ${config.name}, run the installer again and choose Uninstall.\r\n`,
348
+ `${config.name} ${config.version}\r\n\r\nWindows: run ${config.id}-${config.version}-setup.exe instead of this folder.\r\nmacOS: double-click "Install ${safeName}.command".\r\nLinux: run ./install.sh\r\nAnywhere with Node: node installer.mjs (add --cli for the terminal version).\r\n\r\nOn macOS and Linux the installer opens in your browser. Close the Yapin' desktop app first, or the installer will offer to.\r\nTo remove ${config.name}, run the installer again and choose Uninstall.\r\n`,
311
349
  );
312
350
  if (config.installer.zip !== false) {
313
351
  zipFile = join(outDir, `${config.id}-${config.version}-installer.zip`);
@@ -316,14 +354,15 @@ export default (args) => boot(args, { entry, plugins, themes });
316
354
  const windows = config.installer.windows ?? {};
317
355
  if (windows.exe !== false && config.installer.gui !== false) {
318
356
  onStep('Building the Windows setup .exe');
319
- const built = buildSetupExe(config, { installerDir, outDir });
357
+ const built = buildSetupExe(config, { settings, installerDir, outDir });
320
358
  exeFile = built.file;
359
+ exeKind = built.kind;
321
360
  if (built.warning) warnings.push(built.warning);
322
361
  }
323
362
  }
324
363
 
325
364
  writeFileSync(join(outDir, 'build.json'), JSON.stringify({ id: config.id, version: config.version, mode, time: manifest.build.time, plugins: pluginFiles.map((f) => toPosix(relative(root, f))), themes: themeFiles.map((f) => toPosix(relative(root, f))), runtimeBytes: runtime.length, hash: bundled.hash }, null, 2));
326
- return { outDir, payloadDir, installerDir, zipFile, exeFile, userscriptFile, manifest, plugins: pluginFiles, themes: themeFiles, files: bundled.files, warnings, ms: Date.now() - started };
365
+ return { outDir, payloadDir, installerDir, zipFile, exeFile, exeKind, userscriptFile, manifest, plugins: pluginFiles, themes: themeFiles, files: bundled.files, warnings, ms: Date.now() - started };
327
366
  }
328
367
 
329
368
  function camel(id) {