@signalk/app-dock 1.4.0 → 1.4.1
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 +23 -12
- package/app-icon.svg +21 -0
- package/dist/plugin/index.js +141 -0
- package/dist/plugin/schema.js +58 -0
- package/package.json +32 -14
- package/public/config.html +4 -437
- package/public/config.js +22 -0
- package/public/configpanel/PluginConfigurationPanel.js +1 -0
- package/public/configpanel/_virtual_mf-localSharedImportMap___mfe_internal___signalk_app_dock__mf_owner__1-_R-vaMcG.js +1 -0
- package/public/configpanel/dist-D1jGdgWH.js +4 -0
- package/public/configpanel/hostInit-D-z9PzRy.js +1 -0
- package/public/configpanel/pendingShares-BqCOmgFj.js +1 -0
- package/public/configpanel/vite-preload-helper-DHhhL8Zf.js +1 -0
- package/public/dock.css +1 -610
- package/public/dock.js +1 -1129
- package/public/index.html +6 -6
- package/public/remoteEntry.js +1 -1
- package/plugin/index.js +0 -273
- package/public/540.js +0 -2
- package/public/540.js.LICENSE.txt +0 -9
- package/public/805.js +0 -1
- package/public/main.js +0 -1
package/README.md
CHANGED
|
@@ -121,6 +121,10 @@ Visitors who are not admins reach the plugin's routes through `router.access()`,
|
|
|
121
121
|
## Development
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
|
+
cd /path/to/app-dock
|
|
125
|
+
npm install
|
|
126
|
+
npm run build # the server loads dist/ and serves public/; rebuild after a change
|
|
127
|
+
|
|
124
128
|
cd ~/.signalk # or a throwaway SK config dir for dev, e.g. ~/.signalk-app-dock
|
|
125
129
|
npm link /path/to/app-dock
|
|
126
130
|
```
|
|
@@ -135,30 +139,37 @@ PORT=4000 npm start -- -c ~/.signalk-app-dock
|
|
|
135
139
|
|
|
136
140
|
The repo ships three cooperating surfaces:
|
|
137
141
|
|
|
138
|
-
- **`plugin
|
|
139
|
-
- **`
|
|
140
|
-
- **`src/configpanel/`** -- React 19 component exposed to the admin UI via
|
|
142
|
+
- **`src/plugin/`** -- the SK-server plugin, an ES module compiled by `tsc` to `dist/plugin/`. Defines the configuration schema with TypeBox, seeds default apps on first run, exposes HTTP endpoints for the dock and config panel, and owns the `environment.mode` PUT handler.
|
|
143
|
+
- **`src/dock/`** -- the dock UI (`index.html`, `dock.ts`, `dock.css`) and the standalone configurator (`config.html`, `config.ts`), built by Vite into `public/`, which the server serves at `/@signalk/app-dock/`. `src/dock/static/` holds the icons and the web manifest, copied as they are.
|
|
144
|
+
- **`src/configpanel/`** -- React 19 component exposed to the admin UI via Module Federation (`@module-federation/vite`), built into `public/remoteEntry.js` and `public/configpanel/`. It uses the admin UI's own React, which signalk-server 2.27 and later expose.
|
|
145
|
+
|
|
146
|
+
Everything is strict TypeScript. Vite only transpiles, so both Vite builds run the type checker (`vite-plugin-checker`) and fail on a type error. `public/` and `dist/` are build output and not committed; `npm run build` creates them.
|
|
141
147
|
|
|
142
148
|
### Scripts
|
|
143
149
|
|
|
144
|
-
| Command
|
|
145
|
-
|
|
|
146
|
-
| `npm
|
|
147
|
-
| `npm run
|
|
148
|
-
| `npm run
|
|
149
|
-
| `npm run build:
|
|
150
|
+
| Command | Description |
|
|
151
|
+
| -------------------------- | ----------------------------------------------------------------- |
|
|
152
|
+
| `npm run build` | Build the plugin, the dock pages and the config panel |
|
|
153
|
+
| `npm run typecheck:config` | Type-check the Vite and ESLint configs |
|
|
154
|
+
| `npm run build:server` | Compile the plugin and its tests to `dist/` |
|
|
155
|
+
| `npm run build:dock` | Build the dock pages into `public/` (empties it first) |
|
|
156
|
+
| `npm run build:config` | Build the admin UI config panel into `public/` |
|
|
157
|
+
| `npm test` | Run the plugin tests (`node --test`) against `dist/`; build first |
|
|
158
|
+
| `npm run format` | Prettier + ESLint fix |
|
|
159
|
+
| `npm run format:check` | Prettier check + ESLint, as CI runs them |
|
|
160
|
+
| `npm run lint` | ESLint check |
|
|
150
161
|
|
|
151
162
|
### Icon assets
|
|
152
163
|
|
|
153
|
-
`
|
|
164
|
+
`app-icon.svg` has a transparent background: the artwork sits straight on whatever page shows it, so its colours have to read on light and dark backgrounds alike. Check a change on both before committing.
|
|
154
165
|
|
|
155
166
|
The 180-px apple-touch-icon needs an opaque background, because iOS fills transparency with black, and room for the corners iOS rounds off. Regenerate it from the SVG whenever that changes:
|
|
156
167
|
|
|
157
168
|
```bash
|
|
158
|
-
rsvg-convert -w 132 -h 132
|
|
169
|
+
rsvg-convert -w 132 -h 132 app-icon.svg | magick - -background '#0f172a' -gravity center -extent 180x180 -alpha remove -alpha off PNG24:src/dock/static/app-icon-180.png
|
|
159
170
|
```
|
|
160
171
|
|
|
161
|
-
`
|
|
172
|
+
`src/dock/static/app-icon-maskable.svg` is hand-authored: the same artwork on the same dark slate, scaled into Android's 80% safe zone. Update it by hand when the artwork changes.
|
|
162
173
|
|
|
163
174
|
## License
|
|
164
175
|
|
package/app-icon.svg
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" width="72" height="72">
|
|
2
|
+
<!-- Transparent background: the artwork is drawn straight on the canvas in
|
|
3
|
+
mid-tone colours that read on light and dark pages alike -->
|
|
4
|
+
<!-- Compass rose -->
|
|
5
|
+
<circle cx="36" cy="16" r="11" fill="none" stroke="#3b82f6" stroke-opacity="0.7" stroke-width="1.5"/>
|
|
6
|
+
<line x1="36" y1="3" x2="36" y2="7" stroke="#3b82f6" stroke-width="1.5" stroke-linecap="round"/>
|
|
7
|
+
<line x1="23" y1="16" x2="27" y2="16" stroke="#3b82f6" stroke-opacity="0.7" stroke-width="1.5" stroke-linecap="round"/>
|
|
8
|
+
<line x1="45" y1="16" x2="49" y2="16" stroke="#3b82f6" stroke-opacity="0.7" stroke-width="1.5" stroke-linecap="round"/>
|
|
9
|
+
<polygon points="36,8 33,16 36,14 39,16" fill="#ef4444"/>
|
|
10
|
+
<polygon points="36,24 33,16 36,18 39,16" fill="#64748b"/>
|
|
11
|
+
<!-- Dock pill -->
|
|
12
|
+
<rect x="3" y="44" width="66" height="22" rx="11" fill="#3b82f6" fill-opacity="0.15" stroke="#3b82f6" stroke-width="1.5"/>
|
|
13
|
+
<!-- Dock icons - 3 squares in the pill -->
|
|
14
|
+
<rect x="9" y="49" width="12" height="12" rx="3" fill="#3b82f6"/>
|
|
15
|
+
<rect x="30" y="49" width="12" height="12" rx="3" fill="#10b981"/>
|
|
16
|
+
<rect x="51" y="49" width="12" height="12" rx="3" fill="#f59e0b"/>
|
|
17
|
+
<!-- Middle icon magnified (popping up) -->
|
|
18
|
+
<rect x="27" y="29" width="18" height="18" rx="4.5" fill="#10b981"/>
|
|
19
|
+
<!-- Active dot under middle icon -->
|
|
20
|
+
<circle cx="36" cy="69.5" r="2" fill="#3b82f6"/>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ConfigSchema } from './schema.js';
|
|
2
|
+
const DEFAULT_APPS = [
|
|
3
|
+
{
|
|
4
|
+
enabled: true,
|
|
5
|
+
autostart: true,
|
|
6
|
+
url: '/@signalk/freeboard-sk/',
|
|
7
|
+
label: 'Freeboard-SK',
|
|
8
|
+
icon: '/@signalk/freeboard-sk/assets/icons/icon-72x72.png',
|
|
9
|
+
color: ''
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
enabled: true,
|
|
13
|
+
autostart: false,
|
|
14
|
+
url: '/@halos-org/skip/',
|
|
15
|
+
label: 'Skip',
|
|
16
|
+
icon: '/@halos-org/skip/assets/icon-72x72.png',
|
|
17
|
+
color: ''
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
enabled: true,
|
|
21
|
+
autostart: false,
|
|
22
|
+
url: '/admin/',
|
|
23
|
+
label: 'Settings',
|
|
24
|
+
icon: '/@signalk/app-dock/icon-settings.svg',
|
|
25
|
+
color: '#78788c'
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
export default function appDock(app) {
|
|
29
|
+
let pluginSettings = {};
|
|
30
|
+
let resolvedApps = [];
|
|
31
|
+
let running = false;
|
|
32
|
+
function resolveApps(settings) {
|
|
33
|
+
return (Array.isArray(settings.apps) ? settings.apps : [])
|
|
34
|
+
.filter((a) => a.enabled !== false)
|
|
35
|
+
.map((a) => ({
|
|
36
|
+
label: a.label || a.url,
|
|
37
|
+
url: a.url,
|
|
38
|
+
icon: a.icon || null,
|
|
39
|
+
color: a.color || null,
|
|
40
|
+
autostart: a.autostart || false
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
// The configuration as saved, the one /config serves. While the plugin is
|
|
44
|
+
// not running, pluginSettings is empty (never started) or holds what the
|
|
45
|
+
// last start() got, which a later save may have replaced.
|
|
46
|
+
function savedSettings() {
|
|
47
|
+
return app.readPluginOptions().configuration || {};
|
|
48
|
+
}
|
|
49
|
+
function getWebapps() {
|
|
50
|
+
return (app.webapps || [])
|
|
51
|
+
.filter((w) => w.name !== '@signalk/app-dock' && w.name !== '@signalk/server-admin-ui')
|
|
52
|
+
.map((w) => ({
|
|
53
|
+
name: w.name,
|
|
54
|
+
label: w.signalk?.displayName || w.name,
|
|
55
|
+
url: `/${w.name}/`,
|
|
56
|
+
icon: w.signalk?.appIcon ? `/${w.name}/${w.signalk.appIcon.replace(/^\.\//, '')}` : null
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
const plugin = {
|
|
60
|
+
id: 'signalk-app-dock',
|
|
61
|
+
name: 'App Dock',
|
|
62
|
+
start(config) {
|
|
63
|
+
let settings = config;
|
|
64
|
+
if (!Array.isArray(settings.apps)) {
|
|
65
|
+
const seeded = DEFAULT_APPS.map((a) => ({ ...a }));
|
|
66
|
+
const seededSettings = {
|
|
67
|
+
showNightModeButton: true,
|
|
68
|
+
showFullscreenButton: false,
|
|
69
|
+
showExitButton: false,
|
|
70
|
+
...settings,
|
|
71
|
+
apps: seeded
|
|
72
|
+
};
|
|
73
|
+
app.debug('App Dock: first run, seeding %d default apps', seeded.length);
|
|
74
|
+
app.savePluginOptions(seededSettings, (err) => {
|
|
75
|
+
if (err)
|
|
76
|
+
app.error('App Dock: failed to save default apps: ' + err.message);
|
|
77
|
+
else
|
|
78
|
+
app.debug('App Dock: saved default apps to config');
|
|
79
|
+
});
|
|
80
|
+
settings = seededSettings;
|
|
81
|
+
}
|
|
82
|
+
pluginSettings = settings;
|
|
83
|
+
if (settings.showNightModeButton) {
|
|
84
|
+
app.registerPutHandler('vessels.self', 'environment.mode', (_context, _path, value) => {
|
|
85
|
+
if (value !== 'day' && value !== 'night') {
|
|
86
|
+
return { state: 'COMPLETED', statusCode: 400, message: 'Invalid mode' };
|
|
87
|
+
}
|
|
88
|
+
app.handleMessage(plugin.id, {
|
|
89
|
+
updates: [{ values: [{ path: 'environment.mode', value }] }]
|
|
90
|
+
});
|
|
91
|
+
return { state: 'COMPLETED', statusCode: 200 };
|
|
92
|
+
}, plugin.id);
|
|
93
|
+
}
|
|
94
|
+
resolvedApps = resolveApps(settings);
|
|
95
|
+
running = true;
|
|
96
|
+
app.debug('App Dock: resolved %d apps: %s', resolvedApps.length, resolvedApps.map((a) => a.label).join(', '));
|
|
97
|
+
},
|
|
98
|
+
stop() {
|
|
99
|
+
running = false;
|
|
100
|
+
},
|
|
101
|
+
registerWithRouter(router) {
|
|
102
|
+
// Routes registered on the router itself are admin-only. signalk-server
|
|
103
|
+
// 2.31 and later add router.access(), which opens a route to readonly or
|
|
104
|
+
// readwrite users; on older servers every route stays admin-only.
|
|
105
|
+
// PluginRouter declares access() unconditionally, so older servers are
|
|
106
|
+
// told apart at runtime.
|
|
107
|
+
const access = router.access;
|
|
108
|
+
const openTo = (level) => typeof access === 'function' ? access.call(router, level) : router;
|
|
109
|
+
openTo('readonly').get('/settings', (_req, res) => {
|
|
110
|
+
const settings = running ? pluginSettings : savedSettings();
|
|
111
|
+
res.json({
|
|
112
|
+
...settings,
|
|
113
|
+
apps: running ? resolvedApps : resolveApps(settings)
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
router.get('/webapps', (_req, res) => {
|
|
117
|
+
res.json(getWebapps());
|
|
118
|
+
});
|
|
119
|
+
openTo('readonly').get('/mode', (_req, res) => {
|
|
120
|
+
const current = app.getSelfPath('environment.mode');
|
|
121
|
+
res.json({ value: current?.value || 'day' });
|
|
122
|
+
});
|
|
123
|
+
openTo('readwrite').post('/dismiss-tour', (_req, res) => {
|
|
124
|
+
// savePluginOptions replaces the whole saved configuration, so the
|
|
125
|
+
// update starts from a complete one.
|
|
126
|
+
const updated = { ...(running ? pluginSettings : savedSettings()), tourDismissed: true };
|
|
127
|
+
app.savePluginOptions(updated, (err) => {
|
|
128
|
+
if (err) {
|
|
129
|
+
app.error('App Dock: failed to save tourDismissed: ' + err.message);
|
|
130
|
+
res.status(500).json({ error: err.message });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
pluginSettings = updated;
|
|
134
|
+
res.json({ tourDismissed: true });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
schema: ConfigSchema
|
|
139
|
+
};
|
|
140
|
+
return plugin;
|
|
141
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
|
+
// One definition is both the admin UI's configuration form (a TypeBox schema
|
|
3
|
+
// is JSON Schema at runtime) and the TypeScript type of the saved
|
|
4
|
+
// configuration. Every field is optional: a configuration saved by an older
|
|
5
|
+
// release, or by hand, may lack any of them.
|
|
6
|
+
export const AppEntrySchema = Type.Object({
|
|
7
|
+
enabled: Type.Optional(Type.Boolean({ title: 'Enabled', default: true })),
|
|
8
|
+
autostart: Type.Optional(Type.Boolean({
|
|
9
|
+
title: 'Autostart',
|
|
10
|
+
description: 'Load this app automatically when the dock page opens',
|
|
11
|
+
default: false
|
|
12
|
+
})),
|
|
13
|
+
url: Type.String({ title: 'URL' }),
|
|
14
|
+
label: Type.Optional(Type.String({ title: 'Label override' })),
|
|
15
|
+
icon: Type.Optional(Type.String({ title: 'Icon override', description: 'Emoji or image path' })),
|
|
16
|
+
color: Type.Optional(Type.String({ title: 'Background color' }))
|
|
17
|
+
});
|
|
18
|
+
export const ConfigSchema = Type.Object({
|
|
19
|
+
position: Type.Optional(Type.Enum(['bottom', 'top', 'left', 'right'], { type: 'string', title: 'Dock position', default: 'bottom' })),
|
|
20
|
+
iframeMode: Type.Optional(Type.Enum(['keep-alive', 'destroy'], {
|
|
21
|
+
type: 'string',
|
|
22
|
+
title: 'iFrame lifecycle',
|
|
23
|
+
description: 'keep-alive: load once and hide/show (faster switching, more RAM). ' +
|
|
24
|
+
'destroy: reload on every switch (slower, minimal RAM).',
|
|
25
|
+
default: 'keep-alive'
|
|
26
|
+
})),
|
|
27
|
+
iconSize: Type.Optional(Type.Number({ title: 'Icon size (px)', default: 56 })),
|
|
28
|
+
magnification: Type.Optional(Type.Boolean({ title: 'Enable dock magnification effect', default: true })),
|
|
29
|
+
magnificationScale: Type.Optional(Type.Number({ title: 'Magnification scale (1.0 = none, 2.5 = maximum)', default: 1.7 })),
|
|
30
|
+
showNightModeButton: Type.Optional(Type.Boolean({
|
|
31
|
+
title: 'Show night/day mode toggle',
|
|
32
|
+
description: 'Adds a sun/moon button to the dock that toggles environment.mode',
|
|
33
|
+
default: true
|
|
34
|
+
})),
|
|
35
|
+
showFullscreenButton: Type.Optional(Type.Boolean({
|
|
36
|
+
title: 'Show fullscreen toggle',
|
|
37
|
+
description: 'Adds a button to the dock that toggles browser fullscreen. ' +
|
|
38
|
+
'Hidden automatically when the Fullscreen API is not available (e.g. iPadOS Safari — use Add to Home Screen instead).',
|
|
39
|
+
default: false
|
|
40
|
+
})),
|
|
41
|
+
showExitButton: Type.Optional(Type.Boolean({
|
|
42
|
+
title: 'Show exit button',
|
|
43
|
+
description: 'Adds an X button to the dock that returns to Signal K admin UI',
|
|
44
|
+
default: false
|
|
45
|
+
})),
|
|
46
|
+
tourDismissed: Type.Optional(Type.Boolean({
|
|
47
|
+
title: 'Welcome tour dismissed',
|
|
48
|
+
description: 'Set automatically when a user clicks "Don\'t show again" in the welcome tour. Uncheck to show the tour again on all devices.',
|
|
49
|
+
default: false
|
|
50
|
+
})),
|
|
51
|
+
apps: Type.Optional(Type.Array(AppEntrySchema, {
|
|
52
|
+
title: 'Dock Apps',
|
|
53
|
+
description: 'Installed webapps are added here automatically. Reorder, disable, or override labels/icons as needed.',
|
|
54
|
+
default: []
|
|
55
|
+
}))
|
|
56
|
+
}, {
|
|
57
|
+
description: 'Open /@signalk/app-dock/config.html for the visual configurator with discover button and drag-to-reorder.'
|
|
58
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalk/app-dock",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "macOS-style configurable app dock for switching between SignalK webapps on touch screens",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/SignalK/app-dock"
|
|
@@ -21,33 +21,51 @@
|
|
|
21
21
|
"./docs/screenshots/3_app-dock_config.png"
|
|
22
22
|
]
|
|
23
23
|
},
|
|
24
|
-
"
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/plugin/index.js",
|
|
25
26
|
"files": [
|
|
26
|
-
"
|
|
27
|
+
"app-icon.svg",
|
|
28
|
+
"dist/plugin/",
|
|
27
29
|
"public/",
|
|
28
30
|
"docs/screenshots/"
|
|
29
31
|
],
|
|
30
32
|
"scripts": {
|
|
31
|
-
"build:
|
|
32
|
-
"
|
|
33
|
-
"
|
|
33
|
+
"build": "npm run build:server && npm run build:dock && npm run build:config",
|
|
34
|
+
"build:server": "tsc",
|
|
35
|
+
"build:dock": "vite build --config vite.dock.config.ts",
|
|
36
|
+
"build:config": "vite build --config vite.configpanel.config.ts",
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
38
|
+
"format": "prettier --write . && eslint --fix .",
|
|
34
39
|
"lint": "eslint .",
|
|
35
|
-
"
|
|
40
|
+
"format:check": "prettier --check . && eslint .",
|
|
41
|
+
"test": "node --test \"dist/test/**/*.test.js\""
|
|
36
42
|
},
|
|
37
43
|
"author": "Dirk Wahrheit <dirkwahrheit@gmail.com>",
|
|
38
44
|
"license": "Apache-2.0",
|
|
39
45
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
46
|
+
"node": ">=22"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"typebox": "^1.3.37"
|
|
41
50
|
},
|
|
42
51
|
"devDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
52
|
+
"@eslint/js": "^10.0.1",
|
|
53
|
+
"@module-federation/vite": "^1.23.5",
|
|
54
|
+
"@signalk/server-api": "^2.33.0",
|
|
55
|
+
"@types/express": "^5.0.6",
|
|
56
|
+
"@types/node": "^22.19.0",
|
|
57
|
+
"@types/react": "^19.3.0",
|
|
58
|
+
"@types/react-dom": "^19.3.0",
|
|
59
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
46
60
|
"eslint": "^10.1.0",
|
|
47
61
|
"eslint-config-prettier": "^10.1.8",
|
|
62
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
63
|
+
"globals": "^17.13.0",
|
|
64
|
+
"jiti": "^2.7.0",
|
|
48
65
|
"prettier": "^3.8.1",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
66
|
+
"typescript": "~6.0.3",
|
|
67
|
+
"typescript-eslint": "^8.71.2",
|
|
68
|
+
"vite": "^8.3.4",
|
|
69
|
+
"vite-plugin-checker": "^0.14.5"
|
|
52
70
|
}
|
|
53
71
|
}
|