@bongos/core 1.24.88 → 1.24.89

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.
@@ -878,7 +878,7 @@ html[data-theme="dark"] .brand-mark__mark { --mark-ink: #e2e1e4; }
878
878
  with the count over its word, the one accent button (its name may take two lines; its key is in the sheet), and of
879
879
  the key hints only Actions, as its dots (every other hint is a row in its sheet). */
880
880
  html[data-game="on"] #visor { --visor-inset-bottom: 52px; }
881
- html[data-game="on"] .visor__place, html[data-game="on"] .visor__notch, html[data-game="on"] .visor__frame--top, html[data-game="on"] .visor__name { display: none; }
881
+ html[data-game="on"] .visor__place, html[data-game="on"] .visor__progress, html[data-game="on"] .visor__notch, html[data-game="on"] .visor__frame--top, html[data-game="on"] .visor__name { display: none; }
882
882
  html[data-game="on"] .visor__foot { grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--ds-space-hair); }
883
883
  html[data-game="on"] .visor__me, html[data-game="on"] .visor__hints { transform: none; }
884
884
  html[data-game="on"] .visor__me { gap: var(--ds-space-hair); }
@@ -308,9 +308,10 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
308
308
  /* task 1005148: with the visor up (html.visor-on, game mode; visor.js), the sky's own layers keep
309
309
  clear of the glass, as ADR 0399 §2.3 asks of a scene's pop-ups: the top row, the composer, the
310
310
  jump field and the two side panels step below the compass and the place line, and the foot and
311
- the tour rise above the visor's corner pills. Outside game mode nothing here applies. */
312
- html.visor-on .top { padding-top: calc(60px + env(safe-area-inset-top, 0px)); }
313
- html.visor-on .composer, html.visor-on .jump { top: calc(116px + env(safe-area-inset-top, 0px)); }
314
- html.visor-on .panel, html.visor-on .room { top: calc(180px + env(safe-area-inset-top, 0px)); bottom: calc(200px + env(safe-area-inset-bottom, 0px)); }
311
+ the tour rise above the visor's corner pills. Task 1005203 put the version's progress under
312
+ the place, so the top offsets grew by its 24px. Outside game mode nothing here applies. */
313
+ html.visor-on .top { padding-top: calc(84px + env(safe-area-inset-top, 0px)); }
314
+ html.visor-on .composer, html.visor-on .jump { top: calc(140px + env(safe-area-inset-top, 0px)); }
315
+ html.visor-on .panel, html.visor-on .room { top: calc(204px + env(safe-area-inset-top, 0px)); bottom: calc(200px + env(safe-area-inset-bottom, 0px)); }
315
316
  html.visor-on .foot { padding-bottom: calc(132px + env(safe-area-inset-bottom, 0px)); }
316
317
  html.visor-on .tour, html.visor-on .start { bottom: calc(210px + env(safe-area-inset-bottom, 0px)); }
@@ -13,7 +13,7 @@
13
13
  html[data-game="on"] .visor:not([hidden]) {
14
14
  display: block; position: fixed; inset: 0; z-index: 4; pointer-events: none;
15
15
  /* where the glass sits, so a scene's pop-up can keep clear of it (ADR 0399 §2.3) */
16
- --visor-inset-top: 64px; --visor-inset-bottom: 76px;
16
+ --visor-arc: 64px; --visor-inset-top: 88px; --visor-inset-bottom: 76px;
17
17
  --visor-inset-right: max(var(--ds-space-gutter-narrow), var(--helm-inset)); --visor-inset-left: var(--visor-inset-right);
18
18
  color: var(--ink); font-family: var(--font-body);
19
19
  }
@@ -22,14 +22,14 @@ html[data-game="on"] .visor:not([hidden]) {
22
22
  top arc, its notch and the place all sit inside the top inset, above where a scene starts its
23
23
  own top row; the foot arc runs under the foot row. */
24
24
  .visor__frame { position: absolute; left: 0; width: 100%; overflow: visible; }
25
- .visor__frame--top { top: calc(var(--visor-inset-top) - 50px); height: 42px; }
25
+ .visor__frame--top { top: calc(var(--visor-arc) - 50px); height: 42px; }
26
26
  .visor__frame--foot { bottom: calc(var(--visor-inset-bottom) - 44px); height: 18px; }
27
27
  .visor__frame path { vector-effect: non-scaling-stroke; }
28
28
  .visor__line { fill: none; stroke: var(--ink-soft); stroke-opacity: .6; stroke-width: var(--line-strong); }
29
29
  .visor__line--under { stroke: var(--bg); stroke-opacity: 1; stroke-width: 5px; }
30
30
  .visor__dot { fill: var(--ink-soft); stroke: var(--bg); stroke-width: var(--line-strong); }
31
31
  .visor__notch, .visor__sight { position: absolute; left: 50%; transform: translate(-50%, -50%); overflow: visible; }
32
- .visor__notch { top: calc(var(--visor-inset-top) - 36px); width: 12px; height: 9px; }
32
+ .visor__notch { top: calc(var(--visor-arc) - 36px); width: 12px; height: 9px; }
33
33
  .visor__sight { top: 50%; width: 24px; height: 24px; }
34
34
 
35
35
  /* type over the scene stands on a small plate of the night, cut like everything pressable and
@@ -42,11 +42,29 @@ html[data-game="on"] .visor:not([hidden]) {
42
42
 
43
43
  /* the place, under the top arc */
44
44
  .visor__place {
45
- position: absolute; left: 50%; top: calc(var(--visor-inset-top) - 26px); transform: translateX(-50%); margin: 0;
45
+ position: absolute; left: 50%; top: calc(var(--visor-arc) - 26px); transform: translateX(-50%); margin: 0;
46
46
  font-size: var(--ds-text-2xl); letter-spacing: var(--name-space-wide); line-height: 1.2;
47
47
  color: var(--ink-soft); white-space: nowrap; padding: 0 var(--ds-space-xs);
48
48
  }
49
49
 
50
+ /* the version's progress under the place (task 1005203): the kit's four segments, then the same
51
+ reading in words (a row of bars is never the only reading). Done quarters are ice, the quarter
52
+ being built is the accent, the rest are outlines; each segment wears an outline of the night so it
53
+ reads over a midday sky. It sits under the arc band, inside the top inset (--visor-inset-top). */
54
+ .visor__progress {
55
+ position: absolute; left: 50%; top: calc(var(--visor-arc) + 6px); transform: translateX(-50%);
56
+ display: flex; align-items: center; gap: var(--ds-space-xs); white-space: nowrap;
57
+ }
58
+ .visor__segs { flex-wrap: nowrap; }
59
+ .visor__segs .seg { border-color: var(--ink-soft); outline: var(--line-strong) solid var(--night-plate); }
60
+ .visor__segs .seg--done { background: var(--ink-soft); border-color: transparent; }
61
+ .visor__segs .seg--now { background: var(--accent); border-color: transparent; }
62
+ .visor__progress-words {
63
+ margin: 0; font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case);
64
+ font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1; color: var(--ink-soft);
65
+ padding: var(--ds-space-hair) var(--ds-space-xs); background: var(--night-plate); clip-path: var(--cut-path-sm);
66
+ }
67
+
50
68
  /* the foot row: you on the left, the one thing to press in the middle, the key hints on the
51
69
  right. The two side groups lean in a little, as the glass curves. */
52
70
  .visor__foot {
@@ -90,6 +90,39 @@
90
90
  var s = String(Math.abs(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
91
91
  return (v < 0 ? '−' : '') + s + (Math.abs(v) === 1 ? ' credit' : ' credits');
92
92
  }
93
+ // ---------- the version's progress (task 1005203) ----------
94
+ // Four segments under the place: the version the project is building, in quarters. The
95
+ // owner chose it (2026-11-11): the same number /status reports, never an invented one. The
96
+ // version is the starred focus while it is still open, else the first one building; with
97
+ // neither (or the read failing) there are no segments, never empty ones.
98
+ var progress = null; // { id, name, pct } once read
99
+ function pickVersion(rows, focusId) {
100
+ if (!Array.isArray(rows)) return null;
101
+ var open = function (r) { return r && r.status !== 'shipped' && r.status !== 'frozen'; };
102
+ var hit = null;
103
+ for (var i = 0; i < rows.length && !hit; i++) if (focusId && rows[i] && rows[i].version_id === focusId && open(rows[i])) hit = rows[i];
104
+ for (var j = 0; j < rows.length && !hit; j++) if (rows[j] && rows[j].status === 'building') hit = rows[j];
105
+ if (!hit) return null;
106
+ var pct = Number(hit.percent_complete);
107
+ if (!isFinite(pct)) return null;
108
+ return { id: String(hit.version_id || ''), name: String(hit.name || ''), pct: Math.max(0, Math.min(100, Math.round(pct))) };
109
+ }
110
+ // Each segment is a quarter: a quarter wholly built is done; the quarter being built now is
111
+ // the accent (it is where a claim lands); the rest are outlines. Pure, for the tests.
112
+ function segStates(pct) {
113
+ var out = [], full = Math.floor(Math.max(0, Math.min(100, pct)) / 25);
114
+ for (var i = 0; i < 4; i++) out.push(i < full ? 'done' : (i === full ? 'now' : 'todo'));
115
+ return out;
116
+ }
117
+ function readProgress() {
118
+ if (typeof fetch !== 'function') return;
119
+ var get = function (u) { return fetch(u, { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }, function () { return null; }); };
120
+ Promise.all([get('/api/bongos/versions/progress'), get('/api/bongos/versions/focus')]).then(function (res) {
121
+ progress = pickVersion(res[0] && res[0].progress, res[1] && res[1].focus_version_id);
122
+ if (progress) render();
123
+ });
124
+ }
125
+
93
126
  // A link on the hall's own host: '/work' on the builders host, '/builders/work' on the apex.
94
127
  function hallHref(p) {
95
128
  var here = String((window.location && window.location.pathname) || '');
@@ -150,6 +183,7 @@
150
183
  var n = credits();
151
184
  return frameSvg() +
152
185
  (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') +
186
+ progressHtml() +
153
187
  '<div class="visor__foot">' +
154
188
  '<div class="visor__me">' +
155
189
  (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="250" height="290" decoding="async">' : '') +
@@ -165,6 +199,16 @@
165
199
  '</div>' +
166
200
  '</div>';
167
201
  }
202
+ // The kit's segments (controls.css .seg), always beside the words that say the same thing.
203
+ function progressHtml() {
204
+ if (!progress) return '';
205
+ var segs = segStates(progress.pct).map(function (s) { return '<li class="seg' + (s === 'todo' ? '' : ' seg--' + s) + '"></li>'; }).join('');
206
+ var said = progress.id + (progress.name ? ', ' + progress.name : '') + ': ' + progress.pct + '% built';
207
+ return '<div class="visor__progress" data-visor-progress title="' + esc(said) + '">' +
208
+ '<ol class="segs visor__segs" aria-hidden="true">' + segs + '</ol>' +
209
+ '<p class="visor__progress-words"><span class="sr-only">' + esc(said) + '</span>' +
210
+ '<span aria-hidden="true">' + esc(progress.id) + ' · ' + progress.pct + '%</span></p></div>';
211
+ }
168
212
  function sheetHtml(id) {
169
213
  var rows = actionsFor(id).map(function (a) {
170
214
  return '<li><a class="visor__row" data-visor-act="' + esc(a.id) + '" ' + actionAttrs(a) + '><span>' + esc(a.label) + '</span>' + keyHtml(a.key) + '</a></li>';
@@ -319,6 +363,7 @@
319
363
  document.addEventListener('otbcraft:me', render);
320
364
  document.addEventListener('otb:craft', render);
321
365
  render();
366
+ readProgress();
322
367
  }
323
368
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
324
369
  else start();
@@ -335,5 +380,7 @@
335
380
  keyFor: keyFor,
336
381
  creditsText: creditsText,
337
382
  character: character,
383
+ pickVersion: pickVersion,
384
+ segStates: segStates,
338
385
  };
339
386
  })();
@@ -4,6 +4,7 @@ Covers: every shelled hall page (`shell-game.js`, `shell.js`). Spec: `docs/specs
4
4
 
5
5
  ## What it settled
6
6
 
7
+ - **The visor is the control, the scene is the view ([ADR 0403](../../../docs/adr/0403-in-game-mode-the-visor-is-the-control-and-the-scene-is-the-view.md), owner 2026-11-11).** Every main call to action in game mode is on the visor; a game place's scene shows the project and can be moved through, but carries no buttons of its own. A scene hands its actions to the visor (`OTBVisor.scene`). A button in a scene is a defect.
7
8
  - **Game mode is the existing full-screen switch, made hall-wide. There is no second full-screen mode.** The switch was already on every page (task 1004430) but remembered per page, so a reader had to turn it on 33 times. Game mode is one `localStorage` key, `hall-game`; while it is `on`, `shell.js` `storedFullscreen()` answers yes for every page, whatever that page remembers. What hides, the corner button and the Escape hand-off are the shell's, unchanged.
8
9
  - **`shell-game.js` holds the mode and nothing else**: `window.OTBShellGame` = `on()`, `set(bool)`, `hint()`. It stamps `html[data-game="on"]` for a sheet or script that wants to read the mode. `shell.js` reads it through a fallback object, so a page without the file is never in game mode. It is its own file because `shell.js` sits on the 1,500-line ratchet; the start screen and options menu (R03, R04) are expected to grow here, not in `shell.js`.
9
10
  - **Load order**: `shell-game.js` before `shell-trail.js` / `craft-mode.js` / `shell.js`, loaded the same way (deferred or not) as the shell on that page. Test-pinned.
@@ -2,16 +2,21 @@
2
2
 
3
3
  Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thinking`, and the three scene registrations (`sky-interaction.js`, `city.js`, `home-city.js`). Spec: `docs/specs/bongos-v3-game-conversion.md` C2 `you-are-a-character-in-first-person`. Game mode itself is in `game-mode.md`.
4
4
 
5
+ ## The rule every game page keeps (ADR 0403, owner 2026-11-11)
6
+
7
+ The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet.
8
+
5
9
  ## The minimal visor (Helm look 3, task 1005195)
6
10
 
7
11
  The glass was redrawn to the owner's mock-up "Cloud city: in the visor, minimal". Look and layout only: every action, key, id, aria attribute, copy string and data source in `visor.js` is as task 1005148 left it. Where this section and the older bullets below disagree about the look or where a part sits, this section is current.
8
12
 
9
13
  - **What is on the glass.** Two thin arcs (one along the top, one under the foot row), a notch under the top arc, a ring-and-dot sight at the centre, the place in the name voice under the top arc, and one foot row: the sprite in a cut box with the character's name and the credits (white count, ice word); the one accent button, cut corner, with its key; the quiet key hints (a key in a small box and a name, no box round the pair). The compass cells, the side lines, the centre cross and the round pills are gone.
10
14
  - **Where each control went.** The main action is the accent button, centred. The character's other actions (after the game-places filter) are key hints on the right; they were rows in the sheet only. **Actions (A)** is demoted from a top-right pill to a key hint and still opens the same sheet, which now stands over the hints. **Options (Esc)** is the last hint, the same link the sheet's last row is. Nothing was removed.
11
- - **In the mock-up, not built, because the visor has no data for it:** the four progress segments, the marker and name on the building in view (the visor owns no camera, ADR 0399 §2.2), and the party bars with a speaker's name and a Voice key. Each is a scene or a later task handing the visor real data; none is drawn empty.
15
+ - **The four progress segments (task 1005203).** Under the place: the version the project is building, in quarters, then the same reading in words (`BONGOS-V2 · 89%`; the screen reader hears the version's name too). The owner chose the measure (2026-11-11): the version's `percent_complete` from the public `GET /versions/progress`, the number /status reports. The version is the starred focus (`GET /versions/focus`) while it is still open, else the first one `building`; with neither, or a failed read, no segments. A quarter wholly built is the soft ink, the quarter being built is the accent, the rest are outlines; each wears an outline of the night, and the words stand on a night plate (`--night-plate`, the rule task 1005196 set), for a midday sky. The arc band is `--visor-arc` (64px) and the scene inset grew to 88px (`--visor-inset-top`), so the sky's top offsets in `thinking.css` grew by 24px. Hidden on a phone, with the place. Harness fixture: `versions__progress.json`.
16
+ - **In the mock-up, not built, because the visor has no data for it:** the marker and name on the building in view (the visor owns no camera, ADR 0399 §2.2), and the party bars with a speaker's name and a Voice key. Each is a scene or a later task handing the visor real data; none is drawn empty.
12
17
  - **It reads over any sky, with no per-hour style, and it darkens none of it** (task 1005196, owner 2026-11-11: "I don't like how dark the edges of the view are"). The glass paints nothing of its own: the bands of `--night-glass` at its top and bottom edge are gone, and so is the dark halo round its type. The marks carry themselves. The place, each key hint and your name and credits stand on a small cut plate of the night (`--night-plate`, the ground at 88%), sized to the words; every line is drawn twice (a wider solid stroke of the night under the ice hairline, `.visor__line--under`), so an arc is ice on night and a dark line on cream cloud; a key sits on a chip of the night. One new token, `--night-plate`. Tried first and dropped: a 4px night stroke round each letter with no plate, which filled the condensed capitals in.
13
18
  - **A phone.** The shared phone block in `style.css` (no breakpoint of the visor's own): the top arc, its notch and the place step aside (the top of a phone is the scene's and the shell's); the foot row stays one line with nothing leaning (the sprite, the count over its word, the accent button whose name may wrap and whose key is left to the sheet, and of the hints only Actions, as three dots on a night chip). Between a phone and a wide desk the hints wrap onto a second line, right-aligned, rather than run into the button.
14
- - **Scenes.** The top arc, the notch and the place sit inside the top 64px (`--visor-inset-top`), the band the sky already leaves free, so no scene's top row is struck through. Home no longer shifts its greeting row left (there is no pill top-right to clear). The city's door rises above the foot row at every width, because the accent button is centred where the door stood. The sky's `visor-on` offsets are unchanged.
19
+ - **Scenes.** The top arc, the notch and the place sit inside the top 64px (`--visor-arc`; the segments under them take the inset to 88px, `--visor-inset-top`), so no scene's top row is struck through. Home no longer shifts its greeting row left (there is no pill top-right to clear). The city's door rises above the foot row at every width, because the accent button is centred where the door stood. The sky's `visor-on` offsets are unchanged.
15
20
  - **Looking at it.** The stand-in scene in `tests/hall_visor.mjs` over the hall's real sheets; put a bright gradient behind it to check midday.
16
21
 
17
22
  ## What it settled (task 1005148)
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.88",
3
+ "version": "1.24.89",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.88",
9
+ "version": "1.24.89",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.88",
3
+ "version": "1.24.89",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11631,5 +11631,11 @@
11631
11631
  "id": "1005188",
11632
11632
  "text": "Pressing Escape to close a building card on the city page no longer also brings the sidebar back."
11633
11633
  }
11634
+ ],
11635
+ "1.24.89": [
11636
+ {
11637
+ "id": "1005203",
11638
+ "text": "In the game, the visor now shows four small bars for how much of the current version is built, and every main button in the game now belongs on the visor."
11639
+ }
11634
11640
  ]
11635
11641
  }
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.88'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.89'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -66,6 +66,33 @@ test('a key means an action only when nothing else spent it', () => {
66
66
  assert.equal(V.keyFor('artisan', ev('Escape')), null, 'the ambient glass never takes Escape (ADR 0399 §2.4)');
67
67
  });
68
68
 
69
+ // task 1005203: the four segments are the version the project is building, in quarters.
70
+ test('progress: the starred version while open, else the one building, else none', () => {
71
+ const V = loadPure();
72
+ const plain = (x) => JSON.parse(JSON.stringify(x));
73
+ const rows = [
74
+ { version_id: 'V1', name: 'One', status: 'shipped', percent_complete: 100 },
75
+ { version_id: 'V2', name: 'Two', status: 'building', percent_complete: 89 },
76
+ { version_id: 'V3', name: 'Three', status: 'planning', percent_complete: 39 },
77
+ ];
78
+ assert.deepEqual(plain(V.pickVersion(rows, 'V3')), { id: 'V3', name: 'Three', pct: 39 }, 'the starred version wins while it is open');
79
+ assert.equal(V.pickVersion(rows, 'V1').id, 'V2', 'a starred version that shipped falls back to the one building');
80
+ assert.equal(V.pickVersion(rows, null).id, 'V2');
81
+ assert.equal(V.pickVersion(rows.filter((r) => r.status !== 'building'), null), null, 'nothing building: no segments, never empty ones');
82
+ assert.equal(V.pickVersion(null, 'V2'), null, 'a failed read draws nothing');
83
+ assert.equal(V.pickVersion([{ version_id: 'V9', status: 'building' }], null), null, 'no percent: no invented one');
84
+ });
85
+
86
+ test('progress: each segment is a quarter, and only the quarter being built is the accent', () => {
87
+ const V = loadPure();
88
+ const s = (p) => JSON.parse(JSON.stringify(V.segStates(p)));
89
+ assert.deepEqual(s(0), ['now', 'todo', 'todo', 'todo']);
90
+ assert.deepEqual(s(24), ['now', 'todo', 'todo', 'todo']);
91
+ assert.deepEqual(s(25), ['done', 'now', 'todo', 'todo']);
92
+ assert.deepEqual(s(89), ['done', 'done', 'done', 'now']);
93
+ assert.deepEqual(s(100), ['done', 'done', 'done', 'done'], 'a finished version has nothing left to build');
94
+ });
95
+
69
96
  test('credits, never a payout: the count reads as credits, with thousands grouped', () => {
70
97
  const V = loadPure();
71
98
  assert.equal(V.creditsText(56021), '56,021 credits');