@bongos/core 1.21.163 → 1.21.164
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +54 -39
- package/.gitattributes +6 -0
- package/docs/copy-inventory.md +99 -99
- package/docs/copy-registry.json +149 -139
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +141 -306
- 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/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/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 +14 -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/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/promote_goal_id.mjs +8 -0
- package/tests/tweak_batch_apply.mjs +40 -0
- package/tests/tweak_land_heal.mjs +139 -0
|
@@ -691,6 +691,8 @@ function approvalFor({ page, pageId, rounds, round, builderId, isArtist }) {
|
|
|
691
691
|
const applied = pages.parseAppliedBlock(round.description);
|
|
692
692
|
const refusedBy = new Map();
|
|
693
693
|
if (applied.ok) for (const r of applied.applied.refused || []) if (r && r.key) refusedBy.set(String(r.key), String(r.code || 'refused'));
|
|
694
|
+
const pinnedBy = new Map();
|
|
695
|
+
if (applied.ok) for (const p of applied.applied.pinned_tests || []) if (p && p.key && Array.isArray(p.tests)) pinnedBy.set(String(p.key), p.tests.map(String));
|
|
694
696
|
const lines = batch.ok ? batch.batch.lines.map((l) => ({
|
|
695
697
|
key: l.key,
|
|
696
698
|
section: l.section == null ? null : l.section,
|
|
@@ -698,6 +700,7 @@ function approvalFor({ page, pageId, rounds, round, builderId, isArtist }) {
|
|
|
698
700
|
after: l.after,
|
|
699
701
|
placement: l.placement || null,
|
|
700
702
|
refused: refusedBy.get(String(l.key)) || null,
|
|
703
|
+
...(pinnedBy.has(String(l.key)) ? { pinned_by: pinnedBy.get(String(l.key)) } : {}),
|
|
701
704
|
})) : [];
|
|
702
705
|
const shipped = (rounds || []).filter((r) => r.status === 'shipped');
|
|
703
706
|
const now = countsFor(rounds);
|
|
@@ -164,6 +164,11 @@ function composeAppliedBlock(a) {
|
|
|
164
164
|
reading_hash_after: a.reading_hash_after,
|
|
165
165
|
lines_after: (a.lines_after || []).map(String),
|
|
166
166
|
refused: (a.refused || []).map((x) => ({ key: x.key, code: x.code })),
|
|
167
|
+
// task 1004775: tests that still assert a rewritten line's old words, named
|
|
168
|
+
// at apply time so the artist sees them before approving, not CI after.
|
|
169
|
+
...(Array.isArray(a.pinned_tests) && a.pinned_tests.length
|
|
170
|
+
? { pinned_tests: a.pinned_tests.map((p) => ({ key: p.key, tests: (p.tests || []).map(String) })) }
|
|
171
|
+
: {}),
|
|
167
172
|
});
|
|
168
173
|
}
|
|
169
174
|
|
|
@@ -119,6 +119,10 @@
|
|
|
119
119
|
marked: after && !line.refused,
|
|
120
120
|
aside: line.refused ? refusedWords(line.refused) : (after ? `was: ${line.before}` : `becomes: ${line.after}`),
|
|
121
121
|
refused: !!line.refused,
|
|
122
|
+
// task 1004775: a test still asserts the old words, so it fails once this lands.
|
|
123
|
+
...(!line.refused && Array.isArray(line.pinned_by) && line.pinned_by.length
|
|
124
|
+
? { pinned: `A test still checks the old words: ${line.pinned_by.join(', ')}. It needs updating when this ships.` }
|
|
125
|
+
: {}),
|
|
122
126
|
};
|
|
123
127
|
}
|
|
124
128
|
|
|
@@ -226,6 +226,8 @@ html.aq-open { overflow: hidden; }
|
|
|
226
226
|
}
|
|
227
227
|
.aq-line__aside { font-size: 13px; line-height: 1.45; color: var(--aq-ink-faint); overflow-wrap: anywhere; }
|
|
228
228
|
.aq-line--refused .aq-line__aside { color: var(--aq-link); }
|
|
229
|
+
/* task 1004775: a test that still checks the old words. */
|
|
230
|
+
.aq-line__pinned { font-size: 13px; line-height: 1.45; color: var(--aq-link); overflow-wrap: anywhere; }
|
|
229
231
|
.aq-lines__none { margin: 0; font-size: 14px; color: var(--aq-ink-faint); }
|
|
230
232
|
|
|
231
233
|
/* The decision row. */
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
${r.section ? `<span class="aq-line__section">${esc(r.section)}</span>` : ''}
|
|
153
153
|
<span class="aq-line__text">${r.marked ? `<mark class="aq-mark">${words(r.text)}</mark>` : words(r.text)}</span>
|
|
154
154
|
<span class="aq-line__aside">${words(r.aside)}</span>
|
|
155
|
+
${r.pinned ? `<span class="aq-line__pinned">${esc(r.pinned)}</span>` : ''}
|
|
155
156
|
</li>`).join('')}</ol>`;
|
|
156
157
|
}
|
|
157
158
|
|
|
@@ -929,7 +929,7 @@
|
|
|
929
929
|
|
|
930
930
|
// ---- Home "Next up" slice (task 2184) -------------------------------------
|
|
931
931
|
|
|
932
|
-
// Fill the top of the "Your work" card with the first
|
|
932
|
+
// Fill the top of the "Your work" card with the first five rows of the
|
|
933
933
|
// claimable queue (server order: priority, then calibrated estimate) so Home
|
|
934
934
|
// answers "what's my next action". Best-effort: a primer-gated 403, an auth
|
|
935
935
|
// hiccup, or an empty queue hides the slice — Home never shows an error for
|
|
@@ -947,7 +947,8 @@
|
|
|
947
947
|
// already orders by priority then calibrated estimate, so the first two rows ARE
|
|
948
948
|
// the top two. `claimable_total` comes back so the foot can say how many are
|
|
949
949
|
// waiting instead of leaving two rows to imply there are only two.
|
|
950
|
-
|
|
950
|
+
// task 1004774: five rows, level with the five Recently shipped rows beside it.
|
|
951
|
+
const resp = await getJSON(`${API}/tasks/claimable?limit=5`, { softAuth: true });
|
|
951
952
|
// task 1003464: the greeting's "N tasks are claimable right now" — rank-scoped,
|
|
952
953
|
// so it is THIS builder's number. It reads claimable_total rather than the length
|
|
953
954
|
// of a fully-downloaded feed: the same number, without the wire cost that this
|
|
@@ -959,7 +960,8 @@
|
|
|
959
960
|
? claimableTotal
|
|
960
961
|
: (resp && Array.isArray(resp.claimable) ? resp.claimable.length : null);
|
|
961
962
|
renderGreeting();
|
|
962
|
-
|
|
963
|
+
// capped as well as asked for: a server that ignores ?limit must not grow the card.
|
|
964
|
+
const rows = resp && Array.isArray(resp.claimable) ? resp.claimable.slice(0, 5) : [];
|
|
963
965
|
if (!rows.length) { wrap.hidden = true; if (subhead) subhead.hidden = true; return; }
|
|
964
966
|
const total = Number(resp.claimable_total);
|
|
965
967
|
const cur = (window.__BRANDING__ && window.__BRANDING__.currency && window.__BRANDING__.currency.label) || 'credits';
|
|
@@ -1175,7 +1177,7 @@
|
|
|
1175
1177
|
getJSON(`${API}/me/recent-ships?limit=all`),
|
|
1176
1178
|
getJSON(`${API}/versions`),
|
|
1177
1179
|
getJSON(`${API}/public/leaderboard`), // task 1004437: the list with each builder's roles
|
|
1178
|
-
getJSON(`${API}/public/progress`), // task 1003464: "<version> is N percent
|
|
1180
|
+
getJSON(`${API}/public/progress`), // task 1003464: "<version> is N percent complete"
|
|
1179
1181
|
]);
|
|
1180
1182
|
|
|
1181
1183
|
if (progress.status === 'fulfilled') {
|
|
@@ -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
|
|
@@ -4565,3 +4565,98 @@ html.palette-open { overflow: hidden; }
|
|
|
4565
4565
|
.gr-cat { display: inline-flex; align-items: baseline; gap: 5px; margin-left: 8px; padding: 1px 8px; border: 1px solid var(--rule-soft); border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: .01em; color: var(--ink-soft); background: var(--surface-2); vertical-align: baseline; white-space: nowrap; }
|
|
4566
4566
|
.gr-cat--inherited { font-weight: 550; color: var(--ink-faint); }
|
|
4567
4567
|
.gr-cat__from { font-style: normal; font-weight: 500; font-size: 10px; color: var(--ink-faint); opacity: .85; }
|
|
4568
|
+
|
|
4569
|
+
/* ---- Home reshuffle (task 1004774; mock docs/design/mocks/hall/home/v1.html) ----
|
|
4570
|
+
A slimmer banner pulled up under the greeting, the version as a progress tab
|
|
4571
|
+
under it, Standing as a chip at the top right with a hover card, the
|
|
4572
|
+
Needs-attention slices as cards that open a window, and Your work beside
|
|
4573
|
+
Recently shipped. */
|
|
4574
|
+
body[data-page="home"] .page-head--hall { position: relative; margin: 0 0 12px; padding-right: 180px; }
|
|
4575
|
+
body[data-page="home"] .stat-tile { padding: 12px 22px; min-height: 0; gap: 4px; }
|
|
4576
|
+
body[data-page="home"] .stat-tile__num { font-size: clamp(20px, 2vw, 26px); }
|
|
4577
|
+
body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
4578
|
+
|
|
4579
|
+
.version-tab {
|
|
4580
|
+
display: flex; align-items: center; gap: 14px; min-height: 24px;
|
|
4581
|
+
padding: 10px 22px; color: var(--ink); text-decoration: none;
|
|
4582
|
+
background: var(--glass); border: 1px solid var(--glass-edge); border-top: 0;
|
|
4583
|
+
border-radius: 0 0 var(--radius) var(--radius);
|
|
4584
|
+
}
|
|
4585
|
+
.version-tab:hover { text-decoration: none; background: var(--glass-strong); }
|
|
4586
|
+
.version-tab[hidden] { display: none; }
|
|
4587
|
+
.version-tab__name { font-weight: 600; white-space: nowrap; }
|
|
4588
|
+
.version-tab__bar { flex: 1; height: 8px; border-radius: 4px; background: var(--rule-soft); overflow: hidden; }
|
|
4589
|
+
.version-tab__fill { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 4px; }
|
|
4590
|
+
.version-tab__pct { color: var(--ink-soft); font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
4591
|
+
|
|
4592
|
+
.hall-standing { position: absolute; top: 0; right: 0; height: 44px; outline: none; }
|
|
4593
|
+
.hall-standing[hidden] { display: none; }
|
|
4594
|
+
/* The chip IS the top of the card: anchored at the right, it grows LEFT as the
|
|
4595
|
+
name slides in, and only .hall-standing__more drops down below it. */
|
|
4596
|
+
.hall-standing__card {
|
|
4597
|
+
position: absolute; top: 0; right: 0; z-index: 20; min-width: 0;
|
|
4598
|
+
background: var(--bg-card); border-radius: 22px; padding: 4px 6px 4px 4px;
|
|
4599
|
+
transition: min-width .2s ease, border-radius .2s ease, padding .2s ease, box-shadow .2s ease;
|
|
4600
|
+
}
|
|
4601
|
+
.hall-standing__card .home-who { gap: 8px; padding: 0; }
|
|
4602
|
+
.hall-standing__card .home-who__avatar { width: 36px; height: 36px; }
|
|
4603
|
+
.hall-standing__card .home-who__meta {
|
|
4604
|
+
max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap;
|
|
4605
|
+
transition: max-width .2s ease, opacity .2s ease;
|
|
4606
|
+
}
|
|
4607
|
+
.hall-standing__more {
|
|
4608
|
+
max-height: 0; overflow: hidden; opacity: 0;
|
|
4609
|
+
transition: max-height .25s ease, opacity .2s ease;
|
|
4610
|
+
}
|
|
4611
|
+
.hall-standing__more .home-facts { margin-top: 12px; }
|
|
4612
|
+
.hall-standing__more .card-foot { margin: 12px 0 0; }
|
|
4613
|
+
.hall-standing:hover .hall-standing__card,
|
|
4614
|
+
.hall-standing:focus-within .hall-standing__card {
|
|
4615
|
+
min-width: 300px; border-radius: var(--radius); padding: 14px 16px 16px;
|
|
4616
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, .35);
|
|
4617
|
+
}
|
|
4618
|
+
.hall-standing:hover .home-who__meta,
|
|
4619
|
+
.hall-standing:focus-within .home-who__meta { max-width: 200px; opacity: 1; }
|
|
4620
|
+
.hall-standing:hover .hall-standing__more,
|
|
4621
|
+
.hall-standing:focus-within .hall-standing__more { max-height: 420px; opacity: 1; }
|
|
4622
|
+
.hall-standing:focus-visible .hall-standing__card { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
4623
|
+
|
|
4624
|
+
.attn-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
4625
|
+
.attn-card {
|
|
4626
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-height: 100px;
|
|
4627
|
+
padding: 16px 18px; text-align: left; font: inherit; color: var(--ink); cursor: pointer;
|
|
4628
|
+
background: var(--glass); border: 1px solid var(--glass-edge); border-radius: var(--radius);
|
|
4629
|
+
}
|
|
4630
|
+
.attn-card:hover { border-color: var(--accent); }
|
|
4631
|
+
.attn-card__n { font-family: var(--font-display); font-size: 30px; font-weight: 300; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
4632
|
+
.attn-card__t { font-weight: 600; }
|
|
4633
|
+
.attn-card__s { margin-top: auto; font-size: 12px; color: var(--ink-soft); }
|
|
4634
|
+
.attn-dialog {
|
|
4635
|
+
width: min(640px, calc(100vw - 32px)); max-height: 80vh; padding: 22px 24px; overflow: auto;
|
|
4636
|
+
background: var(--bg-card); color: var(--ink); border: 0; border-radius: var(--radius);
|
|
4637
|
+
}
|
|
4638
|
+
.attn-dialog::backdrop { background: var(--scrim); }
|
|
4639
|
+
.attn-dialog__head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
|
4640
|
+
.attn-dialog__h { flex: 1; margin: 0; font-family: var(--font-display); font-weight: 500; font-size: 20px; }
|
|
4641
|
+
.attn-dialog__x { min-width: 32px; min-height: 32px; background: none; border: 0; color: var(--ink-soft); font-size: 22px; cursor: pointer; }
|
|
4642
|
+
.attn-dialog__body .ginbox__sec-h { display: none; }
|
|
4643
|
+
|
|
4644
|
+
.home-split { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }
|
|
4645
|
+
.home-split__col { min-width: 0; }
|
|
4646
|
+
.home-split > .home-split__col[hidden] + .home-split__col { grid-column: 1 / -1; }
|
|
4647
|
+
|
|
4648
|
+
@media (max-width: 760px) {
|
|
4649
|
+
body[data-page="home"] .page-head--hall { padding-right: 0; }
|
|
4650
|
+
.hall-standing { position: relative; display: block; margin-top: 10px; }
|
|
4651
|
+
.hall-standing__card { left: 0; right: auto; }
|
|
4652
|
+
.hall-standing:hover .hall-standing__card,
|
|
4653
|
+
.hall-standing:focus-within .hall-standing__card { min-width: min(300px, calc(100vw - 32px)); }
|
|
4654
|
+
.home-split { grid-template-columns: 1fr; }
|
|
4655
|
+
}
|
|
4656
|
+
/* With Standing gone from the band, a lone card there (Goals) takes the row. */
|
|
4657
|
+
.view-zone--band > .scroll:only-child { grid-column: 1 / -1; }
|
|
4658
|
+
/* The Standing hover card's stat row: four figures in a 2x2 grid, label over number. */
|
|
4659
|
+
.home-facts--grid { grid-template-columns: 1fr 1fr; gap: 10px 14px; padding: 12px 0; border-top: 1px solid var(--rule-soft); border-bottom: 1px solid var(--rule-soft); }
|
|
4660
|
+
.home-facts--grid > div { display: flex; flex-direction: column; gap: 2px; }
|
|
4661
|
+
.home-facts--grid dt, .home-facts--grid dd { border-top: 0; padding: 0; text-align: left; }
|
|
4662
|
+
.home-facts--grid dd { font-family: var(--font-display); font-size: 20px; font-weight: 300; }
|
|
@@ -450,6 +450,12 @@
|
|
|
450
450
|
var priOptions = [1, 2, 3, 4, 5].map(function (p) {
|
|
451
451
|
return '<option value="' + p + '"' + (p === pri ? ' selected' : '') + '>P' + p + '</option>';
|
|
452
452
|
}).join('');
|
|
453
|
+
// A create with no discipline the server cannot infer from the wording is
|
|
454
|
+
// refused (task 1002988), so the builder names the craft here. Engineer is
|
|
455
|
+
// the default, the same fallback bug routing uses (ideas/inbox.js).
|
|
456
|
+
var craftOptions = [['engineer', 'Engineer'], ['artist', 'Artist'], ['ideator', 'Ideator'], ['ui', 'UI']].map(function (c) {
|
|
457
|
+
return '<option value="' + c[0] + '"' + (c[0] === 'engineer' ? ' selected' : '') + '>' + c[1] + '</option>';
|
|
458
|
+
}).join('');
|
|
453
459
|
box.innerHTML =
|
|
454
460
|
'<form class="td-form" data-form="promote">'
|
|
455
461
|
+ '<label class="td-field td-field--wide"><span>Title</span>'
|
|
@@ -457,6 +463,7 @@
|
|
|
457
463
|
+ '<label class="td-field td-field--wide"><span>Version</span><select name="version">' + options + '</select></label>'
|
|
458
464
|
+ '<label class="td-field td-field--wide"><span>Goal</span><select name="goal" data-goal-select>' + goalOptions + '</select></label>'
|
|
459
465
|
+ '<label class="td-field"><span>Priority</span><select name="priority">' + priOptions + '</select></label>'
|
|
466
|
+
+ '<label class="td-field"><span>Craft</span><select name="discipline">' + craftOptions + '</select></label>'
|
|
460
467
|
+ '<label class="td-field"><span>Estimate (min)</span>'
|
|
461
468
|
+ '<input type="number" name="est" min="0" max="100000" step="5" placeholder="—"></label>'
|
|
462
469
|
+ '<label class="td-field"><span>Reward</span>'
|
|
@@ -504,6 +511,7 @@
|
|
|
504
511
|
title: title,
|
|
505
512
|
description: body + provenance,
|
|
506
513
|
priority: Number(form.priority.value) || 3,
|
|
514
|
+
discipline: (form.discipline && form.discipline.value) || 'engineer',
|
|
507
515
|
source: 'idea_inbox',
|
|
508
516
|
source_ref: 'idea:' + idea.id,
|
|
509
517
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# The Home reshuffle — slim banner, version tab, Standing chip, attention cards with windows (task 1004774)
|
|
2
|
+
|
|
3
|
+
The owner's Builders' Hall feedback (idea 1001262), home-page half, built to the approved mock at
|
|
4
|
+
[`docs/design/mocks/hall/home/v1.html`](../../../docs/design/mocks/hall/home/v1.html).
|
|
5
|
+
|
|
6
|
+
- **Banner + version tab.** The stat row is slimmer (`body[data-page="home"]` overrides), and under it
|
|
7
|
+
`#home-version` draws the version the greeting's sub-line names, from the same `currentVersion()`
|
|
8
|
+
read — `renderVersionTab` in `hall-render.js`. Hidden while nothing is being built.
|
|
9
|
+
- **Standing is a chip, not a card.** The `home-standing` widget is gone; `#hall-standing` sits in the
|
|
10
|
+
page head (it is chrome, not a `<section class="scroll">`, so the registry rule is untouched). The
|
|
11
|
+
card it opens on hover or keyboard focus is still `#home-standing-body`, filled by `renderProfile`.
|
|
12
|
+
The place row reads "Leaderboard place" — the owner asked for no "on the board" wording. The chip
|
|
13
|
+
IS the top of the card (owner, 2026-10-23): at rest it shows the avatar and rank with no text and
|
|
14
|
+
no link, and on hover or focus it widens LEFT as the name slides in, while only
|
|
15
|
+
`.hall-standing__more` (the facts and a "View full profile" link to /profile) drops down below it.
|
|
16
|
+
- **Needs attention is cards.** Every slice still renders into its own div exactly as before, kept in
|
|
17
|
+
the hidden `#attention-slices`. `OTBAttention.refresh()` rebuilds one card per non-empty slice from
|
|
18
|
+
the slice's heading (title, count pill = the server total, the `.ginbox__sub` note). Opening a card
|
|
19
|
+
MOVES the slice div into `#attention-dialog`, which sits inside the widget host — so the delegated
|
|
20
|
+
click handler still sees every button and no slice needed a second action path. Closing moves it back.
|
|
21
|
+
`tests/home_attention_cards.mjs` runs the real file in a browser: card totals, open, X, Escape, and a
|
|
22
|
+
window whose slice empties while open.
|
|
23
|
+
- **Goals card** lost its subtitle, and its foot is one "Browse goals" link; the "not joined" line shows only when you have joined none.
|
|
24
|
+
- **Next up is five rows**, level with the five Recently shipped rows, asked for AND capped client-side.
|
|
25
|
+
- **Renamed:** "Stuck on the owner" → "Requires project owner fix"; "Going quiet" → "Things not moving".
|
|
26
|
+
- **Your work beside Recently shipped** — `.home-split` in the `home-recent` widget; shipped takes the
|
|
27
|
+
full row when Next up is hidden, and both stack under 760px.
|
|
28
|
+
|
|
29
|
+
Not built, because the data does not exist yet: the "Tasks you are blocking in other goals"
|
|
30
|
+
(requested / total) card, and the any-owner / main-owner split on "Requires project owner fix" —
|
|
31
|
+
blockers carry no addressee today.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.164",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.164",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|