@bongos/core 1.24.81 → 1.24.83
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 +95 -80
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +4 -0
- package/docs/api/openapi.json +86 -2
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +39 -33
- package/docs/copy-registry.json +107 -52
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +1 -0
- package/docs/page-readings.json +207 -201
- package/modules/builder-settings/hall-view-prefs.js +50 -0
- package/modules/builder-settings/routes/settings.js +32 -0
- package/modules/hall-ui/public/ai-studio.html +3 -3
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city.html +5 -5
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/controls.css +10 -2
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/goal-map.html +5 -5
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +6 -6
- package/modules/hall-ui/public/market-watch-item.html +3 -3
- package/modules/hall-ui/public/market-watch-rules.html +3 -3
- package/modules/hall-ui/public/market-watch.html +3 -3
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/money.html +3 -3
- package/modules/hall-ui/public/my-agents.html +3 -3
- package/modules/hall-ui/public/options-menu.js +22 -1
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/settings-view.js +75 -0
- package/modules/hall-ui/public/settings.css +113 -46
- package/modules/hall-ui/public/settings.html +18 -5
- package/modules/hall-ui/public/shell-game.css +10 -0
- package/modules/hall-ui/public/shell-game.js +98 -33
- package/modules/hall-ui/public/shell.js +4 -3
- package/modules/hall-ui/public/street.html +3 -3
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +149 -61
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +5 -5
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/visor.css +2 -2
- package/modules/hall-ui/public/visor.js +7 -1
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +9 -0
- package/modules/hall-ui/records/game-mode.md +5 -4
- package/modules/hall-ui/records/options-menu.md +11 -0
- package/modules/hall-ui/records/start-screen.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/pref-writes.js +8 -1
- package/src/module-api.js +1 -1
- package/tests/design_tokens_shared.mjs +5 -1
- package/tests/hall_controls.mjs +1 -1
- package/tests/hall_fullscreen.mjs +42 -21
- package/tests/hall_helm.mjs +103 -1
- package/tests/hall_start_screen.mjs +38 -3
- package/tests/hall_topbar_320.mjs +7 -4
- package/tests/hall_view_prefs.mjs +42 -0
- package/tests/hall_visor.mjs +3 -2
- package/tests/nav_permission_atoms.mjs +1 -1
package/release-notes.json
CHANGED
|
@@ -11589,5 +11589,17 @@
|
|
|
11589
11589
|
"id": "1005192",
|
|
11590
11590
|
"text": "The hall now wears the new Helm look: deep night panels, ice-blue text, wide capital titles and cut-corner buttons, all from one shared design system. The start and load screens are rebuilt to match, and the hall is dark only."
|
|
11591
11591
|
}
|
|
11592
|
+
],
|
|
11593
|
+
"1.24.82": [
|
|
11594
|
+
{
|
|
11595
|
+
"id": "1005200",
|
|
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
|
+
}
|
|
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
|
+
}
|
|
11592
11604
|
]
|
|
11593
11605
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
//
|
|
15
15
|
// What it is NOT: storage. Nothing is written to disk and a restart forgets
|
|
16
16
|
// everything; each recorded write is logged so the harness console shows what the
|
|
17
|
-
// page sent. Only these
|
|
17
|
+
// page sent. Only these routes are recorded — every other write is refused
|
|
18
18
|
// exactly as before.
|
|
19
19
|
'use strict';
|
|
20
20
|
|
|
@@ -27,6 +27,7 @@ const wandering = lib('wandering-prefs');
|
|
|
27
27
|
const models = lib('model-alloc-prefs');
|
|
28
28
|
const render = lib('render-prefs');
|
|
29
29
|
const skills = lib('skill-prefs');
|
|
30
|
+
const hallView = lib('hall-view-prefs'); // task 1005200: game or classic
|
|
30
31
|
|
|
31
32
|
// apiPath → the /me slice it answers for (null: the skill prefs, not on /me)
|
|
32
33
|
const ROUTES = {
|
|
@@ -35,6 +36,7 @@ const ROUTES = {
|
|
|
35
36
|
'me/model-allocation': 'model_allocation',
|
|
36
37
|
'me/render': 'render',
|
|
37
38
|
'me/skill-prefs': null,
|
|
39
|
+
'me/hall-view': 'hall_view',
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
function createPrefRecorder({ log = console.log } = {}) {
|
|
@@ -63,6 +65,7 @@ function createPrefRecorder({ log = console.log } = {}) {
|
|
|
63
65
|
if (slice === 'wandering') return wandering.describeForApi(behavior, rank);
|
|
64
66
|
if (slice === 'model_allocation') return models.describeForApi(behavior, instanceModels);
|
|
65
67
|
if (slice === 'render') return render.describeForApi(behavior);
|
|
68
|
+
if (slice === 'hall_view') return hallView.describeForApi(behavior);
|
|
66
69
|
return {
|
|
67
70
|
prefs: skillPrefs, effective: skills.listEffective(skillPrefs), defaults: skills.SKILL_DEFAULTS,
|
|
68
71
|
known_skills: skills.KNOWN_SKILLS, allowed_models: skills.ALLOWED_MODELS,
|
|
@@ -96,6 +99,10 @@ function createPrefRecorder({ log = console.log } = {}) {
|
|
|
96
99
|
v = render.validatePatch(b);
|
|
97
100
|
if (!v.ok) return invalid(v.errors);
|
|
98
101
|
behavior = render.applyPatch(behavior, v.patch);
|
|
102
|
+
} else if (slice === 'hall_view') {
|
|
103
|
+
v = hallView.validatePatch(b);
|
|
104
|
+
if (!v.ok) return invalid(v.errors);
|
|
105
|
+
behavior = hallView.applyView(behavior, v.view);
|
|
99
106
|
} else {
|
|
100
107
|
v = skills.validatePatch(b);
|
|
101
108
|
if (!v.ok) return invalid(v.errors);
|
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.
|
|
84
|
+
const CORE_VERSION = '1.24.83'; // 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', '--
|
|
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
|
});
|
package/tests/hall_controls.mjs
CHANGED
|
@@ -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
|
|
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 ───────────────────────────────────────────
|
|
@@ -94,7 +94,8 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
94
94
|
get cookie() { return ''; }, set cookie(_v) {},
|
|
95
95
|
};
|
|
96
96
|
const assigned = []; // task 1005144: where the page sent the browser (game mode's Escape opens the menu)
|
|
97
|
-
const
|
|
97
|
+
const replaced = []; // task 1005200: a classic page in game mode sends the browser to the city
|
|
98
|
+
const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign(u) { assigned.push(u); }, replace(u) { replaced.push(u); } };
|
|
98
99
|
// `session` is the TAB's sessionStorage: pass one object to several boots and
|
|
99
100
|
// they are page loads in one tab (the trail, task 1004476).
|
|
100
101
|
const sessionStorage = {
|
|
@@ -135,6 +136,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
135
136
|
return {
|
|
136
137
|
store,
|
|
137
138
|
assigned,
|
|
139
|
+
replaced,
|
|
138
140
|
game: () => windowObj.OTBShellGame,
|
|
139
141
|
gameStamp: () => root.dataset.game === 'on',
|
|
140
142
|
// The "how to leave" line: showing = the shell created it and it is not hidden.
|
|
@@ -517,17 +519,36 @@ test('game mode: every hall page carries the switch — none opts out, and each
|
|
|
517
519
|
}
|
|
518
520
|
});
|
|
519
521
|
|
|
520
|
-
|
|
522
|
+
// task 1005200 (owner, 2026-11-11): game OR classic, never both. Inside game mode there is
|
|
523
|
+
// no way back to the bars — the corner button is hidden (style.css) and, pressed
|
|
524
|
+
// anyway, does nothing; Escape is the options menu's, where the view is switched.
|
|
525
|
+
test('game mode: there is no way back to the bars inside it, and a page\'s own memory is never rewritten', () => {
|
|
521
526
|
const store = { 'hall-game': 'on', 'hall-fullscreen:thinking': 'on' };
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
assert.equal(
|
|
525
|
-
assert.equal(store['hall-fullscreen:
|
|
526
|
-
assert.equal(boot({ page: 'goals', optIn: false, store }).on(), true, 'the next load opens without bars again');
|
|
527
|
+
const profile = boot({ page: 'profile', optIn: false, store });
|
|
528
|
+
profile.button().click();
|
|
529
|
+
assert.equal(profile.on(), true, 'the corner button does not leave game mode');
|
|
530
|
+
assert.equal(store['hall-fullscreen:profile'], undefined, 'nothing remembered for the page');
|
|
527
531
|
const sky = boot({ page: 'thinking', store });
|
|
528
532
|
sky.button().click();
|
|
529
|
-
|
|
533
|
+
sky.press('Escape');
|
|
534
|
+
assert.equal(sky.on(), true, 'nor does Escape');
|
|
530
535
|
assert.equal(store['hall-fullscreen:thinking'], 'on', 'the sky\'s own choice survives game mode');
|
|
536
|
+
assert.match(read('style.css'), /html\[data-game="on"\] \.fullscreen-toggle \{ display: none; \}/, 'and the button is not drawn');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('game mode: only the game\'s places are there — a classic page sends the browser to the city', () => {
|
|
540
|
+
const store = { 'hall-game': 'on' };
|
|
541
|
+
assert.deepEqual(boot({ page: 'goals', optIn: false, store }).replaced, ['/'], 'Goals is a classic page');
|
|
542
|
+
assert.deepEqual(boot({ page: 'work', optIn: false, store, path: '/builders/work' }).replaced, ['/builders/'], 'on the apex too');
|
|
543
|
+
for (const page of ['home', 'city', 'goal-map', 'thinking', 'atlas', 'settings', 'profile']) {
|
|
544
|
+
assert.deepEqual(boot({ page, optIn: false, store }).replaced, [], `${page} is a game place`);
|
|
545
|
+
}
|
|
546
|
+
assert.deepEqual(boot({ page: 'goals', optIn: false, store: {} }).replaced, [], 'classic reaches every page');
|
|
547
|
+
const G = boot({ page: 'city', store }).game();
|
|
548
|
+
assert.equal(G.isPlace('/city'), true);
|
|
549
|
+
assert.equal(G.isPlace('/builders/'), true);
|
|
550
|
+
assert.equal(G.isPlace('/work'), false);
|
|
551
|
+
assert.equal(G.isPlace('/blockers?x=1'), false);
|
|
531
552
|
});
|
|
532
553
|
|
|
533
554
|
// task 1005144 (BONGOS-V3 R04, ADR 0399 §2.4 row 6): in game mode the last Escape is the
|
|
@@ -562,35 +583,35 @@ test('game mode: the Escape hand-off is unchanged — a press the page used, or
|
|
|
562
583
|
|
|
563
584
|
test('game mode: set() switches the page live, and turning it off hands each page back its own memory', () => {
|
|
564
585
|
const store = { 'hall-fullscreen:thinking': 'on' };
|
|
565
|
-
const goals = boot({ page: '
|
|
586
|
+
const goals = boot({ page: 'profile', optIn: false, store });
|
|
566
587
|
goals.game().set(true);
|
|
567
588
|
assert.equal(store['hall-game'], 'on');
|
|
568
589
|
assert.equal(goals.on(), true, 'on at once, no reload');
|
|
569
590
|
assert.ok(goals.resizes() >= 1, 'a page that sizes to its box is told to re-measure');
|
|
570
591
|
goals.game().set(false);
|
|
571
|
-
assert.equal(goals.on(), false, '
|
|
592
|
+
assert.equal(goals.on(), false, 'the profile remembered nothing, so its bars return');
|
|
572
593
|
assert.equal(goals.gameStamp(), false);
|
|
573
|
-
assert.equal(store['hall-fullscreen:
|
|
594
|
+
assert.equal(store['hall-fullscreen:profile'], undefined, 'and switching the mode wrote nothing for the page');
|
|
595
|
+
const work = boot({ page: 'work', optIn: false, store: {} });
|
|
596
|
+
work.game().set(true);
|
|
597
|
+
assert.deepEqual(work.replaced, ['/'], 'switched to game on a classic page: to the city');
|
|
574
598
|
const sky = boot({ page: 'thinking', store: Object.assign(store, { 'hall-game': 'on' }) });
|
|
575
599
|
sky.game().set(false);
|
|
576
600
|
assert.equal(sky.on(), true, 'the sky had chosen full screen itself, and keeps it');
|
|
577
601
|
});
|
|
578
602
|
|
|
579
|
-
test('game mode:
|
|
603
|
+
test('game mode: the address does not switch it — Options and the sign-in chooser do (task 1005200)', () => {
|
|
580
604
|
const store = {};
|
|
581
|
-
assert.equal(boot({ page: '
|
|
582
|
-
assert.equal(store['hall-game'], '
|
|
583
|
-
assert.
|
|
584
|
-
assert.equal(boot({ page: 'work', optIn: false, store, search: '?game=off' }).on(), false);
|
|
585
|
-
boot({ page: 'work', optIn: false, store, search: '?game=%22%3E%3Cimg%3E' });
|
|
586
|
-
assert.equal(store['hall-game'], 'off', 'a value that is not on/off is not stored');
|
|
605
|
+
assert.equal(boot({ page: 'profile', optIn: false, store, search: '?game=on' }).on(), false);
|
|
606
|
+
assert.equal(store['hall-game'], undefined, 'nothing stored from the URL');
|
|
607
|
+
assert.doesNotMatch(read('shell-game.js'), /get\('game'\)/, 'the ?game= reader is gone');
|
|
587
608
|
});
|
|
588
609
|
|
|
589
|
-
test('game mode
|
|
610
|
+
test('game mode never says how to leave full screen (there is no leaving); outside it the shell\'s rule stands', () => {
|
|
590
611
|
const store = { 'hall-game': 'on' };
|
|
591
612
|
const session = {};
|
|
592
|
-
assert.equal(boot({ page: '
|
|
593
|
-
assert.equal(boot({ page: '
|
|
613
|
+
assert.equal(boot({ page: 'profile', optIn: false, store, session }).hintShowing(), false, 'no line in game mode');
|
|
614
|
+
assert.equal(boot({ page: 'city', store, session }).hintShowing(), false);
|
|
594
615
|
const own = { 'hall-fullscreen:thinking': 'on' };
|
|
595
616
|
assert.equal(boot({ page: 'thinking', store: own, session: {} }).hintShowing(), true);
|
|
596
617
|
assert.equal(boot({ page: 'thinking', store: own, session: {} }).hintShowing(), true, 'a remembered page says it on every open, as before');
|
package/tests/hall_helm.mjs
CHANGED
|
@@ -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
|
+
});
|
|
@@ -188,12 +188,21 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
188
188
|
const { page, errors } = await signedIn(ctx);
|
|
189
189
|
await page.waitForSelector('#game-start');
|
|
190
190
|
assert.deepEqual(await state(page), { start: true, welcome: false, picker: false, game: null, full: null },
|
|
191
|
-
'the start screen, not the welcome and not the picker; game mode is not on until
|
|
191
|
+
'the start screen, not the welcome and not the picker; game mode is not on until it is chosen');
|
|
192
|
+
// task 1005200: first the chooser — game or classic — then, for game, the start screen.
|
|
193
|
+
const views = await page.evaluate(() => ({
|
|
194
|
+
items: Array.from(document.querySelectorAll('.gs__items:not([hidden]) .gs__item')).map((e) => e.textContent),
|
|
195
|
+
focus: document.activeElement.textContent,
|
|
196
|
+
}));
|
|
197
|
+
assert.deepEqual(views, { items: ['GameThe city, full screen', 'ClassicThe hall with its sidebar'], focus: 'GameThe city, full screen' });
|
|
198
|
+
await page.keyboard.press('Enter');
|
|
199
|
+
await page.waitForSelector('#gs-items:not([hidden])');
|
|
200
|
+
assert.equal((await state(page)).game, 'on', 'choosing Game turns game mode on');
|
|
192
201
|
const menu = await page.evaluate(() => ({
|
|
193
202
|
title: document.getElementById('gs-title').textContent,
|
|
194
|
-
items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).map((e) => e.textContent),
|
|
203
|
+
items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras):not([hidden]) .gs__item')).map((e) => e.textContent),
|
|
195
204
|
focus: document.activeElement.textContent,
|
|
196
|
-
small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
|
|
205
|
+
small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras):not([hidden]) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
|
|
197
206
|
}));
|
|
198
207
|
assert.deepEqual(menu, { title: 'Harbor', items: ['Start', 'Options', 'Achievements', 'Extras', 'Quit'], focus: 'Start', small: 0 });
|
|
199
208
|
|
|
@@ -253,6 +262,29 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
253
262
|
await ctx.close();
|
|
254
263
|
});
|
|
255
264
|
|
|
265
|
+
// task 1005200: Classic at the chooser — the screen goes, game mode is off, and the account
|
|
266
|
+
// is told. The view chosen last time is the one marked and focused.
|
|
267
|
+
test('choosing Classic at sign-in closes the screen, leaves game mode off, and saves it to the account', { skip }, async () => {
|
|
268
|
+
const ctx = await hall();
|
|
269
|
+
const saved = [];
|
|
270
|
+
await ctx.route(ORIGIN + '/api/bongos/me/hall-view', (route) => {
|
|
271
|
+
if (route.request().method() === 'PATCH') saved.push(JSON.parse(route.request().postData()));
|
|
272
|
+
return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ view: null }) });
|
|
273
|
+
});
|
|
274
|
+
const page = await ctx.newPage();
|
|
275
|
+
await page.addInitScript(() => { try { localStorage.setItem('hall-welcome-signin', '1'); localStorage.setItem('hall-game', 'off'); } catch (_) { /* no storage */ } });
|
|
276
|
+
await page.goto(ORIGIN + '/');
|
|
277
|
+
await page.waitForFunction(() => !!window.OTBShell && !!window.OTBWelcome && !!window.OTBShellGame);
|
|
278
|
+
await page.evaluate(() => { const me = { authed: true, rank: 'metic', builder: { id: 7, display_name: 'Ada', github_login: 'ada' } }; window.OTBShell.applyAccess(me); window.OTBShell.applyAccess(me); });
|
|
279
|
+
await page.waitForSelector('#game-start');
|
|
280
|
+
assert.equal(await page.evaluate(() => document.activeElement.textContent), 'ClassicLast timeThe hall with its sidebar', 'the view chosen last time is focused and marked');
|
|
281
|
+
await page.keyboard.press('Enter');
|
|
282
|
+
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
283
|
+
assert.equal((await state(page)).game, null);
|
|
284
|
+
assert.deepEqual(saved, [{ view: 'classic' }]);
|
|
285
|
+
await ctx.close();
|
|
286
|
+
});
|
|
287
|
+
|
|
256
288
|
test('on a phone the panel is the whole width, nothing scrolls sideways and every choice clears the floor (task 1005192)', { skip }, async () => {
|
|
257
289
|
for (const [w, h] of [[390, 844], [320, 700]]) {
|
|
258
290
|
const ctx = await hall({ viewport: { width: w, height: h } });
|
|
@@ -266,6 +298,7 @@ test('on a phone the panel is the whole width, nothing scrolls sideways and ever
|
|
|
266
298
|
};
|
|
267
299
|
});
|
|
268
300
|
assert.deepEqual(m, { x: 0, w, sideways: 0, small: 0 }, `at ${w}px`);
|
|
301
|
+
await page.click('[data-gs="view-game"]');
|
|
269
302
|
await page.click('[data-gs="start"]');
|
|
270
303
|
await page.waitForSelector('#gs-load:not([hidden])');
|
|
271
304
|
assert.equal(await page.evaluate(() => { const gs = document.getElementById('game-start'); return gs.scrollWidth - gs.clientWidth; }), 0, `the load screen fits ${w}px`);
|
|
@@ -279,6 +312,7 @@ test('Quit closes the tab, and says so when the browser keeps it open', { skip }
|
|
|
279
312
|
const { page } = await signedIn(ctx);
|
|
280
313
|
await page.waitForSelector('#game-start');
|
|
281
314
|
await page.evaluate(() => { window.__closed = 0; window.close = () => { window.__closed += 1; }; });
|
|
315
|
+
await page.click('[data-gs="view-game"]');
|
|
282
316
|
await page.click('[data-gs="quit"]');
|
|
283
317
|
await page.waitForSelector('#gs-note:not([hidden])');
|
|
284
318
|
assert.equal(await page.evaluate(() => window.__closed), 1);
|
|
@@ -291,6 +325,7 @@ test('with no city to draw, or with reduced motion, the still and the tip stand
|
|
|
291
325
|
const ctx = await hall(opts);
|
|
292
326
|
const { page, errors } = await signedIn(ctx);
|
|
293
327
|
await page.waitForSelector('#game-start');
|
|
328
|
+
await page.click('[data-gs="view-game"]');
|
|
294
329
|
await page.click('[data-gs="start"]');
|
|
295
330
|
await page.waitForSelector('#gs-load:not([hidden])');
|
|
296
331
|
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
|
@@ -99,7 +99,9 @@ for (const scheme of ['light', 'dark']) {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
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('--
|
|
120
|
+
pill: root.getPropertyValue('--radius-pill').trim(),
|
|
119
121
|
};
|
|
120
122
|
}).finally(() => ctx.close());
|
|
121
|
-
assert.equal(s.
|
|
122
|
-
assert.equal(s.
|
|
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
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// tests/hall_view_prefs.mjs — the game-or-classic choice kept on the account
|
|
2
|
+
// (task 1005200, modules/builder-settings/hall-view-prefs.js), and the two routes
|
|
3
|
+
// that read and write it.
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
13
|
+
const prefs = require('../modules/builder-settings/hall-view-prefs.js');
|
|
14
|
+
|
|
15
|
+
test('never chosen is null, not a default view: the sign-in chooser asks', () => {
|
|
16
|
+
assert.equal(prefs.storedView({}), null);
|
|
17
|
+
assert.equal(prefs.storedView(null), null);
|
|
18
|
+
assert.equal(prefs.storedView({ hall_view: 'fullscreen' }), null, 'an unknown value is not a choice');
|
|
19
|
+
assert.deepEqual(prefs.describeForApi({ hall_view: 'game' }), { view: 'game', views: ['game', 'classic'] });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('a PATCH takes exactly { view: game | classic }', () => {
|
|
23
|
+
assert.deepEqual(prefs.validatePatch({ view: 'classic' }), { ok: true, view: 'classic' });
|
|
24
|
+
assert.equal(prefs.validatePatch({}).ok, false, 'no view');
|
|
25
|
+
assert.equal(prefs.validatePatch({ view: 'both' }).ok, false);
|
|
26
|
+
assert.equal(prefs.validatePatch({ view: 'game', extra: 1 }).ok, false, 'unknown keys refused');
|
|
27
|
+
assert.equal(prefs.validatePatch([]).ok, false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('saving the view keeps every other behaviour preference', () => {
|
|
31
|
+
const before = { wandering: 2, widgets: 'rich', interaction: { verbosity: 'concise' } };
|
|
32
|
+
const after = prefs.applyView(before, 'game');
|
|
33
|
+
assert.deepEqual(after, { ...before, hall_view: 'game' });
|
|
34
|
+
assert.equal(before.hall_view, undefined, 'pure: the input is not changed');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('the routes: own builder only, GET and PATCH on /me/hall-view', () => {
|
|
38
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/builder-settings/routes/settings.js'), 'utf8');
|
|
39
|
+
assert.match(src, /router\.get\('\/me\/hall-view', api\.requireBuilder,/);
|
|
40
|
+
assert.match(src, /router\.patch\('\/me\/hall-view', api\.requireBuilder,/);
|
|
41
|
+
assert.match(src, /hallViewPrefs\.applyView\(existing, v\.view\)/, 'writes through the pure shaper');
|
|
42
|
+
});
|
package/tests/hall_visor.mjs
CHANGED
|
@@ -151,7 +151,7 @@ test('in game mode the glass shows the place, the main action, the sprite and th
|
|
|
151
151
|
}));
|
|
152
152
|
assert.equal(g.shown, 'block');
|
|
153
153
|
assert.equal(g.place, 'Test city');
|
|
154
|
-
assert.match(g.main, /^Claim a task
|
|
154
|
+
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
155
|
assert.equal(g.who, 'Artisan');
|
|
156
156
|
assert.equal(g.credits, '56,021 credits');
|
|
157
157
|
assert.equal(g.sprite, '/builders/characters/artisan.png');
|
|
@@ -189,7 +189,8 @@ test('the sheet lists the character\'s actions, then this place, and runs a scen
|
|
|
189
189
|
const { ctx, page } = await open();
|
|
190
190
|
await page.click('[data-visor-sheet]');
|
|
191
191
|
const rows = await page.$$eval('.visor__sheet .visor__row', (r) => r.map((x) => [...x.children].map((c) => c.textContent).join(' ')));
|
|
192
|
-
|
|
192
|
+
// task 1005200: Claim a task and Blockers open classic pages, so they are off the glass.
|
|
193
|
+
assert.deepEqual(rows, ['Goal sky G', 'Your thinking T', 'Back to centre 0', 'Look around drag', 'Options Esc']);
|
|
193
194
|
await page.click('[data-visor-scene="0"]');
|
|
194
195
|
assert.deepEqual(await page.evaluate(() => ({ centred: window.centred, open: OTBVisor.isOpen() })), { centred: true, open: false });
|
|
195
196
|
await ctx.close();
|
|
@@ -103,7 +103,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
103
103
|
// task 1005070: the remembered nav (the rail paints before /me).
|
|
104
104
|
// task 1005088: the sidebar mark is drawn by reference to the one brand mark.
|
|
105
105
|
// task 1005144: My agents, Project settings, Permissions and Modules left for the options menu.
|
|
106
|
-
const SHELL_SHA = '
|
|
106
|
+
const SHELL_SHA = '89f6f166ed22a8ee';
|
|
107
107
|
|
|
108
108
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
109
109
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|