@bongos/core 1.21.163 → 1.21.165

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.bongos-core.json +60 -45
  2. package/.gitattributes +6 -0
  3. package/docs/copy-inventory.md +127 -127
  4. package/docs/copy-registry.json +205 -185
  5. package/docs/module-api-changelog.md +4 -0
  6. package/docs/page-readings.json +297 -462
  7. package/modules/copy-desk/page-status.js +3 -0
  8. package/modules/copy-desk/pages.js +5 -0
  9. package/modules/hall-ui/public/approval-queue-lib.js +4 -0
  10. package/modules/hall-ui/public/approval-queue.css +2 -0
  11. package/modules/hall-ui/public/approval-queue.js +1 -0
  12. package/modules/hall-ui/public/builders.js +6 -4
  13. package/modules/hall-ui/public/craft-mode.js +32 -158
  14. package/modules/hall-ui/public/craft-review.css +7 -120
  15. package/modules/hall-ui/public/goal-inbox.js +100 -7
  16. package/modules/hall-ui/public/goals.js +2 -4
  17. package/modules/hall-ui/public/hall-render.js +39 -9
  18. package/modules/hall-ui/public/hall-widgets.js +20 -10
  19. package/modules/hall-ui/public/index.html +7 -0
  20. package/modules/hall-ui/public/page-notes.css +64 -1
  21. package/modules/hall-ui/public/page-notes.js +96 -11
  22. package/modules/hall-ui/public/style.css +95 -0
  23. package/modules/hall-ui/public/task-detail.js +8 -0
  24. package/modules/hall-ui/records/home-reshuffle.md +31 -0
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/release-notes.json +20 -0
  28. package/scripts/gds/copy-apply.js +65 -2
  29. package/scripts/gds/git-merge-regen.js +14 -0
  30. package/scripts/gds/page-reader.js +46 -0
  31. package/src/module-api.js +1 -1
  32. package/tests/conflict_resolve.mjs +6 -0
  33. package/tests/craft_review_mode.mjs +21 -104
  34. package/tests/hall_approval_queue.mjs +16 -0
  35. package/tests/hall_board_world.mjs +7 -6
  36. package/tests/home_attention_cards.mjs +119 -0
  37. package/tests/home_bounded_reads.mjs +1 -1
  38. package/tests/page_notes_panel.mjs +161 -27
  39. package/tests/promote_goal_id.mjs +8 -0
  40. package/tests/tweak_batch_apply.mjs +40 -0
  41. package/tests/tweak_land_heal.mjs +139 -0
@@ -691,6 +691,8 @@ function approvalFor({ page, pageId, rounds, round, builderId, isArtist }) {
691
691
  const applied = pages.parseAppliedBlock(round.description);
692
692
  const refusedBy = new Map();
693
693
  if (applied.ok) for (const r of applied.applied.refused || []) if (r && r.key) refusedBy.set(String(r.key), String(r.code || 'refused'));
694
+ const pinnedBy = new Map();
695
+ if (applied.ok) for (const p of applied.applied.pinned_tests || []) if (p && p.key && Array.isArray(p.tests)) pinnedBy.set(String(p.key), p.tests.map(String));
694
696
  const lines = batch.ok ? batch.batch.lines.map((l) => ({
695
697
  key: l.key,
696
698
  section: l.section == null ? null : l.section,
@@ -698,6 +700,7 @@ function approvalFor({ page, pageId, rounds, round, builderId, isArtist }) {
698
700
  after: l.after,
699
701
  placement: l.placement || null,
700
702
  refused: refusedBy.get(String(l.key)) || null,
703
+ ...(pinnedBy.has(String(l.key)) ? { pinned_by: pinnedBy.get(String(l.key)) } : {}),
701
704
  })) : [];
702
705
  const shipped = (rounds || []).filter((r) => r.status === 'shipped');
703
706
  const now = countsFor(rounds);
@@ -164,6 +164,11 @@ function composeAppliedBlock(a) {
164
164
  reading_hash_after: a.reading_hash_after,
165
165
  lines_after: (a.lines_after || []).map(String),
166
166
  refused: (a.refused || []).map((x) => ({ key: x.key, code: x.code })),
167
+ // task 1004775: tests that still assert a rewritten line's old words, named
168
+ // at apply time so the artist sees them before approving, not CI after.
169
+ ...(Array.isArray(a.pinned_tests) && a.pinned_tests.length
170
+ ? { pinned_tests: a.pinned_tests.map((p) => ({ key: p.key, tests: (p.tests || []).map(String) })) }
171
+ : {}),
167
172
  });
168
173
  }
169
174
 
@@ -119,6 +119,10 @@
119
119
  marked: after && !line.refused,
120
120
  aside: line.refused ? refusedWords(line.refused) : (after ? `was: ${line.before}` : `becomes: ${line.after}`),
121
121
  refused: !!line.refused,
122
+ // task 1004775: a test still asserts the old words, so it fails once this lands.
123
+ ...(!line.refused && Array.isArray(line.pinned_by) && line.pinned_by.length
124
+ ? { pinned: `A test still checks the old words: ${line.pinned_by.join(', ')}. It needs updating when this ships.` }
125
+ : {}),
122
126
  };
123
127
  }
124
128
 
@@ -226,6 +226,8 @@ html.aq-open { overflow: hidden; }
226
226
  }
227
227
  .aq-line__aside { font-size: 13px; line-height: 1.45; color: var(--aq-ink-faint); overflow-wrap: anywhere; }
228
228
  .aq-line--refused .aq-line__aside { color: var(--aq-link); }
229
+ /* task 1004775: a test that still checks the old words. */
230
+ .aq-line__pinned { font-size: 13px; line-height: 1.45; color: var(--aq-link); overflow-wrap: anywhere; }
229
231
  .aq-lines__none { margin: 0; font-size: 14px; color: var(--aq-ink-faint); }
230
232
 
231
233
  /* The decision row. */
@@ -152,6 +152,7 @@
152
152
  ${r.section ? `<span class="aq-line__section">${esc(r.section)}</span>` : ''}
153
153
  <span class="aq-line__text">${r.marked ? `<mark class="aq-mark">${words(r.text)}</mark>` : words(r.text)}</span>
154
154
  <span class="aq-line__aside">${words(r.aside)}</span>
155
+ ${r.pinned ? `<span class="aq-line__pinned">${esc(r.pinned)}</span>` : ''}
155
156
  </li>`).join('')}</ol>`;
156
157
  }
157
158
 
@@ -929,7 +929,7 @@
929
929
 
930
930
  // ---- Home "Next up" slice (task 2184) -------------------------------------
931
931
 
932
- // Fill the top of the "Your work" card with the first two rows of the
932
+ // Fill the top of the "Your work" card with the first five rows of the
933
933
  // claimable queue (server order: priority, then calibrated estimate) so Home
934
934
  // answers "what's my next action". Best-effort: a primer-gated 403, an auth
935
935
  // hiccup, or an empty queue hides the slice — Home never shows an error for
@@ -947,7 +947,8 @@
947
947
  // already orders by priority then calibrated estimate, so the first two rows ARE
948
948
  // the top two. `claimable_total` comes back so the foot can say how many are
949
949
  // waiting instead of leaving two rows to imply there are only two.
950
- const resp = await getJSON(`${API}/tasks/claimable?limit=2`, { softAuth: true });
950
+ // task 1004774: five rows, level with the five Recently shipped rows beside it.
951
+ const resp = await getJSON(`${API}/tasks/claimable?limit=5`, { softAuth: true });
951
952
  // task 1003464: the greeting's "N tasks are claimable right now" — rank-scoped,
952
953
  // so it is THIS builder's number. It reads claimable_total rather than the length
953
954
  // of a fully-downloaded feed: the same number, without the wire cost that this
@@ -959,7 +960,8 @@
959
960
  ? claimableTotal
960
961
  : (resp && Array.isArray(resp.claimable) ? resp.claimable.length : null);
961
962
  renderGreeting();
962
- const rows = resp && Array.isArray(resp.claimable) ? resp.claimable : [];
963
+ // capped as well as asked for: a server that ignores ?limit must not grow the card.
964
+ const rows = resp && Array.isArray(resp.claimable) ? resp.claimable.slice(0, 5) : [];
963
965
  if (!rows.length) { wrap.hidden = true; if (subhead) subhead.hidden = true; return; }
964
966
  const total = Number(resp.claimable_total);
965
967
  const cur = (window.__BRANDING__ && window.__BRANDING__.currency && window.__BRANDING__.currency.label) || 'credits';
@@ -1175,7 +1177,7 @@
1175
1177
  getJSON(`${API}/me/recent-ships?limit=all`),
1176
1178
  getJSON(`${API}/versions`),
1177
1179
  getJSON(`${API}/public/leaderboard`), // task 1004437: the list with each builder's roles
1178
- getJSON(`${API}/public/progress`), // task 1003464: "<version> is N percent shipped"
1180
+ getJSON(`${API}/public/progress`), // task 1003464: "<version> is N percent complete"
1179
1181
  ]);
1180
1182
 
1181
1183
  if (progress.status === 'fulfilled') {
@@ -34,7 +34,6 @@
34
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"/>',
35
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"/>',
36
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"/>',
38
37
  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"/>',
39
38
  };
40
39
  // The four crafts. `short` is the switch's label; `line` is the
@@ -342,8 +341,9 @@
342
341
  artistry: [
343
342
  // Named "Artist review" since task 1004499: the public site's button
344
343
  // (public-landing assets/artist-review.js) opens this same panel.
345
- // `menu`: hovering it opens the review-mode menu (task 1004686, below).
346
- { id: 'palette', icon: 'artistry', short: 'Review', label: 'Artist review: write feedback on this page', script: 'page-notes.js', api: 'OTBPageNotes', menu: true },
344
+ // `docked` (task 1004771): no top-bar button. The panel itself sits on
345
+ // the right edge behind a small tab, and carries the review-mode choice.
346
+ { id: 'palette', icon: 'artistry', short: 'Review', label: 'Artist review: write feedback on this page', script: 'page-notes.js', api: 'OTBPageNotes', docked: true },
347
347
  // Task 1004497: this page's words, edited where they stand as a proposed wording change.
348
348
  { id: 'paintbrush', icon: 'brush', short: 'Edit words', label: 'Edit this page\'s words', script: 'paintbrush.js', api: 'OTBPaintbrush' },
349
349
  ],
@@ -393,21 +393,24 @@
393
393
  b.setAttribute('aria-label', t.label);
394
394
  b.title = t.label;
395
395
  b.innerHTML = icon(t.icon, 'craft-tool__icon') + '<span class="craft-tool__label">' + esc(t.short) + '</span>';
396
- b.addEventListener('click', function (e) {
397
- // A phone has no hover, so there a tap on Review opens its menu instead.
398
- if (t.menu && narrow()) { toggleReviewMenu(e && e.detail === 0); return; }
399
- openTool(t, b);
396
+ b.addEventListener('click', function () { openTool(t, b); });
397
+ return b;
398
+ }
399
+ function toolNode(t) { return document.getElementById('craft-tool-' + t.id); }
400
+ // A docked tool has no button: in its mode the panel is docked at the edge,
401
+ // out of it the panel goes. Only a tool already loaded needs closing.
402
+ function applyDocked(mode) {
403
+ var want = toolsFor(mode);
404
+ for (var m in TOOLS) TOOLS[m].forEach(function (t) {
405
+ if (!t.docked) return;
406
+ if (want.indexOf(t) !== -1) loadTool(t).then(function (api) { if (api.dock) api.dock(); }, function () { /* the page still works */ });
407
+ else if (window[t.api] && window[t.api].close) window[t.api].close();
400
408
  });
401
- return t.menu ? reviewGroup(t, b) : b;
402
- }
403
- // The node a tool occupies in the bar: its button, or Review's whole group.
404
- function toolNode(t) {
405
- var b = document.getElementById('craft-tool-' + t.id);
406
- return b && t.menu && b.parentNode && b.parentNode.id === REVIEW_GROUP ? b.parentNode : b;
407
409
  }
408
410
  function applyTools(mode) {
409
411
  var sw = document.getElementById('craft-switch');
410
- var own = toolsFor(mode);
412
+ applyDocked(mode);
413
+ var own = toolsFor(mode).filter(function (t) { return !t.docked; });
411
414
  var old = document.querySelectorAll ? document.querySelectorAll('[data-craft-tool]') : [];
412
415
  for (var i = old.length - 1; i >= 0; i--) {
413
416
  if (own.some(function (t) { return t.id === old[i].getAttribute('data-craft-tool'); })) continue;
@@ -429,26 +432,19 @@
429
432
  var el = document.getElementById('craft-tool-' + t.id);
430
433
  if (el && el.classList) el.classList.toggle('craft-tool--guest', !!review && TOOLS.artistry.indexOf(t) === -1);
431
434
  });
432
- var on = document.getElementById('craft-tool-palette');
433
- if (on) {
434
- on.classList.toggle('craft-tool--reviewing', !!review);
435
- var word = on.querySelector && on.querySelector('.craft-tool__label');
436
- if (word) word.textContent = review ? 'Reviewing' : 'Review';
437
- }
438
435
  }
439
436
 
440
437
  // ---------- artist review mode (task 1004686) ----------
441
- // Hovering Artistry's Review button opens a menu of the four experiences
442
- // (owner, 2026-10-09, in the owner's order and words). Picking one makes the hall
438
+ // The review panel's Review mode row (page-notes.js, task 1004771) offers the four
439
+ // experiences (owner, 2026-10-09, in the owner's order and words). Picking one makes the hall
443
440
  // wear that mode on every page — its tabs, its look, its tools — the way a
444
441
  // builder in it sees it, while the artist keeps Artistry's tools and small
445
442
  // sky-style doodles are scribbled into the top bar's empty space (craft-review.js draws them,
446
- // loaded only in review mode; craft-review.css styles it and the menu).
443
+ // loaded only in review mode; craft-review.css styles it).
447
444
  // The artist's own mode stays Artistry: the experience is this session's
448
445
  // alone (sessionStorage), and choosing any mode from the switch or the
449
446
  // picker ends it. Cosmetic like the mode: a tab the gate hides stays hidden.
450
447
  var REVIEW_KEY = 'hall-craft-review';
451
- var REVIEW_GROUP = 'craft-review';
452
448
  var EXPERIENCES = [
453
449
  { id: 'ideating', label: 'Ideator experience' },
454
450
  { id: 'governing', label: 'Governing experience' },
@@ -478,138 +474,6 @@
478
474
  apply('artistry');
479
475
  }
480
476
 
481
- // The menu. Hover opens it where a pointer can hover; the chevron (or, on a
482
- // phone, Review itself) opens it by click, tap or keyboard. Built on every
483
- // open, so the checked experience is always the live one.
484
- var closeTimer = null;
485
- // Hover is a mouse's alone: a touch also fires pointer events, and a laptop
486
- // with a touch screen can report no hover at all, so ask the pointer itself.
487
- function byMouse(e) { return !!e && e.pointerType === 'mouse'; }
488
- function reviewMenuHtml() {
489
- var items = '<button class="craft-review__item" type="button" role="menuitem" data-review-act="write">' +
490
- icon('write', 'craft-review__icon') + '<span>Write feedback on this page</span></button>' +
491
- '<p class="craft-review__head" role="presentation">Review mode</p>';
492
- items += EXPERIENCES.map(function (x) {
493
- var on = review === x.id;
494
- return '<button class="craft-review__item" type="button" role="menuitemradio" aria-checked="' + (on ? 'true' : 'false') + '" data-review-pick="' + x.id + '">' +
495
- icon(x.id, 'craft-review__icon') + '<span>' + esc(x.label) + '</span>' +
496
- (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>' : '') +
497
- '</button>';
498
- }).join('');
499
- if (review) {
500
- items += '<button class="craft-review__item craft-review__item--stop" type="button" role="menuitem" data-review-act="stop">' +
501
- '<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>';
502
- }
503
- return items;
504
- }
505
- function menuEl() { return document.getElementById(REVIEW_GROUP + '-menu'); }
506
- function menuItems() {
507
- var m = menuEl();
508
- return m && m.querySelectorAll ? Array.prototype.slice.call(m.querySelectorAll('[role^="menuitem"]')) : [];
509
- }
510
- function openReviewMenu(focusFirst) {
511
- var m = menuEl(), more = document.getElementById(REVIEW_GROUP + '-more');
512
- if (!m) return;
513
- clearTimeout(closeTimer);
514
- if (m.hasAttribute('hidden')) {
515
- m.innerHTML = reviewMenuHtml();
516
- m.removeAttribute('hidden');
517
- if (more) more.setAttribute('aria-expanded', 'true');
518
- document.addEventListener('click', onOutside, true);
519
- }
520
- if (focusFirst) {
521
- var first = menuItems()[0];
522
- if (first && first.focus) first.focus();
523
- }
524
- }
525
- function closeReviewMenu(refocus) {
526
- var m = menuEl(), more = document.getElementById(REVIEW_GROUP + '-more');
527
- clearTimeout(closeTimer);
528
- document.removeEventListener('click', onOutside, true);
529
- if (!m || m.hasAttribute('hidden')) return;
530
- m.setAttribute('hidden', '');
531
- if (more) more.setAttribute('aria-expanded', 'false');
532
- if (refocus) {
533
- var back = narrow() ? document.getElementById('craft-tool-palette') : more;
534
- if (back && back.focus) back.focus();
535
- }
536
- }
537
- function toggleReviewMenu(focusFirst) {
538
- var m = menuEl();
539
- if (m && !m.hasAttribute('hidden')) closeReviewMenu(false); else openReviewMenu(focusFirst);
540
- }
541
- function onOutside(e) {
542
- var g = document.getElementById(REVIEW_GROUP);
543
- if (g && g.contains && !g.contains(e.target)) closeReviewMenu(false);
544
- }
545
- function onMenuClick(e) {
546
- var btn = e.target.closest && e.target.closest('button');
547
- if (!btn) return;
548
- var pick = btn.getAttribute('data-review-pick');
549
- var act = btn.getAttribute('data-review-act');
550
- if (!pick && !act) return;
551
- closeReviewMenu(false);
552
- if (pick) startReview(pick);
553
- else if (act === 'stop') stopReview();
554
- else if (act === 'write') openTool(toolFor('palette'), document.getElementById('craft-tool-palette'));
555
- }
556
- // The menu keys: arrows move, Home and End jump, Escape closes back to the
557
- // button that opened it, Tab closes and lets focus move on.
558
- function onMenuKey(e) {
559
- var items = menuItems();
560
- var at = items.indexOf(document.activeElement);
561
- var to = null;
562
- if (e.key === 'ArrowDown') to = (at + 1) % items.length;
563
- else if (e.key === 'ArrowUp') to = (at - 1 + items.length) % items.length;
564
- else if (e.key === 'Home') to = 0;
565
- else if (e.key === 'End') to = items.length - 1;
566
- else if (e.key === 'Escape') { e.preventDefault(); closeReviewMenu(true); return; }
567
- else if (e.key === 'Tab') { closeReviewMenu(false); return; }
568
- if (to === null || !items[to]) return;
569
- e.preventDefault();
570
- items[to].focus();
571
- }
572
- function reviewGroup(t, b) {
573
- var g = document.createElement('span');
574
- g.id = REVIEW_GROUP;
575
- g.className = 'craft-review';
576
- var more = document.createElement('button');
577
- more.type = 'button';
578
- more.id = REVIEW_GROUP + '-more';
579
- more.className = 'craft-review__more';
580
- more.setAttribute('aria-haspopup', 'menu');
581
- more.setAttribute('aria-expanded', 'false');
582
- more.setAttribute('aria-controls', REVIEW_GROUP + '-menu');
583
- more.setAttribute('aria-label', 'Review mode: see the hall as another mode');
584
- more.title = 'Review mode: see the hall as another mode';
585
- 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>';
586
- more.addEventListener('click', function (e) { toggleReviewMenu(e && e.detail === 0); });
587
- more.addEventListener('keydown', function (e) {
588
- if (e.key === 'ArrowDown') { e.preventDefault(); openReviewMenu(true); }
589
- });
590
- var menu = document.createElement('div');
591
- menu.id = REVIEW_GROUP + '-menu';
592
- menu.className = 'craft-review__menu';
593
- menu.setAttribute('role', 'menu');
594
- menu.setAttribute('aria-label', 'Review mode');
595
- menu.setAttribute('hidden', '');
596
- menu.addEventListener('click', onMenuClick);
597
- menu.addEventListener('keydown', onMenuKey);
598
- b.addEventListener('keydown', function (e) {
599
- if (e.key === 'ArrowDown') { e.preventDefault(); openReviewMenu(true); }
600
- });
601
- g.addEventListener('pointerenter', function (e) { if (byMouse(e)) openReviewMenu(false); });
602
- g.addEventListener('pointerleave', function (e) {
603
- if (!byMouse(e)) return;
604
- clearTimeout(closeTimer);
605
- closeTimer = setTimeout(function () { closeReviewMenu(false); }, 220);
606
- });
607
- g.appendChild(b);
608
- g.appendChild(more);
609
- g.appendChild(menu);
610
- return g;
611
- }
612
-
613
477
  // The scribble wall: the top bar's empty space, doodled on while reviewing.
614
478
  // craft-review.js draws it (decorative, aria-hidden); every motion is CSS,
615
479
  // so the reduced-motion kill switch leaves three finished doodles standing.
@@ -637,7 +501,7 @@
637
501
  loadTool(wallTool).then(function (api) {
638
502
  var live = document.getElementById(WALL);
639
503
  if (live === wall && review === mode) wall.innerHTML = api.wallHtml(mode);
640
- }, function () { /* a bare wall; the menu still says you are reviewing */ });
504
+ }, function () { /* a bare wall; the review panel's mode row still says which */ });
641
505
  }
642
506
  // sessionStorage: this session's mode, or that the picker was already shown.
643
507
  var MODE_KEY = 'hall-craft';
@@ -703,7 +567,7 @@
703
567
  function choose(mode, keep) {
704
568
  mode = valid(mode);
705
569
  if (!mode || !offered(mode)) return;
706
- if (review) { drop('sessionStorage', REVIEW_KEY); review = null; closeReviewMenu(false); }
570
+ if (review) { drop('sessionStorage', REVIEW_KEY); review = null; }
707
571
  set('sessionStorage', MODE_KEY, mode);
708
572
  set('sessionStorage', ASKED_KEY, '1');
709
573
  if (keep === true) set('localStorage', KEEP_KEY, mode);
@@ -909,6 +773,16 @@
909
773
  review: startReview,
910
774
  stopReview: stopReview,
911
775
  EXPERIENCES: EXPERIENCES.map(function (x) { return x.id; }),
776
+ // The review panel's Review mode row (page-notes.js) draws the four
777
+ // experiences; `short` is the mode switch's own word for each, so the
778
+ // row and the switch never name one mode two ways.
779
+ experiences: function () {
780
+ return EXPERIENCES.map(function (x) {
781
+ var m = MODES.filter(function (y) { return y.id === x.id; })[0];
782
+ return { id: x.id, label: x.label, short: m ? m.short : x.label };
783
+ });
784
+ },
785
+ iconHtml: icon,
912
786
  open: openPicker,
913
787
  ALWAYS_TABS: ALWAYS_TABS.slice(),
914
788
  tabsFor: tabsFor,
@@ -1,10 +1,10 @@
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.
1
+ /* craft-review.css — artist review mode (task 1004686; craft-review.js draws
2
+ the wall). Imported by style.css, beside page-notes.css.
3
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.
4
+ The experience under review is picked in the review panel's own Review mode
5
+ row (page-notes.js, task 1004771); the top bar has no Review button any more.
6
+ While an artist reviews one, small black-and-white doodles in the sky's hand
7
+ are scribbled into the top bar's empty space.
8
8
 
9
9
  Every colour is a token (tests/hall_audit.mjs holds that). Every motion is
10
10
  CSS on transform and opacity, and the hall's reduced-motion kill switch
@@ -13,126 +13,13 @@
13
13
  An @import lands BEFORE style.css's own rules, so a rule here that restyles
14
14
  .craft-tool must out-rank it (two classes), or style.css silently wins. */
15
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
16
  /* Under 360px the bar holds two tools, not three: reviewing Ideating, its
130
17
  own Ideas button folds away (craft-mode.js marks it a guest). */
131
18
  @media (max-width: 359px) { .craft-tool.craft-tool--guest { display: none; } }
132
19
 
133
20
  /* ---------- the scribble wall ---------- */
134
21
  /* 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". */
22
+ is left (a phone), the review panel's mode row still says which. */
136
23
  .craft-graffiti {
137
24
  position: relative;
138
25
  flex: 1 1 0;