@bongos/core 1.24.50 → 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 (86) hide show
  1. package/.bongos-core.json +104 -89
  2. package/docs/copy-inventory.md +259 -277
  3. package/docs/copy-registry.json +461 -634
  4. package/docs/file-map.md +1 -1
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-inventory.json +35 -2
  7. package/docs/page-readings.json +518 -547
  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/modules/status-ui/public/index.html +43 -43
  65. package/modules/status-ui/public/status.js +26 -27
  66. package/package-lock.json +2 -2
  67. package/package.json +1 -1
  68. package/release-notes.json +12 -0
  69. package/src/module-api.js +1 -1
  70. package/tests/bulk_toolbar.mjs +1 -1
  71. package/tests/hall_approval_queue.mjs +8 -2
  72. package/tests/hall_audit.mjs +1 -1
  73. package/tests/hall_city.mjs +1 -1
  74. package/tests/hall_collab_world.mjs +9 -6
  75. package/tests/hall_dialog.mjs +401 -0
  76. package/tests/hall_error_envelope.mjs +1 -1
  77. package/tests/hall_oversight_world.mjs +9 -7
  78. package/tests/hall_record_world.mjs +1 -1
  79. package/tests/hall_street.mjs +1 -1
  80. package/tests/hall_studio_home.mjs +1 -1
  81. package/tests/home_attention_cards.mjs +2 -0
  82. package/tests/kind_axis_unset.mjs +1 -1
  83. package/tests/loading_mark.mjs +6 -6
  84. package/tests/market_watch_pages.mjs +3 -1
  85. package/tests/status_company_attribution.mjs +1 -1
  86. package/tests/status_page_wording.mjs +129 -0
@@ -90,6 +90,7 @@
90
90
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
91
91
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
92
92
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
93
+ <script src="/builders/hall-dialog.js"></script>
93
94
  <script src="/builders/palette.js" defer></script>
94
95
  <script src="/builders/tweak-editor-lib.js?v=2026-09-28-tw23"></script>
95
96
  <script src="/builders/studio-lib.js?v=2026-10-09-day"></script>
@@ -26,7 +26,6 @@
26
26
  --aq-glass: rgba(255, 255, 255, .075);
27
27
  --aq-glass-edge: rgba(255, 255, 255, .18);
28
28
  --aq-glass-rim: rgba(255, 255, 255, .22);
29
- --aq-drop: rgba(0, 0, 0, .55);
30
29
  --aq-ink: #ffffff;
31
30
  --aq-ink-soft: #c0c0c0;
32
31
  --aq-ink-faint: #a8a8a8;
@@ -44,14 +43,8 @@
44
43
  --aq-rule: rgba(255, 255, 255, .12);
45
44
  --aq-face: var(--font-body, Manrope, "Segoe UI", system-ui, sans-serif);
46
45
  --aq-mono: var(--font-mono, "JetBrains Mono", ui-monospace, Consolas, monospace);
47
- position: fixed;
48
- inset: 0;
49
- z-index: var(--z-modal, 160);
50
- display: flex;
51
- align-items: center;
52
- justify-content: center;
53
- padding: 24px;
54
- box-sizing: border-box;
46
+ /* the layer, the placement and the gutter are the shared .hall-scrim's
47
+ (controls.css, task 1005085); only the artboard's tint is its own */
55
48
  background: var(--aq-scrim);
56
49
  color: var(--aq-ink);
57
50
  font-family: var(--aq-face);
@@ -60,7 +53,6 @@ html[data-theme="dark"] .aq-overlay { --aq-scrim: rgba(4, 5, 9, .42); }
60
53
  @media (prefers-color-scheme: dark) {
61
54
  html:not([data-theme]) .aq-overlay { --aq-scrim: rgba(4, 5, 9, .42); }
62
55
  }
63
- .aq-overlay[hidden] { display: none; }
64
56
  html.aq-open { overflow: hidden; }
65
57
 
66
58
  /* FITS THE SCREEN (task 1004381). The artboard's 1180 by 820 is a ceiling, never
@@ -80,7 +72,7 @@ html.aq-open { overflow: hidden; }
80
72
  border: 1px solid var(--aq-glass-edge);
81
73
  backdrop-filter: blur(30px) saturate(160%);
82
74
  -webkit-backdrop-filter: blur(30px) saturate(160%);
83
- box-shadow: inset 0 1px 0 var(--aq-glass-rim), 0 40px 90px var(--aq-drop);
75
+ box-shadow: inset 0 1px 0 var(--aq-glass-rim), var(--ds-shadow-pop);
84
76
  display: flex;
85
77
  flex-direction: column;
86
78
  gap: 16px;
@@ -79,11 +79,16 @@
79
79
  };
80
80
 
81
81
  const scrim = document.createElement('div');
82
- scrim.className = 'aq-overlay';
82
+ // The shared overlay layer (controls.css .hall-scrim, task 1005085); the
83
+ // glass inside is the artboard's own.
84
+ scrim.className = 'hall-scrim aq-overlay';
83
85
  scrim.hidden = true;
84
86
  scrim.innerHTML = '<div class="aq-dialog" role="dialog" aria-modal="true" aria-labelledby="aq-title" tabindex="-1"></div>';
85
87
  root.appendChild(scrim);
86
88
  const dialog = scrim.querySelector('.aq-dialog');
89
+ // The hall's one dialog behaviour (hall-dialog.js, task 1005085): Escape, the
90
+ // focus trap, a click on the scrim, the scroll lock and focus back to the opener.
91
+ const modal = window.OTBDialog.bind(scrim, { panel: dialog, lockClass: 'aq-open', initialFocus: () => dialog, onClose: closed });
87
92
 
88
93
  const current = () => state.queue[state.idx] || null;
89
94
 
@@ -296,14 +301,11 @@
296
301
  // `pageId` (optional) lands on that page's card when it is in the queue, so a
297
302
  // row elsewhere that says "needs to be reviewed" opens its own card.
298
303
  async function open(pageId, { reload = true } = {}) {
299
- state.opener = document.activeElement;
300
304
  state.open = true;
301
305
  state.error = null;
302
306
  state.noteOpen = false;
303
- scrim.hidden = false;
304
- document.documentElement.classList.add('aq-open');
305
307
  paint();
306
- dialog.focus();
308
+ modal.open();
307
309
  if (reload || !state.loaded) await loadQueue();
308
310
  if (typeof pageId === 'string') {
309
311
  const at = state.queue.findIndex((q) => q.page_id === pageId);
@@ -315,17 +317,17 @@
315
317
  if (first) first.focus();
316
318
  }
317
319
 
318
- function close() {
320
+ function close() { modal.close(); }
321
+
322
+ // Whatever closed it (the close button, Escape, the scrim), the queue settles here.
323
+ function closed() {
319
324
  state.open = false;
320
- scrim.hidden = true;
321
- document.documentElement.classList.remove('aq-open');
322
325
  // A decided page leaves the queue for good; the next open starts fresh.
323
326
  if (state.outcome.size) {
324
327
  state.queue = state.queue.filter((q) => !state.outcome.has(q.task_id));
325
328
  state.outcome.clear();
326
329
  state.idx = 0;
327
330
  }
328
- if (state.opener && state.opener.focus) state.opener.focus();
329
331
  }
330
332
 
331
333
  // A switch repaints the dialog, so the pressed pill is focused again: a
@@ -338,7 +340,6 @@
338
340
  }
339
341
 
340
342
  scrim.addEventListener('click', (ev) => {
341
- if (ev.target === scrim) return close();
342
343
  const t = ev.target.closest('button');
343
344
  if (!t || t.disabled) return undefined;
344
345
  if (('close' in t.dataset)) return close();
@@ -353,17 +354,9 @@
353
354
  return undefined;
354
355
  });
355
356
 
357
+ // Escape and the Tab trap are the shared dialog's (OTBDialog.bind above).
356
358
  scrim.addEventListener('keydown', (ev) => {
357
- if (ev.key === 'Escape') { ev.preventDefault(); close(); return; }
358
- if (ev.key === 'Enter' && ev.target && ev.target.id === 'aq-note') { ev.preventDefault(); decide('sent'); return; }
359
- if (ev.key !== 'Tab') return;
360
- // Keep focus inside the dialog while it is open (aria-modal).
361
- const f = [...dialog.querySelectorAll('button:not([disabled]), input, [tabindex="0"]')];
362
- if (!f.length) return;
363
- const first = f[0];
364
- const last = f[f.length - 1];
365
- if (ev.shiftKey && document.activeElement === first) { ev.preventDefault(); last.focus(); }
366
- else if (!ev.shiftKey && document.activeElement === last) { ev.preventDefault(); first.focus(); }
359
+ if (ev.key === 'Enter' && ev.target && ev.target.id === 'aq-note') { ev.preventDefault(); decide('sent'); }
367
360
  });
368
361
 
369
362
  scrim.addEventListener('input', (ev) => {
@@ -93,6 +93,7 @@
93
93
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
94
94
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
95
95
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
96
+ <script src="/builders/hall-dialog.js"></script>
96
97
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
97
98
  <script src="/builders/palette.js" defer></script>
98
99
  <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
@@ -62,6 +62,7 @@
62
62
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
63
63
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
64
64
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
65
+ <script src="/builders/hall-dialog.js"></script>
65
66
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
66
67
  <script src="/builders/palette.js" defer></script>
67
68
  <!-- the branding pack's currency label, loaded BEFORE blockers.js. -->
@@ -64,6 +64,7 @@
64
64
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
65
65
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
66
66
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
67
+ <script src="/builders/hall-dialog.js"></script>
67
68
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
68
69
  <script src="/builders/palette.js" defer></script>
69
70
  <script src="/builders/hall-kit.js"></script>
@@ -246,8 +246,12 @@
246
246
  if (!entries.length) return;
247
247
  const label = entries.length === 1 ? `#${entries[0][1]}` : `${entries.length} tasks`;
248
248
  const pronoun = entries.length === 1 ? 'It returns' : 'They return';
249
- if (!window.confirm(`Release ${label}? ${pronoun} to the queue and no ${curLabel()} are earned.`)) return;
250
249
  const btn = el('release');
250
+ if (!(await window.OTBDialog.confirm({
251
+ title: `Release ${label}?`,
252
+ body: `${pronoun} to the queue and no ${curLabel()} are earned.`,
253
+ confirmLabel: 'Release', danger: true, trigger: btn,
254
+ }))) return;
251
255
  if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
252
256
  try {
253
257
  const out = await claimAction.releaseMany(entries);
@@ -109,6 +109,7 @@
109
109
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers" defer></script>
110
110
  <script src="/builders/craft-mode.js?v=2026-11-05-goals" defer></script>
111
111
  <script src="/builders/shell.js?v=2026-11-07-brand-mark" defer></script>
112
+ <script src="/builders/hall-dialog.js" defer></script>
112
113
  <script src="/builders/palette.js" defer></script>
113
114
  <script src="/builders/gov-living.js?v=2026-10-09-positions1" defer></script>
114
115
  <script src="/builders/gov-feeds.js?v=2026-10-09-living3" defer></script>
@@ -163,6 +163,7 @@
163
163
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
164
164
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
165
165
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
166
+ <script src="/builders/hall-dialog.js"></script>
166
167
  <script src="/builders/palette.js" defer></script>
167
168
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
168
169
  <script src="/builders/collab.js?v=2026-09-24-archive"></script>
@@ -55,6 +55,7 @@
55
55
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
56
56
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
57
57
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
58
+ <script src="/builders/hall-dialog.js"></script>
58
59
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
59
60
  <script src="/builders/palette.js" defer></script>
60
61
  <script src="/builders/hall-kit.js"></script>
@@ -221,3 +221,103 @@ select:disabled, input[type="text"]:disabled, input[type="search"]:disabled, tex
221
221
  cursor: not-allowed;
222
222
  }
223
223
  select option { background: var(--bg-card); color: var(--ink); }
224
+
225
+ /* ---------- the danger button ----------
226
+ The one fill a confirm spends on an act that cannot be taken back (delete a
227
+ rank, end every session). The status red, with the card's own ground as its
228
+ text: --danger is measured at 4.5:1 against --bg-card in both modes
229
+ (tests/hall_tokens.mjs), so the pair holds whichever way round it is read. */
230
+ .btn--danger {
231
+ color: var(--bg-card);
232
+ background: var(--danger);
233
+ border-color: transparent;
234
+ }
235
+ .btn--danger:hover:not(:disabled) {
236
+ color: var(--bg-card);
237
+ background: color-mix(in srgb, var(--danger) 86%, var(--ink));
238
+ text-decoration: none;
239
+ }
240
+
241
+ /* ---------- the dialog (task 1005085) ----------
242
+ THE HALL'S ONE DIALOG. hall-dialog.js builds it (OTBDialog.open / .confirm)
243
+ or binds a page's own <dialog class="hall-dialog"> (OTBDialog.bind). It is
244
+ DESIGN.md's panel lifted over the page: bg-card, one hairline, the panel
245
+ corner, and the one shadow the hall spends, on an element that floats over
246
+ the scrim. A native dialog opens with showModal(), so it sits in the top
247
+ layer; the z-index is the ladder's modal step for any reader that does not.
248
+ .hall-scrim is the same layer for a div overlay (the approval queue's and the
249
+ studio's glass): fixed over the page, centred, darkened by --scrim. */
250
+ .hall-dialog {
251
+ z-index: var(--ds-z-modal);
252
+ box-sizing: border-box;
253
+ width: min(480px, calc(100vw - 2 * var(--ds-space-sm)));
254
+ max-height: calc(100dvh - 2 * var(--ds-space-md));
255
+ overflow-y: auto;
256
+ margin: auto;
257
+ padding: var(--ds-space-md);
258
+ color: var(--ink);
259
+ background: var(--bg-card);
260
+ border: var(--line) solid var(--rule);
261
+ border-radius: var(--ds-radius-panel);
262
+ box-shadow: var(--ds-shadow-pop);
263
+ }
264
+ .hall-dialog--wide { width: min(720px, calc(100vw - 2 * var(--ds-space-sm))); }
265
+ .hall-dialog::backdrop { background: var(--scrim); }
266
+ .hall-scrim {
267
+ position: fixed;
268
+ inset: 0;
269
+ z-index: var(--ds-z-modal);
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ box-sizing: border-box;
274
+ padding: var(--ds-space-md);
275
+ background: var(--scrim);
276
+ }
277
+ .hall-scrim[hidden] { display: none; }
278
+ .hall-scrim > .hall-dialog { max-height: 100%; }
279
+ .hall-dialog__head {
280
+ display: flex;
281
+ align-items: flex-start;
282
+ gap: var(--ds-space-xs);
283
+ margin: 0 0 var(--ds-space-xs);
284
+ }
285
+ .hall-dialog__title {
286
+ flex: 1;
287
+ margin: 0;
288
+ font-family: var(--font-display);
289
+ font-size: var(--ds-text-3xl);
290
+ font-weight: 500;
291
+ line-height: 1.25;
292
+ color: var(--ink);
293
+ }
294
+ .hall-dialog__x {
295
+ flex: none;
296
+ min-width: 32px;
297
+ min-height: 32px;
298
+ margin: -4px -6px 0 0;
299
+ padding: 0;
300
+ font: inherit;
301
+ font-size: var(--ds-text-4xl);
302
+ line-height: 1;
303
+ color: var(--ink-soft);
304
+ background: transparent;
305
+ border: 0;
306
+ border-radius: var(--radius-pill);
307
+ cursor: pointer;
308
+ }
309
+ .hall-dialog__x:hover { color: var(--ink); }
310
+ .hall-dialog__body p {
311
+ margin: 0 0 var(--ds-space-xs);
312
+ font-size: var(--ds-text-md);
313
+ line-height: 1.5;
314
+ color: var(--ink-soft);
315
+ }
316
+ .hall-dialog__body p:last-child { margin-bottom: 0; }
317
+ .hall-dialog__actions {
318
+ display: flex;
319
+ flex-wrap: wrap;
320
+ justify-content: flex-end;
321
+ gap: var(--ds-space-xs);
322
+ margin-top: var(--ds-space-md);
323
+ }
@@ -2,9 +2,9 @@
2
2
  R02 of goal 1000074), rebuilt on the hall's row archetype by task 1003471:
3
3
  the counts are the shell's glass .stat-row, every list is a run of
4
4
  oversight.css's .ov-row, the filters and the search are the kit's .filters,
5
- the two dialogs are the family's .ov-modal on a native <dialog>. What is
6
- left here is what only this page has: the quoted string as evidence, the
7
- reason as prose, the places a duplicate lives, the dialog's scrim.
5
+ the three dialogs are the hall's one dialog (controls.css .hall-dialog,
6
+ task 1005085). What is left here is what only this page has: the quoted
7
+ string as evidence, the reason as prose, the places a duplicate lives.
8
8
 
9
9
  px and tokens throughout, and state is COLOUR — the pill's verification
10
10
  vocabulary — never opacity. Opacity dims the panel behind the text as well
@@ -67,16 +67,12 @@
67
67
  .cd-find .filter input[type="search"] { width: 100%; }
68
68
  .cd-search-count { margin: 0 0 14px; }
69
69
 
70
- /* The three dialogs are the family modal on a native <dialog>: bg-card, one rule
71
- hairline, no shadow. The backdrop is a SCRIM — it has to darken what is behind
72
- it in EITHER mode, so it can take no ground token and no mix of --ink (which is
73
- white on black). That is why it was a literal; task 1003495 gave the value a
74
- home in style.css as --scrim instead, so the sheet keeps none. */
75
- .cd-dialog { width: min(480px, calc(100vw - 32px)); color: var(--ink); }
76
- .cd-dialog::backdrop { background: var(--scrim); }
70
+ /* The three dialogs are the hall's one dialog (controls.css .hall-dialog: the
71
+ panel, the scrim, the shadow, task 1005085). What stays is the form inside. */
72
+ .cd-dialog .cd-dialog__target { margin: var(--ds-space-xs) 0 0; font-size: var(--ds-text-sm-plus); line-height: 1.5; color: var(--ink-soft); }
77
73
  .cd-dialog .ov-form { margin: 14px 0 0; }
78
74
  .cd-dialog textarea { resize: vertical; line-height: 1.5; }
79
- .ov-modal .cd-dialog__err { min-height: 18px; margin: 8px 0 0; font-size: 13px; line-height: 1.45; color: var(--danger); }
75
+ .cd-dialog .cd-dialog__err { min-height: 18px; margin: 8px 0 0; font-size: 13px; line-height: 1.45; color: var(--danger); }
80
76
 
81
77
  /* The placeholder rule, shown only for a sentence that has holes in it (task
82
78
  1003114 / R03). It is a CONSTRAINT, not a footnote — losing a hole is the one
@@ -77,10 +77,10 @@
77
77
  <!-- The flag composer. A dialog rather than an inline field because the reason is
78
78
  the point: a one-line input invites "typo", and the queue is only worth
79
79
  reading if the reasons are. A native <dialog> (its own focus trap and
80
- Escape) dressed as the family's confirm modal. -->
81
- <dialog id="cd-dialog" class="ov-modal cd-dialog">
80
+ Escape) dressed as the hall's one dialog (controls.css .hall-dialog, task 1005085). -->
81
+ <dialog id="cd-dialog" class="hall-dialog cd-dialog" aria-labelledby="cd-dialog-h">
82
82
  <form method="dialog" id="cd-form">
83
- <h3 class="scroll__h" id="cd-dialog-h">Flag this copy</h3>
83
+ <h3 class="hall-dialog__title" id="cd-dialog-h">Flag this copy</h3>
84
84
  <p class="cd-dialog__target" id="cd-dialog-target"></p>
85
85
  <div class="ov-form">
86
86
  <div class="ov-field ov-field--wide">
@@ -103,17 +103,17 @@
103
103
  <p class="ov-form__why">The artist reads this instead of guessing. A pointer with no judgement costs them a look and tells them nothing.</p>
104
104
  </div>
105
105
  <p class="cd-dialog__err" id="cd-dialog-err" role="alert"></p>
106
- <div class="ov-modal__actions">
107
- <button type="button" class="btn-ghost" id="cd-cancel">Cancel</button>
108
- <button type="button" class="btn-accent" id="cd-submit">File the flag</button>
106
+ <div class="hall-dialog__actions">
107
+ <button type="button" class="btn btn--ghost" id="cd-cancel">Cancel</button>
108
+ <button type="button" class="btn" id="cd-submit">File the flag</button>
109
109
  </div>
110
110
  </form>
111
111
  </dialog>
112
112
 
113
113
  <!-- Closing a flag asks for a note for the same reason opening one does. -->
114
- <dialog id="cd-close-dialog" class="ov-modal cd-dialog">
114
+ <dialog id="cd-close-dialog" class="hall-dialog cd-dialog" aria-labelledby="cd-close-h">
115
115
  <form method="dialog" id="cd-close-form">
116
- <h3 class="scroll__h">Close this flag</h3>
116
+ <h3 class="hall-dialog__title" id="cd-close-h">Close this flag</h3>
117
117
  <p class="cd-dialog__target" id="cd-close-target"></p>
118
118
  <div class="ov-form">
119
119
  <div class="ov-field ov-field--wide">
@@ -135,9 +135,9 @@
135
135
  <p class="ov-form__why">The next person to notice this sentence reads your note instead of re-litigating it.</p>
136
136
  </div>
137
137
  <p class="cd-dialog__err" id="cd-close-err" role="alert"></p>
138
- <div class="ov-modal__actions">
139
- <button type="button" class="btn-ghost" id="cd-close-cancel">Cancel</button>
140
- <button type="button" class="btn-accent" id="cd-close-submit">Close it</button>
138
+ <div class="hall-dialog__actions">
139
+ <button type="button" class="btn btn--ghost" id="cd-close-cancel">Cancel</button>
140
+ <button type="button" class="btn" id="cd-close-submit">Close it</button>
141
141
  </div>
142
142
  </form>
143
143
  </dialog>
@@ -147,9 +147,9 @@
147
147
  wrong, a proposal says this is what it should say instead, and one of them
148
148
  creates work on the board. The textarea opens PREFILLED with the current
149
149
  wording — an artist edits a sentence, they do not retype it from memory. -->
150
- <dialog id="cd-propose-dialog" class="ov-modal cd-dialog">
150
+ <dialog id="cd-propose-dialog" class="hall-dialog cd-dialog" aria-labelledby="cd-propose-h">
151
151
  <form method="dialog" id="cd-propose-form">
152
- <h3 class="scroll__h">Propose new wording</h3>
152
+ <h3 class="hall-dialog__title" id="cd-propose-h">Propose new wording</h3>
153
153
  <p class="cd-dialog__target" id="cd-propose-target"></p>
154
154
  <div class="ov-form">
155
155
  <div class="ov-field ov-field--wide">
@@ -168,9 +168,9 @@
168
168
  <p class="ov-form__why">Nothing changes yet. This files a task carrying your wording; whoever claims it lands the diff, and it ships graded like every other change.</p>
169
169
  </div>
170
170
  <p class="cd-dialog__err" id="cd-propose-err" role="alert"></p>
171
- <div class="ov-modal__actions">
172
- <button type="button" class="btn-ghost" id="cd-propose-cancel">Cancel</button>
173
- <button type="button" class="btn-accent" id="cd-propose-submit">File the change</button>
171
+ <div class="hall-dialog__actions">
172
+ <button type="button" class="btn btn--ghost" id="cd-propose-cancel">Cancel</button>
173
+ <button type="button" class="btn" id="cd-propose-submit">File the change</button>
174
174
  </div>
175
175
  </form>
176
176
  </dialog>
@@ -181,6 +181,7 @@
181
181
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
182
182
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
183
183
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
184
+ <script src="/builders/hall-dialog.js"></script>
184
185
  <script src="/builders/palette.js" defer></script>
185
186
  <script src="/builders/hall-kit.js"></script>
186
187
  <script src="/builders/copy-desk.js?v=2026-09-01-redesign"></script>
@@ -354,6 +354,10 @@
354
354
  // the composer
355
355
  // ---------------------------------------------------------------------------
356
356
 
357
+ // The three dialogs are the hall's one dialog (hall-dialog.js, task 1005085):
358
+ // the shared Escape, focus trap, scrim click and focus return.
359
+ const dialog = (sel) => window.OTBDialog.bind($(sel));
360
+
357
361
  function openComposer(t) {
358
362
  target = t;
359
363
  $('#cd-dialog-target').innerHTML = t.string_id
@@ -361,7 +365,7 @@
361
365
  : `${pill('', 'whole surface')} <span class="ov-key ov-key--faint">${escapeHtml(t.surface)}</span>`;
362
366
  $('#cd-reason').value = '';
363
367
  $('#cd-dialog-err').textContent = '';
364
- $('#cd-dialog').showModal();
368
+ dialog('#cd-dialog').open();
365
369
  }
366
370
 
367
371
  async function submitFlag() {
@@ -387,7 +391,7 @@
387
391
  $('#cd-dialog-err').textContent = d.what_this_means || d.why || err.message || 'Could not file the flag.';
388
392
  return;
389
393
  }
390
- $('#cd-dialog').close();
394
+ dialog('#cd-dialog').close();
391
395
  toast('Flagged. It is in the queue.');
392
396
  await loadQueue();
393
397
  if ($('#cd-search').value.trim()) await runSearch();
@@ -423,7 +427,7 @@
423
427
  + `Keep all ${n} — move them if the new sentence needs them elsewhere, but do not remove one: `
424
428
  + 'the values come from the code, and dropping one is a code change.';
425
429
  }
426
- $('#cd-propose-dialog').showModal();
430
+ dialog('#cd-propose-dialog').open();
427
431
  }
428
432
 
429
433
  async function submitProposal() {
@@ -463,7 +467,7 @@
463
467
  err.textContent = d.what_this_means || d.why || e.message || 'Could not file the change.';
464
468
  return;
465
469
  }
466
- $('#cd-propose-dialog').close();
470
+ dialog('#cd-propose-dialog').close();
467
471
  const task = (res.data && res.data.task) || {};
468
472
  toast(res.data && res.data.created === false
469
473
  ? `Already filed — task #${task.id} carries this exact change.`
@@ -486,7 +490,7 @@
486
490
  $('#cd-close-note').value = '';
487
491
  $('#cd-close-task').value = '';
488
492
  $('#cd-close-err').textContent = '';
489
- $('#cd-close-dialog').showModal();
493
+ dialog('#cd-close-dialog').open();
490
494
  }
491
495
 
492
496
  async function submitClose() {
@@ -506,7 +510,7 @@
506
510
  $('#cd-close-err').textContent = d.what_this_means || d.why || err.message || 'Could not close the flag.';
507
511
  return;
508
512
  }
509
- $('#cd-close-dialog').close();
513
+ dialog('#cd-close-dialog').close();
510
514
  toast('Closed.');
511
515
  await loadQueue();
512
516
  }
@@ -560,11 +564,11 @@
560
564
  if (closeBtn) openCloser(closeBtn.dataset.flag);
561
565
  });
562
566
 
563
- $('#cd-cancel').addEventListener('click', () => $('#cd-dialog').close());
567
+ $('#cd-cancel').addEventListener('click', () => dialog('#cd-dialog').close());
564
568
  $('#cd-submit').addEventListener('click', submitFlag);
565
- $('#cd-propose-cancel').addEventListener('click', () => $('#cd-propose-dialog').close());
569
+ $('#cd-propose-cancel').addEventListener('click', () => dialog('#cd-propose-dialog').close());
566
570
  $('#cd-propose-submit').addEventListener('click', submitProposal);
567
- $('#cd-close-cancel').addEventListener('click', () => $('#cd-close-dialog').close());
571
+ $('#cd-close-cancel').addEventListener('click', () => dialog('#cd-close-dialog').close());
568
572
  $('#cd-close-submit').addEventListener('click', submitClose);
569
573
 
570
574
  (async () => {
@@ -82,6 +82,7 @@
82
82
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
83
83
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
84
84
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
85
+ <script src="/builders/hall-dialog.js"></script>
85
86
  <script src="/builders/palette.js" defer></script>
86
87
  <script src="/builders/hall-kit.js"></script>
87
88
  <!-- the shared ask card the move is drawn with (task 1004829) -->
@@ -583,10 +583,10 @@
583
583
  // The slices still render into their divs below (kept off-screen in
584
584
  // #attention-slices), and refresh() rebuilds the cards from what they hold.
585
585
  '<div class="attn-cards" id="attention-cards"></div>' +
586
- '<dialog class="attn-dialog" id="attention-dialog" aria-labelledby="attention-dialog-h">' +
587
- '<div class="attn-dialog__head"><h3 class="attn-dialog__h" id="attention-dialog-h"></h3>' +
588
- '<button type="button" class="attn-dialog__x" id="attention-dialog-x" aria-label="Close">×</button></div>' +
589
- '<div class="attn-dialog__body" id="attention-dialog-body"></div>' +
586
+ '<dialog class="hall-dialog hall-dialog--wide" id="attention-dialog" aria-labelledby="attention-dialog-h">' +
587
+ '<div class="hall-dialog__head"><h3 class="hall-dialog__title" id="attention-dialog-h"></h3>' +
588
+ '<button type="button" class="hall-dialog__x" id="attention-dialog-x" data-dialog-close aria-label="Close">×</button></div>' +
589
+ '<div class="hall-dialog__body attn-dialog__body" id="attention-dialog-body"></div>' +
590
590
  '</dialog>' +
591
591
  '<div id="attention-slices" hidden>' +
592
592
  // task 1003464: the system's own notices — rebase owed, an input the
@@ -692,36 +692,51 @@
692
692
  cards.innerHTML = html.join('');
693
693
  }
694
694
 
695
- function openCard(id) {
696
- const dlg = host.querySelector('#attention-dialog');
695
+ // The window is the hall's one dialog (hall-dialog.js, task 1005085): the
696
+ // shared panel, scrim, Escape, focus trap and focus return. Closing it by any
697
+ // road (the mark, Escape, the scrim) moves the slice back and redraws the cards.
698
+ let lastCard = null;
699
+ function windowHandle() {
700
+ const dlg = host && host.querySelector('#attention-dialog');
701
+ if (!dlg) return null;
702
+ return window.OTBDialog.bind(dlg, {
703
+ onClose: restoreSlice,
704
+ // the cards were redrawn, so the opener is gone: focus its successor
705
+ returnFocus: (op) => (op && op.isConnected ? op
706
+ : (lastCard && host.querySelector(`[data-attn-open="${CSS.escape(lastCard)}"]`)) || host.querySelector('.attn-card')),
707
+ });
708
+ }
709
+
710
+ function openCard(id, trigger) {
711
+ const h = windowHandle();
697
712
  const div = host.querySelector(`#attention-slices > #${CSS.escape(id)}`);
698
- if (!dlg || !div) return;
713
+ if (!h || !div) return;
699
714
  host.querySelector('#attention-dialog-h').textContent = sliceFacts(div).title;
700
715
  host.querySelector('#attention-dialog-body').appendChild(div);
701
716
  openSlice = div;
702
- if (typeof dlg.showModal === 'function') dlg.showModal(); else dlg.setAttribute('open', '');
717
+ lastCard = id;
718
+ h.open(trigger);
703
719
  }
704
720
 
705
- function closeCard() {
706
- const dlg = host && host.querySelector('#attention-dialog');
721
+ function restoreSlice() {
707
722
  if (openSlice) {
708
723
  host.querySelector('#attention-slices').appendChild(openSlice);
709
724
  openSlice = null;
710
725
  }
711
- if (dlg && dlg.open && typeof dlg.close === 'function') dlg.close();
712
726
  renderCards();
713
727
  }
714
728
 
729
+ function closeCard() {
730
+ const h = windowHandle();
731
+ if (h && h.isOpen()) h.close();
732
+ else restoreSlice();
733
+ }
734
+
715
735
  function wireCards() {
716
736
  host.addEventListener('click', (e) => {
717
737
  const card = e.target.closest('[data-attn-open]');
718
- if (card) { openCard(card.getAttribute('data-attn-open')); return; }
719
- if (e.target.closest('#attention-dialog-x')) { closeCard(); return; }
720
- // a click on the backdrop lands on the <dialog> itself
721
- if (e.target.id === 'attention-dialog') closeCard();
738
+ if (card) openCard(card.getAttribute('data-attn-open'), card);
722
739
  });
723
- const dlg = host.querySelector('#attention-dialog');
724
- if (dlg) dlg.addEventListener('close', () => { if (openSlice) closeCard(); });
725
740
  }
726
741
 
727
742
  // The card's shared visibility contract: any slice renderer calls refresh()
@@ -141,6 +141,7 @@
141
141
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
142
142
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
143
143
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
144
+ <script src="/builders/hall-dialog.js"></script>
144
145
  <script src="/builders/palette.js" defer></script>
145
146
 
146
147
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -540,8 +540,14 @@
540
540
  async function doGoalRelease(claimId, taskId, btn) {
541
541
  const key = `release:${claimId}`;
542
542
  if (pendingClaims.has(key)) return;
543
- if (!window.confirm(`Release your claim on #${taskId}? It returns to the queue and no ${curLabel()} are earned.`)) return;
543
+ // Held while the confirm is open too, so a second click cannot stack a second dialog.
544
544
  pendingClaims.add(key);
545
+ const yes = await window.OTBDialog.confirm({
546
+ title: `Release your claim on #${taskId}?`,
547
+ body: `It returns to the queue and no ${curLabel()} are earned.`,
548
+ confirmLabel: 'Release', danger: true, trigger: btn,
549
+ });
550
+ if (!yes) { pendingClaims.delete(key); return; }
545
551
  if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
546
552
  try {
547
553
  const out = await claimAction.release(claimId, taskId);
@@ -254,6 +254,7 @@
254
254
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
255
255
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
256
256
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
257
+ <script src="/builders/hall-dialog.js"></script>
257
258
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
258
259
  <script src="/builders/palette.js" defer></script>
259
260
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -158,6 +158,7 @@
158
158
  <script src="/builders/shell-trail.js?v=2026-11-05-numbers"></script>
159
159
  <script src="/builders/craft-mode.js?v=2026-11-05-goals"></script>
160
160
  <script src="/builders/shell.js?v=2026-11-07-brand-mark"></script>
161
+ <script src="/builders/hall-dialog.js"></script>
161
162
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
162
163
  <script src="/builders/palette.js" defer></script>
163
164
  <script src="/builders/hall-kit.js"></script>