create-bridgething 0.1.6 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bridgething",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new bridgething webapp (React + Vite + Tailwind v4)",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "typecheck": "tsc --noEmit"
33
33
  },
34
34
  "devDependencies": {
35
+ "@types/node": "^26.1.1",
35
36
  "@types/uuid": "^11.0.0",
36
37
  "tsdown": "^0.21.10"
37
38
  },
@@ -70,6 +70,12 @@ files for a specific method or type.
70
70
  - `config` - user-tunable settings (each becomes a `client.config` value).
71
71
  - `permissions` - capabilities you need (e.g. `net.fetch`, `net.proxy`, `geo`).
72
72
  - `art: { heroPx, thumbPx }` - the pixel sizes you want artwork delivered at.
73
+ - `settings` - one self-contained HTML file (built from `settings/`) the
74
+ companion phone app renders as this webapp's settings page. It talks to the
75
+ companion over `@bridgething/client/settings`, not the on-device SDK. Capped
76
+ at 1 MiB at install. It loads from a `file://` origin: websocket APIs work
77
+ from it, but plain HTTP fetch is CORS-gated (`Origin: null`) - see the
78
+ network caveat in the `/bridgething` skill's sdk reference.
73
79
 
74
80
  ## Workflow
75
81
 
@@ -61,3 +61,10 @@ to bring the connected Car Thing to the latest bridgething release. Multiple dev
61
61
  - `src/index.css` - `@import "tailwindcss";`.
62
62
  - `vite.config.ts` - vite + tailwind plugin, `es2022` target.
63
63
  - `index.html` - 800x480 viewport, no overscroll, no tap highlight.
64
+ - `settings/` - the companion-side settings page (preact), built by
65
+ `vite.settings.config.ts` into a single self-contained `dist/settings.html`.
66
+ It reads and writes settings over `@bridgething/client/settings`.
67
+
68
+ `bun run build` runs both configs: the main app into `dist/`, then the settings
69
+ page as `dist/settings.html`. `manifest.json`'s `settings` field points the
70
+ companion at it.
@@ -133,6 +133,21 @@ const r = await client.config.get({ key: 'units' });
133
133
  const all = await client.config.list();
134
134
  ```
135
135
 
136
+ The user edits these in the companion phone app. The scaffold ships a
137
+ companion-side settings page under `settings/` that reads and writes them via a
138
+ separate SDK, `@bridgething/client/settings` (a `postMessage` bridge to the
139
+ companion host, not this WebSocket). `manifest.json`'s `settings` field points
140
+ the companion at the built `dist/settings.html`.
141
+
142
+ NETWORK CAVEAT for the settings page: it runs on the phone with real internet,
143
+ but it loads from a `file://` origin and the webview enforces CORS on
144
+ fetch/XHR. WebSocket APIs work (the WS handshake is not CORS-gated); plain HTTP
145
+ APIs work only when the server sends permissive CORS headers (requests arrive
146
+ with `Origin: null`). Prefer the service's websocket API from the settings
147
+ page; for a CORS-strict HTTP-only service, fetch from the device webapp via
148
+ `client.net` (phone-tunneled, not origin-restricted) and keep the settings page
149
+ for typing and choosing.
150
+
136
151
  **Internet through the phone (`client.net`)** - the device has no direct network;
137
152
  `net.fetch` / websockets / a SOCKS proxy all tunnel through the phone. Needs the
138
153
  matching `permissions` entry in `manifest.json` (`net.fetch`, `net.ws`,
@@ -30,7 +30,9 @@ bun run share # writes <name>-<version>.zip from dist/
30
30
 
31
31
  Send that zip to anyone with a bridgething-flashed Car Thing. They install it
32
32
  from the bridgething companion app (no rebuild, no dev tools). The zip is just
33
- your built bundle with `manifest.json` at its root.
33
+ your built bundle with `manifest.json` at its root. `bun run build` also emits
34
+ `dist/settings.html` (the companion-side settings page); it ships in the same
35
+ bundle when `manifest.json` declares `settings`.
34
36
 
35
37
  Bump `version` in `public/manifest.json` before sharing an update, so people can
36
38
  tell builds apart. Never touch `manifest.json`'s `id` - it is the webapp's
@@ -5,19 +5,21 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
- "build": "vite build",
8
+ "build": "vite build && vite build -c vite.settings.config.ts",
9
9
  "preview": "vite preview",
10
10
  "push": "bun scripts/push.ts",
11
11
  "share": "bun scripts/share.ts",
12
12
  "update": "bunx @bridgething/updater"
13
13
  },
14
14
  "dependencies": {
15
- "@bridgething/client": "^0.1.6",
15
+ "@bridgething/client": "^0.2.1",
16
+ "preact": "^10.29.7",
16
17
  "react": "^19.2.0",
17
18
  "react-dom": "^19.2.0"
18
19
  },
19
20
  "devDependencies": {
20
21
  "@msgpack/msgpack": "^3.1.3",
22
+ "@preact/preset-vite": "^2.10.5",
21
23
  "@tailwindcss/vite": "^4.1.16",
22
24
  "@types/react": "^19.2.10",
23
25
  "@types/react-dom": "^19.2.3",
@@ -25,6 +27,7 @@
25
27
  "fflate": "^0.8.2",
26
28
  "tailwindcss": "^4.1.16",
27
29
  "typescript": "^6.0.3",
28
- "vite": "^7.1.13"
30
+ "vite": "^7.1.13",
31
+ "vite-plugin-singlefile": "^2.3.3"
29
32
  }
30
33
  }
@@ -3,6 +3,7 @@
3
3
  "name": "__PROJECT_NAME__",
4
4
  "version": "0.1.0",
5
5
  "description": "",
6
+ "settings": "settings.html",
6
7
  "config": [],
7
8
  "permissions": [],
8
9
  "art": { "heroPx": 248, "thumbPx": 96 }
@@ -0,0 +1,103 @@
1
+ import { settings, type ConfigField, type SettingsContext } from '@bridgething/client/settings';
2
+ import { render } from 'preact';
3
+ import { useEffect, useState } from 'preact/hooks';
4
+ import './style.css';
5
+
6
+ function fieldMeta(field: ConfigField): { key: string; label: string } {
7
+ return { key: field.data.key, label: field.data.label };
8
+ }
9
+
10
+ function Settings() {
11
+ const [ctx, setCtx] = useState<SettingsContext | null>(null);
12
+ const [fields, setFields] = useState<ConfigField[]>([]);
13
+ const [values, setValues] = useState<Record<string, string>>({});
14
+ const [status, setStatus] = useState('');
15
+
16
+ useEffect(() => {
17
+ (async () => {
18
+ try {
19
+ setCtx(await settings.context());
20
+ const [schema, entries] = await Promise.all([settings.config.fields(), settings.config.list()]);
21
+ setFields(schema);
22
+ setValues(Object.fromEntries(entries.map(e => [e.key, e.value])));
23
+ } catch (err) {
24
+ setStatus(err instanceof Error ? err.message : String(err));
25
+ }
26
+ })();
27
+ }, []);
28
+
29
+ async function saveConfig(event: Event) {
30
+ event.preventDefault();
31
+ setStatus('saving...');
32
+ try {
33
+ for (const field of fields) {
34
+ const { key } = fieldMeta(field);
35
+ await settings.config.set(key, values[key] ?? '');
36
+ }
37
+ setStatus('settings saved');
38
+ } catch (err) {
39
+ setStatus(err instanceof Error ? err.message : String(err));
40
+ }
41
+ }
42
+
43
+ async function saveToDevice() {
44
+ setStatus('writing to device...');
45
+ try {
46
+ await settings.doc.set('preferences', JSON.stringify({ values, savedAt: Date.now() }));
47
+ setStatus('synced a preferences doc to the device');
48
+ } catch (err) {
49
+ setStatus(err instanceof Error ? err.message : String(err));
50
+ }
51
+ }
52
+
53
+ return (
54
+ <main>
55
+ <h1>{ctx?.name ?? '__PROJECT_NAME__'} settings</h1>
56
+ <p class="hint">{ctx ? `${ctx.webappId} on ${ctx.deviceId}` : 'connecting to the companion host...'}</p>
57
+
58
+ <form onSubmit={saveConfig}>
59
+ {fields.length === 0 && <p class="hint">this webapp declares no config fields yet.</p>}
60
+ {fields.map(field => {
61
+ const { key, label } = fieldMeta(field);
62
+ const value = values[key] ?? '';
63
+ const onInput = (e: Event) => setValues({ ...values, [key]: (e.target as HTMLInputElement).value });
64
+ return (
65
+ <div class="field" key={key}>
66
+ <label for={key}>{label}</label>
67
+ {field.type === 'enum' ? (
68
+ <select id={key} value={value} onInput={onInput}>
69
+ {field.data.choices.map(choice => (
70
+ <option value={choice} key={choice}>
71
+ {choice}
72
+ </option>
73
+ ))}
74
+ </select>
75
+ ) : (
76
+ <input
77
+ id={key}
78
+ type={field.type === 'number' ? 'number' : field.type === 'secret' ? 'password' : 'text'}
79
+ value={value}
80
+ onInput={onInput}
81
+ />
82
+ )}
83
+ </div>
84
+ );
85
+ })}
86
+
87
+ <div class="row">
88
+ <button type="submit">Save settings</button>
89
+ <button type="button" class="secondary" onClick={saveToDevice}>
90
+ Save to device
91
+ </button>
92
+ <button type="button" class="secondary" onClick={() => settings.done()}>
93
+ Done
94
+ </button>
95
+ </div>
96
+ </form>
97
+
98
+ <p class="status">{status}</p>
99
+ </main>
100
+ );
101
+ }
102
+
103
+ render(<Settings />, document.getElementById('root')!);
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
+ <title>__PROJECT_NAME__ settings</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,85 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family:
4
+ system-ui,
5
+ -apple-system,
6
+ sans-serif;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ padding: 20px;
12
+ line-height: 1.5;
13
+ }
14
+
15
+ main {
16
+ max-width: 560px;
17
+ margin: 0 auto;
18
+ }
19
+
20
+ h1 {
21
+ font-size: 1.25rem;
22
+ margin: 0 0 0.25rem;
23
+ }
24
+
25
+ .hint {
26
+ opacity: 0.7;
27
+ font-size: 0.85rem;
28
+ margin: 0 0 1.25rem;
29
+ }
30
+
31
+ .field {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 4px;
35
+ margin-bottom: 14px;
36
+ }
37
+
38
+ .field label {
39
+ font-size: 0.85rem;
40
+ font-weight: 600;
41
+ }
42
+
43
+ .field input,
44
+ .field select {
45
+ padding: 8px 10px;
46
+ font-size: 1rem;
47
+ border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
48
+ border-radius: 8px;
49
+ background: transparent;
50
+ color: inherit;
51
+ }
52
+
53
+ .row {
54
+ display: flex;
55
+ gap: 10px;
56
+ align-items: center;
57
+ margin-top: 8px;
58
+ }
59
+
60
+ button {
61
+ padding: 9px 16px;
62
+ font-size: 0.95rem;
63
+ font-weight: 600;
64
+ border: none;
65
+ border-radius: 8px;
66
+ background: #00a8e8;
67
+ color: #06232e;
68
+ cursor: pointer;
69
+ }
70
+
71
+ button.secondary {
72
+ background: color-mix(in srgb, currentColor 12%, transparent);
73
+ color: inherit;
74
+ }
75
+
76
+ button:disabled {
77
+ opacity: 0.5;
78
+ cursor: default;
79
+ }
80
+
81
+ .status {
82
+ font-size: 0.85rem;
83
+ opacity: 0.8;
84
+ min-height: 1.2em;
85
+ }
@@ -0,0 +1,53 @@
1
+ import preact from '@preact/preset-vite';
2
+ import { statSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { defineConfig, type Plugin } from 'vite';
5
+ import { viteSingleFile } from 'vite-plugin-singlefile';
6
+
7
+ const WARN_BYTES = 200 * 1024;
8
+ const LOUD_BYTES = 500 * 1024;
9
+ const HARD_CAP = '1 MiB';
10
+
11
+ function sizeGuard(): Plugin {
12
+ return {
13
+ name: 'settings-size-guard',
14
+ closeBundle() {
15
+ const out = resolve(__dirname, 'dist', 'settings.html');
16
+ let bytes: number;
17
+ try {
18
+ bytes = statSync(out).size;
19
+ } catch {
20
+ return;
21
+ }
22
+ const kib = (bytes / 1024).toFixed(1);
23
+ if (bytes > LOUD_BYTES) {
24
+ console.warn(
25
+ [
26
+ '',
27
+ '========================================================================',
28
+ ` settings.html is ${kib} KiB.`,
29
+ ` the companion hard-rejects settings pages over ${HARD_CAP} at install.`,
30
+ ' trim dependencies or inline assets before this page stops installing.',
31
+ '========================================================================',
32
+ '',
33
+ ].join('\n'),
34
+ );
35
+ } else if (bytes > WARN_BYTES) {
36
+ console.warn(`settings.html is ${kib} KiB; keep it lean (install hard cap is ${HARD_CAP}).`);
37
+ }
38
+ },
39
+ };
40
+ }
41
+
42
+ export default defineConfig({
43
+ root: 'settings',
44
+ plugins: [preact(), viteSingleFile(), sizeGuard()],
45
+ build: {
46
+ target: 'es2022',
47
+ outDir: resolve(__dirname, 'dist'),
48
+ emptyOutDir: false,
49
+ rollupOptions: {
50
+ input: resolve(__dirname, 'settings', 'settings.html'),
51
+ },
52
+ },
53
+ });