@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.
@@ -1,18 +1,28 @@
1
- // drachmae.js — the in-hall reader for the "Drachmae & karma" onboarding doc
2
- // (task 1472). Fetches the canonical markdown at /builders/drachmae-and-karma.md
3
- // (mounted from docs/onboarding/drachmae-and-karma.md — single source of truth,
4
- // also the one the primer links to) and renders it with the shared OTBMdReader,
5
- // so builders can read exactly how they get paid without leaving the hall for
6
- // GitHub. Reuses the primer's layout/TOC/progress DOM + primer.css; no ack gate
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
- // task 2174: the red-team bounty rate card moved here off Standing. It is a
10
- // published reward SCHEDULE — reference material that reads the same for every
11
- // builder — so it belongs with the rest of the reward model, not in a personal
12
- // standing view. The doc's §"Red-team / security bounty" describes it in prose;
13
- // the LIVE numbers come from /public/bounty-table and are appended as a real
14
- // section (h2 + table) so the TOC, scrollspy and progress bar pick it up like
15
- // any other section of the article.
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
- // Append the live bounty rate card to the article and return its TOC heading,
43
- // or null when the schedule can't be read (the prose section stays; we just
44
- // don't show an empty table or a TOC entry that leads nowhere).
45
- async function appendBountySection(article) {
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 null;
83
+ return false;
54
84
  }
55
85
  const rows = Array.isArray(payload && payload.table) ? payload.table : [];
56
- if (!rows.length) return null;
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 — rewards are paid in ` +
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
- `just now, so a tuned rate shows up here immediately.</p>` +
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.appendChild(section);
78
- return { level: 2, id: BOUNTY_SECTION_ID, text: BOUNTY_SECTION_TITLE };
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
- const h2s = headings.filter((h) => h.level === 2);
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
- list.innerHTML = h2s.map((h) =>
88
- `<li><a href="#${esc(h.id)}" data-section="${esc(h.id)}">${esc(h.text)}</a></li>`
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
- return h2s;
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
- function setupScrollspy(tocSections) {
94
- if (tocSections.length === 0) return;
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 READING_LINE_Y = 100;
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 tocSections) {
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.id; else break;
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 === last) return;
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
- function setupProgressBar(articleEl) {
117
- const bar = $('#primer-progress-bar');
118
- if (!bar) return;
119
- function update() {
120
- const rect = articleEl.getBoundingClientRect();
121
- const viewport = window.innerHeight || document.documentElement.clientHeight;
122
- const scrolled = Math.max(0, -rect.top);
123
- const frac = rect.height > 0 ? Math.min(1, scrolled / Math.max(1, rect.height - viewport)) : 0;
124
- bar.style.width = `${Math.round(frac * 100)}%`;
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
- window.addEventListener('scroll', update, { passive: true });
127
- window.addEventListener('resize', update, { passive: true });
128
- update();
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(), which
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
- const html = parsed.html.replace(/^\s*<h1\b[^>]*>[\s\S]*?<\/h1>\s*/, '');
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 TOC is built so the rate card gets a TOC entry and a
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
- const bountyHeading = await appendBountySection(article);
196
- const tocSections = renderToc(bountyHeading ? [...headings, bountyHeading] : headings);
281
+ await insertBountySection(article);
282
+ addAnchors(article);
197
283
  setupTocToggle();
198
- setupScrollspy(tocSections);
199
- setupProgressBar(article);
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 — a READING ROOM, not a board: it shares the primer .primer-layout and takes .page-head--room. Public (no page gate), so the signed-out render is the one most readers get. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
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 carries no feed for this page, so against it the page renders its EMPTY and SKELETON states — which is the point: those are states the floors must hold in. Against the hall-preview harness the same states render populated. Every declared 404 below is a fixture the harness does not carry, named rather than hidden.",
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
- "wait",
35
- 600
36
- ]
37
- ],
38
- "expect": {
39
- "visible": [
40
- "#primer-article",
41
- "#primer-toc"
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
- out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
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,3})\s+(.+?)\s*$/))) {
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,3})\s/.test(lines[i]) &&
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) and the
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 { max-width: var(--measure); }
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.473",
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.473",
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.473",
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.473'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
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');