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 +46 -0
- package/README.md +1 -1
- package/docs/README.md +1 -0
- package/docs/about-page.md +19 -2
- package/docs/configuration.md +47 -2
- package/docs/getting-started.md +1 -1
- package/docs/installer.md +39 -12
- package/docs/ui.md +50 -3
- package/docs/welcome.md +63 -0
- package/package.json +1 -1
- package/src/build/build.js +63 -24
- package/src/build/nsis.js +763 -0
- package/src/config/defaults.js +37 -3
- package/src/config/load.js +10 -2
- 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 +22 -2
- package/src/runtime/onboarding.js +196 -0
- package/src/runtime/ui.js +85 -10
- package/src/shared/constants.js +1 -1
- package/src/shared/icons.js +80 -0
- package/templates/init/icons/.gitkeep +0 -0
- package/templates/init/yapin-client.config.js +6 -0
- package/types/index.d.ts +47 -1
- package/types/runtime.d.ts +30 -2
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
|
|
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 |
|
package/docs/about-page.md
CHANGED
|
@@ -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
|
-
|
|
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
|
```
|
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 | |
|
|
@@ -113,13 +124,47 @@ settings: {
|
|
|
113
124
|
| --- | --- | --- |
|
|
114
125
|
| `title` | `About` | |
|
|
115
126
|
| `order` | `0` | |
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
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 | |
|
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
29
|
-
2.
|
|
30
|
-
3.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/docs/welcome.md
ADDED
|
@@ -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
package/src/build/build.js
CHANGED
|
@@ -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.
|
|
125
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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\
|
|
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) {
|