@bongos/core 1.19.469 → 1.19.471

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,17 +1,20 @@
1
- /* atlas.css — Page-local styles for the Repo Atlas 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
+ /* atlas.css — the Repo Atlas SCENE (task 1737 amending ADR 0094's styling
2
+ clause; re-skinned in task 1003307; redesigned in task 1003472, goal 1000081).
3
+ The chrome around the scene — the panel, the view switch, the caption, the
4
+ detail card, the island index, the foot — is the shell's and the oversight
5
+ family's, so this sheet holds only what the map has that no other page does:
6
+ the nine semantic colours, the scene box, the detail card's placement, the
7
+ legend swatches and the node/edge states.
5
8
 
6
- /* Repo Atlas — restyled onto the hall design system (task 1737, amending
7
- ADR 0094's styling clause). The CHROME (card, toolbar, caption, legend,
8
- popup, badges) rides the hall tokens, so it follows the instance pack and
9
- dark mode like every other page. The SEMANTIC node/edge palette below
10
- stays instance-independent — the 0094 invariant: a pack's colors must
11
- never scramble the map's color-coded meaning. Non-semantic map values
12
- (label ink/halo, ambient wash, hub glow, badge fills) are scoped vars the
13
- renderer reads at draw time, with dark variants, so the scene adapts to
14
- the theme without the pack touching the encoding. */
9
+ What 1003472 removed, and why: the page's own hairline + accent wash on the
10
+ panel (the panel is the shell's .scroll now, on the owner's standing decision
11
+ that the transparent edge stands — task 1003470 closed that question);
12
+ .ab-modes, a segmented box that was a third toggle grammar beside the shell's
13
+ tab strip; .ab-chip, an accent pill saying "live" on a map whose foot already
14
+ says when it was drawn; the glass hint pill floating over the scene; the two
15
+ box-shadows (the detail card and the empty card — the world draws depth with
16
+ hairlines); and the 5.5s pulse on the hub's glow, which read as a status dot
17
+ (the world's ambient floor is 20s). */
15
18
  .atlas-page{
16
19
  /* THE SEMANTIC MAP PALETTE — the sanctioned exception, alongside the shell's
17
20
  status vocabulary. ADR 0094's invariant is that a pack's colours must never
@@ -26,28 +29,19 @@
26
29
  /* Pure white with alpha as an ambient wash — the Fifteen Rule's one legal
27
30
  literal shape (a scrim, a mask or a halo). */
28
31
  --ab-ambient: rgba(255,255,255,.5);
29
- /* Was #c7cdf7 / #3b418c, a private periwinkle nobody could re-theme. The world
30
- already holds a second hue for exactly this: --sea, which the hall otherwise
32
+ /* The hub's glow: the world's second hue, --sea, which the hall otherwise
31
33
  never spends. */
32
34
  --ab-glow: color-mix(in srgb, var(--sea) 30%, var(--bg-card));
33
- /* The badge fills were eight literals plus eight dark twins. They are the
34
- four-state vocabulary the shell already measures on both grounds in both
35
- modes (tests/hall_tokens.mjs), so they are that vocabulary now — which is
36
- also why this rule needs no dark block for them any more. */
35
+ /* The in-scene badge fills are the four-state vocabulary the shell already
36
+ measures on both grounds in both modes (tests/hall_tokens.mjs). */
37
37
  --ab-b-good-bg:var(--ok-weak); --ab-b-good-fg:var(--ok);
38
38
  --ab-b-warn-bg:var(--warn-weak); --ab-b-warn-fg:var(--warn);
39
39
  --ab-b-danger-bg:var(--danger-weak); --ab-b-danger-fg:var(--danger);
40
40
  --ab-b-info-bg:var(--info-weak); --ab-b-info-fg:var(--info);
41
- color:var(--ink);
42
- border:var(--line) solid var(--rule-soft); border-radius:var(--radius);
43
- background:
44
- radial-gradient(ellipse 70% 55% at 50% 46%, color-mix(in srgb, var(--accent) 4%, transparent), transparent 70%),
45
- var(--bg-card);
46
41
  }
47
- /* The dark twin, still written twice and still byte-identical — that rule does
48
- not bend. What shrank is what it has to SAY: the badge pairs flip themselves
49
- now that they are the status vocabulary, so only the two genuinely scene-level
50
- values are left (task 1003307). */
42
+ /* The dark twin, written twice and byte-identical — that rule does not bend.
43
+ Only the two genuinely scene-level values flip here; the badge pairs flip
44
+ themselves as the status vocabulary. */
51
45
  html[data-theme="dark"] .atlas-page{
52
46
  --ab-ambient: rgba(255,255,255,.03);
53
47
  --ab-glow: color-mix(in srgb, var(--sea) 55%, var(--bg));
@@ -58,69 +52,83 @@ html[data-theme="dark"] .atlas-page{
58
52
  --ab-glow: color-mix(in srgb, var(--sea) 55%, var(--bg));
59
53
  }
60
54
  }
61
- .atlas-page *{box-sizing:border-box}
62
- .atlas-page button{font:inherit}
63
55
 
64
- .atlas-toolbar{display:flex; align-items:center; gap:12px 16px; flex-wrap:wrap; padding:16px 20px 0}
65
- .ab-chip{font-weight:600; font-size:10.5px; text-transform:uppercase; letter-spacing:.1em;
66
- color:var(--accent-ink); background:var(--accent-tint); border:var(--line) solid color-mix(in srgb, var(--accent) 30%, transparent); padding:3px 9px; border-radius: var(--radius-pill)}
67
- .ab-modes{display:inline-flex; gap:0; border:var(--line) solid var(--rule); border-radius:var(--radius-sm); overflow:hidden}
68
- .ab-modes button{font-family:var(--font-body); font-weight:500; font-size:12.5px; cursor:pointer; border:0;
69
- background:var(--bg-card); color:var(--ink-soft); padding:6px 14px; transition:background .15s, color .15s}
70
- .ab-modes button + button{border-left:var(--line) solid var(--rule)}
71
- .ab-modes button.active{background:var(--accent-tint); color:var(--accent-ink)}
72
- .ab-modes button:focus-visible{outline:2px solid var(--accent); outline-offset:-2px}
56
+ /* The view switch sits under the heading with the strip's own rule; the
57
+ caption under it is the shell's lede, so the map's blurb takes the measure
58
+ every other panel's lede takes. The sub line is the caption's faint second
59
+ line: what the map was counted from. */
60
+ .ab-views{ margin: 4px 0 14px; }
61
+ .ab-caption{ margin-bottom: 4px; }
62
+ .ab-caption .sub{ display:block; margin-top:4px; font-size:12.5px; color:var(--ink-faint); font-variant-numeric: tabular-nums; }
73
63
 
74
- .ab-caption{padding:13px 20px 0; margin:0; font-size:13px; line-height:1.5; color:var(--ink-soft); max-width:90ch}
75
- .ab-caption .sub{display:block; margin-top:3px; font-size:11.5px; color:var(--ink-faint)}
64
+ /* The scene: the SVG at its natural aspect, the detail card and the empty
65
+ state positioned inside it. */
66
+ .ab-scene{ position:relative; width:100%; padding:6px 0 0; }
67
+ .ab-scene svg{ display:block; width:100%; height:auto; }
68
+ .ab-hint{ margin: 6px 0 0; }
69
+ /* The empty state flows in the scene's place (the SVG is hidden then), so a
70
+ phone is not handed 300px of ground under a map that is not there. */
71
+ .ab-empty{ display:none; justify-content:center; text-align:center; padding:40px 20px; }
72
+ .ab-empty.show{ display:flex; }
73
+ .ab-empty__state{ max-width: var(--measure); }
74
+ .ab-empty__state p b{ display:block; margin-bottom:4px; font-weight:500; color:var(--ink); }
76
75
 
77
- .ab-scene{position:relative; width:100%; min-height:500px; padding:6px 0}
78
- .ab-scene svg{display:block; width:100%; height:auto}
79
- .ab-hint{position:absolute; left:20px; bottom:12px; font-size:11.5px; color:var(--ink-soft);
80
- background:color-mix(in srgb, var(--bg-card) 75%, transparent); backdrop-filter:blur(4px); padding:5px 11px; border-radius: var(--radius-pill); border:var(--line) solid var(--rule-soft)}
81
- .ab-empty{position:absolute; inset:0; display:none; align-items:center; justify-content:center; text-align:center; padding:40px}
82
- .ab-empty.show{display:flex}
83
- .ab-empty .card{background:var(--bg-card); border:var(--line) solid var(--rule-soft); border-radius:var(--radius); padding:26px 30px; max-width:460px;
84
- box-shadow:var(--shadow-pop)}
85
- .ab-empty h2{margin:0 0 8px; font-size:16px; font-weight:650}
86
- .ab-empty p{margin:0; font-size:13.5px; line-height:1.55; color:var(--ink-soft)}
76
+ /* The legend: the nine colours with their meaning, in the fact voice. The
77
+ swatch is a semantic colour, so it is the one thing on the page that is not
78
+ a token. */
79
+ .ab-legend{ display:flex; flex-wrap:wrap; gap:8px 18px; padding:10px 0 0; font-size:12.5px; color:var(--ink-soft); }
80
+ .ab-legend .lg{ display:flex; align-items:center; gap:7px; }
81
+ .ab-legend .sw{ width:12px; height:12px; border-radius:3px; }
82
+ .ab-legend .ln{ width:18px; border-top:3px solid; }
87
83
 
88
- .ab-legend{display:flex; flex-wrap:wrap; gap:8px 18px; padding:12px 20px 6px; font-size:11.5px; color:var(--ink-soft)}
89
- .ab-legend .lg{display:flex; align-items:center; gap:7px}
90
- .ab-legend .sw{width:13px; height:13px; border-radius:4px}
91
- .ab-legend .ln{width:18px; border-top:3px solid}
92
- .ab-foot{padding:10px 20px 20px; font-size:11px; color:var(--ink-faint); font-variant-numeric:tabular-nums}
84
+ /* The island index: the family's dense rows under a collapsible head. The
85
+ row's title is a disclosure button; the detail it opens is the same panel
86
+ the map's popup shows, laid inline under the row so a phone reads it in
87
+ place. Hovering a row lights the island on the map, the way hovering the
88
+ island does. */
89
+ .ab-index{ margin: var(--sp-md) 0 0; padding-top: 10px; border-top: var(--line) solid var(--rule); }
90
+ .ab-index > summary{ padding: 0 0 10px; border-bottom: var(--line) solid var(--rule); }
91
+ .ab-index[open] > summary{ margin-bottom: 0; }
92
+ .ab-index .kit-collapse__body{ margin-top: 0; }
93
+ .ab-ix__t{ font: inherit; background:none; border:0; padding:2px 4px; margin:-2px -4px; text-align:left; cursor:pointer; }
94
+ .ab-ix__t:hover{ color: var(--accent-ink); text-decoration: underline; text-underline-offset: 3px; }
95
+ .ab-ix__t[aria-expanded="true"]{ color: var(--ink); }
96
+ .ab-ix__body{ margin: 10px 0 4px; max-width: var(--measure); }
97
+ .ab-ix__body .ab-ptitle{ display:none; }
98
+ /* The island's colour, drawn as a 10px square before the title so the index
99
+ and the map share one language. */
100
+ .ab-ix__sw{ display:inline-block; width:10px; height:10px; border-radius:2px; margin-right:9px; vertical-align: 1px; }
101
+ .ab-ix__sw--ghost{ background:transparent !important; border:1.5px dashed currentColor; opacity:.6; }
93
102
 
94
- .ab-popup{position:absolute; z-index:6; width:300px; max-width:88%; background:var(--bg-card);
95
- border:var(--line) solid var(--rule-soft); border-radius:var(--radius); padding:15px 16px; display:none;
96
- box-shadow:var(--shadow-pop)}
97
- .ab-popup.show{display:block}
98
- .ab-popup-close{position:absolute; top:8px; right:11px; cursor:pointer; border:0; background:transparent;
99
- font-size:20px; line-height:1; color:var(--ink-faint)}
100
- .ab-popup-close:hover{color:var(--ink)}
101
- .ab-ptitle{font-weight:650; font-size:15px; display:flex; align-items:center;
102
- gap:8px; flex-wrap:wrap; line-height:1.25; padding-right:18px}
103
- .ab-ppath{font-family: var(--font-mono); font-size:11.5px; color:var(--ink-soft); margin-top:4px; word-break:break-all}
104
- .ab-pdesc{font-size:12.8px; line-height:1.55; margin:11px 0 0; color:var(--ink-soft)}
105
- .ab-prow{margin-top:11px; font-size:10.5px; font-weight:600; color:var(--ink-faint); text-transform:uppercase; letter-spacing:.06em}
106
- .ab-chips{display:flex; flex-wrap:wrap; gap:6px; margin-top:6px}
107
- .ab-chip2{font-size:11px; font-weight:500; background:var(--glass); border:var(--line) solid var(--rule-soft);
108
- padding:4px 9px; border-radius: var(--radius-pill); font-variant-numeric:tabular-nums}
109
- .ab-badge{font-weight:600; font-size:10.5px; text-transform:uppercase; letter-spacing:.05em; padding:3px 8px; border-radius: var(--radius-pill)}
110
- .ab-b-good{background:var(--ab-b-good-bg); color:var(--ab-b-good-fg)} .ab-b-warn{background:var(--ab-b-warn-bg); color:var(--ab-b-warn-fg)}
111
- .ab-b-danger{background:var(--ab-b-danger-bg); color:var(--ab-b-danger-fg)} .ab-b-info{background:var(--ab-b-info-bg); color:var(--ab-b-info-fg)}
103
+ /* The detail card: a --bg-card panel closed by one rule hairline and no shadow
104
+ (the family's .ov-modal, positioned over the scene). */
105
+ .ab-popup{ position:absolute; z-index:6; width:320px; max-width:92%; background:var(--bg-card);
106
+ border:var(--line) solid var(--rule); border-radius:var(--radius); padding:16px 18px; display:none; }
107
+ .ab-popup.show{ display:block; }
108
+ .ab-popup-close{ position:absolute; top:10px; right:10px; display:inline-flex; align-items:center; justify-content:center;
109
+ width:28px; height:28px; cursor:pointer; border:0; border-radius:var(--radius-pill); background:transparent; color:var(--ink-faint); }
110
+ .ab-popup-close:hover{ color:var(--ink); background:var(--glass); }
111
+ .ab-ptitle{ font-weight:500; font-size:15px; letter-spacing:-0.01em; display:flex; align-items:center; gap:8px; flex-wrap:wrap; line-height:1.3; padding-right:28px; color:var(--ink); }
112
+ .ab-ppath{ font-family: var(--font-mono); font-size:11.5px; color:var(--ink-faint); margin-top:4px; overflow-wrap:anywhere; }
113
+ .ab-pdesc{ font-size:13px; line-height:1.55; margin:10px 0 0; color:var(--ink-soft); }
114
+ .ab-prow{ margin-top:12px; font-family:var(--label-face); font-size:var(--label-size); letter-spacing:var(--label-space); text-transform:var(--label-case); color:var(--ink-faint); }
115
+ .ab-chips{ display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
116
+ /* A neighbour is a door: the chip is a button that opens that island. */
117
+ button.ab-jump{ font:inherit; font-family:var(--font-mono); font-size:11px; cursor:pointer; background:none; }
118
+ button.ab-jump:hover{ color:var(--accent-ink); border-color:var(--accent-ink); }
112
119
 
113
- .ab-node{cursor:pointer; transition:opacity .18s}
114
- .ab-node text{font-family:var(--font-body); paint-order:stroke; stroke:var(--ab-label-halo); stroke-width:3px; stroke-linejoin:round; fill:var(--ab-label-ink)}
115
- .ab-node text.ab-sub{fill:var(--ink-faint)}
116
- .ab-node text.ab-badge-text{stroke:none; paint-order:normal}
117
- svg.focus .ab-node{opacity:.18}
118
- svg.focus .ab-node.lit{opacity:1}
119
- .ab-edge{fill:none; opacity:.18; transition:opacity .18s}
120
- svg.focus .ab-edge{opacity:.04}
121
- svg.focus .ab-edge.lit{opacity:1}
122
- .ab-edge.lit{filter:url(#abglow)}
123
- .ab-sun-glow{animation:abbreathe 5.5s ease-in-out infinite}
124
- @keyframes abbreathe{0%,100%{opacity:.4}50%{opacity:.8}}
125
- @media (prefers-reduced-motion: reduce){ .atlas-page *{transition:none!important; animation:none!important} }
126
- @media (max-width:560px){ .ab-modes{width:100%; display:flex} .ab-modes button{flex:1} }
120
+ /* ---- the scene's own states: nodes, edges, the lit set, the hub's glow ---- */
121
+ .ab-node{ cursor:pointer; transition:opacity .18s; }
122
+ .ab-node text{ font-family:var(--font-body); paint-order:stroke; stroke:var(--ab-label-halo); stroke-width:3px; stroke-linejoin:round; fill:var(--ab-label-ink); }
123
+ .ab-node text.ab-sub{ fill:var(--ink-faint); }
124
+ .ab-node text.ab-badge-text{ stroke:none; paint-order:normal; }
125
+ svg.focus .ab-node{ opacity:.18; }
126
+ svg.focus .ab-node.lit{ opacity:1; }
127
+ .ab-edge{ fill:none; opacity:.18; transition:opacity .18s; }
128
+ svg.focus .ab-edge{ opacity:.04; }
129
+ svg.focus .ab-edge.lit{ opacity:1; }
130
+ .ab-edge.lit{ filter:url(#abglow); }
131
+ /* The hub breathes at the world's ambient floor, not as a pulse. */
132
+ .ab-sun-glow{ animation:abbreathe 24s ease-in-out infinite; }
133
+ @keyframes abbreathe{ 0%,100%{opacity:.45} 50%{opacity:.75} }
134
+ @media (prefers-reduced-motion: reduce){ .atlas-page *{ transition:none!important; animation:none!important; } }
@@ -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 island index under the map is a run of oversight rows,
13
+ and the detail panel's facts are the family's lines, so the family sheet
14
+ loads first and atlas.css holds only the scene. -->
15
+ <link rel="stylesheet" href="/builders/oversight.css">
12
16
  <link rel="stylesheet" href="/builders/atlas.css">
13
17
  </head>
14
18
  <body data-page="atlas" data-page-title="Repo Atlas">
@@ -24,42 +28,61 @@
24
28
  board's label voice gives it one without competing with the map. -->
25
29
  <div class="page-head page-head--board">
26
30
  <h1 class="page-head__h">Repo atlas</h1>
27
- <p class="page-head__sub">A self-drawing map of the codebase: what each module is, and what depends on it.</p>
31
+ <p class="page-head__sub">A self-drawing map of the codebase: what each module is, and what depends on it. Redrawn from the code on every deploy.</p>
28
32
  </div>
29
33
 
30
34
  <main id="hall-main">
31
- <!-- The atlas chrome rides the hall tokens (pack + dark aware, task 1737);
32
- only the semantic node/edge palette stays instance-independent so a
33
- pack can never scramble the map's color-coded meaning (ADR 0094). -->
34
- <div class="atlas-page">
35
- <div class="atlas-toolbar">
36
- <div class="ab-modes">
37
- <button data-view="context" class="active">Context Layer</button>
38
- <button data-view="modules">Module Truth</button>
39
- </div>
40
- <span class="ab-chip">live</span>
35
+ <!-- The atlas is NOT a list page: it is an SVG scene with a view switch,
36
+ a caption, a legend and a detail panel. task 1003472 put its CHROME
37
+ in the shell's dialect — the panel is the shell's .scroll, the view
38
+ switch its tab strip, the detail panel a hairline --bg-card card —
39
+ and added the island INDEX under the map: every island as a row a
40
+ keyboard or a phone can open, since the map itself is a hover scene
41
+ that was unreadable at 390px and unreachable without a mouse. Only
42
+ the semantic node/edge palette stays instance-independent, so a
43
+ pack can never scramble the map's colour-coded meaning (ADR 0094). -->
44
+ <section class="scroll atlas-page" aria-labelledby="atlas-h">
45
+ <h2 id="atlas-h" class="scroll__h">The map</h2>
46
+ <div class="kit-tabs ab-views" role="tablist" aria-label="Map view">
47
+ <button type="button" class="setup__tab" role="tab" data-view="context" aria-selected="true">Context layer</button>
48
+ <button type="button" class="setup__tab" role="tab" data-view="modules" aria-selected="false">Module truth</button>
41
49
  </div>
42
50
 
43
- <p class="ab-caption" id="caption">Loading the map…</p>
51
+ <p class="scroll__lede ab-caption" id="caption">Loading the map…</p>
44
52
 
45
53
  <div class="ab-scene" id="scene">
46
54
  <svg id="svg" role="img" aria-label="Isometric map of the repository"></svg>
47
- <div class="ab-hint">Hover an island to light its links · click one for details</div>
48
55
  <div class="ab-popup" id="popup" role="dialog" aria-label="Codebase detail">
49
- <button class="ab-popup-close" id="popupClose" aria-label="Close">×</button>
56
+ <button type="button" class="ab-popup-close" id="popupClose" aria-label="Close">
57
+ <svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true" focusable="false"><path d="M2 2l8 8M10 2l-8 8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
58
+ </button>
50
59
  <div id="popupBody"></div>
51
60
  </div>
52
61
  <div class="ab-empty" id="empty">
53
- <div class="card">
54
- <h2>The map is being drawn</h2>
55
- <p>The Repo Atlas regenerates itself on the server every time code ships. It looks like it hasn’t been generated on this deploy yet — check back after the next deploy.</p>
62
+ <div class="empty-state ab-empty__state">
63
+ <p><b>The map is being drawn.</b> The atlas regenerates itself on the server every time code ships; it has not been generated on this deploy yet. Check back after the next deploy.</p>
56
64
  </div>
57
65
  </div>
58
66
  </div>
59
67
 
68
+ <p class="ab-hint ov-foot" id="hint">Hover an island to light its links, click one for its details, or open it from the index below.</p>
60
69
  <div class="ab-legend" id="legend"></div>
61
- <footer class="ab-foot" id="foot"></footer>
62
- </div>
70
+
71
+ <!-- The island index (task 1003472): the same islands as rows. Closed
72
+ on a desktop, where the map is the read; opened by atlas.js on a
73
+ phone, where the index is. -->
74
+ <details class="kit-collapse ab-index" id="ab-index">
75
+ <summary class="kit-collapse__summary">
76
+ <span class="ov-group__name">Island index</span>
77
+ <span class="ov-group__n" id="ab-index-n"></span>
78
+ </summary>
79
+ <div class="kit-collapse__body">
80
+ <ul class="ov-rows" id="ab-index-rows"></ul>
81
+ </div>
82
+ </details>
83
+
84
+ <p class="ov-foot" id="foot"></p>
85
+ </section>
63
86
  </main>
64
87
  <footer class="app-foot" id="app-foot"></footer>
65
88
  </div>
@@ -71,6 +94,6 @@
71
94
  <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
72
95
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
96
  <script src="/builders/palette.js" defer></script>
74
- <script src="/builders/atlas.js?v=2026-06-27-r1" defer></script>
97
+ <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
75
98
  </body>
76
99
  </html>
@@ -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
  }