@chrisdudek/yg 5.4.1 → 5.4.2
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/dist/bin.js +153 -41
- package/dist/templates/portal/app.css +278 -0
- package/dist/templates/portal/shell.css +257 -0
- package/dist/templates/portal/shell.html +39 -0
- package/dist/templates/portal/tokens.css +269 -0
- package/dist/templates/portal/views-audit.css +108 -0
- package/dist/templates/portal/views-flows.css +258 -0
- package/dist/templates/portal/views-panel.css +249 -0
- package/dist/templates/portal/views-relations.css +218 -0
- package/dist/templates/portal/views-rulebook.css +279 -0
- package/dist/templates/portal/views-start.css +208 -0
- package/dist/templates/portal/views.css +340 -0
- package/package.json +4 -2
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The three-region shell layout — nav rail, top bar, center stage, node panel.
|
|
3
|
+
*
|
|
4
|
+
* The persistent chrome the portal lives in: a left navigation rail (brand, ⌘K trigger,
|
|
5
|
+
* grouped view links), a top bar (live status + Refresh/Approve cluster, breadcrumb, ⌘K
|
|
6
|
+
* search, theme toggle), the center stage the dispatcher renders into, and a right-hand
|
|
7
|
+
* node-panel slot that opens on selection. Border-first elevation; one accent voltage
|
|
8
|
+
* (the primary button + the active rail link). Tokens come from tokens.css.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.app-grid {
|
|
12
|
+
display: grid;
|
|
13
|
+
grid-template-columns: 248px 1fr 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
}
|
|
16
|
+
.app-grid:has(.app-panel.open) {
|
|
17
|
+
grid-template-columns: 248px 1fr 420px;
|
|
18
|
+
}
|
|
19
|
+
.app-rail {
|
|
20
|
+
border-right: 1px solid var(--border-subtle);
|
|
21
|
+
background: var(--surface-1);
|
|
22
|
+
min-width: 0;
|
|
23
|
+
/* Pinned: the rail stays visible on scroll with its own overflow, so the tabs never
|
|
24
|
+
scroll away. The shell grid has min-height:100vh and the page/body scrolls. */
|
|
25
|
+
position: sticky;
|
|
26
|
+
top: 0;
|
|
27
|
+
align-self: start;
|
|
28
|
+
height: 100vh;
|
|
29
|
+
overflow-y: auto;
|
|
30
|
+
}
|
|
31
|
+
.rail-brand {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 9px;
|
|
35
|
+
width: 100%;
|
|
36
|
+
padding: 14px 16px;
|
|
37
|
+
border: 0;
|
|
38
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
39
|
+
background: none;
|
|
40
|
+
color: inherit;
|
|
41
|
+
font: inherit;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
text-align: left;
|
|
44
|
+
}
|
|
45
|
+
.rail-logo {
|
|
46
|
+
width: 36px;
|
|
47
|
+
height: 36px;
|
|
48
|
+
border-radius: 8px;
|
|
49
|
+
/* A deep chip so the gold brand mark reads on either theme; the SVG sits inside, contained.
|
|
50
|
+
A soft drop shadow + a hairline seat the chip on the near-white light rail (so it does not
|
|
51
|
+
read as a flat tile pasted onto the chrome) while staying unobtrusive in the dark theme. */
|
|
52
|
+
background: linear-gradient(160deg, #20242c, #15171b);
|
|
53
|
+
object-fit: contain;
|
|
54
|
+
padding: 4px;
|
|
55
|
+
flex: none;
|
|
56
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
|
|
57
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
58
|
+
}
|
|
59
|
+
.rail-brand b {
|
|
60
|
+
font-weight: 600;
|
|
61
|
+
}
|
|
62
|
+
.rail-brand-sub {
|
|
63
|
+
margin-left: auto;
|
|
64
|
+
font-size: 11px;
|
|
65
|
+
color: var(--text-muted);
|
|
66
|
+
}
|
|
67
|
+
.rail-cmdk {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 8px;
|
|
71
|
+
width: calc(100% - 28px);
|
|
72
|
+
margin: 12px 14px;
|
|
73
|
+
padding: 7px 10px;
|
|
74
|
+
border: 1px solid var(--border);
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
background: var(--bg);
|
|
77
|
+
color: var(--text-muted);
|
|
78
|
+
font: inherit;
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
.rail-cmdk kbd {
|
|
83
|
+
margin-left: auto;
|
|
84
|
+
font-size: 11px;
|
|
85
|
+
border: 1px solid var(--border);
|
|
86
|
+
border-radius: 5px;
|
|
87
|
+
padding: 1px 6px;
|
|
88
|
+
color: var(--text-secondary);
|
|
89
|
+
}
|
|
90
|
+
.rail-h {
|
|
91
|
+
padding: 6px 16px;
|
|
92
|
+
font-size: 11px;
|
|
93
|
+
text-transform: uppercase;
|
|
94
|
+
letter-spacing: 0.6px;
|
|
95
|
+
color: var(--text-muted);
|
|
96
|
+
}
|
|
97
|
+
.rail-nav {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
}
|
|
101
|
+
.rail-link {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 9px;
|
|
105
|
+
padding: 6px 16px;
|
|
106
|
+
border: 0;
|
|
107
|
+
border-left: 2px solid transparent;
|
|
108
|
+
background: none;
|
|
109
|
+
color: var(--text-secondary);
|
|
110
|
+
font: inherit;
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
text-align: left;
|
|
114
|
+
}
|
|
115
|
+
.rail-link:hover {
|
|
116
|
+
background: var(--surface-2);
|
|
117
|
+
}
|
|
118
|
+
.rail-link.on {
|
|
119
|
+
color: var(--text-primary);
|
|
120
|
+
background: var(--surface-3);
|
|
121
|
+
border-left-color: var(--accent);
|
|
122
|
+
}
|
|
123
|
+
.rail-glyph {
|
|
124
|
+
width: 16px;
|
|
125
|
+
text-align: center;
|
|
126
|
+
color: var(--text-muted);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.app-main {
|
|
130
|
+
min-width: 0;
|
|
131
|
+
}
|
|
132
|
+
.app-topbar {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 12px;
|
|
136
|
+
padding: 10px 22px;
|
|
137
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
138
|
+
/* Pinned to the top of the scroll, above the stage, with a solid background so the
|
|
139
|
+
content scrolling beneath it never shows through. */
|
|
140
|
+
position: sticky;
|
|
141
|
+
top: 0;
|
|
142
|
+
z-index: 20;
|
|
143
|
+
background: var(--bg);
|
|
144
|
+
}
|
|
145
|
+
.topbar-live {
|
|
146
|
+
display: inline-flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: 6px;
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
color: var(--text-muted);
|
|
151
|
+
}
|
|
152
|
+
.topbar-dot {
|
|
153
|
+
width: 7px;
|
|
154
|
+
height: 7px;
|
|
155
|
+
border-radius: 50%;
|
|
156
|
+
background: var(--verified);
|
|
157
|
+
}
|
|
158
|
+
.topbar-dot.muted {
|
|
159
|
+
background: var(--text-muted);
|
|
160
|
+
}
|
|
161
|
+
.topbar-crumb {
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
color: var(--text-secondary);
|
|
164
|
+
}
|
|
165
|
+
.topbar-spacer {
|
|
166
|
+
flex: 1;
|
|
167
|
+
}
|
|
168
|
+
.btn {
|
|
169
|
+
display: inline-flex;
|
|
170
|
+
align-items: center;
|
|
171
|
+
gap: 7px;
|
|
172
|
+
border: 1px solid var(--border);
|
|
173
|
+
border-radius: 9px;
|
|
174
|
+
padding: 7px 12px;
|
|
175
|
+
font: inherit;
|
|
176
|
+
font-size: 13px;
|
|
177
|
+
background: var(--bg);
|
|
178
|
+
color: var(--text-primary);
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
}
|
|
181
|
+
.btn:hover:not(:disabled) {
|
|
182
|
+
background: var(--surface-2);
|
|
183
|
+
}
|
|
184
|
+
.btn:disabled {
|
|
185
|
+
opacity: 0.5;
|
|
186
|
+
cursor: not-allowed;
|
|
187
|
+
}
|
|
188
|
+
.btn.primary {
|
|
189
|
+
background: var(--accent);
|
|
190
|
+
border-color: var(--accent);
|
|
191
|
+
color: #fff;
|
|
192
|
+
font-weight: 600;
|
|
193
|
+
}
|
|
194
|
+
.btn.primary:hover:not(:disabled) {
|
|
195
|
+
background: var(--accent-hover);
|
|
196
|
+
}
|
|
197
|
+
.btn-free {
|
|
198
|
+
font-size: 10px;
|
|
199
|
+
color: var(--verified);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.app-stage {
|
|
203
|
+
/* The bottom padding clears the pinned honest-key bar (see .legend-bar) so the last
|
|
204
|
+
row of content is never hidden behind it. */
|
|
205
|
+
padding: 20px 22px 96px;
|
|
206
|
+
/* Use the available width: wide surfaces (matrix, tree, tables) get the room they need.
|
|
207
|
+
A generous cap keeps long prose readable; margin-inline:auto centers it so the stage is
|
|
208
|
+
never a left-clipped column. */
|
|
209
|
+
max-width: 1600px;
|
|
210
|
+
margin-inline: auto;
|
|
211
|
+
width: 100%;
|
|
212
|
+
}
|
|
213
|
+
.app-panel {
|
|
214
|
+
background: var(--surface-1);
|
|
215
|
+
padding: 0;
|
|
216
|
+
/* The panel is its own scroll column: pinned full-height with its own overflow so a long
|
|
217
|
+
refusal reason or folded-input drill-through scrolls inside the panel instead of being
|
|
218
|
+
clipped. Closed, it is a zero-width slot with NO border — a left border on a 0-width grid
|
|
219
|
+
column would otherwise push the page 1px wider than the viewport (a spurious h-scrollbar). */
|
|
220
|
+
position: sticky;
|
|
221
|
+
top: 0;
|
|
222
|
+
align-self: start;
|
|
223
|
+
height: 100vh;
|
|
224
|
+
overflow-y: auto;
|
|
225
|
+
}
|
|
226
|
+
.app-panel.open {
|
|
227
|
+
border-left: 1px solid var(--border-subtle);
|
|
228
|
+
padding: 18px 18px 28px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── Narrow / split-screen desktop (≤900px): keep the layout overflow- and collision-free ──
|
|
232
|
+
The node panel becomes a right overlay SHEET instead of a reserved grid column (so it never
|
|
233
|
+
steals layout width or collides with the top bar), and the top bar drops its verbose live
|
|
234
|
+
caption so the action buttons never wrap or clip. A phone-width layout is out of scope. */
|
|
235
|
+
@media (max-width: 1000px) {
|
|
236
|
+
.app-grid:has(.app-panel.open) {
|
|
237
|
+
grid-template-columns: 248px 1fr 0;
|
|
238
|
+
}
|
|
239
|
+
.app-panel.open {
|
|
240
|
+
position: fixed;
|
|
241
|
+
top: 0;
|
|
242
|
+
right: 0;
|
|
243
|
+
width: min(420px, 92vw);
|
|
244
|
+
height: 100vh;
|
|
245
|
+
z-index: 40;
|
|
246
|
+
box-shadow: -6px 0 22px -6px rgba(0, 0, 0, 0.3);
|
|
247
|
+
}
|
|
248
|
+
.topbar-live span:not(.topbar-dot) {
|
|
249
|
+
display: none;
|
|
250
|
+
}
|
|
251
|
+
.topbar-crumb {
|
|
252
|
+
overflow: hidden;
|
|
253
|
+
text-overflow: ellipsis;
|
|
254
|
+
white-space: nowrap;
|
|
255
|
+
min-width: 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Yggdrasil Portal</title>
|
|
7
|
+
<!-- The brand mark, inlined as a data: URI by the serializer (offline, no network). -->
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="__PORTAL_FAVICON_HREF__" />
|
|
9
|
+
<style>
|
|
10
|
+
/* __PORTAL_CSS__ */
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<main id="portal-root" role="main"></main>
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
The portal data contract, inlined as one JSON blob. The consumer parses this
|
|
18
|
+
element's text content; nothing on the page reaches the network. type="application/json"
|
|
19
|
+
keeps the browser from executing it as script.
|
|
20
|
+
-->
|
|
21
|
+
<script id="portal-data" type="application/json">
|
|
22
|
+
/* __PORTAL_DATA__ */
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<!-- Vendored layout library (offline, no CDN). Exposes the global `d3`. -->
|
|
26
|
+
<script>
|
|
27
|
+
/* __PORTAL_VENDOR__ */
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<!--
|
|
31
|
+
Portal frontend modules, inlined in dependency order. No module system on a single
|
|
32
|
+
offline file — each module attaches to the shared window.YgPortal global, so a plain
|
|
33
|
+
sequence of <script> blocks shares code without import/require. The bootstrap runs last.
|
|
34
|
+
-->
|
|
35
|
+
<script>
|
|
36
|
+
/* __PORTAL_MODULES__ */
|
|
37
|
+
</script>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Design tokens + base + the honest status palette.
|
|
3
|
+
*
|
|
4
|
+
* The visual language's foundation: Radix Slate neutrals + Radix Blue accent as CSS custom
|
|
5
|
+
* properties (light is :root, dark is [data-theme="dark"], so theming "just works"), the
|
|
6
|
+
* base body/type/focus rules, and the HONEST STATUS PALETTE — every state rendered with
|
|
7
|
+
* color + glyph + border-style, never color alone (accessibility). The only saturated color
|
|
8
|
+
* on the page is a state. The shell/component layouts live in sibling files; this file is
|
|
9
|
+
* the source of truth a renderer's state classes resolve against.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #fcfcfd;
|
|
14
|
+
--surface-1: #f9f9fb;
|
|
15
|
+
--surface-2: #f0f0f3;
|
|
16
|
+
--surface-3: #e8e8ec;
|
|
17
|
+
--surface-4: #e0e1e6;
|
|
18
|
+
--border-subtle: #d9d9e0;
|
|
19
|
+
--border: #cdced6;
|
|
20
|
+
--border-strong: #b9bbc6;
|
|
21
|
+
--text-muted: #676b73;
|
|
22
|
+
--text-secondary: #60646c;
|
|
23
|
+
--text-primary: #1c2024;
|
|
24
|
+
--accent: #0090ff;
|
|
25
|
+
--accent-hover: #0588f0;
|
|
26
|
+
--accent-text: #0d74ce;
|
|
27
|
+
--focus-ring: #5eb1ef;
|
|
28
|
+
--verified: #208368;
|
|
29
|
+
--verified-bg: #e6f7ed;
|
|
30
|
+
--refused: #ce2c31;
|
|
31
|
+
--refused-bg: #feebec;
|
|
32
|
+
--unverified: #9a6700;
|
|
33
|
+
--unverified-bg: #fff7c2;
|
|
34
|
+
--norule: #60646c;
|
|
35
|
+
--norule-bg: #f0f0f3;
|
|
36
|
+
--waived: #8e4ec6;
|
|
37
|
+
--waived-bg: #f7edfe;
|
|
38
|
+
--live: #0d74ce;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-theme='dark'] {
|
|
42
|
+
--bg: #111113;
|
|
43
|
+
--surface-1: #18191b;
|
|
44
|
+
--surface-2: #212225;
|
|
45
|
+
--surface-3: #272a2d;
|
|
46
|
+
--surface-4: #2e3135;
|
|
47
|
+
--border-subtle: #363a3f;
|
|
48
|
+
--border: #43484e;
|
|
49
|
+
--border-strong: #5a6168;
|
|
50
|
+
--text-muted: #8b8f96;
|
|
51
|
+
--text-secondary: #b0b4ba;
|
|
52
|
+
--text-primary: #edeef0;
|
|
53
|
+
--accent: #0090ff;
|
|
54
|
+
--accent-hover: #3b9eff;
|
|
55
|
+
--accent-text: #70b8ff;
|
|
56
|
+
--focus-ring: #2870bd;
|
|
57
|
+
--verified: #3dd9a4;
|
|
58
|
+
--verified-bg: #0f2e22;
|
|
59
|
+
--refused: #ff9592;
|
|
60
|
+
--refused-bg: #3b1219;
|
|
61
|
+
--unverified: #ffca16;
|
|
62
|
+
--unverified-bg: #302008;
|
|
63
|
+
--norule: #b0b4ba;
|
|
64
|
+
--norule-bg: #212225;
|
|
65
|
+
--waived: #bf7af0;
|
|
66
|
+
--waived-bg: #2b1c3b;
|
|
67
|
+
--live: #70b8ff;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
* {
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
}
|
|
73
|
+
html,
|
|
74
|
+
body {
|
|
75
|
+
margin: 0;
|
|
76
|
+
}
|
|
77
|
+
body {
|
|
78
|
+
background: var(--bg);
|
|
79
|
+
color: var(--text-primary);
|
|
80
|
+
font: 14px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
81
|
+
}
|
|
82
|
+
.mono,
|
|
83
|
+
.chip-count,
|
|
84
|
+
.tree-type {
|
|
85
|
+
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
86
|
+
font-feature-settings: 'tnum', 'liga';
|
|
87
|
+
}
|
|
88
|
+
:focus-visible {
|
|
89
|
+
outline: 2px solid var(--focus-ring);
|
|
90
|
+
outline-offset: 1px;
|
|
91
|
+
border-radius: 4px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── Honest status palette — color + glyph + border, never color alone ──── */
|
|
95
|
+
.state-glyph {
|
|
96
|
+
display: inline-grid;
|
|
97
|
+
place-items: center;
|
|
98
|
+
width: 22px;
|
|
99
|
+
height: 22px;
|
|
100
|
+
border-radius: 6px;
|
|
101
|
+
font-size: 13px;
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
flex: none;
|
|
104
|
+
}
|
|
105
|
+
.state-verified {
|
|
106
|
+
color: var(--verified);
|
|
107
|
+
}
|
|
108
|
+
.state-glyph.state-verified {
|
|
109
|
+
background: var(--verified-bg);
|
|
110
|
+
border: 2px solid var(--verified);
|
|
111
|
+
}
|
|
112
|
+
.state-refused {
|
|
113
|
+
color: var(--refused);
|
|
114
|
+
}
|
|
115
|
+
.state-glyph.state-refused {
|
|
116
|
+
background: var(--refused-bg);
|
|
117
|
+
border: 2px solid var(--refused);
|
|
118
|
+
}
|
|
119
|
+
.state-unverified {
|
|
120
|
+
color: var(--unverified);
|
|
121
|
+
}
|
|
122
|
+
.state-glyph.state-unverified {
|
|
123
|
+
background: var(--unverified-bg);
|
|
124
|
+
border: 2px dashed var(--unverified);
|
|
125
|
+
}
|
|
126
|
+
.state-no-rule {
|
|
127
|
+
color: var(--norule);
|
|
128
|
+
}
|
|
129
|
+
.state-glyph.state-no-rule {
|
|
130
|
+
background: var(--norule-bg);
|
|
131
|
+
border: 2px dotted var(--norule);
|
|
132
|
+
}
|
|
133
|
+
.state-warning {
|
|
134
|
+
color: var(--unverified);
|
|
135
|
+
}
|
|
136
|
+
.state-glyph.state-warning {
|
|
137
|
+
background: var(--unverified-bg);
|
|
138
|
+
border: 2px dashed var(--unverified);
|
|
139
|
+
}
|
|
140
|
+
.state-not-applicable {
|
|
141
|
+
color: var(--text-muted);
|
|
142
|
+
}
|
|
143
|
+
.state-glyph.state-not-applicable {
|
|
144
|
+
background: var(--surface-2);
|
|
145
|
+
border: 1px solid var(--border-subtle);
|
|
146
|
+
}
|
|
147
|
+
.state-suppressed {
|
|
148
|
+
color: var(--waived);
|
|
149
|
+
}
|
|
150
|
+
.state-glyph.state-suppressed {
|
|
151
|
+
background: var(--waived-bg);
|
|
152
|
+
border: 2px dashed var(--waived);
|
|
153
|
+
}
|
|
154
|
+
.state-draft {
|
|
155
|
+
color: var(--text-muted);
|
|
156
|
+
}
|
|
157
|
+
.state-glyph.state-draft {
|
|
158
|
+
background: var(--surface-2);
|
|
159
|
+
border: 2px dotted var(--text-muted);
|
|
160
|
+
}
|
|
161
|
+
.state-boundary {
|
|
162
|
+
color: var(--refused);
|
|
163
|
+
}
|
|
164
|
+
.state-glyph.state-boundary {
|
|
165
|
+
background: var(--refused-bg);
|
|
166
|
+
border: 2px solid var(--refused);
|
|
167
|
+
background-image: repeating-linear-gradient(
|
|
168
|
+
45deg,
|
|
169
|
+
var(--refused-bg),
|
|
170
|
+
var(--refused-bg) 3px,
|
|
171
|
+
transparent 3px,
|
|
172
|
+
transparent 6px
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ── Legend — the honest key, as ONE pinned collapsible bottom bar ───────── */
|
|
177
|
+
.legend-h {
|
|
178
|
+
font-size: 11px;
|
|
179
|
+
text-transform: uppercase;
|
|
180
|
+
letter-spacing: 0.6px;
|
|
181
|
+
color: var(--text-muted);
|
|
182
|
+
margin: 0;
|
|
183
|
+
}
|
|
184
|
+
.legend-bar {
|
|
185
|
+
/* Pinned once to the bottom of the viewport, full width, above the stage content. The stage
|
|
186
|
+
reserves matching bottom padding so its last row never hides behind the collapsed bar. */
|
|
187
|
+
position: fixed;
|
|
188
|
+
left: 0;
|
|
189
|
+
right: 0;
|
|
190
|
+
bottom: 0;
|
|
191
|
+
z-index: 30;
|
|
192
|
+
background: var(--surface-1);
|
|
193
|
+
border-top: 1px solid var(--border);
|
|
194
|
+
box-shadow: 0 -2px 8px -4px rgba(0, 0, 0, 0.18);
|
|
195
|
+
padding: 8px 22px;
|
|
196
|
+
max-height: 70vh;
|
|
197
|
+
overflow-y: auto;
|
|
198
|
+
}
|
|
199
|
+
.legend-bar-head {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
gap: 14px;
|
|
203
|
+
flex-wrap: wrap;
|
|
204
|
+
}
|
|
205
|
+
.legend-toggle {
|
|
206
|
+
display: inline-flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
gap: 7px;
|
|
209
|
+
flex: none;
|
|
210
|
+
border: 0;
|
|
211
|
+
background: none;
|
|
212
|
+
font: inherit;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
padding: 2px 0;
|
|
215
|
+
color: var(--text-secondary);
|
|
216
|
+
}
|
|
217
|
+
.legend-caret {
|
|
218
|
+
font-size: 10px;
|
|
219
|
+
color: var(--text-muted);
|
|
220
|
+
}
|
|
221
|
+
.legend-chips {
|
|
222
|
+
display: flex;
|
|
223
|
+
flex-wrap: wrap;
|
|
224
|
+
gap: 6px 12px;
|
|
225
|
+
align-items: center;
|
|
226
|
+
}
|
|
227
|
+
.legend-chip {
|
|
228
|
+
display: inline-flex;
|
|
229
|
+
align-items: center;
|
|
230
|
+
gap: 6px;
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
color: var(--text-secondary);
|
|
233
|
+
}
|
|
234
|
+
.legend-chip .state-glyph {
|
|
235
|
+
width: 18px;
|
|
236
|
+
height: 18px;
|
|
237
|
+
font-size: 11px;
|
|
238
|
+
}
|
|
239
|
+
.legend-chip-l {
|
|
240
|
+
white-space: nowrap;
|
|
241
|
+
}
|
|
242
|
+
/* The full descriptions are hidden until the bar is expanded. */
|
|
243
|
+
.legend-grid {
|
|
244
|
+
display: none;
|
|
245
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
246
|
+
gap: 10px;
|
|
247
|
+
margin-top: 12px;
|
|
248
|
+
}
|
|
249
|
+
.legend-bar.legend-open .legend-grid {
|
|
250
|
+
display: grid;
|
|
251
|
+
}
|
|
252
|
+
.legend-item {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 12px;
|
|
256
|
+
border: 1px solid var(--border-subtle);
|
|
257
|
+
border-radius: 10px;
|
|
258
|
+
padding: 10px 12px;
|
|
259
|
+
background: var(--bg);
|
|
260
|
+
}
|
|
261
|
+
.legend-text b {
|
|
262
|
+
font-weight: 600;
|
|
263
|
+
}
|
|
264
|
+
.legend-text span {
|
|
265
|
+
display: block;
|
|
266
|
+
color: var(--text-muted);
|
|
267
|
+
font-size: 12px;
|
|
268
|
+
margin-top: 1px;
|
|
269
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Phase-5 audit-depth styles — the portable-export bar, the file-aware freshness strip on the
|
|
3
|
+
* landing, and the node panel's provenance pins + freshness banner. Kept in its own focused
|
|
4
|
+
* file so the larger view stylesheets stay under the focused-file cap. Tokens come from
|
|
5
|
+
* tokens.css; the only saturated color is a state, resolved through the shared state classes —
|
|
6
|
+
* nothing here invents a green.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* ── Portable export bar (Coverage & Audit + Suppressions) ─────────────────── */
|
|
10
|
+
.exp-bar {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-wrap: wrap;
|
|
13
|
+
align-items: center;
|
|
14
|
+
gap: 8px;
|
|
15
|
+
margin: 12px 0 18px;
|
|
16
|
+
}
|
|
17
|
+
.exp-lbl {
|
|
18
|
+
color: var(--text-secondary);
|
|
19
|
+
font-size: 12px;
|
|
20
|
+
margin-right: 4px;
|
|
21
|
+
}
|
|
22
|
+
.exp-btn {
|
|
23
|
+
border: 1px solid var(--border);
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
background: var(--surface-1);
|
|
26
|
+
color: var(--text-primary);
|
|
27
|
+
font-size: 12px;
|
|
28
|
+
padding: 6px 12px;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
.exp-btn:hover {
|
|
32
|
+
background: var(--surface-2);
|
|
33
|
+
}
|
|
34
|
+
.exp-btn:focus-visible {
|
|
35
|
+
outline: 2px solid var(--accent);
|
|
36
|
+
outline-offset: 2px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* ── V1 file-aware freshness strip (the heartbeat) ─────────────────────────── */
|
|
40
|
+
.ov-fresh {
|
|
41
|
+
border: 1px dashed var(--state-unverified-border, var(--border));
|
|
42
|
+
border-left-width: 4px;
|
|
43
|
+
border-radius: 12px;
|
|
44
|
+
background: var(--surface-1);
|
|
45
|
+
padding: 14px 16px;
|
|
46
|
+
margin-bottom: 16px;
|
|
47
|
+
}
|
|
48
|
+
.ov-fresh-head {
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 8px;
|
|
52
|
+
font-size: 15px;
|
|
53
|
+
}
|
|
54
|
+
.ov-fresh-sub {
|
|
55
|
+
color: var(--text-secondary);
|
|
56
|
+
font-size: 12.5px;
|
|
57
|
+
line-height: 1.5;
|
|
58
|
+
margin: 6px 0 10px;
|
|
59
|
+
}
|
|
60
|
+
.ov-fresh-chips {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
gap: 6px;
|
|
64
|
+
}
|
|
65
|
+
.ov-fresh-chip {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 6px;
|
|
69
|
+
border: 1px solid var(--border);
|
|
70
|
+
border-radius: 999px;
|
|
71
|
+
background: var(--surface-2);
|
|
72
|
+
color: var(--text-primary);
|
|
73
|
+
font-size: 11.5px;
|
|
74
|
+
padding: 4px 10px;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
}
|
|
77
|
+
.ov-fresh-chip:focus-visible {
|
|
78
|
+
outline: 2px solid var(--accent);
|
|
79
|
+
outline-offset: 2px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── Node panel: provenance pins + freshness banner ────────────────────────── */
|
|
83
|
+
.pan-prov {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
margin: 8px 0 4px;
|
|
88
|
+
font-size: 11px;
|
|
89
|
+
color: var(--text-muted);
|
|
90
|
+
}
|
|
91
|
+
.pan-prov-k {
|
|
92
|
+
border: 1px solid var(--border);
|
|
93
|
+
border-radius: 6px;
|
|
94
|
+
padding: 2px 7px;
|
|
95
|
+
background: var(--surface-2);
|
|
96
|
+
}
|
|
97
|
+
.pan-fresh {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: flex-start;
|
|
100
|
+
gap: 8px;
|
|
101
|
+
border: 1px dashed var(--state-unverified-border, var(--border));
|
|
102
|
+
border-radius: 10px;
|
|
103
|
+
padding: 10px 12px;
|
|
104
|
+
margin: 8px 0;
|
|
105
|
+
font-size: 12px;
|
|
106
|
+
line-height: 1.45;
|
|
107
|
+
color: var(--text-secondary);
|
|
108
|
+
}
|