@bongos/core 1.21.164 → 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.
@@ -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;
@@ -16,7 +16,9 @@
16
16
  --pn-glass: var(--glass, rgba(255, 255, 255, .08));
17
17
  }
18
18
 
19
- /* The page feedback doc: a sheet from the right over a scrim. */
19
+ /* A sheet from the right over a scrim: the lightbulb's panel (lightbulb.js), and
20
+ the feedback doc wherever it is OPENED rather than docked (the public site's
21
+ Artist review button, artist-review.js). */
20
22
  html.pn-open { overflow: hidden; }
21
23
  .pn {
22
24
  position: fixed;
@@ -26,6 +28,67 @@ html.pn-open { overflow: hidden; }
26
28
  justify-content: flex-end;
27
29
  background: var(--pn-scrim);
28
30
  }
31
+ /* The page feedback doc (.pn--dock, task 1004771): docked on the right with no
32
+ scrim and no scroll lock, so the reviewer keeps scrolling the page while
33
+ writing about it. A small tab on its left edge slides it away and back
34
+ (.pn--collapsed). */
35
+ .pn.pn--dock {
36
+ inset: 0 0 0 auto;
37
+ display: block;
38
+ width: min(560px, calc(100% - 36px));
39
+ background: none;
40
+ transition: transform .25s ease;
41
+ }
42
+ .pn--collapsed { transform: translateX(100%); }
43
+ /* Tucked away, the panel's controls leave the tab order once it has slid out. */
44
+ .pn--collapsed .pn__panel { visibility: hidden; transition: visibility 0s .25s; }
45
+ @media (prefers-reduced-motion: reduce) { .pn.pn--dock { transition: none; } }
46
+ .pn__tab {
47
+ position: absolute;
48
+ top: 50%;
49
+ right: 100%;
50
+ transform: translateY(-50%);
51
+ display: inline-flex; align-items: center; justify-content: center;
52
+ flex-direction: column; gap: 10px;
53
+ width: 36px; height: 96px; padding: 0;
54
+ border: 0;
55
+ border-radius: var(--radius-sm, 8px) 0 0 var(--radius-sm, 8px);
56
+ background: var(--bg-card);
57
+ color: var(--craft, var(--ink));
58
+ box-shadow: 0 0 0 1px var(--craft, var(--rule)), var(--pn-shadow-pop);
59
+ cursor: pointer;
60
+ }
61
+ .pn__tab:hover { background: var(--pn-glass); }
62
+ .pn__tab:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
63
+ .pn__tab-icon { width: 18px; height: 18px; }
64
+ .pn__tab-chev { width: 12px; height: 12px; transition: transform .25s ease; }
65
+ .pn--collapsed .pn__tab-chev { transform: rotate(180deg); }
66
+ @media (prefers-reduced-motion: reduce) { .pn__tab-chev { transition: none; } }
67
+ /* The Review mode row: which experience the hall wears while reviewed. */
68
+ .pn__modes { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 12px; }
69
+ .pn__modes-h {
70
+ width: 100%;
71
+ font-family: var(--label-face);
72
+ font-size: 11px;
73
+ letter-spacing: .12em;
74
+ text-transform: uppercase;
75
+ color: var(--ink-faint);
76
+ }
77
+ .pn__mode {
78
+ display: inline-flex; align-items: center; gap: 6px;
79
+ min-height: 30px; padding: 0 12px;
80
+ border: 1px solid var(--rule);
81
+ border-radius: var(--radius-pill);
82
+ background: none;
83
+ color: var(--ink-soft);
84
+ font: inherit; font-size: 13px;
85
+ cursor: pointer;
86
+ }
87
+ .pn__mode:hover { color: var(--ink); background: var(--pn-glass); }
88
+ .pn__mode[aria-pressed="true"] { color: var(--ink); border-color: var(--craft, var(--ink)); background: var(--pn-glass); }
89
+ .pn__mode:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
90
+ .pn__mode-icon { width: 14px; height: 14px; }
91
+ .pn__mode--stop { border-style: dashed; }
29
92
  .pn__panel {
30
93
  position: relative;
31
94
  width: min(560px, 100%);
@@ -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 '<div class="pn__panel" role="dialog" aria-modal="true" aria-labelledby="pn-title">' +
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-close')) close();
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
- function mountSheet() {
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
- returnFocus = document.activeElement;
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
- openNote: function (id) { if (mountSheet()) refreshList().then(function () { return showNote(id); }); },
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,