@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.
- package/.bongos-core.json +19 -19
- package/docs/copy-inventory.md +30 -30
- package/docs/copy-registry.json +58 -48
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +199 -199
- package/modules/hall-ui/public/craft-mode.js +32 -158
- package/modules/hall-ui/public/craft-review.css +7 -120
- package/modules/hall-ui/public/page-notes.css +64 -1
- package/modules/hall-ui/public/page-notes.js +96 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/craft_review_mode.mjs +21 -104
- package/tests/page_notes_panel.mjs +161 -27
|
@@ -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
|
-
// `
|
|
346
|
-
|
|
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 (
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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;
|
|
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-
|
|
2
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
/*
|
|
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
|
|
133
|
+
return (docked
|
|
134
|
+
? '<button class="pn__tab" type="button" data-pn-tab aria-expanded="true" aria-controls="pn-panel" aria-label="Hide the review panel">' +
|
|
135
|
+
PALETTE_SVG +
|
|
136
|
+
'<svg class="pn__tab-chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg></button>' +
|
|
137
|
+
'<div class="pn__panel" id="pn-panel" role="region" aria-labelledby="pn-title">'
|
|
138
|
+
: '<div class="pn__panel" role="dialog" aria-modal="true" aria-labelledby="pn-title">') +
|
|
106
139
|
'<div class="pn__head">' +
|
|
107
140
|
'<p class="pn__eyebrow">Artist review · ' + esc(p.title) + '</p>' +
|
|
108
141
|
'<h2 class="pn__title" id="pn-title">What should this page be?</h2>' +
|
|
142
|
+
'<div id="pn-modes">' + modesHtml() + '</div>' +
|
|
109
143
|
'<button class="pn__close" type="button" data-pn-close aria-label="Close">' +
|
|
110
144
|
'<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M5 5l8 8M13 5l-8 8"/></svg></button>' +
|
|
111
145
|
'</div>' +
|
|
@@ -285,10 +319,18 @@
|
|
|
285
319
|
}
|
|
286
320
|
|
|
287
321
|
function onClick(e) {
|
|
288
|
-
if (e.target === sheet) { close(); return; }
|
|
322
|
+
if (!docked && e.target === sheet) { close(); return; }
|
|
289
323
|
var btn = e.target.closest && e.target.closest('button');
|
|
290
324
|
if (!btn) return;
|
|
291
|
-
if (btn.hasAttribute('data-pn-
|
|
325
|
+
if (btn.hasAttribute('data-pn-tab')) toggle();
|
|
326
|
+
else if (btn.hasAttribute('data-pn-mode')) {
|
|
327
|
+
var c = craft(), id = btn.getAttribute('data-pn-mode');
|
|
328
|
+
if (c) { if (id) c.review(id); else c.stopReview(); }
|
|
329
|
+
paintModes();
|
|
330
|
+
}
|
|
331
|
+
// Docked, the X tucks the panel away like Escape: removing it would take
|
|
332
|
+
// the tab, the reviewer's only way back in, with it.
|
|
333
|
+
else if (btn.hasAttribute('data-pn-close')) { if (docked) toggle(true); else close(); }
|
|
292
334
|
else if (btn.hasAttribute('data-pn-submit')) submit();
|
|
293
335
|
else if (btn.hasAttribute('data-pn-note')) showNote(btn.getAttribute('data-pn-note'));
|
|
294
336
|
else if (btn.hasAttribute('data-pn-back') || btn.hasAttribute('data-pn-new')) {
|
|
@@ -298,6 +340,13 @@
|
|
|
298
340
|
}
|
|
299
341
|
function onKey(e) {
|
|
300
342
|
if (!sheet) return;
|
|
343
|
+
// Docked, the panel is not modal: the page behind it stays scrollable and
|
|
344
|
+
// reachable, and Escape inside it tucks it away behind its tab, draft kept.
|
|
345
|
+
if (docked) {
|
|
346
|
+
if (e.key === 'Escape' && sheet.contains(document.activeElement)) { e.preventDefault(); toggle(true); }
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Opened, it is the modal sheet: Escape closes it, Tab stays inside it.
|
|
301
350
|
if (e.key === 'Escape') { e.preventDefault(); close(); return; }
|
|
302
351
|
if (e.key !== 'Tab') return;
|
|
303
352
|
var f = Array.prototype.slice.call(sheet.querySelectorAll('button, textarea, input, a[href]')).filter(function (x) { return !x.disabled; });
|
|
@@ -305,20 +354,46 @@
|
|
|
305
354
|
if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
|
|
306
355
|
else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
|
|
307
356
|
}
|
|
308
|
-
|
|
357
|
+
// Slide the panel off the right edge, leaving only its tab, or bring it back.
|
|
358
|
+
// `quiet` (docking) moves no focus. A docked panel opens its draft only
|
|
359
|
+
// the first time it is shown, so artist mode costs no request until then.
|
|
360
|
+
function toggle(collapse, quiet) {
|
|
361
|
+
if (!sheet) return;
|
|
362
|
+
var on = typeof collapse === 'boolean' ? collapse : !sheet.classList.contains('pn--collapsed');
|
|
363
|
+
sheet.classList.toggle('pn--collapsed', on);
|
|
364
|
+
var tab = sheet.querySelector('[data-pn-tab]');
|
|
365
|
+
if (tab) {
|
|
366
|
+
tab.setAttribute('aria-expanded', on ? 'false' : 'true');
|
|
367
|
+
tab.setAttribute('aria-label', on ? 'Show the review panel' : 'Hide the review panel');
|
|
368
|
+
if (on && !quiet) tab.focus();
|
|
369
|
+
}
|
|
370
|
+
if (on) save();
|
|
371
|
+
else if (state.pending) { state.pending = false; openDraft(); }
|
|
372
|
+
}
|
|
373
|
+
// `dock` mounts the docked shape quietly (no focus moved); otherwise the modal sheet.
|
|
374
|
+
function mountSheet(dock) {
|
|
309
375
|
if (sheet) return false;
|
|
310
|
-
|
|
376
|
+
docked = !!dock;
|
|
377
|
+
returnFocus = docked ? null : document.activeElement;
|
|
311
378
|
state = { note: null, images: [], list: [] };
|
|
312
379
|
sheet = document.createElement('div');
|
|
313
|
-
sheet.className = 'pn';
|
|
380
|
+
sheet.className = docked ? 'pn pn--dock' : 'pn';
|
|
314
381
|
sheet.id = 'page-notes';
|
|
315
382
|
sheet.innerHTML = shellHtml(page());
|
|
316
383
|
sheet.addEventListener('click', onClick);
|
|
384
|
+
if (docked) {
|
|
385
|
+
// A mouse peeks (owner, 2026-10-23): over the tab the panel opens, and the
|
|
386
|
+
// moment the pointer leaves the panel it tucks away again, draft kept.
|
|
387
|
+
// Touch and keyboard keep the tab's click, which a hover cannot replace.
|
|
388
|
+
sheet.querySelector('[data-pn-tab]').addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') toggle(false, true); });
|
|
389
|
+
sheet.addEventListener('pointerleave', function (e) { if (e.pointerType === 'mouse') toggle(true, true); });
|
|
390
|
+
} else {
|
|
391
|
+
document.documentElement.classList.add('pn-open');
|
|
392
|
+
}
|
|
317
393
|
document.addEventListener('keydown', onKey, true);
|
|
318
394
|
document.body.appendChild(sheet);
|
|
319
|
-
document.documentElement.classList.add('pn-open');
|
|
320
395
|
var c = sheet.querySelector('[data-pn-close]');
|
|
321
|
-
if (c) c.focus();
|
|
396
|
+
if (c && !docked) c.focus();
|
|
322
397
|
return true;
|
|
323
398
|
}
|
|
324
399
|
function close() {
|
|
@@ -328,14 +403,24 @@
|
|
|
328
403
|
document.removeEventListener('keydown', onKey, true);
|
|
329
404
|
if (sheet.parentNode) sheet.parentNode.removeChild(sheet);
|
|
330
405
|
sheet = null;
|
|
406
|
+
docked = false;
|
|
331
407
|
document.documentElement.classList.remove('pn-open');
|
|
332
408
|
if (returnFocus && returnFocus.focus) { try { returnFocus.focus(); } catch (_) { /* gone */ } }
|
|
333
409
|
returnFocus = null;
|
|
334
410
|
}
|
|
335
411
|
|
|
336
412
|
window.OTBPageNotes = {
|
|
337
|
-
open: function () { if (mountSheet()) openDraft(); },
|
|
338
|
-
|
|
413
|
+
open: function () { if (mountSheet()) openDraft(); else if (docked) toggle(false); },
|
|
414
|
+
// Artist mode (craft-mode.js, task 1004771): the panel waits at the right
|
|
415
|
+
// edge, tucked behind its tab; already there, it repaints its mode row.
|
|
416
|
+
dock: function () {
|
|
417
|
+
if (mountSheet(true)) { state.pending = true; toggle(true, true); }
|
|
418
|
+
else paintModes();
|
|
419
|
+
},
|
|
420
|
+
openNote: function (id) {
|
|
421
|
+
if (!mountSheet() && docked) { state.pending = false; toggle(false); }
|
|
422
|
+
refreshList().then(function () { return showNote(id); });
|
|
423
|
+
},
|
|
339
424
|
close: close,
|
|
340
425
|
configure: configure,
|
|
341
426
|
ideaHref: ideaHref,
|