animader-cli 0.0.0-stage → 0.1.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 +121 -0
- package/adapters/build-node.mjs +261 -0
- package/adapters/devserver.mjs +481 -0
- package/adapters/devsession.mjs +619 -0
- package/adapters/export-node.mjs +601 -0
- package/adapters/export-run.mjs +126 -0
- package/adapters/generations.mjs +211 -0
- package/adapters/http.mjs +294 -0
- package/adapters/launch.mjs +83 -0
- package/adapters/node-fs.mjs +68 -0
- package/adapters/pngine.mjs +87 -0
- package/adapters/snapshot.mjs +129 -0
- package/adapters/target.mjs +104 -0
- package/adapters/watch.mjs +355 -0
- package/adapters/webaudio.mjs +102 -0
- package/bin/animader.mjs +31 -0
- package/hello/join.js +46 -0
- package/hello/run.js +259 -0
- package/lib/build.mjs +126 -0
- package/lib/buildloop.mjs +254 -0
- package/lib/contract.mjs +371 -0
- package/lib/errors.mjs +51 -0
- package/lib/export.mjs +445 -0
- package/lib/new.mjs +304 -0
- package/lib/pipeline.mjs +247 -0
- package/lib/receipt.mjs +331 -0
- package/lib/routes.mjs +170 -0
- package/lib/score.mjs +434 -0
- package/lib/show.mjs +458 -0
- package/lib/soundtrack.mjs +67 -0
- package/lib/vendor.mjs +99 -0
- package/lib/wav.mjs +197 -0
- package/package.json +47 -4
- package/player/boot.mjs +792 -0
- package/player/controller.mjs +366 -0
- package/player/devchannel.mjs +306 -0
- package/player/index.html +35 -0
- package/player/page.mjs +134 -0
- package/player/player.css +393 -0
- package/player/release.mjs +76 -0
- package/src/commands/build.mjs +201 -0
- package/src/commands/dev.mjs +214 -0
- package/src/commands/doctor.mjs +348 -0
- package/src/commands/export.mjs +158 -0
- package/src/commands/new.mjs +164 -0
- package/src/commands/project.mjs +46 -0
- package/src/config.mjs +110 -0
- package/src/router.mjs +149 -0
- package/src/toolchain.mjs +691 -0
- package/starters/circle/README.md +50 -0
- package/starters/circle/animader.json +4 -0
- package/starters/circle/gitignore +9 -0
- package/starters/circle/kernels/circle.wgsl +32 -0
- package/starters/circle/program.sjon +34 -0
- package/starters/circle/timeline.piece.sjon +26 -0
- package/starters/ember/bake/spans.bin +0 -0
- package/starters/ember/kernels/curl.wgsl +10 -0
- package/starters/ember/kernels/ember.wgsl +10 -0
- package/starters/ember/kernels/hash.wgsl +12 -0
- package/starters/ember/kernels/simplex.wgsl +27 -0
- package/starters/ember/program.sjon +328 -0
- package/starters/ember/timeline.piece.sjon +34 -0
- package/toolchain/licenses/animader.txt +121 -0
- package/toolchain/licenses/esbuild.txt +21 -0
- package/toolchain/licenses/pacer.txt +121 -0
- package/toolchain/licenses/pngine.txt +121 -0
- package/toolchain/manifest.json +212 -0
- package/toolchain/prepare.mjs +446 -0
- package/toolchain/uniform-type-name.js +184 -0
- package/README.md +0 -3
|
@@ -0,0 +1,35 @@
|
|
|
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">
|
|
6
|
+
<meta name="color-scheme" content="dark">
|
|
7
|
+
<title>Animader demo</title>
|
|
8
|
+
<link rel="icon" href="data:,">
|
|
9
|
+
<link rel="stylesheet" href="/player/player.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<main>
|
|
13
|
+
<h1 class="visually-hidden">Animader demo</h1>
|
|
14
|
+
<div id="performance">
|
|
15
|
+
<div id="stage" class="stage">
|
|
16
|
+
<canvas id="canvas" width="1280" height="720" role="img" aria-label="Live performance canvas"></canvas>
|
|
17
|
+
</div>
|
|
18
|
+
<p id="status" class="status" role="status" aria-live="polite">loading</p>
|
|
19
|
+
<div id="controls" class="panel" role="group" aria-label="Playback controls">
|
|
20
|
+
<button id="primary" type="button" data-player-control="primary" hidden>Start</button>
|
|
21
|
+
<button id="fullscreen" type="button" class="secondary" data-player-control="fullscreen" hidden>Fullscreen</button>
|
|
22
|
+
</div>
|
|
23
|
+
<div id="error" class="banner" role="alert" tabindex="-1" hidden>
|
|
24
|
+
<span class="marker">error:</span> <span id="error-message"></span>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<details id="diagnostics" class="diagnostics" hidden>
|
|
28
|
+
<summary>diagnostics</summary>
|
|
29
|
+
<pre id="diagnostics-text"></pre>
|
|
30
|
+
</details>
|
|
31
|
+
<noscript>This demo needs JavaScript to load the prepared viewer and its audio clock.</noscript>
|
|
32
|
+
</main>
|
|
33
|
+
<script type="module" src="/player/page.mjs"></script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
package/player/page.mjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The dev entry: the original server-mounted UI and URLs, injected into the
|
|
2
|
+
// shared page implementation. The release entry replaces this profile with
|
|
3
|
+
// static imports and relative export assets; it does not import this file.
|
|
4
|
+
//
|
|
5
|
+
// Generation pinning (plan section 5): the page boots either on the
|
|
6
|
+
// generation the reload URL carries (?gen=N) or, on a bare load, learns the
|
|
7
|
+
// current generation from /config.json before any show asset is requested —
|
|
8
|
+
// then every project-derived fetch (config, payload, timeline, soundtrack)
|
|
9
|
+
// is pinned to that one immutable /gen/<id>/ prefix, so a rebuild under a
|
|
10
|
+
// live page can never mix a new timeline with an old payload or audio. The
|
|
11
|
+
// development-only channel in devchannel.mjs reloads the page onto a new
|
|
12
|
+
// generation when the server says so.
|
|
13
|
+
|
|
14
|
+
import { boot, reportBootFailure } from './boot.mjs';
|
|
15
|
+
import { connectDevChannel, showStaleNotice } from './devchannel.mjs';
|
|
16
|
+
|
|
17
|
+
function pinnedGenerationFromUrl() {
|
|
18
|
+
const param = new URLSearchParams(window.location.search).get('gen');
|
|
19
|
+
if (param !== null && /^[1-9][0-9]*$/.test(param)) return Number(param);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function currentGeneration() {
|
|
24
|
+
const pinned = pinnedGenerationFromUrl();
|
|
25
|
+
if (pinned !== null) return pinned;
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch('/config.json', { cache: 'no-store' });
|
|
28
|
+
if (!response.ok) return null;
|
|
29
|
+
const config = await response.json();
|
|
30
|
+
if (Number.isInteger(config?.generation) && config.generation > 0) return config.generation;
|
|
31
|
+
return null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const generation = await currentGeneration();
|
|
38
|
+
const prefix = generation === null ? '' : `/gen/${generation}`;
|
|
39
|
+
|
|
40
|
+
const documents = {
|
|
41
|
+
runtimeModule: '/runtime/pngine.mjs',
|
|
42
|
+
config: `${prefix}/config.json`,
|
|
43
|
+
payload: 'out/show.png',
|
|
44
|
+
manifest: 'manifest.json',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
async function importModule(path) {
|
|
48
|
+
try {
|
|
49
|
+
return await import(path);
|
|
50
|
+
} catch (cause) {
|
|
51
|
+
const error = new Error(`${path} could not be loaded: ${cause?.message ?? cause}`);
|
|
52
|
+
error.code = 'module_failed';
|
|
53
|
+
error.stage = 'modules';
|
|
54
|
+
error.document = path;
|
|
55
|
+
error.cause = cause;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const devProfile = {
|
|
61
|
+
name: 'dev',
|
|
62
|
+
remedy: 're-run the one-time prepare (node demo/prepare.mjs) to re-vendor it from the actual build',
|
|
63
|
+
documents,
|
|
64
|
+
loadController: () => importModule('./controller.mjs'),
|
|
65
|
+
loadModules: async () => {
|
|
66
|
+
const [
|
|
67
|
+
scoreModule,
|
|
68
|
+
showModule,
|
|
69
|
+
soundtrackData,
|
|
70
|
+
webaudioModule,
|
|
71
|
+
pngineModule,
|
|
72
|
+
pacerModule,
|
|
73
|
+
routeModule,
|
|
74
|
+
pieceModule,
|
|
75
|
+
runModule,
|
|
76
|
+
] = await Promise.all([
|
|
77
|
+
importModule('/app/lib/score.mjs'),
|
|
78
|
+
importModule('/app/lib/show.mjs'),
|
|
79
|
+
importModule('/app/lib/soundtrack.mjs'),
|
|
80
|
+
importModule('/app/adapters/webaudio.mjs'),
|
|
81
|
+
importModule('/runtime/pngine.mjs'),
|
|
82
|
+
importModule('/runtime/pacer/index.js'),
|
|
83
|
+
importModule('/runtime/pacer/route.js'),
|
|
84
|
+
importModule('/runtime/animader/piece.js'),
|
|
85
|
+
importModule('/examples/hello/player/run.js'),
|
|
86
|
+
]);
|
|
87
|
+
return {
|
|
88
|
+
score: scoreModule,
|
|
89
|
+
show: showModule,
|
|
90
|
+
soundtrack: soundtrackData,
|
|
91
|
+
webaudio: webaudioModule,
|
|
92
|
+
pngine: pngineModule,
|
|
93
|
+
pacer: pacerModule,
|
|
94
|
+
route: routeModule,
|
|
95
|
+
piece: pieceModule,
|
|
96
|
+
run: runModule,
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
configUrl: `${prefix}/config.json`,
|
|
100
|
+
manifestUrl: null,
|
|
101
|
+
configManifestPath: null,
|
|
102
|
+
assets: [
|
|
103
|
+
{ key: 'payload', url: `${prefix}/project/out/show.png`, kind: 'bytes' },
|
|
104
|
+
{ key: 'timeline', url: `${prefix}/project/timeline.piece.sjon`, kind: 'text' },
|
|
105
|
+
{ key: 'provenance', url: '/runtime/provenance.json', kind: 'json' },
|
|
106
|
+
{ key: 'pacerWasm', url: '/runtime/pacer/pacer.wasm', kind: 'bytes' },
|
|
107
|
+
{ key: 'pieceWasm', url: '/runtime/animader/animader.wasm', kind: 'bytes' },
|
|
108
|
+
],
|
|
109
|
+
// The audio URL comes from the validated config selection, never from a
|
|
110
|
+
// hardcoded name: the loader appends this asset after config validation.
|
|
111
|
+
soundtrackAsset: (name) => ({ key: 'soundtrack', url: `${prefix}/project/${name}`, kind: 'bytes' }),
|
|
112
|
+
loadScore: ({ modules, assets, reflection }) => modules.score.loadShow({
|
|
113
|
+
document: {
|
|
114
|
+
name: 'timeline.piece.sjon',
|
|
115
|
+
text: assets.timeline.text,
|
|
116
|
+
},
|
|
117
|
+
dep: {
|
|
118
|
+
pieceWasm: assets.pieceWasm.bytes,
|
|
119
|
+
pacerWasm: assets.pacerWasm.bytes,
|
|
120
|
+
reflection,
|
|
121
|
+
},
|
|
122
|
+
factories: {
|
|
123
|
+
instantiatePieceHost: modules.piece.PieceHost.instantiate,
|
|
124
|
+
loadPacer: modules.pacer.Pacer.load,
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// The stale notice is built by the dev channel module; the stylesheet owns
|
|
130
|
+
// its out-of-flow layout contract, so it overlays without reflowing the
|
|
131
|
+
// page and the primary control stays reachable.
|
|
132
|
+
connectDevChannel({ generation, onStale: (error) => showStaleNotice(document, error) });
|
|
133
|
+
|
|
134
|
+
boot(devProfile).catch(reportBootFailure);
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/* The demo player extends the incumbent hello tokens; the canvas is the only
|
|
2
|
+
thing that scales. The artboard's CSS content box stays 1280x720 and pngine
|
|
3
|
+
owns its backing store at the actual device DPR. There is deliberately no
|
|
4
|
+
canvas media query: the wrapper letterboxes by transform, so a narrow window
|
|
5
|
+
never changes the canvas content box or fires a mid-run reallocation. */
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
color-scheme: dark;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
* {
|
|
12
|
+
box-sizing: border-box;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
background: #0a0608;
|
|
18
|
+
color: #d8cfc4;
|
|
19
|
+
font: 14px/1.4 system-ui, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.visually-hidden {
|
|
23
|
+
position: absolute;
|
|
24
|
+
width: 1px;
|
|
25
|
+
height: 1px;
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: -1px;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
clip: rect(0 0 0 0);
|
|
30
|
+
white-space: nowrap;
|
|
31
|
+
border: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
main {
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: minmax(0, 1fr);
|
|
37
|
+
gap: 12px;
|
|
38
|
+
width: 100%;
|
|
39
|
+
max-width: 1312px;
|
|
40
|
+
margin: 0 auto;
|
|
41
|
+
padding: 16px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#performance {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: 12px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.stage {
|
|
51
|
+
position: relative;
|
|
52
|
+
width: 100%;
|
|
53
|
+
max-width: 1280px;
|
|
54
|
+
aspect-ratio: 16 / 9;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
background: #000;
|
|
57
|
+
border: 1px solid #2c2026;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#canvas {
|
|
61
|
+
position: absolute;
|
|
62
|
+
top: 0;
|
|
63
|
+
left: 0;
|
|
64
|
+
display: block;
|
|
65
|
+
background: #000;
|
|
66
|
+
transform-origin: 0 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#performance:fullscreen {
|
|
70
|
+
background: #0a0608;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
gap: 12px;
|
|
74
|
+
padding: 16px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#performance:fullscreen .stage {
|
|
78
|
+
flex: 1;
|
|
79
|
+
min-height: 0;
|
|
80
|
+
max-width: none;
|
|
81
|
+
aspect-ratio: auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.status {
|
|
85
|
+
margin: 0;
|
|
86
|
+
color: #8f857a;
|
|
87
|
+
font-variant-numeric: tabular-nums;
|
|
88
|
+
overflow-wrap: anywhere;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.panel {
|
|
92
|
+
display: flex;
|
|
93
|
+
gap: 16px;
|
|
94
|
+
align-items: center;
|
|
95
|
+
flex-wrap: wrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
button {
|
|
99
|
+
background: #241a20;
|
|
100
|
+
color: #d8cfc4;
|
|
101
|
+
border: 1px solid #3a2a33;
|
|
102
|
+
padding: 4px 10px;
|
|
103
|
+
font: inherit;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
button:hover {
|
|
107
|
+
border-color: #8f857a;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
button:focus-visible {
|
|
111
|
+
outline: 2px solid #f0b27a;
|
|
112
|
+
outline-offset: 2px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#primary {
|
|
116
|
+
border-color: #f0b27a;
|
|
117
|
+
color: #f0b27a;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.secondary {
|
|
121
|
+
background: transparent;
|
|
122
|
+
color: #8f857a;
|
|
123
|
+
border-color: #2c2026;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.banner {
|
|
127
|
+
padding: 8px 10px;
|
|
128
|
+
background: #140d11;
|
|
129
|
+
border: 1px solid #f0b27a;
|
|
130
|
+
color: #d8cfc4;
|
|
131
|
+
font: 12px/1.4 ui-monospace, monospace;
|
|
132
|
+
overflow-wrap: anywhere;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.banner:focus-visible {
|
|
136
|
+
outline: 2px solid #f0b27a;
|
|
137
|
+
outline-offset: 2px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.marker {
|
|
141
|
+
color: #f0b27a;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* The dev-only stale notice overlays the viewport top out of flow: a fixed
|
|
145
|
+
element is removed from normal flow, so the stage, the status line and
|
|
146
|
+
the controls keep the exact offsets they have without the notice — an
|
|
147
|
+
in-flow notice once pushed the primary control below the fold on a
|
|
148
|
+
1280x800 laptop. The message wraps instead of truncating or overflowing.
|
|
149
|
+
The release page has no dev channel and never renders the element. */
|
|
150
|
+
|
|
151
|
+
/* The notice's height cap is geometry, not a device class: the notice may
|
|
152
|
+
grow down to the canvas's central third — the middle band of the picture,
|
|
153
|
+
where a subject sits — and no further, at every width. The stage is
|
|
154
|
+
width: 100% / max-width: 1280px / aspect-ratio: 16/9, so a third of the
|
|
155
|
+
canvas is min(100vw, 1280px) * 9/16 / 3 = min(100vw, 1280px) * 0.1875
|
|
156
|
+
tall. Two measured corrections make that expression honest (Chromium
|
|
157
|
+
153, viewport height 800, the 430–1280 sweep of 2026-10-28 recorded in
|
|
158
|
+
.agent/control/stale-cap-verify.md; canvas.top measured 17 at every
|
|
159
|
+
width — 16px main padding + 1px stage border-top):
|
|
160
|
+
|
|
161
|
+
- the canvas starts 17px down, which the expression does not know;
|
|
162
|
+
- main's 32px horizontal padding and the stage's 2px borders make the
|
|
163
|
+
real canvas height (min(100vw, 1312px) - 34px) * 0.5625, so the
|
|
164
|
+
expression over-counts a third by (34 * 0.5625) / 3 = 6.375px.
|
|
165
|
+
|
|
166
|
+
The exact allowance is therefore 17 - 6.375 = 10.625px; the largest
|
|
167
|
+
whole pixel under it, 10px, leaves the cap 0.19px (431 and 767px),
|
|
168
|
+
0.33–0.5px (the other measured widths) and structurally 0.625px below
|
|
169
|
+
the central third at every width up to 1280px. No max-width media query
|
|
170
|
+
owns this: the 767px boundary capped tablet presentations that already
|
|
171
|
+
cleared the third (at 640px the natural six-line notice cleared it by
|
|
172
|
+
11.6px) and answered a hypothetical longer message instead of the
|
|
173
|
+
measured one — diagnostic length is unbounded, so only the relationship
|
|
174
|
+
between the notice and the canvas bounds it.
|
|
175
|
+
|
|
176
|
+
The cap bounds the notice as a GRID of two rows: the summary's auto row
|
|
177
|
+
and the opened content's minmax(0, 1fr) row. The 1fr track is what makes
|
|
178
|
+
the cap reach the text: a fr row resolves against the clamped container
|
|
179
|
+
while an auto row (or a flex basis) cannot, and the engine's
|
|
180
|
+
::details-content wrapper — the box that actually hosts the body inside a
|
|
181
|
+
<details> — is the grid item, so it carries min-height: 0 and the body
|
|
182
|
+
fills it under max-height: 100% and scrolls there. The notice itself is
|
|
183
|
+
never the scroller, because a scrollable details measures (Chromium,
|
|
184
|
+
2026-10-29) as a tab stop BEFORE its own summary while Tab forward from
|
|
185
|
+
the summary left the notice; the tab order is now summary → (open) body
|
|
186
|
+
→ out (round 3, finding 1). The one media query that touches the notice
|
|
187
|
+
swaps text only — below the covered range the summary's recovery
|
|
188
|
+
sentence becomes a compact phrase that runs inline with the affordance —
|
|
189
|
+
never geometry; that is a sentence-length decision, not the rejected
|
|
190
|
+
width cap returning.
|
|
191
|
+
|
|
192
|
+
The collapsed summary (block lines: lead, recovery, affordance — see
|
|
193
|
+
devchannel.mjs) keeps its own tighter line-height (1.2 → 14.4px a line)
|
|
194
|
+
and the lead keeps to one introducing word. At 360px and wider it is
|
|
195
|
+
four lines: 4 × 14.4px + 18px chrome = 75.6px against the 77.5px cap at
|
|
196
|
+
360px, 83.125px at 390px, 85.9375px at 405px — it fits at rest at
|
|
197
|
+
360/375/390/405 and everywhere wider. Below 360px the compact phrase
|
|
198
|
+
takes over: at 320px the cap (70px) holds exactly three lines, the lead
|
|
199
|
+
alone takes two of them, and the compact phrase plus the affordance
|
|
200
|
+
share the third (36 model characters against the 38-character line), so
|
|
201
|
+
the collapsed summary is 3 × 14.4px + 18px = 61.2px — it fits at rest
|
|
202
|
+
down to 320px and (by the same arithmetic) to roughly 299px. Below that
|
|
203
|
+
floor not even the compact fits; the notice cannot scroll while
|
|
204
|
+
collapsed any more, so overflow: hidden clips at the cap — the geometry
|
|
205
|
+
holds, the sub-floor text is the recorded gap, and the proof, per width,
|
|
206
|
+
is the arithmetic guard in npm/cli/test/stale-cap.test.js.
|
|
207
|
+
100vw includes a classic scrollbar where one exists, which can move the
|
|
208
|
+
cap a couple of px past the true third there; only the opened, scrolling
|
|
209
|
+
state can ever reach the cap. Fullscreen does not follow this formula:
|
|
210
|
+
#performance:fullscreen .stage is flex: 1 with aspect-ratio: auto, so
|
|
211
|
+
the stage stops being 16:9 and no width-derived expression describes its
|
|
212
|
+
height. The cap keeps its >=1280px value (250px) there — conservative on
|
|
213
|
+
a tall stage, but on a short one it can sit past the stage's true third;
|
|
214
|
+
the collapsed summary is what shows at rest either way. */
|
|
215
|
+
#dev-stale {
|
|
216
|
+
position: fixed;
|
|
217
|
+
top: 0;
|
|
218
|
+
left: 0;
|
|
219
|
+
right: 0;
|
|
220
|
+
z-index: 1;
|
|
221
|
+
margin: 0;
|
|
222
|
+
padding: 8px 12px;
|
|
223
|
+
border: 1px solid #3a2a33;
|
|
224
|
+
background: #140d11;
|
|
225
|
+
color: #f0b27a;
|
|
226
|
+
font: 12px/1.4 ui-monospace, monospace;
|
|
227
|
+
overflow-wrap: anywhere;
|
|
228
|
+
white-space: pre-line;
|
|
229
|
+
max-height: calc(min(100vw, 1280px) * 0.1875 + 10px);
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* The collapsed state is a native <details>: the summary is the keyboard
|
|
236
|
+
affordance on every engine without scripting — the review that reshaped
|
|
237
|
+
this found the previous capped scroll box reachable only where the
|
|
238
|
+
engine happens to make scroll containers tab stops. The UA marker is
|
|
239
|
+
dropped in favour of the summary's last line, which says in words that
|
|
240
|
+
the full failure is one press away (the ::-webkit-details-marker rule
|
|
241
|
+
covers engines whose summary ignores list-style). The focus ring is the
|
|
242
|
+
house 2px amber :focus-visible treatment that buttons, .banner and the
|
|
243
|
+
diagnostics summary already carry, not the UA default.
|
|
244
|
+
The summary's own line-height is deliberately tighter than the notice's
|
|
245
|
+
1.4: at 360px and wider the collapsed state is four lines (one-word
|
|
246
|
+
lead, twice-wrapped recovery sentence, affordance) and must fit the cap
|
|
247
|
+
at rest — 4 × 14.4px + 18px chrome = 75.6px against the 77.5px cap; at
|
|
248
|
+
1.4 the same four lines are 85.2px and over the 83.125px cap at 390px,
|
|
249
|
+
which is the defect round 2 fixed. The expanded body keeps the notice's
|
|
250
|
+
1.4 rhythm. The summary's grid row is auto, so the cap's pressure lands
|
|
251
|
+
on the content row — the element that can scroll. */
|
|
252
|
+
#dev-stale summary {
|
|
253
|
+
display: block;
|
|
254
|
+
list-style: none;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
line-height: 1.2;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#dev-stale summary::-webkit-details-marker {
|
|
260
|
+
display: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#dev-stale summary:focus-visible {
|
|
264
|
+
outline: 2px solid #f0b27a;
|
|
265
|
+
outline-offset: 2px;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Round 3, finding 2 — the compact recovery phrase, the one place a width
|
|
269
|
+
query is allowed to touch the notice, because it chooses a sentence, not
|
|
270
|
+
a box. At 360px and wider the full reassurance fits (four summary lines,
|
|
271
|
+
75.6px, against caps of 77.5px at 360 and up). Below that the cap
|
|
272
|
+
shrinks 0.1875px per pixel until the full sentence no longer fits, so
|
|
273
|
+
the summary swaps it for a phrase that runs inline with the affordance
|
|
274
|
+
on one line — at 320px the cap (70px) holds exactly three 14.4px lines,
|
|
275
|
+
the lead alone takes two, and "— last good view; the full failure …"
|
|
276
|
+
(36 model characters against a 38-character line) carries the
|
|
277
|
+
reassurance AND the affordance on the third. The full sentence stays in
|
|
278
|
+
the opened body verbatim, and the display:none half is out of the
|
|
279
|
+
accessibility tree, so the summary's name matches what is painted.
|
|
280
|
+
The swap boundary is 359px — the widest pixel the full-sentence proof
|
|
281
|
+
(360 and up) does not cover; the compact phrase is proven down to 320px
|
|
282
|
+
and (by the guard's arithmetic) to roughly 299px. */
|
|
283
|
+
#dev-stale .stale-recovery-compact {
|
|
284
|
+
display: none;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@media (max-width: 359px) {
|
|
288
|
+
#dev-stale .stale-recovery {
|
|
289
|
+
display: none;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#dev-stale .stale-recovery-compact,
|
|
293
|
+
#dev-stale .stale-more {
|
|
294
|
+
display: inline;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#dev-stale summary span {
|
|
299
|
+
display: block;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#dev-stale .stale-more {
|
|
303
|
+
color: #8f857a;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* The expanded body: the whole failure verbatim, the built newlines painted
|
|
307
|
+
by the inherited pre-line, separated from the summary by the diagnostics
|
|
308
|
+
rhythm — and the notice's ONLY scroller (round 3, finding 1). Inside a
|
|
309
|
+
<details> the engine hosts the non-summary children in its own
|
|
310
|
+
::details-content wrapper — THAT box is the second grid row, so it must
|
|
311
|
+
carry min-height: 0 or the row can never shrink below the body's
|
|
312
|
+
min-content and the notice clips instead of the body scrolling. The body
|
|
313
|
+
then fills the row under max-height: 100% and scrolls there, and
|
|
314
|
+
tabindex="0" (set as markup by devchannel.mjs) makes that scroller a tab
|
|
315
|
+
stop after the summary — so the summary is the notice's first and only
|
|
316
|
+
stop while collapsed, and summary → body → out once open. The closed
|
|
317
|
+
disclosure does not render the body, so it is not a stop until it can be
|
|
318
|
+
read. */
|
|
319
|
+
#dev-stale::details-content {
|
|
320
|
+
min-height: 0;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#dev-stale .stale-body {
|
|
324
|
+
margin: 8px 0 0;
|
|
325
|
+
max-height: 100%;
|
|
326
|
+
overflow-y: auto;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* The scroller's own ring: the same house 2px amber :focus-visible
|
|
330
|
+
treatment as the summary and the buttons, on the element that actually
|
|
331
|
+
scrolls — the opened body. */
|
|
332
|
+
#dev-stale .stale-body:focus-visible {
|
|
333
|
+
outline: 2px solid #f0b27a;
|
|
334
|
+
outline-offset: 2px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Round 3, finding 3 — when the opened body scrolls, say so. A scroller at
|
|
338
|
+
rest shows a partial last line, and at 320px the round-2 reading showed
|
|
339
|
+
exactly that reading as the end of the message ("successful edit" cut to
|
|
340
|
+
a sliver, no scrollbar painted). The builder measures scrollHeight
|
|
341
|
+
against clientHeight when the disclosure opens (the native toggle event)
|
|
342
|
+
and on every delivery, and wears the class; the cue is then a sticky
|
|
343
|
+
::after inside the scroller: pinned to the scrollport's bottom edge —
|
|
344
|
+
opaque, in the notice's panel colour with the diagnostics separator — so
|
|
345
|
+
it covers the partial line while text follows, and it settles after the
|
|
346
|
+
last line once the end is reached. No hover anywhere: a cue that needs a
|
|
347
|
+
pointer hides the problem from touch and from the keyboard. */
|
|
348
|
+
#dev-stale .stale-body.scrolls-more::after {
|
|
349
|
+
content: '…';
|
|
350
|
+
display: block;
|
|
351
|
+
position: sticky;
|
|
352
|
+
bottom: 0;
|
|
353
|
+
color: #8f857a;
|
|
354
|
+
text-align: right;
|
|
355
|
+
background: #140d11;
|
|
356
|
+
border-top: 1px solid #2c2026;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.diagnostics {
|
|
360
|
+
padding-top: 8px;
|
|
361
|
+
border-top: 1px solid #2c2026;
|
|
362
|
+
color: #8f857a;
|
|
363
|
+
font: 12px/1.4 ui-monospace, monospace;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.diagnostics summary {
|
|
367
|
+
cursor: pointer;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.diagnostics summary:focus-visible {
|
|
371
|
+
outline: 2px solid #f0b27a;
|
|
372
|
+
outline-offset: 2px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
#diagnostics-text {
|
|
376
|
+
margin: 8px 0 0;
|
|
377
|
+
color: #d8cfc4;
|
|
378
|
+
white-space: pre-wrap;
|
|
379
|
+
overflow-wrap: anywhere;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.diag-line {
|
|
383
|
+
display: block;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
::selection {
|
|
387
|
+
background: #f0b27a;
|
|
388
|
+
color: #0a0608;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
[hidden] {
|
|
392
|
+
display: none !important;
|
|
393
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The release entry. It statically imports the portable core, the prepared
|
|
2
|
+
// pngine viewer, the playback-only pacer runtime and the hello run core, then
|
|
3
|
+
// points the shared page implementation at the exported files relative to
|
|
4
|
+
// this module. The third-party playback runtime arrives as bare
|
|
5
|
+
// specifiers — 'pngine/viewer', '@mr_hugo/pacer/rt', '@mr_hugo/pacer/route' — which the
|
|
6
|
+
// bundler resolves against the toolchain's pinned artifacts (the vendored
|
|
7
|
+
// bundle in contributor mode, the installed packages otherwise), so this
|
|
8
|
+
// file never carries a path out of the package. The authoring compiler
|
|
9
|
+
// (animader.wasm/piece.js), pacer's text compile driver, the dev profile
|
|
10
|
+
// and every server-mount path are absent from this graph by construction;
|
|
11
|
+
// esbuild bundles this file and its static imports into the single shipped
|
|
12
|
+
// app.mjs.
|
|
13
|
+
//
|
|
14
|
+
// `import.meta.url` is app.mjs at the export root, so every URL below stays
|
|
15
|
+
// correct when the whole export is served from a nested path.
|
|
16
|
+
|
|
17
|
+
import { createPlayerController, audioDurationNote } from './controller.mjs';
|
|
18
|
+
import { boot, reportBootFailure } from './boot.mjs';
|
|
19
|
+
import { loadPackedShow, ShowError } from '../lib/score.mjs';
|
|
20
|
+
import { createShow } from '../lib/show.mjs';
|
|
21
|
+
import * as soundtrackData from '../lib/soundtrack.mjs';
|
|
22
|
+
import { createWebAudioPort } from '../adapters/webaudio.mjs';
|
|
23
|
+
import { pngine, draw, setUniform, getUniforms, restartSettled } from 'pngine/viewer';
|
|
24
|
+
import { PacerRt } from '@mr_hugo/pacer/rt';
|
|
25
|
+
import { routeWrites, toUniforms } from '@mr_hugo/pacer/route';
|
|
26
|
+
import { createPlayer } from '../hello/run.js';
|
|
27
|
+
|
|
28
|
+
const root = new URL('./', import.meta.url);
|
|
29
|
+
|
|
30
|
+
const releaseProfile = {
|
|
31
|
+
name: 'release',
|
|
32
|
+
remedy: 'rebuild the export with node demo/export.mjs so the prepared viewer carries restartSettled',
|
|
33
|
+
documents: {
|
|
34
|
+
runtimeModule: './app.mjs',
|
|
35
|
+
config: './config.json',
|
|
36
|
+
payload: 'assets/show.png',
|
|
37
|
+
manifest: 'manifest.json',
|
|
38
|
+
},
|
|
39
|
+
loadController: async () => ({ createPlayerController, audioDurationNote }),
|
|
40
|
+
loadModules: async () => ({
|
|
41
|
+
score: { loadPackedShow, ShowError },
|
|
42
|
+
show: { createShow },
|
|
43
|
+
soundtrack: soundtrackData,
|
|
44
|
+
webaudio: { createWebAudioPort },
|
|
45
|
+
pngine: { pngine, draw, setUniform, getUniforms, restartSettled },
|
|
46
|
+
pacerRt: { PacerRt },
|
|
47
|
+
route: { routeWrites, toUniforms },
|
|
48
|
+
run: { createPlayer },
|
|
49
|
+
}),
|
|
50
|
+
configUrl: new URL('./config.json', root),
|
|
51
|
+
configManifestPath: 'config.json',
|
|
52
|
+
manifestUrl: new URL('./manifest.json', root),
|
|
53
|
+
assets: [
|
|
54
|
+
{ key: 'payload', url: new URL('./assets/show.png', root), kind: 'bytes', manifestPath: 'assets/show.png' },
|
|
55
|
+
{ key: 'score', url: new URL('./assets/score.pacb', root), kind: 'bytes', manifestPath: 'assets/score.pacb' },
|
|
56
|
+
{ key: 'pacerRtWasm', url: new URL('./pacer-rt.wasm', root), kind: 'bytes', manifestPath: 'pacer-rt.wasm' },
|
|
57
|
+
],
|
|
58
|
+
// The audio asset follows the validated config selection so a release can
|
|
59
|
+
// ship any recognized name; the loader appends it after config validation.
|
|
60
|
+
soundtrackAsset: (name) => ({
|
|
61
|
+
key: 'soundtrack',
|
|
62
|
+
url: new URL(`./assets/${name}`, root),
|
|
63
|
+
kind: 'bytes',
|
|
64
|
+
manifestPath: `assets/${name}`,
|
|
65
|
+
}),
|
|
66
|
+
loadScore: ({ modules, assets, reflection }) => modules.score.loadPackedShow({
|
|
67
|
+
document: { name: 'score.pacb', bytes: assets.score.bytes },
|
|
68
|
+
dep: {
|
|
69
|
+
pacerRtWasm: assets.pacerRtWasm.bytes,
|
|
70
|
+
reflection,
|
|
71
|
+
},
|
|
72
|
+
factories: { loadPacerRt: modules.pacerRt.PacerRt.load },
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
boot(releaseProfile).catch(reportBootFailure);
|