@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.
- package/.bongos-core.json +54 -34
- package/clients/bongos-client/index.d.ts +2 -2
- package/docs/adr/0226-one-stale-timer-reporting-degrades-enforcement-refuses.md +121 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +9 -5
- package/docs/copy-inventory.md +40 -43
- package/docs/copy-registry.json +155 -182
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/atlas.css +96 -88
- package/modules/hall-ui/public/atlas.html +43 -20
- package/modules/hall-ui/public/atlas.js +118 -20
- package/modules/hall-ui/public/atlas.states.json +52 -1
- package/modules/hall-ui/public/modules.css +67 -86
- package/modules/hall-ui/public/modules.html +46 -35
- package/modules/hall-ui/public/modules.js +195 -103
- package/modules/hall-ui/public/modules.states.json +48 -1
- package/modules/hall-ui/public/style.css +8 -8
- package/modules/hall-ui/public/task-lib.js +7 -3
- package/modules/lifecycle/db-ship.js +22 -11
- package/modules/sessions/pulse.js +15 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +6 -0
- package/scripts/gds/pulse.js +48 -24
- package/src/bongos/routes/builders.js +56 -24
- package/src/module-api.js +16 -1
- package/src/stale-timer.js +177 -0
- package/tests/hall_catalog_world.mjs +181 -0
- package/tests/module_api.mjs +1 -0
- package/tests/onboarding_observability.mjs +41 -1
- package/tests/stale_timer.mjs +230 -0
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
/* atlas.css —
|
|
2
|
-
|
|
3
|
-
the
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
(
|
|
13
|
-
|
|
14
|
-
the
|
|
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
|
-
/*
|
|
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
|
|
34
|
-
|
|
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,
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.ab-
|
|
69
|
-
|
|
70
|
-
.ab-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.ab-
|
|
82
|
-
.ab-
|
|
83
|
-
.ab-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
.
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.ab-popup
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.ab-
|
|
102
|
-
|
|
103
|
-
.ab-ppath{font-family: var(--font-mono); font-size:11.5px; color:var(--ink-
|
|
104
|
-
.ab-pdesc{font-size:
|
|
105
|
-
.ab-prow{margin-top:
|
|
106
|
-
.ab-chips{display:flex; flex-wrap:wrap; gap:6px; margin-top:6px}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.ab-
|
|
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
|
-
|
|
114
|
-
.ab-node
|
|
115
|
-
.ab-node text
|
|
116
|
-
.ab-node text.ab-
|
|
117
|
-
|
|
118
|
-
svg.focus .ab-node
|
|
119
|
-
.ab-
|
|
120
|
-
|
|
121
|
-
svg.focus .ab-edge
|
|
122
|
-
.ab-edge.lit{
|
|
123
|
-
.ab-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
@
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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"
|
|
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="
|
|
54
|
-
<
|
|
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
|
-
|
|
62
|
-
|
|
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-
|
|
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
|
|
3
|
-
// scripts/gds/gen-atlas.js on every deploy) and draws an
|
|
4
|
-
// hover-connective, click-for-detail map of the repository. Two
|
|
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
|
-
|
|
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
|
|
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="
|
|
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' ? '
|
|
268
|
-
html += '<div class="ab-chips"><span class="
|
|
269
|
-
(n.author ? '<span class="
|
|
270
|
-
(n.origin ? '<span class="
|
|
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="
|
|
274
|
-
var conns = (adj[n.id] || []).map(function (e) { var o = e.a === n.id ? e.b : e.a; var on =
|
|
275
|
-
if (conns.length) html += '<div class="ab-prow">Connects to</div><div class="ab-chips">' + conns.map(function (cc) { return '<
|
|
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 { '&': '&', '<': '<', '>': '>', '"': '"' }[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-
|
|
306
|
-
for (var i = 0; i < btns.length; i++) btns[i].
|
|
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-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|