@bongos/core 1.19.468 → 1.19.470

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.
Files changed (41) hide show
  1. package/.bongos-core.json +94 -44
  2. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +169 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/copy-inventory.md +42 -50
  5. package/docs/copy-registry.json +167 -271
  6. package/docs/file-map.md +5 -1
  7. package/docs/module-api-changelog.md +4 -0
  8. package/docs/research/task-network-map/a-every-task-p2.png +0 -0
  9. package/docs/research/task-network-map/a-every-task-p40.png +0 -0
  10. package/docs/research/task-network-map/b-dependency-forest-p2.png +0 -0
  11. package/docs/research/task-network-map/b-dependency-forest-p40.png +0 -0
  12. package/docs/research/task-network-map/c-goals-p2.png +0 -0
  13. package/docs/research/task-network-map/c-goals-p40.png +0 -0
  14. package/docs/research/task-network-map/d-joined-by-goal.png +0 -0
  15. package/docs/research/task-network-map-readability.md +191 -0
  16. package/modules/hall-ui/public/atlas.css +96 -88
  17. package/modules/hall-ui/public/atlas.html +43 -20
  18. package/modules/hall-ui/public/atlas.js +118 -20
  19. package/modules/hall-ui/public/atlas.states.json +52 -1
  20. package/modules/hall-ui/public/modules.css +67 -86
  21. package/modules/hall-ui/public/modules.html +46 -35
  22. package/modules/hall-ui/public/modules.js +195 -103
  23. package/modules/hall-ui/public/modules.states.json +48 -1
  24. package/modules/hall-ui/public/people.css +3 -2
  25. package/modules/hall-ui/public/people.html +2 -2
  26. package/modules/hall-ui/public/people.js +5 -4
  27. package/modules/hall-ui/public/people.states.json +1 -1
  28. package/modules/hall-ui/public/settings.js +1 -1
  29. package/modules/hall-ui/public/style.css +12 -11
  30. package/package-lock.json +2 -2
  31. package/package.json +1 -1
  32. package/scripts/gds/spike-task-map.js +730 -0
  33. package/src/bongos/serve-internal.js +20 -8
  34. package/src/module-api.js +1 -1
  35. package/tests/hall_catalog_world.mjs +181 -0
  36. package/tests/hall_page_gate_map.mjs +4 -2
  37. package/tests/hall_reading_world.mjs +2 -2
  38. package/tests/hall_roster_world.mjs +40 -12
  39. package/tests/spike_task_map.mjs +0 -0
  40. package/modules/hall-ui/public/scouting.html +0 -69
  41. package/modules/hall-ui/public/scouting.states.json +0 -59
@@ -1,8 +1,11 @@
1
- // atlas.js — the Repo Atlas renderer (ADR 0094, idea #457; restyled task 1737).
2
- // Reads the server-generated /builders/atlas.json (produced by
3
- // scripts/gds/gen-atlas.js on every deploy) and draws an isometric,
4
- // hover-connective, click-for-detail map of the repository. Two views: the
5
- // .md/context layer and the module truth.
1
+ // atlas.js — the Repo Atlas renderer (ADR 0094, idea #457; restyled task 1737;
2
+ // redesigned task 1003472). Reads the server-generated /builders/atlas.json
3
+ // (produced by scripts/gds/gen-atlas.js on every deploy) and draws an
4
+ // isometric, hover-connective, click-for-detail map of the repository. Two
5
+ // views: the .md/context layer and the module truth. Under the map, the ISLAND
6
+ // INDEX renders the same nodes as rows a keyboard or a phone can open — the
7
+ // scene is a hover surface, so without the index a third of the hall's readers
8
+ // could not reach a single detail.
6
9
  //
7
10
  // Vanilla, no framework (the hall convention). The SEMANTIC node/edge palette
8
11
  // (TOKENS/EDGE_STYLE) is fixed — never instance-pack-driven, so a pack can't
@@ -26,6 +29,10 @@
26
29
  monolith: { c: TOKENS.db, w: 3, dash: 0 }, ghost: { c: TOKENS.ghost, w: 1.8, dash: '5 5' },
27
30
  seam: { c: TOKENS.sun, w: 2, dash: 0 },
28
31
  };
32
+ // The in-scene badge kinds map onto the shell's status pills in the HTML
33
+ // detail (good → ok, the rest by name), so a badge reads the same on the
34
+ // island and in its card.
35
+ var PILL = { good: 'ok', warn: 'warn', danger: 'danger', info: 'info' };
29
36
 
30
37
  // Theme-dependent, NON-semantic scene values — resolved from the .atlas-page
31
38
  // scoped vars at render time (they flip with html[data-theme]).
@@ -58,6 +65,10 @@
58
65
  var emptyEl = document.getElementById('empty');
59
66
  var popup = document.getElementById('popup');
60
67
  var popupBody = document.getElementById('popupBody');
68
+ var hintEl = document.getElementById('hint');
69
+ var indexEl = document.getElementById('ab-index');
70
+ var indexRows = document.getElementById('ab-index-rows');
71
+ var indexN = document.getElementById('ab-index-n');
61
72
 
62
73
  var DATA = null, view = 'context', selected = null, nodeEls = {}, edgeEls = [], adj = {};
63
74
 
@@ -70,6 +81,7 @@
70
81
  function ptsOf(a) { return a.map(function (p) { return r1(p.x) + ',' + r1(p.y); }).join(' '); }
71
82
  function cx(n) { return n.gx + n.w / 2; }
72
83
  function cy(n) { return n.gy + n.d / 2; }
84
+ function nodeById(id) { return DATA.views[view].nodes.filter(function (x) { return x.id === id; })[0]; }
73
85
 
74
86
  function nodeFill(n) {
75
87
  if (view === 'context') return { hub: TOKENS.sun, nested: TOKENS.sky, docs: TOKENS.paper, db: TOKENS.coral, generated: TOKENS.sky }[n.kind] || TOKENS.sky;
@@ -210,7 +222,8 @@
210
222
  });
211
223
 
212
224
  drawLegend(d);
213
- captionEl.innerHTML = d.blurb + '<span class="sub">' + (d.sub || '') + '</span>';
225
+ drawIndex(d);
226
+ captionEl.innerHTML = esc(d.blurb || '') + '<span class="sub">' + esc(d.sub || '') + '</span>';
214
227
  }
215
228
 
216
229
  function buildNode(n) {
@@ -256,23 +269,25 @@
256
269
  for (var k in nodeEls) nodeEls[k].classList.remove('lit');
257
270
  }
258
271
 
272
+ // The detail of one island: the same markup in the map's card and in the
273
+ // index row. Facts are the shell's pills and chips; a neighbour is a door
274
+ // (a .ab-jump button that opens that island) rather than a dead chip.
259
275
  function detailHTML(n) {
260
- var d = DATA.views[view];
261
- var b = n.badge ? '<span class="ab-badge ab-b-' + n.badge.k + '">' + n.badge.t + '</span>' : '';
276
+ var b = n.badge ? '<span class="fact-pill fact-pill--' + (PILL[n.badge.k] || 'quiet') + '">' + esc(n.badge.t) + '</span>' : '';
262
277
  var html = '<div class="ab-ptitle">' + esc(n.label) + ' ' + b + '</div>';
263
278
  html += '<div class="ab-ppath">' + esc(n.path || '') + '</div>';
264
- if (n.metric) html += '<div class="ab-chips"><span class="ab-chip2">' + esc(n.metric) + '</span></div>';
279
+ if (n.metric) html += '<div class="ab-chips"><span class="fact-chip">' + esc(n.metric) + '</span></div>';
265
280
  html += '<p class="ab-pdesc">' + esc(n.desc || '') + '</p>';
266
281
  if (n.provenance) {
267
- var provLabel = n.provenance === 'core' ? 'Core-loaded (Cloud Bongos)' : 'Built by this project';
268
- html += '<div class="ab-chips"><span class="ab-chip2">' + esc(provLabel) + '</span>' +
269
- (n.author ? '<span class="ab-chip2">by ' + esc(n.author) + '</span>' : '') +
270
- (n.origin ? '<span class="ab-chip2">from ' + esc(n.origin) + '</span>' : '') +
282
+ var provLabel = n.provenance === 'core' ? 'core-loaded' : 'built here';
283
+ html += '<div class="ab-chips"><span class="fact-pill' + (n.provenance === 'core' ? ' fact-pill--info' : '') + '">' + esc(provLabel) + '</span>' +
284
+ (n.author ? '<span class="ov-fact">by <b>' + esc(n.author) + '</b></span>' : '') +
285
+ (n.origin ? '<span class="ov-fact">from <b>' + esc(n.origin) + '</b></span>' : '') +
271
286
  '</div>';
272
287
  }
273
- if (n.provides && n.provides.length) html += '<div class="ab-prow">Provides (seams)</div><div class="ab-chips">' + n.provides.map(function (p) { return '<span class="ab-chip2">' + esc(p) + '</span>'; }).join('') + '</div>';
274
- var conns = (adj[n.id] || []).map(function (e) { var o = e.a === n.id ? e.b : e.a; var on = d.nodes.filter(function (x) { return x.id === o; })[0]; return on ? on.label : o; });
275
- if (conns.length) html += '<div class="ab-prow">Connects to</div><div class="ab-chips">' + conns.map(function (cc) { return '<span class="ab-chip2">↔ ' + esc(cc) + '</span>'; }).join('') + '</div>';
288
+ if (n.provides && n.provides.length) html += '<div class="ab-prow">Provides (seams)</div><div class="ab-chips">' + n.provides.map(function (p) { return '<span class="fact-chip">' + esc(p) + '</span>'; }).join('') + '</div>';
289
+ var conns = (adj[n.id] || []).map(function (e) { var o = e.a === n.id ? e.b : e.a; var on = nodeById(o); return on ? { id: on.id, label: on.label } : null; }).filter(Boolean);
290
+ if (conns.length) html += '<div class="ab-prow">Connects to</div><div class="ab-chips">' + conns.map(function (cc) { return '<button type="button" class="fact-chip ab-jump" data-node="' + esc(cc.id) + '">' + esc(cc.label) + '</button>'; }).join('') + '</div>';
276
291
  return html;
277
292
  }
278
293
  function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]; }); }
@@ -300,25 +315,108 @@
300
315
  }).join('');
301
316
  }
302
317
 
318
+ // ---- the island index --------------------------------------------------
319
+ // Every island of the current view as a dense row: its colour, its label, its
320
+ // sub and metric, its badge as a pill. The title is a disclosure button; the
321
+ // body it opens is detailHTML laid inline, so the same facts read in place on
322
+ // a phone. Hovering a row lights the island on the map.
323
+ function drawIndex(d) {
324
+ if (!indexRows) return;
325
+ var rows = d.nodes.slice().sort(function (a, b) {
326
+ var fa = a.fixed ? 0 : 1, fb = b.fixed ? 0 : 1;
327
+ if (fa !== fb) return fa - fb;
328
+ return String(a.label).localeCompare(String(b.label));
329
+ });
330
+ indexRows.innerHTML = rows.map(function (n, i) {
331
+ var fill = nodeFill(n);
332
+ var sw = '<span class="ab-ix__sw' + (n.kind === 'ghost' ? ' ab-ix__sw--ghost' : '') + '" style="background:' + fill + ';color:' + fill + '" aria-hidden="true"></span>';
333
+ var facts = [];
334
+ if (n.badge) facts.push('<span class="fact-pill fact-pill--' + (PILL[n.badge.k] || 'quiet') + '">' + esc(n.badge.t) + '</span>');
335
+ if (n.metric) facts.push('<span class="fact-chip">' + esc(n.metric) + '</span>');
336
+ if (n.path) facts.push('<span class="ov-mono">' + esc(n.path) + '</span>');
337
+ return '<li class="ov-row ov-row--dense ab-ix" data-node="' + esc(n.id) + '">' +
338
+ '<div class="ov-row__main">' +
339
+ '<button type="button" class="ov-row__title ab-ix__t" aria-expanded="false" aria-controls="ab-ix-' + i + '">' + sw + esc(n.label) + (n.sub ? '<span class="ov-mono">' + esc(n.sub) + '</span>' : '') + '</button>' +
340
+ (facts.length ? '<div class="ov-row__facts">' + facts.join('') + '</div>' : '') +
341
+ '<div class="ab-ix__body" id="ab-ix-' + i + '" hidden></div>' +
342
+ '</div></li>';
343
+ }).join('');
344
+ if (indexN) indexN.textContent = String(rows.length);
345
+ }
346
+
347
+ // Open one index row (fold the others), fill it with the island's detail,
348
+ // and light the island on the map while it is open.
349
+ function openIndexRow(id, scroll) {
350
+ var rows = indexRows.querySelectorAll('.ab-ix');
351
+ var target = null;
352
+ for (var i = 0; i < rows.length; i++) {
353
+ var r = rows[i], hit = r.getAttribute('data-node') === id;
354
+ var t = r.querySelector('.ab-ix__t'), body = r.querySelector('.ab-ix__body');
355
+ t.setAttribute('aria-expanded', hit ? 'true' : 'false');
356
+ body.hidden = !hit;
357
+ if (hit) { target = r; body.innerHTML = detailHTML(nodeById(id)); }
358
+ else body.innerHTML = '';
359
+ }
360
+ if (target) {
361
+ if (indexEl && !indexEl.open) indexEl.open = true;
362
+ selected = id; lightNode(id);
363
+ if (scroll) target.scrollIntoView({ block: 'nearest' });
364
+ }
365
+ }
366
+ function closeIndexRows() {
367
+ var rows = indexRows.querySelectorAll('.ab-ix');
368
+ for (var i = 0; i < rows.length; i++) {
369
+ rows[i].querySelector('.ab-ix__t').setAttribute('aria-expanded', 'false');
370
+ var body = rows[i].querySelector('.ab-ix__body'); body.hidden = true; body.innerHTML = '';
371
+ }
372
+ }
373
+
303
374
  function setView(v) {
304
375
  view = v; selected = null;
305
- var btns = document.querySelectorAll('.ab-modes button');
306
- for (var i = 0; i < btns.length; i++) btns[i].classList.toggle('active', btns[i].getAttribute('data-view') === v);
376
+ var btns = document.querySelectorAll('.ab-views [data-view]');
377
+ for (var i = 0; i < btns.length; i++) btns[i].setAttribute('aria-selected', btns[i].getAttribute('data-view') === v ? 'true' : 'false');
307
378
  render();
308
379
  }
309
380
 
310
381
  function wire() {
311
- var btns = document.querySelectorAll('.ab-modes button');
382
+ var btns = document.querySelectorAll('.ab-views [data-view]');
312
383
  for (var i = 0; i < btns.length; i++) btns[i].addEventListener('click', function () { setView(this.getAttribute('data-view')); });
313
384
  document.getElementById('popupClose').addEventListener('click', function (ev) { ev.stopPropagation(); closePopup(); });
314
385
  svg.addEventListener('click', function () { closePopup(); });
315
- window.addEventListener('resize', function () { if (selected && nodeEls[selected]) { var n = DATA.views[view].nodes.filter(function (x) { return x.id === selected; })[0]; if (n) openPopup(n, nodeEls[selected]); } });
386
+ document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && popup.classList.contains('show')) closePopup(); });
387
+ // A neighbour chip in the map's card opens that island's card.
388
+ popup.addEventListener('click', function (ev) {
389
+ var jump = ev.target.closest('.ab-jump'); if (!jump) return;
390
+ ev.stopPropagation();
391
+ var n = nodeById(jump.getAttribute('data-node')); if (n && nodeEls[n.id]) openPopup(n, nodeEls[n.id]);
392
+ });
393
+ // The index: the title opens the row; a neighbour chip inside it opens that
394
+ // row; hovering a row lights the island.
395
+ indexRows.addEventListener('click', function (ev) {
396
+ var jump = ev.target.closest('.ab-jump');
397
+ if (jump) { openIndexRow(jump.getAttribute('data-node'), true); return; }
398
+ var t = ev.target.closest('.ab-ix__t'); if (!t) return;
399
+ var row = t.closest('.ab-ix'), id = row.getAttribute('data-node');
400
+ if (t.getAttribute('aria-expanded') === 'true') { closeIndexRows(); selected = null; clearLight(); }
401
+ else { closePopup(); openIndexRow(id, false); }
402
+ });
403
+ indexRows.addEventListener('mouseover', function (ev) {
404
+ var row = ev.target.closest('.ab-ix'); if (row && !selected) lightNode(row.getAttribute('data-node'));
405
+ });
406
+ indexRows.addEventListener('mouseout', function (ev) {
407
+ var row = ev.target.closest('.ab-ix'); if (row && !selected) clearLight();
408
+ });
409
+ window.addEventListener('resize', function () { if (selected && nodeEls[selected] && popup.classList.contains('show')) { var n = nodeById(selected); if (n) openPopup(n, nodeEls[selected]); } });
410
+ // On a phone the map is a thumbnail and the index is the read.
411
+ if (indexEl && window.matchMedia && window.matchMedia('(max-width: 640px)').matches) indexEl.open = true;
316
412
  }
317
413
 
318
414
  function showEmpty() {
319
415
  emptyEl.classList.add('show'); svg.style.display = 'none';
320
416
  captionEl.textContent = 'The map has not been generated on this deploy yet.';
321
417
  footEl.textContent = '';
418
+ if (hintEl) hintEl.hidden = true;
419
+ if (indexEl) indexEl.hidden = true;
322
420
  }
323
421
 
324
422
  // Re-render when the shell flips the theme — the scene's non-semantic values
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "Repo atlas — the self-drawing module map. Its SEMANTIC node palette is fixed by ADR 0094 and is not the token tier's business; everything around it (toolbar, chips, legend, popup, badges) is chrome and rides the world. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
2
+ "_": "Repo atlas — the self-drawing module map. Its SEMANTIC node palette is fixed by ADR 0094 and is not the token tier's business; everything around it (the panel, the view switch, the caption, the detail card, the legend, the island index) is chrome and rides the world. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once; the redesign (task 1003472) added the `truth`, `detail` and `index` states. Populated look: the hall-preview harness serves modules/hall-ui/public/atlas.json when scripts/gds/gen-atlas.js has written it (the file is generated on deploy and gitignored, so run the generator first).",
3
3
  "page": "/builders/atlas",
4
4
  "surface": "hall-ui",
5
5
  "stub": {
@@ -44,6 +44,57 @@
44
44
  ]
45
45
  }
46
46
  },
47
+ "truth": {
48
+ "auth": true,
49
+ "actions": [
50
+ [
51
+ "wait",
52
+ 900
53
+ ],
54
+ [
55
+ "clickIfVisible",
56
+ ".ab-views [data-view=\"modules\"]"
57
+ ],
58
+ [
59
+ "wait",
60
+ 500
61
+ ]
62
+ ]
63
+ },
64
+ "detail": {
65
+ "auth": true,
66
+ "actions": [
67
+ [
68
+ "wait",
69
+ 900
70
+ ],
71
+ [
72
+ "dispatchClick",
73
+ "#svg .ab-node"
74
+ ],
75
+ [
76
+ "wait",
77
+ 300
78
+ ]
79
+ ]
80
+ },
81
+ "index": {
82
+ "auth": true,
83
+ "actions": [
84
+ [
85
+ "wait",
86
+ 900
87
+ ],
88
+ [
89
+ "dispatchClick",
90
+ "#ab-index-rows .ab-ix:first-child .ab-ix__t"
91
+ ],
92
+ [
93
+ "wait",
94
+ 300
95
+ ]
96
+ ]
97
+ },
47
98
  "out": {
48
99
  "auth": false
49
100
  }
@@ -1,93 +1,74 @@
1
- /* modules.css — Page-local styles for the Modules page.
2
- Lifted verbatim out of the page's inline <style> block (task 1002176);
3
- the ?v= cache-bust is stamped automatically from this file's content hash
4
- at boot (src/asset-version.js). */
1
+ /* modules.css — what a MODULE row has that no other oversight row does (task
2
+ 1003472, goal 1000081). The panel, the filter bar, the figure band, the group
3
+ heads, the row, the toggle and the foot line are all the shell's or the
4
+ family's (oversight.css); this sheet adds the clamped description, the
5
+ per-row details disclosure and the lines it opens onto.
5
6
 
6
- /* Page-local styles for Modules, layered on the shared design tokens
7
- (style.css) — same pattern as settings.html's page-local block. */
8
- .mod-grid {
9
- display: grid;
10
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
11
- gap: 14px;
12
- margin-top: 10px;
13
- }
14
- .mod-card {
15
- border: var(--line) solid var(--rule-soft);
16
- border-radius: var(--radius-sm);
17
- background: var(--bg-card);
18
- padding: 14px 16px;
7
+ ROWS, NOT CARDS — the call task 1003472 asked for. Modules was the one hall
8
+ page already rendering its record's description, so a card looked like the
9
+ case where prose earns a box. Rendered against the real catalog it is the
10
+ opposite: the descriptions run from 120 to 1,200 characters, so the 280px
11
+ auto-fill grid stood a 500px card beside a 120px one, the toggle (the row's
12
+ one control) sat at a different height in every column, and a builder
13
+ scanning for a NAME and a STATE read paragraphs to find either. A row puts
14
+ every title on one x, every switch on another, clamps the prose to two lines
15
+ and opens the rest on request. The card grammar (.mod-grid/.mod-card), the
16
+ boxed .mod-browse field and the .mod-chip alias are gone. */
17
+
18
+ /* The filter bar closes on a hairline, the goals panel's rule: the count and
19
+ the figures under it read as the panel's content, not as more controls. */
20
+ #mod-filters { margin: 0 0 12px; padding: 0 0 6px; border-bottom: var(--line) solid var(--rule-soft); }
21
+ #mod-filters .filters { margin-bottom: 10px; }
22
+ .mod-count { margin: 0 0 14px; font-family: var(--font-mono); font-size: 12.5px; font-variant-numeric: tabular-nums; color: var(--ink-faint); }
23
+ .mod-count:empty { display: none; }
24
+
25
+ /* One group after another: the second head carries the family's air above it,
26
+ and the first row after a head starts flush (oversight.css draws the rule). */
27
+ .mod-panel .ov-group + .ov-group { margin-top: var(--sp-md); }
28
+ .mod-loading { padding: 10px 0; font-size: 13.5px; color: var(--ink-soft); }
29
+
30
+ /* The description is the sub line, clamped to two lines of the measure so
31
+ twenty rows scan as twenty rows. The details disclosure under it opens the
32
+ rest; while it is open the clamp lifts, so the prose is never shown twice. */
33
+ .mod-row__desc {
34
+ display: -webkit-box;
35
+ -webkit-box-orient: vertical;
36
+ -webkit-line-clamp: 2;
37
+ overflow: hidden;
19
38
  }
20
- .mod-card__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
21
- .mod-card__title { font-weight: 650; font-size: 14.5px; color: var(--ink); }
22
- .mod-card__key { font-size: 12px; color: var(--ink-faint); font-family: var(--font-mono); }
23
- .mod-card__desc { font-size: 13px; color: var(--ink-soft); margin: 8px 0; }
24
- .mod-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
25
- /* Base: the shell's .fact-pill (task 1003307).
26
- One override, and it is the reason the pill is not simply reused as-is: the
27
- archetype's chip holds a short machine word (a module key, a version id) and
28
- is nowrap, but THIS page's chips hold prose: "by <author>", "from <origin>",
29
- "upkeep: undeclared". Inheriting nowrap put a single 559px chip on a 320px
30
- viewport. A chip that carries a sentence wraps. */
31
- .mod-chip { white-space: normal; }
32
- .mod-chip--core { color: var(--info); }
33
- .mod-chip--own { color: var(--accent-ink); }
34
- .mod-card__actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 10px; }
35
- button.mod-toggle {
36
- font-family: var(--font-body);
39
+ .mod-row:has(.mod-more[open]) .mod-row__desc { display: block; -webkit-line-clamp: unset; overflow: visible; }
40
+
41
+ /* The disclosure is a text control in a 13px facts line: inline-block with the
42
+ family's padding trick so it clears the 24px floor on both axes. No marker
43
+ — the word says what it does, and the open state says so too. */
44
+ .mod-more { margin: 8px 0 0; }
45
+ .mod-more > summary {
46
+ display: inline-block;
47
+ list-style: none;
48
+ min-height: 24px;
49
+ min-width: 24px;
50
+ padding: 3px 4px;
51
+ margin: -3px -4px;
52
+ line-height: 18px;
37
53
  font-size: 12.5px;
38
- font-weight: 600;
39
- padding: 5px 16px;
40
- min-width: 78px;
41
- border-radius: var(--radius-pill);
42
- border: var(--line) solid var(--rule);
43
- background: var(--bg-card);
44
- color: var(--ink-soft);
54
+ color: var(--accent-ink);
45
55
  cursor: pointer;
46
56
  }
47
- button.mod-toggle[data-on="1"] { background: var(--accent); border-color: var(--accent); color: var(--accent-on); }
48
- button.mod-toggle:disabled { opacity: 0.5; cursor: default; }
49
- button.mod-submit {
50
- font-family: var(--font-body);
51
- font-size: 12px;
52
- font-weight: 600;
53
- padding: 5px 12px;
54
- border-radius: var(--radius-sm);
55
- border: var(--line) solid var(--rule);
56
- background: transparent;
57
- color: var(--ink-soft);
58
- cursor: pointer;
59
- }
60
- button.mod-submit:disabled { opacity: 0.45; cursor: not-allowed; }
61
- /* Italic was this sheet's alone — nothing else in the hall sets it, and an
62
- empty state is a plain statement, not an aside (task 1003307). */
63
- .mod-empty { color: var(--ink-faint); font-size: 13px; padding: 10px 0; }
64
- .mod-note { font-size: 12px; color: var(--ink-faint); margin-top: 4px; }
57
+ .mod-more > summary::-webkit-details-marker { display: none; }
58
+ .mod-more > summary:hover { text-decoration: underline; text-underline-offset: 3px; }
59
+ .mod-more > summary::before { content: "details"; }
60
+ .mod-more[open] > summary::before { content: "fewer details"; }
61
+ /* The lines the disclosure opens onto are the family's key/value lines; the
62
+ list of what a module contributes wraps as chips on its own line. */
63
+ .mod-more .ov-lines { margin-top: 8px; }
64
+ .mod-more .ov-lines .ov-lines__v { text-align: left; margin-left: 0; }
65
+ .mod-more .ov-lines .ov-lines__v .fact-chip { margin: 0 6px 4px 0; }
65
66
 
66
- /* Catalog browse bar (task 1001772) — one filter over both grids. */
67
- .mod-browse { margin: 0 0 16px; }
68
- .mod-browse__label {
69
- display: block;
70
- font-size: 12px;
71
- font-weight: 600;
72
- color: var(--ink-soft);
73
- margin-bottom: 6px;
74
- }
75
- .mod-browse__input {
76
- font-family: var(--font-body);
77
- font-size: 13.5px;
78
- width: 100%;
79
- max-width: 420px;
80
- padding: 8px 12px;
81
- border-radius: var(--radius-sm);
82
- border: var(--line) solid var(--rule);
83
- background: var(--bg-card);
84
- color: var(--ink);
85
- }
86
67
  /* The marker on an inferred credit — quiet, but hoverable for the reason. */
87
- .mod-inferred {
88
- border: 0;
89
- text-decoration: none;
90
- cursor: help;
91
- color: var(--ink-faint);
92
- margin-left: 1px;
93
- }
68
+ .mod-inferred { border: 0; text-decoration: none; cursor: help; color: var(--ink-faint); margin-left: 1px; }
69
+
70
+ /* The right column holds the switch and, on a module built here, the submit
71
+ door under it. The switch is the family's grant toggle: enabled reads in ink,
72
+ disabled in the faint ink, the accent only on hover. A viewer below the floor
73
+ sees the state as a pill, not a dead control. */
74
+ .mod-row__right .btn-ghost { white-space: nowrap; }
@@ -9,6 +9,10 @@
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
+ <!-- task 1003472: the catalog rides the oversight family's row (a titled row
13
+ with facts and one action on the right), so the family sheet loads first
14
+ and modules.css holds only what a module row has that no other row does. -->
15
+ <link rel="stylesheet" href="/builders/oversight.css">
12
16
  <link rel="stylesheet" href="/builders/modules.css">
13
17
  </head>
14
18
  <body data-page="modules" data-page-title="Modules">
@@ -19,48 +23,53 @@
19
23
  <div class="app-body">
20
24
  <header class="app-topbar" id="app-topbar"></header>
21
25
  <div class="app-content">
22
- <!-- task 1003307: the board page head. Modules shipped with NO <h1> at all
23
- (one-header-main-h1 measured 0 on every render), so the re-skin
24
- carries the structural fix rather than porting the hole. The heading
25
- is the LABEL voice, not a display line: on a working surface the
26
- page's own name is the least interesting thing on it. -->
26
+ <!-- The board page head (task 1003307): the label voice, because on a
27
+ working surface the page's own name is the least interesting thing on
28
+ it. The sub says what the page IS; the numbers moved into the panel's
29
+ figure band (task 1003472), where a number belongs. -->
27
30
  <div class="page-head page-head--board">
28
31
  <h1 class="page-head__h">Modules</h1>
29
- <p class="page-head__sub" id="modules-sub">Loading modules…</p>
32
+ <p class="page-head__sub" id="modules-sub">Everything this instance can run: what the core ships, what this project built, and who keeps each one true as the core advances.</p>
30
33
  </div>
31
34
 
32
35
  <main id="hall-main">
33
- <!-- Browse — one filter over both grids below (task 1001772). -->
34
- <div class="mod-browse">
35
- <label class="mod-browse__label" for="mod-search">Search the catalog</label>
36
- <input type="search" id="mod-search" class="mod-browse__input" autocomplete="off"
37
- placeholder="name, author, origin project, or licence…">
38
- <p class="mod-note" id="mod-browse-count" role="status" aria-live="polite"></p>
39
- </div>
40
-
41
- <!-- The catalog — every module the CORE ships, which this instance can
42
- opt into (ADR 0107 §6: accepted modules land default-off). Each row
43
- carries its author + origin credit (§2/§3: credit now, money later). -->
44
- <section class="scroll" id="core-scroll" aria-labelledby="core-h">
36
+ <!-- task 1003472: ONE panel. The search and the state lens sit on a
37
+ hairline at the top, the figure band under them, then two runs of
38
+ rows under group heads — shipped with the core, built by this
39
+ project. Before this the page was a boxed search on the ground over
40
+ three panels, the middle two holding 280px card grids whose height
41
+ varied tenfold with the description, so the one control per record
42
+ (the toggle) landed at a different height on every card. A record
43
+ carrying this many facts is a row (docs/design/hall-direction-v2.md
44
+ section 6; the archetype at docs/design/mocks/hall/goals/v3.html). -->
45
+ <section class="scroll mod-panel" id="core-scroll" aria-labelledby="core-h">
45
46
  <h2 id="core-h" class="scroll__h">Module catalog</h2>
46
- <p class="scroll__lede">Modules the Cloud Bongos core ships — the shared repository built by its sub-projects, each credited to its author and the project it came from. Enable the ones you want; Metic+ can flip the switch, everyone can see the state.</p>
47
- <div class="mod-grid" id="core-grid"><div class="mod-empty">Loading…</div></div>
48
- </section>
47
+ <p class="scroll__lede">The shared repository the core ships and the modules this project built for itself, each credited to its author and the project it came from. Everyone can see the state; the switch is rank-gated.</p>
48
+ <div id="mod-filters"></div>
49
+ <p class="mod-count" id="mod-count" role="status" aria-live="polite"></p>
50
+ <div class="ov-figs" id="mod-figs" aria-label="Catalog at a glance"></div>
49
51
 
50
- <!-- This instance's own modules — the pool `bongos module submit` draws
51
- from. Separate from the catalog because these ship from this repo,
52
- not from the core package. -->
53
- <section class="scroll" id="own-scroll" aria-labelledby="own-h">
54
- <h2 id="own-h" class="scroll__h">Built by this project</h2>
55
- <p class="scroll__lede">Modules that live in this instance's own repo rather than arriving with the core. Submit one upstream below to offer it to every other instance.</p>
56
- <div class="mod-grid" id="own-grid"><div class="mod-empty">None on disk.</div></div>
57
- </section>
52
+ <!-- Shipped with the core — the catalog this instance opts into
53
+ (ADR 0107 §6: accepted modules land default-off). -->
54
+ <div class="ov-group" id="core-group">
55
+ <div class="ov-group__head">
56
+ <span class="ov-group__name">Shipped with the core</span>
57
+ <span class="ov-group__n" id="core-n"></span>
58
+ </div>
59
+ <ul class="ov-rows" id="core-grid"><li class="mod-loading">Loading the catalog…</li></ul>
60
+ </div>
58
61
 
59
- <!-- Submit for review — the front door to bongos module submit (ADR 0107 §1/§9). -->
60
- <section class="scroll" id="submit-scroll" aria-labelledby="submit-h">
61
- <h2 id="submit-h" class="scroll__h">Contribute a module upstream</h2>
62
- <p class="scroll__lede" id="submit-lede">Submit one of this project's own modules for review, so it can join the shared Cloud Bongos catalog with your name and this project credited.</p>
63
- <p class="settings-status" id="submit-status" role="status" aria-live="polite"></p>
62
+ <!-- Built by this project — the pool `bongos module submit` draws from
63
+ (ADR 0107 §1/§9). The submit door is on the row, beside the module
64
+ it files, so the third panel this page used to carry is gone. -->
65
+ <div class="ov-group" id="own-group">
66
+ <div class="ov-group__head">
67
+ <span class="ov-group__name">Built by this project</span>
68
+ <span class="ov-group__n" id="own-n"></span>
69
+ </div>
70
+ <ul class="ov-rows" id="own-grid"></ul>
71
+ <p class="ov-foot" id="submit-lede">A module built here can be submitted upstream for review, so it can join the shared catalog with your name and this project credited.</p>
72
+ </div>
64
73
  </section>
65
74
  </main>
66
75
 
@@ -75,6 +84,8 @@
75
84
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
76
85
  <script src="/builders/palette.js" defer></script>
77
86
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
78
- <script src="/builders/modules.js?v=2026-08-12-catalog"></script>
87
+ <script src="/builders/brand-apply.js"></script>
88
+ <script src="/builders/hall-kit.js"></script>
89
+ <script src="/builders/modules.js?v=2026-09-02-rows"></script>
79
90
  </body>
80
91
  </html>