@signalk/app-dock 1.3.3 → 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 CHANGED
@@ -31,7 +31,7 @@ Open: `http://your-sk-server:3000/@signalk/app-dock/`
31
31
 
32
32
  ## Usage
33
33
 
34
- **Double-tap anywhere** on the screen to open the dock. Tap an app icon to switch. The dock auto-dismisses after selection, or tap the backdrop to close it. Double-click works the same way for mouse users.
34
+ **Double-tap anywhere** on the screen to open the dock. Tap an app icon to switch. Tap the icon of the app already on screen to reload it, for example after the app was updated on the server. The dock auto-dismisses after selection, or tap the backdrop to close it. Double-click works the same way for mouse users.
35
35
 
36
36
  Tip: double-tapping a clickable element in the underlying webapp will activate that element once before the dock opens -- aim double-taps at non-interactive regions (maps, gauge backgrounds, empty space).
37
37
 
@@ -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/index.js`** -- CommonJS SK-server plugin. Registers the schema, seeds default apps on first run, exposes HTTP endpoints for the dock and config panel, and owns the `environment.mode` PUT handler.
139
- - **`public/dock.js` + `dock.css` + `index.html`** -- the dock UI itself. Plain JS, served as-is. **No build step**: edit and reload the browser.
140
- - **`src/configpanel/`** -- React 19 component exposed to the admin UI via Webpack Module Federation. Rebuild with `npm run build:config` after changes here; output lands in `public/remoteEntry.js` and companion chunks.
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 | Description |
145
- | ---------------------- | ----------------------------------------------------------------- |
146
- | `npm test` | Run plugin tests (`node --test`) |
147
- | `npm run format` | Prettier + ESLint fix |
148
- | `npm run lint` | ESLint check |
149
- | `npm run build:config` | Rebuild the admin UI config panel (required after `src/` changes) |
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
- `public/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.
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 public/app-icon.svg | magick - -background '#0f172a' -gravity center -extent 180x180 -alpha remove -alpha off PNG24:public/app-icon-180.png
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
- `public/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.
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.3.3",
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
- "main": "plugin/index.js",
24
+ "type": "module",
25
+ "main": "dist/plugin/index.js",
25
26
  "files": [
26
- "plugin/",
27
+ "app-icon.svg",
28
+ "dist/plugin/",
27
29
  "public/",
28
30
  "docs/screenshots/"
29
31
  ],
30
32
  "scripts": {
31
- "build:config": "webpack",
32
- "prepublishOnly": "webpack",
33
- "format": "prettier --write '**/*.{js,json,md,css,html}' && eslint --fix .",
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
- "test": "node --test test/plugin.test.js"
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": ">=18"
46
+ "node": ">=22"
47
+ },
48
+ "dependencies": {
49
+ "typebox": "^1.3.37"
41
50
  },
42
51
  "devDependencies": {
43
- "@babel/core": "^7.29.0",
44
- "@babel/preset-react": "^7.28.5",
45
- "babel-loader": "^10.1.1",
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
- "react": "^19.2.4",
50
- "webpack": "^5.105.4",
51
- "webpack-cli": "^7.0.2"
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
  }