@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
@@ -8,6 +8,8 @@
8
8
  // - in game mode the last Escape reaches the options menu, never the visor.
9
9
  // Plus the criterion's own proof half that is the visor's: an Artisan and an Artist
10
10
  // are offered DIFFERENT actions on the glass. The pure parts run in a vm.
11
+ // The minimal visor (Helm look 3, task 1005195) adds the look's own pins: the sheet spends
12
+ // tokens only, and over the hall's real sheets the foot row holds at a desk, 390 and 320.
11
13
  import assert from 'node:assert/strict';
12
14
  import { test, after } from 'node:test';
13
15
  import { createRequire } from 'node:module';
@@ -87,6 +89,35 @@ test('source: the visor loads after the shell on every scene page, with its shee
87
89
  assert.match(read('visor.css'), /pointer-events: none/, 'the glass lets the pointer through');
88
90
  });
89
91
 
92
+ // Helm look 3 (task 1005195): the glass is the kit's, and nothing on it is typed in.
93
+ test('source: the minimal visor spends the Helm tokens and types in no colour, size, corner, shadow or breakpoint', () => {
94
+ const css = read('visor.css').replace(/\/\*[\s\S]*?\*\//g, '');
95
+ assert.doesNotMatch(css, /#[0-9a-f]{3,8}\b|\brgba?\(|\bhsla?\(/i, 'no colour literal');
96
+ assert.doesNotMatch(css, /font-size:\s*[\d.]/, 'every size is a --ds-text-* step');
97
+ assert.doesNotMatch(css, /border-radius:(?!\s*var\(--press-radius\))/, 'no corner but the press corner: Helm draws no pill');
98
+ assert.doesNotMatch(css, /@media|@container/, 'the phone rules live in style.css (the design-count ratchet)');
99
+ assert.doesNotMatch(css, /animation|@keyframes/, 'nothing on the glass moves by itself');
100
+ for (const m of css.match(/(?:box|text)-shadow:[^;]+/g) || []) assert.match(m, /^(?:box|text)-shadow:\s*(?:none|var\(--[a-z-]+\))$/, `${m}: a shadow is a token`);
101
+ for (const m of css.match(/transition:[^;]+/g) || []) assert.match(m, /var\(--ds-dur-/, `${m}: a duration token, so reduced motion stills it`);
102
+ assert.match(css, /\.visor__main \{[^}]*background: var\(--accent\)[^}]*clip-path: var\(--cut-path\)/, 'the one accent button is cut');
103
+ assert.match(css, /\.visor__main:focus-visible[^{]*\{[^}]*box-shadow: var\(--press-focus\)/, 'a cut control takes the inset ring');
104
+ assert.match(css, /\.visor__hint \{[^}]*background: none; border: 0/, 'a key hint has no box');
105
+ assert.match(css, /\.visor__place, \.visor__name, \.visor__unit, \.visor__main, \.visor__hint[^{]*\{\s*font-family: var\(--name-face\)/, 'names are in the name voice');
106
+ // it reads over any sky with one treatment, never a per-hour style
107
+ assert.match(css, /html\[data-game="on"\] \.visor:not\(\[hidden\]\) \{[^}]*background: linear-gradient\(to bottom, var\(--night-glass\), transparent[^;]*var\(--night-glass\)\)/, 'the glass darkens toward its edges');
108
+ assert.match(css, /text-shadow: var\(--type-halo\); -webkit-text-stroke: 4px var\(--night-glass\); paint-order: stroke fill/, 'type wears the halo over a stroke of the night');
109
+ assert.match(css, /\.visor__line--under \{ stroke: var\(--night-glass\)/, 'every line is drawn over the night');
110
+ assert.doesNotMatch(css, /data-hour|data-time|data-sky/, 'no per-hour style');
111
+ // the frame is two arcs, a notch and a sight; the compass, the side lines and the pills are gone
112
+ assert.equal((VISOR.match(/svgOpen\('visor__frame /g) || []).length, 2, 'two arcs');
113
+ assert.doesNotMatch(VISOR + css, /visor__cell|visor__mark|visor__line--side|visor__pill/, 'the old frame and the pills are gone');
114
+ const phone = read('style.css').match(/tasks 1005148 and 1005195, the visor[\s\S]*?\n\}/);
115
+ assert.ok(phone, 'the phone rules sit in the shared phone block');
116
+ assert.match(phone[0], /\.visor__hint:not\(\[data-visor-sheet\]\) \{ display: none; \}/, 'a phone keeps only Actions of the hints');
117
+ assert.doesNotMatch(read('home-city.css'), /visor-on/, 'Home has no top-right pill to clear any more');
118
+ assert.match(read('city.css'), /\nhtml\.visor-on \.city-door \{ bottom: 150px; \}/, 'the city\'s door clears the centred button at every width');
119
+ });
120
+
90
121
  // ---------- in a browser: the glass over a stand-in scene ----------
91
122
  const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
92
123
  let browser = null;
@@ -95,7 +126,7 @@ after(() => browser && browser.close());
95
126
  const skip = !browser && 'no system Edge or Chrome';
96
127
 
97
128
  const ORIGIN = 'http://hall.test';
98
- const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png' };
129
+ const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.svg': 'image/svg+xml' };
99
130
  // The stand-in scene: a canvas filling the window that pans on drag, and the sky's Escape rule.
100
131
  const SCENE = `<script>
101
132
  window.cam = { x: 0, y: 0 }; window.picked = true; window.optionsOpened = 0;
@@ -109,18 +140,23 @@ const SCENE = `<script>
109
140
  me() { return { builder: { total_credits: 56021 } }; }, spriteSrc(id) { return '/builders/characters/' + id + '.png'; }, openTool(id) { window.toolOpened = id; } };
110
141
  window.__visorScene = { name: 'test', place: 'Test city', actions: [{ label: 'Back to centre', key: '0', run() { window.centred = true; } }], keys: [{ label: 'Look around', key: 'drag' }] };
111
142
  </script>`;
112
- const PAGE = (game) => `<!doctype html><html${game ? ' data-game="on"' : ''}><head><meta charset="utf-8">
143
+ // `helm` links the hall's real sheet (the token tier) instead of the stand-in colours.
144
+ const PAGE = (game, helm) => helm ? `<!doctype html><html data-theme="dark" data-faces="helm"${game ? ' data-game="on"' : ''}><head><meta charset="utf-8">
145
+ <link rel="stylesheet" href="/builders/style.css"><link rel="stylesheet" href="/builders/visor.css">
146
+ <style>body{margin:0}#world{position:fixed;inset:0;width:100vw;height:100vh;display:block}</style></head>
147
+ <body><canvas id="world"></canvas>${SCENE}<script src="/builders/shell-game.js"></script><script src="/builders/visor.js"></script></body></html>`
148
+ : `<!doctype html><html${game ? ' data-game="on"' : ''}><head><meta charset="utf-8">
113
149
  <link rel="stylesheet" href="/builders/visor.css"><style>:root{--ink:#111;--ink-soft:#555;--ink-faint:#777;--rule:#ccc;--rule-soft:#ddd;--bg-card:#fff;--bg-deep:#eee;--accent:#fb9569;--accent-on:#111;--accent-deep:#a3481a;--focus:#a3481a}
114
150
  body{margin:0}#world{position:fixed;inset:0;width:100vw;height:100vh;display:block}</style></head>
115
151
  <body><canvas id="world"></canvas>${SCENE}<script src="/builders/shell-game.js"></script><script src="/builders/visor.js"></script></body></html>`;
116
152
 
117
- async function open({ game = true } = {}) {
118
- const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
153
+ async function open({ game = true, helm = false, width = 1280, height = 800 } = {}) {
154
+ const ctx = await browser.newContext({ viewport: { width, height } });
119
155
  await ctx.route(ORIGIN + '/**', (route) => {
120
156
  const p = new URL(route.request().url()).pathname;
121
- if (p === '/' ) return route.fulfill({ contentType: 'text/html', body: PAGE(game) });
122
- const f = path.join(HALL, p.replace(/^\/builders\//, ''));
123
- if (p.startsWith('/builders/') && TYPES[path.extname(p)] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[path.extname(p)], body: fs.readFileSync(f) });
157
+ if (p === '/' ) return route.fulfill({ contentType: 'text/html', body: PAGE(game, helm) });
158
+ const f = p.startsWith('/modules/') ? path.join(ROOT, p) : path.join(HALL, p.replace(/^\/builders\//, ''));
159
+ if ((p.startsWith('/builders/') || p.startsWith('/modules/')) && TYPES[path.extname(p)] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[path.extname(p)], body: fs.readFileSync(f) });
124
160
  return route.fulfill({ status: 404, body: '' });
125
161
  });
126
162
  const page = await ctx.newPage();
@@ -139,12 +175,16 @@ test('outside game mode the hall is as it was: no glass', { skip }, async () =>
139
175
  await ctx.close();
140
176
  });
141
177
 
142
- test('in game mode the glass shows the place, the main action, the sprite and the credits', { skip }, async () => {
178
+ test('in game mode the glass shows the place, the main action, the key hints, the sprite and the credits', { skip }, async () => {
143
179
  const { ctx, page, errors } = await open();
144
180
  const g = await page.evaluate(() => ({
145
181
  shown: getComputedStyle(document.getElementById('visor')).display,
146
182
  place: document.querySelector('.visor__place').textContent,
147
- main: document.querySelector('.visor__pill--main').textContent,
183
+ main: document.querySelector('.visor__main').textContent,
184
+ hints: [...document.querySelectorAll('.visor__hints .visor__hint')].map((h) => h.textContent),
185
+ num: document.querySelector('.visor__num').textContent,
186
+ unit: document.querySelector('.visor__unit').textContent,
187
+ arcs: document.querySelectorAll('.visor__frame').length,
148
188
  who: document.querySelector('.visor__name').textContent,
149
189
  credits: document.querySelector('.visor__credits').textContent,
150
190
  sprite: document.querySelector('.visor__sprite').getAttribute('src'),
@@ -154,6 +194,10 @@ test('in game mode the glass shows the place, the main action, the sprite and th
154
194
  assert.match(g.main, /^Goal sky/, 'Claim a task opens a classic page, so it is off the glass in game mode (task 1005200)');
155
195
  assert.equal(g.who, 'Artisan');
156
196
  assert.equal(g.credits, '56,021 credits');
197
+ assert.deepEqual([g.num, g.unit], ['56,021', 'credits'], 'the count and its word are two spans of the same text');
198
+ // task 1005195: every other control is a quiet key hint. Actions kept its key and its sheet; nothing was dropped.
199
+ assert.deepEqual(g.hints, ['TYour thinking', 'AActions', 'EscOptions']);
200
+ assert.equal(g.arcs, 2, 'two arcs frame the glass');
157
201
  assert.equal(g.sprite, '/builders/characters/artisan.png');
158
202
  assert.deepEqual(errors, []);
159
203
  await ctx.close();
@@ -199,7 +243,10 @@ test('the sheet lists the character\'s actions, then this place, and runs a scen
199
243
  test('an Artist\'s glass offers other actions, and its keys open the Artist\'s tools', { skip }, async () => {
200
244
  const { ctx, page } = await open();
201
245
  await page.evaluate(() => { OTBCraftMode.held = 'artist'; OTBVisor.render(); });
202
- assert.match(await page.textContent('.visor__pill--main'), /^Review this page/);
246
+ assert.match(await page.textContent('.visor__main'), /^Review this page/);
247
+ assert.deepEqual(await page.$$eval('.visor__hints [data-visor-act]', (h) => h.map((x) => x.getAttribute('data-visor-act'))), ['words', 'idea'], 'the Artist\'s other tools are key hints');
248
+ await page.click('.visor__hints [data-visor-act="idea"]');
249
+ assert.equal(await page.evaluate(() => window.toolOpened), 'lightbulb', 'a hint runs its action, like its row in the sheet');
203
250
  await page.keyboard.press('e');
204
251
  assert.equal(await page.evaluate(() => window.toolOpened), 'paintbrush');
205
252
  await ctx.close();
@@ -223,3 +270,74 @@ test('no character, or no scene: no glass', { skip }, async () => {
223
270
  assert.equal(await page.evaluate(() => document.getElementById('visor').hidden), true);
224
271
  await ctx.close();
225
272
  });
273
+
274
+ // ---------- Helm look 3 (task 1005195): the foot row over the hall's real sheets ----------
275
+ const box = (sel) => `(() => [...document.querySelectorAll(${JSON.stringify(sel)})].filter((e) => e.getClientRects().length).map((e) => { const b = e.getBoundingClientRect(); return { l: b.left, t: b.top, r: b.right, b: b.bottom, w: b.width, h: b.height }; }))()`;
276
+ const apart = (a, b) => a.r <= b.l + 0.5 || b.r <= a.l + 0.5 || a.b <= b.t + 0.5 || b.b <= a.t + 0.5;
277
+
278
+ for (const [width, height] of [[1440, 900], [390, 844], [320, 640]]) {
279
+ test(`at ${width}px the foot row fits: no sideways scroll, nothing collides, every control clears 24px`, { skip }, async () => {
280
+ for (const who of ['artisan', 'artist', 'patron']) {
281
+ const { ctx, page, errors } = await open({ helm: true, width, height });
282
+ await page.evaluate((w) => { OTBCraftMode.held = w; OTBVisor.render(); }, who);
283
+ assert.equal(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth), 0, `${who}: no sideways scroll`);
284
+ const [me] = await page.evaluate(box('.visor__me'));
285
+ const [main] = await page.evaluate(box('.visor__main'));
286
+ const hints = await page.evaluate(box('.visor__hint'));
287
+ const parts = [me, main, ...hints];
288
+ for (const p of parts) assert.ok(p.l >= 0 && p.r <= width + 0.5 && p.b <= height, `${who}: every part is inside the window`);
289
+ for (let i = 0; i < parts.length; i++) for (let j = i + 1; j < parts.length; j++) assert.ok(apart(parts[i], parts[j]), `${who}: parts ${i} and ${j} keep apart`);
290
+ for (const p of [main, ...hints]) assert.ok(p.w >= 24 && p.h >= 24, `${who}: a control is at least 24px both ways (${Math.round(p.w)}x${Math.round(p.h)})`);
291
+ assert.equal(await page.evaluate(() => document.querySelector('[data-visor-sheet]').getClientRects().length), 1, `${who}: Actions is on the glass at every width`);
292
+ if (width <= 640) {
293
+ assert.equal(hints.length, 1, `${who}: a phone keeps only Actions of the hints`);
294
+ assert.equal(await page.evaluate(() => document.querySelectorAll('.visor__place, .visor__frame--top, .visor__notch').length), 3);
295
+ assert.equal(await page.evaluate(() => [...document.querySelectorAll('.visor__place, .visor__frame--top, .visor__notch')].filter((e) => e.getClientRects().length).length), 0, `${who}: the top of a phone is the scene's`);
296
+ assert.ok(height - main.b >= 52 - 0.5 && height - main.b < 76, `${who}: the foot row sits lower on a phone`);
297
+ } else {
298
+ const [place] = await page.evaluate(box('.visor__place'));
299
+ const [arc] = await page.evaluate(box('.visor__frame--top'));
300
+ assert.ok(place && Math.abs((place.l + place.r) / 2 - width / 2) < 1, `${who}: the place is centred under the top arc`);
301
+ assert.ok(arc.t >= 0 && place.b <= 64, `${who}: the arc and the place keep inside the top inset, above a scene's own top row`);
302
+ }
303
+ await page.click('[data-visor-sheet]');
304
+ const [sheet] = await page.evaluate(box('.visor__sheet'));
305
+ assert.ok(sheet.l >= 0 && sheet.r <= width && sheet.t >= 0 && sheet.b <= Math.min(...parts.map((p) => p.t)) + 0.5, `${who}: the sheet stands inside the window, over the foot row`);
306
+ assert.deepEqual(errors, []);
307
+ await ctx.close();
308
+ }
309
+ });
310
+ }
311
+
312
+ test('the glass is the kit\'s: the cut accent button, ice hints with no box, the name voice, the inset focus ring', { skip }, async () => {
313
+ const { ctx, page } = await open({ helm: true, width: 1440, height: 900 });
314
+ const g = await page.evaluate(() => {
315
+ const cs = (s) => getComputedStyle(document.querySelector(s));
316
+ const root = getComputedStyle(document.documentElement);
317
+ const probe = (v) => { const d = document.createElement('i'); d.style.color = v; document.body.appendChild(d); const c = getComputedStyle(d).color; d.remove(); return c; };
318
+ return {
319
+ mainBg: cs('.visor__main').backgroundColor, accent: probe('var(--accent)'), mainClip: cs('.visor__main').clipPath,
320
+ mainCase: cs('.visor__main').textTransform, hintBg: cs('.visor__hint').backgroundColor, hintBorder: cs('.visor__hint').borderTopWidth,
321
+ hintColor: cs('.visor__hint').color, ice: probe('var(--ink-soft)'), num: cs('.visor__num').color, unit: cs('.visor__unit').color, white: probe('var(--ink)'),
322
+ placeShadow: cs('.visor__place').textShadow, radius: cs('.visor__sprite').borderTopLeftRadius, spriteClip: cs('.visor__sprite').clipPath,
323
+ cut: root.getPropertyValue('--cut-path').trim() !== '',
324
+ };
325
+ });
326
+ assert.equal(g.mainBg, g.accent, 'the one accent fill is the button you press');
327
+ assert.match(g.mainClip, /^polygon\(/, 'cut corner');
328
+ assert.equal(g.mainCase, 'uppercase', 'the name voice');
329
+ assert.equal(g.hintBg, 'rgba(0, 0, 0, 0)');
330
+ assert.equal(g.hintBorder, '0px', 'a hint has no box');
331
+ assert.equal(g.hintColor, g.ice, 'hints are ice');
332
+ assert.equal(g.num, g.white, 'the count is white');
333
+ assert.equal(g.unit, g.ice, 'its word is ice');
334
+ assert.notEqual(g.placeShadow, 'none', 'type over the scene wears the halo');
335
+ assert.equal(g.radius, '0px', 'no round thing on the glass');
336
+ assert.match(g.spriteClip, /^polygon\(/, 'the sprite sits in a cut box');
337
+ await page.keyboard.press('Tab');
338
+ const f = await page.evaluate(() => { const a = document.activeElement; const s = getComputedStyle(a); return { cls: a.className, outline: s.outlineStyle, ring: s.boxShadow }; });
339
+ assert.equal(f.cls, 'visor__main', 'the accent button is the first stop');
340
+ assert.equal(f.outline, 'none');
341
+ assert.match(f.ring, /inset/, 'focus is the inset two-tone ring (--press-focus)');
342
+ await ctx.close();
343
+ });