@bongos/core 1.19.474 → 1.19.476
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 +56 -36
- package/docs/adr/0229-the-title-is-the-door-and-the-record-exits-to-where-it-was-opened-from.md +38 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +255 -258
- package/docs/copy-registry.json +376 -414
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/builders.js +13 -64
- package/modules/hall-ui/public/dom-utils.js +2 -1
- package/modules/hall-ui/public/goals-page.js +4 -3
- package/modules/hall-ui/public/goals-render.js +5 -3
- package/modules/hall-ui/public/hall-render.js +10 -142
- package/modules/hall-ui/public/hall-widgets.js +6 -16
- package/modules/hall-ui/public/index.html +0 -3
- package/modules/hall-ui/public/index.states.json +3 -2
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/roster.html +63 -0
- package/modules/hall-ui/public/roster.js +227 -0
- package/modules/hall-ui/public/roster.states.json +100 -0
- package/modules/hall-ui/public/shell.js +28 -4
- package/modules/hall-ui/public/style.css +20 -18
- package/modules/hall-ui/public/task-detail.js +16 -4
- package/modules/hall-ui/public/task.html +5 -3
- package/modules/hall-ui/public/task.js +58 -4
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/work.js +10 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_board_world.mjs +69 -0
- package/tests/hall_nav.mjs +36 -1
- package/tests/hall_page_gate_map.mjs +8 -0
- package/tests/hall_record_world.mjs +83 -4
- package/tests/rank_tier_single_source.mjs +8 -1
|
@@ -1385,5 +1385,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1385
1385
|
landed since 1.19.472 with no explicit bump. run 33646919877. (task 1002620)
|
|
1386
1386
|
1.19.474 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1387
1387
|
landed since 1.19.473 with no explicit bump. run 33650008858. (task 1002620)
|
|
1388
|
+
1.19.475 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1389
|
+
landed since 1.19.474 with no explicit bump. run 33653632532. (task 1002620)
|
|
1390
|
+
1.19.476 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1391
|
+
landed since 1.19.475 with no explicit bump. run 33655706909. (task 1002620)
|
|
1388
1392
|
---------------------------------------------------------------------------
|
|
1389
1393
|
```
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
|
|
231
231
|
// task 1723: which container a widget mounts into. A widget may declare
|
|
232
232
|
// `zone` (a fine-grained Home slot), `group` (a Standing rail section, task
|
|
233
|
-
// 2174) or `view` (home/standing
|
|
233
|
+
// 2174) or `view` (home/standing); all default to the Home main
|
|
234
234
|
// zone so pre-redesign module widgets still land somewhere visible. Every
|
|
235
235
|
// container lives INSIDE <main>, so the `main .scroll` sweeps (newcomer gate,
|
|
236
236
|
// landing hide-all) keep working.
|
|
@@ -910,26 +910,12 @@
|
|
|
910
910
|
}
|
|
911
911
|
}
|
|
912
912
|
|
|
913
|
-
// ----
|
|
914
|
-
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
// task 2174: OTBKit pager. 25/page is what task 1750 set for the top-100 Roll;
|
|
920
|
-
// the per-page select now lets a builder widen it and the choice persists.
|
|
921
|
-
|
|
922
|
-
// Stable sort. Server returns rows in drachmae-DESC order, which is also the
|
|
923
|
-
// default view; for other modes (rank, karma, drachmae-ASC), re-sort here.
|
|
924
|
-
// The original server order is preserved as a tie-breaker so equal-value rows
|
|
925
|
-
// don't jitter between renders. Drachmae is the secondary tie-breaker when
|
|
926
|
-
// sorting by karma (V3.R26 #246) so the karma toggle still reads sensibly
|
|
927
|
-
// when many builders share the same karma value (e.g. zero).
|
|
928
|
-
|
|
929
|
-
// Map a sortable column id → the lbState.sortBy key. Adding a new sortable
|
|
930
|
-
// column means adding an entry here + a header in hall-widgets.js with id
|
|
931
|
-
// `lb-sort-<key>`. The handler walks aria-sort across all sortable headers so
|
|
932
|
-
// only one column shows its arrow at a time.
|
|
913
|
+
// ---- the Roll’s rows, for the Standing card -----------------------------
|
|
914
|
+
//
|
|
915
|
+
// task 1003441 moved the leaderboard TABLE to its own page (/roster,
|
|
916
|
+
// roster.js): the pager, the sort lenses, LB_SORT_HEADERS and the achievement
|
|
917
|
+
// laurels went with it. Only the place lookup below stays — it needs the same
|
|
918
|
+
// rows, and the Standing card that shows it is on THIS page.
|
|
933
919
|
|
|
934
920
|
// #757: this builder's place on the Roll, ranked by drachmae (total_credits)
|
|
935
921
|
// descending — the canonical leaderboard order, independent of whatever column
|
|
@@ -939,28 +925,6 @@
|
|
|
939
925
|
// builder isn't on the roll). Matched by github_login (rows carry no id).
|
|
940
926
|
|
|
941
927
|
|
|
942
|
-
// Cycle: pick column → desc → asc → back to default (drachmae desc).
|
|
943
|
-
// For a click on the active column we just flip direction; for a click on
|
|
944
|
-
// a different column we activate it at 'desc' (the natural "best first"
|
|
945
|
-
// direction for all three columns). Clicking the same column a third time
|
|
946
|
-
// returns to the default drachmae-desc — discoverable, no dead state.
|
|
947
|
-
function onLbSortClick(key) {
|
|
948
|
-
if (lbState.sortBy === key) {
|
|
949
|
-
if (lbState.sortDir === 'desc') {
|
|
950
|
-
lbState.sortDir = 'asc';
|
|
951
|
-
} else {
|
|
952
|
-
// Third click returns to the default view.
|
|
953
|
-
lbState.sortBy = 'drachmae';
|
|
954
|
-
lbState.sortDir = 'desc';
|
|
955
|
-
}
|
|
956
|
-
} else {
|
|
957
|
-
lbState.sortBy = key;
|
|
958
|
-
lbState.sortDir = 'desc';
|
|
959
|
-
}
|
|
960
|
-
lbPager.page = 1;
|
|
961
|
-
renderLeaderboard();
|
|
962
|
-
}
|
|
963
|
-
|
|
964
928
|
// ---- onboarding live-tick poll (task 1444) -------------------------------
|
|
965
929
|
//
|
|
966
930
|
// The onboarding checklist is rendered once from the page-load /me snapshot.
|
|
@@ -1160,14 +1124,15 @@
|
|
|
1160
1124
|
|
|
1161
1125
|
if (lb.status === 'fulfilled') {
|
|
1162
1126
|
lbState.rows = lb.value.leaderboard || [];
|
|
1163
|
-
lbPager.page = 1;
|
|
1164
|
-
renderLeaderboard();
|
|
1165
1127
|
// #757: the Standing card painted before the Roll loaded, so re-render it
|
|
1166
1128
|
// now that lbState.rows exists — this is when its "#N on the Roll" stat
|
|
1167
1129
|
// can resolve.
|
|
1168
1130
|
if (lastMeState && lastMeState.builder) renderProfile(lastMeState.builder);
|
|
1169
1131
|
} else {
|
|
1170
|
-
|
|
1132
|
+
// The Roll’s rows only feed the Standing card’s "#N" stat now — the table
|
|
1133
|
+
// itself is /roster (task 1003441) — so a failed fetch degrades that one
|
|
1134
|
+
// stat rather than blanking a panel this page no longer has.
|
|
1135
|
+
lbState.rows = [];
|
|
1171
1136
|
}
|
|
1172
1137
|
}
|
|
1173
1138
|
|
|
@@ -1241,25 +1206,9 @@
|
|
|
1241
1206
|
// task 2174: the kit wires prev/next/per-page for both tables against the
|
|
1242
1207
|
// #ships-*/#lb-* ids its own pagerBarHtml() stamped in hall-widgets.js.
|
|
1243
1208
|
window.OTBKit.wirePager(shipsPager, renderRecentShips);
|
|
1244
|
-
|
|
1209
|
+
|
|
1245
1210
|
window.OTBKit.wirePager(acclaimPager, renderAcclaimRows);
|
|
1246
1211
|
|
|
1247
|
-
// Sortable leaderboard headers — clickable + keyboard-operable. Each
|
|
1248
|
-
// sortable column registers under LB_SORT_HEADERS; the click handler
|
|
1249
|
-
// routes the column key into the shared cycle.
|
|
1250
|
-
for (const [key, headerId] of Object.entries(LB_SORT_HEADERS)) {
|
|
1251
|
-
const head = document.getElementById(headerId);
|
|
1252
|
-
if (!head) continue;
|
|
1253
|
-
head.setAttribute('tabindex', '0');
|
|
1254
|
-
head.setAttribute('role', 'button');
|
|
1255
|
-
head.addEventListener('click', () => onLbSortClick(key));
|
|
1256
|
-
head.addEventListener('keydown', (e) => {
|
|
1257
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
1258
|
-
e.preventDefault();
|
|
1259
|
-
onLbSortClick(key);
|
|
1260
|
-
}
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
1212
|
|
|
1264
1213
|
// Sign-out is wired centrally in shell.js (doLogout + a delegated click
|
|
1265
1214
|
// listener on the sign-out anchors), so it runs the real POST -> cookie-clear
|
|
@@ -1365,7 +1314,7 @@
|
|
|
1365
1314
|
// reads what this page holds NOW rather than what it held at boot. Everything
|
|
1366
1315
|
// else is stable for the life of the page. This call sits AFTER every
|
|
1367
1316
|
// declaration it passes — moving it earlier is a temporal-dead-zone crash.
|
|
1368
|
-
const {
|
|
1317
|
+
const { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey } = window.OTBHallRender({
|
|
1369
1318
|
API, RANK_ORDER, api, brandWorld, curLabel, emptyRow, fmtDate, mins, normalizeRank, pct, postJSON, rankLabel, rankWeight, signClasses, toast, usd,
|
|
1370
1319
|
get instanceRepo() { return instanceRepo; },
|
|
1371
1320
|
get lastCostState() { return lastCostState; },
|
|
@@ -153,7 +153,8 @@
|
|
|
153
153
|
// third-party widget can't blank the hall (the structural id/label/mount
|
|
154
154
|
// contract below still throws — that one is first-party programmer error).
|
|
155
155
|
const HALL_ZONES = ['home-banner', 'home-stats', 'home-band', 'home-main'];
|
|
156
|
-
|
|
156
|
+
// task 1003441: 'leaderboard' left — it is a page (/roster) now, not a view.
|
|
157
|
+
const HALL_VIEWS = ['home', 'standing'];
|
|
157
158
|
|
|
158
159
|
const _hallWidgets = [];
|
|
159
160
|
function contributeHallWidget(widget) {
|
|
@@ -1067,10 +1067,11 @@
|
|
|
1067
1067
|
if (copyBtn) { copyPromptForTask(copyBtn.dataset.copyTask, copyBtn); return; }
|
|
1068
1068
|
const releaseBtn = e.target.closest('[data-release-claim]');
|
|
1069
1069
|
if (releaseBtn) { e.stopPropagation(); doGoalRelease(releaseBtn.dataset.releaseClaim, releaseBtn.dataset.releaseTask, releaseBtn); return; }
|
|
1070
|
-
|
|
1071
|
-
|
|
1070
|
+
// task 1003468: the title is the way into the record — a real link the
|
|
1071
|
+
// browser follows (it carries where it was opened from); let it be.
|
|
1072
|
+
if (e.target.closest('.task__link')) return;
|
|
1072
1073
|
// Click elsewhere: a ready card toggles its selection; a read-only (active /
|
|
1073
|
-
// backlog) card opens the
|
|
1074
|
+
// backlog) card opens the record (the board's affordance, work.js).
|
|
1074
1075
|
const card = e.target.closest('.task');
|
|
1075
1076
|
if (!card || !card.dataset.taskId) return;
|
|
1076
1077
|
if (card.classList.contains('task--readonly')) {
|
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
};
|
|
48
48
|
const taskSettled = (t) => window.OTBBranding?.taskSettled?.(t) ?? (!!t && (t.status === 'confirmed' || t.status === 'shipped'));
|
|
49
49
|
const creditVal = (t) => Number(window.OTBBranding?.taskCreditValue?.(t) ?? t?.credits_reward) || 0;
|
|
50
|
+
// task 1003468 — the record link a card's title carries; it names where it
|
|
51
|
+
// was opened from so the record can offer the way back (task-detail.js).
|
|
52
|
+
const taskHref = (id) => window.OTBTaskDetail.href(id);
|
|
50
53
|
function statusBadge(status) {
|
|
51
54
|
const cls = status === 'achieved' ? 'goal-badge--achieved'
|
|
52
55
|
: (status === 'archived' ? 'goal-badge--archived' : 'goal-badge--open');
|
|
@@ -327,7 +330,7 @@
|
|
|
327
330
|
? `<input type="checkbox" class="task__select" data-task-id="${escapeHtml(id)}" aria-label="Select task ${escapeHtml(id)} for bulk claim"${selected ? ' checked' : ''}>`
|
|
328
331
|
: `<span class="task__tag" title="Task status">${escapeHtml(t.status || '')}</span>`;
|
|
329
332
|
// Actions: ready → Claim + copy; mine-active → Release; other's active →
|
|
330
|
-
// "claimed · @who"; anything else → nothing (open via the
|
|
333
|
+
// "claimed · @who"; anything else → nothing (open via the title / card).
|
|
331
334
|
let actions = '';
|
|
332
335
|
if (isReady) {
|
|
333
336
|
actions = `<button class="task__claim" type="button" data-claim-task="${escapeHtml(id)}" aria-label="Claim task ${escapeHtml(id)}">Claim</button>`
|
|
@@ -343,10 +346,9 @@
|
|
|
343
346
|
<div class="task__id">
|
|
344
347
|
${lead}
|
|
345
348
|
<span>#${escapeHtml(id)}</span>
|
|
346
|
-
<button class="task__open" type="button" data-task-id="${escapeHtml(id)}" title="Open task details" aria-label="Open details for task ${escapeHtml(id)}">⤢</button>
|
|
347
349
|
</div>
|
|
348
350
|
<div class="task__body">
|
|
349
|
-
<p class="task__title">${escapeHtml(t.title || '')}</p>
|
|
351
|
+
<p class="task__title"><a class="task__link" href="${escapeHtml(taskHref(id))}">${escapeHtml(t.title || '')}</a></p>
|
|
350
352
|
<div class="task__meta">
|
|
351
353
|
<span class="task__tag">${escapeHtml(t.version_id || '?')}</span>
|
|
352
354
|
<span class="task__tag">${escapeHtml(t.kind || 'unclassified')}</span>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Carved out of builders.js by task 1003206 (goal 1000079, the R03 de-monolith
|
|
4
4
|
// plan): that file was 2,558 lines, the largest first-party file left over the
|
|
5
5
|
// 1,500-line ratchet. This holds the view painters — landing, profile, welcome,
|
|
6
|
-
// onboarding, recent ships, the AI-dev curve, the
|
|
6
|
+
// onboarding, recent ships, the AI-dev curve, the nav and hall
|
|
7
7
|
// widgets — plus the helpers and view state only they touch.
|
|
8
8
|
//
|
|
9
9
|
// WHY A FACTORY, and not a flat namespace like goals-lib.js. goals-lib.js can be
|
|
@@ -85,10 +85,13 @@
|
|
|
85
85
|
`</span>` +
|
|
86
86
|
`</span>`;
|
|
87
87
|
}
|
|
88
|
+
// Mirrors dom-utils.js HALL_VIEWS. task 1003441 dropped the `leaderboard`
|
|
89
|
+
// key with the view: no widget can declare view:’leaderboard’ any more, so
|
|
90
|
+
// widgetHost() could never have read it, and index.html no longer carries the
|
|
91
|
+
// #zone-leaderboard mount it named.
|
|
88
92
|
const VIEW_CONTAINERS = {
|
|
89
93
|
home: 'zone-home-main',
|
|
90
94
|
standing: 'zone-standing',
|
|
91
|
-
leaderboard: 'zone-leaderboard',
|
|
92
95
|
};
|
|
93
96
|
const STANDING_GROUPS = [
|
|
94
97
|
{ key: 'work', label: 'Work' },
|
|
@@ -1099,99 +1102,10 @@
|
|
|
1099
1102
|
kindsEl.innerHTML = '';
|
|
1100
1103
|
}
|
|
1101
1104
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
'streak-3': '🔥',
|
|
1107
|
-
'streak-7': '🌋',
|
|
1108
|
-
'art-pipeline-shipper': '🎨',
|
|
1109
|
-
'gds-shipper': '⚙️',
|
|
1110
|
-
'breaking-the-100c-bar': '💯',
|
|
1111
|
-
};
|
|
1112
|
-
function renderAchievementRow(ids) {
|
|
1113
|
-
if (!Array.isArray(ids) || ids.length === 0) return '';
|
|
1114
|
-
const inner = ids
|
|
1115
|
-
.map((id) => {
|
|
1116
|
-
const emoji = ACH_EMOJI[id];
|
|
1117
|
-
if (!emoji) return '';
|
|
1118
|
-
// Title attribute is the achievement id (kebab) — terse enough that hover
|
|
1119
|
-
// discloses the laurel without overstating the prose.
|
|
1120
|
-
return `<span class="lb-ach" title="${escapeHtml(id)}" aria-label="${escapeHtml(id)}">${emoji}</span>`;
|
|
1121
|
-
})
|
|
1122
|
-
.join('');
|
|
1123
|
-
return inner ? `<span class="lb-ach-row" aria-label="achievements">${inner}</span>` : '';
|
|
1124
|
-
}
|
|
1125
|
-
const lbState = {
|
|
1126
|
-
rows: [],
|
|
1127
|
-
sortBy: 'drachmae', // 'rank' | 'drachmae' | 'karma' | 'tokens'
|
|
1128
|
-
sortDir: 'desc', // 'desc' | 'asc'
|
|
1129
|
-
};
|
|
1130
|
-
const lbPager = window.OTBKit.makePager('lb', 'otb.standing.leaderboardPerPage', { defaultPerPage: 25 });
|
|
1131
|
-
function sortedLeaderboardRows() {
|
|
1132
|
-
const indexed = lbState.rows.map((row, idx) => ({ row, idx }));
|
|
1133
|
-
const dir = lbState.sortDir === 'asc' ? 1 : -1;
|
|
1134
|
-
if (lbState.sortBy === 'rank') {
|
|
1135
|
-
return indexed
|
|
1136
|
-
.sort((a, b) => {
|
|
1137
|
-
const diff = (rankWeight(a.row.rank) - rankWeight(b.row.rank)) * dir;
|
|
1138
|
-
return diff !== 0 ? diff : a.idx - b.idx;
|
|
1139
|
-
})
|
|
1140
|
-
.map(({ row }) => row);
|
|
1141
|
-
}
|
|
1142
|
-
if (lbState.sortBy === 'karma') {
|
|
1143
|
-
return indexed
|
|
1144
|
-
.sort((a, b) => {
|
|
1145
|
-
const ak = Number(a.row.karma) || 0;
|
|
1146
|
-
const bk = Number(b.row.karma) || 0;
|
|
1147
|
-
const diff = (ak - bk) * dir;
|
|
1148
|
-
if (diff !== 0) return diff;
|
|
1149
|
-
// Tie-break on drachmae DESC, then stable on server order.
|
|
1150
|
-
const ac = Number(a.row.total_credits) || 0;
|
|
1151
|
-
const bc = Number(b.row.total_credits) || 0;
|
|
1152
|
-
if (ac !== bc) return bc - ac;
|
|
1153
|
-
return a.idx - b.idx;
|
|
1154
|
-
})
|
|
1155
|
-
.map(({ row }) => row);
|
|
1156
|
-
}
|
|
1157
|
-
if (lbState.sortBy === 'tokens') {
|
|
1158
|
-
// task 1473: sort by total tokens used. tokens_used arrives as a string
|
|
1159
|
-
// (pg bigint), so coerce. Tie-break on drachmae DESC then server order,
|
|
1160
|
-
// matching the karma branch.
|
|
1161
|
-
return indexed
|
|
1162
|
-
.sort((a, b) => {
|
|
1163
|
-
const at = Number(a.row.tokens_used) || 0;
|
|
1164
|
-
const bt = Number(b.row.tokens_used) || 0;
|
|
1165
|
-
const diff = (at - bt) * dir;
|
|
1166
|
-
if (diff !== 0) return diff;
|
|
1167
|
-
const ac = Number(a.row.total_credits) || 0;
|
|
1168
|
-
const bc = Number(b.row.total_credits) || 0;
|
|
1169
|
-
if (ac !== bc) return bc - ac;
|
|
1170
|
-
return a.idx - b.idx;
|
|
1171
|
-
})
|
|
1172
|
-
.map(({ row }) => row);
|
|
1173
|
-
}
|
|
1174
|
-
if (lbState.sortBy === 'drachmae') {
|
|
1175
|
-
// Server already DESCs by drachmae, so the default mode is a no-op
|
|
1176
|
-
// copy. The ASC branch flips it.
|
|
1177
|
-
if (lbState.sortDir === 'desc') return lbState.rows.slice();
|
|
1178
|
-
return indexed
|
|
1179
|
-
.sort((a, b) => {
|
|
1180
|
-
const ac = Number(a.row.total_credits) || 0;
|
|
1181
|
-
const bc = Number(b.row.total_credits) || 0;
|
|
1182
|
-
const diff = (ac - bc) * dir;
|
|
1183
|
-
return diff !== 0 ? diff : a.idx - b.idx;
|
|
1184
|
-
})
|
|
1185
|
-
.map(({ row }) => row);
|
|
1186
|
-
}
|
|
1187
|
-
return lbState.rows.slice();
|
|
1188
|
-
}
|
|
1189
|
-
const LB_SORT_HEADERS = {
|
|
1190
|
-
rank: 'lb-sort-rank',
|
|
1191
|
-
drachmae: 'lb-sort-drachmae',
|
|
1192
|
-
karma: 'lb-sort-karma',
|
|
1193
|
-
tokens: 'lb-sort-tokens',
|
|
1194
|
-
};
|
|
1105
|
+
// The Roll’s rows, kept ONLY for leaderboardPlace() below — the Standing
|
|
1106
|
+
// card’s "#N on the leaderboard" stat. sortBy/sortDir went to /roster with
|
|
1107
|
+
// the table (task 1003441); nothing on this page sorts them any more.
|
|
1108
|
+
const lbState = { rows: [] };
|
|
1195
1109
|
function leaderboardPlace(login) {
|
|
1196
1110
|
const rows = Array.isArray(lbState.rows) ? lbState.rows : [];
|
|
1197
1111
|
if (!rows.length || !login) return null;
|
|
@@ -1201,53 +1115,7 @@
|
|
|
1201
1115
|
const idx = ranked.findIndex((r) => r.github_login === login);
|
|
1202
1116
|
return idx === -1 ? null : { place: idx + 1, total: ranked.length };
|
|
1203
1117
|
}
|
|
1204
|
-
function renderLeaderboard() {
|
|
1205
|
-
const rows = sortedLeaderboardRows();
|
|
1206
|
-
// paginate() clamps the page, slices, and paints the whole #lb-* bar.
|
|
1207
|
-
const slice = window.OTBKit.paginate(rows, lbPager);
|
|
1208
|
-
const start = (lbPager.page - 1) * lbPager.perPage;
|
|
1209
|
-
const tbody = $('#leaderboard tbody');
|
|
1210
|
-
|
|
1211
|
-
if (!slice.length) {
|
|
1212
|
-
tbody.innerHTML = emptyRow(6, 'No more builders to show.');
|
|
1213
|
-
} else {
|
|
1214
|
-
tbody.innerHTML = slice.map((b, i) => {
|
|
1215
|
-
const place = start + i + 1;
|
|
1216
|
-
const name = b.display_name || b.github_login || 'unknown builder';
|
|
1217
|
-
const badges = renderAchievementRow(b.achievements);
|
|
1218
|
-
const rank = renderRankBadge(normalizeRank(b.rank));
|
|
1219
|
-
// task 1467: link the name to the builder's public profile page. Guarded
|
|
1220
|
-
// on b.id (the /leaderboard row carries it) so a row without an id degrades
|
|
1221
|
-
// to plain text rather than a broken /profile?id=undefined link.
|
|
1222
|
-
const nameCell = b.id != null
|
|
1223
|
-
? `<a class="lb-name" href="/profile?id=${encodeURIComponent(b.id)}">${escapeHtml(name)}</a>`
|
|
1224
|
-
: escapeHtml(name);
|
|
1225
|
-
return `
|
|
1226
|
-
<tr>
|
|
1227
|
-
<td class="ledger__num">${place}</td>
|
|
1228
|
-
<td>${nameCell} <span style="color:var(--ink-faint);font-size:0.86rem">@${escapeHtml(b.github_login || '')}</span>${badges}</td>
|
|
1229
|
-
<td class="ledger__rank">${rank}</td>
|
|
1230
|
-
<td class="ledger__drachmae">${fmtNum(b.total_credits)}</td>
|
|
1231
|
-
<td class="ledger__drachmae">${fmtNum(b.karma)}</td>
|
|
1232
|
-
<td class="ledger__drachmae" title="${Number(b.total_credits) > 0 ? `${fmtNum(Math.round(Number(b.tokens_used) / Number(b.total_credits)))} tokens per drachma` : 'no drachmae earned yet'}">${fmtNum(b.tokens_used)}</td>
|
|
1233
|
-
</tr>
|
|
1234
|
-
`;
|
|
1235
|
-
}).join('');
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// Reflect the active sort on every sortable header. Only one column
|
|
1239
|
-
// shows its arrow at a time; the others reset to "none" — matches the
|
|
1240
|
-
// ARIA-sort spec.
|
|
1241
|
-
for (const [key, headerId] of Object.entries(LB_SORT_HEADERS)) {
|
|
1242
|
-
const head = document.getElementById(headerId);
|
|
1243
|
-
if (!head) continue;
|
|
1244
|
-
const ariaSort = (lbState.sortBy === key && lbState.sortDir)
|
|
1245
|
-
? (lbState.sortDir === 'asc' ? 'ascending' : 'descending')
|
|
1246
|
-
: 'none';
|
|
1247
|
-
head.setAttribute('aria-sort', ariaSort);
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
1118
|
|
|
1251
|
-
return {
|
|
1119
|
+
return { SETUP_CONFIG_KEY, analyticsState, lbState, mountHallWidgets, onboardingBox, renderCurveBody, renderLanding, renderNav, renderOnboarding, renderProfile, renderRecentShips, renderWelcomeCore, shipState, shipsPager, welcomeDismissalKey };
|
|
1252
1120
|
};
|
|
1253
1121
|
})();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Every widget — core here, module-contributed from a module's own client script —
|
|
4
4
|
// registers into the window.OTB hall-widget registry (dom-utils.js), and builders.js
|
|
5
5
|
// mounts the registered set into the shell's view containers (task 1723 redesign):
|
|
6
|
-
// view: 'home' | 'standing'
|
|
6
|
+
// view: 'home' | 'standing' (default 'home')
|
|
7
7
|
// zone: a finer mount target inside the Home view
|
|
8
8
|
// ('home-banner' | 'home-stats' | 'home-band' | 'home-main')
|
|
9
9
|
// group: a finer mount target inside Standing — which rail section this card
|
|
@@ -198,19 +198,9 @@
|
|
|
198
198
|
'<div class="ach-grid" id="ach-grid"><div class="profile__placeholder">…</div></div>',
|
|
199
199
|
{ view: 'standing', group: 'achievements', labelledby: 'ach-h' });
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
'<table class="ledger" id="leaderboard"><thead><tr>' +
|
|
207
|
-
'<th>Place</th><th>Builder</th>' +
|
|
208
|
-
'<th class="ledger__sortable" id="lb-sort-rank" aria-sort="none" title="Sort by rank">Rank</th>' +
|
|
209
|
-
'<th class="ledger__sortable" id="lb-sort-drachmae" aria-sort="descending" title="Sort by ' + curLabel() + ' (work shipped)" data-currency-label="caps">Credits</th>' +
|
|
210
|
-
'<th class="ledger__sortable" id="lb-sort-karma" aria-sort="none" title="Sort by karma (peer recognition + approvals)">Karma</th>' +
|
|
211
|
-
'<th class="ledger__sortable" id="lb-sort-tokens" aria-sort="none" title="Sort by tokens used across recorded sessions — efficiency is tokens ÷ credits (hover a row for tokens-per-drachma)">Tokens</th>' +
|
|
212
|
-
'</tr></thead><tbody><tr><td colspan="6" class="ledger__empty">…</td></tr></tbody></table>' +
|
|
213
|
-
// task 2174: OTBKit pager — same #lb-* id prefix as the bar it replaces.
|
|
214
|
-
window.OTBKit.pagerBarHtml('lb', { label: 'builders' }),
|
|
215
|
-
{ view: 'leaderboard', labelledby: 'lb-h' });
|
|
201
|
+
// task 1003441: the Leaderboard widget is GONE from this page. It is a real
|
|
202
|
+
// page now (/roster — roster.html + roster.js), which is what the Community
|
|
203
|
+
// nav group needed: it used to read as one page plus a link into index.html.
|
|
204
|
+
// Nothing here declares view:'leaderboard' any more, and dom-utils.js's
|
|
205
|
+
// HALL_VIEWS no longer accepts it.
|
|
216
206
|
})();
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
"_feeds": "As work.states.json: the kit's stub is not the hall's feed set, so Home renders its skeletons and empty states here. The populated dashboard is the hall-preview harness.",
|
|
8
8
|
"ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/me/", "/api/(gds|bongos)/tasks", "/api/(gds|bongos)/claims", "/api/(gds|bongos)/goals", "/api/(gds|bongos)/inbox", "/api/(gds|bongos)/live", "/api/(gds|bongos)/analytics/", "/api/(gds|bongos)/achievements", "/api/(gds|bongos)/versions", "/api/(gds|bongos)/leaderboard"],
|
|
9
9
|
"ignoreConsole": ["/api/(gds|bongos)/", "\b401\b", "\b404\b", "Unauthorized", "Not Found", "load failed"],
|
|
10
|
+
"_1003441": "The `leaderboard` state left with the view: the Roll is the /roster page now and #leaderboard REDIRECTS there, so there is no #view-leaderboard on this page to make visible. The redirect is proven in tests/hall_nav.mjs (it asserts location.replace('/roster') fires on the index and nowhere else) rather than by a render state, because a state whose page navigates away measures the destination, not this page. `standing-karma` replaces the coverage: task 2178 made each Standing rail section addressable and none had a measured state.",
|
|
10
11
|
"states": {
|
|
11
|
-
"home": { "auth": true, "expect": { "visible": ["#view-home"], "hidden": ["#view-standing"
|
|
12
|
+
"home": { "auth": true, "expect": { "visible": ["#view-home"], "hidden": ["#view-standing"] } },
|
|
12
13
|
"standing": { "auth": true, "url": "/builders/#standing", "actions": [["wait", 400]], "expect": { "visible": ["#view-standing"] } },
|
|
13
|
-
"
|
|
14
|
+
"standing-karma": { "auth": true, "url": "/builders/#standing-karma", "actions": [["wait", 400]], "expect": { "visible": ["#view-standing"] } },
|
|
14
15
|
"out": { "auth": false }
|
|
15
16
|
},
|
|
16
17
|
"reducedMotion": { "auth": true }
|
|
@@ -555,7 +555,7 @@
|
|
|
555
555
|
head.innerHTML = `
|
|
556
556
|
<div class="profile__placeholder">
|
|
557
557
|
No builder selected. Open a builder from the
|
|
558
|
-
<a href="
|
|
558
|
+
<a href="/roster">roster</a>, or
|
|
559
559
|
<a href="${API}/auth/web/start">sign in</a> to see your own profile.
|
|
560
560
|
</div>`;
|
|
561
561
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Builders — {{worldName}}</title>
|
|
7
|
+
<meta name="description" content="Who builds on this project: the roster, ranked by work shipped, with rank, credits, karma and tokens.">
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
+
<!-- task 1003441: the identity cell (avatar, name, login) is shared with the
|
|
13
|
+
cross-project directory — one definition, both rosters. -->
|
|
14
|
+
<link rel="stylesheet" href="/builders/people.css">
|
|
15
|
+
</head>
|
|
16
|
+
<body data-page="roster" data-page-title="Builders">
|
|
17
|
+
|
|
18
|
+
<div class="app">
|
|
19
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
20
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
21
|
+
<div class="app-body">
|
|
22
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
23
|
+
<div class="app-content">
|
|
24
|
+
<!-- The board page head (task 1003307): the label voice. The sub is
|
|
25
|
+
rewritten by roster.js once the roster has loaded. -->
|
|
26
|
+
<div class="page-head page-head--board">
|
|
27
|
+
<h1 class="page-head__h">Builders</h1>
|
|
28
|
+
<p class="page-head__sub" id="roster-sub">Who builds on this project, ranked by work shipped. This project's own ledger — unlike the cross-project directory, it lists everyone who builds here.</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<main id="hall-main">
|
|
32
|
+
<!-- task 1003441: this was the #leaderboard HASH VIEW on the home page,
|
|
33
|
+
which is why the Community group used to read as one real page plus a
|
|
34
|
+
link into index.html. It is a page now; /#leaderboard redirects here. -->
|
|
35
|
+
<section class="scroll" id="roster-scroll" aria-labelledby="roster-h">
|
|
36
|
+
<h2 id="roster-h" class="scroll__h">The roster<span class="count-pill" id="roster-count" hidden></span></h2>
|
|
37
|
+
<div id="roster-filters"></div>
|
|
38
|
+
<div id="roster-body">
|
|
39
|
+
<div class="profile__placeholder">Loading the roster…</div>
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
</main>
|
|
43
|
+
|
|
44
|
+
<footer class="app-foot" id="app-foot"></footer>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
50
|
+
|
|
51
|
+
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
52
|
+
chrome exists before the page script runs. -->
|
|
53
|
+
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
54
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
55
|
+
<script src="/builders/palette.js" defer></script>
|
|
56
|
+
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
57
|
+
branding pack's currency label, loaded BEFORE roster.js. -->
|
|
58
|
+
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
59
|
+
<script src="/builders/brand-apply.js"></script>
|
|
60
|
+
<script src="/builders/hall-kit.js"></script>
|
|
61
|
+
<script src="/builders/roster.js"></script>
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|