@bongos/core 1.21.40 → 1.21.41

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 (72) hide show
  1. package/.bongos-core.json +128 -68
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +16 -0
  4. package/clients/bongos-client/index.cjs +16 -0
  5. package/clients/bongos-client/index.d.ts +25 -0
  6. package/clients/bongos-client/index.mjs +16 -0
  7. package/docs/api/openapi.json +492 -3
  8. package/docs/api-reference.md +13 -1
  9. package/docs/copy-inventory.md +29 -12
  10. package/docs/copy-registry.json +177 -14
  11. package/docs/file-map.md +1 -0
  12. package/docs/module-api-changelog.md +2 -0
  13. package/docs/page-readings.json +229 -229
  14. package/modules/hall-ui/public/atlas.html +3 -3
  15. package/modules/hall-ui/public/blockers.html +3 -3
  16. package/modules/hall-ui/public/board-room.html +3 -3
  17. package/modules/hall-ui/public/city.html +3 -3
  18. package/modules/hall-ui/public/collab.html +3 -3
  19. package/modules/hall-ui/public/commands.html +3 -3
  20. package/modules/hall-ui/public/copy-desk.html +3 -3
  21. package/modules/hall-ui/public/craft-mode.js +62 -0
  22. package/modules/hall-ui/public/deploy.html +3 -3
  23. package/modules/hall-ui/public/diagrams.html +3 -3
  24. package/modules/hall-ui/public/drachmae.html +3 -3
  25. package/modules/hall-ui/public/fleet.html +3 -3
  26. package/modules/hall-ui/public/gate.html +3 -3
  27. package/modules/hall-ui/public/goal-map.html +3 -3
  28. package/modules/hall-ui/public/goals.html +3 -3
  29. package/modules/hall-ui/public/government.html +3 -3
  30. package/modules/hall-ui/public/idea.html +3 -3
  31. package/modules/hall-ui/public/ideas.html +3 -3
  32. package/modules/hall-ui/public/index.html +3 -3
  33. package/modules/hall-ui/public/modules.html +3 -3
  34. package/modules/hall-ui/public/page-notes.js +320 -0
  35. package/modules/hall-ui/public/primer.html +3 -3
  36. package/modules/hall-ui/public/profile.html +3 -3
  37. package/modules/hall-ui/public/project-settings.html +3 -3
  38. package/modules/hall-ui/public/ranks.html +3 -3
  39. package/modules/hall-ui/public/roadmap.html +3 -3
  40. package/modules/hall-ui/public/roster.html +3 -3
  41. package/modules/hall-ui/public/sessions.html +3 -3
  42. package/modules/hall-ui/public/settings.html +3 -3
  43. package/modules/hall-ui/public/street.html +3 -3
  44. package/modules/hall-ui/public/studio.html +3 -3
  45. package/modules/hall-ui/public/style.css +144 -0
  46. package/modules/hall-ui/public/task.html +3 -3
  47. package/modules/hall-ui/public/thinking.html +3 -3
  48. package/modules/hall-ui/public/tweak-editor.html +3 -3
  49. package/modules/hall-ui/public/watch.html +3 -3
  50. package/modules/hall-ui/public/work.html +3 -3
  51. package/modules/hall-ui/records/page-notes.md +16 -0
  52. package/modules/page-notes/CLAUDE.md +15 -0
  53. package/modules/page-notes/db.js +147 -0
  54. package/modules/page-notes/migrations/page_notes_001_notes.sql +68 -0
  55. package/modules/page-notes/module.json +13 -0
  56. package/modules/page-notes/notes.js +174 -0
  57. package/modules/page-notes/routes/page-notes.js +172 -0
  58. package/modules/page-notes/split.js +68 -0
  59. package/modules/provisioning/starter-bundles.js +1 -0
  60. package/modules/ui-design/kit/serve.js +1 -1
  61. package/package-lock.json +2 -2
  62. package/package.json +1 -1
  63. package/release-notes.json +6 -0
  64. package/scripts/gds/publish-manifest.js +1 -0
  65. package/src/bongos/module-scope-map.js +4 -0
  66. package/src/bongos/routes.js +4 -1
  67. package/src/module-api.js +1 -1
  68. package/tests/hall_tweak_editor.mjs +1 -1
  69. package/tests/module_loader.mjs +1 -1
  70. package/tests/page_notes.mjs +289 -0
  71. package/tests/page_notes_db.mjs +140 -0
  72. package/tests/page_notes_panel.mjs +145 -0
@@ -0,0 +1,145 @@
1
+ // tests/page_notes_panel.mjs — the hall side of the artist's page feedback doc
2
+ // (task 1004496): the palette tool craft-mode.js puts in the top bar, and the
3
+ // reader in page-notes.js. The server half is tests/page_notes.mjs.
4
+ //
5
+ // Runs the REAL craft-mode.js over a small DOM: the palette shows in Artistry
6
+ // alone and leaves when the mode changes, pressing it loads page-notes.js once
7
+ // (same ?v as craft-mode.js) and a ?note= link opens the doc in any mode. Then
8
+ // the REAL page-notes.js reader: it escapes the doc and shows only this module's
9
+ // own images, which is what keeps a doc from pulling a picture off another site.
10
+ import assert from 'node:assert/strict';
11
+ import { test } from 'node:test';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import vm from 'node:vm';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+ const HALL = path.join(ROOT, 'modules/hall-ui/public');
19
+ const CRAFT = fs.readFileSync(path.join(HALL, 'craft-mode.js'), 'utf8');
20
+ const NOTES = fs.readFileSync(path.join(HALL, 'page-notes.js'), 'utf8');
21
+
22
+ function node(tag, id, cls) {
23
+ const attrs = {};
24
+ const listeners = {};
25
+ const el = {
26
+ tagName: tag, id: id || '', className: cls || '', dataset: {}, children: [], parentNode: null, innerHTML: '', title: '', type: '', src: '',
27
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
28
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
29
+ get nextSibling() { const s = this.parentNode && this.parentNode.children; return s ? s[s.indexOf(this) + 1] || null : null; },
30
+ appendChild(c) { c.parentNode = this; this.children.push(c); if (this.onAppend) this.onAppend(c); return c; },
31
+ insertBefore(c, ref) { c.parentNode = this; const at = ref ? this.children.indexOf(ref) : -1; if (at === -1) this.children.push(c); else this.children.splice(at, 0, c); return c; },
32
+ removeChild(c) { this.children.splice(this.children.indexOf(c), 1); c.parentNode = null; return c; },
33
+ querySelector(sel) { return all(this).find((n) => sel === '.' + n.className) || null; },
34
+ querySelectorAll() { return []; },
35
+ addEventListener(t, fn) { (listeners[t] ||= []).push(fn); },
36
+ click() { (listeners.click || []).forEach((fn) => fn({ target: el })); },
37
+ classList: { toggle() {} },
38
+ };
39
+ return el;
40
+ }
41
+ function all(root) { return root.children.flatMap((c) => [c, ...all(c)]); }
42
+
43
+ function boot({ session = {}, search = '' } = {}) {
44
+ const sidebar = node('NAV', 'app-sidebar');
45
+ const topbar = node('DIV', 'app-topbar');
46
+ const side = topbar.appendChild(node('DIV', '', 'topbar-side'));
47
+ const sw = side.appendChild(node('DIV', 'craft-switch'));
48
+ const head = node('HEAD');
49
+ const loaded = [];
50
+ const root = node('HTML');
51
+ const everything = () => [sidebar, topbar, ...all(sidebar), ...all(topbar)];
52
+ const document = {
53
+ documentElement: root, body: node('BODY'), head, activeElement: null,
54
+ currentScript: { src: 'https://h/builders/craft-mode.js?v=2026-10-04-test' },
55
+ getElementById: (id) => everything().find((n) => n.id === id) || null,
56
+ querySelectorAll: (sel) => (sel === '[data-craft-tool]' ? everything().filter((n) => n.hasAttribute('data-craft-tool')) : []),
57
+ createElement: (t) => node(t.toUpperCase()),
58
+ addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
59
+ };
60
+ const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
61
+ const opened = [];
62
+ const window = { localStorage: mk({}), sessionStorage: mk(session), matchMedia: () => ({ matches: false }), location: { search } };
63
+ // "Loading" page-notes.js: record the tag, then define the API it would define.
64
+ head.onAppend = (tag) => {
65
+ loaded.push(tag.src);
66
+ window.OTBPageNotes = { open: () => opened.push('open'), openNote: (id) => opened.push('note ' + id) };
67
+ tag.onload();
68
+ };
69
+ const sandbox = { window, document, Promise, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
70
+ vm.createContext(sandbox);
71
+ vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
72
+ window.OTBCraftMode.mount();
73
+ return { craft: window.OTBCraftMode, side, sw, loaded, opened, tools: () => all(side).filter((n) => n.hasAttribute('data-craft-tool')) };
74
+ }
75
+ const flush = () => new Promise((r) => setTimeout(r, 0));
76
+
77
+ test('THE PALETTE: in the top bar in Artistry alone, right before the switch, and gone when the mode changes', () => {
78
+ const p = boot({ session: { 'hall-craft': 'artistry' } });
79
+ assert.deepEqual(p.tools().map((t) => t.id), ['craft-tool-palette']);
80
+ assert.equal(p.tools()[0].nextSibling, p.sw, 'it sits right before the mode switch');
81
+ assert.equal(p.tools()[0].getAttribute('aria-label'), 'Write feedback on this page');
82
+ for (const m of ['building', 'ideating', 'governing', 'legacy']) {
83
+ p.craft.choose(m);
84
+ assert.equal(p.tools().length, 0, `no palette in ${m}`);
85
+ }
86
+ p.craft.choose('artistry');
87
+ p.craft.choose('artistry');
88
+ assert.equal(p.tools().length, 1, 'choosing Artistry twice draws one palette');
89
+ assert.equal(boot().tools().length, 0, 'and none with no mode at all');
90
+ });
91
+
92
+ test('pressing it loads page-notes.js once, at the same version as craft-mode.js, and opens it', async () => {
93
+ const p = boot({ session: { 'hall-craft': 'artistry' } });
94
+ assert.deepEqual(p.loaded, [], 'nothing loads until the palette is pressed');
95
+ p.tools()[0].click();
96
+ await flush();
97
+ p.tools()[0].click();
98
+ await flush();
99
+ assert.deepEqual(p.loaded, ['/builders/page-notes.js?v=2026-10-04-test']);
100
+ assert.deepEqual(p.opened, ['open', 'open']);
101
+ });
102
+
103
+ test('a ?note= link opens that doc on arrival, in any mode', async () => {
104
+ const p = boot({ search: '?note=42' });
105
+ await flush();
106
+ assert.deepEqual(p.opened, ['note 42']);
107
+ const q = boot({ session: { 'hall-craft': 'governing' }, search: '?x=1&note=7' });
108
+ await flush();
109
+ assert.deepEqual(q.opened, ['note 7']);
110
+ const bad = boot({ search: '?note=7x' });
111
+ await flush();
112
+ assert.deepEqual(bad.opened, [], 'a malformed id opens nothing');
113
+ });
114
+
115
+ // ---------- the reader ----------
116
+ function reader() {
117
+ const window = { location: { pathname: '/studio' } };
118
+ const document = { body: { dataset: { page: 'studio', pageTitle: 'Studio' } }, title: 'Studio — hall' };
119
+ const sandbox = { window, document };
120
+ vm.createContext(sandbox);
121
+ vm.runInContext(NOTES, sandbox, { filename: 'page-notes.js' });
122
+ return window.OTBPageNotes;
123
+ }
124
+
125
+ test('the reader escapes the doc and shows ONLY this module\'s own images', () => {
126
+ const R = reader();
127
+ const html = R.renderBody('# Header <b>\n- one\n- two\n\nSee ![shot](/api/bongos/page-notes/images/12) and ![x](https://evil.example/a.png)\n<script>alert(1)</script>');
128
+ assert.match(html, /<h3 class="pn-doc__h">Header &lt;b&gt;<\/h3>/);
129
+ assert.match(html, /<ul><li>one<\/li><li>two<\/li><\/ul>/);
130
+ assert.match(html, /<img class="pn-doc__img" src="\/api\/bongos\/page-notes\/images\/12" alt="shot" loading="lazy">/);
131
+ assert.doesNotMatch(html, /<img[^>]+evil\.example/, 'another site\'s image is left as text');
132
+ assert.doesNotMatch(html, /<script/);
133
+ assert.equal((html.match(/<img/g) || []).length, 1);
134
+ });
135
+
136
+ test('the doc names its page the way the page inventory does', () => {
137
+ const R = reader();
138
+ assert.deepEqual(JSON.parse(JSON.stringify(R.page())), { id: 'builders:studio', path: '/studio', title: 'Studio' });
139
+ });
140
+
141
+ test('colours live in style.css, and the panel and tool are styled there', () => {
142
+ assert.doesNotMatch(NOTES, /#[0-9a-f]{3,6}\b|rgba?\(/i, 'no colour literal in the panel script');
143
+ const css = fs.readFileSync(path.join(HALL, 'style.css'), 'utf8');
144
+ for (const sel of ['.craft-tool {', '.pn {', '.pn__panel {', '.pn__text {', '.pn-doc__img {']) assert.ok(css.includes(sel), sel);
145
+ });