@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.
- package/.bongos-core.json +60 -45
- package/.gitattributes +6 -0
- package/docs/copy-inventory.md +127 -127
- package/docs/copy-registry.json +205 -185
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +297 -462
- package/modules/copy-desk/page-status.js +3 -0
- package/modules/copy-desk/pages.js +5 -0
- package/modules/hall-ui/public/approval-queue-lib.js +4 -0
- package/modules/hall-ui/public/approval-queue.css +2 -0
- package/modules/hall-ui/public/approval-queue.js +1 -0
- package/modules/hall-ui/public/builders.js +6 -4
- 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/goal-inbox.js +100 -7
- package/modules/hall-ui/public/goals.js +2 -4
- package/modules/hall-ui/public/hall-render.js +39 -9
- package/modules/hall-ui/public/hall-widgets.js +20 -10
- package/modules/hall-ui/public/index.html +7 -0
- package/modules/hall-ui/public/page-notes.css +64 -1
- package/modules/hall-ui/public/page-notes.js +96 -11
- package/modules/hall-ui/public/style.css +95 -0
- package/modules/hall-ui/public/task-detail.js +8 -0
- package/modules/hall-ui/records/home-reshuffle.md +31 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/copy-apply.js +65 -2
- package/scripts/gds/git-merge-regen.js +14 -0
- package/scripts/gds/page-reader.js +46 -0
- package/src/module-api.js +1 -1
- package/tests/conflict_resolve.mjs +6 -0
- package/tests/craft_review_mode.mjs +21 -104
- package/tests/hall_approval_queue.mjs +16 -0
- package/tests/hall_board_world.mjs +7 -6
- package/tests/home_attention_cards.mjs +119 -0
- package/tests/home_bounded_reads.mjs +1 -1
- package/tests/page_notes_panel.mjs +161 -27
- package/tests/promote_goal_id.mjs +8 -0
- package/tests/tweak_batch_apply.mjs +40 -0
- package/tests/tweak_land_heal.mjs +139 -0
|
@@ -482,7 +482,7 @@
|
|
|
482
482
|
}
|
|
483
483
|
const shown = rows.slice(0, MAX_ROT_ROWS);
|
|
484
484
|
const more = rows.length > shown.length
|
|
485
|
-
? `<p class="ginbox__pending">…${rows.length - shown.length} more
|
|
485
|
+
? `<p class="ginbox__pending">…${rows.length - shown.length} more not moving.</p>`
|
|
486
486
|
: '';
|
|
487
487
|
// The heading names the TIMER, not just the count. A reader who disagrees with
|
|
488
488
|
// the line ("30 days is too short for a backlog") needs to know which number
|
|
@@ -493,7 +493,7 @@
|
|
|
493
493
|
const timerNote = t.invalid
|
|
494
494
|
? `<span class="ginbox__sub"> — ${escapeHtml(String(t.describe || ''))}</span>`
|
|
495
495
|
: `<span class="ginbox__sub"> — untouched ${escapeHtml(String(t.rot_days ?? '?'))}+ days</span>`;
|
|
496
|
-
box.innerHTML = '<h3 class="ginbox__sec-h">
|
|
496
|
+
box.innerHTML = '<h3 class="ginbox__sec-h">Things not moving '
|
|
497
497
|
+ `<span class="count-pill">${rows.length}</span>${timerNote}</h3>`
|
|
498
498
|
+ `<ul class="ginbox">${shown.map(rotRowHtml).join('')}</ul>${more}`;
|
|
499
499
|
window.OTBAttention?.refresh();
|
|
@@ -511,7 +511,7 @@
|
|
|
511
511
|
// this handler swallowed it into an empty section that looked like "nothing
|
|
512
512
|
// is rotting". A silent catch on the ONLY caller of a route is indistin-
|
|
513
513
|
// guishable from a healthy empty state — which is the whole failure.
|
|
514
|
-
console.warn('[hall] rot feed unavailable — the "
|
|
514
|
+
console.warn('[hall] rot feed unavailable — the "Things not moving" section is empty '
|
|
515
515
|
+ 'because the request failed, NOT because nothing is rotting:', err);
|
|
516
516
|
});
|
|
517
517
|
}
|
|
@@ -560,7 +560,7 @@
|
|
|
560
560
|
const more = total > rows.length
|
|
561
561
|
? `<p class="ginbox__pending">…${total - rows.length} more — <a href="/blockers">open the queue</a>.</p>`
|
|
562
562
|
: '';
|
|
563
|
-
box.innerHTML = '<h3 class="ginbox__sec-h">
|
|
563
|
+
box.innerHTML = '<h3 class="ginbox__sec-h">Requires project owner fix '
|
|
564
564
|
+ `<span class="count-pill">${total}</span></h3>`
|
|
565
565
|
+ `<ul class="ginbox">${rows.map(blockerRowHtml).join('')}</ul>${more}`;
|
|
566
566
|
window.OTBAttention?.refresh();
|
|
@@ -579,7 +579,7 @@
|
|
|
579
579
|
if (box) box.innerHTML = '';
|
|
580
580
|
window.OTBAttention?.refresh();
|
|
581
581
|
if (err.code !== 'auth-needed' && err.code !== 'not-found') {
|
|
582
|
-
console.warn('[hall] blockers feed unavailable — the "
|
|
582
|
+
console.warn('[hall] blockers feed unavailable — the "Requires project owner fix" section is empty '
|
|
583
583
|
+ 'because the request failed, NOT because nothing is open:', err);
|
|
584
584
|
}
|
|
585
585
|
});
|
|
@@ -595,6 +595,17 @@
|
|
|
595
595
|
host.hidden = true; // stays hidden until a slice finds something actionable
|
|
596
596
|
host.innerHTML =
|
|
597
597
|
'<h2 id="attention-h" class="scroll__h">Needs your attention<span class="count-pill" id="attention-count" hidden></span></h2>' +
|
|
598
|
+
// task 1004774: every slice is a CARD showing its total, and a click opens the
|
|
599
|
+
// slice itself in a window, so its buttons work exactly as they do inline.
|
|
600
|
+
// The slices still render into their divs below (kept off-screen in
|
|
601
|
+
// #attention-slices), and refresh() rebuilds the cards from what they hold.
|
|
602
|
+
'<div class="attn-cards" id="attention-cards"></div>' +
|
|
603
|
+
'<dialog class="attn-dialog" id="attention-dialog" aria-labelledby="attention-dialog-h">' +
|
|
604
|
+
'<div class="attn-dialog__head"><h3 class="attn-dialog__h" id="attention-dialog-h"></h3>' +
|
|
605
|
+
'<button type="button" class="attn-dialog__x" id="attention-dialog-x" aria-label="Close">×</button></div>' +
|
|
606
|
+
'<div class="attn-dialog__body" id="attention-dialog-body"></div>' +
|
|
607
|
+
'</dialog>' +
|
|
608
|
+
'<div id="attention-slices" hidden>' +
|
|
598
609
|
// task 1003464: the system's own notices — rebase owed, an input the
|
|
599
610
|
// system needs, the monthly budget — used to stack as banners INSIDE the
|
|
600
611
|
// Standing card. They are lines here now (rendered by hall-render.js
|
|
@@ -639,8 +650,10 @@
|
|
|
639
650
|
// the card on purpose: every slice above is somebody waiting on YOU right
|
|
640
651
|
// now, while rot is a standing question that has already waited weeks and
|
|
641
652
|
// can wait until they are read.
|
|
642
|
-
'<div id="attention-rot"></div>'
|
|
653
|
+
'<div id="attention-rot"></div>' +
|
|
654
|
+
'</div>';
|
|
643
655
|
host.addEventListener('click', onClick);
|
|
656
|
+
wireCards();
|
|
644
657
|
load();
|
|
645
658
|
loadNodQueue();
|
|
646
659
|
loadRemovals();
|
|
@@ -651,6 +664,83 @@
|
|
|
651
664
|
window.OTBProfileNudge?.load();
|
|
652
665
|
}
|
|
653
666
|
|
|
667
|
+
// ---- the cards + the window (task 1004774) --------------------------------
|
|
668
|
+
//
|
|
669
|
+
// Each slice div is one card. The card reads its title from the slice's own
|
|
670
|
+
// heading and its total from the heading's count pill (the server total, which
|
|
671
|
+
// can exceed the rows shown) or, failing that, the rows. Opening a card MOVES
|
|
672
|
+
// the slice div into the dialog — the dialog sits inside `host`, so the
|
|
673
|
+
// delegated click handler above still sees every button — and closing moves it
|
|
674
|
+
// back to where it came from.
|
|
675
|
+
const ITEM_SEL = '.ginbox__item, .triage-inbox__item';
|
|
676
|
+
let openSlice = null;
|
|
677
|
+
|
|
678
|
+
function sliceFacts(div) {
|
|
679
|
+
const h = div.querySelector('h3, .ginbox__sec-h, h4');
|
|
680
|
+
let title = '';
|
|
681
|
+
if (h) {
|
|
682
|
+
const copy = h.cloneNode(true);
|
|
683
|
+
copy.querySelectorAll('.count-pill, .ginbox__sub').forEach((n) => n.remove());
|
|
684
|
+
title = copy.textContent.replace(/\s+/g, ' ').trim();
|
|
685
|
+
}
|
|
686
|
+
const pill = h && h.querySelector('.count-pill');
|
|
687
|
+
const pillN = pill ? parseInt(pill.textContent, 10) : NaN;
|
|
688
|
+
const rows = div.querySelectorAll(ITEM_SEL).length;
|
|
689
|
+
const total = Number.isFinite(pillN) ? pillN : rows;
|
|
690
|
+
const sub = h && h.querySelector('.ginbox__sub');
|
|
691
|
+
return { title: title || 'Needs attention', total, rows, note: sub ? sub.textContent.replace(/^\s*—\s*/, '').trim() : '' };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function renderCards() {
|
|
695
|
+
const cards = host && host.querySelector('#attention-cards');
|
|
696
|
+
const slices = host && host.querySelector('#attention-slices');
|
|
697
|
+
if (!cards || !slices) return;
|
|
698
|
+
const html = [];
|
|
699
|
+
for (const div of slices.children) {
|
|
700
|
+
if (!div.id || !div.innerHTML.trim()) continue;
|
|
701
|
+
const f = sliceFacts(div);
|
|
702
|
+
if (!f.rows && !div.querySelector('.ginbox__pending')) continue;
|
|
703
|
+
html.push(`<button type="button" class="attn-card" data-attn-open="${escapeHtml(div.id)}">`
|
|
704
|
+
+ `<span class="attn-card__n">${escapeHtml(String(f.total))}</span>`
|
|
705
|
+
+ `<span class="attn-card__t">${escapeHtml(f.title)}</span>`
|
|
706
|
+
+ (f.note ? `<span class="attn-card__s">${escapeHtml(f.note)}</span>` : '')
|
|
707
|
+
+ '</button>');
|
|
708
|
+
}
|
|
709
|
+
cards.innerHTML = html.join('');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function openCard(id) {
|
|
713
|
+
const dlg = host.querySelector('#attention-dialog');
|
|
714
|
+
const div = host.querySelector(`#attention-slices > #${CSS.escape(id)}`);
|
|
715
|
+
if (!dlg || !div) return;
|
|
716
|
+
host.querySelector('#attention-dialog-h').textContent = sliceFacts(div).title;
|
|
717
|
+
host.querySelector('#attention-dialog-body').appendChild(div);
|
|
718
|
+
openSlice = div;
|
|
719
|
+
if (typeof dlg.showModal === 'function') dlg.showModal(); else dlg.setAttribute('open', '');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function closeCard() {
|
|
723
|
+
const dlg = host && host.querySelector('#attention-dialog');
|
|
724
|
+
if (openSlice) {
|
|
725
|
+
host.querySelector('#attention-slices').appendChild(openSlice);
|
|
726
|
+
openSlice = null;
|
|
727
|
+
}
|
|
728
|
+
if (dlg && dlg.open && typeof dlg.close === 'function') dlg.close();
|
|
729
|
+
renderCards();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function wireCards() {
|
|
733
|
+
host.addEventListener('click', (e) => {
|
|
734
|
+
const card = e.target.closest('[data-attn-open]');
|
|
735
|
+
if (card) { openCard(card.getAttribute('data-attn-open')); return; }
|
|
736
|
+
if (e.target.closest('#attention-dialog-x')) { closeCard(); return; }
|
|
737
|
+
// a click on the backdrop lands on the <dialog> itself
|
|
738
|
+
if (e.target.id === 'attention-dialog') closeCard();
|
|
739
|
+
});
|
|
740
|
+
const dlg = host.querySelector('#attention-dialog');
|
|
741
|
+
if (dlg) dlg.addEventListener('close', () => { if (openSlice) closeCard(); });
|
|
742
|
+
}
|
|
743
|
+
|
|
654
744
|
// The card's shared visibility contract: any slice renderer calls refresh()
|
|
655
745
|
// after painting; the card shows iff any slice has actionable rows, and the
|
|
656
746
|
// headline pill is the cross-slice total.
|
|
@@ -658,11 +748,14 @@
|
|
|
658
748
|
refresh() {
|
|
659
749
|
const sec = document.getElementById('attention-scroll');
|
|
660
750
|
if (!sec) return;
|
|
661
|
-
const n = sec.querySelectorAll(
|
|
751
|
+
const n = sec.querySelectorAll(ITEM_SEL).length;
|
|
662
752
|
const pending = sec.querySelector('.ginbox__pending');
|
|
663
753
|
const pill = document.getElementById('attention-count');
|
|
664
754
|
if (pill) { pill.hidden = !n; pill.textContent = String(n); }
|
|
665
755
|
sec.hidden = n === 0 && !pending;
|
|
756
|
+
// An open window keeps its slice; the card behind it is redrawn on close.
|
|
757
|
+
if (!openSlice) renderCards();
|
|
758
|
+
else if (!openSlice.querySelector(ITEM_SEL)) closeCard();
|
|
666
759
|
},
|
|
667
760
|
};
|
|
668
761
|
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// task 2184 (Home rule: a summary card never lists more than 5 rows): cap the
|
|
95
|
-
// joined-goals list
|
|
95
|
+
// joined-goals list. The foot is one "Browse goals" link (owner, task 1004774).
|
|
96
96
|
const MAX_HOME_ROWS = 5;
|
|
97
97
|
function render(joined, counts) {
|
|
98
98
|
const body = host.querySelector('#goals-body');
|
|
@@ -101,14 +101,12 @@
|
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
const shown = joined.slice(0, MAX_HOME_ROWS);
|
|
104
|
-
const more = joined.length - shown.length;
|
|
105
104
|
body.innerHTML = `<ul class="goal-mine-list">${shown.map((d) => row(d, counts)).join('')}</ul>` +
|
|
106
|
-
`<p class="goal-mine__foot card-foot"><a href="/goals"
|
|
105
|
+
`<p class="goal-mine__foot card-foot"><a href="/goals">Browse goals →</a></p>`;
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
const SKELETON =
|
|
110
109
|
'<h2 id="goals-h" class="scroll__h">Goals</h2>' +
|
|
111
|
-
'<p class="scroll__lede">The open goals you’ve joined, and the work you can claim in each. Browse, join, and leave goals — and read their criteria — on the <a href="/goals">Goals page</a>.</p>' +
|
|
112
110
|
'<div id="goals-body"><div class="profile__placeholder">Loading your goals…</div></div>';
|
|
113
111
|
|
|
114
112
|
async function load() {
|
|
@@ -638,15 +638,25 @@
|
|
|
638
638
|
</div>
|
|
639
639
|
<span class="home-who__rank">${builder.rank ? renderRankBadge(normalizeRank(builder.rank), { tip: false }) : ''}</span>
|
|
640
640
|
</div>
|
|
641
|
-
<
|
|
642
|
-
<
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
641
|
+
<div class="hall-standing__more">
|
|
642
|
+
<dl class="home-facts home-facts--grid">
|
|
643
|
+
<div><dt>${escapeHtml(curLabel(true))}</dt><dd>${fmtNum(builder.total_credits)}</dd></div>
|
|
644
|
+
<div><dt>Karma</dt><dd>${fmtNum(karmaTotal)}</dd></div>
|
|
645
|
+
<div><dt>Shipped</dt><dd id="standing-shipped">${escapeHtml((document.getElementById('stat-shipped') || {}).textContent || '—')}</dd></div>
|
|
646
|
+
<div><dt>Leaderboard place</dt><dd>${place}</dd></div>
|
|
647
|
+
</dl>
|
|
648
|
+
<dl class="home-facts">
|
|
649
|
+
<div><dt>Preferred work</dt><dd>${prefs.length ? escapeHtml(prefs.join(', ')) : PREFS_EMPTY}</dd></div>
|
|
650
|
+
</dl>
|
|
651
|
+
${builder.id != null ? `<p class="card-foot"><a href="/profile?id=${encodeURIComponent(builder.id)}">View full profile →</a></p>` : ''}
|
|
652
|
+
</div>
|
|
646
653
|
`;
|
|
647
654
|
}
|
|
648
|
-
|
|
649
|
-
|
|
655
|
+
// task 1004774: Standing is the page-head chip now, not a Home card. The
|
|
656
|
+
// who-line above IS the chip: at rest it shows the avatar and rank, and on
|
|
657
|
+
// hover it widens left to show the name while .hall-standing__more drops down.
|
|
658
|
+
const standingChip = document.getElementById('hall-standing');
|
|
659
|
+
if (standingChip && !ctx.newcomerGated) standingChip.hidden = false;
|
|
650
660
|
renderSystemNotices({ rebaseTasks, need, actionNeeds, cost });
|
|
651
661
|
}
|
|
652
662
|
|
|
@@ -724,6 +734,22 @@
|
|
|
724
734
|
return rows.filter((r) => r && r.status === 'building')
|
|
725
735
|
.sort((a, b) => (Number(b.total_count) || 0) - (Number(a.total_count) || 0))[0] || null;
|
|
726
736
|
}
|
|
737
|
+
// task 1004774: the version tab under the Home banner — the same version the
|
|
738
|
+
// sub-line names, drawn as a bar. Hidden while no version is being built.
|
|
739
|
+
function renderVersionTab(v) {
|
|
740
|
+
const tab = document.getElementById('home-version');
|
|
741
|
+
if (!tab) return;
|
|
742
|
+
if (!v) { tab.hidden = true; return; }
|
|
743
|
+
const pct = Math.max(0, Math.min(100, Math.round(Number(v.percent_complete) || 0)));
|
|
744
|
+
const name = document.getElementById('home-version-name');
|
|
745
|
+
const fill = document.getElementById('home-version-fill');
|
|
746
|
+
const pctEl = document.getElementById('home-version-pct');
|
|
747
|
+
if (name) name.textContent = v.version_id;
|
|
748
|
+
if (fill) fill.style.width = `${pct}%`;
|
|
749
|
+
if (pctEl) pctEl.textContent = `${pct}% complete`;
|
|
750
|
+
tab.setAttribute('aria-label', `${v.version_id} is ${pct} percent complete`);
|
|
751
|
+
tab.hidden = false;
|
|
752
|
+
}
|
|
727
753
|
function renderGreeting() {
|
|
728
754
|
const h1 = document.getElementById('hall-h1');
|
|
729
755
|
const sub = document.getElementById('hall-sub');
|
|
@@ -731,10 +757,11 @@
|
|
|
731
757
|
if (h1 && builder) {
|
|
732
758
|
h1.textContent = `${greetingWord(new Date().getHours())}, ${builder.display_name || builder.github_login || 'builder'}`;
|
|
733
759
|
}
|
|
760
|
+
const v = currentVersion(ctx.lastProgressState);
|
|
761
|
+
renderVersionTab(v);
|
|
734
762
|
if (!sub) return;
|
|
735
763
|
const parts = [];
|
|
736
|
-
|
|
737
|
-
if (v) parts.push(`${v.version_id} is ${Math.round(Number(v.percent_complete) || 0)} percent shipped.`);
|
|
764
|
+
if (v) parts.push(`${v.version_id} is ${Math.round(Number(v.percent_complete) || 0)} percent complete.`);
|
|
738
765
|
const n = ctx.lastClaimableCount;
|
|
739
766
|
if (typeof n === 'number') {
|
|
740
767
|
if (n === 0) parts.push('Nothing is claimable right now.');
|
|
@@ -1036,6 +1063,9 @@
|
|
|
1036
1063
|
// 5-row recent-work table are fed from the same feed. Guarded writes.
|
|
1037
1064
|
const statShipped = document.getElementById('stat-shipped');
|
|
1038
1065
|
if (statShipped) statShipped.textContent = fmtNum(shipState.all.length);
|
|
1066
|
+
// task 1004774: the Standing hover card carries the same count.
|
|
1067
|
+
const standingShipped = document.getElementById('standing-shipped');
|
|
1068
|
+
if (standingShipped) standingShipped.textContent = fmtNum(shipState.all.length);
|
|
1039
1069
|
const homeRecent = document.querySelector('#home-recent-table tbody');
|
|
1040
1070
|
if (homeRecent) {
|
|
1041
1071
|
const latest = sortShips(shipState.all, 'recent').slice(0, 5);
|
|
@@ -75,16 +75,20 @@
|
|
|
75
75
|
'<a class="stat-tile" href="#standing"><span class="stat-tile__label">Shipped</span><span class="stat-tile__num" id="stat-shipped">—</span><span class="stat-tile__sub" id="stat-shipped-sub"></span></a>' +
|
|
76
76
|
'<a class="stat-tile" href="#standing"><span class="stat-tile__label">Karma</span><span class="stat-tile__num" id="stat-karma">—</span><span class="stat-tile__sub" id="stat-karma-sub"></span></a>' +
|
|
77
77
|
'<a class="stat-tile" href="/ranks"><span class="stat-tile__label">Rank</span><span class="stat-tile__num stat-tile__num--badge" id="stat-rank">—</span><span class="stat-tile__sub" id="stat-rank-sub"></span></a>' +
|
|
78
|
-
'</div>'
|
|
78
|
+
'</div>' +
|
|
79
|
+
// task 1004774: the version tab under the banner — how far the version being
|
|
80
|
+
// built has come. hall-render.js renderGreeting fills it from the same
|
|
81
|
+
// progress read as the greeting's sub-line; hidden until there is a version.
|
|
82
|
+
'<a class="version-tab" id="home-version" href="/roadmap" hidden>' +
|
|
83
|
+
'<span class="version-tab__name" id="home-version-name"></span>' +
|
|
84
|
+
'<span class="version-tab__bar" aria-hidden="true"><span class="version-tab__fill" id="home-version-fill"></span></span>' +
|
|
85
|
+
'<span class="version-tab__pct" id="home-version-pct"></span>' +
|
|
86
|
+
'</a>',
|
|
79
87
|
{ view: 'home', zone: 'home-stats', sectionClass: 'scroll scroll--bare', labelledby: 'home-stats-h', hiddenByDefault: true });
|
|
80
88
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
'<h2 id="home-standing-h" class="scroll__h">Standing</h2>' +
|
|
85
|
-
'<div id="home-standing-body"><div class="profile__placeholder">…</div></div>' +
|
|
86
|
-
'<p class="card-foot"><a href="#standing">See your Full Account. →</a></p>',
|
|
87
|
-
{ view: 'home', zone: 'home-band', labelledby: 'home-standing-h', hiddenByDefault: true });
|
|
89
|
+
// (task 1004774: the Standing summary is no longer a Home card. It is the
|
|
90
|
+
// chip at the top right of the page head — index.html #hall-standing — whose
|
|
91
|
+
// hover card holds #home-standing-body, filled by hall-render.js renderProfile.)
|
|
88
92
|
|
|
89
93
|
// (Goals — goals.js registers itself into the other half of the Home band.)
|
|
90
94
|
|
|
@@ -99,15 +103,21 @@
|
|
|
99
103
|
// gated or empty), then the 5-row recent-ships table below it.
|
|
100
104
|
w('home-recent', 'Your work', 28,
|
|
101
105
|
'<h2 id="home-recent-h" class="scroll__h">Your work</h2>' +
|
|
102
|
-
|
|
106
|
+
// task 1004774: Next up and Recently shipped sit side by side (stacking
|
|
107
|
+
// again on a phone); when Next up is hidden, shipped takes the full width.
|
|
108
|
+
'<div class="home-split">' +
|
|
109
|
+
'<div class="home-split__col" id="home-next" hidden>' +
|
|
103
110
|
'<h3 class="ginbox__sec-h">Next up — ready to claim</h3>' +
|
|
104
111
|
'<ul class="next-up" id="home-next-list"></ul>' +
|
|
105
112
|
'<p class="next-up__foot"><a href="/work" id="home-next-foot">See all ready work →</a></p>' +
|
|
106
113
|
'</div>' +
|
|
114
|
+
'<div class="home-split__col">' +
|
|
107
115
|
'<h3 class="ginbox__sec-h" id="home-recent-subh" hidden>Recently shipped</h3>' +
|
|
108
116
|
'<table class="ledger" id="home-recent-table"><thead><tr><th>№</th><th>Work</th><th data-currency-label="caps">Credits</th><th>Shipped</th></tr></thead>' +
|
|
109
117
|
'<tbody><tr><td colspan="4" class="ledger__empty">…</td></tr></tbody></table>' +
|
|
110
|
-
'<p class="card-foot"><a href="#standing">All your work →</a></p>'
|
|
118
|
+
'<p class="card-foot"><a href="#standing">All your work →</a></p>' +
|
|
119
|
+
'</div>' +
|
|
120
|
+
'</div>',
|
|
111
121
|
{ view: 'home', zone: 'home-main', labelledby: 'home-recent-h', hiddenByDefault: true });
|
|
112
122
|
|
|
113
123
|
// 0f. The founding wall (task 1004424) — everyone who joined while the project was
|
|
@@ -48,6 +48,13 @@
|
|
|
48
48
|
<div class="page-head page-head--board page-head--hall">
|
|
49
49
|
<h1 id="hall-h1" class="page-head__h">Builders' Hall</h1>
|
|
50
50
|
<p id="hall-sub" class="page-head__sub" hidden></p>
|
|
51
|
+
<!-- task 1004774: Standing left the Home band for this chip. At rest it
|
|
52
|
+
shows the avatar and rank. On hover or keyboard focus the chip itself
|
|
53
|
+
widens to the left into the top of the card, and only the facts below
|
|
54
|
+
drop down (#home-standing-body, filled by hall-render.js renderProfile). -->
|
|
55
|
+
<div class="hall-standing" id="hall-standing" hidden tabindex="0" aria-label="Your standing">
|
|
56
|
+
<div class="hall-standing__card" id="home-standing-body"></div>
|
|
57
|
+
</div>
|
|
51
58
|
</div>
|
|
52
59
|
|
|
53
60
|
<!-- BV1.R75 (ADR 0062 §4): hall widgets are NOT hardcoded here. Every widget
|
|
@@ -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,
|