@bongos/core 1.19.1051 → 1.19.1052

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 (50) hide show
  1. package/.bongos-core.json +60 -55
  2. package/docs/copy-inventory.md +85 -79
  3. package/docs/copy-registry.json +212 -138
  4. package/docs/module-api-changelog.md +2 -0
  5. package/modules/hall-ui/public/atlas.html +1 -1
  6. package/modules/hall-ui/public/blockers.html +1 -1
  7. package/modules/hall-ui/public/board-room.html +1 -1
  8. package/modules/hall-ui/public/collab.html +1 -1
  9. package/modules/hall-ui/public/copy-desk.html +1 -1
  10. package/modules/hall-ui/public/deploy.html +1 -1
  11. package/modules/hall-ui/public/diagrams.html +1 -1
  12. package/modules/hall-ui/public/drachmae.html +1 -1
  13. package/modules/hall-ui/public/fleet.html +1 -1
  14. package/modules/hall-ui/public/gate.html +1 -1
  15. package/modules/hall-ui/public/goals.html +1 -1
  16. package/modules/hall-ui/public/government.html +1 -1
  17. package/modules/hall-ui/public/idea.html +1 -1
  18. package/modules/hall-ui/public/ideas.html +1 -1
  19. package/modules/hall-ui/public/index.html +1 -1
  20. package/modules/hall-ui/public/modules.html +1 -1
  21. package/modules/hall-ui/public/primer.html +1 -1
  22. package/modules/hall-ui/public/profile.html +1 -1
  23. package/modules/hall-ui/public/project-settings.html +1 -1
  24. package/modules/hall-ui/public/ranks.html +1 -1
  25. package/modules/hall-ui/public/roadmap.html +1 -1
  26. package/modules/hall-ui/public/roster.html +1 -1
  27. package/modules/hall-ui/public/sessions.html +1 -1
  28. package/modules/hall-ui/public/settings.html +1 -1
  29. package/modules/hall-ui/public/shell.js +77 -1
  30. package/modules/hall-ui/public/sky-data.js +13 -2
  31. package/modules/hall-ui/public/sky-interaction.js +13 -8
  32. package/modules/hall-ui/public/sky-layout.js +6 -1
  33. package/modules/hall-ui/public/studio.html +10 -4
  34. package/modules/hall-ui/public/studio.states.json +76 -1
  35. package/modules/hall-ui/public/style.css +53 -0
  36. package/modules/hall-ui/public/task.html +1 -1
  37. package/modules/hall-ui/public/thinking.css +54 -16
  38. package/modules/hall-ui/public/thinking.html +38 -18
  39. package/modules/hall-ui/public/thinking.states.json +19 -2
  40. package/modules/hall-ui/public/watch.html +1 -1
  41. package/modules/hall-ui/public/work.html +1 -1
  42. package/modules/hall-ui/records/sky.md +9 -0
  43. package/package-lock.json +2 -2
  44. package/package.json +1 -1
  45. package/release-notes.json +6 -0
  46. package/src/module-api.js +1 -1
  47. package/tests/goal_map_page.mjs +5 -3
  48. package/tests/hall_audit.mjs +2 -1
  49. package/tests/hall_fullscreen.mjs +196 -0
  50. package/tests/nav_permission_atoms.mjs +1 -1
@@ -0,0 +1,196 @@
1
+ // tests/hall_fullscreen.mjs — the hall's shared full-screen switch (task 1004327,
2
+ // BV2.TW20, goal 1000095; owner decision 2026-09-27).
3
+ //
4
+ // The studio and Your thinking sit in the hall chrome, and each carries ONE
5
+ // switch that hides the sidebar and the top bar so the page fills the window.
6
+ // The switch is the shell's, not a page's: a page opts in with
7
+ // <body data-fullscreen-switch>, and the shell renders the button, keeps the
8
+ // state on html[data-fullscreen="on"], remembers it PER PAGE in localStorage and
9
+ // lets Escape leave. These cases boot the REAL shell.js in a DOM stub and drive
10
+ // it the way a reader does — press the button, reload, press Escape — so a
11
+ // switch that renders but does not remember, or an Escape that fights the sky's
12
+ // own use of the key, fails here. The two source assertions at the end are
13
+ // labelled as such: the CSS half (what "hidden" means) is proven by the kit
14
+ // renders in the ship notes, not by a regex.
15
+ import assert from 'node:assert/strict';
16
+ import { test } from 'node:test';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import vm from 'node:vm';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
+ const HALL = path.join(ROOT, 'modules/hall-ui/public');
24
+ const SHELL = fs.readFileSync(path.join(HALL, 'shell.js'), 'utf8');
25
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
26
+
27
+ // Boot the shell once — one page load. `store` is the browser's localStorage and
28
+ // is SHARED across boots on purpose: a second boot over the same store is a reload.
29
+ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = false } = {}) {
30
+ const els = new Map();
31
+ const windowListeners = {};
32
+ const timers = [];
33
+ const makeEl = (id) => {
34
+ const attrs = {};
35
+ return {
36
+ id, dataset: {}, style: {}, textContent: '', hidden: false, _html: '', title: '',
37
+ classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
38
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; }, removeAttribute(k) { delete attrs[k]; },
39
+ addEventListener(ev, fn) { (this._on ||= {})[ev] = fn; },
40
+ querySelector() { return null; }, querySelectorAll() { return []; },
41
+ appendChild() {}, focus() {}, click() { if (this._on && this._on.click) this._on.click({ target: this }); },
42
+ insertAdjacentElement() {}, remove() {},
43
+ set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
44
+ };
45
+ };
46
+ // The topbar's innerHTML is the shell's own markup; an id the shell rendered
47
+ // into it resolves only if that markup really carries it, so a button the
48
+ // shell forgot to render cannot be clicked into existence by the stub.
49
+ const topbar = makeEl('app-topbar');
50
+ const getElementById = (id) => {
51
+ if (id === 'app-topbar') return topbar;
52
+ if (/^(fullscreen-toggle|theme-toggle|zoom-toggle|topbar-menu|topbar-title|topbar-jump)$/.test(id)
53
+ && !topbar._html.includes(`id="${id}"`)) return null;
54
+ if (!els.has(id)) els.set(id, makeEl(id));
55
+ return els.get(id);
56
+ };
57
+ const body = {
58
+ dataset: { page },
59
+ hasAttribute: (k) => optIn && k === 'data-fullscreen-switch',
60
+ };
61
+ const documentObj = {
62
+ body,
63
+ documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
64
+ getElementById,
65
+ querySelectorAll() { return []; },
66
+ createElement() { return makeEl(null); },
67
+ addEventListener() {}, dispatchEvent() {},
68
+ get cookie() { return ''; }, set cookie(_v) {},
69
+ };
70
+ const location = { hostname: 'h', origin: 'https://h', pathname: '/' + page, hash: '', search: '', assign() {}, replace() {} };
71
+ const localStorage = {
72
+ getItem: (k) => { if (storageThrows) throw new Error('SecurityError'); return k in store ? store[k] : null; },
73
+ setItem: (k, v) => { if (storageThrows) throw new Error('SecurityError'); store[k] = String(v); },
74
+ };
75
+ let resizes = 0;
76
+ const windowObj = {
77
+ __BRANDING__: {}, __MODULES__: { enabled: {} },
78
+ BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
79
+ matchMedia: () => ({ matches: false, addEventListener() {} }),
80
+ localStorage, location,
81
+ requestAnimationFrame() {}, scrollTo() {},
82
+ addEventListener(type, fn) { (windowListeners[type] ||= []).push(fn); },
83
+ dispatchEvent(ev) { if (ev && ev.type === 'resize') resizes += 1; return true; },
84
+ setTimeout: (fn) => { timers.push(fn); return timers.length; },
85
+ };
86
+ const sandbox = {
87
+ window: windowObj, document: documentObj, location, console, clearTimeout() {}, queueMicrotask,
88
+ setTimeout: windowObj.setTimeout,
89
+ Event: class { constructor(t) { this.type = t; } },
90
+ CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } },
91
+ };
92
+ sandbox.globalThis = sandbox;
93
+ vm.createContext(sandbox);
94
+ vm.runInContext(SHELL, sandbox, { filename: 'shell.js' });
95
+ const root = documentObj.documentElement;
96
+ return {
97
+ store,
98
+ button: () => getElementById('fullscreen-toggle'),
99
+ on: () => root.dataset.fullscreen === 'on',
100
+ resizes: () => resizes,
101
+ // Press a key the way the browser dispatches it: every window listener,
102
+ // then the page may mark the press used, then the queued timers run.
103
+ press(key, { target = { tagName: 'CANVAS' }, pageUses = false } = {}) {
104
+ const ev = { key, target, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
105
+ for (const fn of windowListeners.keydown || []) fn(ev);
106
+ if (pageUses) ev.preventDefault();
107
+ while (timers.length) timers.shift()();
108
+ },
109
+ };
110
+ }
111
+
112
+ test('a page that does not opt in gets no switch, and never goes full screen', () => {
113
+ const store = { 'hall-fullscreen:work': 'on' };
114
+ const p = boot({ page: 'work', optIn: false, store });
115
+ assert.equal(p.button(), null, 'no button rendered');
116
+ assert.equal(p.on(), false, 'a stored value for a page with no switch is ignored');
117
+ });
118
+
119
+ test('an opted-in page renders ONE switch in the top bar, off by default', () => {
120
+ const p = boot();
121
+ const btn = p.button();
122
+ assert.ok(btn, 'the switch is in the top bar');
123
+ assert.equal(p.on(), false);
124
+ assert.equal(btn.getAttribute('aria-pressed'), 'false');
125
+ assert.equal(btn.getAttribute('aria-label'), 'Full screen');
126
+ });
127
+
128
+ test('the switch hides the chrome, the same button brings it back', () => {
129
+ const p = boot();
130
+ p.button().click();
131
+ assert.equal(p.on(), true, 'html[data-fullscreen="on"]');
132
+ assert.equal(p.button().getAttribute('aria-pressed'), 'true');
133
+ assert.equal(p.button().getAttribute('aria-label'), 'Leave full screen');
134
+ assert.ok(p.resizes() >= 1, 'a page that sizes to its box is told to re-measure');
135
+ p.button().click();
136
+ assert.equal(p.on(), false, 'and back');
137
+ });
138
+
139
+ test('THE CHOICE SURVIVES A RELOAD — and it is remembered per page', () => {
140
+ const store = {};
141
+ boot({ page: 'thinking', store }).button().click();
142
+ assert.equal(store['hall-fullscreen:thinking'], 'on');
143
+ assert.equal(boot({ page: 'thinking', store }).on(), true, 'the reload opens in full screen');
144
+ assert.equal(boot({ page: 'studio', store }).on(), false, 'the studio did not follow the sky');
145
+ boot({ page: 'thinking', store }).button().click();
146
+ assert.equal(boot({ page: 'thinking', store }).on(), false, 'leaving is remembered too');
147
+ });
148
+
149
+ test('Escape leaves full screen', () => {
150
+ const p = boot({ store: { 'hall-fullscreen:thinking': 'on' } });
151
+ assert.equal(p.on(), true);
152
+ p.press('Escape');
153
+ assert.equal(p.on(), false);
154
+ });
155
+
156
+ test('an Escape the PAGE used (the sky\'s tour, menu or pick) does not also leave', () => {
157
+ const p = boot({ store: { 'hall-fullscreen:thinking': 'on' } });
158
+ p.press('Escape', { pageUses: true });
159
+ assert.equal(p.on(), true, 'the page marked the press used, after the shell heard it');
160
+ });
161
+
162
+ test('an Escape inside a text field belongs to the field', () => {
163
+ const p = boot({ store: { 'hall-fullscreen:thinking': 'on' } });
164
+ p.press('Escape', { target: { tagName: 'INPUT' } });
165
+ assert.equal(p.on(), true);
166
+ p.press('x');
167
+ assert.equal(p.on(), true, 'other keys are not the switch\'s');
168
+ });
169
+
170
+ test('storage that throws (a private window) leaves the chrome showing and the switch working', () => {
171
+ const p = boot({ storageThrows: true });
172
+ assert.equal(p.on(), false);
173
+ p.button().click();
174
+ assert.equal(p.on(), true, 'the switch still works for this page load');
175
+ });
176
+
177
+ test('the studio and Your thinking opt in, inside the hall chrome; the project sky does not (yet)', () => {
178
+ for (const f of ['studio.html', 'thinking.html']) {
179
+ const html = read(f);
180
+ assert.match(html, /<body[^>]*\bdata-fullscreen-switch\b/, `${f} opts in`);
181
+ for (const id of ['app-sidebar', 'app-topbar']) assert.match(html, new RegExp(`id="${id}"`), `${f} carries #${id}`);
182
+ assert.match(html, /<main id="hall-main"/, `${f}: the skip link's target`);
183
+ assert.equal((html.replace(/<!--[\s\S]*?-->/g, '').match(/<header\b/g) || []).length, 1, `${f}: the top bar is the one <header>`);
184
+ }
185
+ assert.doesNotMatch(read('goal-map.html'), /data-fullscreen-switch|shell\.js/, '/goal-map keeps its standalone form');
186
+ });
187
+
188
+ // SOURCE ASSERTION (the CSS half): full screen hides the sidebar and the foot
189
+ // but keeps the top bar IN the document, collapsed to its switch — so the page
190
+ // keeps one <header> and the way back is where the way in was.
191
+ test('source: full screen hides the sidebar and foot, and collapses the top bar rather than removing it', () => {
192
+ const css = read('style.css');
193
+ assert.match(css, /html\[data-fullscreen="on"\] \.app-sidebar,\s*html\[data-fullscreen="on"\] \.app-scrim,\s*html\[data-fullscreen="on"\] \.app-foot \{ display: none; \}/);
194
+ assert.match(css, /html\[data-fullscreen="on"\] \.topbar-side > :not\(\.fullscreen-toggle\)/);
195
+ assert.doesNotMatch(css, /html\[data-fullscreen="on"\] \.app-topbar \{ display: none/);
196
+ });
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = '6b28297270d1e1f0';
90
+ const SHELL_SHA = '7f66604d74d544e8';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);