@bongos/core 1.25.31 → 1.25.33
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/.bongos-core.json +36 -41
- package/docs/copy-inventory.md +28 -28
- package/docs/copy-registry.json +30 -30
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +202 -175
- package/modules/autonomy/runner-health.js +19 -0
- package/modules/hall-ui/public/gate.js +9 -5
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/runner-button.js +7 -0
- package/modules/hall-ui/public/shell-game.css +2 -3
- package/modules/hall-ui/public/shell-game.js +6 -5
- package/modules/hall-ui/public/welcome-boot.css +18 -31
- package/modules/hall-ui/public/welcome-boot.js +81 -97
- package/modules/hall-ui/records/start-screen.md +6 -6
- package/modules/hall-ui/records/welcome-tower.md +29 -20
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/autobongos-host.js +22 -11
- package/scripts/bongos/autobongos-run.js +37 -5
- package/src/module-api.js +1 -1
- package/tests/autobongos_cannot_fetch.mjs +123 -0
- package/tests/hall_sky_time.mjs +3 -2
- package/tests/hall_start_screen.mjs +3 -2
- package/tests/welcome_boot.mjs +35 -27
- package/tests/welcome_boot_browser.mjs +25 -19
- package/modules/hall-ui/public/welcome-day.webp +0 -0
- package/modules/hall-ui/public/welcome-night.webp +0 -0
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
// it carries the fix as `remedy`. The runner decides the grace and says
|
|
31
31
|
// `config_idle:<code>` on its heartbeat; only a code config-idle.js knows is
|
|
32
32
|
// honoured, so the owner's kill switch and pause can never read as an alarm.
|
|
33
|
+
// · cannot_fetch — alive, but it has failed to reach the project several times
|
|
34
|
+
// in a row (task 1005202), so it is pinned to the code it has. NOT ok, and it
|
|
35
|
+
// carries the fix as `remedy`, like idle_config.
|
|
33
36
|
// · stopped — alive, and its builder pressed Stop on this device (task
|
|
34
37
|
// 1005094). Not an alarm and not "running": the device is doing what it was
|
|
35
38
|
// told, and its card shows Run. Only a runner that is still checking in can
|
|
@@ -56,6 +59,11 @@ const RESTART_S = 10 * 60;
|
|
|
56
59
|
|
|
57
60
|
const configIdle = require('./config-idle.js');
|
|
58
61
|
|
|
62
|
+
// The fix for `cannot_fetch` (task 1005202). Seen on a real Mac: the GitHub
|
|
63
|
+
// account git was using could not see the project. The runner's own log names
|
|
64
|
+
// that account; this is the step to take on the device.
|
|
65
|
+
const CANNOT_FETCH_REMEDY = 'On that device, git cannot reach this project on GitHub — usually it is signed in to a GitHub account that cannot see it. Run gh auth switch there to pick one that can (or gh auth login to add it); the runner\'s log names the account it is using. It picks up new code by itself once it can.';
|
|
66
|
+
|
|
59
67
|
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
60
68
|
const sha = (v) => (typeof v === 'string' && SHA_RE.test(v) ? v : null);
|
|
61
69
|
|
|
@@ -141,6 +149,17 @@ function describeRunner(heartbeat, { nowMs = Date.now(), aliveS = ALIVE_S, lateS
|
|
|
141
149
|
holdUntil: Number.isFinite(holdMs) && holdMs > nowMs ? new Date(holdMs).toISOString() : null,
|
|
142
150
|
};
|
|
143
151
|
|
|
152
|
+
// Talking, but it cannot reach the project to fetch new code (task 1005202), so
|
|
153
|
+
// it works forever on the code it has. NOT ok, and it carries the fix. Only
|
|
154
|
+
// while it is still checking in: past that, silence is the truth.
|
|
155
|
+
if (heartbeat.last_event === 'cannot_fetch' && age <= aliveS) {
|
|
156
|
+
return {
|
|
157
|
+
state: 'cannot_fetch', ok: false, ageS: age, age: humanAge(age), ...ident,
|
|
158
|
+
why: 'it cannot fetch new code from GitHub', remedy: CANNOT_FETCH_REMEDY,
|
|
159
|
+
detail: `running old code${where} — it cannot fetch new code from GitHub`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
if (heartbeat.last_event === 'upgrade_exit') {
|
|
145
164
|
if (age <= RESTART_S) {
|
|
146
165
|
return {
|
|
@@ -99,8 +99,10 @@
|
|
|
99
99
|
// stopped (task 1005094): checking in, and its builder pressed Stop on that
|
|
100
100
|
// device in Settings. Connected, not an alarm, and not the one to describe
|
|
101
101
|
// while another device is working.
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// cannot_fetch (task 1005202): talking, but stuck on old code because it cannot
|
|
103
|
+
// reach the project on GitHub. A warning with its fix, like idle_config.
|
|
104
|
+
const HEALTH_RANK = { alive: 0, late: 1, idle_config: 2, cannot_fetch: 2, restarting: 3, stopped: 4, dead: 5, never_seen: 6 };
|
|
105
|
+
const HEALTH_DOT = { alive: 'ok', late: 'ok', idle_config: 'warn', cannot_fetch: 'warn', restarting: 'info', stopped: 'info', dead: 'danger', never_seen: 'info' };
|
|
104
106
|
|
|
105
107
|
// The panel is drawn from both reads at once; each is kept so a write that only
|
|
106
108
|
// reloads one of them redraws with the other's last answer.
|
|
@@ -124,7 +126,7 @@
|
|
|
124
126
|
// `project`, and then there is no second layer to check.
|
|
125
127
|
function readiness(runner, fence, mine) {
|
|
126
128
|
const h = (runner && runner.health) || {};
|
|
127
|
-
const connected = ['alive', 'late', 'restarting', 'idle_config', 'stopped'].includes(h.state);
|
|
129
|
+
const connected = ['alive', 'late', 'restarting', 'idle_config', 'cannot_fetch', 'stopped'].includes(h.state);
|
|
128
130
|
const goals = (fence && fence.goals) || [];
|
|
129
131
|
const items = [];
|
|
130
132
|
if (!runner) {
|
|
@@ -138,7 +140,9 @@
|
|
|
138
140
|
}
|
|
139
141
|
if (!connected) items.push({ key: 'claude', state: 'wait', text: 'Claude signed in' });
|
|
140
142
|
else if (h.account) items.push({ key: 'claude', state: 'ok', text: 'Claude signed in' });
|
|
141
|
-
if (
|
|
143
|
+
if (h.state === 'cannot_fetch') {
|
|
144
|
+
items.push({ key: 'code', state: 'todo', text: 'Cannot fetch new code', hint: h.remedy || '' });
|
|
145
|
+
} else if (connected && h.code && h.code.behind === true) {
|
|
142
146
|
items.push({ key: 'code', state: 'wait', text: 'Updating to the latest code', hint: 'It does this by itself.' });
|
|
143
147
|
} else if (connected && h.code) {
|
|
144
148
|
items.push({ key: 'code', state: 'ok', text: 'Up to date' });
|
|
@@ -388,7 +392,7 @@
|
|
|
388
392
|
: 'has picked no goals, so their runner takes no work';
|
|
389
393
|
const beats = (b.runners || []).map((r) => {
|
|
390
394
|
const h = r.health || {};
|
|
391
|
-
const label = { dead: 'not checking in', late: 'quiet', alive: 'checking in', restarting: 'restarting', stopped: 'stopped on that device', idle_config: `idle on a setting — ${h.remedy || 'see Settings'}
|
|
395
|
+
const label = { dead: 'not checking in', late: 'quiet', alive: 'checking in', restarting: 'restarting', stopped: 'stopped on that device', idle_config: `idle on a setting — ${h.remedy || 'see Settings'}`, cannot_fetch: 'stuck on old code — it cannot fetch from GitHub' }[h.state] || 'unknown';
|
|
392
396
|
return `<span class="ov-fact"><span class="rn-dot rn-dot--sm rn-dot--${HEALTH_DOT[h.state] || 'info'}" aria-hidden="true"></span> ${escapeHtml(h.host || r.host || '')} ${escapeHtml(label)}</span>`;
|
|
393
397
|
}).join('');
|
|
394
398
|
const paused = b.owner_paused === true;
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
21
21
|
<link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
|
|
22
22
|
<link rel="stylesheet" href="/builders/visor.css?v=2026-11-14-world">
|
|
23
|
-
<link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-11-
|
|
24
|
-
<link rel="stylesheet" href="/builders/shell-game.css?v=2026-11-
|
|
23
|
+
<link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-11-14-cloud">
|
|
24
|
+
<link rel="stylesheet" href="/builders/shell-game.css?v=2026-11-14-cloud">
|
|
25
25
|
<link rel="stylesheet" href="/builders/hall-ask.css">
|
|
26
26
|
<link rel="stylesheet" href="/builders/home-city.css?v=2026-11-14-world">
|
|
27
27
|
<link rel="stylesheet" href="/builders/home-gallery.css">
|
|
@@ -102,8 +102,8 @@
|
|
|
102
102
|
BEFORE builders.js so the chrome ids exist when events are wired. -->
|
|
103
103
|
<!-- task 1004785: the first-visit-of-the-day welcome (the tower). Before shell.js, which asks it
|
|
104
104
|
before the craft picker so the two never stack. Home only. -->
|
|
105
|
-
<script src="/builders/welcome-boot.js?v=2026-11-
|
|
106
|
-
<script src="/builders/shell-game.js?v=2026-11-
|
|
105
|
+
<script src="/builders/welcome-boot.js?v=2026-11-14-cloud" defer></script>
|
|
106
|
+
<script src="/builders/shell-game.js?v=2026-11-14-cloud" defer></script>
|
|
107
107
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers" defer></script>
|
|
108
108
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor" defer></script>
|
|
109
109
|
<script src="/builders/shell.js?v=2026-11-13-founding" defer></script>
|
|
@@ -126,6 +126,13 @@
|
|
|
126
126
|
sub: `It is on, but ${hold}.` };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// Checking in, but pinned to the code it has: it cannot reach the project on
|
|
130
|
+
// GitHub (task 1005202). Said the way "off or asleep" is, with the fix under it.
|
|
131
|
+
if (h.state === 'cannot_fetch') {
|
|
132
|
+
return { status: 'Stuck on old code', act: 'stop', label: 'Stop', tone: 'warn', busy: false, alt: null,
|
|
133
|
+
sub: `It is on, but it cannot fetch new code from GitHub. ${h.remedy || ''}`.trim() };
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
if (h.state === 'idle_config') {
|
|
130
137
|
return { status: 'Ready', act: 'stop', label: 'Stop', tone: 'warn', busy: false, alt: null,
|
|
131
138
|
sub: `It is on, but idle because ${h.why || 'of a setting'}. ${h.remedy || ''}`.trim() };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* shell-game.css — the game's start screen and load screen (task 1005143; shell-game.js start()),
|
|
2
|
-
in the Helm look (task 1005192, ADR 0402). Both stand over the
|
|
2
|
+
in the Helm look (task 1005192, ADR 0402). Both stand over the city in the clouds (task 1005308). The start
|
|
3
3
|
screen is one full-height night panel on the left, closed left and right by an ice hairline:
|
|
4
4
|
the mark and the project's name in the name voice, the five choices as full-width rows, one
|
|
5
5
|
line at the foot. The load screen is the same panel language, centred: the week in the name
|
|
@@ -18,11 +18,10 @@ html.gs-open { overflow: hidden; }
|
|
|
18
18
|
.gs--leaving { opacity: 0; pointer-events: none; }
|
|
19
19
|
.gs--load { align-items: center; justify-content: center; padding: var(--ds-space-lg) var(--ds-space-gutter-narrow); }
|
|
20
20
|
|
|
21
|
-
/* the still: the
|
|
21
|
+
/* the still: the city in the clouds (.cloud-scene, welcome-boot.css), covering the screen. The start screen leaves it bare (the panel is
|
|
22
22
|
its own ground); the load screen lays the scrim over it so the centred panel reads anywhere. */
|
|
23
23
|
.gs__art { position: absolute; inset: 0; }
|
|
24
24
|
.gs--load .gs__art::after { content: ""; position: absolute; inset: 0; background: var(--scrim); }
|
|
25
|
-
.gs__img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
26
25
|
|
|
27
26
|
/* the two panels: night over the art, an ice hairline left and right, no corner, no shadow */
|
|
28
27
|
.gs__menu, .gs__load {
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
|
|
88
88
|
// ---------- the start screen and the load screen (task 1005143, BONGOS-V3 R03) ----------
|
|
89
89
|
// A fresh sign-in opens on the start screen: Start, Options, Achievements, Extras, Quit over a
|
|
90
|
-
//
|
|
90
|
+
// the city in the clouds (task 1005308; the painting before it is retired). Start turns game mode on and passes through the load screen, where the project city
|
|
91
91
|
// builds itself up week by week under a tip, and then the world is there. welcome-boot.js calls
|
|
92
|
-
// start() on Home (where a sign-in lands) and hands in the
|
|
92
|
+
// start() on Home (where a sign-in lands) and hands in the scene's painter; this file draws both screens.
|
|
93
93
|
// Rules are in modules/hall-ui/records/start-screen.md.
|
|
94
94
|
var MENU = [
|
|
95
95
|
{ id: 'start', label: 'Start' },
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
}
|
|
156
156
|
// The Helm panel (task 1005192, ADR 0402): the mark and the name, the choices, one line at the foot.
|
|
157
157
|
function screenHtml(o) {
|
|
158
|
-
return '<div class="gs__art" aria-hidden="true"
|
|
158
|
+
return '<div class="gs__art" aria-hidden="true"></div>' +
|
|
159
159
|
'<div class="gs__menu">' +
|
|
160
160
|
'<div class="gs__top">' +
|
|
161
161
|
'<div class="gs__head">' + markHtml() +
|
|
@@ -369,8 +369,8 @@
|
|
|
369
369
|
return v === 'game' || v === 'classic' ? v : null;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
// Put the start screen up. `o`: { name,
|
|
373
|
-
//
|
|
372
|
+
// Put the start screen up. `o`: { name, scene, done, signIn } — the project's name, the painter of
|
|
373
|
+
// the city that stands behind both screens (scene(host), welcome-boot.js paintScene), what to do once the world is showing, and (signed out) how to sign
|
|
374
374
|
// in once a view is picked. True when it is up; false for a classic pick made before sign-in,
|
|
375
375
|
// which goes straight to the hall.
|
|
376
376
|
function start(o) {
|
|
@@ -387,6 +387,7 @@
|
|
|
387
387
|
root.setAttribute('aria-modal', 'true');
|
|
388
388
|
root.setAttribute('aria-labelledby', 'gs-title');
|
|
389
389
|
root.innerHTML = screenHtml(opts);
|
|
390
|
+
if (typeof opts.scene === 'function') { try { opts.scene(root.querySelector('.gs__art')); } catch (_) { /* the ground stands */ } }
|
|
390
391
|
root.addEventListener('click', onClick);
|
|
391
392
|
document.addEventListener('keydown', onKey, true);
|
|
392
393
|
document.body.appendChild(root);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* welcome-boot.css — the first-visit-of-the-day welcome (tasks 1004785, 1004818; welcome-boot.js).
|
|
2
|
-
The scene is the
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
The scene is the city in the clouds, drawn in the hour's sky (task 1005308; the circuit-city
|
|
3
|
+
painting it replaced is retired). Its ink follows day or night rather than the hall theme, so it
|
|
4
|
+
is fixed here rather than --ink. .cloud-scene is also the ground of the game's start and load
|
|
5
|
+
screens (shell-game.js), which this sheet shares the page with. */
|
|
5
6
|
html.wb-open { overflow: hidden; }
|
|
6
7
|
.wb {
|
|
7
8
|
position: fixed; inset: 0; z-index: 1000; overflow: hidden;
|
|
@@ -10,35 +11,21 @@ html.wb-open { overflow: hidden; }
|
|
|
10
11
|
}
|
|
11
12
|
.wb--day { color: #14161c; background: linear-gradient(180deg, #a9d6dc 0%, #f1d9d0 55%, #e9cfc6 100%); }
|
|
12
13
|
|
|
13
|
-
/* the
|
|
14
|
+
/* the scene: the city in the clouds (welcome-boot.js paintScene), on the hour's sky. It covers the
|
|
15
|
+
screen; the city stands on the bottom edge, so a tall screen shows more sky and a wide one crops it. */
|
|
14
16
|
.wb__art {
|
|
15
|
-
position: absolute;
|
|
16
|
-
width: max(100vw, calc(100vh * 1024 / 572)); aspect-ratio: 1024 / 572;
|
|
17
|
-
transform: translate(-50%, -50%);
|
|
17
|
+
position: absolute; inset: 0; z-index: 1;
|
|
18
18
|
transition: transform 2.2s cubic-bezier(.45, 0, .25, 1);
|
|
19
19
|
}
|
|
20
|
-
.wb__pan { position: absolute; inset: 0; transform-origin:
|
|
21
|
-
@keyframes wb-pan { from { transform: scale(1); } to { transform: scale(1.
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/* the motion: pulses climb the circuit towers, cars cross, and at night the lights blink */
|
|
27
|
-
.wb-pulses { opacity: 0; transition: opacity 1.6s ease-out; }
|
|
28
|
-
.wb--live .wb-pulses { opacity: 1; }
|
|
29
|
-
.wb-pulse { fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-dasharray: 7 100; stroke-dashoffset: 7; animation: wb-climb linear infinite; }
|
|
30
|
-
@keyframes wb-climb { to { stroke-dashoffset: -100; } }
|
|
31
|
-
.wb--night .wb-pulse { filter: drop-shadow(0 0 3px currentColor); }
|
|
32
|
-
.wb--day .wb-pulse { opacity: .8; stroke-width: 1.8; }
|
|
33
|
-
.wb-car { animation: wb-car linear infinite; }
|
|
34
|
-
@keyframes wb-car {
|
|
35
|
-
0% { transform: translate(0, 0); opacity: 0; }
|
|
36
|
-
10%, 88% { opacity: 1; }
|
|
37
|
-
100% { transform: translate(var(--wb-dx), var(--wb-dy)); opacity: 0; }
|
|
20
|
+
.wb__pan { position: absolute; inset: 0; transform-origin: 50% 70%; animation: wb-pan 40s ease-in-out infinite alternate; }
|
|
21
|
+
@keyframes wb-pan { from { transform: scale(1); } to { transform: scale(1.05) translateX(-1%); } }
|
|
22
|
+
.cloud-scene { background: linear-gradient(to bottom, var(--sky-now-top), var(--sky-now-mid) 46%, var(--sky-now-haze)); }
|
|
23
|
+
.cloud-scene__svg {
|
|
24
|
+
position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none;
|
|
25
|
+
background: radial-gradient(ellipse 70% 60% at 50% 72%, var(--sky-now-cloud), var(--sky-now-base) 30%, var(--sky-now-haze) 62%, transparent);
|
|
38
26
|
}
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
@keyframes wb-moon { from { opacity: .35; } to { opacity: .8; } }
|
|
27
|
+
.cloud-scene__svg .hc-stars { opacity: var(--sky-now-stars); }
|
|
28
|
+
|
|
42
29
|
.wb__cloud { position: absolute; z-index: 2; border-radius: 50%; background: rgba(255, 255, 255, .06); filter: blur(30px); animation: wb-drift linear infinite; pointer-events: none; }
|
|
43
30
|
.wb--day .wb__cloud { background: rgba(255, 255, 255, .35); filter: blur(28px); }
|
|
44
31
|
.wb__cloud--a { top: 8vh; width: 40vw; height: 7vh; animation-duration: 80s; animation-delay: -20s; }
|
|
@@ -71,7 +58,7 @@ html.wb-open { overflow: hidden; }
|
|
|
71
58
|
font-family: var(--font-display); font-size: clamp(18px, 2.6vw, 26px); font-weight: 300;
|
|
72
59
|
letter-spacing: -.01em; text-transform: lowercase; white-space: nowrap;
|
|
73
60
|
}
|
|
74
|
-
/* the mark's ink follows the
|
|
61
|
+
/* the mark's ink follows the hour: pale by night, dark by day; the moon stays salmon */
|
|
75
62
|
.wb__mark { --mark-ink: rgba(255, 255, 255, .88); }
|
|
76
63
|
.wb__planet { transform-box: fill-box; transform-origin: center; }
|
|
77
64
|
.wb__moonlet { transform-box: view-box; transform-origin: 100px 100px; }
|
|
@@ -137,7 +124,7 @@ html.wb-open { overflow: hidden; }
|
|
|
137
124
|
.wb__role small { display: block; margin-top: 3px; }
|
|
138
125
|
.wb__role i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 7px; }
|
|
139
126
|
|
|
140
|
-
/* the veil: white by day, black by night, lifting slowly once the
|
|
127
|
+
/* the veil: white by day, black by night, lifting slowly once the scene is painted */
|
|
141
128
|
.wb__veil { position: absolute; inset: 0; z-index: 6; pointer-events: none; background: #000; opacity: 1; transition: opacity 1.6s ease-out; }
|
|
142
129
|
.wb--day .wb__veil { background: #fff; }
|
|
143
130
|
.wb--lit .wb__veil { opacity: 0; }
|
|
@@ -145,7 +132,7 @@ html.wb-open { overflow: hidden; }
|
|
|
145
132
|
/* the way out: the view sinks past the rooftops and the welcome fades into the hall underneath;
|
|
146
133
|
bound for another room, the veil comes back instead */
|
|
147
134
|
.wb--leaving { pointer-events: none; }
|
|
148
|
-
.wb--leaving .wb__art { transform:
|
|
135
|
+
.wb--leaving .wb__art { transform: translateY(-30vh) scale(1.12); }
|
|
149
136
|
.wb--leaving .wb__banner { transform: translate(-50%, -40vh); }
|
|
150
137
|
.wb--done.wb--leaving .wb__after { opacity: 0; transform: translateY(24px); transition: opacity .5s, transform .5s; }
|
|
151
138
|
.wb--leaving:not(.wb--to-room) { opacity: 0; transition: opacity 1.8s ease-in .5s; }
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// welcome-boot.js — the welcome on a builder's first visit of the day (tasks 1004785, 1004818; idea 1001262).
|
|
2
2
|
//
|
|
3
|
-
// Home only. The scene is the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// Home only. The scene is the city in the clouds (task 1005196), drawn by home-city-draw.js and
|
|
4
|
+
// city-draw.js in the hour's sky (sky-time.js): the Cloud Bongos core and the museum standing in
|
|
5
|
+
// cloud, with a slow drift. It replaced the owner's circuit-city painting (a day and a
|
|
6
|
+
// night WebP still), retired everywhere by task 1005308 (owner, 2026-11-14), and with it the
|
|
7
|
+
// lights that were pinned to the painting's pixels. paintScene() is also what the game's start and
|
|
8
|
+
// load screens stand on (shell-game.js), so every screen before the hall shows the same city.
|
|
9
|
+
// The banner at the top is the logo, animated, and a "Built on cloudbongos.com" credit sits in the
|
|
10
|
+
// bottom-right corner (task 1004953). The ink follows the local time of day: day from 06:00 to
|
|
11
|
+
// 17:59, night otherwise. It opens from a white (day) or black (night) veil, then
|
|
12
|
+
// "Good morning, <name>" comes up with two choices. The way out is slow: the view sinks past the
|
|
13
|
+
// rooftops and fades into the hall underneath.
|
|
11
14
|
//
|
|
12
15
|
// Rules:
|
|
13
16
|
// - A signed-in builder sees it once per LOCAL day. A fresh sign-in (the sign-in click leaves a
|
|
@@ -22,10 +25,11 @@
|
|
|
22
25
|
// picker asked (OTBCraftMode.markAsked), so the two never stack.
|
|
23
26
|
// - Picking a role also sets the hall's craft mode. Governing is a seat (craft-mode.js), so the
|
|
24
27
|
// Owner role shows only when OTBCraftMode offers it.
|
|
25
|
-
// -
|
|
26
|
-
//
|
|
28
|
+
// - Without the city's painters (a page that does not load them) the hour's sky gradient stands alone,
|
|
29
|
+
// and the welcome works the same.
|
|
30
|
+
// - prefers-reduced-motion: no motion; the still city and the greeting at once, leaving is immediate.
|
|
27
31
|
//
|
|
28
|
-
// The pure parts (the day rule, the logo, the roles, the
|
|
32
|
+
// The pure parts (the day rule, the logo, the roles, the scene's node rewrite) are exported for
|
|
29
33
|
// tests/welcome_boot.mjs; tests/welcome_boot_browser.mjs runs the overlay itself in a real browser.
|
|
30
34
|
(function () {
|
|
31
35
|
'use strict';
|
|
@@ -33,8 +37,7 @@
|
|
|
33
37
|
var DAY_KEY = 'hall-welcome-day:';
|
|
34
38
|
var SIGNIN_KEY = 'hall-welcome-signin';
|
|
35
39
|
var SIGNIN_HREF = '/api/bongos/auth/web/start';
|
|
36
|
-
var
|
|
37
|
-
var ART_WAIT = 1500; // the veil lifts once the painting is in, or after this long
|
|
40
|
+
var ART_WAIT = 1500; // the veil lifts once the scene is painted, or after this long
|
|
38
41
|
var RUN_MS = 3400; // the city powering up before the greeting
|
|
39
42
|
var LEAVE_MS = 2400; // the way down into the hall
|
|
40
43
|
|
|
@@ -46,9 +49,9 @@
|
|
|
46
49
|
var h = now.getHours();
|
|
47
50
|
return h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
|
|
48
51
|
}
|
|
49
|
-
// Day is 06:00 to 17:59 local: a white veil and
|
|
52
|
+
// Day is 06:00 to 17:59 local: a white veil and dark ink. Night: a black veil and pale ink.
|
|
53
|
+
// (The scene's own colours follow the hour more finely: sky-time.js.)
|
|
50
54
|
function timeOfDay(now) { var h = now.getHours(); return h >= 6 && h < 18 ? 'day' : 'night'; }
|
|
51
|
-
function artFor(when) { return when === 'day' ? ART.day : ART.night; }
|
|
52
55
|
function nameOf(builder) {
|
|
53
56
|
return String((builder && (builder.display_name || builder.github_login)) || 'builder');
|
|
54
57
|
}
|
|
@@ -88,73 +91,61 @@
|
|
|
88
91
|
}
|
|
89
92
|
function roleById(id) { return ROLES.filter(function (r) { return r.id === id; })[0] || null; }
|
|
90
93
|
|
|
91
|
-
// ---------- the
|
|
92
|
-
//
|
|
93
|
-
// the
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
var TOWERS = [
|
|
97
|
-
{ x: 140, y: 168, w: 62, h: 118, glow: '#60a5fa' },
|
|
98
|
-
{ x: 280, y: 150, w: 58, h: 200, glow: '#22d3ee' },
|
|
99
|
-
{ x: 184, y: 296, w: 100, h: 196, glow: 'var(--accent)' },
|
|
100
|
-
{ x: 636, y: 222, w: 42, h: 140, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
|
|
101
|
-
{ x: 702, y: 212, w: 84, h: 190, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
|
|
102
|
-
{ x: 2, y: 196, w: 52, h: 150, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
|
|
103
|
-
{ x: 910, y: 410, w: 110, h: 160, glow: '#22d3ee' },
|
|
104
|
-
];
|
|
105
|
-
// The hover-car lanes on the right (the masts' blinking lights went in task 1005087).
|
|
106
|
-
var LANES = [
|
|
107
|
-
{ x1: 770, y1: 350, x2: 1040, y2: 362, dur: 5.5 },
|
|
108
|
-
{ x1: 1040, y1: 340, x2: 760, y2: 352, dur: 7 },
|
|
109
|
-
{ x1: 820, y1: 258, x2: 1040, y2: 254, dur: 6.5 },
|
|
110
|
-
];
|
|
111
|
-
var MOON = [665, 82];
|
|
112
|
-
function seeded(s) { return function () { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }; }
|
|
113
|
-
// Where each data pulse climbs: a vertical run inside a tower, bottom to top. Pure, so it is testable.
|
|
114
|
-
function pulseRuns(towers, perTower, seed) {
|
|
115
|
-
var rnd = seeded(seed || 7), out = [];
|
|
116
|
-
towers.forEach(function (t, ti) {
|
|
117
|
-
for (var i = 0; i < perTower; i++) {
|
|
118
|
-
var x = Math.round((t.x + 6 + rnd() * (t.w - 12)) * 10) / 10;
|
|
119
|
-
var y1 = Math.round((t.y + t.h * (0.75 + rnd() * 0.25)) * 10) / 10;
|
|
120
|
-
var y0 = Math.round((t.y + t.h * rnd() * 0.3) * 10) / 10;
|
|
121
|
-
out.push({ tower: ti, x: x, y0: y0, y1: y1, glow: t.glow, dur: Math.round((2.2 + rnd() * 2.4) * 100) / 100, delay: Math.round(rnd() * 400) / 100 });
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
|
|
94
|
+
// ---------- the scene: the city in the clouds (task 1005308) ----------
|
|
95
|
+
// Home's own painters draw it (window.OTBHomeCityDraw + window.GovCity), from an empty week: the
|
|
96
|
+
// Cloud Bongos core and the museum, standing in cloud, in the hour's --sky-now-* tokens. It is a
|
|
97
|
+
// backdrop, so every door the city carries on Home (a link, a tooltip, a goal) becomes a plain
|
|
98
|
+
// group, nothing in it takes focus, and its ids are prefixed so they never meet Home's own city's.
|
|
127
99
|
var NS = 'http://www.w3.org/2000/svg';
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
var
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// data pulses climbing the circuit towers (the whole set brightens as the city powers up)
|
|
145
|
-
var pulses = el('g', { 'class': 'wb-pulses' }, svg);
|
|
146
|
-
pulseRuns(TOWERS, 3, 7).forEach(function (r) {
|
|
147
|
-
el('line', { 'class': 'wb-pulse', x1: r.x, y1: r.y1, x2: r.x, y2: r.y0, pathLength: 100, stroke: night ? r.glow : '#ffffff',
|
|
148
|
-
style: 'color:' + r.glow + ';animation-duration:' + r.dur + 's;animation-delay:' + (-r.delay) + 's' }, pulses);
|
|
149
|
-
});
|
|
150
|
-
// hover cars, each a light with a short trail, crossing on its lane
|
|
151
|
-
LANES.forEach(function (l, i) {
|
|
152
|
-
var g = el('g', { 'class': 'wb-car', style: '--wb-dx:' + (l.x2 - l.x1) + 'px;--wb-dy:' + (l.y2 - l.y1) + 'px;animation-duration:' + l.dur + 's;animation-delay:' + (-i * 1.7) + 's' }, svg);
|
|
153
|
-
var dir = l.x2 > l.x1 ? -1 : 1;
|
|
154
|
-
el('line', { x1: l.x1, y1: l.y1, x2: l.x1 + dir * 26, y2: l.y1, stroke: night ? 'rgba(125,211,252,.55)' : 'rgba(255,255,255,.7)', 'stroke-width': 2, 'stroke-linecap': 'round' }, g);
|
|
155
|
-
el('circle', { cx: l.x1, cy: l.y1, r: 2.4, fill: night ? '#e0f2fe' : '#ffffff' }, g);
|
|
100
|
+
var SCENE_ID = 'cs-';
|
|
101
|
+
var DOOR_ATTRS = /^(href|tabindex|aria-label|data-tip|data-tip-body|data-goal|data-group|data-building|data-craft-door)$/;
|
|
102
|
+
var QUIET_RE = /\bhc-quiet\b/g, SPACES_RE = /\s+/g, URL_REF_RE = /url\(#/g;
|
|
103
|
+
// One shape node rewritten for the backdrop: { tag, attrs, kids, text } -> the same, door-free. Pure.
|
|
104
|
+
function sceneShape(n) {
|
|
105
|
+
var attrs = {};
|
|
106
|
+
Object.keys(n.attrs || {}).forEach(function (k) {
|
|
107
|
+
if (DOOR_ATTRS.test(k)) return;
|
|
108
|
+
var v = String(n.attrs[k]);
|
|
109
|
+
if (k === 'id') v = SCENE_ID + v;
|
|
110
|
+
else if (k === 'class') v = v.replace(QUIET_RE, '').replace(SPACES_RE, ' ').trim();
|
|
111
|
+
else v = v.replace(URL_REF_RE, 'url(#' + SCENE_ID);
|
|
112
|
+
if (k === 'class' && !v) return;
|
|
113
|
+
attrs[k] = v;
|
|
156
114
|
});
|
|
157
|
-
return
|
|
115
|
+
return { tag: n.tag === 'a' ? 'g' : n.tag, attrs: attrs, kids: (n.kids || []).map(sceneShape), text: n.text == null ? null : n.text };
|
|
116
|
+
}
|
|
117
|
+
// The scene as shapes, or null when the painters are not on the page. Pure given the painters.
|
|
118
|
+
function sceneShapes(Draw, Gov) {
|
|
119
|
+
if (!Draw || typeof Draw.citySvg !== 'function' || typeof Draw.cityModel !== 'function') return null;
|
|
120
|
+
var out = Draw.citySvg(Draw.cityModel({ weeks: [{}] }), 0, { gov: Gov || null });
|
|
121
|
+
return { viewBox: out.viewBox, nodes: out.nodes.map(sceneShape) };
|
|
122
|
+
}
|
|
123
|
+
function build(n) {
|
|
124
|
+
var e = document.createElementNS(NS, n.tag);
|
|
125
|
+
Object.keys(n.attrs).forEach(function (k) { e.setAttribute(k, n.attrs[k]); });
|
|
126
|
+
if (n.text != null) e.textContent = n.text;
|
|
127
|
+
n.kids.forEach(function (c) { e.appendChild(build(c)); });
|
|
128
|
+
return e;
|
|
129
|
+
}
|
|
130
|
+
// Paint the city into host (it covers it; the host's class gives it the hour's sky). True when
|
|
131
|
+
// the city is drawn; false leaves the sky alone, which is still the right picture.
|
|
132
|
+
function paintScene(host) {
|
|
133
|
+
if (!host || typeof document === 'undefined') return false;
|
|
134
|
+
host.classList.add('cloud-scene');
|
|
135
|
+
var sky = window.OTBSkyTime;
|
|
136
|
+
if (sky && typeof sky.apply === 'function') { try { sky.apply(document); } catch (_) { /* the tokens' night default stands */ } }
|
|
137
|
+
var shapes;
|
|
138
|
+
try { shapes = sceneShapes(window.OTBHomeCityDraw, window.GovCity); } catch (_) { shapes = null; }
|
|
139
|
+
if (!shapes) return false;
|
|
140
|
+
var svg = document.createElementNS(NS, 'svg');
|
|
141
|
+
svg.setAttribute('class', 'cloud-scene__svg');
|
|
142
|
+
svg.setAttribute('viewBox', shapes.viewBox);
|
|
143
|
+
svg.setAttribute('preserveAspectRatio', 'xMidYMax slice');
|
|
144
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
145
|
+
svg.setAttribute('focusable', 'false');
|
|
146
|
+
shapes.nodes.forEach(function (n) { svg.appendChild(build(n)); });
|
|
147
|
+
host.appendChild(svg);
|
|
148
|
+
return true;
|
|
158
149
|
}
|
|
159
150
|
|
|
160
151
|
// ---------- the overlay ----------
|
|
@@ -170,10 +161,7 @@
|
|
|
170
161
|
|
|
171
162
|
function bannerTag(builder) { return builder ? 'div' : 'header'; }
|
|
172
163
|
function overlayHtml(builder, now, when) {
|
|
173
|
-
return '<div class="wb__art" aria-hidden="true"><div class="wb__pan">' +
|
|
174
|
-
'<img class="wb__img" alt="" src="' + artFor(when) + '" decoding="async">' +
|
|
175
|
-
'<div class="wb__fx" id="wb-fx"></div>' +
|
|
176
|
-
'</div></div>' +
|
|
164
|
+
return '<div class="wb__art" aria-hidden="true"><div class="wb__pan" id="wb-clouds"></div></div>' +
|
|
177
165
|
'<div class="wb__cloud wb__cloud--a" aria-hidden="true"></div><div class="wb__cloud wb__cloud--b" aria-hidden="true"></div>' +
|
|
178
166
|
// For a visitor the welcome IS the page (the shell draws no header for them), so its logo
|
|
179
167
|
// banner is the page's one <header>; over a signed-in hall the shell's header stands, and the
|
|
@@ -224,7 +212,7 @@
|
|
|
224
212
|
if (first && first.focus) first.focus();
|
|
225
213
|
}
|
|
226
214
|
|
|
227
|
-
// The veil lifts once the
|
|
215
|
+
// The veil lifts once the scene is painted (or after ART_WAIT), then the city powers up.
|
|
228
216
|
function begin() {
|
|
229
217
|
if (!root || root.classList.contains('wb--lit')) return;
|
|
230
218
|
root.classList.add('wb--lit');
|
|
@@ -324,13 +312,9 @@
|
|
|
324
312
|
document.addEventListener('keydown', onKey, true);
|
|
325
313
|
document.body.appendChild(root);
|
|
326
314
|
document.documentElement.classList.add('wb-open');
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
var go1 = function () { if (window.requestAnimationFrame) window.requestAnimationFrame(function () { window.requestAnimationFrame(begin); }); else begin(); };
|
|
331
|
-
img.addEventListener('load', go1);
|
|
332
|
-
img.addEventListener('error', function () { img.hidden = true; if (root) root.classList.add('wb--bare'); go1(); });
|
|
333
|
-
if (img.complete && img.naturalWidth) go1();
|
|
315
|
+
// No city painters on the page: the hour's sky stands alone; the welcome goes on regardless.
|
|
316
|
+
if (!paintScene(root.querySelector('#wb-clouds'))) root.classList.add('wb--bare');
|
|
317
|
+
if (window.requestAnimationFrame) window.requestAnimationFrame(function () { window.requestAnimationFrame(begin); });
|
|
334
318
|
at(ART_WAIT, begin);
|
|
335
319
|
var skip = root.querySelector('[data-wb-skip]');
|
|
336
320
|
if (skip && skip.focus) skip.focus();
|
|
@@ -378,7 +362,7 @@
|
|
|
378
362
|
// The hall's front opens on Game or Classic (task 1005219): the pick is kept, the visitor
|
|
379
363
|
// signs in, and comes back to that menu (shell-game.js). Without that file, the welcome.
|
|
380
364
|
if (canChoose && game.start({
|
|
381
|
-
name: projectName(window.__BRANDING__),
|
|
365
|
+
name: projectName(window.__BRANDING__), scene: paintScene,
|
|
382
366
|
signIn: function () { markSigningIn(); go(SIGNIN_HREF); },
|
|
383
367
|
})) return true;
|
|
384
368
|
show(null);
|
|
@@ -398,11 +382,11 @@
|
|
|
398
382
|
shown = true;
|
|
399
383
|
var c = craft();
|
|
400
384
|
// A fresh sign-in, or a new visit to Home, opens on the game's start screen instead (tasks
|
|
401
|
-
// 1005143, 1005219; shell-game.js): the same
|
|
385
|
+
// 1005143, 1005219; shell-game.js): the same city behind it, and the craft picker asked once
|
|
402
386
|
// the world is showing. Classic picked before signing in: no screen, straight to the hall.
|
|
403
387
|
if ((fresh || ask) && canChoose) {
|
|
404
388
|
if (game.start({
|
|
405
|
-
name: projectName(window.__BRANDING__),
|
|
389
|
+
name: projectName(window.__BRANDING__), scene: paintScene,
|
|
406
390
|
done: function () { if (c && typeof c.onAccess === 'function') c.onAccess(access, builder); },
|
|
407
391
|
})) return true;
|
|
408
392
|
if (fresh) return false;
|
|
@@ -413,9 +397,9 @@
|
|
|
413
397
|
}
|
|
414
398
|
|
|
415
399
|
var api = {
|
|
416
|
-
DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES,
|
|
400
|
+
DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, SCENE_ID: SCENE_ID,
|
|
417
401
|
localDay: localDay, firstVisitToday: firstVisitToday, greetingFor: greetingFor, nameOf: nameOf, timeOfDay: timeOfDay,
|
|
418
|
-
|
|
402
|
+
projectName: projectName, logoHtml: logoHtml, BUILT_ON: BUILT_ON, rolesFor: rolesFor, sceneShape: sceneShape, sceneShapes: sceneShapes, paintScene: paintScene,
|
|
419
403
|
onAccess: onAccess, finish: finish, close: close,
|
|
420
404
|
};
|
|
421
405
|
if (typeof window !== 'undefined') { window.OTBWelcome = api; watchSignInLinks(); }
|
|
@@ -4,23 +4,23 @@ Covers: Home (`shell-game.js` `start()`, `shell-game.css`, the hand-off in `welc
|
|
|
4
4
|
|
|
5
5
|
## What it settled
|
|
6
6
|
|
|
7
|
-
- **A fresh sign-in opens on the start screen, on Home.** Home is where a sign-in lands (`postLoginLanding`), and `welcome-boot.js` already knows a fresh sign-in from a return visit (the flag the sign-in click leaves). On one, `OTBWelcome.onAccess` calls `OTBShellGame.start({ name,
|
|
7
|
+
- **A fresh sign-in opens on the start screen, on Home.** Home is where a sign-in lands (`postLoginLanding`), and `welcome-boot.js` already knows a fresh sign-in from a return visit (the flag the sign-in click leaves). On one, `OTBWelcome.onAccess` calls `OTBShellGame.start({ name, scene, done })` in place of its own greeting. The day's first visit without a sign-in keeps the welcome; a signed-out visitor keeps the welcome. A sign-in that returns to a deep link or another page goes straight there, as the welcome always did. `shell.js` is untouched.
|
|
8
8
|
- **Five choices, one column: Start, Options, Achievements, Extras, Quit.** Start is the one filled pill. Options is a link to `/settings`, which IS the one options menu since task 1005144 (`records/options-menu.md`); Achievements is `/profile`. Extras opens three links under the menu (the thinking sky, the atlas, the docs). Quit calls `window.close()`; a browser only closes a tab a script opened, so when the tab is still there 300ms later the screen says "Close it to quit" instead of doing nothing.
|
|
9
9
|
- **Start turns game mode on** (`set(true)`), then shows the load screen. Nothing else in the hall's interface sets it.
|
|
10
|
-
- **The load screen is the still, a tip, and the city building up.** The still is the
|
|
10
|
+
- **The load screen is the still, a tip, and the city building up.** The still is the city in the clouds in the hour's sky, painted by `welcome-boot.js` `paintScene`, which is handed in as `scene` (this file draws no city of its own for it; task 1005308). In a card of the hall's own tokens, the project city is drawn by `home-city-draw.js` `citySvg` from the one bounded read Home already uses (`GET /home/city?weeks=10`), at 31 stops from the oldest week to now, about three seconds, with "The city, N weeks ago" under it. Each stop is a whole drawing set as state; nothing animates in JavaScript. With reduced motion it is the finished city at once. With no city to draw (the read failed, the scripts are missing) the still and the tip stand for 1.4s. Either way the world opens.
|
|
11
11
|
- **Tips rotate** (`localStorage 'hall-game-tip'` counts them), so each load shows the next one. A tip must be true of the hall today; the first one is the Escape rule from `game-mode.md` (rewritten by task 1005144, and again by task 1005250: Escape opens the game menu).
|
|
12
12
|
- **The craft picker waits for the world.** While the start screen is up the shell asks no picker (`onAccess` returned true). `done` runs once the screen is gone and asks `OTBCraftMode.onAccess` then.
|
|
13
13
|
- **Keys.** While either screen is up it keeps every key: the arrows and Tab walk the menu, Escape folds Extras away and does nothing else, so the shell's Escape cannot flip the bars underneath. Once the world shows, the last Escape opens the game menu (task 1005250; the options menu until then); the screen's capture listener calls `preventDefault`, so the menu never opens from under it.
|
|
14
|
-
- **The look is Helm (task 1005192, [ADR 0402](../../../docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md)).** The start screen is one full-height night panel over the
|
|
15
|
-
- **The load screen is the same panel, centred** over the
|
|
14
|
+
- **The look is Helm (task 1005192, [ADR 0402](../../../docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md)).** The start screen is one full-height night panel over the city in the clouds (`--night-glass`, an ice hairline left and right at `--line-strong`), `--helm-panel-w` wide. On a desk it stands off the left edge by `--helm-inset`; that token is a `clamp`, so on a phone the panel is the whole width and the sheet still has no `@media`. In it: the mark (the project's logo when it has one) over the name in the name voice; Start as a full-width accent bar under `--accent-on` that names its key ("Enter", drawn by CSS so the choice's own text stays "Start"); the other four as quiet rows on `--rule-soft` hairlines; and at the foot, over the strong hairline, the tip the load screen will show next (`#gs-line`), with the Quit note under it when the browser keeps the tab. The rows are list rows in a panel, so they are bars, not cut buttons. A row's focus is the inset two-tone ring (`--press-focus`), which is what makes focus visible on the accent bar.
|
|
15
|
+
- **The load screen is the same panel, centred** over the city in the clouds under the hall's `--scrim`: the week in the name voice (`#gs-when`), one segment per week of the history (`#gs-segs`, the shared `.segs` / `.seg`; filled up to the week being shown, counted the way the sentence words it, and hidden from a screen reader because the sentence says the same), the city, and the tip under a hairline. With no city there are no segments.
|
|
16
16
|
- **The literal floor is zero.** Every colour, size, corner, shadow, face and measure in `shell-game.css` is a hall-tier token (`style.css`); `tests/hall_start_screen.mjs` fails on a typed one. The fade into the world is one CSS opacity transition, stopped by the hall's reduced-motion rule.
|
|
17
17
|
|
|
18
18
|
## For the tasks after this one
|
|
19
19
|
|
|
20
20
|
- R04 (task 1005144): point Options at the options menu and move Achievements/Extras targets if those pages lose their interfaces. Escape on the start screen is free for the menu to use.
|
|
21
|
-
- `shell-game.js` is loaded on every hall page but only Home calls `start()`; `shell-game.css` is linked on Home only. A second page that wants the start screen links the sheet and calls `start()` with its own
|
|
21
|
+
- `shell-game.js` is loaded on every hall page but only Home calls `start()`; `shell-game.css` is linked on Home only. A second page that wants the start screen links the sheet and calls `start()` with its own `scene` painter.
|
|
22
22
|
|
|
23
|
-
-
|
|
23
|
+
- Since task 1005308 (owner, 2026-11-14) both screens stand on the city in the clouds from task 1005196; the circuit-city painting is retired everywhere.
|
|
24
24
|
|
|
25
25
|
Tests: `tests/hall_start_screen.mjs` (the hand-off and pure parts in a vm; both screens, the panel's place at desk and phone widths, the segments, Quit, the no-city and reduced-motion paths in a real browser).
|
|
26
26
|
|