@m4l-jweb/build 0.1.0 → 0.3.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/LICENSE +21 -0
- package/package.json +6 -4
- package/src/amxd.mjs +93 -93
- package/src/chains.mjs +338 -119
- package/src/index.mjs +246 -228
- package/src/init.mjs +38 -33
- package/templates/starter/.prettierrc +5 -0
- package/templates/starter/README.md +38 -12
- package/templates/starter/index.html +1 -1
- package/templates/starter/package.json +40 -34
- package/templates/starter/patcher/devices.mjs +33 -24
- package/templates/starter/scripts/build-ui.mjs +19 -0
- package/templates/starter/scripts/dev.mjs +24 -0
- package/templates/starter/scripts/devices.d.mts +24 -0
- package/templates/starter/scripts/devices.mjs +35 -0
- package/templates/starter/src/app/shared/Frame.tsx +61 -0
- package/templates/starter/src/app/shared/device.ts +79 -0
- package/templates/starter/src/app/{worker.ts → shared/worker.ts} +8 -8
- package/templates/starter/src/app/{{name}}/App.tsx +62 -0
- package/templates/starter/src/app/{{name}}/protocol.ts +32 -0
- package/templates/starter/src/app/{{name}}/surface.ts +26 -0
- package/templates/starter/src/index.css +134 -56
- package/templates/starter/src/main.tsx +35 -4
- package/templates/starter/src/vite-env.d.ts +2 -2
- package/templates/starter/tsconfig.app.json +26 -22
- package/templates/starter/tsconfig.node.json +1 -1
- package/templates/starter/vite.config.ts +61 -28
- package/templates/starter/vitest.config.ts +1 -1
- package/templates/starter/src/app/App.tsx +0 -87
- package/templates/starter/src/app/protocol.ts +0 -32
|
@@ -3,102 +3,180 @@
|
|
|
3
3
|
* An overgrown layout does not scroll, it silently clips at the bottom.
|
|
4
4
|
*/
|
|
5
5
|
:root {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
--bg: #2f2f2f;
|
|
7
|
+
--fg: #d6d6d6;
|
|
8
|
+
--muted: #8a8a8a;
|
|
9
|
+
--accent: #6fd6a8;
|
|
10
|
+
--warn: #e8a33d;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
* {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
margin: 0;
|
|
16
|
+
padding: 0;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
body {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
background: var(--bg);
|
|
21
|
+
color: var(--fg);
|
|
22
|
+
font:
|
|
23
|
+
11px/1.4 "Segoe UI",
|
|
24
|
+
system-ui,
|
|
25
|
+
sans-serif;
|
|
26
|
+
height: 169px;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* Dev only (@m4l-jweb/surface/dev). The harness renders beside the device, so
|
|
32
|
+
* the PAGE is no longer the device view - but the DEVICE still is. It keeps its
|
|
33
|
+
* real 169 px box, deliberately: a UI that clips in Live must clip here too, or
|
|
34
|
+
* the harness is lying to you about the one constraint that is cheapest to catch.
|
|
35
|
+
*/
|
|
36
|
+
body:has(.dev-layout) {
|
|
37
|
+
height: 100vh;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dev-layout {
|
|
41
|
+
align-items: flex-start;
|
|
42
|
+
background: #0e1013;
|
|
43
|
+
display: flex;
|
|
44
|
+
height: 100vh;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.dev-layout .device {
|
|
48
|
+
border: 1px solid #262a31;
|
|
49
|
+
flex: 1;
|
|
50
|
+
height: 169px;
|
|
51
|
+
max-width: 640px;
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
.device {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
gap: 6px;
|
|
58
|
+
height: 100%;
|
|
59
|
+
padding: 8px 10px;
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
header {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
align-items: center;
|
|
64
|
+
border-bottom: 1px solid #444;
|
|
65
|
+
display: flex;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
padding-bottom: 5px;
|
|
41
68
|
}
|
|
42
69
|
|
|
43
70
|
h1 {
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
letter-spacing: 0.08em;
|
|
46
73
|
}
|
|
47
74
|
|
|
48
75
|
.badge {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
border-radius: 3px;
|
|
77
|
+
font-size: 9px;
|
|
78
|
+
padding: 1px 5px;
|
|
79
|
+
text-transform: uppercase;
|
|
53
80
|
}
|
|
54
81
|
.badge.live {
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
background: var(--accent);
|
|
83
|
+
color: #1a1a1a;
|
|
57
84
|
}
|
|
58
85
|
.badge.dev {
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
background: #555;
|
|
87
|
+
color: var(--fg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* The build stamps, top right. Deliberately NOT in a footer: the device view is
|
|
91
|
+
a fixed ~169 px and clips at the bottom, so a footer stamp vanishes exactly
|
|
92
|
+
when the UI has grown enough for staleness to be worth checking. The stamps
|
|
93
|
+
are long (version + ISO timestamp), so they ellipsize - the full text is in
|
|
94
|
+
the title attribute, and the hover works in Max's jweb. */
|
|
95
|
+
.stamp {
|
|
96
|
+
color: var(--muted);
|
|
97
|
+
display: flex;
|
|
98
|
+
font-size: 9px;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
margin-left: auto;
|
|
101
|
+
max-width: 45%;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
text-overflow: ellipsis;
|
|
104
|
+
white-space: nowrap;
|
|
61
105
|
}
|
|
62
106
|
|
|
63
107
|
dl {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
column-gap: 10px;
|
|
109
|
+
display: grid;
|
|
110
|
+
grid-template-columns: auto 1fr;
|
|
111
|
+
row-gap: 3px;
|
|
68
112
|
}
|
|
69
113
|
|
|
70
114
|
dt {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
color: var(--muted);
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
font-size: 9px;
|
|
118
|
+
align-self: center;
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
dd {
|
|
78
|
-
|
|
122
|
+
font-variant-numeric: tabular-nums;
|
|
79
123
|
}
|
|
80
124
|
|
|
81
125
|
.dot {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
126
|
+
background: #666;
|
|
127
|
+
border-radius: 50%;
|
|
128
|
+
display: inline-block;
|
|
129
|
+
height: 6px;
|
|
130
|
+
width: 6px;
|
|
87
131
|
}
|
|
88
132
|
.dot.on {
|
|
89
|
-
|
|
133
|
+
background: var(--accent);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.slider {
|
|
137
|
+
align-items: center;
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
display: flex;
|
|
140
|
+
gap: 8px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.slider input {
|
|
144
|
+
accent-color: var(--accent);
|
|
145
|
+
cursor: pointer;
|
|
146
|
+
margin: 0;
|
|
147
|
+
width: 120px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.slider strong {
|
|
151
|
+
color: var(--accent);
|
|
152
|
+
min-width: 26px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* The transport belongs to Live, and nothing in the device starts it. */
|
|
156
|
+
.row {
|
|
157
|
+
align-items: baseline;
|
|
158
|
+
display: flex;
|
|
159
|
+
gap: 8px;
|
|
160
|
+
justify-content: space-between;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.hint {
|
|
164
|
+
color: var(--warn);
|
|
165
|
+
font-size: 9px;
|
|
166
|
+
font-style: normal;
|
|
167
|
+
white-space: nowrap;
|
|
90
168
|
}
|
|
91
169
|
|
|
92
170
|
footer {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
171
|
+
border-top: 1px solid #444;
|
|
172
|
+
color: var(--muted);
|
|
173
|
+
display: flex;
|
|
174
|
+
font-size: 9px;
|
|
175
|
+
gap: 10px;
|
|
176
|
+
margin-top: auto;
|
|
177
|
+
padding-top: 5px;
|
|
100
178
|
}
|
|
101
179
|
|
|
102
180
|
.warn {
|
|
103
|
-
|
|
181
|
+
color: var(--warn);
|
|
104
182
|
}
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
import { StrictMode } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
|
-
import App from "./app/App";
|
|
4
3
|
import "./index.css";
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* ONE DEVICE PER BUNDLE.
|
|
7
|
+
*
|
|
8
|
+
* `@device` is an alias resolved in vite.config.ts to `src/app/<device>/`, where
|
|
9
|
+
* <device> comes from the dev/build script (see scripts/). So this entry point is
|
|
10
|
+
* shared but contains no branching, and each bundle holds exactly one device: a
|
|
11
|
+
* MIDI device's bundle carries no audio code, and vice versa.
|
|
12
|
+
*
|
|
13
|
+
* That separation is not tidiness. Every .amxd embeds its own UI bundle, so a
|
|
14
|
+
* device ships what it is, not what its siblings are.
|
|
15
|
+
*/
|
|
16
|
+
import App from "@device/App";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The mocked-Live dev harness renders BESIDE the app in dev, and must never reach
|
|
20
|
+
* a device.
|
|
21
|
+
*
|
|
22
|
+
* `import.meta.env.DEV` is replaced by the literal `false` in a production build,
|
|
23
|
+
* so this branch - and with it the only reference to @m4l-jweb/surface/dev -
|
|
24
|
+
* becomes dead code that rollup drops before the bundle is inlined into the
|
|
25
|
+
* .amxd. tests/bundle.test.mjs asserts it actually did: a dev panel shipped
|
|
26
|
+
* inside someone's device throws no error, it just sits there, in Live.
|
|
27
|
+
*/
|
|
28
|
+
const DevHarness = import.meta.env.DEV ? (await import("@m4l-jweb/surface/dev")).DevHarness : null;
|
|
29
|
+
|
|
6
30
|
createRoot(document.getElementById("root")!).render(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
31
|
+
<StrictMode>
|
|
32
|
+
{DevHarness ? (
|
|
33
|
+
<div className="dev-layout">
|
|
34
|
+
<DevHarness />
|
|
35
|
+
<App />
|
|
36
|
+
</div>
|
|
37
|
+
) : (
|
|
38
|
+
<App />
|
|
39
|
+
)}
|
|
40
|
+
</StrictMode>,
|
|
10
41
|
);
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"paths": {
|
|
12
|
+
"@/*": ["./src/*"],
|
|
13
|
+
// vite rebinds this per build (see vite.config.ts). tsc cannot, so it
|
|
14
|
+
// typechecks src/main.tsx against ONE device. Every device's app lives
|
|
15
|
+
// under src/ and is typechecked on its own regardless.
|
|
16
|
+
"@device/*": ["./src/app/{{name}}/*"]
|
|
17
|
+
},
|
|
18
|
+
"isolatedModules": true,
|
|
19
|
+
"moduleDetection": "force",
|
|
20
|
+
"noEmit": true,
|
|
21
|
+
"jsx": "react-jsx",
|
|
22
|
+
"strict": true,
|
|
23
|
+
"noUnusedLocals": true,
|
|
24
|
+
"noUnusedParameters": true,
|
|
25
|
+
"noFallthroughCasesInSwitch": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
24
28
|
}
|
|
@@ -1,34 +1,67 @@
|
|
|
1
|
-
import { defineConfig } from "vite";
|
|
1
|
+
import { defineConfig, type UserConfig } from "vite";
|
|
2
2
|
import { fileURLToPath, URL } from "node:url";
|
|
3
3
|
import react from "@vitejs/plugin-react";
|
|
4
4
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
|
5
5
|
import pkg from "./package.json";
|
|
6
|
+
import { devices, uiDir } from "./scripts/devices.mjs";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
8
|
+
/**
|
|
9
|
+
* ONE BUILD PER DEVICE.
|
|
10
|
+
*
|
|
11
|
+
* A repo can hold several devices, each .amxd embeds its OWN UI bundle, and a
|
|
12
|
+
* device should ship what it is - not its siblings' code. So the app to bundle is
|
|
13
|
+
* chosen here, by DEVICE, and src/main.tsx imports it through the `@device`
|
|
14
|
+
* alias. There is no `if (mode === ...)` anywhere in the app.
|
|
15
|
+
*
|
|
16
|
+
* DEVICE is set by scripts/dev.mjs and scripts/build-ui.mjs (which read the
|
|
17
|
+
* device list from patcher/devices.mjs). It is an env var rather than vite's
|
|
18
|
+
* `--mode` deliberately: `--mode` also flips `import.meta.env.DEV`, and a build
|
|
19
|
+
* with DEV=true would ship the dev harness inside the device.
|
|
20
|
+
*/
|
|
21
|
+
//
|
|
22
|
+
// NOTE this is a FACTORY, not a plain object. scripts/build-ui.mjs sets DEVICE
|
|
23
|
+
// and calls vite's build() once per device in the same process; a top-level
|
|
24
|
+
// `const DEVICE = process.env.DEVICE` would be evaluated once, when the module
|
|
25
|
+
// was first loaded, and every device after the first would be built from the
|
|
26
|
+
// first one's sources. Vite invokes the factory on each config load, so reading
|
|
27
|
+
// the env var HERE is what makes the loop work.
|
|
28
|
+
export default defineConfig(() => {
|
|
29
|
+
// Falls back to the first device in the manifest, so a bare `vite` still runs
|
|
30
|
+
// something and this config carries no device name of its own.
|
|
31
|
+
const DEVICE = process.env.DEVICE ?? uiDir(devices[0]);
|
|
32
|
+
|
|
33
|
+
// The device UI is bundled into ONE self-contained index.html (every script,
|
|
34
|
+
// style and asset inlined) so it can be embedded in the .amxd as a base64
|
|
35
|
+
// payload and extracted to a real file:// path that jweb (Chromium) reads.
|
|
36
|
+
const config: UserConfig = {
|
|
37
|
+
base: "./",
|
|
38
|
+
plugins: [react(), viteSingleFile()],
|
|
39
|
+
resolve: {
|
|
40
|
+
alias: [
|
|
41
|
+
{ find: "@device", replacement: fileURLToPath(new URL(`./src/app/${DEVICE}`, import.meta.url)) },
|
|
42
|
+
{ find: "@", replacement: fileURLToPath(new URL("./src", import.meta.url)) },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
define: {
|
|
46
|
+
__APP_VERSION__: JSON.stringify(pkg.version),
|
|
47
|
+
__DEVICE__: JSON.stringify(DEVICE),
|
|
48
|
+
},
|
|
49
|
+
// A compute worker (src/app/shared/worker.ts) is inlined into the single chunk:
|
|
50
|
+
// a ?worker&inline blob URL cannot resolve relative chunk imports at runtime,
|
|
51
|
+
// so bundle dynamic imports in too.
|
|
52
|
+
worker: {
|
|
53
|
+
format: "es",
|
|
54
|
+
rollupOptions: {
|
|
55
|
+
output: {
|
|
56
|
+
inlineDynamicImports: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
build: {
|
|
61
|
+
// dist/ui/<device>/index.html - one per device, picked up by `m4l-jweb build`.
|
|
62
|
+
outDir: `dist/ui/${DEVICE}`,
|
|
63
|
+
emptyOutDir: true,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
return config;
|
|
34
67
|
});
|
|
@@ -6,7 +6,7 @@ export default defineConfig({
|
|
|
6
6
|
alias: [{ find: "@", replacement: fileURLToPath(new URL("./src", import.meta.url)) }],
|
|
7
7
|
},
|
|
8
8
|
test: {
|
|
9
|
-
include: ["src/**/*.test.{ts,tsx}"],
|
|
9
|
+
include: ["tests/**/*.test.{ts,mjs}", "packages/*/tests/**/*.test.{ts,mjs}", "src/**/*.test.{ts,tsx}"],
|
|
10
10
|
environment: "node",
|
|
11
11
|
},
|
|
12
12
|
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* App.tsx - the jweb UI. A hello-world React page wired to the Max bridge.
|
|
3
|
-
*
|
|
4
|
-
* It demonstrates the whole device loop end to end:
|
|
5
|
-
* - announce `ui_ready` on mount and show the state the wrapper replies with
|
|
6
|
-
* (mode, build stamp, tempo, transport ticks);
|
|
7
|
-
* - forward each transport tick into an optional Web Worker and render the
|
|
8
|
-
* count the worker sends back;
|
|
9
|
-
* - flag a stale install when the wrapper's build stamp does not match the
|
|
10
|
-
* one baked into this page.
|
|
11
|
-
*
|
|
12
|
-
* Replace the body with your device UI. The bridge surface never changes.
|
|
13
|
-
*/
|
|
14
|
-
import { useEffect, useRef, useState } from "react";
|
|
15
|
-
import { bindInlet, inJweb, uiReady } from "@m4l-jweb/bridge";
|
|
16
|
-
import { IN } from "./protocol";
|
|
17
|
-
import DemoWorker from "./worker.ts?worker&inline";
|
|
18
|
-
|
|
19
|
-
declare const __APP_VERSION__: string;
|
|
20
|
-
|
|
21
|
-
export default function App() {
|
|
22
|
-
const [mode, setMode] = useState("dev");
|
|
23
|
-
const [wrapperBuild, setWrapperBuild] = useState<string | null>(null);
|
|
24
|
-
const [tempo, setTempo] = useState<number | null>(null);
|
|
25
|
-
const [playing, setPlaying] = useState(false);
|
|
26
|
-
const [beats, setBeats] = useState(0);
|
|
27
|
-
const [workerTicks, setWorkerTicks] = useState(0);
|
|
28
|
-
const workerRef = useRef<Worker | null>(null);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
const worker = new DemoWorker();
|
|
32
|
-
worker.onmessage = (e: MessageEvent) => {
|
|
33
|
-
const [type, value] = e.data as [string, number];
|
|
34
|
-
if (type === "ticks") setWorkerTicks(value);
|
|
35
|
-
};
|
|
36
|
-
workerRef.current = worker;
|
|
37
|
-
|
|
38
|
-
bindInlet(IN.mode, (m) => setMode(String(m)));
|
|
39
|
-
bindInlet(IN.build, (b) => setWrapperBuild(String(b)));
|
|
40
|
-
bindInlet(IN.tempo, (bpm) => setTempo(Number(bpm)));
|
|
41
|
-
bindInlet(IN.tick, (isPlaying, position) => {
|
|
42
|
-
setPlaying(Number(isPlaying) === 1);
|
|
43
|
-
setBeats(Number(position));
|
|
44
|
-
worker.postMessage(["tick"]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Handshake: the page loads asynchronously, so never assume the wrapper
|
|
48
|
-
// already sent state - announce readiness and let it reply.
|
|
49
|
-
uiReady();
|
|
50
|
-
|
|
51
|
-
return () => worker.terminate();
|
|
52
|
-
}, []);
|
|
53
|
-
|
|
54
|
-
// The wrapper stamp is "<version> <iso date>"; the UI only bakes in the
|
|
55
|
-
// version. A mismatch means a mixed install: Live embeds a copy of the
|
|
56
|
-
// device in the set, so a reinstalled .amxd does NOT update instances
|
|
57
|
-
// already on tracks.
|
|
58
|
-
const stale = wrapperBuild !== null && wrapperBuild.split(" ")[0] !== __APP_VERSION__;
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<main className="device">
|
|
62
|
-
<header>
|
|
63
|
-
<h1>{{name}}</h1>
|
|
64
|
-
<span className={`badge ${inJweb ? "live" : "dev"}`}>{inJweb ? "in Max" : "browser dev"}</span>
|
|
65
|
-
</header>
|
|
66
|
-
|
|
67
|
-
<dl>
|
|
68
|
-
<dt>mode</dt>
|
|
69
|
-
<dd>{mode}</dd>
|
|
70
|
-
<dt>tempo</dt>
|
|
71
|
-
<dd>{tempo === null ? "-" : `${tempo.toFixed(1)} BPM`}</dd>
|
|
72
|
-
<dt>transport</dt>
|
|
73
|
-
<dd>
|
|
74
|
-
<span className={playing ? "dot on" : "dot"} /> {playing ? "playing" : "stopped"} @ {beats.toFixed(2)} beats
|
|
75
|
-
</dd>
|
|
76
|
-
<dt>worker ticks</dt>
|
|
77
|
-
<dd>{workerTicks}</dd>
|
|
78
|
-
</dl>
|
|
79
|
-
|
|
80
|
-
<footer>
|
|
81
|
-
<span>ui {__APP_VERSION__}</span>
|
|
82
|
-
<span>wrapper {wrapperBuild ?? "-"}</span>
|
|
83
|
-
{stale && <span className="warn">stale install - delete and re-drag the device</span>}
|
|
84
|
-
</footer>
|
|
85
|
-
</main>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* protocol.ts - the typed list of selectors crossing the UI <-> device bridge.
|
|
3
|
-
*
|
|
4
|
-
* This is the single source of truth for BOTH sides of the bridge:
|
|
5
|
-
* - the web app binds/emits these selectors (via @m4l-jweb/bridge)
|
|
6
|
-
* - the [js] wrapper and the generated patcher route these selectors
|
|
7
|
-
*
|
|
8
|
-
* A Max message is a selector word followed by arguments
|
|
9
|
-
* (e.g. `tick 1 12.5`). Keep selectors here so a CI lint can assert every one
|
|
10
|
-
* appears in a route or handler on the patcher/wrapper side.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** Selectors the DEVICE sends INTO the UI (bindInlet these). */
|
|
14
|
-
export const IN = {
|
|
15
|
-
/** wrapper -> UI: current run mode (midi | audio | instrument). */
|
|
16
|
-
mode: "mode",
|
|
17
|
-
/** wrapper -> UI: build stamp, for the stale-install check. */
|
|
18
|
-
build: "build",
|
|
19
|
-
/** wrapper -> UI: transport state. args: `<playing 0|1> <beats>`. */
|
|
20
|
-
tick: "tick",
|
|
21
|
-
/** wrapper -> UI: Live tempo in BPM. args: `<bpm>`. */
|
|
22
|
-
tempo: "tempo",
|
|
23
|
-
} as const;
|
|
24
|
-
|
|
25
|
-
/** Selectors the UI sends OUT to the device (outlet these). */
|
|
26
|
-
export const OUT = {
|
|
27
|
-
/** UI -> wrapper: page finished loading; reply with current state. */
|
|
28
|
-
ui_ready: "ui_ready",
|
|
29
|
-
} as const;
|
|
30
|
-
|
|
31
|
-
export type InSelector = (typeof IN)[keyof typeof IN];
|
|
32
|
-
export type OutSelector = (typeof OUT)[keyof typeof OUT];
|