@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
|
@@ -1383,5 +1383,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1383
1383
|
landed since 1.19.471 with no explicit bump. run 33646171957. (task 1002620)
|
|
1384
1384
|
1.19.473 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1385
1385
|
landed since 1.19.472 with no explicit bump. run 33646919877. (task 1002620)
|
|
1386
|
+
1.19.474 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
1386
1390
|
---------------------------------------------------------------------------
|
|
1387
1391
|
```
|
|
@@ -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) {
|
|
@@ -9,18 +9,17 @@
|
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
-
<!--
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
prose-link 24px floor, no table wrapper. Loading it is the fix; the order
|
|
16
|
-
matches primer.html (room first, the page's own sheet on top). -->
|
|
12
|
+
<!-- The reading-room archetype is the whole of this page's sheet. primer.css
|
|
13
|
+
holds only the acknowledge gate, which a reference has no use for, so it
|
|
14
|
+
is no longer loaded here (task 1003473; it had been since task 1003307). -->
|
|
17
15
|
<link rel="stylesheet" href="/builders/room.css">
|
|
18
|
-
<link rel="stylesheet" href="/builders/primer.css?v=2026-07-02-shell1">
|
|
19
16
|
</head>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
<!-- `room` opts into the archetype's composition tokens (room.css); they sit on
|
|
18
|
+
the body, never in a page :root. `room--reference` is what the room owes a
|
|
19
|
+
document people SCAN rather than read (task 1003473): the outline goes to
|
|
20
|
+
h3 depth, every heading carries its own link, and the linear-read
|
|
21
|
+
instruments (the progress hairline, the section counters) are off. -->
|
|
22
|
+
<body class="room room--reference" data-page="drachmae" data-page-title="Drachmae & karma">
|
|
24
23
|
|
|
25
24
|
<div class="app">
|
|
26
25
|
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
@@ -29,59 +28,66 @@
|
|
|
29
28
|
<header class="app-topbar" id="app-topbar"></header>
|
|
30
29
|
<div class="app-content">
|
|
31
30
|
|
|
32
|
-
<!--
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
it
|
|
31
|
+
<!-- The reading room announces its document (task 1003306's archetype):
|
|
32
|
+
the label voice on a one-word eyebrow, then the room's name lowercase
|
|
33
|
+
in the display face. The sub line says what KIND of document this is,
|
|
34
|
+
not how long it takes: nobody reads a reference to the end. -->
|
|
36
35
|
<div class="page-head page-head--room">
|
|
37
36
|
<p class="page-head__eyebrow">Docs</p>
|
|
38
37
|
<h1 class="page-head__h">Drachmae & karma</h1>
|
|
39
|
-
<p class="page-head__sub">How you get paid, and how you earn trust
|
|
38
|
+
<p class="page-head__sub">How you get paid, and how you earn trust. A reference to jump around in: every section has its own link, and the bounty rates are read live.</p>
|
|
40
39
|
</div>
|
|
41
40
|
|
|
42
41
|
<main id="hall-main">
|
|
43
42
|
<div class="primer-layout">
|
|
44
43
|
|
|
45
|
-
<!--
|
|
46
|
-
|
|
44
|
+
<!-- The outline. Built from the article's h2s AND h3s once the
|
|
45
|
+
markdown renders: a reference is found by its sub-questions, not
|
|
46
|
+
read in order, so the contents go a level deeper than the
|
|
47
|
+
primer's and carry no numerals. Under 760px it collapses behind a
|
|
48
|
+
toggle whose middle names the part the reader is in. -->
|
|
49
|
+
<aside class="primer-toc" id="primer-toc" aria-label="Sections in this reference">
|
|
47
50
|
<button type="button" class="primer-toc__toggle" id="primer-toc-toggle" aria-expanded="false" aria-controls="primer-toc-list">
|
|
48
|
-
<span>
|
|
49
|
-
<span class="primer-
|
|
51
|
+
<span class="primer-toc__toggle-label">On this page</span>
|
|
52
|
+
<span class="primer-toc__current" id="primer-toc-current"></span>
|
|
53
|
+
<svg class="primer-toc__chevron" viewBox="0 0 10 6" width="10" height="6" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 1.2 5 4.8 9 1.2"/></svg>
|
|
50
54
|
</button>
|
|
51
55
|
<nav class="primer-toc__inner">
|
|
52
|
-
<p class="primer-toc__heading" id="primer-toc-heading">
|
|
56
|
+
<p class="primer-toc__heading" id="primer-toc-heading">On this page</p>
|
|
53
57
|
<ol class="primer-toc__list" id="primer-toc-list" aria-labelledby="primer-toc-heading">
|
|
54
58
|
<li class="primer-toc__placeholder">Loading…</li>
|
|
55
59
|
</ol>
|
|
56
60
|
</nav>
|
|
57
61
|
</aside>
|
|
58
62
|
|
|
59
|
-
<!--
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
63
|
+
<!-- The article column: three panels of the same card at the same
|
|
64
|
+
width (skeleton, document, failure), so the column never changes
|
|
65
|
+
shape as the markdown resolves. -->
|
|
66
|
+
<div class="primer-col">
|
|
67
|
+
|
|
68
|
+
<div class="primer-skeleton scroll" id="primer-skeleton" aria-hidden="true">
|
|
69
|
+
<span class="primer-sk primer-skeleton__h"></span>
|
|
70
|
+
<div class="primer-skeleton__p">
|
|
71
|
+
<span class="primer-sk"></span><span class="primer-sk"></span>
|
|
72
|
+
<span class="primer-sk"></span><span class="primer-sk"></span>
|
|
73
|
+
<span class="primer-sk"></span>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="primer-skeleton__rule"></div>
|
|
76
|
+
<div class="primer-skeleton__p">
|
|
77
|
+
<span class="primer-sk"></span><span class="primer-sk"></span>
|
|
78
|
+
<span class="primer-sk"></span>
|
|
79
|
+
</div>
|
|
74
80
|
</div>
|
|
75
|
-
</div>
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
<article class="primer-article scroll" id="primer-article" aria-busy="true" hidden></article>
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
<div class="primer-note primer-note--error scroll" id="primer-error" hidden>
|
|
85
|
+
<p class="primer-note__label">The reference did not load</p>
|
|
86
|
+
<p class="primer-note__body">The markdown behind this page could not be fetched. Reload to try again, or read it in the repo.</p>
|
|
87
|
+
<p class="primer-note__acts"><a class="btn btn--ghost" href="/builders">Back to home</a></p>
|
|
88
|
+
</div>
|
|
84
89
|
|
|
90
|
+
</div>
|
|
85
91
|
</div>
|
|
86
92
|
</main>
|
|
87
93
|
|
|
@@ -95,8 +101,8 @@
|
|
|
95
101
|
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
96
102
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
97
103
|
<script src="/builders/palette.js" defer></script>
|
|
98
|
-
<script src="/builders/md-reader.js?v=2026-
|
|
99
|
-
<script src="/builders/drachmae.js?v=2026-
|
|
104
|
+
<script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
|
|
105
|
+
<script src="/builders/drachmae.js?v=2026-09-02-drachmae-2" defer></script>
|
|
100
106
|
<script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
|
|
101
107
|
</body>
|
|
102
108
|
</html>
|
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
// drachmae.js — the in-hall reader for the "Drachmae & karma"
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// (this is reference reading, not an onboarding checkpoint).
|
|
1
|
+
// drachmae.js — the in-hall reader for the "Drachmae & karma" doc (task 1472),
|
|
2
|
+
// rebuilt as the hall's first REFERENCE room (task 1003473, goal 1000081).
|
|
3
|
+
// Fetches the canonical markdown at /builders/drachmae-and-karma.md (mounted
|
|
4
|
+
// from docs/onboarding/drachmae-and-karma.md — single source of truth, also the
|
|
5
|
+
// one the primer links to) and renders it through the shared OTBMdReader, so
|
|
6
|
+
// builders can read exactly how they get paid without leaving the hall.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
8
|
+
// WHAT A REFERENCE OWES THAT THE PRIMER DOES NOT. People arrive at this page to
|
|
9
|
+
// answer one question about how they get paid, so it is scanned and deep-linked
|
|
10
|
+
// rather than read start to finish. Three things follow, and they are the whole
|
|
11
|
+
// difference between this file and primer.js (the room itself — the layout, the
|
|
12
|
+
// glass contents, the card — is room.css's, shared):
|
|
13
|
+
// * the outline goes to h3 depth and the scrollspy lights the section AND
|
|
14
|
+
// its part (the trail), with the phone toggle naming where the reader is;
|
|
15
|
+
// * every h2/h3/h4 carries its own link (.room-anchor), which copies the
|
|
16
|
+
// address on click, and a deep link lands after the async render;
|
|
17
|
+
// * there is no progress hairline and no acknowledge gate: a reference has
|
|
18
|
+
// no end to reach.
|
|
19
|
+
//
|
|
20
|
+
// task 2174: the red-team bounty rate card lives here. It is a published reward
|
|
21
|
+
// SCHEDULE — reference material that reads the same for every builder — so it
|
|
22
|
+
// belongs with the rest of the reward model. The doc's §"Red-team / security
|
|
23
|
+
// bounty" describes it in prose; the LIVE numbers come from /public/bounty-table
|
|
24
|
+
// and are inserted as a real section (h2 + table) ahead of the closing links,
|
|
25
|
+
// so the outline and the scrollspy pick it up like any other section.
|
|
16
26
|
|
|
17
27
|
(() => {
|
|
18
28
|
'use strict';
|
|
@@ -23,12 +33,31 @@
|
|
|
23
33
|
const BOUNTY_URL = '/api/gds/public/bounty-table';
|
|
24
34
|
const BOUNTY_SECTION_ID = 'security-bounty-rates';
|
|
25
35
|
const BOUNTY_SECTION_TITLE = 'Security bounty rates';
|
|
36
|
+
// The section the live table is inserted AHEAD of: the doc's closing links.
|
|
37
|
+
// Matched by its opening words, so a retitled footer falls back to appending.
|
|
38
|
+
const CLOSING_H2 = /^see it/i;
|
|
39
|
+
// The y (viewport space) treated as "where the reader is looking": below the
|
|
40
|
+
// sticky top bar, where the eye lands reading down a page.
|
|
41
|
+
const READING_LINE_Y = 100;
|
|
42
|
+
const LINK_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 9.4a2.7 2.7 0 0 0 3.8 0l2.3-2.3a2.7 2.7 0 0 0-3.8-3.8L7.7 4.5"/><path d="M9.4 6.6a2.7 2.7 0 0 0-3.8 0L3.3 8.9a2.7 2.7 0 0 0 3.8 3.8l1.2-1.2"/></svg>';
|
|
26
43
|
|
|
27
44
|
let repoBlobBase = null;
|
|
28
45
|
let repoTreeBase = null;
|
|
29
46
|
|
|
30
47
|
function $(sel) { return document.querySelector(sel); }
|
|
31
48
|
|
|
49
|
+
// The shell's toast (style.css .toast[data-visible="1"]), the same contract
|
|
50
|
+
// primer.js and copy-on-click.js speak; this page loads no dom-utils.js.
|
|
51
|
+
function toast(msg) {
|
|
52
|
+
const el = $('#toast');
|
|
53
|
+
if (!el) return;
|
|
54
|
+
el.textContent = msg;
|
|
55
|
+
el.dataset.type = 'info';
|
|
56
|
+
el.dataset.visible = '1';
|
|
57
|
+
clearTimeout(toast._t);
|
|
58
|
+
toast._t = setTimeout(() => { el.dataset.visible = '0'; }, 2400);
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
async function loadRepoInfoBases() {
|
|
33
62
|
try {
|
|
34
63
|
const r = await fetch('/api/gds/public/repo-info', { credentials: 'same-origin' });
|
|
@@ -39,10 +68,11 @@
|
|
|
39
68
|
} catch (_) { /* links degrade to the original href */ }
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
71
|
+
// The live rate card as a real section of the article, placed ahead of the
|
|
72
|
+
// closing links so the doc still ends on "dig deeper". Returns false when the
|
|
73
|
+
// schedule can't be read: the prose section stays, and no empty table or
|
|
74
|
+
// outline entry that leads nowhere is rendered.
|
|
75
|
+
async function insertBountySection(article) {
|
|
46
76
|
let payload;
|
|
47
77
|
try {
|
|
48
78
|
const r = await fetch(BOUNTY_URL, { credentials: 'same-origin' });
|
|
@@ -50,19 +80,19 @@
|
|
|
50
80
|
payload = await r.json();
|
|
51
81
|
} catch (err) {
|
|
52
82
|
console.warn('[drachmae] bounty schedule unavailable:', err.message);
|
|
53
|
-
return
|
|
83
|
+
return false;
|
|
54
84
|
}
|
|
55
85
|
const rows = Array.isArray(payload && payload.table) ? payload.table : [];
|
|
56
|
-
if (!rows.length) return
|
|
86
|
+
if (!rows.length) return false;
|
|
57
87
|
|
|
58
88
|
const esc = window.OTBMdReader.escapeHtml;
|
|
59
89
|
const currency = payload.currency || 'credits';
|
|
60
90
|
const section = document.createElement('section');
|
|
61
91
|
section.innerHTML =
|
|
62
92
|
`<h2 id="${esc(BOUNTY_SECTION_ID)}">${esc(BOUNTY_SECTION_TITLE)}</h2>` +
|
|
63
|
-
`<p>Find a flaw in the system’s own walls and report it
|
|
93
|
+
`<p>Find a flaw in the system’s own walls and report it: rewards are paid in ` +
|
|
64
94
|
`${esc(currency)} by severity. These are the live numbers, read from the server ` +
|
|
65
|
-
`
|
|
95
|
+
`when you opened this page, so a tuned rate shows up here immediately.</p>` +
|
|
66
96
|
'<table><thead><tr><th>Severity</th>' +
|
|
67
97
|
`<th>${esc(currency.charAt(0).toUpperCase() + currency.slice(1))}</th>` +
|
|
68
98
|
'<th>What counts</th></tr></thead><tbody>' +
|
|
@@ -74,58 +104,117 @@
|
|
|
74
104
|
`<td>${esc(r.blurb || '')}</td></tr>`;
|
|
75
105
|
}).join('') +
|
|
76
106
|
'</tbody></table>';
|
|
77
|
-
article.
|
|
78
|
-
|
|
107
|
+
const closing = [...article.querySelectorAll('h2')].find((h) => CLOSING_H2.test(h.textContent.trim()));
|
|
108
|
+
if (closing) article.insertBefore(section, closing); else article.appendChild(section);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The outline is read from the RENDERED headings rather than the parser's raw
|
|
113
|
+
// text, so `*not*` and a [link](…) in a heading arrive as plain words, and so
|
|
114
|
+
// the live section inserted above is simply one more heading in order.
|
|
115
|
+
function collectHeadings(article) {
|
|
116
|
+
return [...article.querySelectorAll('h2[id], h3[id]')].map((el) => ({
|
|
117
|
+
level: Number(el.tagName.charAt(1)), id: el.id, text: el.textContent.trim(),
|
|
118
|
+
}));
|
|
79
119
|
}
|
|
80
120
|
|
|
121
|
+
// h2s as the list, each h3 nested under its part; no numerals (room.css turns
|
|
122
|
+
// the room's counter off for a reference). Returns the flat list in document
|
|
123
|
+
// order, each entry carrying its part's id and title, which is what the
|
|
124
|
+
// scrollspy walks and what the phone toggle names.
|
|
81
125
|
function renderToc(headings) {
|
|
82
126
|
const list = $('#primer-toc-list');
|
|
83
127
|
if (!list) return [];
|
|
84
|
-
|
|
85
|
-
if (h2s.length === 0) { list.innerHTML = '<li class="primer-toc__placeholder">(no sections)</li>'; return []; }
|
|
128
|
+
if (headings.length === 0) { list.innerHTML = '<li class="primer-toc__placeholder">(no sections)</li>'; return []; }
|
|
86
129
|
const esc = window.OTBMdReader.escapeHtml;
|
|
87
|
-
|
|
88
|
-
|
|
130
|
+
const link = (h) => `<a href="#${esc(h.id)}" data-section="${esc(h.id)}">${esc(h.text)}</a>`;
|
|
131
|
+
const parts = [];
|
|
132
|
+
for (const h of headings) {
|
|
133
|
+
const last = parts[parts.length - 1];
|
|
134
|
+
if (h.level === 3 && last) last.subs.push(h);
|
|
135
|
+
else parts.push({ head: h, subs: [] });
|
|
136
|
+
}
|
|
137
|
+
list.innerHTML = parts.map((p) =>
|
|
138
|
+
`<li>${link(p.head)}${p.subs.length
|
|
139
|
+
? `<ol class="primer-toc__sub">${p.subs.map((s) => `<li>${link(s)}</li>`).join('')}</ol>`
|
|
140
|
+
: ''}</li>`
|
|
89
141
|
).join('');
|
|
90
|
-
|
|
142
|
+
const flat = [];
|
|
143
|
+
for (const p of parts) {
|
|
144
|
+
flat.push({ id: p.head.id, parent: null, part: p.head.text });
|
|
145
|
+
for (const s of p.subs) flat.push({ id: s.id, parent: p.head.id, part: p.head.text });
|
|
146
|
+
}
|
|
147
|
+
return flat;
|
|
91
148
|
}
|
|
92
149
|
|
|
93
|
-
|
|
94
|
-
|
|
150
|
+
// The active entry is the LAST heading whose top has scrolled past the
|
|
151
|
+
// reading line, so it stays put through a section's paragraphs. Its part, when
|
|
152
|
+
// the active entry is an h3, is the trail; the toggle's middle names the part.
|
|
153
|
+
function setupScrollspy(sections) {
|
|
154
|
+
if (sections.length === 0) return;
|
|
95
155
|
const links = new Map();
|
|
96
156
|
for (const a of document.querySelectorAll('.primer-toc__list a')) links.set(a.dataset.section, a);
|
|
97
157
|
if (links.size === 0) return;
|
|
98
|
-
const
|
|
158
|
+
const current = $('#primer-toc-current');
|
|
99
159
|
let last = null;
|
|
100
160
|
function update() {
|
|
101
161
|
let active = null;
|
|
102
|
-
for (const s of
|
|
162
|
+
for (const s of sections) {
|
|
103
163
|
const el = document.getElementById(s.id);
|
|
104
164
|
if (!el) continue;
|
|
105
|
-
if (el.getBoundingClientRect().top <= READING_LINE_Y) active = s
|
|
165
|
+
if (el.getBoundingClientRect().top <= READING_LINE_Y) active = s; else break;
|
|
166
|
+
}
|
|
167
|
+
const id = active ? active.id : null;
|
|
168
|
+
if (id === last) return;
|
|
169
|
+
last = id;
|
|
170
|
+
const trail = active && active.parent;
|
|
171
|
+
for (const [sid, a] of links) {
|
|
172
|
+
a.classList.toggle('is-active', sid === id);
|
|
173
|
+
a.classList.toggle('is-trail', !!trail && sid === trail);
|
|
106
174
|
}
|
|
107
|
-
if (active
|
|
108
|
-
last = active;
|
|
109
|
-
for (const [id, a] of links) a.classList.toggle('is-active', id === active);
|
|
175
|
+
if (current) current.textContent = active ? active.part : '';
|
|
110
176
|
}
|
|
111
177
|
window.addEventListener('scroll', update, { passive: true });
|
|
112
178
|
window.addEventListener('resize', update, { passive: true });
|
|
113
179
|
update();
|
|
114
180
|
}
|
|
115
181
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
182
|
+
// Every heading is an address. The link is the heading's own id, so it works
|
|
183
|
+
// with the browser's hash navigation and no script; the click additionally
|
|
184
|
+
// copies the absolute address, so a reader can hand a colleague the exact
|
|
185
|
+
// answer, which is the reason a reference is deep-linked at all.
|
|
186
|
+
function addAnchors(article) {
|
|
187
|
+
for (const h of article.querySelectorAll('h2[id], h3[id], h4[id]')) {
|
|
188
|
+
const a = document.createElement('a');
|
|
189
|
+
a.className = 'room-anchor';
|
|
190
|
+
a.href = `#${h.id}`;
|
|
191
|
+
a.setAttribute('aria-label', `Link to this section: ${h.textContent.trim()}`);
|
|
192
|
+
a.innerHTML = LINK_ICON;
|
|
193
|
+
h.appendChild(a);
|
|
125
194
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
195
|
+
article.addEventListener('click', (e) => {
|
|
196
|
+
const a = e.target.closest('a.room-anchor');
|
|
197
|
+
if (!a) return;
|
|
198
|
+
if (!(navigator.clipboard && navigator.clipboard.writeText && window.isSecureContext)) return;
|
|
199
|
+
const url = new URL(a.getAttribute('href'), window.location.href).href;
|
|
200
|
+
navigator.clipboard.writeText(url).then(() => toast('Link copied.'), () => { /* the hash still navigated */ });
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// A deep link arrives before the article exists, so the browser's own jump
|
|
205
|
+
// finds nothing and its :target was resolved against nothing. Once the
|
|
206
|
+
// document and the live section are in the DOM the target is scrolled into
|
|
207
|
+
// view (scroll-margin-top clears the top bar) and marked (.is-target) until
|
|
208
|
+
// the next hash change hands the mark back to :target.
|
|
209
|
+
function landOnHash() {
|
|
210
|
+
let id = '';
|
|
211
|
+
// a malformed percent-escape in the fragment throws; there is nothing to land on
|
|
212
|
+
try { id = decodeURIComponent(window.location.hash.slice(1)); } catch (_) { return; }
|
|
213
|
+
const el = id && document.getElementById(id);
|
|
214
|
+
if (!el) return;
|
|
215
|
+
el.classList.add('is-target');
|
|
216
|
+
el.scrollIntoView({ block: 'start' });
|
|
217
|
+
window.addEventListener('hashchange', () => el.classList.remove('is-target'), { once: true });
|
|
129
218
|
}
|
|
130
219
|
|
|
131
220
|
function setupTocToggle() {
|
|
@@ -146,8 +235,7 @@
|
|
|
146
235
|
});
|
|
147
236
|
}
|
|
148
237
|
|
|
149
|
-
// The three panels the article can resolve into (the primer's show()
|
|
150
|
-
// this page's DOM has mirrored since task 1472 and now finally matches).
|
|
238
|
+
// The three panels the article can resolve into (the primer's show()).
|
|
151
239
|
function show(which) {
|
|
152
240
|
for (const [id, on] of [['#primer-skeleton', which === 'loading'],
|
|
153
241
|
['#primer-article', which === 'article'],
|
|
@@ -185,18 +273,18 @@
|
|
|
185
273
|
// The doc's leading "# …" is the same page name .page-head--room already
|
|
186
274
|
// renders. Two <h1>s fails one-header-main-h1, and the page head is the one
|
|
187
275
|
// that exists before the fetch and survives its failure (task 1003307).
|
|
188
|
-
|
|
189
|
-
const headings = parsed.headings.filter((h) => h.level > 1);
|
|
190
|
-
article.innerHTML = html;
|
|
276
|
+
article.innerHTML = parsed.html.replace(/^\s*<h1\b[^>]*>[\s\S]*?<\/h1>\s*/, '');
|
|
191
277
|
article.removeAttribute('aria-busy');
|
|
192
278
|
show('article');
|
|
193
|
-
// Awaited before the
|
|
279
|
+
// Awaited before the outline is built so the rate card gets its entry and a
|
|
194
280
|
// scrollspy target; the article itself is already on screen by now.
|
|
195
|
-
|
|
196
|
-
|
|
281
|
+
await insertBountySection(article);
|
|
282
|
+
addAnchors(article);
|
|
197
283
|
setupTocToggle();
|
|
198
|
-
setupScrollspy(
|
|
199
|
-
|
|
284
|
+
setupScrollspy(renderToc(collectHeadings(article)));
|
|
285
|
+
// After the fonts: a swap after the jump moves everything above the target
|
|
286
|
+
// (measured: 300px short on a phone, past it on a desktop).
|
|
287
|
+
(document.fonts ? document.fonts.ready : Promise.resolve()).then(landOnHash);
|
|
200
288
|
}
|
|
201
289
|
|
|
202
290
|
if (document.readyState === 'loading') {
|