@bongos/core 1.21.164 → 1.21.166
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 +34 -29
- package/docs/copy-inventory.md +91 -91
- package/docs/copy-registry.json +126 -116
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +304 -305
- package/modules/builder-settings/builder-needs.js +5 -0
- package/modules/hall-ui/public/builders.js +3 -17
- package/modules/hall-ui/public/craft-mode.js +32 -158
- package/modules/hall-ui/public/craft-review.css +7 -120
- package/modules/hall-ui/public/hall-render.js +8 -24
- package/modules/hall-ui/public/index.html +3 -4
- package/modules/hall-ui/public/page-notes.css +64 -1
- package/modules/hall-ui/public/page-notes.js +96 -11
- package/modules/hall-ui/public/settings.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/bongos/routes/me.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/craft_review_mode.mjs +21 -104
- package/tests/hall_board_world.mjs +18 -39
- package/tests/home_attention_cards.mjs +6 -2
- package/tests/page_notes_panel.mjs +161 -27
- package/tests/settings_art_key_absent.mjs +137 -0
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
var MAX_IMAGES = 8;
|
|
20
20
|
var TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
21
21
|
|
|
22
|
-
var sheet = null, returnFocus = null, state = null, saveTimer = null, pollTimer = null;
|
|
22
|
+
var sheet = null, returnFocus = null, state = null, saveTimer = null, pollTimer = null, docked = false;
|
|
23
23
|
|
|
24
24
|
function esc(s) {
|
|
25
25
|
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
@@ -101,11 +101,45 @@
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ---------- the sheet ----------
|
|
104
|
+
// The tab wears the Review button's own palette glyph (craft-mode.js ICONS
|
|
105
|
+
// artistry), drawn here too because the public site has no craft-mode.js.
|
|
106
|
+
var PALETTE_SVG = '<svg class="pn__tab-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
|
|
107
|
+
'<path d="M8 2a6 6 0 1 0 0 12c.9 0 1.4-.6 1.4-1.3 0-.8-.6-1-.6-1.7 0-.6.5-1 1.2-1h1.6A2.4 2.4 0 0 0 14 7.6C14 4.5 11.3 2 8 2Z"/><path d="M5 6.2h.01M7.6 4.6h.01M10.4 5.2h.01M4.6 9h.01" stroke-width="2.2"/></svg>';
|
|
108
|
+
// The Review mode row: which experience the hall wears while the artist
|
|
109
|
+
// reviews it (craft-mode.js review mode). Hall only: the public site has no
|
|
110
|
+
// modes, so it draws nothing there.
|
|
111
|
+
function craft() { var c = window.OTBCraftMode; return c && c.experiences && c.review ? c : null; }
|
|
112
|
+
function modesHtml() {
|
|
113
|
+
var c = craft();
|
|
114
|
+
if (!c) return '';
|
|
115
|
+
var on = c.reviewing();
|
|
116
|
+
return '<div class="pn__modes" role="group" aria-label="Review mode">' +
|
|
117
|
+
'<span class="pn__modes-h">Review mode</span>' +
|
|
118
|
+
c.experiences().map(function (x) {
|
|
119
|
+
return '<button class="pn__mode" type="button" data-pn-mode="' + esc(x.id) + '" aria-pressed="' + (on === x.id ? 'true' : 'false') + '">' +
|
|
120
|
+
c.iconHtml(x.id, 'pn__mode-icon') + '<span>' + esc(x.short) + '</span></button>';
|
|
121
|
+
}).join('') +
|
|
122
|
+
(on ? '<button class="pn__mode pn__mode--stop" type="button" data-pn-mode="">Stop reviewing</button>' : '') +
|
|
123
|
+
'</div>';
|
|
124
|
+
}
|
|
125
|
+
function paintModes() {
|
|
126
|
+
var row = sheet && sheet.querySelector('#pn-modes');
|
|
127
|
+
if (row) row.innerHTML = modesHtml();
|
|
128
|
+
}
|
|
129
|
+
// Two shapes. Docked (dock(), the hall's artist mode, task 1004771): a
|
|
130
|
+
// non-modal region at the right edge behind a tab. Opened (open(), e.g. the
|
|
131
|
+
// public site's Artist review button): the modal sheet over a scrim it was.
|
|
104
132
|
function shellHtml(p) {
|
|
105
|
-
return
|
|
133
|
+
return (docked
|
|
134
|
+
? '<button class="pn__tab" type="button" data-pn-tab aria-expanded="true" aria-controls="pn-panel" aria-label="Hide the review panel">' +
|
|
135
|
+
PALETTE_SVG +
|
|
136
|
+
'<svg class="pn__tab-chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg></button>' +
|
|
137
|
+
'<div class="pn__panel" id="pn-panel" role="region" aria-labelledby="pn-title">'
|
|
138
|
+
: '<div class="pn__panel" role="dialog" aria-modal="true" aria-labelledby="pn-title">') +
|
|
106
139
|
'<div class="pn__head">' +
|
|
107
140
|
'<p class="pn__eyebrow">Artist review · ' + esc(p.title) + '</p>' +
|
|
108
141
|
'<h2 class="pn__title" id="pn-title">What should this page be?</h2>' +
|
|
142
|
+
'<div id="pn-modes">' + modesHtml() + '</div>' +
|
|
109
143
|
'<button class="pn__close" type="button" data-pn-close aria-label="Close">' +
|
|
110
144
|
'<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M5 5l8 8M13 5l-8 8"/></svg></button>' +
|
|
111
145
|
'</div>' +
|
|
@@ -285,10 +319,18 @@
|
|
|
285
319
|
}
|
|
286
320
|
|
|
287
321
|
function onClick(e) {
|
|
288
|
-
if (e.target === sheet) { close(); return; }
|
|
322
|
+
if (!docked && e.target === sheet) { close(); return; }
|
|
289
323
|
var btn = e.target.closest && e.target.closest('button');
|
|
290
324
|
if (!btn) return;
|
|
291
|
-
if (btn.hasAttribute('data-pn-
|
|
325
|
+
if (btn.hasAttribute('data-pn-tab')) toggle();
|
|
326
|
+
else if (btn.hasAttribute('data-pn-mode')) {
|
|
327
|
+
var c = craft(), id = btn.getAttribute('data-pn-mode');
|
|
328
|
+
if (c) { if (id) c.review(id); else c.stopReview(); }
|
|
329
|
+
paintModes();
|
|
330
|
+
}
|
|
331
|
+
// Docked, the X tucks the panel away like Escape: removing it would take
|
|
332
|
+
// the tab, the reviewer's only way back in, with it.
|
|
333
|
+
else if (btn.hasAttribute('data-pn-close')) { if (docked) toggle(true); else close(); }
|
|
292
334
|
else if (btn.hasAttribute('data-pn-submit')) submit();
|
|
293
335
|
else if (btn.hasAttribute('data-pn-note')) showNote(btn.getAttribute('data-pn-note'));
|
|
294
336
|
else if (btn.hasAttribute('data-pn-back') || btn.hasAttribute('data-pn-new')) {
|
|
@@ -298,6 +340,13 @@
|
|
|
298
340
|
}
|
|
299
341
|
function onKey(e) {
|
|
300
342
|
if (!sheet) return;
|
|
343
|
+
// Docked, the panel is not modal: the page behind it stays scrollable and
|
|
344
|
+
// reachable, and Escape inside it tucks it away behind its tab, draft kept.
|
|
345
|
+
if (docked) {
|
|
346
|
+
if (e.key === 'Escape' && sheet.contains(document.activeElement)) { e.preventDefault(); toggle(true); }
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Opened, it is the modal sheet: Escape closes it, Tab stays inside it.
|
|
301
350
|
if (e.key === 'Escape') { e.preventDefault(); close(); return; }
|
|
302
351
|
if (e.key !== 'Tab') return;
|
|
303
352
|
var f = Array.prototype.slice.call(sheet.querySelectorAll('button, textarea, input, a[href]')).filter(function (x) { return !x.disabled; });
|
|
@@ -305,20 +354,46 @@
|
|
|
305
354
|
if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
|
|
306
355
|
else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
|
|
307
356
|
}
|
|
308
|
-
|
|
357
|
+
// Slide the panel off the right edge, leaving only its tab, or bring it back.
|
|
358
|
+
// `quiet` (docking) moves no focus. A docked panel opens its draft only
|
|
359
|
+
// the first time it is shown, so artist mode costs no request until then.
|
|
360
|
+
function toggle(collapse, quiet) {
|
|
361
|
+
if (!sheet) return;
|
|
362
|
+
var on = typeof collapse === 'boolean' ? collapse : !sheet.classList.contains('pn--collapsed');
|
|
363
|
+
sheet.classList.toggle('pn--collapsed', on);
|
|
364
|
+
var tab = sheet.querySelector('[data-pn-tab]');
|
|
365
|
+
if (tab) {
|
|
366
|
+
tab.setAttribute('aria-expanded', on ? 'false' : 'true');
|
|
367
|
+
tab.setAttribute('aria-label', on ? 'Show the review panel' : 'Hide the review panel');
|
|
368
|
+
if (on && !quiet) tab.focus();
|
|
369
|
+
}
|
|
370
|
+
if (on) save();
|
|
371
|
+
else if (state.pending) { state.pending = false; openDraft(); }
|
|
372
|
+
}
|
|
373
|
+
// `dock` mounts the docked shape quietly (no focus moved); otherwise the modal sheet.
|
|
374
|
+
function mountSheet(dock) {
|
|
309
375
|
if (sheet) return false;
|
|
310
|
-
|
|
376
|
+
docked = !!dock;
|
|
377
|
+
returnFocus = docked ? null : document.activeElement;
|
|
311
378
|
state = { note: null, images: [], list: [] };
|
|
312
379
|
sheet = document.createElement('div');
|
|
313
|
-
sheet.className = 'pn';
|
|
380
|
+
sheet.className = docked ? 'pn pn--dock' : 'pn';
|
|
314
381
|
sheet.id = 'page-notes';
|
|
315
382
|
sheet.innerHTML = shellHtml(page());
|
|
316
383
|
sheet.addEventListener('click', onClick);
|
|
384
|
+
if (docked) {
|
|
385
|
+
// A mouse peeks (owner, 2026-10-23): over the tab the panel opens, and the
|
|
386
|
+
// moment the pointer leaves the panel it tucks away again, draft kept.
|
|
387
|
+
// Touch and keyboard keep the tab's click, which a hover cannot replace.
|
|
388
|
+
sheet.querySelector('[data-pn-tab]').addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') toggle(false, true); });
|
|
389
|
+
sheet.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') toggle(true, true); });
|
|
390
|
+
} else {
|
|
391
|
+
document.documentElement.classList.add('pn-open');
|
|
392
|
+
}
|
|
317
393
|
document.addEventListener('keydown', onKey, true);
|
|
318
394
|
document.body.appendChild(sheet);
|
|
319
|
-
document.documentElement.classList.add('pn-open');
|
|
320
395
|
var c = sheet.querySelector('[data-pn-close]');
|
|
321
|
-
if (c) c.focus();
|
|
396
|
+
if (c && !docked) c.focus();
|
|
322
397
|
return true;
|
|
323
398
|
}
|
|
324
399
|
function close() {
|
|
@@ -328,14 +403,24 @@
|
|
|
328
403
|
document.removeEventListener('keydown', onKey, true);
|
|
329
404
|
if (sheet.parentNode) sheet.parentNode.removeChild(sheet);
|
|
330
405
|
sheet = null;
|
|
406
|
+
docked = false;
|
|
331
407
|
document.documentElement.classList.remove('pn-open');
|
|
332
408
|
if (returnFocus && returnFocus.focus) { try { returnFocus.focus(); } catch (_) { /* gone */ } }
|
|
333
409
|
returnFocus = null;
|
|
334
410
|
}
|
|
335
411
|
|
|
336
412
|
window.OTBPageNotes = {
|
|
337
|
-
open: function () { if (mountSheet()) openDraft(); },
|
|
338
|
-
|
|
413
|
+
open: function () { if (mountSheet()) openDraft(); else if (docked) toggle(false); },
|
|
414
|
+
// Artist mode (craft-mode.js, task 1004771): the panel waits at the right
|
|
415
|
+
// edge, tucked behind its tab; already there, it repaints its mode row.
|
|
416
|
+
dock: function () {
|
|
417
|
+
if (mountSheet(true)) { state.pending = true; toggle(true, true); }
|
|
418
|
+
else paintModes();
|
|
419
|
+
},
|
|
420
|
+
openNote: function (id) {
|
|
421
|
+
if (!mountSheet() && docked) { state.pending = false; toggle(false); }
|
|
422
|
+
refreshList().then(function () { return showNote(id); });
|
|
423
|
+
},
|
|
339
424
|
close: close,
|
|
340
425
|
configure: configure,
|
|
341
426
|
ideaHref: ideaHref,
|
|
@@ -1030,6 +1030,10 @@
|
|
|
1030
1030
|
renderArtKeyBanner(artKeyNeedItem(me));
|
|
1031
1031
|
const res = await api.request('GET', `${API}/me/art-key/own`);
|
|
1032
1032
|
if (res.status === 401) { toAuth(); return; }
|
|
1033
|
+
// 404: the art pipeline is not installed, so there is no key to add here —
|
|
1034
|
+
// keep the section (and any banner) hidden rather than show a reload error
|
|
1035
|
+
// (task 1004786).
|
|
1036
|
+
if (res.status === 404) { renderArtKeyBanner(null); return; }
|
|
1033
1037
|
if (!res.ok) throw new Error(`art-key GET → ${res.status}`);
|
|
1034
1038
|
renderArtKeyCard(res.data);
|
|
1035
1039
|
wireArtKeyCard();
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.166",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.166",
|
|
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.21.
|
|
3
|
+
"version": "1.21.166",
|
|
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
|
@@ -10034,5 +10034,21 @@
|
|
|
10034
10034
|
"id": "1004774",
|
|
10035
10035
|
"text": "Home got the owner's reshuffle: a slimmer banner with the version's progress bar under it; your picture and rank at the top right, opening on hover into your standing card with a View full profile link; every Needs-attention s"
|
|
10036
10036
|
}
|
|
10037
|
+
],
|
|
10038
|
+
"1.21.165": [
|
|
10039
|
+
{
|
|
10040
|
+
"id": "1004771",
|
|
10041
|
+
"text": "Artist review is now a small tab on the right edge of every hall page in artist mode. Hover the tab and the review panel slides out; move the cursor back onto the page and it tucks away, keeping your draft. The page stays scro"
|
|
10042
|
+
}
|
|
10043
|
+
],
|
|
10044
|
+
"1.21.166": [
|
|
10045
|
+
{
|
|
10046
|
+
"id": "1004780",
|
|
10047
|
+
"text": "Home's greeting stands on its own: the \"BONGOS-V2 is 83 percent complete. 150 tasks are claimable right now.\" line under \"Good morning\" is gone, because the version bar and the ready-work list beside it already say both."
|
|
10048
|
+
},
|
|
10049
|
+
{
|
|
10050
|
+
"id": "1004786",
|
|
10051
|
+
"text": "Settings no longer shows a broken image-generation key card on projects without the art tools. Before, builders were told their free art coverage had ended and to add a key, but the card could never load, so there was nowhere"
|
|
10052
|
+
}
|
|
10037
10053
|
]
|
|
10038
10054
|
}
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -569,6 +569,9 @@ module.exports = function buildMeRouter() {
|
|
|
569
569
|
rank: builder?.rank,
|
|
570
570
|
ownKeySet: !!(geminiKeyRow && geminiKeyRow.enc),
|
|
571
571
|
sharedKeyConfigured: artSharedKeyConfigured(),
|
|
572
|
+
// task 1004786: the art-pipeline module provides this port when enabled;
|
|
573
|
+
// without it there are no key routes, so the need is not raised.
|
|
574
|
+
artPipeline: seams.hasProvider('art.sharedKeyConfigured'),
|
|
572
575
|
// founding grace (task 1002739 / direction §5.6): a newborn instance's
|
|
573
576
|
// founder gets a welcome roster, not a coverage nag. The module owns the
|
|
574
577
|
// policy (window length, copy); we pass the raw facts.
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.166'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// tests/craft_review_mode.mjs — artist review mode (task 1004686, goal 1000095).
|
|
2
2
|
//
|
|
3
|
-
// Owner ask, 2026-10-09:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Owner ask, 2026-10-09: the artist picks one of the four experiences (Ideator,
|
|
4
|
+
// Governing, Building, Artistry) and reviews the hall in that mode's view, with
|
|
5
|
+
// graffiti across the top bar's empty space while it lasts. Since task 1004771
|
|
6
|
+
// the choice lives in the docked review panel's own Review mode row
|
|
7
|
+
// (page-notes.js; tests/page_notes_panel.mjs runs that row), not a top-bar menu. These cases run the REAL
|
|
7
8
|
// craft-mode.js (and, when it is loaded, the real craft-review.js) over a small
|
|
8
9
|
// DOM, and read the real craft-review.css for the motion rules.
|
|
9
10
|
import assert from 'node:assert/strict';
|
|
@@ -83,7 +84,7 @@ function boot({ session = {}, store = {}, narrow = false } = {}) {
|
|
|
83
84
|
head.onAppend = (tag) => {
|
|
84
85
|
loaded.push(tag.src);
|
|
85
86
|
if (/craft-review\.js/.test(tag.src)) vm.runInContext(REVIEW, sandbox, { filename: 'craft-review.js' });
|
|
86
|
-
else window.OTBPageNotes = { open: () => opened.push('open'), openNote: () => {} };
|
|
87
|
+
else window.OTBPageNotes = { open: () => opened.push('open'), openNote: () => {}, dock: () => opened.push('dock'), close: () => opened.push('close') };
|
|
87
88
|
tag.onload();
|
|
88
89
|
};
|
|
89
90
|
vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
|
|
@@ -93,107 +94,43 @@ function boot({ session = {}, store = {}, narrow = false } = {}) {
|
|
|
93
94
|
return {
|
|
94
95
|
craft: window.OTBCraftMode, root, topbar, side, sw, title, loaded, opened, session, byId, docListeners,
|
|
95
96
|
tools: () => all(side).filter((n) => n.hasAttribute('data-craft-tool')).map((n) => n.id.replace('craft-tool-', '')),
|
|
96
|
-
menu: () => byId('craft-review-menu'),
|
|
97
|
-
menuOpen: () => !!byId('craft-review-menu') && !byId('craft-review-menu').hasAttribute('hidden'),
|
|
98
|
-
// A click on a menu item: the menu's own listener reads the button off the target.
|
|
99
|
-
pick: (attrs) => byId('craft-review-menu').fire('click', { target: { closest: () => ({ getAttribute: (k) => (k in attrs ? attrs[k] : null) }) } }),
|
|
100
97
|
};
|
|
101
98
|
}
|
|
102
99
|
const artist = () => ({ 'hall-craft': 'artistry', 'hall-craft-asked': '1' });
|
|
103
100
|
|
|
104
|
-
test('
|
|
101
|
+
test('NO TOP-BAR REVIEW BUTTON: in Artistry the bar holds Edit words alone, and no review-mode menu', async () => {
|
|
105
102
|
const p = boot({ session: artist() });
|
|
106
|
-
|
|
107
|
-
assert.
|
|
108
|
-
assert.equal(p.byId('craft-
|
|
109
|
-
|
|
110
|
-
assert.
|
|
111
|
-
assert.equal(more.getAttribute('aria-haspopup'), 'menu');
|
|
112
|
-
assert.equal(more.getAttribute('aria-expanded'), 'false');
|
|
113
|
-
assert.equal(more.getAttribute('aria-controls'), 'craft-review-menu');
|
|
114
|
-
assert.equal(p.menu().getAttribute('role'), 'menu');
|
|
115
|
-
assert.ok(p.menu().hasAttribute('hidden'), 'the menu starts closed');
|
|
103
|
+
await flush();
|
|
104
|
+
assert.deepEqual(p.tools(), ['paintbrush'], 'the review panel docks at the edge instead (task 1004771)');
|
|
105
|
+
assert.equal(p.byId('craft-review'), null, 'no split pill');
|
|
106
|
+
assert.equal(p.byId('craft-review-menu'), null, 'no menu');
|
|
107
|
+
assert.deepEqual(p.opened, ['dock']);
|
|
116
108
|
assert.equal(p.byId('craft-graffiti'), null, 'no wall until a review starts');
|
|
117
109
|
assert.equal(p.root.getAttribute('data-craft-review'), null);
|
|
118
|
-
|
|
119
|
-
p.craft.choose(m);
|
|
120
|
-
assert.equal(p.byId('craft-review'), null, `no Review pill in ${m}`);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('THE MENU: write feedback first, then the four experiences in the owner\'s order and words', () => {
|
|
125
|
-
const p = boot({ session: artist() });
|
|
126
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
127
|
-
assert.ok(p.menuOpen(), 'the chevron opens it');
|
|
128
|
-
assert.equal(p.byId('craft-review-more').getAttribute('aria-expanded'), 'true');
|
|
129
|
-
const html = p.menu().innerHTML;
|
|
130
|
-
const labels = [...html.matchAll(/<span>([^<]+)<\/span>/g)].map((m) => m[1]);
|
|
131
|
-
assert.deepEqual(labels, ['Write feedback on this page', 'Ideator experience', 'Governing experience', 'Building experience', 'Artistry experience']);
|
|
132
|
-
assert.deepEqual([...html.matchAll(/data-review-pick="([a-z]+)"/g)].map((m) => m[1]), ['ideating', 'governing', 'building', 'artistry']);
|
|
133
|
-
assert.equal((html.match(/role="menuitemradio" aria-checked="true"/g) || []).length, 0, 'nothing is checked before a review');
|
|
134
|
-
assert.doesNotMatch(html, /Stop reviewing/, 'no way out of a review that has not started');
|
|
135
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
136
|
-
assert.ok(!p.menuOpen(), 'the chevron closes it again');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('HOVER opens it for a mouse, never for a touch, and leaving closes it after a beat', async () => {
|
|
140
|
-
const p = boot({ session: artist() });
|
|
141
|
-
const pill = p.byId('craft-review');
|
|
142
|
-
pill.fire('pointerenter', { pointerType: 'touch' });
|
|
143
|
-
assert.ok(!p.menuOpen(), 'a touch is not a hover');
|
|
144
|
-
pill.fire('pointerenter', { pointerType: 'mouse' });
|
|
145
|
-
assert.ok(p.menuOpen(), 'a mouse over the pill opens the menu');
|
|
146
|
-
pill.fire('pointerleave', { pointerType: 'mouse' });
|
|
147
|
-
assert.ok(p.menuOpen(), 'it stays a beat, so the pointer can reach the menu');
|
|
148
|
-
pill.fire('pointerenter', { pointerType: 'mouse' });
|
|
149
|
-
await new Promise((r) => setTimeout(r, 260));
|
|
150
|
-
assert.ok(p.menuOpen(), 'coming back in cancels the close');
|
|
151
|
-
pill.fire('pointerleave', { pointerType: 'mouse' });
|
|
152
|
-
await new Promise((r) => setTimeout(r, 260));
|
|
153
|
-
assert.ok(!p.menuOpen(), 'and leaving for good closes it');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('CLICKING Review still opens the feedback panel; on a phone a tap opens the menu instead', async () => {
|
|
157
|
-
const p = boot({ session: artist() });
|
|
158
|
-
p.byId('craft-tool-palette').click({ detail: 1 });
|
|
159
|
-
await flush();
|
|
160
|
-
assert.deepEqual(p.opened, ['open'], 'the feedback doc opens, as before');
|
|
161
|
-
assert.ok(!p.menuOpen());
|
|
162
|
-
const phone = boot({ session: artist(), narrow: true });
|
|
163
|
-
phone.byId('craft-tool-palette').click({ detail: 1 });
|
|
164
|
-
await flush();
|
|
165
|
-
assert.deepEqual(phone.opened, [], 'no panel on the tap');
|
|
166
|
-
assert.ok(phone.menuOpen(), 'the menu instead (a phone has no hover)');
|
|
167
|
-
phone.pick({ 'data-review-act': 'write' });
|
|
168
|
-
await flush();
|
|
169
|
-
assert.deepEqual(phone.opened, ['open'], 'and its first line opens the feedback doc');
|
|
110
|
+
assert.deepEqual(JSON.parse(JSON.stringify(p.craft.experiences().map((x) => x.label))), ['Ideator experience', 'Governing experience', 'Building experience', 'Artistry experience'], 'the four, in the owner\'s order and words, for the review panel row');
|
|
170
111
|
});
|
|
171
112
|
|
|
172
113
|
test('PICKING an experience: the hall wears that mode, the artist keeps the Review tools, and the wall goes up', async () => {
|
|
173
114
|
const p = boot({ session: artist() });
|
|
174
|
-
p.
|
|
175
|
-
p.pick({ 'data-review-pick': 'governing' });
|
|
176
|
-
assert.ok(!p.menuOpen(), 'the menu closes on a pick');
|
|
115
|
+
p.craft.review('governing');
|
|
177
116
|
assert.equal(p.root.dataset.craft, 'governing', 'the hall wears Governing: its tabs, look and tallies follow');
|
|
178
117
|
assert.equal(p.root.getAttribute('data-craft-review'), 'governing');
|
|
179
118
|
assert.equal(p.craft.current(), 'governing');
|
|
180
119
|
assert.equal(p.craft.reviewing(), 'governing');
|
|
181
120
|
assert.equal(p.session['hall-craft'], 'artistry', 'the artist\'s own mode stays Artistry');
|
|
182
121
|
assert.equal(p.session['hall-craft-review'], 'governing', 'the review lasts the session');
|
|
183
|
-
assert.deepEqual(p.tools(), ['
|
|
184
|
-
|
|
122
|
+
assert.deepEqual(p.tools(), ['paintbrush'], 'Edit words stays in the bar');
|
|
123
|
+
await flush();
|
|
124
|
+
assert.ok(p.opened.filter((x) => x === 'dock').length >= 2, 'the docked panel is told again, so its mode row repaints');
|
|
185
125
|
const wall = p.byId('craft-graffiti');
|
|
186
126
|
assert.ok(wall, 'the graffiti wall is up');
|
|
187
127
|
assert.equal(wall.parentNode, p.topbar, 'in the top bar');
|
|
188
128
|
assert.equal(wall.nextSibling, p.side, 'in the empty space before the controls');
|
|
189
129
|
assert.equal(wall.getAttribute('aria-hidden'), 'true', 'decorative');
|
|
190
130
|
await flush();
|
|
191
|
-
assert.
|
|
131
|
+
assert.ok(p.loaded.includes('/builders/craft-review.js?v=2026-10-09-test'), 'the wall\'s script loads on demand, at craft-mode.js\'s version');
|
|
192
132
|
assert.equal((wall.innerHTML.match(/class="gx-tag /g) || []).length, 6, 'six doodles');
|
|
193
133
|
assert.doesNotMatch(wall.innerHTML, /<text/, 'scribbles, not words (owner, 2026-10-09)');
|
|
194
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
195
|
-
assert.match(p.menu().innerHTML, /aria-checked="true" data-review-pick="governing"/, 'the menu checks the live experience');
|
|
196
|
-
assert.match(p.menu().innerHTML, /data-review-act="stop"/, 'and offers a way out');
|
|
197
134
|
});
|
|
198
135
|
|
|
199
136
|
test('IT LASTS THE SESSION: a reload keeps the review; a new session starts without one', () => {
|
|
@@ -215,7 +152,7 @@ test('IT LASTS THE SESSION: a reload keeps the review; a new session starts with
|
|
|
215
152
|
test('REVIEWING Ideating shows its own tool too, marked a guest so a tiny phone can fold it away', () => {
|
|
216
153
|
const p = boot({ session: artist() });
|
|
217
154
|
p.craft.review('ideating');
|
|
218
|
-
assert.deepEqual(p.tools(), ['
|
|
155
|
+
assert.deepEqual(p.tools(), ['paintbrush', 'lightbulb'], 'the artist\'s tool, then the experience\'s own, in order');
|
|
219
156
|
assert.ok(p.byId('craft-tool-lightbulb').classList.contains('craft-tool--guest'));
|
|
220
157
|
assert.ok(!p.byId('craft-tool-paintbrush').classList.contains('craft-tool--guest'));
|
|
221
158
|
p.craft.choose('ideating');
|
|
@@ -227,13 +164,11 @@ test('ENDING it: Stop reviewing, or any mode from the switch, puts the artist ba
|
|
|
227
164
|
const p = boot({ session: artist() });
|
|
228
165
|
p.craft.review('building');
|
|
229
166
|
assert.equal(p.root.dataset.craft, 'building');
|
|
230
|
-
p.
|
|
231
|
-
p.pick({ 'data-review-act': 'stop' });
|
|
167
|
+
p.craft.stopReview();
|
|
232
168
|
assert.equal(p.root.dataset.craft, 'artistry', 'Stop reviewing is back in Artistry');
|
|
233
169
|
assert.equal(p.root.getAttribute('data-craft-review'), null);
|
|
234
170
|
assert.equal(p.byId('craft-graffiti'), null, 'the wall comes down');
|
|
235
171
|
assert.equal(p.session['hall-craft-review'], undefined, 'and the session forgets it');
|
|
236
|
-
assert.ok(!p.byId('craft-tool-palette').classList.contains('craft-tool--reviewing'));
|
|
237
172
|
p.craft.review('governing');
|
|
238
173
|
p.craft.choose('artistry');
|
|
239
174
|
assert.equal(p.craft.reviewing(), null, 'choosing a mode from the switch, even Artistry, ends a review');
|
|
@@ -242,25 +177,7 @@ test('ENDING it: Stop reviewing, or any mode from the switch, puts the artist ba
|
|
|
242
177
|
p.craft.choose('governing');
|
|
243
178
|
assert.equal(p.root.dataset.craft, 'governing');
|
|
244
179
|
assert.equal(p.root.getAttribute('data-craft-review'), null, 'Governing chosen for real is not a review');
|
|
245
|
-
assert.
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test('THE KEYS: arrows move through the menu, Escape closes it, an outside click closes it', () => {
|
|
249
|
-
const p = boot({ session: artist() });
|
|
250
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
251
|
-
assert.equal(p.docListeners.click && p.docListeners.click.size, 1, 'an open menu listens for a click outside');
|
|
252
|
-
p.menu().fire('keydown', { key: 'Escape' });
|
|
253
|
-
assert.ok(!p.menuOpen(), 'Escape closes it');
|
|
254
|
-
assert.equal(p.docListeners.click.size, 0, 'and stops listening');
|
|
255
|
-
p.byId('craft-review-more').fire('keydown', { key: 'ArrowDown' });
|
|
256
|
-
assert.ok(p.menuOpen(), 'ArrowDown on the chevron opens it');
|
|
257
|
-
[...p.docListeners.click][0]({ target: p.title });
|
|
258
|
-
assert.ok(!p.menuOpen(), 'a click elsewhere closes it');
|
|
259
|
-
p.byId('craft-tool-palette').fire('keydown', { key: 'ArrowDown' });
|
|
260
|
-
assert.ok(p.menuOpen(), 'ArrowDown on Review opens it too');
|
|
261
|
-
assert.match(CRAFT, /e\.key === 'ArrowUp'/);
|
|
262
|
-
assert.match(CRAFT, /e\.key === 'Home'/);
|
|
263
|
-
assert.match(CRAFT, /e\.key === 'End'/);
|
|
180
|
+
assert.ok(p.opened.includes('close'), 'and the review panel leaves with Artistry');
|
|
264
181
|
});
|
|
265
182
|
|
|
266
183
|
test('THE WALL is scribbled in CSS only, in ink alone, and stands still under reduced motion', () => {
|
|
@@ -244,12 +244,15 @@ const hallRenderJs = read('hall-render.js');
|
|
|
244
244
|
const buildersJs = read('builders.js');
|
|
245
245
|
const goalInboxJs = read('goal-inbox.js');
|
|
246
246
|
|
|
247
|
-
test('the greeting IS the page\'s one h1, in the board\'s label voice, with
|
|
247
|
+
test('the greeting IS the page\'s one h1, in the board\'s label voice, with no sub-line under it', () => {
|
|
248
248
|
const h1s = indexHtml.match(/<h1\b/g) || [];
|
|
249
249
|
assert.equal(h1s.length, 1, 'one-header-main-h1: the greeting must BECOME the h1, never add a second');
|
|
250
|
-
assert.match(indexHtml, /<div class="page-head page-head--board page-head--hall">\s*<h1 id="hall-h1" class="page-head__h">Builders' Hall<\/h1
|
|
251
|
-
'the h1 is visible (no sr-only)
|
|
250
|
+
assert.match(indexHtml, /<div class="page-head page-head--board page-head--hall">\s*<h1 id="hall-h1" class="page-head__h">Builders' Hall<\/h1>/,
|
|
251
|
+
'the h1 is visible (no sr-only) and keeps the id brand-apply.js writes into');
|
|
252
252
|
assert.doesNotMatch(indexHtml, /id="hall-h1" class="sr-only"/, 'the sr-only h1 is gone');
|
|
253
|
+
// task 1004780 (owner): the status sentence repeated the version tab and Next up, so it went.
|
|
254
|
+
assert.doesNotMatch(indexHtml, /id="hall-sub"/, 'no status line under the greeting');
|
|
255
|
+
assert.doesNotMatch(hallRenderJs, /hall-sub|claimable right now/, 'and nothing renders one');
|
|
253
256
|
});
|
|
254
257
|
|
|
255
258
|
test('the band ends level: no align-items on the grid, so the two cards stretch', () => {
|
|
@@ -300,28 +303,18 @@ test('the system notices are inbox LINES, first in the card, and counted by its
|
|
|
300
303
|
}
|
|
301
304
|
});
|
|
302
305
|
|
|
303
|
-
test('the greeting reads /me
|
|
306
|
+
test('the greeting reads /me and the public progress (for the version tab), and builders.js feeds both', () => {
|
|
304
307
|
assert.match(buildersJs, /getJSON\(`\$\{API\}\/public\/progress`\)/, 'Home fetches the public progress once');
|
|
305
|
-
// The count comes off the SAME Next-up response — one request, not a second
|
|
306
|
-
// fetch. It reads `claimable_total` rather than the length of the returned array,
|
|
307
|
-
// because that request is now bounded (?limit=2, task 1003545): the array holds two
|
|
308
|
-
// rows and the total holds the whole rank-scoped queue. Asserting the derivation
|
|
309
|
-
// rather than one literal expression is deliberate — the previous version pinned
|
|
310
|
-
// `resp.claimable.length`, which read as a requirement to download the feed.
|
|
311
|
-
assert.match(buildersJs, /lastClaimableCount = Number\.isFinite\(claimableTotal\)/,
|
|
312
|
-
'the claimable feed Home already fetches for Next-up answers the count');
|
|
313
|
-
assert.match(buildersJs, /const claimableTotal = Number\(resp && resp\.claimable_total\);/,
|
|
314
|
-
'and it takes the count from the response total, so a bounded read still knows it');
|
|
315
308
|
assert.doesNotMatch(buildersJs, /getJSON\(`\$\{API\}\/tasks\/claimable`/,
|
|
316
|
-
'no unbounded claimable fetch on Home
|
|
309
|
+
'no unbounded claimable fetch on Home');
|
|
317
310
|
assert.match(buildersJs, /get lastProgressState\(\)/, 'the renderer reads progress through a getter, so it cannot freeze at boot');
|
|
318
|
-
assert.
|
|
319
|
-
assert.equal((buildersJs.match(/renderGreeting\(\)/g) || []).length,
|
|
311
|
+
assert.doesNotMatch(buildersJs, /lastClaimableCount/, 'the greeting no longer needs the claimable count (task 1004780)');
|
|
312
|
+
assert.equal((buildersJs.match(/renderGreeting\(\)/g) || []).length, 2, 'rendered after /me and after progress — each as it lands');
|
|
320
313
|
});
|
|
321
314
|
|
|
322
315
|
// Boot hall-render.js against a stub DOM and a ctx of getters (the way
|
|
323
316
|
// builders.js builds it) and read what the greeting and the card actually say.
|
|
324
|
-
function bootHallRender({ me, progress = null,
|
|
317
|
+
function bootHallRender({ me, progress = null, leaderboard = [] }) {
|
|
325
318
|
const els = new Map();
|
|
326
319
|
const makeEl = () => ({
|
|
327
320
|
dataset: {}, style: {}, textContent: '', hidden: false, _html: '', innerHTML: '',
|
|
@@ -359,7 +352,7 @@ function bootHallRender({ me, progress = null, claimable = null, leaderboard = [
|
|
|
359
352
|
instanceRepo: null, newcomerGated: false,
|
|
360
353
|
get lastMeState() { return me; }, get lastCostState() { return me.cost || null; }, get lastKarmaState() { return me.karma || null; },
|
|
361
354
|
get lastNeedsState() { return me.needs || null; }, get lastRebaseState() { return me.needs_rebase_tasks || null; },
|
|
362
|
-
get lastProgressState() { return progress; },
|
|
355
|
+
get lastProgressState() { return progress; },
|
|
363
356
|
};
|
|
364
357
|
const api = windowObj.OTBHallRender(ctx);
|
|
365
358
|
api.lbState.rows = leaderboard;
|
|
@@ -375,30 +368,16 @@ const ME = {
|
|
|
375
368
|
const LEADERBOARD = [{ github_login: 'a', total_credits: 20000 }, { github_login: 'percy', total_credits: 14441 }, { github_login: 'b', total_credits: 3 }, { github_login: 'c', total_credits: 1 }];
|
|
376
369
|
const PROGRESS = { progress: [{ version_id: 'BONGOS-V1', status: 'building', total_count: 605, percent_complete: 75 }, { version_id: 'V2', status: 'planning', total_count: 9000, percent_complete: 0 }] };
|
|
377
370
|
|
|
378
|
-
test('RUN: the greeting names the builder, and the
|
|
379
|
-
const { api, el } = bootHallRender({ me: ME, progress: PROGRESS
|
|
371
|
+
test('RUN: the greeting names the builder, and the version tab carries how far the version is', () => {
|
|
372
|
+
const { api, el } = bootHallRender({ me: ME, progress: PROGRESS });
|
|
380
373
|
api.renderGreeting();
|
|
381
374
|
assert.match(el('hall-h1').textContent, /^Good (morning|afternoon|evening), Percy Jackson$/);
|
|
382
|
-
assert.equal(el('
|
|
383
|
-
assert.equal(el('
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
test('RUN: each half of the sentence renders as its data lands, and the line hides with neither', () => {
|
|
375
|
+
assert.equal(el('home-version-name').textContent, 'BONGOS-V1', 'the building version, not the bigger planning one');
|
|
376
|
+
assert.equal(el('home-version-pct').textContent, '75% complete');
|
|
377
|
+
assert.equal(el('home-version').hidden, false);
|
|
387
378
|
const none = bootHallRender({ me: ME });
|
|
388
379
|
none.api.renderGreeting();
|
|
389
|
-
assert.equal(none.el('
|
|
390
|
-
const onlyProgress = bootHallRender({ me: ME, progress: PROGRESS });
|
|
391
|
-
onlyProgress.api.renderGreeting();
|
|
392
|
-
assert.equal(onlyProgress.el('hall-sub').textContent, 'BONGOS-V1 is 75 percent complete.');
|
|
393
|
-
const zero = bootHallRender({ me: ME, progress: PROGRESS, claimable: 0 });
|
|
394
|
-
zero.api.renderGreeting();
|
|
395
|
-
assert.match(zero.el('hall-sub').textContent, /Nothing is claimable right now\.$/);
|
|
396
|
-
const one = bootHallRender({ me: ME, claimable: 1 });
|
|
397
|
-
one.api.renderGreeting();
|
|
398
|
-
assert.equal(one.el('hall-sub').textContent, 'One task is claimable right now.');
|
|
399
|
-
const many = bootHallRender({ me: ME, claimable: 63 });
|
|
400
|
-
many.api.renderGreeting();
|
|
401
|
-
assert.equal(many.el('hall-sub').textContent, '63 tasks are claimable right now.');
|
|
380
|
+
assert.equal(none.el('home-version').hidden, true, 'no progress yet: no tab');
|
|
402
381
|
});
|
|
403
382
|
|
|
404
383
|
test('RUN: the Standing card carries the three facts, the place as "2nd of 4", and no banner', () => {
|
|
@@ -97,8 +97,12 @@ test('closing natively (Escape) also returns the slice, so a reopen still finds
|
|
|
97
97
|
const { ctx, page } = await bootInbox();
|
|
98
98
|
await page.click('[data-attn-open="attention-blockers"]');
|
|
99
99
|
await page.keyboard.press('Escape');
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// `open` turns false at once, but the dialog's `close` EVENT (which moves the slice home) is
|
|
101
|
+
// queued after it, so wait for the slice itself rather than for `open` (task 1004780 — reading
|
|
102
|
+
// straight after `open` flipped raced the event and failed CI about half the time).
|
|
103
|
+
const back = await page.waitForFunction(
|
|
104
|
+
() => !document.getElementById('attention-dialog').open && !!document.querySelector('#attention-slices > #attention-blockers'),
|
|
105
|
+
null, { timeout: 3000 }).then(() => true, () => false);
|
|
102
106
|
assert.equal(back, true, 'the dialog close event moves the slice home');
|
|
103
107
|
await page.click('[data-attn-open="attention-blockers"]');
|
|
104
108
|
const rows = await page.evaluate(() => document.querySelectorAll('#attention-dialog-body .ginbox__item').length);
|