@bongos/core 1.24.61 → 1.24.63
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 +54 -44
- package/docs/copy-inventory.md +152 -151
- package/docs/copy-registry.json +232 -203
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +1 -0
- package/docs/page-readings.json +281 -249
- package/modules/hall-ui/public/approval-queue.css +5 -3
- package/modules/hall-ui/public/controls.css +10 -0
- package/modules/hall-ui/public/gate.js +11 -2
- package/modules/hall-ui/public/hall-dialog.js +50 -2
- package/modules/hall-ui/public/hall-kit.js +8 -3
- package/modules/hall-ui/public/roster.js +13 -5
- package/modules/hall-ui/public/settings-passkeys.js +4 -1
- package/modules/hall-ui/public/studio.css +19 -20
- package/modules/hall-ui/public/studio.html +1 -2
- package/modules/hall-ui/public/studio.js +6 -2
- package/modules/hall-ui/public/style.css +17 -5
- package/modules/hall-ui/public/thinking.css +18 -1
- package/modules/hall-ui/records/studio.md +2 -1
- package/modules/hall-ui/specimens/components.css +7 -0
- package/modules/npm-release/public/work.js +6 -1
- package/modules/public-landing/public/assets/cosmos.css +36 -13
- package/modules/public-landing/public/index.html +9 -1
- package/modules/public-landing/public/projects.html +11 -3
- package/modules/raise/page.js +3 -1
- package/modules/raise/public/edit.js +10 -3
- package/modules/raise/public/raise.css +31 -4
- package/modules/render-deploy/public/deploy.js +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/hall_dialog.mjs +169 -17
- package/tests/hall_studio_home.mjs +8 -1
- package/tests/hall_topbar_320.mjs +145 -0
- package/tests/landing_shared_controls.mjs +157 -0
- package/tests/npm_release_release.mjs +1 -1
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_pre_uat.mjs +3 -1
- package/tests/roster_government_columns.mjs +3 -1
package/modules/raise/page.js
CHANGED
|
@@ -131,7 +131,9 @@ async function renderPage({ editor }) {
|
|
|
131
131
|
return R.fill(view('page.html'), {
|
|
132
132
|
base: BASE,
|
|
133
133
|
assetVersion: v,
|
|
134
|
-
|
|
134
|
+
// the editor asks its questions in the hall's one dialog: the shared escaper first,
|
|
135
|
+
// then the dialog, then the editor (task 1005136)
|
|
136
|
+
editorAssets: editor ? `<script src="/modules/hall-ui/public/dom-utils.js" defer></script><script src="/modules/hall-ui/public/hall-dialog.js" defer></script><script src="${BASE}/assets/edit.js?v=${v}" defer></script>` : '',
|
|
135
137
|
draftBanner: c.draft ? '<div class="draft" role="note">DRAFT</div>' : '',
|
|
136
138
|
editBar: editor ? editBar(editor, c) : '',
|
|
137
139
|
entity: R.esc(c.entity),
|
|
@@ -120,10 +120,17 @@
|
|
|
120
120
|
|
|
121
121
|
startBtn.addEventListener('click', function () { status.textContent = ''; setEditing(true); });
|
|
122
122
|
|
|
123
|
+
// Cancel with edits asks in the hall's own dialog (hall-dialog.js, loaded with this
|
|
124
|
+
// file for an editor; task 1005136), never the browser's box.
|
|
123
125
|
cancelBtn.addEventListener('click', function () {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
var asked = changes().count
|
|
127
|
+
? window.OTBDialog.confirm({ title: 'Discard your unsaved changes?', confirmLabel: 'Discard', trigger: cancelBtn })
|
|
128
|
+
: Promise.resolve(true);
|
|
129
|
+
asked.then(function (yes) {
|
|
130
|
+
if (!yes) return;
|
|
131
|
+
editing = false;
|
|
132
|
+
location.reload();
|
|
133
|
+
});
|
|
127
134
|
});
|
|
128
135
|
|
|
129
136
|
saveBtn.addEventListener('click', function () {
|
|
@@ -66,9 +66,9 @@ p { margin: 0 0 16px; max-width: 64ch; }
|
|
|
66
66
|
|
|
67
67
|
.btn {
|
|
68
68
|
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
69
|
-
font: 600
|
|
70
|
-
border: 0; border-radius:
|
|
71
|
-
transition: transform
|
|
69
|
+
font: 600 var(--ds-text-xl) var(--font); color: var(--on-accent); background: var(--accent);
|
|
70
|
+
border: 0; border-radius: var(--ds-radius-pill); padding: 13px 26px; cursor: pointer;
|
|
71
|
+
transition: transform var(--ds-dur-fast) var(--ds-ease), filter var(--ds-dur-fast) var(--ds-ease);
|
|
72
72
|
}
|
|
73
73
|
.btn:hover:not(:disabled) { filter: brightness(1.05); transform: translateY(-1px); }
|
|
74
74
|
.btn:disabled { background: var(--deep); color: var(--ink-faint); cursor: not-allowed; }
|
|
@@ -198,7 +198,7 @@ section { padding: 72px 0; border-bottom: 1px solid var(--rule-soft); }
|
|
|
198
198
|
.draft { letter-spacing: .2em; }
|
|
199
199
|
|
|
200
200
|
/* ── team: Sign in with Bongos + in-place editing ───────── */
|
|
201
|
-
.btn--ghost { background: transparent; color: var(--ink); border:
|
|
201
|
+
.btn--ghost { background: transparent; color: var(--ink); border: var(--ds-line) solid var(--rule); padding: 10px 20px; font-size: var(--ds-text-lg); }
|
|
202
202
|
.btn--ghost:hover { border-color: var(--accent); }
|
|
203
203
|
.gate__team { margin-top: 36px; padding-top: 24px; border-top: 1px solid var(--rule-soft); display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; font-size: 14px; color: var(--ink-soft); text-decoration: none; }
|
|
204
204
|
.gate__team a { text-decoration: none; }
|
|
@@ -232,3 +232,30 @@ section { padding: 72px 0; border-bottom: 1px solid var(--rule-soft); }
|
|
|
232
232
|
.tok { white-space: nowrap; }
|
|
233
233
|
.is-editing .tok, .is-editing .cite { background: color-mix(in srgb, var(--sea) 18%, transparent); border-radius: 3px; padding: 0 2px; cursor: default; }
|
|
234
234
|
.is-editing [data-kind="para"], .is-editing [data-kind="list"] { padding: 4px 6px; margin-left: -6px; margin-right: -6px; }
|
|
235
|
+
|
|
236
|
+
/* ── the dialog ─────────────────────────────────────────────
|
|
237
|
+
The editor's "discard your changes?" is the hall's one dialog (hall-dialog.js,
|
|
238
|
+
OTBDialog.confirm; task 1005136), drawn here on this page's tokens and the shared
|
|
239
|
+
scales: the card ground, one hairline, the panel corner, the one floating shadow,
|
|
240
|
+
the modal layer, over a black scrim (the one literal the Fifteen Rule allows). */
|
|
241
|
+
.hall-dialog {
|
|
242
|
+
z-index: var(--ds-z-modal); box-sizing: border-box;
|
|
243
|
+
width: min(480px, calc(100vw - 2 * var(--ds-space-sm)));
|
|
244
|
+
max-height: calc(100dvh - 2 * var(--ds-space-md)); overflow-y: auto;
|
|
245
|
+
margin: auto; padding: var(--ds-space-md);
|
|
246
|
+
color: var(--ink); background: var(--card);
|
|
247
|
+
border: var(--ds-line) solid var(--rule); border-radius: var(--ds-radius-panel);
|
|
248
|
+
box-shadow: var(--ds-shadow-pop);
|
|
249
|
+
}
|
|
250
|
+
.hall-dialog::backdrop { background: rgba(0, 0, 0, .55); }
|
|
251
|
+
.hall-dialog__head { display: flex; align-items: flex-start; gap: var(--ds-space-xs); margin: 0 0 var(--ds-space-xs); }
|
|
252
|
+
.hall-dialog__title { flex: 1; margin: 0; font-size: var(--ds-text-3xl); font-weight: 500; line-height: 1.25; }
|
|
253
|
+
.hall-dialog__x {
|
|
254
|
+
flex: none; min-width: 32px; min-height: 32px; padding: 0;
|
|
255
|
+
font: inherit; font-size: var(--ds-text-4xl); line-height: 1; color: var(--ink-soft);
|
|
256
|
+
background: transparent; border: 0; border-radius: var(--ds-radius-pill); cursor: pointer;
|
|
257
|
+
}
|
|
258
|
+
.hall-dialog__x:hover { color: var(--ink); }
|
|
259
|
+
.hall-dialog__body p { margin: 0 0 var(--ds-space-xs); font-size: var(--ds-text-md); line-height: 1.5; color: var(--ink-soft); }
|
|
260
|
+
.hall-dialog__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--ds-space-xs); margin-top: var(--ds-space-md); }
|
|
261
|
+
.hall-dialog__actions .btn { font-size: var(--ds-text-lg); padding: 10px 20px; }
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
});
|
|
198
198
|
|
|
199
199
|
// Step two. Closes over `p`, the press that was shown, so what is sent is what was displayed.
|
|
200
|
-
const
|
|
200
|
+
const confirmAction = () => {
|
|
201
201
|
const p = state.pending;
|
|
202
202
|
if (!p) return;
|
|
203
203
|
return run(async () => {
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
const rb = t.closest('[data-rollback]');
|
|
219
219
|
if (rb) { state.pending = { action: 'rollback', deployId: rb.getAttribute('data-rollback'), liveWas: state.hist.liveDeployId }; state.error = null; state.done = null; return paint(); }
|
|
220
220
|
if (t.closest('#rd-cancel')) { state.pending = null; return paint(); }
|
|
221
|
-
if (t.closest('#rd-confirm')) return
|
|
221
|
+
if (t.closest('#rd-confirm')) return confirmAction();
|
|
222
222
|
const v = t.closest('[data-view]');
|
|
223
223
|
if (v) { state.viewing = v.getAttribute('data-view'); return paint(); }
|
|
224
224
|
if (t.closest('#rd-unview')) { state.viewing = null; return paint(); }
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.63",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.63",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.63",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11341,5 +11341,17 @@
|
|
|
11341
11341
|
"id": "1005139",
|
|
11342
11342
|
"text": "The design docs now describe what is actually live, and the landing page shows a company's logo only when that company's settings provide one, so other projects no longer show the Verro Labs medallion."
|
|
11343
11343
|
}
|
|
11344
|
+
],
|
|
11345
|
+
"1.24.62": [
|
|
11346
|
+
{
|
|
11347
|
+
"id": "1005137",
|
|
11348
|
+
"text": "On a phone the hall's top bar now fits without cutting off buttons, the Studio stays a night room in dark mode, the sky page's dots no longer cover its labels, and the components reference page is readable in dark mode."
|
|
11349
|
+
}
|
|
11350
|
+
],
|
|
11351
|
+
"1.24.63": [
|
|
11352
|
+
{
|
|
11353
|
+
"id": "1005136",
|
|
11354
|
+
"text": "The last browser pop-up boxes are gone: every question the site asks (rename, give a reason, are you sure) now opens in Cloud Bongos's own dialog, and on a phone the signed-in front page no longer squeezes the logo under the m"
|
|
11355
|
+
}
|
|
11344
11356
|
]
|
|
11345
11357
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.63'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/hall_dialog.mjs
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
// branded confirm (what will happen, in words) and runs its write only when the answer is yes;
|
|
11
11
|
// 3. the guards: no window.confirm anywhere in modules/hall-ui, and each of the six builds now
|
|
12
12
|
// runs on OTBDialog with no scrim/backdrop rule of its own.
|
|
13
|
+
// Task 1005136 adds the prompt variant (OTBDialog.prompt, which replaced every window.prompt), its
|
|
14
|
+
// flows, and widens the guard repo-wide: no browser confirm, alert or prompt in any served page
|
|
15
|
+
// (every module's public/ and views/, the API docs, and the pages src/ renders).
|
|
13
16
|
import assert from 'node:assert/strict';
|
|
14
17
|
import { test, after } from 'node:test';
|
|
15
18
|
import { createRequire } from 'node:module';
|
|
@@ -55,9 +58,13 @@ async function runFlow(file, name, args, { answer, globals = {} } = {}) {
|
|
|
55
58
|
const writes = [];
|
|
56
59
|
const record = (m, u, b) => { writes.push({ method: m, url: String(u), body: b }); return { ok: true, status: 200, data: { permission_count: 3 } }; };
|
|
57
60
|
const window = {
|
|
58
|
-
OTBDialog: {
|
|
61
|
+
OTBDialog: {
|
|
62
|
+
confirm: async (o) => { asked.push(o); return answer; },
|
|
63
|
+
prompt: async (o) => { asked.push(o); return answer; },
|
|
64
|
+
},
|
|
59
65
|
location: { assign() {} },
|
|
60
66
|
confirm: () => { throw new Error('window.confirm was called'); },
|
|
67
|
+
prompt: () => { throw new Error('window.prompt was called'); },
|
|
61
68
|
};
|
|
62
69
|
const ctx = vm.createContext({
|
|
63
70
|
window,
|
|
@@ -135,6 +142,37 @@ for (const f of FLOWS) {
|
|
|
135
142
|
});
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
// Every replaced window.prompt whose flow is a named function: the branded prompt is asked, a
|
|
146
|
+
// dismissal (null) sends nothing, and the typed text is what the write carries.
|
|
147
|
+
const PROMPT_FLOWS = [
|
|
148
|
+
{ flow: 'name a passkey card', file: 'settings-passkeys.js', fn: 'rename',
|
|
149
|
+
args: () => ['cred-1', { querySelector: (q) => (q === 'strong' ? { textContent: 'Old card' } : {}) }],
|
|
150
|
+
write: /passkeys\/rename/, carries: (w, text) => w.body.nickname === text },
|
|
151
|
+
{ flow: 'release a studio round', file: 'studio.js', fn: 'releasePage', args: () => ['page-1', 77, fakeBtn()],
|
|
152
|
+
globals: { lib: { PAGE_ID_RE: /^[a-z0-9-]+$/ }, WRITE_ROUTES: { release: (id) => `/tweak/${id}/release` }, loadPages: async () => {}, paintPanel() {}, byId: () => null },
|
|
153
|
+
write: /tweak\/page-1\/release/, carries: (w, text) => w.body.note === text && w.body.task_id === 77 },
|
|
154
|
+
{ flow: 'rename a saved view', file: 'hall-kit.js', fn: 'renameView', args: () => [{ id: 5, name: 'Mine' }],
|
|
155
|
+
viewCall: true, globals: { act: (fn) => fn(), views: { page: 'work' } },
|
|
156
|
+
write: /^\/5$/, carries: (w, text) => w.body.name === text },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
for (const f of PROMPT_FLOWS) {
|
|
160
|
+
test(`${f.flow}: the branded prompt asks, a dismissal sends nothing, the typed text is sent`, async () => {
|
|
161
|
+
let sent = [];
|
|
162
|
+
const extra = { ...(f.globals || {}) };
|
|
163
|
+
if (f.viewCall) extra.call = (m, u, b) => { sent.push({ method: m, url: u, body: b }); };
|
|
164
|
+
const no = await runFlow(f.file, f.fn, f.args(), { answer: null, globals: extra });
|
|
165
|
+
assert.equal(no.asked.length, 1, 'the prompt is shown once');
|
|
166
|
+
assert.ok(no.asked[0].title && no.asked[0].confirmLabel, 'it has a title and names its act');
|
|
167
|
+
assert.equal([...no.writes, ...sent].filter((w) => f.write.test(w.url)).length, 0, 'a dismissal sends nothing');
|
|
168
|
+
sent = [];
|
|
169
|
+
const yes = await runFlow(f.file, f.fn, f.args(), { answer: 'A new line', globals: extra });
|
|
170
|
+
const hits = [...yes.writes, ...sent].filter((w) => f.write.test(w.url));
|
|
171
|
+
assert.equal(hits.length, 1, 'the answer sends the write, once');
|
|
172
|
+
assert.ok(f.carries(hits[0], 'A new line'), 'the write carries the typed text');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
138
176
|
test('a verify-kind confirm attests the verification; any other kind asks nothing', async () => {
|
|
139
177
|
const sent = [];
|
|
140
178
|
const runLifecycle = (b, u, body) => sent.push({ u, body });
|
|
@@ -182,35 +220,74 @@ test('the bulk release on the shared toolbar asks first, and releases only on ye
|
|
|
182
220
|
|
|
183
221
|
// ── the guards ──────────────────────────────────────────────────────────────
|
|
184
222
|
|
|
185
|
-
|
|
223
|
+
// Every file a browser is served, repo-wide (task 1005136): each module's public/ and views/, the
|
|
224
|
+
// API docs, and src/ (whose server-rendered pages carry inline scripts). Vendored bundles excluded.
|
|
225
|
+
function servedFiles() {
|
|
186
226
|
const out = [];
|
|
187
227
|
const walk = (d) => {
|
|
228
|
+
if (!fs.existsSync(d)) return;
|
|
188
229
|
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
189
230
|
const p = path.join(d, e.name);
|
|
190
|
-
if (e.isDirectory()) walk(p);
|
|
191
|
-
else if (/\.(js|html|mjs|cjs)$/.test(e.name)) out.push(p);
|
|
231
|
+
if (e.isDirectory()) { if (!/^(node_modules|vendor|fixtures)$/.test(e.name)) walk(p); }
|
|
232
|
+
else if (/\.(js|html|mjs|cjs)$/.test(e.name) && !/\.(standalone|min)\.js$/.test(e.name)) out.push(p);
|
|
192
233
|
}
|
|
193
234
|
};
|
|
194
|
-
|
|
235
|
+
for (const m of fs.readdirSync(path.join(ROOT, 'modules'))) {
|
|
236
|
+
walk(path.join(ROOT, 'modules', m, 'public'));
|
|
237
|
+
walk(path.join(ROOT, 'modules', m, 'views'));
|
|
238
|
+
}
|
|
239
|
+
walk(path.join(ROOT, 'public-docs'));
|
|
240
|
+
walk(path.join(ROOT, 'src'));
|
|
195
241
|
return out;
|
|
196
242
|
}
|
|
197
243
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
244
|
+
// A call to one of the browser's three boxes: window.confirm/alert/prompt, or a bare confirm( /
|
|
245
|
+
// alert( / prompt( that is not a method (OTBDialog.confirm) or the definition of one. Read line
|
|
246
|
+
// by line, comments out: a whole-file comment stripper eats code across a `/*` inside a string
|
|
247
|
+
// ('/api/bongos/*'), which would let a real call hide behind it. A block comment is one that
|
|
248
|
+
// OPENS a line (how every file here writes them); a same-line /* */, <!-- --> and a trailing
|
|
249
|
+
// // comment are cut where they stand.
|
|
250
|
+
const BOX = /\bwindow\s*\.\s*(confirm|alert|prompt)\b|(^|[^.\w$])(confirm|alert|prompt)\s*\(/;
|
|
251
|
+
function browserBoxCall(rawSrc) {
|
|
252
|
+
let block = null;
|
|
253
|
+
for (const raw of String(rawSrc).split('\n')) {
|
|
254
|
+
let t = raw.trim();
|
|
255
|
+
if (block) {
|
|
256
|
+
const end = t.indexOf(block);
|
|
257
|
+
if (end === -1) continue;
|
|
258
|
+
t = t.slice(end + block.length);
|
|
259
|
+
block = null;
|
|
205
260
|
}
|
|
261
|
+
if (t.startsWith('/*') && !t.includes('*/')) { block = '*/'; continue; }
|
|
262
|
+
if (t.startsWith('<!--') && !t.includes('-->')) { block = '-->'; continue; }
|
|
263
|
+
if (/^(\/\/|\*)/.test(t)) continue;
|
|
264
|
+
const code = t.replace(/\/\*.*?\*\//g, '').replace(/<!--.*?-->/g, '')
|
|
265
|
+
.replace(/(^|[^:'"`\\])\/\/.*$/, '$1').replace(/function\s+(confirm|alert|prompt)\s*\(/g, '');
|
|
266
|
+
if (BOX.test(code)) return true;
|
|
206
267
|
}
|
|
207
|
-
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
test('no browser confirm, alert or prompt in any served page (comments stripped)', () => {
|
|
272
|
+
const files = servedFiles();
|
|
273
|
+
assert.ok(files.length > 200 && files.some((f) => f.includes(`${path.sep}public-landing${path.sep}`)) && files.some((f) => f.includes(`${path.sep}raise${path.sep}`)),
|
|
274
|
+
'the scan reaches beyond the hall');
|
|
275
|
+
const hits = files.filter((f) => browserBoxCall(fs.readFileSync(f, 'utf8'))).map((f) => path.relative(ROOT, f));
|
|
276
|
+
assert.deepEqual(hits, [], 'use OTBDialog.confirm / OTBDialog.prompt (hall-dialog.js) instead of the browser\'s box');
|
|
208
277
|
});
|
|
209
278
|
|
|
210
|
-
test('the guard catches a
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
279
|
+
test('the guard catches a browser box when one comes back, and leaves the shared dialog alone', () => {
|
|
280
|
+
for (const bad of ["if (!window.confirm('Delete?')) return;", "const n = window.prompt('Name');", "alert('done')",
|
|
281
|
+
"if (x && !confirm('Discard?')) return;", "var r = prompt('Why?') || null;"]) {
|
|
282
|
+
assert.ok(browserBoxCall(bad), `caught: ${bad}`);
|
|
283
|
+
}
|
|
284
|
+
// a `/*` inside a string must not hide a real call on a later line
|
|
285
|
+
assert.ok(browserBoxCall("const p = '/api/bongos/*';\nif (!confirm('Go?')) return;\nconst q = 'a */ b';"), 'caught past a /* in a string');
|
|
286
|
+
for (const ok of ["// window.confirm('old') is history\nOTBDialog.confirm({})", '/* window.prompt() was here */ await window.OTBDialog.prompt({ title: "x" })',
|
|
287
|
+
'/* the browser box:\n window.confirm() calls whose box\n*/ OTBDialog.confirm({})', '<!-- an old\n confirm() lived here -->',
|
|
288
|
+
'function confirm(opts) {}', 'const confirmAction = () => {}; confirmAction();']) {
|
|
289
|
+
assert.ok(!browserBoxCall(ok), `left alone: ${ok}`);
|
|
290
|
+
}
|
|
214
291
|
});
|
|
215
292
|
|
|
216
293
|
test('every hall page loads the shared dialog right after the shell, deferred alike', () => {
|
|
@@ -399,3 +476,78 @@ test('a confirm over a bound overlay: Escape closes the confirm only, then the o
|
|
|
399
476
|
assert.equal(ov.modal, 'true');
|
|
400
477
|
await ctx.close();
|
|
401
478
|
});
|
|
479
|
+
|
|
480
|
+
// ── the prompt (task 1005136) ────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
test('the prompt: a labelled field in the shared dialog, focused with its text selected; Enter answers with what was typed', { skip }, async () => {
|
|
483
|
+
const { ctx, page } = await boot();
|
|
484
|
+
await page.focus('#go');
|
|
485
|
+
await page.evaluate(() => { window.__r = window.OTBDialog.prompt({ title: 'Name this card', label: 'Shown on your sign-in list', value: 'Old name', confirmLabel: 'Save' }); });
|
|
486
|
+
const s = await page.evaluate(() => {
|
|
487
|
+
const d = document.querySelector('dialog.hall-dialog');
|
|
488
|
+
const f = d.querySelector('input.hall-dialog__field');
|
|
489
|
+
const lab = document.querySelector(`label[for="${f.id}"]`);
|
|
490
|
+
return {
|
|
491
|
+
modal: d.matches(':modal'), title: document.getElementById(d.getAttribute('aria-labelledby')).textContent,
|
|
492
|
+
label: lab && lab.textContent, focused: document.activeElement === f,
|
|
493
|
+
selected: f.value.slice(f.selectionStart, f.selectionEnd), buttons: [...d.querySelectorAll('.hall-dialog__actions button')].map((b) => b.textContent),
|
|
494
|
+
width: Math.round(f.getBoundingClientRect().width), panel: Math.round(d.querySelector('.hall-dialog__body').getBoundingClientRect().width),
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
assert.equal(s.modal, true);
|
|
498
|
+
assert.equal(s.title, 'Name this card');
|
|
499
|
+
assert.equal(s.label, 'Shown on your sign-in list', 'the field is labelled');
|
|
500
|
+
assert.equal(s.focused, true, 'the field takes focus');
|
|
501
|
+
assert.equal(s.selected, 'Old name', 'its text is selected, so typing replaces it');
|
|
502
|
+
assert.deepEqual(s.buttons, ['Cancel', 'Save']);
|
|
503
|
+
assert.equal(s.width, s.panel, 'the field spans the dialog');
|
|
504
|
+
await page.keyboard.type('New name');
|
|
505
|
+
await page.keyboard.press('Enter');
|
|
506
|
+
assert.equal(await page.evaluate(() => window.__r), 'New name', 'Enter answers with the typed text');
|
|
507
|
+
assert.equal(await page.evaluate(() => document.querySelector('dialog.hall-dialog')), null, 'and the dialog is gone');
|
|
508
|
+
assert.equal(await page.evaluate(() => document.activeElement.id), 'go', 'focus is back on the trigger');
|
|
509
|
+
await ctx.close();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('the prompt: Cancel, Escape and the scrim answer null; an empty answer is still an answer', { skip }, async () => {
|
|
513
|
+
const { ctx, page } = await boot();
|
|
514
|
+
const ask = (o) => page.evaluate((opts) => { window.__r = window.OTBDialog.prompt(opts); }, o);
|
|
515
|
+
await ask({ title: 'Why?' });
|
|
516
|
+
assert.equal(await page.evaluate(() => document.querySelector('input.hall-dialog__field').getAttribute('aria-label')), 'Why?', 'no label: the title names the field');
|
|
517
|
+
await page.click('.hall-dialog__actions .btn--ghost');
|
|
518
|
+
assert.equal(await page.evaluate(() => window.__r), null, 'Cancel is null');
|
|
519
|
+
await ask({ title: 'Why?' });
|
|
520
|
+
await page.keyboard.press('Escape');
|
|
521
|
+
assert.equal(await page.evaluate(() => window.__r), null, 'Escape is null');
|
|
522
|
+
await ask({ title: 'Why?' });
|
|
523
|
+
await page.mouse.click(5, 5);
|
|
524
|
+
assert.equal(await page.evaluate(() => window.__r), null, 'the scrim is null');
|
|
525
|
+
await ask({ title: 'Why?', confirmLabel: 'Go' });
|
|
526
|
+
await page.click('.hall-dialog__actions .btn:not(.btn--ghost)');
|
|
527
|
+
assert.equal(await page.evaluate(() => window.__r), '', 'confirming a blank field answers the empty string, not null');
|
|
528
|
+
await ctx.close();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('the prompt: a required field keeps its confirm off until something is typed', { skip }, async () => {
|
|
532
|
+
const { ctx, page } = await boot();
|
|
533
|
+
await page.evaluate(() => { window.__r = window.OTBDialog.prompt({ title: 'New name for this view', required: true, confirmLabel: 'Rename' }); });
|
|
534
|
+
const ok = '.hall-dialog__actions .btn:not(.btn--ghost)';
|
|
535
|
+
assert.equal(await page.$eval(ok, (b) => b.disabled), true, 'blank: the confirm is off');
|
|
536
|
+
await page.keyboard.press('Enter');
|
|
537
|
+
assert.ok(await page.evaluate(() => !!document.querySelector('dialog.hall-dialog')), 'Enter on a blank required field does not answer');
|
|
538
|
+
await page.keyboard.type(' ');
|
|
539
|
+
assert.equal(await page.$eval(ok, (b) => b.disabled), true, 'spaces are still blank');
|
|
540
|
+
await page.keyboard.type('Mine');
|
|
541
|
+
assert.equal(await page.$eval(ok, (b) => b.disabled), false, 'typed: the confirm is on');
|
|
542
|
+
await page.click(ok);
|
|
543
|
+
assert.equal(await page.evaluate(() => window.__r), ' Mine', 'the text is handed back exactly as typed');
|
|
544
|
+
await ctx.close();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('the prompt\'s title, body and label are text, never markup', { skip }, async () => {
|
|
548
|
+
const { ctx, page } = await boot();
|
|
549
|
+
await page.evaluate(() => { window.OTBDialog.prompt({ title: '<b>t</b>', body: '<img src=x onerror="window.__x=1">', label: '<i>l</i>' }); });
|
|
550
|
+
const s = await page.evaluate(() => ({ img: !!document.querySelector('.hall-dialog img'), i: !!document.querySelector('.hall-dialog i'), b: !!document.querySelector('.hall-dialog b'), x: window.__x }));
|
|
551
|
+
assert.deepEqual(s, { img: false, i: false, b: false, x: undefined });
|
|
552
|
+
await ctx.close();
|
|
553
|
+
});
|
|
@@ -639,7 +639,14 @@ try {
|
|
|
639
639
|
assert.match(CSS, /\.st-light \{[^}]*background: var\(--st-pane\)/, 'the panes are lit by night and unlit by day through one token');
|
|
640
640
|
assert.match(CSS, /body\[data-sky="day"\] \.st-cloud \{ opacity: \.9; \}/);
|
|
641
641
|
assert.match(CSS, /body\[data-sky="day"\] \.st-sunpatch \{ opacity: \.7; \}/);
|
|
642
|
-
assert.match(CSS, /body\[data-sky="day"\] \.st-star,\s*body\[data-sky="day"\] \.st-glow \{ opacity: 0; \}/, 'no stars, and the lamp is off');
|
|
642
|
+
assert.match(CSS, /body\[data-sky="day"\] \.st-star,\s*html:not\(\[data-theme="dark"\]\) body\[data-sky="day"\] \.st-glow \{ opacity: 0; \}/, 'no stars, and the lamp is off');
|
|
643
|
+
// task 1005137: the day room is a light-theme room. Under the hall's dark theme a cream room
|
|
644
|
+
// sat beside the black rail, so EVERY day rule is scoped off dark and dark keeps the night room.
|
|
645
|
+
const daySelectors = CSS.replace(/\/\*[\s\S]*?\*\//g, '').split(/[{}]/).filter((_, i) => i % 2 === 0)
|
|
646
|
+
.flatMap((sel) => sel.split(',')).map((x) => x.trim()).filter((x) => x.includes('[data-sky="day"]'));
|
|
647
|
+
assert.ok(daySelectors.length >= 10, `the day rules are found (${daySelectors.length})`);
|
|
648
|
+
for (const sel of daySelectors) assert.ok(sel.startsWith('html:not([data-theme="dark"]) '), `${sel} is scoped off the dark theme`);
|
|
649
|
+
assert.ok(!/st-eyebrow/.test(CSS + HTML), 'no mono eyebrow above the greeting (DESIGN.md bans a label over the h1)');
|
|
643
650
|
assert.match(HTML, /class="st-cloud st-cloud--1"/);
|
|
644
651
|
assert.match(HTML, /class="st-sunpatch"/);
|
|
645
652
|
const anims = [...CSS.matchAll(/animation:\s*[^;]+;|transition:\s*[^;]+;/g)].map((m) => m.index);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// tests/hall_topbar_320.mjs — the hall's top bar fits a 320px phone, and the components
|
|
2
|
+
// specimen sits above the plate veil (task 1005137, goal 1000138).
|
|
3
|
+
//
|
|
4
|
+
// WHY. A visual check of the design system found every hall page 340px wide at 320: the menu, the
|
|
5
|
+
// Update pill, the search, the mode switch and the three toggles did not fit, so the theme and
|
|
6
|
+
// full-screen icons were cut off at the edge. And modules/hall-ui/specimens/components.html read as
|
|
7
|
+
// near-black in dark mode, because style.css's body::after veil (--plate-veil) covered the page: a
|
|
8
|
+
// hall page lifts its content over the plate (.app-body, z-index 1) and the specimen did not.
|
|
9
|
+
// Both are layout, so both are measured in a real browser on the REAL markup and the REAL sheets:
|
|
10
|
+
// the shell's own top bar on the Tasks page with the widest set of buttons a page can carry (the
|
|
11
|
+
// Update pill, the search, the full-screen switch), and the specimen as the kit renders it.
|
|
12
|
+
// Skips when the machine has no system Edge or Chrome, as welcome_boot_browser does.
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { test, after } from 'node:test';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
23
|
+
const SPEC = path.join(ROOT, 'modules', 'hall-ui', 'specimens');
|
|
24
|
+
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
25
|
+
|
|
26
|
+
let browser = null;
|
|
27
|
+
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
28
|
+
after(() => browser && browser.close());
|
|
29
|
+
const skip = !browser && 'no system Edge or Chrome';
|
|
30
|
+
|
|
31
|
+
const ORIGIN = 'http://hall.test';
|
|
32
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.html': 'text/html' };
|
|
33
|
+
|
|
34
|
+
// The Tasks page's real markup, its scripts swapped for the shell's own, on a page that opts into
|
|
35
|
+
// the full-screen switch (Studio and Thinking do), so the bar carries every button it can.
|
|
36
|
+
const WORK = fs.readFileSync(path.join(HALL, 'work.html'), 'utf8')
|
|
37
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '')
|
|
38
|
+
.replace(/\{\{[^}]*\}\}/g, '')
|
|
39
|
+
.replace('<body data-page="work"', '<body data-page="work" data-fullscreen-switch');
|
|
40
|
+
const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
|
|
41
|
+
const SCRIPTS = ['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'shell.js'].map((f) => `<script src="/builders/${f}"></script>`).join('');
|
|
42
|
+
const PAGE = WORK.replace('</body>', STUB + SCRIPTS + '</body>');
|
|
43
|
+
|
|
44
|
+
async function context(width, scheme) {
|
|
45
|
+
const ctx = await browser.newContext({ viewport: { width, height: 700 }, colorScheme: scheme });
|
|
46
|
+
await ctx.route(ORIGIN + '/**', (route) => {
|
|
47
|
+
const p = new URL(route.request().url()).pathname;
|
|
48
|
+
if (p === '/work') return route.fulfill({ contentType: 'text/html', body: PAGE });
|
|
49
|
+
let f = null;
|
|
50
|
+
if (p.startsWith('/builders/')) f = path.join(HALL, p.slice('/builders/'.length));
|
|
51
|
+
else if (p.startsWith('/modules/hall-ui/public/')) f = path.join(HALL, p.slice('/modules/hall-ui/public/'.length));
|
|
52
|
+
else if (p.startsWith('/modules/ui-design/public/')) f = path.join(ROOT, 'modules', 'ui-design', 'public', p.slice('/modules/ui-design/public/'.length));
|
|
53
|
+
else if (p.startsWith('/specimens/')) f = path.join(SPEC, p.slice('/specimens/'.length));
|
|
54
|
+
if (f && TYPES[path.extname(f)] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[path.extname(f)], body: fs.readFileSync(f) });
|
|
55
|
+
return route.fulfill({ status: 404, body: '' });
|
|
56
|
+
});
|
|
57
|
+
return ctx;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The bar as update-pill.js and palette.js leave it: the Update pill first in the side, the search
|
|
61
|
+
// shown. Then every button's box, and how wide the document is.
|
|
62
|
+
async function measureBar(width, scheme) {
|
|
63
|
+
const ctx = await context(width, scheme);
|
|
64
|
+
const page = await ctx.newPage();
|
|
65
|
+
await page.goto(ORIGIN + '/work');
|
|
66
|
+
await page.waitForSelector('#app-topbar .topbar-side #theme-toggle');
|
|
67
|
+
return page.evaluate(() => {
|
|
68
|
+
const side = document.querySelector('#app-topbar .topbar-side');
|
|
69
|
+
if (!document.getElementById('upd-pill')) {
|
|
70
|
+
const b = document.createElement('button');
|
|
71
|
+
b.type = 'button'; b.id = 'upd-pill'; b.className = 'upd-pill'; b.setAttribute('data-face', 'ready');
|
|
72
|
+
b.innerHTML = '<span class="upd-pill__dot" aria-hidden="true"></span><span class="upd-pill__label">Update</span>';
|
|
73
|
+
side.insertBefore(b, side.firstChild);
|
|
74
|
+
}
|
|
75
|
+
const jump = document.getElementById('topbar-jump'); if (jump) jump.hidden = false;
|
|
76
|
+
const fs = document.getElementById('fullscreen-toggle'); if (fs) fs.hidden = false;
|
|
77
|
+
const buttons = [...document.querySelectorAll('#app-topbar button')]
|
|
78
|
+
.filter((b) => getComputedStyle(b).display !== 'none' && b.getClientRects().length)
|
|
79
|
+
.map((b) => { const r = b.getBoundingClientRect(); return { id: b.id || b.className, left: r.left, right: r.right, w: r.width, h: r.height }; });
|
|
80
|
+
const bar = document.getElementById('app-topbar').getBoundingClientRect();
|
|
81
|
+
return { scrollWidth: document.documentElement.scrollWidth, bar: { left: bar.left, right: bar.right }, buttons };
|
|
82
|
+
}).finally(() => ctx.close());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const scheme of ['light', 'dark']) {
|
|
86
|
+
test(`at 320px the top bar fits, every button inside it and at least 24px (${scheme})`, { skip }, async () => {
|
|
87
|
+
const m = await measureBar(320, scheme);
|
|
88
|
+
const ids = m.buttons.map((b) => b.id);
|
|
89
|
+
for (const want of ['upd-pill', 'topbar-jump', 'zoom-toggle', 'theme-toggle', 'fullscreen-toggle']) {
|
|
90
|
+
assert.ok(ids.includes(want), `${want} is on the bar (got ${ids.join(', ')})`);
|
|
91
|
+
}
|
|
92
|
+
assert.ok(m.scrollWidth <= 320, `the page is ${m.scrollWidth}px wide at 320 (the top bar spills)`);
|
|
93
|
+
for (const b of m.buttons) {
|
|
94
|
+
assert.ok(b.left >= 0 && b.right <= 320, `${b.id} sits at ${Math.round(b.left)}..${Math.round(b.right)}, outside 0..320`);
|
|
95
|
+
assert.ok(b.w >= 24 && b.h >= 24, `${b.id} is ${Math.round(b.w)}x${Math.round(b.h)}, under the 24px floor`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test('the top bar search and the mode switch are one shape: a pill with the same hairline', { skip }, async () => {
|
|
101
|
+
const ctx = await context(1440, 'dark');
|
|
102
|
+
const page = await ctx.newPage();
|
|
103
|
+
await page.goto(ORIGIN + '/work');
|
|
104
|
+
await page.waitForSelector('#app-topbar #topbar-jump', { state: 'attached' });
|
|
105
|
+
const s = await page.evaluate(() => {
|
|
106
|
+
const css = (el) => getComputedStyle(el);
|
|
107
|
+
const jump = document.getElementById('topbar-jump');
|
|
108
|
+
jump.hidden = false; // palette.js shows it; the shape is the sheet's either way
|
|
109
|
+
const sw = document.getElementById('craft-switch');
|
|
110
|
+
const root = css(document.documentElement);
|
|
111
|
+
return {
|
|
112
|
+
jumpRadius: css(jump).borderTopLeftRadius,
|
|
113
|
+
switchRadius: css(sw).borderTopLeftRadius,
|
|
114
|
+
jumpEdge: css(jump).borderTopColor,
|
|
115
|
+
switchShadow: css(sw).boxShadow,
|
|
116
|
+
pill: root.getPropertyValue('--ds-radius-pill').trim(),
|
|
117
|
+
};
|
|
118
|
+
}).finally(() => ctx.close());
|
|
119
|
+
assert.equal(s.jumpRadius, s.pill, 'the search is a full pill');
|
|
120
|
+
assert.equal(s.switchRadius, s.pill, 'the mode switch is a full pill');
|
|
121
|
+
assert.ok(s.switchShadow.startsWith(s.jumpEdge), `the two share one hairline colour (${s.jumpEdge} vs ${s.switchShadow})`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the components specimen sits above the plate veil in dark (the hall layering)', { skip }, async () => {
|
|
125
|
+
const ctx = await context(1440, 'dark');
|
|
126
|
+
const page = await ctx.newPage();
|
|
127
|
+
await page.goto(ORIGIN + '/specimens/components.html');
|
|
128
|
+
await page.waitForSelector('.spec-main');
|
|
129
|
+
const r = await page.evaluate(() => {
|
|
130
|
+
const veil = getComputedStyle(document.body, '::after');
|
|
131
|
+
const blocks = [...document.body.children].filter((el) => !/^(SCRIPT|TEMPLATE)$/.test(el.tagName) && getComputedStyle(el).display !== 'none')
|
|
132
|
+
.map((el) => { const cs = getComputedStyle(el); return { el: el.tagName.toLowerCase() + '.' + el.className, position: cs.position, z: cs.zIndex }; });
|
|
133
|
+
return { veilImage: veil.backgroundImage, veilZ: veil.zIndex, veilPosition: veil.position, blocks };
|
|
134
|
+
}).finally(() => ctx.close());
|
|
135
|
+
// The veil is really there in dark — otherwise this test proves nothing.
|
|
136
|
+
assert.notEqual(r.veilImage, 'none', 'dark paints the plate veil');
|
|
137
|
+
assert.equal(r.veilPosition, 'fixed');
|
|
138
|
+
const veilZ = Number(r.veilZ) || 0;
|
|
139
|
+
assert.ok(r.blocks.length >= 2, 'the specimen has its head and its main');
|
|
140
|
+
for (const b of r.blocks) {
|
|
141
|
+
if (b.position === 'fixed' || b.position === 'absolute') continue;
|
|
142
|
+
assert.notEqual(b.position, 'static', `${b.el} is static, so the fixed veil paints over it`);
|
|
143
|
+
assert.ok(Number(b.z) > veilZ, `${b.el} has z-index ${b.z}, not above the veil's ${veilZ}`);
|
|
144
|
+
}
|
|
145
|
+
});
|