@bongos/core 1.24.82 → 1.24.84

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.
Files changed (59) hide show
  1. package/.bongos-core.json +63 -63
  2. package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +4 -0
  3. package/docs/copy-inventory.md +4 -4
  4. package/docs/copy-registry.json +6 -6
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-readings.json +42 -42
  7. package/modules/hall-ui/public/ai-studio.html +1 -1
  8. package/modules/hall-ui/public/atlas.html +1 -1
  9. package/modules/hall-ui/public/blockers.html +1 -1
  10. package/modules/hall-ui/public/board-room.html +1 -1
  11. package/modules/hall-ui/public/city.css +3 -2
  12. package/modules/hall-ui/public/city.html +4 -4
  13. package/modules/hall-ui/public/collab.html +1 -1
  14. package/modules/hall-ui/public/commands.html +1 -1
  15. package/modules/hall-ui/public/controls.css +10 -2
  16. package/modules/hall-ui/public/copy-desk.html +1 -1
  17. package/modules/hall-ui/public/deploy.html +1 -1
  18. package/modules/hall-ui/public/goal-map.html +3 -3
  19. package/modules/hall-ui/public/goals.html +1 -1
  20. package/modules/hall-ui/public/government.html +1 -1
  21. package/modules/hall-ui/public/home-city.css +0 -4
  22. package/modules/hall-ui/public/idea.html +1 -1
  23. package/modules/hall-ui/public/ideas.html +1 -1
  24. package/modules/hall-ui/public/index.html +4 -4
  25. package/modules/hall-ui/public/market-watch-item.html +1 -1
  26. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  27. package/modules/hall-ui/public/market-watch.html +1 -1
  28. package/modules/hall-ui/public/modules.html +1 -1
  29. package/modules/hall-ui/public/money.html +1 -1
  30. package/modules/hall-ui/public/my-agents.html +1 -1
  31. package/modules/hall-ui/public/options-menu.js +20 -0
  32. package/modules/hall-ui/public/primer.html +1 -1
  33. package/modules/hall-ui/public/profile.html +1 -1
  34. package/modules/hall-ui/public/project-settings.html +1 -1
  35. package/modules/hall-ui/public/roster.html +1 -1
  36. package/modules/hall-ui/public/settings.css +113 -46
  37. package/modules/hall-ui/public/settings.html +3 -3
  38. package/modules/hall-ui/public/street.html +1 -1
  39. package/modules/hall-ui/public/studio.html +1 -1
  40. package/modules/hall-ui/public/style.css +167 -71
  41. package/modules/hall-ui/public/task.html +1 -1
  42. package/modules/hall-ui/public/thinking.html +3 -3
  43. package/modules/hall-ui/public/tweak-editor.html +1 -1
  44. package/modules/hall-ui/public/visor.css +109 -48
  45. package/modules/hall-ui/public/visor.js +50 -33
  46. package/modules/hall-ui/public/watch.html +1 -1
  47. package/modules/hall-ui/public/work.html +1 -1
  48. package/modules/hall-ui/records/craft-mode.md +9 -0
  49. package/modules/hall-ui/records/options-menu.md +11 -0
  50. package/modules/hall-ui/records/visor.md +14 -2
  51. package/package-lock.json +2 -2
  52. package/package.json +1 -1
  53. package/release-notes.json +12 -0
  54. package/src/module-api.js +1 -1
  55. package/tests/design_tokens_shared.mjs +5 -1
  56. package/tests/hall_controls.mjs +1 -1
  57. package/tests/hall_helm.mjs +103 -1
  58. package/tests/hall_topbar_320.mjs +7 -4
  59. package/tests/hall_visor.mjs +128 -10
@@ -1,19 +1,21 @@
1
1
  // visor.js — the first-person visor: glass over the scene (task 1005148, BONGOS-V3 R08,
2
2
  // goal 1000142, criterion `you-are-a-character-in-first-person`). Built to ADR 0399.
3
3
  //
4
- // What is on the glass (the design the owner agreed on 2026-11-10, task 1005148):
5
- // - the frame: thin curved visor lines at the top, the bottom and both sides, a small
6
- // centre cross, and a compass of nine slanted cells along the top line with the one
7
- // salmon marker under its centre. Drawn once as SVG; it is ornament, never a control.
8
- // - the place, in a mono line under the compass ("THE SKY", "GOVERNOR CITY", …).
9
- // - bottom-left: your character's main action, the one accent pill, with its key.
10
- // - bottom-right: your sprite (third person) with your character's name and your credits
4
+ // What is on the glass (the minimal visor, Helm look 3, task 1005195; first built in task
5
+ // 1005148):
6
+ // - the frame: two thin arcs, one along the top of the glass and one under the foot row,
7
+ // a small notch under the top arc and a ring-and-dot sight at the centre. Drawn as SVG;
8
+ // it is ornament, never a control.
9
+ // - the place, in the name voice under the top arc ("THE SKY", "GOVERNOR CITY", …).
10
+ // - the foot row. Left: your sprite (third person), your character's name and your credits
11
11
  // (builders.total_credits — credits only, never a payout; the spec's "does not exist").
12
- // - top-right: "Actions" (key A), which opens the sheet: your character's actions, then
13
- // the keys of the place you are in, then Options on Escape.
12
+ // Centre: your character's main action, the one accent button, with its key. Right: the
13
+ // quiet key hints, no boxes: your other actions, "Actions" (key A), which opens the
14
+ // sheet (your character's actions, then the keys of the place you are in, then Options
15
+ // on Escape), and Options itself.
14
16
  //
15
17
  // The rules it keeps (ADR 0399):
16
- // - one overlay, #visor, with pointer-events: none; only its pills take a pointer, so a
18
+ // - one overlay, #visor, with pointer-events: none; only its controls take a pointer, so a
17
19
  // drag, a wheel or a click on the world reaches the scene unchanged (§2.1).
18
20
  // - no camera: a scene registers itself once with OTBVisor.scene({ name, place, actions,
19
21
  // keys }) and a movement action calls the scene's own function (§2.2). A page with no
@@ -34,7 +36,7 @@
34
36
 
35
37
  // ---------- each character's actions ----------
36
38
  // `href` opens a page the hall already has; `tool` opens one of craft-mode's tools on
37
- // this page. The first is the character's main action (the accent pill).
39
+ // this page. The first is the character's main action (the accent button).
38
40
  var ACTIONS = {
39
41
  artisan: [
40
42
  { id: 'claim', label: 'Claim a task', key: 'c', href: '/work' },
@@ -112,23 +114,33 @@
112
114
  // ---------- drawing ----------
113
115
  var root = null, sheet = null, returnFocus = null;
114
116
  var NS = 'http://www.w3.org/2000/svg';
115
- // The frame is drawn in a 1000×1000 box stretched over the stage (preserveAspectRatio
116
- // none), with non-scaling strokes so every line stays one hairline at any size.
117
+ // Every mark is drawn twice: a wider stroke of the night under the ice hairline, so the
118
+ // line reads over a midday sky as well as at night (visor.css .visor__line--under).
119
+ function twice(shape) { return shape.replace('class="', 'class="visor__line--under ') + shape; }
120
+ function svgOpen(cls, box, stretch) {
121
+ return '<svg class="' + cls + '" viewBox="' + box + '"' + (stretch ? ' preserveAspectRatio="none"' : '') + ' aria-hidden="true" focusable="false" xmlns="' + NS + '">';
122
+ }
123
+ // The two arcs are each a 1000×100 box stretched across the stage (preserveAspectRatio
124
+ // none), with non-scaling strokes so each stays one hairline at any size. The notch and the
125
+ // sight are drawn at their own size, so neither is ever stretched.
117
126
  function frameSvg() {
118
- var cells = '';
119
- for (var i = -4; i <= 4; i++) {
120
- var x = 500 + i * 38, y = 30 + Math.pow(i / 4, 2) * 14, s = i * 2;
121
- cells += '<path class="visor__cell" d="M' + (x - 15) + ' ' + (y + 4 - s / 2) + 'l30 ' + s + 'l-6 15l-30 ' + (-s) + 'z"/>';
122
- }
123
- return '<svg class="visor__frame" viewBox="0 0 1000 1000" preserveAspectRatio="none" aria-hidden="true" focusable="false" xmlns="' + NS + '">' +
124
- '<path class="visor__line" d="M0 120Q500 -10 1000 120"/>' +
125
- '<path class="visor__line" d="M0 880Q500 1010 1000 880"/>' +
126
- '<path class="visor__line visor__line--side" d="M18 120Q4 500 18 880M982 120Q996 500 982 880"/>' +
127
- '<path class="visor__line" d="M490 500h-12M510 500h12M500 490v-14M500 510v14"/>' +
128
- cells + '<path class="visor__mark" d="M494 76l6 9l6 -9z"/>' +
129
- '</svg>';
127
+ return svgOpen('visor__frame visor__frame--top', '0 0 1000 100', true) + twice('<path class="visor__line" d="M76 100Q500 -100 924 100"/>') + '</svg>' +
128
+ svgOpen('visor__frame visor__frame--foot', '0 0 1000 100', true) + twice('<path class="visor__line" d="M76 0Q500 200 924 0"/>') + '</svg>' +
129
+ svgOpen('visor__notch', '0 0 12 9') + twice('<path class="visor__line" d="M0 0l6 9l6 -9"/>') + '</svg>' +
130
+ svgOpen('visor__sight', '0 0 24 24') + twice('<circle class="visor__line" cx="12" cy="12" r="11"/>') + '<circle class="visor__dot" cx="12" cy="12" r="2"/></svg>';
130
131
  }
131
132
  function keyHtml(k) { k = String(k); return '<kbd class="visor__key">' + esc(k.length === 1 ? k.toUpperCase() : k) + '</kbd>'; }
133
+ // The count and its unit are two spans, so the number can be white and the word ice; the
134
+ // text between them is still creditsText's, word for word.
135
+ function creditsHtml(n) {
136
+ var t = creditsText(n), at = t.lastIndexOf(' ');
137
+ if (at < 0) return esc(t);
138
+ return '<span class="visor__num">' + esc(t.slice(0, at)) + '</span> <span class="visor__unit">' + esc(t.slice(at + 1)) + '</span>';
139
+ }
140
+ // A quiet key hint: the key in its small box, then the name. No box round the pair.
141
+ function hintHtml(a) {
142
+ return '<a class="visor__hint" data-visor-act="' + esc(a.id) + '" ' + actionAttrs(a) + '>' + keyHtml(a.key) + '<span>' + esc(a.label) + '</span></a>';
143
+ }
132
144
  function actionAttrs(a) {
133
145
  return a.href ? 'href="' + esc(hallHref(a.href)) + '"' : 'href="#" role="button"';
134
146
  }
@@ -138,14 +150,19 @@
138
150
  var n = credits();
139
151
  return frameSvg() +
140
152
  (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') +
141
- '<a class="visor__pill visor__pill--main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
142
- '<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>' +
143
- '<button class="visor__pill visor__pill--actions" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
144
- '<span class="visor__dots" aria-hidden="true"></span><span>Actions</span>' + keyHtml(SHEET_KEY) + '</button>' +
145
- '<div class="visor__me">' +
146
- (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="250" height="290" decoding="async">' : '') +
147
- '<span class="visor__who"><span class="visor__name">' + esc(NAMES[id]) + '</span>' +
148
- '<span class="visor__credits" data-visor-credits>' + esc(creditsText(n)) + '</span></span>' +
153
+ '<div class="visor__foot">' +
154
+ '<div class="visor__me">' +
155
+ (sprite ? '<img class="visor__sprite" src="' + esc(sprite) + '" alt="" width="250" height="290" decoding="async">' : '') +
156
+ '<span class="visor__who"><span class="visor__name">' + esc(NAMES[id]) + '</span>' +
157
+ '<span class="visor__credits" data-visor-credits>' + creditsHtml(n) + '</span></span>' +
158
+ '</div>' +
159
+ '<a class="visor__main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
160
+ '<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>' +
161
+ '<div class="visor__hints">' + acts.slice(1).map(hintHtml).join('') +
162
+ '<button class="visor__hint visor__hint--sheet" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
163
+ '<span class="visor__dots" aria-hidden="true"></span>' + keyHtml(SHEET_KEY) + '<span>Actions</span></button>' +
164
+ '<a class="visor__hint visor__hint--options" href="' + esc(hallHref('/settings')) + '">' + keyHtml('Esc') + '<span>Options</span></a>' +
165
+ '</div>' +
149
166
  '</div>';
150
167
  }
151
168
  function sheetHtml(id) {
@@ -11,7 +11,7 @@
11
11
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
12
12
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
13
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-view2">
14
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
15
15
  <!-- The oversight family's sheet (task 1003469): the figure band, the group
16
16
  head, the row, the form. Then the page's own: severities, the report
17
17
  reader, the grade trend. The attention tiles + tabs come from style.css's
@@ -8,7 +8,7 @@
8
8
  <link rel="icon" href="/modules/ui-design/public/brand/favicon.svg" type="image/svg+xml">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-view2">
11
+ <link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm3">
12
12
  <!-- Page-local styles (task 2170) — extracted from the old inline <style>
13
13
  block per the hall placement rules; shared pager/count-pill styles now
14
14
  live in style.css's hall-kit section. -->
@@ -38,6 +38,15 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
38
38
  - **Always-present tabs widened** (owner, 2026-10-03): `ALWAYS_TABS` is Builders, Collab and Profile, so every mode keeps the You and Community groups.
39
39
  - **Governing's header tallies** (task 1004563): `applyTallies()` draws "needs you N · moving M" right after the eyebrow, Governing only, hidden at <=960px (below that they squeezed the jump button under its own label, measured at 800px). Needs you is `GET /blockers?limit=1`'s `page.total`; moving is `GET /moving` (lifecycle, tasks `active` + `confirmed`, owner 2026-10-08), a count in SQL so the page never pulls task rows. Read once per page; a figure that cannot be read is LEFT OUT, never drawn as 0. The warm dot marks needs-you only above zero. It toggles the `hidden` ATTRIBUTE, never `.hidden =` (the nav gate owns that property; `tests/craft_mode_nav.mjs`). Pinned by `tests/craft_mode_tallies.mjs`.
40
40
 
41
+ ## The pickers in Helm (task 1005194, ADR 0402)
42
+
43
+ - **Character creation is a screen, not a box**: the night itself (`.craft-picker--create` on `--bg`), the title centred in the name voice, three tall doors, and the one line at the foot. The line is the same `#craft-picker-lede` the dialog is described by; CSS `order` moves it under the doors, the markup and words are unchanged.
44
+ - **A door is pressed, so it is cut** (`--cut-path`; the small cut on a phone, where the three stack as rows). The owner's portrait fills the top of the door under the cut; the name is the largest thing on it, in the name voice. The door the hall suggests (`.craft-door--pre`) is the one thing lit: the accent wash inside the accent edge. The door under the keyboard shows the inset ring and names its key ("Enter", a CSS label, as the start screen's Start does).
45
+ - **The edge is drawn twice.** A door's hairline, the suggested edge and the focus ring are inset shadows, and a portrait that fills the door would cover them, so `.craft-door::after` repeats the door's `box-shadow` over its contents.
46
+ - **The session picker** ("What are you doing now?") shares the rules: a square night panel with the strong hairline over the scrim, the title in the name voice, cut doors. Its four scenes (`.craft-art`) are untouched.
47
+ - **Not taken from the mock:** the two outer doors' perspective tilt (pixel art resamples badly under a 3D transform), the mark above the title, and the "Lands in …" line (no such words exist; the line under each name is still `CHARACTER_LINES`).
48
+ - **Tests:** `tests/hall_helm.mjs` ("character select is a Helm screen"); `tests/character_creation.mjs` still runs the behaviour.
49
+
41
50
  ## The paintbrush (task 1004497)
42
51
 
43
52
  - **Artist carries the palette and the paintbrush** (and the lightbulb, below): the palette (a feedback doc, `page-notes.js`, record `page-notes.md`) and the paintbrush (`paintbrush.js`). Both are `TOOLS` entries in `craft-mode.js` and load their own script on first press.
@@ -13,3 +13,14 @@ Every setting and configuration is reached from one vertical, grouped menu with
13
13
  - **Left the sidebar:** My agents, Project settings, Permissions, Modules (and their glyphs). The palette's agent rows open the menu; its Government rows are gone.
14
14
  - **Tests:** `tests/hall_options_menu.mjs` (the catalogue, gates, search, addresses, both redirect maps, the embedded page, game-mode Escape); `tests/hall_fullscreen.mjs` (Escape in game mode); `tests/market_watch_pages.mjs` (the settings panel in the frame).
15
15
  - **Known:** sounds are switched off (task 1004554), so the Sound entry shows its note, not a toggle.
16
+
17
+ ## The Helm look (task 1005194, ADR 0402)
18
+
19
+ The menu is a Helm screen, written from the hall tier's tokens alone (`settings.css` types in no colour or corner for it). Words, order, addresses, keys and aria are unchanged.
20
+
21
+ - **The menu** is one night panel (`--night-glass`) closed left and right by the strong ice hairline. The search is the hall's one field (`controls.css`), on the glass. An entry is a full-width row in the name voice on a soft hairline; the open entry is the accent bar under its dark ink; a group head is an ice label. Focus is the inset ring (`--press-focus`), so it reads on the accent bar too.
22
+ - **The open screen** carries its name over the strong hairline: `options-menu.js` `nameScreen()` writes the open entry's own label into `#opt-screen` (a `<p aria-hidden="true">`; the rail's `aria-current` link and each frame's `aria-label` already say it). Settings' own sections are rows on soft hairlines, not cards, with their titles in the name voice. A frame sits under the name and still fills the column to the window's foot.
23
+ - **Full screen and game mode:** the panel runs the window's full height from the top edge and names itself (`.opt-title`, the nav's own label, hidden while the top bar is up because the bar already says it). The pull to the top edge is the 60px the shell leaves a plain page above its content (`style.css`, `data-fullscreen-plain`); if that changes, change `.opt-rail`'s `margin-top` with it.
24
+ - **Widths:** the panel is `min(--helm-panel-w, 32%)` of the content column; below 860px it stacks above the screen as before. A long label wraps inside its row.
25
+ - **Not built** (the mock shows them on a Controls screen the hall does not have): the key-binding rows, "Reset all keys", and a visible "Back to the city" button. Escape still closes the menu in game mode.
26
+ - **Tests:** `tests/hall_helm.mjs` ("the options menu is a Helm screen"); the kit states in `settings.states.json` render clean at 1440, 390 and 320.
@@ -2,9 +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
- ## What it settled
5
+ ## The minimal visor (Helm look 3, task 1005195)
6
6
 
7
- - **The look the owner agreed (2026-11-10, five mock-up rounds).** A quiet layout with "hairline instruments" from the helmet round: thin curved visor lines across the top, bottom and both sides, a small centre cross, and a compass of nine slanted cells along the top line with the one salmon marker under its centre. No tracker or radar (tried and dropped), no clipped corners: the pills stay round (DESIGN.md "pill or nothing"). No glow (DESIGN.md's rejected near-black-plus-neon HUD). The frame is one SVG in a 1000×1000 box stretched over the stage with non-scaling strokes, so it is a hairline at every size.
7
+ 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
+
9
+ - **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
+ - **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.
12
+ - **It reads over any sky, with no per-hour style.** The glass darkens toward its top and bottom edge (`--night-glass`), type wears `--type-halo` and a stroke of the night painted under each letter, every line is drawn twice (a wider night stroke under the ice hairline, `.visor__line--under`), and a key sits on a chip of the night. No new token.
13
+ - **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.
15
+ - **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
+
17
+ ## What it settled (task 1005148)
18
+
19
+ - **The first look (2026-11-10, five mock-up rounds; replaced by the minimal visor above).** A quiet layout with "hairline instruments" from the helmet round: thin curved visor lines across the top, bottom and both sides, a small centre cross, and a compass of nine slanted cells along the top line with the one salmon marker under its centre. No tracker or radar (tried and dropped), no clipped corners: the pills stay round (DESIGN.md "pill or nothing"). No glow (DESIGN.md's rejected near-black-plus-neon HUD). The frame is one SVG in a 1000×1000 box stretched over the stage with non-scaling strokes, so it is a hairline at every size.
8
20
  - **What sits where.** Bottom-left: the character's main action, the one accent pill, with its key. Bottom-right: the sprite (task 1005147's art, `OTBCraftMode.spriteSrc`) with the character's name and the credit count (`/me` `builder.total_credits` through `OTBCraftMode.me()`; credits only, never a payout). Top-right: Actions (key A), which opens the sheet: the character's actions, then "This place" (the scene's own actions and keys), then Options on Escape. A place line under the compass names the scene; the sky leaves it empty because its own heading names it.
9
21
  - **Each character's actions** (`ACTIONS`): Artisan Claim a task C (`/work`), Blockers B, Goal sky G, Your thinking T. Artist Review this page R, Edit its words E, Write an idea I (the three craft-mode tools, through `OTBCraftMode.openTool`), Studio S. Patron Board room O, Governor city V, Blockers B, Watch W. Every one is a page or tool the hall already has; the visor gates nothing, and the server's rank checks answer exactly as before (ADR 0399 §2.5). No key a scene binds is reused (the sky's L 0 / ?, the gallery's F, the palette's K, the city card's digits, arrows and Tab).
10
22
  - **Glass, not layout (ADR 0399 §2.1).** `#visor` is `position: fixed` with `pointer-events: none`; only the pills and the sheet take a pointer. A drag across a frame line pans the sky (test-pinned).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.82",
3
+ "version": "1.24.84",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.82",
9
+ "version": "1.24.84",
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.82",
3
+ "version": "1.24.84",
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",
@@ -11595,5 +11595,17 @@
11595
11595
  "id": "1005200",
11596
11596
  "text": "The hall now has two separate views, Game and Classic, and never mixes them. You choose one each time you sign in and can switch only in Options. Game mode stays full screen and only opens the game's places."
11597
11597
  }
11598
+ ],
11599
+ "1.24.83": [
11600
+ {
11601
+ "id": "1005194",
11602
+ "text": "The options menu and the choose-your-character screen now wear the Helm look: a night panel with wide capital rows, and three tall cut-corner doors with the real characters. Fields and tags across the hall are squared to match"
11603
+ }
11604
+ ],
11605
+ "1.24.84": [
11606
+ {
11607
+ "id": "1005195",
11608
+ "text": "The in-game visor is now minimal: two thin arcs, the place name, your credits, one salmon button for the current action and a quiet row of key hints. It stays readable over a bright daytime sky as well as at night."
11609
+ }
11598
11610
  ]
11599
11611
  }
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.82'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.84'; // 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');
@@ -133,13 +133,17 @@ test('the landing, hub, status and raise pages link it before their own sheet',
133
133
 
134
134
  test('the hall\'s old token names still resolve — each alias names a defined --ds-* token', () => {
135
135
  const style = decomment(read('modules', 'hall-ui', 'public', 'style.css'));
136
- const want = ['--line', '--radius', '--radius-sm', '--radius-pill', '--sp-hair', '--sp-xs', '--sp-sm', '--sp-md', '--sp-lg', '--sp-xl',
136
+ const want = ['--line', '--radius', '--radius-sm', '--sp-hair', '--sp-xs', '--sp-sm', '--sp-md', '--sp-lg', '--sp-xl',
137
137
  '--shadow-pop', '--z-progress', '--z-topbar', '--z-sidebar', '--z-scrim', '--z-drawer', '--z-modal', '--z-diagram-full', '--label-size'];
138
138
  for (const n of want) {
139
139
  const m = style.match(new RegExp(`${n}\\s*:\\s*var\\((--ds-[\\w-]+)\\)`));
140
140
  assert.ok(m, `${n} is an alias onto the shared sheet`);
141
141
  assert.ok(ROOT_TOKENS[m[1]], `${n} → ${m[1]} is defined in tokens.css`);
142
142
  }
143
+ // --radius-pill left the aliases with Helm (task 1005194, ADR 0402): the hall draws no pill,
144
+ // so its tier sets the name to a square corner; the shared --ds-radius-pill is untouched.
145
+ assert.match(style, /--radius-pill: 0px;/, 'the hall has no pill');
146
+ assert.equal(ROOT_TOKENS['--ds-radius-pill'], '999px', 'the shared pill is still a pill for every other surface');
143
147
  // the four-step --space-* scale still rides the --sp-* aliases
144
148
  for (const n of ['--space-1', '--space-2', '--space-3', '--space-4']) assert.match(style, new RegExp(`${n}\\s*:\\s*var\\(--sp-`));
145
149
  });
@@ -161,7 +161,7 @@ test('primary is the 46–48px door; the row size and the text button keep the 2
161
161
  }
162
162
  const field = sharedRule('select');
163
163
  assert.ok(px(prop(field, 'min-height')) >= 24, 'the field clears the floor');
164
- assert.equal(prop(field, 'border-radius'), 'var(--radius-pill)', 'a field is a pill too');
164
+ assert.equal(prop(field, 'border-radius'), 'var(--radius-pill)', 'a field spends the pill name: a pill on the status page, square in the hall, whose tier sets --radius-pill to 0px (tests/hall_helm.mjs)');
165
165
  });
166
166
 
167
167
  // ── 3. AA, measured, in both modes ───────────────────────────────────────────
@@ -14,7 +14,9 @@
14
14
  // cannot see because the link is built at serve time;
15
15
  // * no hall script writes a theme choice, and the shell offers no switch;
16
16
  // * the Helm neutrals: the two dark twins carry the night, the ice and no black;
17
- // * the kit's recipes exist once, as hall-tier tokens and shared rules.
17
+ // * the kit's recipes exist once, as hall-tier tokens and shared rules;
18
+ // * Helm look 2 (task 1005194): the hall has no pill, and the options menu and
19
+ // character select are Helm screens written from the tokens alone.
18
20
  //
19
21
  // Run: node --test tests/hall_helm.mjs
20
22
  import { test } from 'node:test';
@@ -185,3 +187,103 @@ test('the kit\'s recipes are named once: the name voice, the cut, the segment, t
185
187
  assert.match(controls, /\n\.seg--done \{ background: var\(--ink-soft\);/, 'a finished step is information, so it is ice, not the accent');
186
188
  assert.match(controls, /\n\.seg--now \{ background: var\(--accent\);/, 'the accent is the step you can act on');
187
189
  });
190
+
191
+ // ── Helm look 2 (task 1005194): the square corner, the options menu, character select ──
192
+
193
+ // one rule's body, by its exact selector line, from a decommented sheet
194
+ const ruleOf = (css, sel) => {
195
+ const at = css.indexOf('\n' + sel + ' {');
196
+ assert.notEqual(at, -1, `no rule ${sel}`);
197
+ const open = css.indexOf('{', at);
198
+ return css.slice(open + 1, css.indexOf('}', open));
199
+ };
200
+ const NO_COLOUR_LITERAL = /#[0-9a-fA-F]{3,8}\b|\brgba?\(|\bhsla?\(/;
201
+
202
+ test('the hall has no pill: --radius-pill is a square corner in the hall tier, and only there', () => {
203
+ assert.equal(root['--radius-pill'], '0px', 'Helm draws no pill');
204
+ // the panel and the tile keep their corners until each page's own task (DESIGN.md "The hall")
205
+ assert.equal(root['--radius'], 'var(--ds-radius-panel)');
206
+ assert.equal(root['--radius-sm'], 'var(--ds-radius-tile)');
207
+ // the shared scale is untouched, so the landing and the status page keep their pill
208
+ assert.match(read('modules', 'ui-design', 'public', 'tokens.css'), /--ds-radius-pill:\s*999px;/);
209
+ const controls = decomment(read('modules', 'hall-ui', 'public', 'controls.css'));
210
+ // the field and the status tag spend the name, so they are square in the hall with no rule of their own
211
+ assert.match(controls, /\.input \{[^}]*border-radius: var\(--radius-pill\);/);
212
+ assert.match(controls, /\ntextarea \{[^}]*border-radius: var\(--press-radius, var\(--radius-sm\)\);/, 'a textarea is square too, the tile where the tier is not loaded');
213
+ });
214
+
215
+ test('what is pressed in the top bar, and a filter chip, take the small cut and the inset focus ring', () => {
216
+ const css = decomment(STYLE.replace(/\r\n/g, '\n'));
217
+ for (const sel of ['.upd-pill', '.chip', 'html[data-fullscreen="on"] .fullscreen-toggle']) {
218
+ const body = ruleOf(css, sel);
219
+ assert.match(body, /clip-path: var\(--cut-path-sm\);/, `${sel} is cut`);
220
+ assert.match(body, /--focus-ring: var\(--press-focus\);/, `${sel}: a cut clips an outline, so the ring is inside`);
221
+ assert.match(body, /border-radius: var\(--press-radius\);/, `${sel} has no round corner`);
222
+ }
223
+ // an outer shadow would be clipped away with the corner, and would outrank the focus ring
224
+ assert.doesNotMatch(ruleOf(css, 'html[data-fullscreen="on"] .upd-pill'), /box-shadow/);
225
+ });
226
+
227
+ test('the options menu is a Helm screen: a night panel, name-voice rows, the open one the accent bar', () => {
228
+ const sheet = decomment(read('modules', 'hall-ui', 'public', 'settings.css').replace(/\r\n/g, '\n'));
229
+ const menu = sheet.slice(0, sheet.indexOf('.cli-result'));
230
+ assert.ok(menu.includes('.opt-rail') && menu.includes('.opt-screen'), 'the menu and the open screen are the first part of the sheet');
231
+ assert.doesNotMatch(menu, NO_COLOUR_LITERAL, 'no colour is typed in');
232
+ assert.doesNotMatch(menu, /border-radius:\s*\d/, 'no corner is typed in');
233
+ const rail = ruleOf(menu, '.opt-rail');
234
+ assert.match(rail, /background: var\(--night-glass\);/);
235
+ assert.match(rail, /border-left-width: var\(--line-strong\); border-right-width: var\(--line-strong\);/, 'closed left and right by the strong hairline');
236
+ const link = ruleOf(menu, '.opt-link');
237
+ assert.match(link, /font-family: var\(--name-face\);/);
238
+ assert.match(link, /text-transform: var\(--name-case\);/);
239
+ assert.match(link, /min-height: var\(--ds-space-xl\);/, 'a row clears the 24px floor many times over');
240
+ assert.match(link, /border-bottom: var\(--line\) solid var\(--rule-soft\);/, 'rows on hairlines, not boxes');
241
+ assert.match(ruleOf(menu, '.opt-link--active, .opt-link--active:hover'), /color: var\(--accent-on\); background: var\(--accent\);/);
242
+ assert.match(ruleOf(menu, '.opt-link:focus-visible'), /box-shadow: var\(--press-focus\);/, 'the ring is inside the row, so it reads on the accent bar');
243
+ const screen = ruleOf(menu, '.opt-screen');
244
+ assert.match(screen, /font-family: var\(--name-face\);/);
245
+ assert.match(screen, /border-bottom: var\(--line-strong\) solid var\(--rule\);/);
246
+ const section = ruleOf(menu, '.settings-groups .scroll');
247
+ assert.match(section, /background: none;/, 'a section is a row on a hairline, not a card');
248
+ assert.match(section, /border-bottom: var\(--line\) solid var\(--rule-soft\);/);
249
+ // the search is the hall's one field: the sheet draws no edge, corner or focus of its own for it
250
+ assert.doesNotMatch(ruleOf(menu, '.opt-search input'), /border|outline/);
251
+ assert.doesNotMatch(menu, /\.opt-search input:focus/);
252
+ // the two names are words the menu already had: its own label, and the open entry's
253
+ const js = read('modules', 'hall-ui', 'public', 'options-menu.js');
254
+ assert.match(js, /<p class="opt-title" aria-hidden="true">Options<\/p>/);
255
+ assert.match(js, /h\.setAttribute\('aria-hidden', 'true'\);/);
256
+ assert.match(js, /h\.textContent = e\.label;/);
257
+ assert.match(js, /<nav|opt-search/, 'the search is still drawn');
258
+ assert.match(read('modules', 'hall-ui', 'public', 'settings.html'), /<nav class="opt-rail" id="settings-rail" aria-label="Options"><\/nav>/);
259
+ });
260
+
261
+ test('character select is a Helm screen: the night, cut doors, the suggested door lit, the ring inside the cut', () => {
262
+ const css = decomment(STYLE.replace(/\r\n/g, '\n'));
263
+ const picker = css.slice(css.indexOf('\n.craft-picker {'), css.indexOf('\n.craft-art {'));
264
+ assert.doesNotMatch(picker, NO_COLOUR_LITERAL, 'no colour is typed in');
265
+ assert.doesNotMatch(picker, /border-radius:\s*\d|font-size:\s*\d/, 'no corner or size is typed in');
266
+ assert.match(ruleOf(css, '.craft-picker--create'), /background: var\(--bg\);/, 'a screen on the night, not a box on a scrim');
267
+ assert.match(ruleOf(css, '.craft-picker__panel--create'), /background: none;/);
268
+ const title = ruleOf(css, '.craft-picker__title');
269
+ assert.match(title, /font-family: var\(--name-face\);/);
270
+ assert.match(title, /text-transform: var\(--name-case\);/);
271
+ const door = ruleOf(css, '.craft-door');
272
+ assert.match(door, /clip-path: var\(--cut-path\);/, 'a door is pressed, so it is cut');
273
+ assert.match(door, /background: var\(--night-glass\);/);
274
+ assert.match(ruleOf(css, '.craft-door:focus-visible'), /outline: 0;[^}]*box-shadow: var\(--press-focus\);/);
275
+ assert.match(ruleOf(css, '.craft-door--pre, .craft-door--pre:hover'), /box-shadow: inset 0 0 0 2px var\(--accent\);/);
276
+ assert.match(ruleOf(css, '.craft-door::after'), /box-shadow: inherit;/, 'the edge is drawn over the portrait as well as under it');
277
+ assert.match(ruleOf(css, '.craft-door__name'), /font-family: var\(--name-face\);/);
278
+ // the real sprites, and the words where they were
279
+ const js = read('modules', 'hall-ui', 'public', 'craft-mode.js');
280
+ for (const id of ['artisan', 'artist', 'patron']) assert.ok(fs.existsSync(path.join(HALL, 'characters', id + '.png')), `${id}.png`);
281
+ assert.match(js, /id="craft-picker-title">Choose your character<\/h2>/);
282
+ assert.match(js, /aria-describedby="craft-picker-lede"/);
283
+ });
284
+
285
+ test('the visor reads in the Helm night: no hover falls back to the pack\'s dark accent', () => {
286
+ const visor = decomment(read('modules', 'hall-ui', 'public', 'visor.css'));
287
+ assert.doesNotMatch(visor, /--accent-deep/, 'the deep accent is a light-ground colour; on the night it is under 4.5:1');
288
+ assert.match(visor, /\.visor__row:hover \{ color: var\(--accent-ink\); \}/);
289
+ });
@@ -99,7 +99,9 @@ for (const scheme of ['light', 'dark']) {
99
99
  });
100
100
  }
101
101
 
102
- test('the top bar search and the mode switch are one shape: a pill with the same hairline', { skip }, async () => {
102
+ // Helm (task 1005194): the hall has no pill, so the one shape the two share is the hall's
103
+ // square corner (--radius-pill is 0px in the hall tier) inside the same hairline.
104
+ test('the top bar search and the mode switch are one shape: the square corner of the hall with the same hairline', { skip }, async () => {
103
105
  const ctx = await context(1440, 'dark');
104
106
  const page = await ctx.newPage();
105
107
  await page.goto(ORIGIN + '/work');
@@ -115,11 +117,12 @@ test('the top bar search and the mode switch are one shape: a pill with the same
115
117
  switchRadius: css(sw).borderTopLeftRadius,
116
118
  jumpEdge: css(jump).borderTopColor,
117
119
  switchShadow: css(sw).boxShadow,
118
- pill: root.getPropertyValue('--ds-radius-pill').trim(),
120
+ pill: root.getPropertyValue('--radius-pill').trim(),
119
121
  };
120
122
  }).finally(() => ctx.close());
121
- assert.equal(s.jumpRadius, s.pill, 'the search is a full pill');
122
- assert.equal(s.switchRadius, s.pill, 'the mode switch is a full pill');
123
+ assert.equal(s.pill, '0px', 'the hall tier squares the pill');
124
+ assert.equal(s.jumpRadius, s.pill, 'the search takes the corner of the hall');
125
+ assert.equal(s.switchRadius, s.pill, 'the mode switch takes the same corner');
123
126
  assert.ok(s.switchShadow.startsWith(s.jumpEdge), `the two share one hairline colour (${s.jumpEdge} vs ${s.switchShadow})`);
124
127
  });
125
128