@bongos/core 1.19.473 → 1.19.475
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 -31
- package/docs/adr/0228-the-reading-room-owes-a-reference-content-rules-not-a-second-archetype.md +44 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +144 -140
- package/docs/copy-registry.json +262 -206
- 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/drachmae.html +50 -44
- package/modules/hall-ui/public/drachmae.js +147 -59
- package/modules/hall-ui/public/drachmae.states.json +20 -26
- 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/md-reader.js +10 -4
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/room.css +116 -3
- 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/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_nav.mjs +36 -1
- package/tests/hall_page_gate_map.mjs +8 -0
- package/tests/hall_reference_world.mjs +222 -0
- package/tests/rank_tier_single_source.mjs +8 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "Drachmae & karma —
|
|
2
|
+
"_": "Drachmae & karma — the hall's first REFERENCE room (task 1003473): the reading-room archetype (room.css) with the reference rules on (`room--reference`). Public (no page gate), so the signed-out render is the one most readers get. Measured at three widths in both modes rather than looked at once.",
|
|
3
3
|
"page": "/builders/drachmae",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
5
|
"stub": {
|
|
6
6
|
"prefix": "/builders/"
|
|
7
7
|
},
|
|
8
8
|
"modeQuery": false,
|
|
9
|
-
"_feeds": "The kit's own stub
|
|
9
|
+
"_feeds": "The kit's own stub serves neither the markdown nor the bounty feed, so against it the page renders its SKELETON and then its FAILURE note — states the floors must hold in. The POPULATED document, the outline, the anchors and the live table need the hall-preview harness (node scripts/hall-preview/server.js --port 4620 --fixture-me, then this file with --base-in/--base-out pointed at it): it serves docs/onboarding/drachmae-and-karma.md and the checked-in public__bounty-table.json. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
|
|
10
10
|
"ignoreRequests": [
|
|
11
11
|
"/api/(gds|bongos)/me$",
|
|
12
12
|
"/api/(gds|bongos)/me/",
|
|
@@ -26,39 +26,33 @@
|
|
|
26
26
|
"Not Implemented",
|
|
27
27
|
"load failed"
|
|
28
28
|
],
|
|
29
|
+
"_expect": "read pins the article, so this file is run against the harness (the kit stub can only ever show the failure note). jump is the deep link: the browser's own hash jump finds nothing on a page whose document arrives after load, so drachmae.js lands it, and the outline's lit entry with its trail, plus the phone toggle naming the part, are what the shot proves.",
|
|
29
30
|
"states": {
|
|
30
31
|
"read": {
|
|
31
32
|
"auth": true,
|
|
32
|
-
"actions": [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
],
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
"actions": [["wait", 600]],
|
|
34
|
+
"expect": { "visible": ["#primer-article", "#primer-toc"] }
|
|
35
|
+
},
|
|
36
|
+
"toc-open": {
|
|
37
|
+
"auth": true,
|
|
38
|
+
"actions": [["clickIfVisible", "#primer-toc-toggle"], ["wait", 250]],
|
|
39
|
+
"_": "the room's one disclosure. Below 760px the contents collapse behind this toggle; above it the toggle is not rendered at all, so clickIfVisible is the right verb and the state is a no-op at 1440.",
|
|
40
|
+
"expect": { "visible": ["#primer-toc"] }
|
|
41
|
+
},
|
|
42
|
+
"jump": {
|
|
43
|
+
"auth": true,
|
|
44
|
+
"url": "/builders/drachmae#how-karma-is-earned-and-lost",
|
|
45
|
+
"actions": [["wait", 700]],
|
|
46
|
+
"_": "a deep link to an h3 under Part 3. At 1440 the outline shows the entry lit and its part in the trail; at 390 the collapsed toggle reads the part's name.",
|
|
47
|
+
"expect": { "visible": ["#primer-article", "#primer-toc"] }
|
|
44
48
|
},
|
|
45
49
|
"out": {
|
|
46
50
|
"auth": false,
|
|
47
|
-
"actions": [
|
|
48
|
-
[
|
|
49
|
-
"wait",
|
|
50
|
-
600
|
|
51
|
-
]
|
|
52
|
-
]
|
|
51
|
+
"actions": [["wait", 600]]
|
|
53
52
|
}
|
|
54
53
|
},
|
|
55
54
|
"reducedMotion": {
|
|
56
55
|
"auth": true,
|
|
57
|
-
"actions": [
|
|
58
|
-
[
|
|
59
|
-
"wait",
|
|
60
|
-
600
|
|
61
|
-
]
|
|
62
|
-
]
|
|
56
|
+
"actions": [["wait", 600]]
|
|
63
57
|
}
|
|
64
58
|
}
|
|
@@ -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 }
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
// Stable slug for heading anchors — lowercase, strip punctuation, dash-join.
|
|
34
34
|
// Matches the primer's slugify so in-doc anchor links behave identically.
|
|
35
35
|
function slugify(text) {
|
|
36
|
+
// A [link](target) in a heading slugs by its TEXT alone (task 1003473):
|
|
37
|
+
// the target's path is not part of the address a reader copies.
|
|
38
|
+
text = String(text).replace(/\[([^\]]+)\]\([^)]*\)/g, '$1');
|
|
36
39
|
return String(text)
|
|
37
40
|
.toLowerCase()
|
|
38
41
|
.replace(/[—–]/g, '-')
|
|
@@ -113,7 +116,9 @@
|
|
|
113
116
|
const attrs = external ? ' target="_blank" rel="noopener"' : '';
|
|
114
117
|
return `<a href="${escapeHtml(resolved)}"${attrs}>${text}</a>`;
|
|
115
118
|
});
|
|
116
|
-
|
|
119
|
+
// Bold first, and lazily, so an *emphasis* INSIDE a bold run still renders
|
|
120
|
+
// (the "**It's the API *list* price**" line showed its asterisks before).
|
|
121
|
+
out = out.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
117
122
|
out = out.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1<em>$2</em>');
|
|
118
123
|
out = out.replace(/ CODE(\d+) /g, (_m, idx) => `<code>${escapeHtml(codeSpans[Number(idx)])}</code>`);
|
|
119
124
|
return out;
|
|
@@ -147,9 +152,10 @@
|
|
|
147
152
|
continue;
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
// Headings
|
|
155
|
+
// Headings, to h4: the economy doc's six earning sources are #### and
|
|
156
|
+
// rendered as paragraphs with a literal "####" until task 1003473.
|
|
151
157
|
let hm;
|
|
152
|
-
if ((hm = raw.match(/^(#{1,
|
|
158
|
+
if ((hm = raw.match(/^(#{1,4})\s+(.+?)\s*$/))) {
|
|
153
159
|
const level = hm[1].length;
|
|
154
160
|
const text = hm[2];
|
|
155
161
|
const id = slugify(text);
|
|
@@ -215,7 +221,7 @@
|
|
|
215
221
|
i += 1;
|
|
216
222
|
while (i < lines.length &&
|
|
217
223
|
!/^\s*$/.test(lines[i]) &&
|
|
218
|
-
!/^(#{1,
|
|
224
|
+
!/^(#{1,4})\s/.test(lines[i]) &&
|
|
219
225
|
!/^---+\s*$/.test(lines[i]) &&
|
|
220
226
|
!/^```/.test(lines[i]) &&
|
|
221
227
|
!/^>\s?/.test(lines[i]) &&
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* room.css — the READING-ROOM archetype: a long document read end to end
|
|
2
|
-
(task 1003306, goal 1000081). Shared by the primer (primer.html)
|
|
3
|
-
diagrams index (diagrams.html)
|
|
2
|
+
(task 1003306, goal 1000081). Shared by the primer (primer.html), the
|
|
3
|
+
diagrams index (diagrams.html) and, as the hall's first REFERENCE, drachmae
|
|
4
|
+
(task 1003473; §8 below is what the room owes a document that is scanned).
|
|
4
5
|
|
|
5
6
|
THE ARCHETYPE is v3 "the glass hall", the owner's pick from the composition
|
|
6
7
|
round — docs/design/hall-direction-v2.md §6, mock
|
|
@@ -203,7 +204,8 @@ body.room .page-head { max-width: var(--room-max); }
|
|
|
203
204
|
.primer-article > ol,
|
|
204
205
|
.primer-article > blockquote,
|
|
205
206
|
.primer-article > h2,
|
|
206
|
-
.primer-article > h3
|
|
207
|
+
.primer-article > h3,
|
|
208
|
+
.primer-article > h4 { max-width: var(--measure); }
|
|
207
209
|
|
|
208
210
|
/* the document's own h1 is suppressed — the page head already names the room,
|
|
209
211
|
and a second h1 in <main> fails the one-header-main-h1 floor outright */
|
|
@@ -244,6 +246,20 @@ body.room .page-head { max-width: var(--room-max); }
|
|
|
244
246
|
scroll-margin-top: calc(var(--topbar-h) + 28px);
|
|
245
247
|
}
|
|
246
248
|
|
|
249
|
+
/* the fourth level: a run-in head in the body face at the body size, one weight
|
|
250
|
+
step up. The parser only reached h3 until task 1003473, so the economy doc's
|
|
251
|
+
six earning sources rendered as paragraphs with a literal "####". */
|
|
252
|
+
.primer-article h4 {
|
|
253
|
+
margin: var(--sp-lg) 0 var(--sp-xs);
|
|
254
|
+
font-family: var(--font-body);
|
|
255
|
+
font-size: var(--room-size);
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
line-height: 1.4;
|
|
258
|
+
letter-spacing: -0.01em;
|
|
259
|
+
color: var(--ink);
|
|
260
|
+
scroll-margin-top: calc(var(--topbar-h) + 28px);
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
.primer-article p { margin: 0 0 var(--sp-md); }
|
|
248
264
|
.primer-article p:last-child { margin-bottom: 0; }
|
|
249
265
|
/* the subtitle line at the very top of the document */
|
|
@@ -439,3 +455,100 @@ body.room .page-head { max-width: var(--room-max); }
|
|
|
439
455
|
overflow-x: auto;
|
|
440
456
|
}
|
|
441
457
|
}
|
|
458
|
+
|
|
459
|
+
/* ---- 8. the reference ----------------------------------------------------
|
|
460
|
+
The same room, a second document (task 1003473). The primer is read once,
|
|
461
|
+
start to finish, with a gate at the end; drachmae is a REFERENCE that people
|
|
462
|
+
open to answer one question about how they get paid. Nothing about the
|
|
463
|
+
composition changes — the head, the glass contents beside the card, the
|
|
464
|
+
measure — because the room is right for both. What changes is what the room
|
|
465
|
+
does for a reader who is looking rather than reading, and `room--reference`
|
|
466
|
+
on the body switches those content rules on:
|
|
467
|
+
* the counters are off (no numerals in the contents, no "01" over a
|
|
468
|
+
heading), because a reference has no order to keep;
|
|
469
|
+
* the contents nest h3s under their h2s, since the sub-questions are what
|
|
470
|
+
a reader scans for, and the spy lights the entry AND its part (.is-trail);
|
|
471
|
+
* every heading carries its own link (.room-anchor), always visible in the
|
|
472
|
+
faint ink — an address is part of the content, not a hover secret — and
|
|
473
|
+
in the accent when it is the one the reader arrived at (:target);
|
|
474
|
+
* the phone toggle's middle names the part the reader is in.
|
|
475
|
+
There is no progress hairline on a reference: the page omits the markup and
|
|
476
|
+
§1 above stays for the primer, whose gate is fed by it. Why a modifier and
|
|
477
|
+
not a second archetype: the third document that is scanned (the ranks, the
|
|
478
|
+
API docs) takes these four rules by one body class, and a sheet of its own
|
|
479
|
+
would be room.css again with the counter deleted. */
|
|
480
|
+
.room--reference .primer-article h2::before,
|
|
481
|
+
.room--reference .primer-toc__list a::before { content: none; }
|
|
482
|
+
.room--reference .primer-toc__list a.is-trail { color: var(--ink); }
|
|
483
|
+
.room--reference .primer-toc__sub {
|
|
484
|
+
list-style: none;
|
|
485
|
+
margin: 0 0 6px var(--sp-xs);
|
|
486
|
+
padding: 0 0 0 10px;
|
|
487
|
+
border-left: var(--line) solid var(--rule-soft);
|
|
488
|
+
}
|
|
489
|
+
.room--reference .primer-toc__sub a {
|
|
490
|
+
min-height: 30px;
|
|
491
|
+
padding: 6px var(--sp-xs);
|
|
492
|
+
font-size: 13px;
|
|
493
|
+
color: var(--ink-faint);
|
|
494
|
+
}
|
|
495
|
+
.room--reference .primer-toc__sub a:hover,
|
|
496
|
+
.room--reference .primer-toc__sub a.is-active { color: var(--ink); }
|
|
497
|
+
|
|
498
|
+
/* the toggle's middle: the part the reader is in, in the faint ink, cut with an
|
|
499
|
+
ellipsis before it can push the chevron off the row. Empty until the spy has
|
|
500
|
+
something to say, and then it takes no room at all. */
|
|
501
|
+
.room--reference .primer-toc__toggle-label { flex: none; }
|
|
502
|
+
.room--reference .primer-toc__current {
|
|
503
|
+
flex: 1 1 auto;
|
|
504
|
+
min-width: 0;
|
|
505
|
+
overflow: hidden;
|
|
506
|
+
text-overflow: ellipsis;
|
|
507
|
+
white-space: nowrap;
|
|
508
|
+
text-align: right;
|
|
509
|
+
font-size: 13px;
|
|
510
|
+
color: var(--ink-faint);
|
|
511
|
+
}
|
|
512
|
+
.room--reference .primer-toc__current:empty { display: none; }
|
|
513
|
+
|
|
514
|
+
/* the address. Outranks the prose-link rule (.primer-article a): no underline,
|
|
515
|
+
no padding, a 24px box of its own so the floor holds without the padding
|
|
516
|
+
trick, and the glass under it on hover so the target reads as one. */
|
|
517
|
+
.room--reference .primer-article .room-anchor {
|
|
518
|
+
display: inline-flex;
|
|
519
|
+
align-items: center;
|
|
520
|
+
justify-content: center;
|
|
521
|
+
width: 24px;
|
|
522
|
+
height: 24px;
|
|
523
|
+
margin-left: 6px;
|
|
524
|
+
padding: 0;
|
|
525
|
+
vertical-align: middle;
|
|
526
|
+
color: var(--ink-faint);
|
|
527
|
+
text-decoration: none;
|
|
528
|
+
border-radius: var(--radius-sm);
|
|
529
|
+
transition: color 200ms cubic-bezier(.2, .7, .3, 1), background 200ms cubic-bezier(.2, .7, .3, 1);
|
|
530
|
+
}
|
|
531
|
+
.room--reference .primer-article .room-anchor:hover { color: var(--accent-ink); background: var(--glass); }
|
|
532
|
+
/* the one the reader arrived at. :target alone misses a deep link into a
|
|
533
|
+
document that renders after load, so drachmae.js marks the landing itself. */
|
|
534
|
+
.room--reference .primer-article :is(:target, .is-target) > .room-anchor { color: var(--accent-ink); }
|
|
535
|
+
|
|
536
|
+
/* On a phone the collapsed outline rides under the top bar, so the reader can
|
|
537
|
+
open it from anywhere in the document and read which part they are in. The
|
|
538
|
+
toggle is glass, the same material as the bar it sits under, and below the
|
|
539
|
+
bar in the z ladder for the same reason the progress hairline is. */
|
|
540
|
+
@media (max-width: 760px) {
|
|
541
|
+
.room--reference .primer-toc {
|
|
542
|
+
position: sticky;
|
|
543
|
+
top: calc(var(--topbar-h) + 8px);
|
|
544
|
+
z-index: var(--z-progress);
|
|
545
|
+
}
|
|
546
|
+
/* Riding over the article, the toggle and the open list take the card's
|
|
547
|
+
ground at 92% rather than the bare glass: light's glass has no blur, so
|
|
548
|
+
over scrolling prose the bare tint was the prose showing through the pill
|
|
549
|
+
(measured on the deep-link render at 390). The edge stays the glass edge. */
|
|
550
|
+
.room--reference .primer-toc__toggle,
|
|
551
|
+
.room--reference .primer-toc__inner { background: color-mix(in srgb, var(--bg-card) 92%, transparent); }
|
|
552
|
+
/* a landed heading has to clear the sticky toggle as well as the top bar */
|
|
553
|
+
.room--reference .primer-article :is(h2, h3, h4) { scroll-margin-top: calc(var(--topbar-h) + 72px); }
|
|
554
|
+
}
|
|
@@ -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>
|