create-bridgething 0.1.6 → 0.2.0
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 +2 -1
- package/template/CLAUDE.md +6 -0
- package/template/README.md +7 -0
- package/template/_claude/skills/bridgething/reference/sdk.md +15 -0
- package/template/_claude/skills/bridgething/reference/ship.md +3 -1
- package/template/package.json +6 -3
- package/template/public/manifest.json +1 -0
- package/template/settings/main.tsx +103 -0
- package/template/settings/settings.html +12 -0
- package/template/settings/style.css +85 -0
- package/template/vite.settings.config.ts +53 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-bridgething",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
},
|
package/template/CLAUDE.md
CHANGED
|
@@ -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
|
|
package/template/README.md
CHANGED
|
@@ -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
|
package/template/package.json
CHANGED
|
@@ -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.
|
|
15
|
+
"@bridgething/client": "^0.2.0",
|
|
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
|
}
|
|
@@ -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
|
+
});
|