@bongos/core 1.24.51 → 1.24.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +125 -95
- package/docs/copy-inventory.md +225 -242
- package/docs/copy-registry.json +278 -452
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +33 -0
- package/docs/page-readings.json +515 -531
- package/modules/discord/webhook.js +28 -6
- package/modules/hall-ui/public/ai-studio.html +1 -0
- package/modules/hall-ui/public/approval-queue.css +3 -11
- package/modules/hall-ui/public/approval-queue.js +13 -20
- package/modules/hall-ui/public/atlas.html +1 -0
- package/modules/hall-ui/public/blockers.html +1 -0
- package/modules/hall-ui/public/board-room.html +1 -0
- package/modules/hall-ui/public/bulk-toolbar.js +5 -1
- package/modules/hall-ui/public/city.html +1 -0
- package/modules/hall-ui/public/collab.html +1 -0
- package/modules/hall-ui/public/commands.html +1 -0
- package/modules/hall-ui/public/controls.css +100 -0
- package/modules/hall-ui/public/copy-desk.css +7 -11
- package/modules/hall-ui/public/copy-desk.html +17 -16
- package/modules/hall-ui/public/copy-desk.js +13 -9
- package/modules/hall-ui/public/deploy.html +1 -0
- package/modules/hall-ui/public/goal-inbox.js +32 -17
- package/modules/hall-ui/public/goal-map.html +1 -0
- package/modules/hall-ui/public/goals-page.js +7 -1
- package/modules/hall-ui/public/goals.html +1 -0
- package/modules/hall-ui/public/government.html +1 -0
- package/modules/hall-ui/public/government.js +10 -2
- package/modules/hall-ui/public/hall-dialog.js +255 -0
- package/modules/hall-ui/public/hall-kit.js +9 -2
- package/modules/hall-ui/public/idea.html +1 -0
- package/modules/hall-ui/public/ideas.html +1 -0
- package/modules/hall-ui/public/index.html +1 -0
- package/modules/hall-ui/public/market-watch-item.html +2 -1
- package/modules/hall-ui/public/market-watch-lib.js +16 -9
- package/modules/hall-ui/public/market-watch-rules.html +1 -0
- package/modules/hall-ui/public/market-watch.css +2 -3
- package/modules/hall-ui/public/market-watch.html +2 -1
- package/modules/hall-ui/public/modules.html +1 -0
- package/modules/hall-ui/public/money.html +1 -0
- package/modules/hall-ui/public/my-agents.html +1 -0
- package/modules/hall-ui/public/oversight.css +3 -33
- package/modules/hall-ui/public/primer.html +1 -0
- package/modules/hall-ui/public/profile.html +1 -0
- package/modules/hall-ui/public/project-settings.html +1 -0
- package/modules/hall-ui/public/roster.html +1 -0
- package/modules/hall-ui/public/settings-ai-keys.js +5 -1
- package/modules/hall-ui/public/settings-passkeys.js +5 -1
- package/modules/hall-ui/public/settings-sessions.js +10 -2
- package/modules/hall-ui/public/settings.html +1 -0
- package/modules/hall-ui/public/settings.js +5 -1
- package/modules/hall-ui/public/street.html +1 -0
- package/modules/hall-ui/public/studio.css +6 -32
- package/modules/hall-ui/public/studio.html +4 -3
- package/modules/hall-ui/public/studio.js +40 -35
- package/modules/hall-ui/public/style.css +2 -8
- package/modules/hall-ui/public/task.html +1 -0
- package/modules/hall-ui/public/task.js +26 -12
- package/modules/hall-ui/public/thinking.html +1 -0
- package/modules/hall-ui/public/tweak-editor.html +1 -0
- package/modules/hall-ui/public/watch.html +1 -0
- package/modules/hall-ui/public/work.html +1 -0
- package/modules/hall-ui/public/work.js +7 -1
- package/modules/public-landing/public/index.html +11 -8
- package/modules/ui-design/public/brand/og-card.png +0 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-docs/index.html +107 -12
- package/release-notes.json +12 -0
- package/scripts/bongos/gen-brand.js +40 -6
- package/scripts/discord/lib.js +17 -3
- package/scripts/status-mirror-sync.js +11 -7
- package/src/bongos/serve-internal.js +3 -2
- package/src/error-page.js +120 -0
- package/src/link-preview.js +71 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +22 -8
- package/tests/brand_surfaces.mjs +213 -0
- package/tests/bulk_toolbar.mjs +1 -1
- package/tests/discord_board_broadcast.mjs +23 -10
- package/tests/docs_openapi_route.mjs +2 -1
- package/tests/hall_approval_queue.mjs +8 -2
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_collab_world.mjs +9 -6
- package/tests/hall_dialog.mjs +401 -0
- package/tests/hall_error_envelope.mjs +1 -1
- package/tests/hall_oversight_world.mjs +9 -7
- package/tests/hall_record_world.mjs +1 -1
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_studio_home.mjs +1 -1
- package/tests/home_attention_cards.mjs +2 -0
- package/tests/kind_axis_unset.mjs +1 -1
- package/tests/market_watch_pages.mjs +3 -1
|
@@ -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="
|
|
587
|
-
'<div class="
|
|
588
|
-
'<button type="button" class="
|
|
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
|
-
|
|
696
|
-
|
|
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 (!
|
|
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
|
-
|
|
717
|
+
lastCard = id;
|
|
718
|
+
h.open(trigger);
|
|
703
719
|
}
|
|
704
720
|
|
|
705
|
-
function
|
|
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)
|
|
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
|
-
|
|
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>
|
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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 += '
|
|
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 += '
|
|
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 += '
|
|
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="
|
|
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 () {
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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>
|