@bongos/core 1.19.473 → 1.19.474
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 +30 -20
- 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 +25 -23
- package/docs/copy-registry.json +80 -52
- package/docs/module-api-changelog.md +2 -0
- 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/md-reader.js +10 -4
- package/modules/hall-ui/public/room.css +116 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_reference_world.mjs +222 -0
|
@@ -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') {
|
|
@@ -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
|
}
|
|
@@ -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]) &&
|
|
@@ -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
|
+
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.474",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.474",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.474",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -34,7 +34,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
34
34
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
35
35
|
// the entry to that file. Look for a version's history there, not here.
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
-
const CORE_VERSION = '1.19.
|
|
37
|
+
const CORE_VERSION = '1.19.474'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
38
38
|
|
|
39
39
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
40
40
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|