@bongos/core 1.24.51 → 1.24.52

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 (81) hide show
  1. package/.bongos-core.json +95 -85
  2. package/docs/copy-inventory.md +199 -215
  3. package/docs/copy-registry.json +230 -395
  4. package/docs/file-map.md +1 -1
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-inventory.json +33 -0
  7. package/docs/page-readings.json +450 -463
  8. package/modules/hall-ui/public/ai-studio.html +1 -0
  9. package/modules/hall-ui/public/approval-queue.css +3 -11
  10. package/modules/hall-ui/public/approval-queue.js +13 -20
  11. package/modules/hall-ui/public/atlas.html +1 -0
  12. package/modules/hall-ui/public/blockers.html +1 -0
  13. package/modules/hall-ui/public/board-room.html +1 -0
  14. package/modules/hall-ui/public/bulk-toolbar.js +5 -1
  15. package/modules/hall-ui/public/city.html +1 -0
  16. package/modules/hall-ui/public/collab.html +1 -0
  17. package/modules/hall-ui/public/commands.html +1 -0
  18. package/modules/hall-ui/public/controls.css +100 -0
  19. package/modules/hall-ui/public/copy-desk.css +7 -11
  20. package/modules/hall-ui/public/copy-desk.html +17 -16
  21. package/modules/hall-ui/public/copy-desk.js +13 -9
  22. package/modules/hall-ui/public/deploy.html +1 -0
  23. package/modules/hall-ui/public/goal-inbox.js +32 -17
  24. package/modules/hall-ui/public/goal-map.html +1 -0
  25. package/modules/hall-ui/public/goals-page.js +7 -1
  26. package/modules/hall-ui/public/goals.html +1 -0
  27. package/modules/hall-ui/public/government.html +1 -0
  28. package/modules/hall-ui/public/government.js +10 -2
  29. package/modules/hall-ui/public/hall-dialog.js +255 -0
  30. package/modules/hall-ui/public/hall-kit.js +9 -2
  31. package/modules/hall-ui/public/idea.html +1 -0
  32. package/modules/hall-ui/public/ideas.html +1 -0
  33. package/modules/hall-ui/public/index.html +1 -0
  34. package/modules/hall-ui/public/market-watch-item.html +2 -1
  35. package/modules/hall-ui/public/market-watch-lib.js +16 -9
  36. package/modules/hall-ui/public/market-watch-rules.html +1 -0
  37. package/modules/hall-ui/public/market-watch.css +2 -3
  38. package/modules/hall-ui/public/market-watch.html +2 -1
  39. package/modules/hall-ui/public/modules.html +1 -0
  40. package/modules/hall-ui/public/money.html +1 -0
  41. package/modules/hall-ui/public/my-agents.html +1 -0
  42. package/modules/hall-ui/public/oversight.css +3 -33
  43. package/modules/hall-ui/public/primer.html +1 -0
  44. package/modules/hall-ui/public/profile.html +1 -0
  45. package/modules/hall-ui/public/project-settings.html +1 -0
  46. package/modules/hall-ui/public/roster.html +1 -0
  47. package/modules/hall-ui/public/settings-ai-keys.js +5 -1
  48. package/modules/hall-ui/public/settings-passkeys.js +5 -1
  49. package/modules/hall-ui/public/settings-sessions.js +10 -2
  50. package/modules/hall-ui/public/settings.html +1 -0
  51. package/modules/hall-ui/public/settings.js +5 -1
  52. package/modules/hall-ui/public/street.html +1 -0
  53. package/modules/hall-ui/public/studio.css +6 -32
  54. package/modules/hall-ui/public/studio.html +4 -3
  55. package/modules/hall-ui/public/studio.js +40 -35
  56. package/modules/hall-ui/public/style.css +2 -8
  57. package/modules/hall-ui/public/task.html +1 -0
  58. package/modules/hall-ui/public/task.js +26 -12
  59. package/modules/hall-ui/public/thinking.html +1 -0
  60. package/modules/hall-ui/public/tweak-editor.html +1 -0
  61. package/modules/hall-ui/public/watch.html +1 -0
  62. package/modules/hall-ui/public/work.html +1 -0
  63. package/modules/hall-ui/public/work.js +7 -1
  64. package/package-lock.json +2 -2
  65. package/package.json +1 -1
  66. package/release-notes.json +6 -0
  67. package/src/module-api.js +1 -1
  68. package/tests/bulk_toolbar.mjs +1 -1
  69. package/tests/hall_approval_queue.mjs +8 -2
  70. package/tests/hall_audit.mjs +1 -1
  71. package/tests/hall_city.mjs +1 -1
  72. package/tests/hall_collab_world.mjs +9 -6
  73. package/tests/hall_dialog.mjs +401 -0
  74. package/tests/hall_error_envelope.mjs +1 -1
  75. package/tests/hall_oversight_world.mjs +9 -7
  76. package/tests/hall_record_world.mjs +1 -1
  77. package/tests/hall_street.mjs +1 -1
  78. package/tests/hall_studio_home.mjs +1 -1
  79. package/tests/home_attention_cards.mjs +2 -0
  80. package/tests/kind_axis_unset.mjs +1 -1
  81. package/tests/market_watch_pages.mjs +3 -1
@@ -299,7 +299,11 @@
299
299
  // action here that discards work in bulk — a drifted rank can be ~60 rows.
300
300
  async function resetRank(rank, btn) {
301
301
  const label = rank.label || rank.rank_key;
302
- if (!window.confirm(`Reset ${label} to its default permissions?\n\nEvery change you've made to this rank is discarded, and it takes effect for every builder at this rank on their next request.`)) return;
302
+ if (!(await window.OTBDialog.confirm({
303
+ title: `Reset ${label} to its default permissions?`,
304
+ body: 'Every change you\'ve made to this rank is discarded, and it takes effect for every builder at this rank on their next request.',
305
+ confirmLabel: 'Reset rank', danger: true, trigger: btn,
306
+ }))) return;
303
307
  btn.disabled = true;
304
308
  try {
305
309
  const out = await sendJson('POST', `/government/ranks/${encodeURIComponent(rank.rank_key)}/reset`);
@@ -316,7 +320,11 @@
316
320
  // them, so the toast can say exactly whose rank to remove first.
317
321
  async function deleteRank(rank, btn) {
318
322
  const label = rank.label || rank.rank_key;
319
- if (!window.confirm(`Delete the ${label} rank?\n\nIts permissions go with it. This cannot be undone, though you can create a rank with the same name again.`)) return;
323
+ if (!(await window.OTBDialog.confirm({
324
+ title: `Delete the ${label} rank?`,
325
+ body: 'Its permissions go with it. This cannot be undone, though you can create a rank with the same name again.',
326
+ confirmLabel: 'Delete rank', danger: true, trigger: btn,
327
+ }))) return;
320
328
  btn.disabled = true;
321
329
  const res = await api.request('DELETE', `${API}/government/ranks/${encodeURIComponent(rank.rank_key)}`);
322
330
  if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
@@ -0,0 +1,255 @@
1
+ /* modules/hall-ui/public/hall-dialog.js — THE HALL'S ONE DIALOG
2
+ (task 1005085, goal 1000138 "One Cloud Bongos design system").
3
+
4
+ The UI audit found six dialog builds (attn, cd, mw, aq, ov, st), each with
5
+ its own scrim, layer, Escape handling and focus rules, and fourteen
6
+ window.confirm() calls whose browser box neither looks like the hall nor
7
+ reads in its voice. This is the one of each that replaces them.
8
+
9
+ window.OTBDialog:
10
+ confirm({ title, body, confirmLabel, cancelLabel, danger }) → Promise<boolean>
11
+ The branded confirm. `body` says what will happen; a blank line in it
12
+ starts a new paragraph. Resolves true only on the confirm button;
13
+ Cancel, the close mark, Escape and a click on the scrim all resolve
14
+ false. A `danger` confirm opens with focus on Cancel.
15
+ open({ title, body, actions, wide, className, initialFocus }) → { el, close(value), result }
16
+ A dialog built here and removed on close; `result` resolves with the
17
+ pressed action's `value` (or null when dismissed).
18
+ bind(root, { panel, initialFocus, returnFocus, lockClass, backdropCloses, onOpen, onClose })
19
+ → { open(trigger), close(), isOpen() }
20
+ The behaviour for a dialog a page draws itself: a native <dialog
21
+ class="hall-dialog"> (root is the dialog), or a div overlay whose
22
+ `panel` carries role="dialog" (the approval queue's and the studio's
23
+ glass, which are designs of record). Either way it gets the same rules.
24
+
25
+ THE RULES, every dialog alike: role="dialog" + aria-modal, labelled by its
26
+ title; focus moves in on open and is trapped there (Tab and Shift+Tab wrap);
27
+ Escape closes the TOP dialog only (a confirm over the attention window
28
+ closes the confirm, not both); a click on the scrim closes; focus returns to
29
+ the control that opened it. Native dialogs open with showModal(), so the
30
+ page behind is inert and the dialog sits in the top layer; the look is
31
+ controls.css's `.hall-dialog` (bg-card, the hairline, the panel corner, the
32
+ one floating shadow) over `--scrim`.
33
+
34
+ No motion here (DESIGN.md: no motion in JavaScript). Plain script, no
35
+ build; node-require-safe so tests can load it in a vm. */
36
+ (function () {
37
+ 'use strict';
38
+
39
+ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), '
40
+ + 'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
41
+
42
+ // The open dialogs, oldest first. Escape and Tab act on the last one only.
43
+ const stack = [];
44
+ let seq = 0;
45
+ let keysWired = false;
46
+
47
+ const doc = () => (typeof document !== 'undefined' ? document : null);
48
+ const isNative = (el) => String(el && el.tagName || '').toUpperCase() === 'DIALOG';
49
+
50
+ function shown(el) {
51
+ for (let n = el; n && n.nodeType === 1; n = n.parentNode) {
52
+ if (n.hidden) return false;
53
+ }
54
+ return true;
55
+ }
56
+
57
+ function focusables(panel) {
58
+ return Array.from(panel.querySelectorAll(FOCUSABLE)).filter(shown);
59
+ }
60
+
61
+ function onKey(ev) {
62
+ const top = stack[stack.length - 1];
63
+ if (!top) return;
64
+ if (ev.key === 'Escape') {
65
+ ev.preventDefault();
66
+ if (ev.stopPropagation) ev.stopPropagation();
67
+ top.close();
68
+ return;
69
+ }
70
+ if (ev.key !== 'Tab') return;
71
+ const f = focusables(top.panel);
72
+ if (!f.length) { ev.preventDefault(); return; }
73
+ const d = doc();
74
+ const at = d && d.activeElement;
75
+ const inside = at && top.panel.contains(at);
76
+ const first = f[0];
77
+ const last = f[f.length - 1];
78
+ if (ev.shiftKey && (at === first || !inside)) { ev.preventDefault(); last.focus(); }
79
+ else if (!ev.shiftKey && (at === last || !inside)) { ev.preventDefault(); first.focus(); }
80
+ }
81
+
82
+ function wireKeys() {
83
+ const d = doc();
84
+ if (keysWired || !d || typeof d.addEventListener !== 'function') return;
85
+ keysWired = true;
86
+ // Capture, so a page's own Escape listener never also fires for a key a
87
+ // dialog has already spent.
88
+ d.addEventListener('keydown', onKey, true);
89
+ }
90
+
91
+ function bind(root, opts = {}) {
92
+ if (!root) throw new Error('OTBDialog.bind needs an element');
93
+ // Once per element: a page that re-fills one <dialog> and opens it again
94
+ // (the market watch's propose dialog) gets the same handle, not a second
95
+ // set of listeners.
96
+ if (root.__otbDialog) return root.__otbDialog;
97
+ const panel = opts.panel || root;
98
+ const native = isNative(root);
99
+ let opener = null;
100
+ let open = false;
101
+
102
+ if (!panel.getAttribute('role')) panel.setAttribute('role', 'dialog');
103
+ panel.setAttribute('aria-modal', 'true');
104
+ // The panel takes focus when nothing inside can, so Escape and the trap
105
+ // still see the dialog as focused.
106
+ if (panel.getAttribute('tabindex') === null) panel.setAttribute('tabindex', '-1');
107
+
108
+ const handle = {
109
+ root,
110
+ panel,
111
+ open: show,
112
+ close: hide,
113
+ isOpen: () => open,
114
+ };
115
+ root.__otbDialog = handle;
116
+
117
+ root.addEventListener('click', (ev) => {
118
+ if (!open || opts.backdropCloses === false) return;
119
+ // A native dialog's backdrop click lands on the <dialog> itself, as does
120
+ // a div overlay's on the overlay; anything inside the panel does not.
121
+ if (ev.target === root && (native || panel !== root)) hide();
122
+ else if (ev.target && ev.target.closest && ev.target.closest('[data-dialog-close]')
123
+ && root.contains(ev.target)) hide();
124
+ });
125
+ if (native) {
126
+ // Escape is spent by onKey; this is the browser's own close path (a
127
+ // form method="dialog" submit), which must still settle the handle.
128
+ root.addEventListener('cancel', (ev) => { ev.preventDefault(); hide(); });
129
+ root.addEventListener('close', () => { if (open) settle(); });
130
+ }
131
+
132
+ function show(trigger) {
133
+ if (open) return handle;
134
+ const d = doc();
135
+ opener = trigger || (d && d.activeElement) || null;
136
+ open = true;
137
+ stack.push(handle);
138
+ wireKeys();
139
+ if (native) {
140
+ if (!root.open) {
141
+ if (typeof root.showModal === 'function') root.showModal();
142
+ else root.setAttribute('open', '');
143
+ }
144
+ } else {
145
+ root.hidden = false;
146
+ }
147
+ if (opts.lockClass && d) d.documentElement.classList.add(opts.lockClass);
148
+ if (typeof opts.onOpen === 'function') opts.onOpen();
149
+ let first = null;
150
+ if (typeof opts.initialFocus === 'function') first = opts.initialFocus();
151
+ else if (typeof opts.initialFocus === 'string') first = panel.querySelector(opts.initialFocus);
152
+ if (!first) first = focusables(panel)[0] || panel;
153
+ if (first && first.focus) first.focus();
154
+ return handle;
155
+ }
156
+
157
+ // Everything closing does except hiding the element, so the browser's own
158
+ // close (which has already hidden it) can share it.
159
+ function settle(value) {
160
+ open = false;
161
+ const i = stack.indexOf(handle);
162
+ if (i !== -1) stack.splice(i, 1);
163
+ const d = doc();
164
+ if (opts.lockClass && d) d.documentElement.classList.remove(opts.lockClass);
165
+ if (typeof opts.onClose === 'function') opts.onClose(value);
166
+ const back = typeof opts.returnFocus === 'function' ? opts.returnFocus(opener) : opener;
167
+ if (back && back.focus && back.isConnected !== false) back.focus();
168
+ }
169
+
170
+ function hide(value) {
171
+ if (!open) return;
172
+ if (native) {
173
+ open = false; // the close event below must not settle twice
174
+ if (root.open && typeof root.close === 'function') root.close();
175
+ else root.removeAttribute('open');
176
+ open = true;
177
+ } else {
178
+ root.hidden = true;
179
+ }
180
+ settle(value);
181
+ }
182
+
183
+ return handle;
184
+ }
185
+
186
+ // The hall's one browser escaper (dom-utils.js, loaded first on every page).
187
+ const esc = (s) => window.OTB.escapeHtml(s == null ? '' : s);
188
+
189
+ // A string body is TEXT, never markup: a blank line starts a paragraph.
190
+ function bodyHtml(body) {
191
+ return String(body).split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean)
192
+ .map((p) => `<p>${esc(p)}</p>`).join('');
193
+ }
194
+
195
+ function open(opts = {}) {
196
+ const d = doc();
197
+ const id = `hall-dialog-${++seq}`;
198
+ const el = d.createElement('dialog');
199
+ el.className = ['hall-dialog', opts.wide ? 'hall-dialog--wide' : '', opts.className || ''].filter(Boolean).join(' ');
200
+ el.setAttribute('aria-labelledby', `${id}-t`);
201
+ const actions = (opts.actions || []).map((a, i) => {
202
+ const cls = a.kind === 'ghost' ? 'btn btn--ghost' : a.kind === 'danger' ? 'btn btn--danger' : 'btn';
203
+ return `<button type="button" class="${cls}" data-dialog-value="${i}">${esc(a.label)}</button>`;
204
+ }).join('');
205
+ el.innerHTML = `<div class="hall-dialog__head"><h2 class="hall-dialog__title" id="${id}-t">${esc(opts.title || '')}</h2>`
206
+ + '<button type="button" class="hall-dialog__x" data-dialog-close aria-label="Close">×</button></div>'
207
+ + `<div class="hall-dialog__body" id="${id}-b"></div>`
208
+ + (actions ? `<div class="hall-dialog__actions">${actions}</div>` : '');
209
+ const bodyEl = el.querySelector('.hall-dialog__body');
210
+ if (opts.body && typeof opts.body === 'object' && opts.body.nodeType) bodyEl.appendChild(opts.body);
211
+ else if (opts.body != null && opts.body !== '') {
212
+ bodyEl.innerHTML = bodyHtml(opts.body);
213
+ el.setAttribute('aria-describedby', `${id}-b`);
214
+ }
215
+ d.body.appendChild(el);
216
+
217
+ let resolve;
218
+ const result = new Promise((r) => { resolve = r; });
219
+ const handle = bind(el, {
220
+ initialFocus: opts.initialFocus,
221
+ onClose: (value) => {
222
+ if (el.parentNode) el.parentNode.removeChild(el);
223
+ resolve(value === undefined ? null : value);
224
+ },
225
+ });
226
+ el.addEventListener('click', (ev) => {
227
+ const b = ev.target && ev.target.closest && ev.target.closest('[data-dialog-value]');
228
+ if (!b) return;
229
+ const a = (opts.actions || [])[Number(b.getAttribute('data-dialog-value'))];
230
+ handle.close(a ? a.value : null);
231
+ });
232
+ handle.open(opts.trigger);
233
+ return { el, close: (v) => handle.close(v), result };
234
+ }
235
+
236
+ function confirm(opts = {}) {
237
+ const actions = [
238
+ { label: opts.cancelLabel || 'Cancel', value: false, kind: 'ghost' },
239
+ { label: opts.confirmLabel || 'Confirm', value: true, kind: opts.danger ? 'danger' : 'primary' },
240
+ ];
241
+ const dlg = open({
242
+ title: opts.title,
243
+ body: opts.body,
244
+ actions,
245
+ trigger: opts.trigger,
246
+ // A destructive confirm starts on Cancel, so Enter does no harm.
247
+ initialFocus: `[data-dialog-value="${opts.danger ? 0 : 1}"]`,
248
+ });
249
+ return dlg.result.then((v) => v === true);
250
+ }
251
+
252
+ const api = { bind, open, confirm };
253
+ if (typeof window !== 'undefined') window.OTBDialog = api;
254
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
255
+ })();
@@ -442,10 +442,17 @@
442
442
  const name = v && window.prompt('New name for this view', v.name);
443
443
  if (name && name.trim() && name.trim() !== v.name) act(() => call('PATCH', `/${v.id}`, { page: views.page, name }), 'Renamed.');
444
444
  } else if (t.hasAttribute('data-delete')) {
445
- const v = pick('data-delete');
446
- if (v && window.confirm(`Delete the view “${v.name}”?`)) act(() => call('DELETE', `/${v.id}?${pageQ}`), 'Deleted.');
445
+ deleteView(pick('data-delete'), t);
447
446
  }
448
447
  });
448
+ async function deleteView(v, trigger) {
449
+ if (!v || !(await window.OTBDialog.confirm({
450
+ title: `Delete the view “${v.name}”?`,
451
+ body: 'The saved filters go. The tasks it showed are not touched.',
452
+ confirmLabel: 'Delete view', danger: true, trigger,
453
+ }))) return;
454
+ act(() => call('DELETE', `/${v.id}?${pageQ}`), 'Deleted.');
455
+ }
449
456
  menu?.addEventListener('submit', (e) => {
450
457
  e.preventDefault();
451
458
  const input = e.target.querySelector('input[name="name"]');
@@ -54,6 +54,7 @@
54
54
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
55
55
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
56
56
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
57
+ <script src="/builders/hall-dialog.js"></script>
57
58
  <script src="/builders/palette.js" defer></script>
58
59
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
59
60
  written inside an idea's own prose keeps resolving from this page. -->
@@ -229,6 +229,7 @@
229
229
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
230
230
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
231
231
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
232
+ <script src="/builders/hall-dialog.js"></script>
232
233
  <script src="/builders/palette.js" defer></script>
233
234
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
234
235
  <script src="/builders/vote-control.js"></script>
@@ -110,6 +110,7 @@
110
110
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers" defer></script>
111
111
  <script src="/builders/craft-mode.js?v=2026-11-05-goals" defer></script>
112
112
  <script src="/builders/shell.js?v=2026-11-07-brand-mark" defer></script>
113
+ <script src="/builders/hall-dialog.js" defer></script>
113
114
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
114
115
  <script src="/builders/palette.js" defer></script>
115
116
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -68,13 +68,14 @@
68
68
  </div>
69
69
  </div>
70
70
 
71
- <dialog class="mw-dialog" id="mw-propose" aria-labelledby="mw-propose-title"></dialog>
71
+ <dialog class="hall-dialog mw-dialog" id="mw-propose" aria-labelledby="mw-propose-title"></dialog>
72
72
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
73
73
 
74
74
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
75
75
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
76
76
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
77
77
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
78
+ <script src="/builders/hall-dialog.js"></script>
78
79
  <script src="/builders/palette.js" defer></script>
79
80
  <script src="/builders/hall-kit.js"></script>
80
81
  <script src="/builders/market-watch-lib.js"></script>
@@ -147,20 +147,26 @@
147
147
  ['add_source', 'Add a source to watch'],
148
148
  ['correct_entry', 'Correct an entry'],
149
149
  ];
150
+ // The dialog is the hall's one dialog (controls.css .hall-dialog, hall-dialog.js;
151
+ // task 1005085): its title row carries the close mark the shared component wires.
152
+ function HEAD(title) {
153
+ return '<div class="hall-dialog__head"><h2 class="hall-dialog__title" id="mw-propose-title">' + esc(title) + '</h2>'
154
+ + '<button type="button" class="hall-dialog__x" data-dialog-close aria-label="Close">×</button></div>';
155
+ }
150
156
  function dialogHtml(mode, ctx) {
151
157
  ctx = ctx || {};
152
158
  var it = ctx.item || {};
153
159
  var h = '<form method="dialog" novalidate>';
154
160
  if (mode === 'scout') {
155
- h += '<span class="mw-label" id="mw-propose-title">send a new scout</span>';
161
+ h += HEAD('send a new scout');
156
162
  h += '<label>What should we watch?<select name="scout"><option value="competitor">A competitor</option><option value="partner">A partner</option><option value="topic">A topic</option></select></label>';
157
163
  h += '<label>Its name<input name="name" maxlength="120" autocomplete="off" required></label>';
158
164
  h += '<label>A link, if it has one<input name="url" type="url" maxlength="500" placeholder="https://" autocomplete="off"></label>';
159
165
  } else if (mode === 'stop') {
160
- h += '<span class="mw-label" id="mw-propose-title">stop tracking</span>';
166
+ h += HEAD('stop tracking');
161
167
  h += '<p class="mw-quiet" style="margin:0 0 12px">Asks the government to take ' + esc(it.name) + ' off the watch. Its case file stays readable.</p>';
162
168
  } else {
163
- h += '<span class="mw-label" id="mw-propose-title">propose a change</span>';
169
+ h += HEAD('propose a change');
164
170
  h += '<label>What should change?<select name="kind">';
165
171
  CHANGE_KINDS.forEach(function (k) {
166
172
  if (k[0] === 'move_kind' && it.kind === 'topic') return;
@@ -183,7 +189,7 @@
183
189
  }
184
190
  h += '<label>Why, in one line<input name="reason" maxlength="280" autocomplete="off" required placeholder="' + (mode === 'scout' ? 'e.g. they launched an agent marketplace this week' : mode === 'stop' ? 'e.g. they shut down in September' : 'e.g. they now pay maintainers, so economy should be 4') + '"></label>';
185
191
  h += '<p class="mw-dialog__err" role="alert"></p>';
186
- h += '<div class="mw-dialog__acts"><button class="btn btn--ghost" value="cancel" type="button" data-cancel>cancel</button><button class="btn" type="submit">send to the government</button></div>';
192
+ h += '<div class="hall-dialog__actions"><button class="btn btn--ghost" value="cancel" type="button" data-cancel>cancel</button><button class="btn" type="submit">send to the government</button></div>';
187
193
  return h + '</form>';
188
194
  }
189
195
 
@@ -213,6 +219,9 @@
213
219
  // fixed aria-label would win over aria-labelledby, so none is left on the element.
214
220
  dlg.removeAttribute('aria-label');
215
221
  dlg.setAttribute('aria-labelledby', 'mw-propose-title');
222
+ // The shared dialog behaviour: Escape, the focus trap, the scrim, focus back to
223
+ // the button that opened it (task 1005085). The first field takes focus.
224
+ var dh = root.OTBDialog.bind(dlg, { initialFocus: 'select, input' });
216
225
  var f = dlg.querySelector('form');
217
226
  var err = dlg.querySelector('.mw-dialog__err');
218
227
  var sync = function () {
@@ -235,7 +244,7 @@
235
244
  fill();
236
245
  wr.addEventListener('change', function () { tk.disabled = wr.checked; });
237
246
  }
238
- dlg.querySelector('[data-cancel]').addEventListener('click', function () { dlg.close(); });
247
+ dlg.querySelector('[data-cancel]').addEventListener('click', function () { dh.close(); });
239
248
  f.addEventListener('submit', function (ev) {
240
249
  ev.preventDefault();
241
250
  var body = bodyFor(mode, f, ctx.item || {});
@@ -253,14 +262,12 @@
253
262
  return r.json().catch(function () { return {}; }).then(function (j) { return { status: r.status, ok: r.ok, data: j }; });
254
263
  }).then(function (res) {
255
264
  btn.disabled = false;
256
- if (res.ok) { dlg.close(); onDone(res.data); return; }
265
+ if (res.ok) { dh.close(); onDone(res.data); return; }
257
266
  var e = (res.data && res.data.error) || {};
258
267
  err.textContent = res.status === 401 ? 'Sign in again to send this.' : (e.message && e.message !== e.code ? e.message : 'That did not send. Try again in a moment.');
259
268
  }).catch(function () { btn.disabled = false; err.textContent = 'Could not reach the server.'; });
260
269
  });
261
- dlg.showModal();
262
- var first = f.querySelector('select, input');
263
- if (first) first.focus();
270
+ dh.open();
264
271
  }
265
272
 
266
273
  var api = {
@@ -44,6 +44,7 @@
44
44
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
45
45
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
46
46
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
47
+ <script src="/builders/hall-dialog.js"></script>
47
48
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
48
49
  <script src="/builders/palette.js" defer></script>
49
50
  <script src="/builders/hall-kit.js"></script>
@@ -161,14 +161,13 @@
161
161
  }
162
162
 
163
163
  /* ---- the propose dialog (both pages) ---------------------------------------- */
164
- .mw-dialog { border: 0; border-radius: var(--radius); background: var(--bg-card); color: var(--ink); padding: 20px; width: min(440px, calc(100vw - 32px)); box-shadow: var(--shadow-pop); }
165
- .mw-dialog::backdrop { background: var(--scrim); }
164
+ /* The dialog itself is the hall's one dialog (controls.css .hall-dialog, task
165
+ 1005085): its panel, scrim, shadow and actions row. What stays is the form. */
166
166
  .mw-dialog label { display: block; font-size: 13px; color: var(--ink-soft); margin-bottom: 12px; }
167
167
  .mw-dialog input, .mw-dialog select { display: block; width: 100%; margin-top: 6px; font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--rule); border-radius: var(--radius-sm); padding: 8px 9px; min-height: 36px; }
168
168
  .mw-dialog label.mw-check { display: flex; gap: 8px; align-items: center; }
169
169
  .mw-dialog label.mw-check input { display: inline-block; width: auto; min-height: 0; margin: 0; }
170
170
  .mw-dialog .mw-row2 { display: grid; grid-template-columns: minmax(0, 1fr) 90px; gap: 8px; }
171
- .mw-dialog__acts { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
172
171
  .mw-dialog__err { color: var(--danger); font-size: 12.5px; margin: 0 0 8px; min-height: 1em; }
173
172
  .mw-filter { margin: 0 0 8px; }
174
173
  .mw-rules-link { display: inline-flex; align-items: center; min-height: 28px; margin-top: 12px; font-size: 12.5px; color: var(--ink-soft); }
@@ -182,13 +182,14 @@
182
182
  </div>
183
183
  </div>
184
184
 
185
- <dialog class="mw-dialog" id="mw-propose" aria-labelledby="mw-propose-title"></dialog>
185
+ <dialog class="hall-dialog mw-dialog" id="mw-propose" aria-labelledby="mw-propose-title"></dialog>
186
186
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
187
187
 
188
188
  <script src="/builders/dom-utils.js?v=2026-11-05-numbers"></script>
189
189
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
190
190
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
191
191
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
192
+ <script src="/builders/hall-dialog.js"></script>
192
193
  <script src="/builders/palette.js" defer></script>
193
194
  <script src="/builders/hall-kit.js"></script>
194
195
  <script src="/builders/market-watch-lib.js"></script>
@@ -129,6 +129,7 @@
129
129
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
130
130
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
131
131
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
132
+ <script src="/builders/hall-dialog.js"></script>
132
133
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
133
134
  <script src="/builders/palette.js" defer></script>
134
135
  <script src="/builders/brand-apply.js"></script>
@@ -83,6 +83,7 @@
83
83
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
84
84
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
85
85
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
86
+ <script src="/builders/hall-dialog.js"></script>
86
87
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
87
88
  <script src="/builders/palette.js" defer></script>
88
89
  <script src="/builders/hall-kit.js"></script>
@@ -85,6 +85,7 @@
85
85
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
86
86
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
87
87
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
88
+ <script src="/builders/hall-dialog.js"></script>
88
89
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
89
90
  <script src="/builders/palette.js" defer></script>
90
91
  <script src="/builders/gov-living.js?v=2026-10-10-real1"></script>
@@ -1,8 +1,9 @@
1
1
  /* oversight.css — the oversight family's archetype (task 1003469, goal
2
2
  1000081): what an oversight ROW is in the chrome world, and the pieces the
3
3
  pages (government, watch, gate) share around it — the figure
4
- band inside a panel, the group head, the inline form, the grant toggle, the
5
- confirm modal. The third family sheet after record.css and room.css: a page
4
+ band inside a panel, the group head, the inline form, the grant toggle (the
5
+ confirm modal it once kept is the hall's one dialog now, controls.css
6
+ .hall-dialog, task 1005085). The third family sheet after record.css and room.css: a page
6
7
  sheet holds only what one page has and the others do not. The collaboration
7
8
  pages (collab, copy desk — task 1003471) ride the same row rather than a
8
9
  fourth: a page whose repeated thing is a titled row with facts and actions
@@ -318,34 +319,3 @@ a.fact-chip:hover { color: var(--accent-ink); border-color: var(--accent-ink); t
318
319
  the 24px floor and the equal negative margin hands the space back. */
319
320
  .ov-foot a { display: inline-block; color: var(--accent-ink); min-height: 24px; min-width: 24px; padding: 3px 4px; margin: -3px -4px; line-height: 18px; }
320
321
 
321
- /* ---- the confirm modal ----------------------------------------------------------
322
- Gate's confirm dialog. A --bg-card panel closed by one rule
323
- hairline and no shadow (the hub's project card is the reference), on the
324
- shell's --z-modal layer, over a scrim mixed from the ink. */
325
- .ov-modal-bg {
326
- position: fixed;
327
- inset: 0;
328
- z-index: var(--z-modal);
329
- display: flex;
330
- align-items: center;
331
- justify-content: center;
332
- padding: 20px;
333
- background: var(--scrim);
334
- }
335
- .ov-modal {
336
- width: 100%;
337
- max-width: 480px;
338
- padding: 22px 24px 24px;
339
- background: var(--bg-card);
340
- border: var(--line) solid var(--rule);
341
- border-radius: var(--radius);
342
- }
343
- .ov-modal .scroll__h { margin-bottom: 10px; }
344
- .ov-modal p { margin: 0 0 12px; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); }
345
- .ov-modal p strong, .ov-modal p b { color: var(--ink); font-weight: 500; }
346
- .ov-modal p:last-of-type { margin-bottom: 0; }
347
- .ov-modal label:not(.ov-modal__check) { display: block; margin: 14px 0 6px; font-family: var(--label-face); font-size: 10.5px; letter-spacing: var(--label-space); text-transform: var(--label-case); color: var(--ink-faint); }
348
- .ov-modal input[type="text"], .ov-modal textarea { width: 100%; box-sizing: border-box; }
349
- .ov-modal__check { display: flex; align-items: flex-start; gap: 8px; margin: 14px 0 0; font-size: 13px; color: var(--ink); }
350
- .ov-modal__check input { margin-top: 3px; width: 16px; height: 16px; }
351
- .ov-modal__actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
@@ -114,6 +114,7 @@
114
114
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
115
115
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
116
116
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
117
+ <script src="/builders/hall-dialog.js"></script>
117
118
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
118
119
  <script src="/builders/palette.js" defer></script>
119
120
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -250,6 +250,7 @@
250
250
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
251
251
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
252
252
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
253
+ <script src="/builders/hall-dialog.js"></script>
253
254
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
254
255
  <script src="/builders/palette.js" defer></script>
255
256
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -78,6 +78,7 @@
78
78
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
79
79
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
80
80
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
81
+ <script src="/builders/hall-dialog.js"></script>
81
82
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
82
83
  <script src="/builders/palette.js" defer></script>
83
84
  <script src="/builders/hall-kit.js"></script>
@@ -60,6 +60,7 @@
60
60
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
61
61
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
62
62
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
63
+ <script src="/builders/hall-dialog.js"></script>
63
64
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
64
65
  <script src="/builders/palette.js" defer></script>
65
66
  <!-- the branding pack's currency label, loaded BEFORE roster.js. -->
@@ -91,7 +91,11 @@
91
91
  }
92
92
 
93
93
  async function removeKey(provider, label) {
94
- if (!window.confirm(`Remove your ${label} key? Generations will run on ours and be charged to your balance.`)) return;
94
+ if (!(await window.OTBDialog.confirm({
95
+ title: `Remove your ${label} key?`,
96
+ body: 'Generations will run on ours and be charged to your balance.',
97
+ confirmLabel: 'Remove key', danger: true,
98
+ }))) return;
95
99
  const res = await api.request('DELETE', `${API}/ai-studio/keys/${encodeURIComponent(provider)}`);
96
100
  if (res.status === 401) { toAuth(); return; }
97
101
  if (!res.ok) { toast('Could not remove your key. Try again.'); return; }
@@ -52,7 +52,11 @@
52
52
  }
53
53
 
54
54
  async function remove(id) {
55
- if (!window.confirm('Remove this card? You can register it again later.')) return;
55
+ if (!(await window.OTBDialog.confirm({
56
+ title: 'Remove this card?',
57
+ body: 'You will no longer sign in with it. You can register it again later.',
58
+ confirmLabel: 'Remove card', danger: true,
59
+ }))) return;
56
60
  try {
57
61
  const res = await api.request('POST', `${API}/me/passkeys/remove`, { body: { credential_id: id } });
58
62
  if (!res.ok) throw fail(res);
@@ -71,7 +71,11 @@
71
71
  const row = btn.closest('.sessions-row');
72
72
  const id = row.getAttribute('data-id');
73
73
  const isCurrent = !!row.querySelector('.sessions-row__badge');
74
- if (isCurrent && !window.confirm('That is the session you are using right now. Ending it signs you out of this page.')) return;
74
+ if (isCurrent && !(await window.OTBDialog.confirm({
75
+ title: 'End this session?',
76
+ body: 'That is the session you are using right now. Ending it signs you out of this page.',
77
+ confirmLabel: 'End session', danger: true, trigger: btn,
78
+ }))) return;
75
79
  btn.disabled = true;
76
80
  try {
77
81
  const res = await api.request('POST', `${API}/me/sessions/revoke`, { body: { session_id: id } });
@@ -91,7 +95,11 @@
91
95
  // then sends the builder back to sign-in rather than leaving a dead page up.
92
96
  async function endAll() {
93
97
  const btn = $('#sessions-revoke-all');
94
- if (!window.confirm('End every session, including this one? You will need to sign in again, and any CLI token will need re-issuing.')) return;
98
+ if (!(await window.OTBDialog.confirm({
99
+ title: 'End every session, including this one?',
100
+ body: 'You will need to sign in again, and any CLI token will need re-issuing.',
101
+ confirmLabel: 'Sign out everywhere', danger: true, trigger: btn,
102
+ }))) return;
95
103
  btn.disabled = true;
96
104
  try {
97
105
  const res = await api.request('POST', `${API}/me/sessions/revoke-all`, { body: {} });