@bongos/core 1.21.109 → 1.21.110
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 -24
- package/docs/copy-inventory.md +14 -10
- package/docs/copy-registry.json +48 -12
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +195 -195
- package/modules/copy-desk/skills/tweak/SKILL.md +28 -0
- package/modules/hall-ui/public/craft-mode.js +276 -21
- package/modules/hall-ui/public/craft-review.css +227 -0
- package/modules/hall-ui/public/craft-review.js +64 -0
- package/modules/hall-ui/public/style.css +1 -0
- package/modules/hall-ui/records/craft-mode.md +8 -0
- package/modules/lifecycle/autopickup-files.js +140 -0
- package/modules/lifecycle/db-ship.js +2 -1
- package/modules/lifecycle/routes/tasks.js +4 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/gds/copy-apply.js +17 -2
- package/scripts/gds/tweak-halt.js +69 -0
- package/src/module-api.js +1 -1
- package/tests/craft_review_mode.mjs +307 -0
- package/tests/page_notes_panel.mjs +4 -1
- package/tests/tweak_batch_apply.mjs +16 -0
- package/tests/tweak_unattended.mjs +274 -0
|
@@ -72,6 +72,34 @@ Heal whatever ship-check names first (`git add` new files before it). Run ship.j
|
|
|
72
72
|
|
|
73
73
|
A failed grade is an ordinary one: read it (`GET /api/bongos/tasks/<task-id>?include=grade`), fix, commit, `node scripts/gds/ship.js <task-id> --regrade`. If the branch did not publish, a plain `node scripts/gds/ship.js <task-id>` publishes it again without re-grading.
|
|
74
74
|
|
|
75
|
+
## Unattended mode: `/tweak --page <page-id> --unattended`
|
|
76
|
+
|
|
77
|
+
The Autobongos tweak lane (ADR 0371 D1, D8) runs this skill with nobody at the terminal. It names ONE page and claims that page's submitted round with the `auto_pickup` mark before you start. In this mode:
|
|
78
|
+
|
|
79
|
+
- **Never ask.** Every question the steps above would put to a person is a halt instead.
|
|
80
|
+
- **Work only the named page.** Skip step 1's queue read; the round is the one you hold. Pass `--page <page-id>` to every script: `copy-apply.js --batch --page <page-id> --unattended` refuses a round for another page (exit 5) and, on any non-zero exit, releases the round with a plain note itself.
|
|
81
|
+
- **Change only the page's own files.** No shell cache-bust, no hand fix of a refused line, no edit outside the page's files in `docs/page-inventory.json` (besides the copy registry, the page readings and the generated maps ship.js refreshes), and no session-log file (it would move `CLAUDE.md`, a protected path). A rewrite that needs anything else is already its own engineer task.
|
|
82
|
+
- **Ship one round, once, graded.** Never `--skip-grade`, `--force` or `--allow-empty`. Stop at WAITING FOR THE ARTIST.
|
|
83
|
+
|
|
84
|
+
**On any halt, release and exit.** Run the matching line and stop; do not retry, and do not try another round:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
node scripts/gds/tweak-halt.js <task-id> --code <code>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| What happened | `--code` |
|
|
91
|
+
|---|---|
|
|
92
|
+
| the claim was refused | `claim_refused` (nothing to release: just exit) |
|
|
93
|
+
| `copy-apply` exited non-zero | already released by `--unattended` |
|
|
94
|
+
| `ship-check` named something stale it cannot heal by regenerating | `ship_check_stale` |
|
|
95
|
+
| the grade came back GRADER UNAVAILABLE | `grader_unavailable` |
|
|
96
|
+
| the grade failed | `grade_failed` |
|
|
97
|
+
| the diff would touch a file outside the page | `outside_page` |
|
|
98
|
+
|
|
99
|
+
The note is fixed wording per code, so nothing you write reaches the round.
|
|
100
|
+
|
|
101
|
+
**The server holds these rules, not you.** A page tweak reaches `confirmed` only through its artist's approval or the owner's override (`POST /tasks/:id/confirm` refuses it), and the publish of an `auto_pickup` round whose pull request touches a file outside the page, or a protected path, is refused and its pull request closed. Breaking a rule above gets the round refused, not landed.
|
|
102
|
+
|
|
75
103
|
## What happens next (not yours)
|
|
76
104
|
|
|
77
105
|
The artist approves in the studio's Approval queue (TW12), which lands it like any confirmed task, or sends it back with a note, which returns the round to the queue for the next `/tweak`.
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
// It is a MODE the existing hall wears, never a second hall (the founding-mode
|
|
10
10
|
// precedent, docs/design/project-startup-direction.md). The nav follows the mode
|
|
11
11
|
// (task 1004493, MODE_TABS below), and so does the look (task 1004495, LOOKS
|
|
12
|
-
// below); the mode tools are their own tasks (1004496 onward)
|
|
12
|
+
// below); the mode tools are their own tasks (1004496 onward), and Artistry's
|
|
13
|
+
// Review button opens review mode (task 1004686): the hall wears another
|
|
14
|
+
// mode's experience while an artist reviews it. Legacy mode, a
|
|
13
15
|
// fifth choice, was removed (task 1004567): Building already shows every tab,
|
|
14
16
|
// and a stored 'legacy' now reads as no mode.
|
|
15
17
|
//
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
governing: '<path d="M2 6 8 2.5 14 6M3.5 6.5v6M6.5 6.5v6M9.5 6.5v6M12.5 6.5v6M2 13.5h12"/>',
|
|
33
35
|
brush: '<path d="M13.6 2.4 7.4 8.6"/><path d="M7.4 8.6c-1.3-.7-3-.1-3.4 1.5-.3 1.3-.8 2.2-1.9 2.9 2.4.7 4.6.1 5.6-1.4.6-.9.5-2.1-.3-3Z"/>',
|
|
34
36
|
note: '<path d="M9.5 2.5h-6v11h9v-6"/><path d="M5.5 8h5M5.5 10.5h3.5"/><path d="M12.5 1.5v4M10.5 3.5h4"/>',
|
|
37
|
+
write: '<path d="M2.5 13.5h11"/><path d="M10.4 2.6l2.9 2.9-6.4 6.4H4V9z"/>',
|
|
35
38
|
modes: '<rect x="2.5" y="2.5" width="4.5" height="4.5" rx="1"/><rect x="9" y="2.5" width="4.5" height="4.5" rx="1"/><rect x="2.5" y="9" width="4.5" height="4.5" rx="1"/><rect x="9" y="9" width="4.5" height="4.5" rx="1"/>',
|
|
36
39
|
};
|
|
37
40
|
// The four crafts. `short` is the switch's label; `line` is the
|
|
@@ -266,7 +269,8 @@
|
|
|
266
269
|
artistry: [
|
|
267
270
|
// Named "Artist review" since task 1004499: the public site's button
|
|
268
271
|
// (public-landing assets/artist-review.js) opens this same panel.
|
|
269
|
-
|
|
272
|
+
// `menu`: hovering it opens the review-mode menu (task 1004686, below).
|
|
273
|
+
{ id: 'palette', icon: 'artistry', short: 'Review', label: 'Artist review: write feedback on this page', script: 'page-notes.js', api: 'OTBPageNotes', menu: true },
|
|
270
274
|
// Task 1004497: this page's words, edited where they stand as a proposed wording change.
|
|
271
275
|
{ id: 'paintbrush', icon: 'brush', short: 'Edit words', label: 'Edit this page\'s words', script: 'paintbrush.js', api: 'OTBPaintbrush' },
|
|
272
276
|
],
|
|
@@ -297,29 +301,270 @@
|
|
|
297
301
|
for (var m in TOOLS) for (var i = 0; i < TOOLS[m].length; i++) if (TOOLS[m][i].id === id) return TOOLS[m][i];
|
|
298
302
|
return null;
|
|
299
303
|
}
|
|
304
|
+
// The tools a mode shows. Reviewing another experience (task 1004686), the
|
|
305
|
+
// artist keeps Artistry's tools and gains that experience's own after them.
|
|
306
|
+
function toolsFor(mode) {
|
|
307
|
+
var own = Object.prototype.hasOwnProperty.call(TOOLS, mode) ? TOOLS[mode] : [];
|
|
308
|
+
if (!review || mode === 'artistry') return own;
|
|
309
|
+
return TOOLS.artistry.concat(own);
|
|
310
|
+
}
|
|
311
|
+
function openTool(t, b) {
|
|
312
|
+
loadTool(t).then(function (api) { api.open(); }, function () { b.setAttribute('aria-disabled', 'true'); });
|
|
313
|
+
}
|
|
314
|
+
function toolButton(t) {
|
|
315
|
+
var b = document.createElement('button');
|
|
316
|
+
b.type = 'button';
|
|
317
|
+
b.id = 'craft-tool-' + t.id;
|
|
318
|
+
b.className = 'craft-tool craft-tool--' + t.id;
|
|
319
|
+
b.setAttribute('data-craft-tool', t.id);
|
|
320
|
+
b.setAttribute('aria-label', t.label);
|
|
321
|
+
b.title = t.label;
|
|
322
|
+
b.innerHTML = icon(t.icon, 'craft-tool__icon') + '<span class="craft-tool__label">' + esc(t.short) + '</span>';
|
|
323
|
+
b.addEventListener('click', function (e) {
|
|
324
|
+
// A phone has no hover, so there a tap on Review opens its menu instead.
|
|
325
|
+
if (t.menu && narrow()) { toggleReviewMenu(e && e.detail === 0); return; }
|
|
326
|
+
openTool(t, b);
|
|
327
|
+
});
|
|
328
|
+
return t.menu ? reviewGroup(t, b) : b;
|
|
329
|
+
}
|
|
330
|
+
// The node a tool occupies in the bar: its button, or Review's whole group.
|
|
331
|
+
function toolNode(t) {
|
|
332
|
+
var b = document.getElementById('craft-tool-' + t.id);
|
|
333
|
+
return b && t.menu && b.parentNode && b.parentNode.id === REVIEW_GROUP ? b.parentNode : b;
|
|
334
|
+
}
|
|
300
335
|
function applyTools(mode) {
|
|
301
336
|
var sw = document.getElementById('craft-switch');
|
|
302
|
-
var own =
|
|
337
|
+
var own = toolsFor(mode);
|
|
303
338
|
var old = document.querySelectorAll ? document.querySelectorAll('[data-craft-tool]') : [];
|
|
304
339
|
for (var i = old.length - 1; i >= 0; i--) {
|
|
305
|
-
if (
|
|
340
|
+
if (own.some(function (t) { return t.id === old[i].getAttribute('data-craft-tool'); })) continue;
|
|
341
|
+
var t0 = toolFor(old[i].getAttribute('data-craft-tool'));
|
|
342
|
+
remove(t0 ? toolNode(t0) : old[i]);
|
|
306
343
|
}
|
|
307
344
|
if (!sw || !sw.parentNode) return;
|
|
345
|
+
// Right to left, each missing tool goes in front of the one after it, so
|
|
346
|
+
// the bar reads in TOOLS order whichever tools were already there.
|
|
347
|
+
var before = sw;
|
|
348
|
+
for (var j = own.length - 1; j >= 0; j--) {
|
|
349
|
+
var node = toolNode(own[j]);
|
|
350
|
+
if (!node) node = sw.parentNode.insertBefore(toolButton(own[j]), before);
|
|
351
|
+
before = node;
|
|
352
|
+
}
|
|
353
|
+
// A guest is the reviewed experience's own tool beside the artist's: under
|
|
354
|
+
// 360px the bar has no room for three, so craft-review.css folds it away.
|
|
308
355
|
own.forEach(function (t) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
b.type = 'button';
|
|
312
|
-
b.id = 'craft-tool-' + t.id;
|
|
313
|
-
b.className = 'craft-tool craft-tool--' + t.id;
|
|
314
|
-
b.setAttribute('data-craft-tool', t.id);
|
|
315
|
-
b.setAttribute('aria-label', t.label);
|
|
316
|
-
b.title = t.label;
|
|
317
|
-
b.innerHTML = icon(t.icon, 'craft-tool__icon') + '<span class="craft-tool__label">' + esc(t.short) + '</span>';
|
|
318
|
-
b.addEventListener('click', function () {
|
|
319
|
-
loadTool(t).then(function (api) { api.open(); }, function () { b.setAttribute('aria-disabled', 'true'); });
|
|
320
|
-
});
|
|
321
|
-
sw.parentNode.insertBefore(b, sw);
|
|
356
|
+
var el = document.getElementById('craft-tool-' + t.id);
|
|
357
|
+
if (el && el.classList) el.classList.toggle('craft-tool--guest', !!review && TOOLS.artistry.indexOf(t) === -1);
|
|
322
358
|
});
|
|
359
|
+
var on = document.getElementById('craft-tool-palette');
|
|
360
|
+
if (on) {
|
|
361
|
+
on.classList.toggle('craft-tool--reviewing', !!review);
|
|
362
|
+
var word = on.querySelector && on.querySelector('.craft-tool__label');
|
|
363
|
+
if (word) word.textContent = review ? 'Reviewing' : 'Review';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ---------- artist review mode (task 1004686) ----------
|
|
368
|
+
// Hovering Artistry's Review button opens a menu of the four experiences
|
|
369
|
+
// (owner, 2026-10-09, in the owner's order and words). Picking one makes the hall
|
|
370
|
+
// wear that mode on every page — its tabs, its look, its tools — the way a
|
|
371
|
+
// builder in it sees it, while the artist keeps Artistry's tools and small
|
|
372
|
+
// sky-style doodles are scribbled into the top bar's empty space (craft-review.js draws them,
|
|
373
|
+
// loaded only in review mode; craft-review.css styles it and the menu).
|
|
374
|
+
// The artist's own mode stays Artistry: the experience is this session's
|
|
375
|
+
// alone (sessionStorage), and choosing any mode from the switch or the
|
|
376
|
+
// picker ends it. Cosmetic like the mode: a tab the gate hides stays hidden.
|
|
377
|
+
var REVIEW_KEY = 'hall-craft-review';
|
|
378
|
+
var REVIEW_GROUP = 'craft-review';
|
|
379
|
+
var EXPERIENCES = [
|
|
380
|
+
{ id: 'ideating', label: 'Ideator experience' },
|
|
381
|
+
{ id: 'governing', label: 'Governing experience' },
|
|
382
|
+
{ id: 'building', label: 'Building experience' },
|
|
383
|
+
{ id: 'artistry', label: 'Artistry experience' },
|
|
384
|
+
];
|
|
385
|
+
// The experience being reviewed, or null. Read from the session once, then
|
|
386
|
+
// held here, so a browser whose storage throws still reviews this page.
|
|
387
|
+
var review = null;
|
|
388
|
+
function storedReview() {
|
|
389
|
+
return read() === 'artistry' ? valid(get('sessionStorage', REVIEW_KEY)) : null;
|
|
390
|
+
}
|
|
391
|
+
// The mode the hall wears: the experience under review, else the chosen mode.
|
|
392
|
+
function wearing() { return review || read(); }
|
|
393
|
+
function startReview(mode) {
|
|
394
|
+
mode = valid(mode);
|
|
395
|
+
if (!mode) return;
|
|
396
|
+
set('sessionStorage', MODE_KEY, 'artistry');
|
|
397
|
+
set('sessionStorage', ASKED_KEY, '1');
|
|
398
|
+
set('sessionStorage', REVIEW_KEY, mode);
|
|
399
|
+
review = mode;
|
|
400
|
+
apply(mode);
|
|
401
|
+
}
|
|
402
|
+
function stopReview() {
|
|
403
|
+
drop('sessionStorage', REVIEW_KEY);
|
|
404
|
+
review = null;
|
|
405
|
+
apply('artistry');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// The menu. Hover opens it where a pointer can hover; the chevron (or, on a
|
|
409
|
+
// phone, Review itself) opens it by click, tap or keyboard. Built on every
|
|
410
|
+
// open, so the checked experience is always the live one.
|
|
411
|
+
var closeTimer = null;
|
|
412
|
+
// Hover is a mouse's alone: a touch also fires pointer events, and a laptop
|
|
413
|
+
// with a touch screen can report no hover at all, so ask the pointer itself.
|
|
414
|
+
function byMouse(e) { return !!e && e.pointerType === 'mouse'; }
|
|
415
|
+
function reviewMenuHtml() {
|
|
416
|
+
var items = '<button class="craft-review__item" type="button" role="menuitem" data-review-act="write">' +
|
|
417
|
+
icon('write', 'craft-review__icon') + '<span>Write feedback on this page</span></button>' +
|
|
418
|
+
'<p class="craft-review__head" role="presentation">Review mode</p>';
|
|
419
|
+
items += EXPERIENCES.map(function (x) {
|
|
420
|
+
var on = review === x.id;
|
|
421
|
+
return '<button class="craft-review__item" type="button" role="menuitemradio" aria-checked="' + (on ? 'true' : 'false') + '" data-review-pick="' + x.id + '">' +
|
|
422
|
+
icon(x.id, 'craft-review__icon') + '<span>' + esc(x.label) + '</span>' +
|
|
423
|
+
(on ? '<svg class="craft-review__check" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>' : '') +
|
|
424
|
+
'</button>';
|
|
425
|
+
}).join('');
|
|
426
|
+
if (review) {
|
|
427
|
+
items += '<button class="craft-review__item craft-review__item--stop" type="button" role="menuitem" data-review-act="stop">' +
|
|
428
|
+
'<svg class="craft-review__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg><span>Stop reviewing</span></button>';
|
|
429
|
+
}
|
|
430
|
+
return items;
|
|
431
|
+
}
|
|
432
|
+
function menuEl() { return document.getElementById(REVIEW_GROUP + '-menu'); }
|
|
433
|
+
function menuItems() {
|
|
434
|
+
var m = menuEl();
|
|
435
|
+
return m && m.querySelectorAll ? Array.prototype.slice.call(m.querySelectorAll('[role^="menuitem"]')) : [];
|
|
436
|
+
}
|
|
437
|
+
function openReviewMenu(focusFirst) {
|
|
438
|
+
var m = menuEl(), more = document.getElementById(REVIEW_GROUP + '-more');
|
|
439
|
+
if (!m) return;
|
|
440
|
+
clearTimeout(closeTimer);
|
|
441
|
+
if (m.hasAttribute('hidden')) {
|
|
442
|
+
m.innerHTML = reviewMenuHtml();
|
|
443
|
+
m.removeAttribute('hidden');
|
|
444
|
+
if (more) more.setAttribute('aria-expanded', 'true');
|
|
445
|
+
document.addEventListener('click', onOutside, true);
|
|
446
|
+
}
|
|
447
|
+
if (focusFirst) {
|
|
448
|
+
var first = menuItems()[0];
|
|
449
|
+
if (first && first.focus) first.focus();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function closeReviewMenu(refocus) {
|
|
453
|
+
var m = menuEl(), more = document.getElementById(REVIEW_GROUP + '-more');
|
|
454
|
+
clearTimeout(closeTimer);
|
|
455
|
+
document.removeEventListener('click', onOutside, true);
|
|
456
|
+
if (!m || m.hasAttribute('hidden')) return;
|
|
457
|
+
m.setAttribute('hidden', '');
|
|
458
|
+
if (more) more.setAttribute('aria-expanded', 'false');
|
|
459
|
+
if (refocus) {
|
|
460
|
+
var back = narrow() ? document.getElementById('craft-tool-palette') : more;
|
|
461
|
+
if (back && back.focus) back.focus();
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function toggleReviewMenu(focusFirst) {
|
|
465
|
+
var m = menuEl();
|
|
466
|
+
if (m && !m.hasAttribute('hidden')) closeReviewMenu(false); else openReviewMenu(focusFirst);
|
|
467
|
+
}
|
|
468
|
+
function onOutside(e) {
|
|
469
|
+
var g = document.getElementById(REVIEW_GROUP);
|
|
470
|
+
if (g && g.contains && !g.contains(e.target)) closeReviewMenu(false);
|
|
471
|
+
}
|
|
472
|
+
function onMenuClick(e) {
|
|
473
|
+
var btn = e.target.closest && e.target.closest('button');
|
|
474
|
+
if (!btn) return;
|
|
475
|
+
var pick = btn.getAttribute('data-review-pick');
|
|
476
|
+
var act = btn.getAttribute('data-review-act');
|
|
477
|
+
if (!pick && !act) return;
|
|
478
|
+
closeReviewMenu(false);
|
|
479
|
+
if (pick) startReview(pick);
|
|
480
|
+
else if (act === 'stop') stopReview();
|
|
481
|
+
else if (act === 'write') openTool(toolFor('palette'), document.getElementById('craft-tool-palette'));
|
|
482
|
+
}
|
|
483
|
+
// The menu keys: arrows move, Home and End jump, Escape closes back to the
|
|
484
|
+
// button that opened it, Tab closes and lets focus move on.
|
|
485
|
+
function onMenuKey(e) {
|
|
486
|
+
var items = menuItems();
|
|
487
|
+
var at = items.indexOf(document.activeElement);
|
|
488
|
+
var to = null;
|
|
489
|
+
if (e.key === 'ArrowDown') to = (at + 1) % items.length;
|
|
490
|
+
else if (e.key === 'ArrowUp') to = (at - 1 + items.length) % items.length;
|
|
491
|
+
else if (e.key === 'Home') to = 0;
|
|
492
|
+
else if (e.key === 'End') to = items.length - 1;
|
|
493
|
+
else if (e.key === 'Escape') { e.preventDefault(); closeReviewMenu(true); return; }
|
|
494
|
+
else if (e.key === 'Tab') { closeReviewMenu(false); return; }
|
|
495
|
+
if (to === null || !items[to]) return;
|
|
496
|
+
e.preventDefault();
|
|
497
|
+
items[to].focus();
|
|
498
|
+
}
|
|
499
|
+
function reviewGroup(t, b) {
|
|
500
|
+
var g = document.createElement('span');
|
|
501
|
+
g.id = REVIEW_GROUP;
|
|
502
|
+
g.className = 'craft-review';
|
|
503
|
+
var more = document.createElement('button');
|
|
504
|
+
more.type = 'button';
|
|
505
|
+
more.id = REVIEW_GROUP + '-more';
|
|
506
|
+
more.className = 'craft-review__more';
|
|
507
|
+
more.setAttribute('aria-haspopup', 'menu');
|
|
508
|
+
more.setAttribute('aria-expanded', 'false');
|
|
509
|
+
more.setAttribute('aria-controls', REVIEW_GROUP + '-menu');
|
|
510
|
+
more.setAttribute('aria-label', 'Review mode: see the hall as another mode');
|
|
511
|
+
more.title = 'Review mode: see the hall as another mode';
|
|
512
|
+
more.innerHTML = '<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 4.5 6 7.5 9 4.5"/></svg>';
|
|
513
|
+
more.addEventListener('click', function (e) { toggleReviewMenu(e && e.detail === 0); });
|
|
514
|
+
more.addEventListener('keydown', function (e) {
|
|
515
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); openReviewMenu(true); }
|
|
516
|
+
});
|
|
517
|
+
var menu = document.createElement('div');
|
|
518
|
+
menu.id = REVIEW_GROUP + '-menu';
|
|
519
|
+
menu.className = 'craft-review__menu';
|
|
520
|
+
menu.setAttribute('role', 'menu');
|
|
521
|
+
menu.setAttribute('aria-label', 'Review mode');
|
|
522
|
+
menu.setAttribute('hidden', '');
|
|
523
|
+
menu.addEventListener('click', onMenuClick);
|
|
524
|
+
menu.addEventListener('keydown', onMenuKey);
|
|
525
|
+
b.addEventListener('keydown', function (e) {
|
|
526
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); openReviewMenu(true); }
|
|
527
|
+
});
|
|
528
|
+
g.addEventListener('pointerenter', function (e) { if (byMouse(e)) openReviewMenu(false); });
|
|
529
|
+
g.addEventListener('pointerleave', function (e) {
|
|
530
|
+
if (!byMouse(e)) return;
|
|
531
|
+
clearTimeout(closeTimer);
|
|
532
|
+
closeTimer = setTimeout(function () { closeReviewMenu(false); }, 220);
|
|
533
|
+
});
|
|
534
|
+
g.appendChild(b);
|
|
535
|
+
g.appendChild(more);
|
|
536
|
+
g.appendChild(menu);
|
|
537
|
+
return g;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// The scribble wall: the top bar's empty space, doodled on while reviewing.
|
|
541
|
+
// craft-review.js draws it (decorative, aria-hidden); every motion is CSS,
|
|
542
|
+
// so the reduced-motion kill switch leaves three finished doodles standing.
|
|
543
|
+
var WALL = 'craft-graffiti';
|
|
544
|
+
var wallTool = { script: 'craft-review.js', api: 'OTBCraftReview' };
|
|
545
|
+
function applyWall(mode) {
|
|
546
|
+
var wall = document.getElementById(WALL);
|
|
547
|
+
var root = document.documentElement;
|
|
548
|
+
if (!review) {
|
|
549
|
+
remove(wall);
|
|
550
|
+
if (root.removeAttribute) root.removeAttribute('data-craft-review');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (root.setAttribute) root.setAttribute('data-craft-review', mode);
|
|
554
|
+
var side = document.querySelector && document.querySelector('#app-topbar > .topbar-side');
|
|
555
|
+
if (wall && wall.getAttribute('data-wall') === mode) return;
|
|
556
|
+
remove(wall);
|
|
557
|
+
if (!side || !side.parentNode) return;
|
|
558
|
+
wall = document.createElement('div');
|
|
559
|
+
wall.id = WALL;
|
|
560
|
+
wall.className = 'craft-graffiti';
|
|
561
|
+
wall.setAttribute('data-wall', mode);
|
|
562
|
+
wall.setAttribute('aria-hidden', 'true');
|
|
563
|
+
side.parentNode.insertBefore(wall, side);
|
|
564
|
+
loadTool(wallTool).then(function (api) {
|
|
565
|
+
var live = document.getElementById(WALL);
|
|
566
|
+
if (live === wall && review === mode) wall.innerHTML = api.wallHtml(mode);
|
|
567
|
+
}, function () { /* a bare wall; the menu still says you are reviewing */ });
|
|
323
568
|
}
|
|
324
569
|
// sessionStorage: this session's mode, or that the picker was already shown.
|
|
325
570
|
var MODE_KEY = 'hall-craft';
|
|
@@ -371,13 +616,16 @@
|
|
|
371
616
|
applyLook(mode);
|
|
372
617
|
applyTallies(mode);
|
|
373
618
|
applyTools(mode);
|
|
374
|
-
|
|
619
|
+
applyWall(mode);
|
|
620
|
+
try { document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: mode, review: !!review } })); } catch (_) { /* old engine */ }
|
|
375
621
|
}
|
|
376
622
|
|
|
377
623
|
// Choose a mode: this session always; across sessions only when asked to keep it.
|
|
624
|
+
// Choosing any mode, Artistry included, ends a review (task 1004686).
|
|
378
625
|
function choose(mode, keep) {
|
|
379
626
|
mode = valid(mode);
|
|
380
627
|
if (!mode) return;
|
|
628
|
+
if (review) { drop('sessionStorage', REVIEW_KEY); review = null; closeReviewMenu(false); }
|
|
381
629
|
set('sessionStorage', MODE_KEY, mode);
|
|
382
630
|
set('sessionStorage', ASKED_KEY, '1');
|
|
383
631
|
if (keep === true) set('localStorage', KEEP_KEY, mode);
|
|
@@ -389,7 +637,8 @@
|
|
|
389
637
|
// ---------- the header switch ----------
|
|
390
638
|
function switchHtml() {
|
|
391
639
|
if (pageOptsOut()) return '';
|
|
392
|
-
|
|
640
|
+
review = storedReview();
|
|
641
|
+
var current = review || read();
|
|
393
642
|
return '<div class="craft-switch' + (current ? '' : ' craft-switch--unset') + '" id="craft-switch" role="group" aria-label="Mode">' +
|
|
394
643
|
MODES.map(function (m) {
|
|
395
644
|
return '<button class="craft-switch__opt craft-switch__opt--' + m.id + '" type="button" data-craft-opt="' + m.id + '"' +
|
|
@@ -551,7 +800,8 @@
|
|
|
551
800
|
var mounted = false;
|
|
552
801
|
function mount() {
|
|
553
802
|
if (pageOptsOut()) return;
|
|
554
|
-
|
|
803
|
+
review = storedReview();
|
|
804
|
+
apply(wearing());
|
|
555
805
|
if (mounted) return;
|
|
556
806
|
mounted = true;
|
|
557
807
|
var sw = document.getElementById('craft-switch');
|
|
@@ -572,8 +822,13 @@
|
|
|
572
822
|
|
|
573
823
|
window.OTBCraftMode = {
|
|
574
824
|
MODES: IDS.slice(),
|
|
575
|
-
current:
|
|
825
|
+
current: wearing,
|
|
576
826
|
choose: choose,
|
|
827
|
+
// Review mode (task 1004686): the experience under review, or null.
|
|
828
|
+
reviewing: function () { return review; },
|
|
829
|
+
review: startReview,
|
|
830
|
+
stopReview: stopReview,
|
|
831
|
+
EXPERIENCES: EXPERIENCES.map(function (x) { return x.id; }),
|
|
577
832
|
open: openPicker,
|
|
578
833
|
ALWAYS_TABS: ALWAYS_TABS.slice(),
|
|
579
834
|
tabsFor: tabsFor,
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/* craft-review.css — artist review mode (task 1004686; craft-mode.js draws the
|
|
2
|
+
menu, craft-review.js the wall). Imported by style.css, beside page-notes.css.
|
|
3
|
+
|
|
4
|
+
The Review button becomes a split pill: Review still opens the feedback doc,
|
|
5
|
+
and the chevron (or, where a pointer can hover, the whole pill) opens a menu
|
|
6
|
+
of the four experiences. While an artist reviews one, small black-and-white
|
|
7
|
+
doodles in the sky's hand are scribbled into the top bar's empty space.
|
|
8
|
+
|
|
9
|
+
Every colour is a token (tests/hall_audit.mjs holds that). Every motion is
|
|
10
|
+
CSS on transform and opacity, and the hall's reduced-motion kill switch
|
|
11
|
+
(style.css) leaves three doodles standing, finished, with no pen.
|
|
12
|
+
|
|
13
|
+
An @import lands BEFORE style.css's own rules, so a rule here that restyles
|
|
14
|
+
.craft-tool must out-rank it (two classes), or style.css silently wins. */
|
|
15
|
+
|
|
16
|
+
/* ---------- the split pill ---------- */
|
|
17
|
+
.craft-review {
|
|
18
|
+
position: relative;
|
|
19
|
+
display: inline-flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
flex-shrink: 0;
|
|
22
|
+
border-radius: var(--radius-pill);
|
|
23
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--craft, var(--ink)) 40%, transparent);
|
|
24
|
+
}
|
|
25
|
+
.craft-review > .craft-tool {
|
|
26
|
+
box-shadow: none;
|
|
27
|
+
border-radius: var(--radius-pill) 0 0 var(--radius-pill);
|
|
28
|
+
}
|
|
29
|
+
.craft-review__more {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
width: 24px;
|
|
34
|
+
height: 32px;
|
|
35
|
+
padding: 0 4px 0 0;
|
|
36
|
+
border: 0;
|
|
37
|
+
border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
|
|
38
|
+
background: none;
|
|
39
|
+
color: var(--craft, var(--ink-soft));
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
}
|
|
42
|
+
.craft-review__more svg { width: 12px; height: 12px; transition: transform 200ms cubic-bezier(.2, .7, .3, 1); }
|
|
43
|
+
.craft-review__more[aria-expanded="true"] svg { transform: rotate(180deg); }
|
|
44
|
+
.craft-review__more:hover { background: color-mix(in srgb, var(--craft, var(--ink)) 12%, transparent); }
|
|
45
|
+
.craft-review__more:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
46
|
+
@media (min-width: 1181px) {
|
|
47
|
+
.craft-review > .craft-tool { padding-right: 6px; }
|
|
48
|
+
}
|
|
49
|
+
@media (max-width: 1180px) {
|
|
50
|
+
.craft-review > .craft-tool { width: 30px; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Reviewing: the pill takes a wash of the experience's colour, says
|
|
54
|
+
"Reviewing", and a drop of wet paint hangs from it. */
|
|
55
|
+
.craft-tool.craft-tool--reviewing { position: relative; background: color-mix(in srgb, var(--craft, var(--sea)) 14%, transparent); }
|
|
56
|
+
.craft-tool.craft-tool--reviewing::after {
|
|
57
|
+
content: '';
|
|
58
|
+
position: absolute;
|
|
59
|
+
left: 13px;
|
|
60
|
+
bottom: -4px;
|
|
61
|
+
width: 4px;
|
|
62
|
+
height: 7px;
|
|
63
|
+
border-radius: 0 0 3px 3px;
|
|
64
|
+
background: var(--craft, var(--sea));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ---------- the menu ---------- */
|
|
68
|
+
.craft-review__menu {
|
|
69
|
+
position: absolute;
|
|
70
|
+
top: calc(100% + 8px);
|
|
71
|
+
right: 0;
|
|
72
|
+
z-index: var(--z-drawer);
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: 2px;
|
|
76
|
+
min-width: 248px;
|
|
77
|
+
padding: 6px;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
background: var(--bg-card);
|
|
80
|
+
border-radius: var(--radius);
|
|
81
|
+
box-shadow: 0 0 0 1px var(--glass-edge), var(--shadow-pop);
|
|
82
|
+
}
|
|
83
|
+
.craft-review__menu[hidden] { display: none; }
|
|
84
|
+
/* A bridge over the gap, so a pointer travelling down to the menu never
|
|
85
|
+
leaves the pill and the hover menu does not close under it. */
|
|
86
|
+
.craft-review__menu::before { content: ''; position: absolute; left: 0; right: 0; top: -8px; height: 8px; }
|
|
87
|
+
.craft-review__head {
|
|
88
|
+
margin: 8px 12px 2px;
|
|
89
|
+
font-family: var(--label-face);
|
|
90
|
+
font-size: 11px;
|
|
91
|
+
letter-spacing: .12em;
|
|
92
|
+
text-transform: uppercase;
|
|
93
|
+
color: var(--ink-faint);
|
|
94
|
+
}
|
|
95
|
+
.craft-review__item {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 10px;
|
|
99
|
+
min-height: 36px;
|
|
100
|
+
padding: 0 12px;
|
|
101
|
+
border: 0;
|
|
102
|
+
border-radius: var(--radius-sm);
|
|
103
|
+
background: none;
|
|
104
|
+
color: var(--ink);
|
|
105
|
+
font: inherit;
|
|
106
|
+
font-size: 14px;
|
|
107
|
+
text-align: left;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
}
|
|
111
|
+
.craft-review__item:hover { background: color-mix(in srgb, var(--craft, var(--ink)) 10%, transparent); }
|
|
112
|
+
.craft-review__item:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
|
|
113
|
+
.craft-review__icon { width: 16px; height: 16px; flex-shrink: 0; color: var(--ink-soft); }
|
|
114
|
+
.craft-review__item[aria-checked="true"] { color: var(--craft, var(--ink)); font-weight: 500; }
|
|
115
|
+
.craft-review__item[aria-checked="true"] .craft-review__icon { color: var(--craft, var(--ink)); }
|
|
116
|
+
.craft-review__check { width: 14px; height: 14px; margin-left: auto; flex-shrink: 0; }
|
|
117
|
+
.craft-review__item--stop { margin-top: 4px; border-top: 1px solid var(--rule-soft); border-radius: 0 0 var(--radius-sm) var(--radius-sm); }
|
|
118
|
+
|
|
119
|
+
/* A phone: no hover and no room for the chevron, so Review itself opens the
|
|
120
|
+
menu (craft-mode.js), and the menu spans the bar instead of hanging off a
|
|
121
|
+
pill that may sit near the left edge. */
|
|
122
|
+
@media (max-width: 640px) {
|
|
123
|
+
.craft-review { position: static; }
|
|
124
|
+
.craft-review__more { display: none; }
|
|
125
|
+
.craft-review > .craft-tool { width: 28px; border-radius: var(--radius-pill); }
|
|
126
|
+
.craft-review__menu { top: calc(100% + 6px); left: 12px; right: 12px; min-width: 0; }
|
|
127
|
+
.craft-review__item { min-height: 44px; }
|
|
128
|
+
}
|
|
129
|
+
/* Under 360px the bar holds two tools, not three: reviewing Ideating, its
|
|
130
|
+
own Ideas button folds away (craft-mode.js marks it a guest). */
|
|
131
|
+
@media (max-width: 359px) { .craft-tool.craft-tool--guest { display: none; } }
|
|
132
|
+
|
|
133
|
+
/* ---------- the scribble wall ---------- */
|
|
134
|
+
/* The top bar's empty space, between the title and the controls. Where none
|
|
135
|
+
is left (a phone), the Review pill still says "Reviewing". */
|
|
136
|
+
.craft-graffiti {
|
|
137
|
+
position: relative;
|
|
138
|
+
flex: 1 1 0;
|
|
139
|
+
min-width: 0;
|
|
140
|
+
align-self: stretch;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
pointer-events: none;
|
|
143
|
+
container: gx / inline-size;
|
|
144
|
+
}
|
|
145
|
+
@media (max-width: 640px) { .craft-graffiti { display: none; } }
|
|
146
|
+
/* Small walls keep the small doodles; a wall too tight for even one shows none. */
|
|
147
|
+
@container gx (max-width: 58px) { .gx-tag--wide { display: none; } }
|
|
148
|
+
@container gx (max-width: 34px) { .gx-tag { display: none; } }
|
|
149
|
+
|
|
150
|
+
/* Six doodles in the sky's hand (craft-review.js), each scribbled at its own
|
|
151
|
+
spot and height across the wall: a new one every five seconds of a
|
|
152
|
+
thirty-second round, each staying about ten, so two or three are up at once. */
|
|
153
|
+
.gx-tag {
|
|
154
|
+
position: absolute;
|
|
155
|
+
top: calc(50% - 12px + var(--gx-dy, 0px));
|
|
156
|
+
width: var(--gx-w);
|
|
157
|
+
height: 24px;
|
|
158
|
+
--gx-delay: 0s;
|
|
159
|
+
}
|
|
160
|
+
.gx-tag--1 { left: calc((100% - var(--gx-w)) * .06); --gx-dy: -7px; }
|
|
161
|
+
.gx-tag--2 { left: calc((100% - var(--gx-w)) * .55); --gx-dy: 6px; --gx-delay: 5s; }
|
|
162
|
+
.gx-tag--3 { left: calc((100% - var(--gx-w)) * .3); --gx-dy: 4px; --gx-delay: 10s; }
|
|
163
|
+
.gx-tag--4 { left: calc((100% - var(--gx-w)) * .84); --gx-dy: -6px; --gx-delay: 15s; }
|
|
164
|
+
.gx-tag--5 { left: calc((100% - var(--gx-w)) * .42); --gx-dy: -5px; --gx-delay: 20s; }
|
|
165
|
+
.gx-tag--6 { left: calc((100% - var(--gx-w)) * .72); --gx-dy: 7px; --gx-delay: 25s; }
|
|
166
|
+
|
|
167
|
+
/* The wet line: the window slides in from the left while the drawing inside
|
|
168
|
+
it slides the other way by the same amount, so the doodle stays put and is
|
|
169
|
+
uncovered left to right. The pen tip rides that edge, jittering up and
|
|
170
|
+
down as it scribbles. */
|
|
171
|
+
.gx-win, .gx-track { position: absolute; inset: -2px -3px; }
|
|
172
|
+
.gx-win { overflow: hidden; }
|
|
173
|
+
.gx-ink { position: absolute; inset: 0; padding: 2px 3px; }
|
|
174
|
+
.gx-svg { display: block; overflow: visible; }
|
|
175
|
+
.gx-win, .gx-ink, .gx-track {
|
|
176
|
+
animation-duration: 30s;
|
|
177
|
+
animation-timing-function: linear;
|
|
178
|
+
animation-iteration-count: infinite;
|
|
179
|
+
animation-fill-mode: both;
|
|
180
|
+
animation-delay: var(--gx-delay);
|
|
181
|
+
}
|
|
182
|
+
.gx-win { animation-name: gx-win; }
|
|
183
|
+
.gx-ink { animation-name: gx-ink; }
|
|
184
|
+
.gx-track { animation-name: gx-track; opacity: 0; }
|
|
185
|
+
/* The rest frame (and the reduced-motion one): three doodles, finished. */
|
|
186
|
+
.gx-tag:not(.gx-tag--1):not(.gx-tag--2):not(.gx-tag--4) .gx-win { opacity: 0; }
|
|
187
|
+
|
|
188
|
+
@keyframes gx-win {
|
|
189
|
+
0% { transform: translateX(-100%); opacity: 1; }
|
|
190
|
+
4% { transform: translateX(0); }
|
|
191
|
+
28% { opacity: 1; }
|
|
192
|
+
32%, 100% { transform: translateX(0); opacity: 0; }
|
|
193
|
+
}
|
|
194
|
+
@keyframes gx-ink {
|
|
195
|
+
0% { transform: translateX(100%); }
|
|
196
|
+
4%, 100% { transform: translateX(0); }
|
|
197
|
+
}
|
|
198
|
+
/* The pen waits unseen: a doodle not yet begun shows no tip. */
|
|
199
|
+
@keyframes gx-track {
|
|
200
|
+
0% { transform: translateX(-100%); opacity: 0; }
|
|
201
|
+
.3% { opacity: 1; }
|
|
202
|
+
4% { transform: translateX(0); opacity: 1; }
|
|
203
|
+
5%, 100% { transform: translateX(0); opacity: 0; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.gx-nib {
|
|
207
|
+
position: absolute;
|
|
208
|
+
right: -1.5px;
|
|
209
|
+
top: calc(50% - 1.5px);
|
|
210
|
+
width: 3px;
|
|
211
|
+
height: 3px;
|
|
212
|
+
border-radius: 999px;
|
|
213
|
+
background: var(--ink);
|
|
214
|
+
animation: gx-scribble .16s ease-in-out infinite alternate;
|
|
215
|
+
}
|
|
216
|
+
@keyframes gx-scribble {
|
|
217
|
+
from { transform: translateY(-7px); }
|
|
218
|
+
to { transform: translateY(7px); }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* The ink: hairlines and dots, black on the light bar and white on the dark
|
|
222
|
+
one, with the sky's own ladder for the fainter marks. */
|
|
223
|
+
.gx-ln { fill: none; stroke: var(--ink); stroke-width: 1.3px; stroke-linecap: round; stroke-linejoin: round; }
|
|
224
|
+
.gx-ln--soft { stroke: color-mix(in srgb, var(--ink) 55%, transparent); }
|
|
225
|
+
.gx-ln--dash { stroke: color-mix(in srgb, var(--ink) 55%, transparent); stroke-width: 1px; stroke-dasharray: 2 2.5; }
|
|
226
|
+
.gx-pt { fill: var(--ink); }
|
|
227
|
+
.gx-halo { fill: none; stroke: color-mix(in srgb, var(--ink) 28%, transparent); stroke-width: 1px; }
|