@booboo-brain/panel 0.5.3 → 0.5.5
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/index.d.ts +2 -1
- package/dist/index.js +1186 -248
- package/dist-app/assets/index-DuaKsKpo.js +1090 -0
- package/dist-app/index.html +24 -19
- package/package.json +2 -2
- package/dist-app/assets/index-BhChad7x.js +0 -586
package/dist/index.js
CHANGED
|
@@ -1,73 +1,182 @@
|
|
|
1
1
|
// src/Panel.tsx
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { orgBootSlice } from "@booboo-brain/spec";
|
|
3
|
+
import { orgBootSlice, relTime } from "@booboo-brain/spec";
|
|
4
4
|
|
|
5
5
|
// src/panel-css.ts
|
|
6
6
|
var PANEL_CSS = String.raw`
|
|
7
|
+
/* GENERATED from design/tokens.json by scripts/gen-tokens.mjs — DO NOT EDIT */
|
|
8
|
+
.pnl, .pnl-fatal {
|
|
9
|
+
--bg: #06080e;
|
|
10
|
+
--bg-lift: #0a0d15;
|
|
11
|
+
--card: #0f131c;
|
|
12
|
+
--line: #1c2130;
|
|
13
|
+
--line-hi: #2b3243;
|
|
14
|
+
--text: #E8DCC4;
|
|
15
|
+
--dim: #8a8268;
|
|
16
|
+
--faint: #585240;
|
|
17
|
+
--gold: #c9a04a;
|
|
18
|
+
--gold-hi: #E8C877;
|
|
19
|
+
--teal: #4ECDC4;
|
|
20
|
+
--violet: #a78bd0;
|
|
21
|
+
--green: #5fae7e;
|
|
22
|
+
--amber: #d6a23e;
|
|
23
|
+
--red: #d05a5a;
|
|
24
|
+
--wine: #0b0509;
|
|
25
|
+
--wine2: #140a11;
|
|
26
|
+
--plate: #1c1119;
|
|
27
|
+
--plate-lift: #241624;
|
|
28
|
+
--brass: #d9a05b;
|
|
29
|
+
--brass-hi: #efc389;
|
|
30
|
+
--brass-deep: #a9753a;
|
|
31
|
+
--mint: #8be9b0;
|
|
32
|
+
--cream: #f4eee2;
|
|
33
|
+
--cream-dim: #b3a897;
|
|
34
|
+
--cream-faint: #7c7365;
|
|
35
|
+
--verb-reports-to: #c9a04a;
|
|
36
|
+
--verb-declares: #E8C877;
|
|
37
|
+
--verb-amends: #E8C877;
|
|
38
|
+
--verb-inherits: #8a8268;
|
|
39
|
+
--verb-owns: #4ECDC4;
|
|
40
|
+
--verb-reads: #3a7a74;
|
|
41
|
+
--verb-escalates-to: #d05a5a;
|
|
42
|
+
--verb-covers: #5fae7e;
|
|
43
|
+
--verb-supplies: #a8815a;
|
|
44
|
+
--verb-audits: #a78bd0;
|
|
45
|
+
--verb-spine: #29242f;
|
|
46
|
+
--display: Fraunces, ui-serif, Georgia, serif;
|
|
47
|
+
--ui: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
|
|
48
|
+
--mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
|
49
|
+
--ease-swift: cubic-bezier(0.2, 0.7, 0.25, 1);
|
|
50
|
+
--ease-settle: cubic-bezier(0.16, 1, 0.3, 1);
|
|
51
|
+
--z-canvas: 0;
|
|
52
|
+
--z-hud: 10;
|
|
53
|
+
--z-dossier: 20;
|
|
54
|
+
--z-palette: 30;
|
|
55
|
+
--z-toast: 40;
|
|
56
|
+
}
|
|
57
|
+
|
|
7
58
|
/* Booboo Organigram — alive, simple, for everyone. One accent (verdigris,
|
|
8
59
|
the dachshund's collar), a slow aurora breathing behind everything, cards
|
|
9
60
|
that lift and settle, numbers that count up. Dark by default. */
|
|
10
61
|
|
|
11
|
-
:root
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
62
|
+
/* Scoped to the panel's own roots, NEVER :root — see scripts/gen-tokens.mjs.
|
|
63
|
+
A custom property on :root is shadowed by any ancestor declaring the same
|
|
64
|
+
name, so a host whose shell declares --bg-1/--line/--gold silently repaints
|
|
65
|
+
the board. Declaring them here means nothing can sit between these names and
|
|
66
|
+
the elements that read them. */
|
|
67
|
+
.pnl, .pnl-fatal {
|
|
68
|
+
/* THE STAFF BOARD — a master-key rack, not a dashboard.
|
|
69
|
+
Structure comes from two real artefacts: a master-key hierarchy (passe
|
|
70
|
+
général → passe partiel → cylinder, which is literally how a hotel is
|
|
71
|
+
keyed, and exactly how authority cascades here) and a ministry organigram
|
|
72
|
+
(rank is NAMED in column headers, never inferred from indentation).
|
|
73
|
+
Material comes from the approved FHQ hero designs — wine-black ground,
|
|
74
|
+
brass rails, real vendor marks on white chips.
|
|
75
|
+
Raw values are generated into app/tokens.css from design/tokens.json
|
|
76
|
+
(scripts/gen-tokens.mjs); these are the panel's semantic aliases over them.
|
|
77
|
+
RANK READS THROUGH FINISH, NEVER HUE. The old board coloured each card by
|
|
78
|
+
hash(agent.id) — nine departments, nine arbitrary neon hues — which is the
|
|
79
|
+
loudest "generated by a dev tool" signal there is, and it disagreed with
|
|
80
|
+
the cosmos's own gold/silver/bronze ladder. */
|
|
81
|
+
/* LIGHT IS THE DEFAULT (Jesse, 2026-07-19: "design for everyone — a white
|
|
82
|
+
background, very light bubbles, beautiful refined display"). A whole hotel
|
|
83
|
+
uses this, not a room of developers; a stranger should not land in a
|
|
84
|
+
terminal. Dark remains, one toggle away, below.
|
|
85
|
+
The ground is a WARM white, never flat #fff — a lit room, not a sheet of
|
|
86
|
+
paper. Depth comes from layered shadow, so ink stays the only dark thing
|
|
87
|
+
on screen and brass drops from material to ~5% accent. */
|
|
88
|
+
--bg: #fcfbf9;
|
|
89
|
+
--bg-1: #fffefb; /* tinted toward the brass hue, never flat #fff */
|
|
90
|
+
--bg-2: #f6f4f0;
|
|
91
|
+
--line: #e9e5df;
|
|
92
|
+
--line-soft: #f0ece6;
|
|
93
|
+
--line-2: #d9d3c9;
|
|
94
|
+
--surface-tray: rgba(255, 255, 255, .5);
|
|
95
|
+
/* the ground-plane dot. White-on-white rendered NOTHING on the light board
|
|
96
|
+
— the texture was there in the stylesheet and absent on screen. */
|
|
97
|
+
--dot: rgba(27, 24, 21, 0.055);
|
|
98
|
+
--surface-lift: linear-gradient(180deg, #fffdf8, #ffffff);
|
|
99
|
+
--surface-lift-edge: #e8d9bd;
|
|
100
|
+
--ink: #1b1815;
|
|
101
|
+
--ink-2: #6b6459;
|
|
102
|
+
--ink-3: #9a9287;
|
|
103
|
+
--accent: #a9753a;
|
|
104
|
+
--accent-2: #c9954f;
|
|
105
|
+
--accent-dim: #f7f0e4;
|
|
106
|
+
--warn: #b8862c;
|
|
107
|
+
--warn-wash: #fbf3e3;
|
|
108
|
+
--ok: #3f9c6d;
|
|
109
|
+
--ok-wash: #eaf5ef;
|
|
110
|
+
--fail: #c0553f;
|
|
111
|
+
--fail-wash: #fbeeea;
|
|
112
|
+
/* one shadow set, three depths — the only source of elevation */
|
|
113
|
+
--sh-1: 0 1px 2px rgba(27, 24, 21, .04), 0 1px 1px rgba(27, 24, 21, .03);
|
|
114
|
+
--sh-2: 0 2px 4px rgba(27, 24, 21, .04), 0 6px 16px -6px rgba(27, 24, 21, .10);
|
|
115
|
+
--sh-3: 0 4px 8px rgba(27, 24, 21, .05), 0 16px 34px -12px rgba(27, 24, 21, .16);
|
|
116
|
+
/* motion — slight, never showy. Everything eases on these two. */
|
|
117
|
+
--ease: cubic-bezier(.2, .7, .25, 1);
|
|
118
|
+
--settle: cubic-bezier(.16, 1, .3, 1);
|
|
119
|
+
--font: "DM Sans", -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
|
|
120
|
+
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
121
|
+
--display: "Fraunces", ui-serif, Georgia, serif;
|
|
122
|
+
--brass-line: linear-gradient(180deg, var(--brass-hi), var(--brass) 55%, var(--brass-deep));
|
|
25
123
|
}
|
|
26
124
|
|
|
27
|
-
/*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
125
|
+
/* DARK — the wine-and-brass board, now one toggle away instead of the default.
|
|
126
|
+
Same structure, same shadows-as-depth logic, inverted ground.
|
|
127
|
+
Scoped to the panel's own root (not :root) for two reasons: the panel must
|
|
128
|
+
not read a HOST's data-theme as its own, and it must not write one either. */
|
|
129
|
+
.pnl[data-theme="dark"], .pnl-fatal[data-theme="dark"] {
|
|
130
|
+
--bg: var(--wine);
|
|
131
|
+
--bg-1: var(--plate);
|
|
132
|
+
--bg-2: var(--wine2);
|
|
133
|
+
--line: rgba(244, 238, 226, 0.13);
|
|
134
|
+
--line-soft: rgba(244, 238, 226, 0.08);
|
|
135
|
+
--line-2: rgba(244, 238, 226, 0.22);
|
|
136
|
+
--surface-tray: rgba(11, 5, 9, .4);
|
|
137
|
+
--dot: rgba(255, 255, 255, 0.035);
|
|
138
|
+
--surface-lift: linear-gradient(168deg, #3a2a16, #1d1410);
|
|
139
|
+
--surface-lift-edge: rgba(239, 195, 137, .4);
|
|
140
|
+
--ink: var(--cream);
|
|
141
|
+
--ink-2: var(--cream-dim);
|
|
142
|
+
--ink-3: var(--cream-faint);
|
|
143
|
+
--accent: var(--brass);
|
|
144
|
+
--accent-2: var(--brass-hi);
|
|
145
|
+
--accent-dim: rgba(217, 160, 91, 0.12);
|
|
146
|
+
--warn: var(--brass);
|
|
147
|
+
--warn-wash: rgba(217, 160, 91, 0.10);
|
|
148
|
+
--ok: var(--mint);
|
|
149
|
+
--ok-wash: rgba(139, 233, 176, 0.10);
|
|
150
|
+
--fail: #e07b6a;
|
|
151
|
+
--fail-wash: rgba(224, 123, 106, 0.10);
|
|
152
|
+
--sh-1: 0 1px 2px rgba(0, 0, 0, .5);
|
|
153
|
+
--sh-2: 0 2px 4px rgba(0, 0, 0, .5), 0 16px 34px -18px rgba(0, 0, 0, .85);
|
|
154
|
+
--sh-3: 0 4px 8px rgba(0, 0, 0, .5), 0 22px 48px -18px rgba(0, 0, 0, .9);
|
|
155
|
+
}
|
|
156
|
+
/* The [data-theme="dark"] body rule that used to live here is GONE. A mounted
|
|
157
|
+
component has no business repainting its host's <body> — in Dionisos OS it
|
|
158
|
+
turned the cockpit's body white while the cards stayed dark, which is half of
|
|
159
|
+
what "split-brain" looked like. The standalone app paints its own body in
|
|
160
|
+
app/index.html, which is the page that actually owns it. */
|
|
161
|
+
.pnl[data-theme="dark"] .ag { background: linear-gradient(168deg, var(--plate-lift), #180e16); }
|
|
162
|
+
.pnl[data-theme="dark"] .bar, .pnl[data-theme="dark"] .tabs { background: rgba(11, 5, 9, 0.72); }
|
|
163
|
+
.pnl[data-theme="dark"] .rack .wt { background: #fff; border-color: transparent; }
|
|
164
|
+
.pnl[data-theme="dark"] .pnl-stars { opacity: .45; }
|
|
58
165
|
|
|
59
|
-
* { box-sizing: border-box; }
|
|
60
|
-
body { font-family: var(--font); color: var(--ink); background: var(--bg); overflow-x: hidden; }
|
|
166
|
+
.pnl, .pnl-fatal, .pnl *, .pnl-fatal * { box-sizing: border-box; }
|
|
61
167
|
|
|
168
|
+
.pnl, .pnl-fatal { font-family: var(--font); color: var(--ink); background: var(--bg); }
|
|
62
169
|
.pnl { display: flex; flex-direction: column; position: relative; overflow: hidden; }
|
|
63
170
|
|
|
64
|
-
/* the
|
|
171
|
+
/* the ground — warm wine light pooling from the top-left, exactly as the hero
|
|
172
|
+
grades it. NO GRAIN/NOISE LAYER: the mockup carried a feTurbulence overlay
|
|
173
|
+
and it read as a blurry filter smeared over everything (Jesse, 2026-07-19).
|
|
174
|
+
The atmosphere is gradient + constellation only. */
|
|
65
175
|
.pnl-aurora {
|
|
66
|
-
position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity:
|
|
176
|
+
position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity: 1;
|
|
67
177
|
background:
|
|
68
|
-
radial-gradient(
|
|
69
|
-
radial-gradient(
|
|
70
|
-
radial-gradient(24% 22% at 55% 15%, rgba(79, 184, 160, 0.06), transparent 70%);
|
|
178
|
+
radial-gradient(55% 45% at 12% 0%, rgba(201, 149, 79, .055), transparent 62%),
|
|
179
|
+
radial-gradient(45% 40% at 95% 100%, rgba(63, 156, 109, .035), transparent 62%);
|
|
71
180
|
animation: aurora 48s ease-in-out infinite alternate;
|
|
72
181
|
}
|
|
73
182
|
@keyframes aurora {
|
|
@@ -87,12 +196,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
87
196
|
/* top bar */
|
|
88
197
|
.bar {
|
|
89
198
|
position: relative; z-index: 2; display: flex; align-items: center; gap: 22px;
|
|
90
|
-
padding: 14px 26px; border-bottom: 1px solid var(--line);
|
|
91
|
-
background: rgba(
|
|
199
|
+
padding: 14px 26px; border-bottom: 1px solid var(--line-soft);
|
|
200
|
+
background: rgba(252, 251, 249, 0.82); backdrop-filter: blur(10px);
|
|
92
201
|
flex-wrap: wrap;
|
|
93
202
|
}
|
|
94
|
-
.bar-brand { font-size:
|
|
95
|
-
.bar-brand b { font-weight:
|
|
203
|
+
.bar-brand { font-size: 19px; font-family: var(--display); font-weight: 600; letter-spacing: -0.01em; }
|
|
204
|
+
.bar-brand b { font-weight: 600; color: var(--accent-2); }
|
|
96
205
|
.bar-stats { display: flex; gap: 18px; color: var(--ink-2); font-size: 12.5px; }
|
|
97
206
|
.bar-stats b { color: var(--ink); font-variant-numeric: tabular-nums; font-size: 14px; }
|
|
98
207
|
.bar-stats .tap { cursor: pointer; transition: color 0.15s; }
|
|
@@ -106,22 +215,23 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
106
215
|
.btn {
|
|
107
216
|
border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
|
|
108
217
|
border-radius: 9px; padding: 8px 15px; font-size: 12.5px; cursor: pointer;
|
|
109
|
-
transition: border-color
|
|
218
|
+
transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
|
|
110
219
|
}
|
|
111
220
|
.btn:hover { border-color: var(--accent); }
|
|
112
221
|
.btn:active { transform: scale(0.97); }
|
|
113
|
-
.btn.primary { background: var(--accent); border-color: var(--accent); color: #
|
|
222
|
+
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 650; }
|
|
114
223
|
.btn.primary:disabled { opacity: 0.5; cursor: wait; }
|
|
115
224
|
.btn.ghost { background: transparent; color: var(--ink-2); }
|
|
116
225
|
|
|
117
226
|
/* tabs */
|
|
118
227
|
.tabs {
|
|
119
228
|
position: relative; z-index: 2; display: flex; gap: 4px; padding: 0 22px;
|
|
120
|
-
border-bottom: 1px solid var(--line); background: rgba(
|
|
229
|
+
border-bottom: 1px solid var(--line); background: rgba(252, 251, 249, 0.7); backdrop-filter: blur(8px);
|
|
121
230
|
overflow-x: auto;
|
|
122
231
|
}
|
|
123
232
|
.tab {
|
|
124
233
|
background: none; border: none; border-bottom: 2px solid transparent; color: var(--ink-2);
|
|
234
|
+
transition: color .18s var(--ease), border-color .22s var(--ease);
|
|
125
235
|
font-size: 12.5px; padding: 12px 15px 11px; cursor: pointer; white-space: nowrap;
|
|
126
236
|
transition: color 0.15s, border-color 0.2s;
|
|
127
237
|
}
|
|
@@ -197,7 +307,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
197
307
|
|
|
198
308
|
/* rules */
|
|
199
309
|
.rule-list { display: flex; flex-direction: column; gap: 9px; }
|
|
200
|
-
.rule-card { background: var(--bg-1); border: 1px solid var(--line); border-
|
|
310
|
+
.rule-card { background: var(--bg-1); border: 1px solid var(--line); border-radius: 11px; padding: 12px 15px 10px; animation: settle 0.35s ease both; transition: border-color 0.15s; }
|
|
201
311
|
.rule-card:hover { border-color: var(--accent); }
|
|
202
312
|
.rule-top { display: flex; align-items: baseline; gap: 10px; margin-bottom: 5px; }
|
|
203
313
|
.rule-ref { font-family: var(--mono); font-size: 12.5px; }
|
|
@@ -229,19 +339,17 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
229
339
|
/* the machine rack — a recessed slot flush under the owner's card */
|
|
230
340
|
.oc-tray {
|
|
231
341
|
display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;
|
|
232
|
-
max-width: 210px; margin-top: -
|
|
233
|
-
background:
|
|
234
|
-
border: 1px solid var(--line); border-top: 0; border-radius: 0 0 14px 14px;
|
|
235
|
-
box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.55);
|
|
342
|
+
max-width: 210px; margin-top: -4px; padding: 10px 8px 7px;
|
|
343
|
+
background: transparent; border: 0; padding: 8px 0 0;
|
|
236
344
|
}
|
|
237
|
-
.oc-tray.fail { border-color: rgba(224, 123, 106, 0.
|
|
238
|
-
.oc-tray.warn { border-color: rgba(
|
|
345
|
+
.oc-tray.fail { border-color: rgba(224, 123, 106, 0.5); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.5), 0 6px 18px -8px rgba(224, 123, 106, 0.28); }
|
|
346
|
+
.oc-tray.warn { border-color: rgba(217, 160, 91, 0.42); }
|
|
239
347
|
.oc-mac {
|
|
240
|
-
display: inline-flex; align-items: center; gap:
|
|
241
|
-
background:
|
|
242
|
-
border-radius:
|
|
243
|
-
font-family: var(--mono); font-size:
|
|
244
|
-
transition: border-color
|
|
348
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
349
|
+
background: var(--bg-2); border: 1px solid var(--line);
|
|
350
|
+
border-radius: 8px; padding: 5px 9px; cursor: pointer;
|
|
351
|
+
font-family: var(--mono); font-size: 9.5px; color: var(--ink-2);
|
|
352
|
+
transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
|
|
245
353
|
}
|
|
246
354
|
.oc-mac:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
|
|
247
355
|
.oc-mac.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--ink); }
|
|
@@ -249,6 +357,21 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
249
357
|
.oc-mac.dragging { opacity: 0.45; cursor: grabbing; }
|
|
250
358
|
.oc-mac.more:hover { color: var(--accent); }
|
|
251
359
|
.mac-emoji { font-size: 11px; }
|
|
360
|
+
/* real vendor marks, tinted to the house so twelve logos read as one system.
|
|
361
|
+
Full brand colour on hover — recognition when you want it, calm when you
|
|
362
|
+
don't. */
|
|
363
|
+
/* the hero's treatment: a real vendor mark on a WHITE chip. Twelve tinted
|
|
364
|
+
monochrome logos read as a sticker album; twelve white chips read as a rack
|
|
365
|
+
of real systems — and it is the one thing on the old board that already
|
|
366
|
+
looked genuine, so everything else was raised to it rather than it lowered. */
|
|
367
|
+
.mac-brand {
|
|
368
|
+
width: 24px; height: 24px; flex: 0 0 24px; display: block;
|
|
369
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 7px;
|
|
370
|
+
padding: 4px; box-sizing: border-box;
|
|
371
|
+
transition: transform .2s ease, box-shadow .2s ease;
|
|
372
|
+
}
|
|
373
|
+
.oc-mac:hover .mac-brand,
|
|
374
|
+
.oc-mac.sel .mac-brand { transform: scale(1.06); box-shadow: 0 5px 14px -5px rgba(0, 0, 0, 0.85); }
|
|
252
375
|
.mac-name { white-space: nowrap; max-width: 76px; overflow: hidden; text-overflow: ellipsis; }
|
|
253
376
|
.mac-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; display: inline-block; }
|
|
254
377
|
.mac-dot.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
|
|
@@ -285,10 +408,35 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
285
408
|
.bk-agents.unassigned { color: var(--ink-3); font-style: italic; }
|
|
286
409
|
|
|
287
410
|
.pending {
|
|
288
|
-
position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap;
|
|
289
|
-
padding: 10px 26px; border-bottom: 1px solid var(--line); background:
|
|
411
|
+
position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
|
412
|
+
padding: 10px 26px; border-bottom: 1px solid var(--line); background: var(--warn-wash);
|
|
413
|
+
}
|
|
414
|
+
.pending-lead {
|
|
415
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
|
|
416
|
+
color: var(--warn); margin-right: 2px;
|
|
417
|
+
}
|
|
418
|
+
/* every pending change is its own undo button. A global discard makes one
|
|
419
|
+
mis-drop cost every other edit, so people stop dragging entirely. */
|
|
420
|
+
.pending-item {
|
|
421
|
+
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
|
|
422
|
+
font-size: 11.5px; font-family: var(--mono); color: var(--ink-2);
|
|
423
|
+
background: var(--bg-1); border: 1px solid var(--line); border-radius: 100px;
|
|
424
|
+
padding: 4px 6px 4px 12px;
|
|
425
|
+
transition: border-color .18s var(--ease), color .18s ease, transform .16s var(--ease);
|
|
426
|
+
}
|
|
427
|
+
.pending-item i {
|
|
428
|
+
font-style: normal; font-size: 8.5px; letter-spacing: .12em; text-transform: uppercase;
|
|
429
|
+
color: var(--warn); border: 1px solid rgba(184,134,44,.3); border-radius: 100px; padding: 2px 7px;
|
|
430
|
+
transition: background .18s ease, color .18s ease;
|
|
431
|
+
}
|
|
432
|
+
.pending-item:hover { border-color: var(--warn); color: var(--ink); transform: translateY(-1px); }
|
|
433
|
+
.pending-item:hover i { background: var(--warn); color: var(--bg-1); }
|
|
434
|
+
|
|
435
|
+
/* the lane lights up as a drop target, so a near-miss is impossible */
|
|
436
|
+
.oc-row.lanes > .oc-child.lane-over {
|
|
437
|
+
border-color: var(--accent); background: var(--accent-dim);
|
|
438
|
+
box-shadow: 0 0 0 3px var(--accent-dim), var(--sh-2);
|
|
290
439
|
}
|
|
291
|
-
.pending-item { font-size: 11.5px; color: var(--warn); font-family: var(--mono); }
|
|
292
440
|
.pnl-err { position: relative; z-index: 2; padding: 10px 26px; color: #e07b6a; font-size: 12.5px; }
|
|
293
441
|
|
|
294
442
|
/* layout — 100% so a HOST container decides the height (embedded panel);
|
|
@@ -301,17 +449,59 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
301
449
|
+ a verdigris pool anchoring the eye at the root. Static — capture-safe. */
|
|
302
450
|
background:
|
|
303
451
|
radial-gradient(900px 420px at 50% 120px, rgba(79, 184, 160, 0.05), transparent 70%),
|
|
304
|
-
radial-gradient(circle at 1px 1px,
|
|
452
|
+
radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 1.5px);
|
|
305
453
|
background-size: auto, 28px 28px;
|
|
306
454
|
}
|
|
307
|
-
.tree-hint { color: var(--ink-3); font-size:
|
|
455
|
+
.tree-hint { color: var(--ink-3); font-family: var(--mono); font-size: 10px; letter-spacing: .05em; margin: 0 0 14px; text-align: center; }
|
|
456
|
+
|
|
457
|
+
/* ── RANK HEADERS (from the ministry organigram) ───────────────────────────
|
|
458
|
+
Rank is NAMED, never inferred from indentation. Roman numerals + a rule
|
|
459
|
+
under each, spanning the columns the cascade actually occupies. */
|
|
460
|
+
/* four headers over the four real cascade columns: law · gm · departments ·
|
|
461
|
+
staff. Widths mirror .cascade so a header sits over the column it names —
|
|
462
|
+
headers labelling columns that do not exist is worse than no headers. */
|
|
463
|
+
.ranks {
|
|
464
|
+
display: grid;
|
|
465
|
+
grid-template-columns: 232px var(--rail-w, 56px) 268px var(--rail-w, 56px) minmax(232px, 300px) var(--rail-w, 56px) 1fr;
|
|
466
|
+
margin: 0 0 16px; padding: 0 16px; flex: 0 0 auto;
|
|
467
|
+
}
|
|
468
|
+
.rank:nth-child(1) { grid-column: 1; }
|
|
469
|
+
.rank:nth-child(2) { grid-column: 3; }
|
|
470
|
+
.rank:nth-child(3) { grid-column: 5; }
|
|
471
|
+
.rank:nth-child(4) { grid-column: 7; }
|
|
472
|
+
.rank {
|
|
473
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: .2em; text-transform: uppercase;
|
|
474
|
+
color: var(--ink-3); padding-bottom: 8px; border-bottom: 1px solid rgba(217, 160, 91, 0.16);
|
|
475
|
+
position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
476
|
+
}
|
|
477
|
+
.rank b { color: var(--brass); font-weight: 500; margin-right: 7px; }
|
|
478
|
+
.rank::after { content: ""; position: absolute; left: 0; bottom: -1px; width: 24px; height: 1px; background: var(--brass); }
|
|
479
|
+
|
|
480
|
+
/* the constellation — the hero's own atmosphere, drawn once behind the rack.
|
|
481
|
+
Gradient + points only; NO grain/noise overlay (it read as a blurry filter
|
|
482
|
+
smeared across the board). */
|
|
483
|
+
.pnl-stars { position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0; width: 100%; height: 100%; }
|
|
308
484
|
|
|
309
485
|
/* THE COMPANY CHART — root on top, branches fanning beneath. Connectors are
|
|
310
486
|
crafted: curved elbows at the row's ends, branch-hued drops, a glowing
|
|
311
487
|
junction node where the parent's line meets the bar. Pure CSS. */
|
|
312
488
|
/* the measuring viewport — flex:1 + min-height:0 lets it consume the height
|
|
313
489
|
.tree gives it; align/justify center the scaled block for narrow orgs. */
|
|
314
|
-
|
|
490
|
+
/* Always scrollable. It used to be overflow:hidden unless the user manually
|
|
491
|
+
zoomed — safe only while auto-fit guaranteed the whole board fit one screen.
|
|
492
|
+
A lane board grows downward, so that assumption sealed every lane below the
|
|
493
|
+
fold behind an invisible wall. */
|
|
494
|
+
.chart-fit { position: relative; flex: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: center; overflow: auto; }
|
|
495
|
+
/* transform:scale() does not change layout size, so the scaled block gets a
|
|
496
|
+
spacer sized to its VISUAL footprint — otherwise the scroll extent is the
|
|
497
|
+
unscaled height and you scroll into empty space. */
|
|
498
|
+
/* overflow:hidden clips the UNSCALED layout box of the absolutely-positioned
|
|
499
|
+
chart, which otherwise adds its full natural height to the scroll extent —
|
|
500
|
+
~900px of empty scroll past the last lane. The visual (scaled) content is
|
|
501
|
+
exactly the spacer's size, so nothing real is clipped. */
|
|
502
|
+
.chart-scaled { position: relative; flex: 0 0 auto; overflow: hidden; }
|
|
503
|
+
.chart-scaled > .chart { position: absolute; top: 0; left: 50%; transform-origin: top center; }
|
|
504
|
+
.chart-fit.zoomed .chart-scaled > .chart { left: 0; transform-origin: top left; }
|
|
315
505
|
/* user-zoomed: native scroll is the pan; top-left origin keeps the scaled
|
|
316
506
|
overflow fully reachable (top/center origin would clip the left half). */
|
|
317
507
|
.chart-fit.zoomed { overflow: auto; justify-content: flex-start; }
|
|
@@ -329,35 +519,252 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
329
519
|
.zoomer button:hover { background: var(--accent-dim); color: var(--ink); }
|
|
330
520
|
.zoomer button.on { color: var(--accent); }
|
|
331
521
|
.zoomer-pct { min-width: 40px; text-align: center; color: var(--ink-3); }
|
|
522
|
+
.zoomer-sep { width: 1px; height: 16px; background: var(--line); margin: 0 2px; }
|
|
523
|
+
|
|
524
|
+
/* ── the ledger shelf: every bucket as a pigeonhole, hover a role to see its
|
|
525
|
+
reach. Docked under the chart, always visible — the memory half of the
|
|
526
|
+
two pillars (structure above, memory below), never a separate tab. */
|
|
527
|
+
.ledger-shelf {
|
|
528
|
+
position: relative; z-index: 2; flex: 0 0 auto; margin-top: 14px;
|
|
529
|
+
padding: 10px 16px 12px; border-radius: 10px;
|
|
530
|
+
border: 1px solid var(--line); background: var(--bg-1); box-shadow: var(--sh-2);
|
|
531
|
+
width: min(1180px, 92vw);
|
|
532
|
+
}
|
|
533
|
+
.shelf-label {
|
|
534
|
+
display: block; font-family: var(--mono); font-size: 9px; letter-spacing: 0.14em;
|
|
535
|
+
text-transform: uppercase; color: var(--brass); margin-bottom: 8px;
|
|
536
|
+
}
|
|
537
|
+
.shelf-slots { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
538
|
+
.shelf-slot {
|
|
539
|
+
font-family: var(--mono); font-size: 10px; color: var(--ink-3);
|
|
540
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 5px;
|
|
541
|
+
padding: 4px 9px; transition: border-color .18s ease, color .18s ease, box-shadow .25s ease, transform .18s ease;
|
|
542
|
+
}
|
|
543
|
+
.shelf-slot.lit {
|
|
544
|
+
color: var(--brass-hi); border-color: var(--brass); transform: translateY(-1px);
|
|
545
|
+
box-shadow: 0 0 0 1px rgba(201, 160, 74, 0.3), 0 6px 16px -6px rgba(201, 160, 74, 0.5);
|
|
546
|
+
background: rgba(201, 160, 74, 0.1);
|
|
547
|
+
}
|
|
548
|
+
.shelf-slot.sealed { color: var(--ink-3); font-style: italic; border-style: dashed; opacity: 0.75; }
|
|
549
|
+
.shelf-slot.sealed.lit { color: var(--ink-3); border-color: var(--line); background: var(--bg-2); box-shadow: none; transform: none; }
|
|
550
|
+
@media (prefers-reduced-motion: reduce) { .shelf-slot { transition: none; } }
|
|
551
|
+
|
|
552
|
+
/* the law toggle in the top bar */
|
|
553
|
+
.law-toggle { font-family: var(--mono); letter-spacing: 0.02em; }
|
|
554
|
+
.law-toggle.on { color: var(--brass-hi); border-color: var(--brass); background: rgba(201, 160, 74, 0.12); }
|
|
332
555
|
/* min-width:max-content keeps connectors geometrically correct at natural size
|
|
333
556
|
BEFORE scaling; the measured --fit shrinks the whole block toward its head. */
|
|
557
|
+
/* transform is set inline by Panel.tsx (see the note there) — only origin,
|
|
558
|
+
sizing and the easing live here. */
|
|
334
559
|
.chart {
|
|
335
560
|
min-width: max-content; padding: 8px 14px 20px;
|
|
336
|
-
transform:
|
|
561
|
+
transform-origin: top center; position: relative; /* the rail plane's origin */
|
|
337
562
|
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); will-change: transform;
|
|
338
563
|
}
|
|
564
|
+
/* plates sit above the rail plane */
|
|
565
|
+
.chart .cascade { position: relative; z-index: 1; }
|
|
566
|
+
|
|
567
|
+
/* rank I — the law. Brass-edged like the GM but unfilled: it is a document,
|
|
568
|
+
not an operator. */
|
|
569
|
+
.law-plate { width: 232px; cursor: pointer; }
|
|
570
|
+
.law-plate .ag-ava { font-family: var(--display); color: var(--accent); }
|
|
571
|
+
.law-plate .ag-name { color: var(--accent); font-size: 13px; }
|
|
572
|
+
.law-plate .ag-role { word-break: break-all; }
|
|
573
|
+
/* ── THE CASCADE (master-key structure) ───────────────────────────────────
|
|
574
|
+
Rank runs LEFT TO RIGHT, one column each, exactly as a master-key chart
|
|
575
|
+
reads: THE STANDARD → GM → departments → staff. Two nested grids give the
|
|
576
|
+
four columns; the brass elbows between them are drawn by CascadeRails as
|
|
577
|
+
one measured SVG (orthogonal geometry between arbitrary boxes is not a
|
|
578
|
+
thing CSS pseudo-elements can do honestly). */
|
|
579
|
+
.cascade {
|
|
580
|
+
display: grid;
|
|
581
|
+
grid-template-columns: max-content var(--rail-w, 56px) 1fr;
|
|
582
|
+
/* START, not centre. Centring is right for a 3-branch key chart; with nine
|
|
583
|
+
departments the stack is ~2,200px tall, so it buried the master key and the
|
|
584
|
+
GM below the fold — the two plates that must be seen first. Top-aligned
|
|
585
|
+
also matches the ministry chart's reading order. */
|
|
586
|
+
align-items: start;
|
|
587
|
+
}
|
|
588
|
+
.cascade > .law-plate { grid-column: 1; }
|
|
589
|
+
.cascade > .ocn { grid-column: 3; }
|
|
590
|
+
|
|
591
|
+
/* rank II–IV live inside the root .ocn, which is itself a cascade */
|
|
592
|
+
.cascade > .ocn {
|
|
593
|
+
display: grid;
|
|
594
|
+
/* the lanes column is BOUNDED so staff wrap inside their lane. Left at 1fr
|
|
595
|
+
under .chart's max-content sizing it grew to fit every staff plate on one
|
|
596
|
+
row — 2,400px natural width, which fit-to-width then scaled to 52% and made
|
|
597
|
+
every plate unreadable. Rank IV wraps; the board stays legible. */
|
|
598
|
+
grid-template-columns: max-content var(--rail-w, 56px) minmax(420px, 940px);
|
|
599
|
+
grid-template-areas: "gm rail lanes";
|
|
600
|
+
align-items: start;
|
|
601
|
+
}
|
|
602
|
+
.cascade > .ocn > .casc-head { grid-area: gm; display: flex; flex-direction: column; align-items: stretch; }
|
|
603
|
+
.cascade > .ocn > .oc-down { grid-area: rail; }
|
|
604
|
+
.cascade > .ocn > .oc-row.lanes { grid-area: lanes; }
|
|
605
|
+
|
|
606
|
+
/* the SVG rail plane — sits under the plates, never intercepts a click */
|
|
607
|
+
.rails { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: visible; width: 100%; height: 100%; }
|
|
608
|
+
|
|
339
609
|
.ocn { display: flex; flex-direction: column; align-items: center; }
|
|
340
|
-
|
|
341
|
-
|
|
610
|
+
/* the connective tissue reads brass everywhere — a company chart drawn with
|
|
611
|
+
the house's own material, not a generic diagram line. */
|
|
612
|
+
.oc-down { width: 2px; height: 18px; position: relative; background: var(--brass-line); opacity: 0.75; }
|
|
342
613
|
.oc-down::after { content: ""; position: absolute; bottom: -2px; left: 50%;
|
|
343
614
|
width: 5px; height: 5px; transform: translateX(-50%); border-radius: 50%;
|
|
344
|
-
background: var(--
|
|
615
|
+
background: var(--brass-hi); box-shadow: 0 0 8px rgba(201, 160, 74, 0.55); }
|
|
345
616
|
.oc-row { display: flex; align-items: flex-start; }
|
|
346
|
-
.oc-child { position: relative; padding: 18px 5px 0; display: flex; flex-direction: column; align-items: center; }
|
|
347
|
-
.oc-child::before { content: ""; position: absolute; top: 0; left: 50%; width: 1.5px; height: 18px;
|
|
348
|
-
background: linear-gradient(180deg, var(--line), hsl(var(--h, 165) 32% 34%)); }
|
|
349
|
-
.oc-child::after { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 16px;
|
|
350
|
-
border-top: 1.5px solid var(--line); }
|
|
351
|
-
.oc-child:first-child::after { left: 50%; border-left: 1.5px solid var(--line); border-top-left-radius: 14px; }
|
|
352
|
-
.oc-child:last-child::after { right: 50%; border-right: 1.5px solid var(--line); border-top-right-radius: 14px; }
|
|
353
|
-
.oc-child:first-child::before, .oc-child:last-child::before { display: none; }
|
|
354
|
-
.oc-child:only-child::after { display: none; }
|
|
355
|
-
.oc-child:only-child::before { display: block; }
|
|
356
617
|
|
|
357
|
-
/*
|
|
358
|
-
|
|
359
|
-
.
|
|
360
|
-
|
|
618
|
+
/* ── THE STAFF BOARD: departments as lanes, stacking DOWNWARD ──────────────
|
|
619
|
+
Nine departments fanned sideways forced a canvas nobody could read at 17%
|
|
620
|
+
zoom. Each lane is a row of the board: a brass rail down the left, the
|
|
621
|
+
head's card, then its people flowing right and wrapping inside the lane.
|
|
622
|
+
The board scrolls like a document; every card stays at legible size. */
|
|
623
|
+
/* the lanes are rank III+IV: one shelf per department, stacked. Width now comes
|
|
624
|
+
from the cascade's 1fr column, not a hardcoded viewport clamp. */
|
|
625
|
+
.oc-row.lanes {
|
|
626
|
+
flex-direction: column;
|
|
627
|
+
align-items: stretch;
|
|
628
|
+
gap: 14px;
|
|
629
|
+
padding-top: 0;
|
|
630
|
+
width: 100%;
|
|
631
|
+
}
|
|
632
|
+
/* a lane is a SHELF on the rack — a hairline tray, not a coloured panel. The
|
|
633
|
+
old lane had a hash-hue left border and tint per department; the rack is one
|
|
634
|
+
material throughout and the department is named, not coloured. */
|
|
635
|
+
.oc-row.lanes > .oc-child {
|
|
636
|
+
flex: 1 1 auto;
|
|
637
|
+
position: relative;
|
|
638
|
+
border: 1px solid var(--line);
|
|
639
|
+
border-radius: 18px;
|
|
640
|
+
background: var(--surface-tray);
|
|
641
|
+
padding: 16px 18px 18px;
|
|
642
|
+
transition: border-color .25s ease, box-shadow .35s var(--ease);
|
|
643
|
+
}
|
|
644
|
+
.oc-row.lanes > .oc-child:hover { border-color: var(--line-2, #d9d3c9); box-shadow: var(--sh-2); }
|
|
645
|
+
@media (prefers-reduced-motion: reduce) {
|
|
646
|
+
.oc-row.lanes > .oc-child:hover { transform: none; }
|
|
647
|
+
}
|
|
648
|
+
/* lanes own their connectors — kill the fan's drop-lines inside them */
|
|
649
|
+
.oc-row.lanes > .oc-child::before,
|
|
650
|
+
.oc-row.lanes > .oc-child::after { display: none; }
|
|
651
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
|
|
652
|
+
|
|
653
|
+
/* inside a lane: head card top-left, its service tray beneath the head, a
|
|
654
|
+
brass rail column, its people filling the right. Explicit areas — an
|
|
655
|
+
implicit 2-column grid put the tray in the staff slot the moment every
|
|
656
|
+
department gained services, which shunted the people into a one-card
|
|
657
|
+
column. The rail column is the elbow HANDOFF.md flagged as hidden: the
|
|
658
|
+
department head commanding its people, drawn once, orthogonally. */
|
|
659
|
+
.oc-row.lanes > .oc-child > .ocn {
|
|
660
|
+
display: grid;
|
|
661
|
+
grid-template-columns: minmax(278px, 300px) var(--rail-w, 56px) 1fr;
|
|
662
|
+
grid-template-areas:
|
|
663
|
+
"head rail staff"
|
|
664
|
+
"macs rail staff";
|
|
665
|
+
grid-template-rows: auto 1fr;
|
|
666
|
+
align-items: start;
|
|
667
|
+
gap: 10px 0;
|
|
668
|
+
}
|
|
669
|
+
.oc-row.lanes > .oc-child > .ocn > .ag { grid-area: head; }
|
|
670
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray { grid-area: macs; }
|
|
671
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row { grid-area: staff; margin-left: 4px; }
|
|
672
|
+
|
|
673
|
+
/* the rail: a vertical brass spine for the department, capped where it meets
|
|
674
|
+
the head, with one orthogonal branch reaching into the staff block. This
|
|
675
|
+
is the ".oc-down" element — hidden below (line ~490 in the old cascade),
|
|
676
|
+
here it's re-purposed as the rail instead of the plain tree drop-line. */
|
|
677
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-down {
|
|
678
|
+
display: block; grid-area: rail; position: relative; width: 100%; height: 100%;
|
|
679
|
+
min-height: 44px;
|
|
680
|
+
/* the base .oc-down is a 2px-wide FILLED brass drop-line. Widening it to the
|
|
681
|
+
rail column without clearing its background painted a solid brass slab down
|
|
682
|
+
every lane — the rail is drawn by ::before/::after, not by the box itself. */
|
|
683
|
+
background: none;
|
|
684
|
+
}
|
|
685
|
+
/* No ::before/::after brass here any more — the SVG rail plane draws every
|
|
686
|
+
connector from measured box positions. Leaving these would double each rail. */
|
|
687
|
+
|
|
688
|
+
/* semantic zoom lever: the rail itself is the fold/unfold control at the
|
|
689
|
+
department level — house → department → role, one click at a time. */
|
|
690
|
+
.oc-down.foldable { cursor: pointer; }
|
|
691
|
+
.oc-down.foldable:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; border-radius: 4px; }
|
|
692
|
+
.oc-fold-glyph {
|
|
693
|
+
position: absolute; left: 50%; top: 22px; transform: translate(-50%, -50%);
|
|
694
|
+
font-style: normal; font-size: 10px; line-height: 1; width: 15px; height: 15px;
|
|
695
|
+
display: grid; place-items: center; border-radius: 50%;
|
|
696
|
+
background: var(--bg-1); border: 1px solid var(--line-2); color: var(--accent);
|
|
697
|
+
transition: transform .15s ease, background .15s ease;
|
|
698
|
+
}
|
|
699
|
+
.oc-down.foldable:hover .oc-fold-glyph { background: rgba(201, 160, 74, 0.18); transform: translate(-50%, -50%) scale(1.15); }
|
|
700
|
+
.oc-down.folded .oc-fold-glyph { color: var(--ink-2); border-color: var(--line); }
|
|
701
|
+
|
|
702
|
+
/* the folded department: head visible, staff summarised to one line — the
|
|
703
|
+
"department" rung of house → department → role. */
|
|
704
|
+
.oc-folded-summary {
|
|
705
|
+
grid-area: staff; align-self: center; justify-self: start; text-align: left;
|
|
706
|
+
background: none; border: 1px dashed var(--line); border-radius: 8px;
|
|
707
|
+
padding: 9px 13px; margin-left: 4px; cursor: pointer;
|
|
708
|
+
font-family: var(--mono); font-size: 11px; color: var(--ink-3);
|
|
709
|
+
transition: border-color .15s ease, color .15s ease;
|
|
710
|
+
}
|
|
711
|
+
.oc-folded-summary:hover { border-color: var(--brass); color: var(--brass-hi); }
|
|
712
|
+
/* "show the law": the SAME rails, re-read as authority. The SVG plane swaps to
|
|
713
|
+
a travelling dashed brass so the law is seen flowing down the cascade. */
|
|
714
|
+
.rail-line { stroke: var(--line-2, #d9d3c9); stroke-width: 1.5; stroke-linecap: round; fill: none; }
|
|
715
|
+
.rail-dash { stroke: var(--accent-2); stroke-width: 1; fill: none; stroke-dasharray: 2 9; opacity: 0; }
|
|
716
|
+
.law-on .rail-line { stroke: var(--brass-hi); stroke-width: 1.9; opacity: .9; }
|
|
717
|
+
.law-on .rail-dash { opacity: .95; animation: rail-flow 700ms linear infinite; }
|
|
718
|
+
@keyframes rail-flow { to { stroke-dashoffset: -11; } }
|
|
719
|
+
@media (prefers-reduced-motion: reduce) { .law-on .rail-dash { animation: none; } }
|
|
720
|
+
|
|
721
|
+
/* the tray is the department's stack of real services — one per line, so the
|
|
722
|
+
vendor marks read as a list of systems rather than a scatter of chips */
|
|
723
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray {
|
|
724
|
+
display: flex; flex-direction: column; align-items: stretch;
|
|
725
|
+
gap: 4px; margin: 0; padding: 0; max-width: 232px;
|
|
726
|
+
}
|
|
727
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac { justify-content: flex-start; width: 100%; }
|
|
728
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac .mac-name { max-width: none; }
|
|
729
|
+
|
|
730
|
+
@media (max-width: 820px) {
|
|
731
|
+
.oc-row.lanes > .oc-child > .ocn {
|
|
732
|
+
grid-template-columns: 1fr;
|
|
733
|
+
grid-template-areas: "head" "macs" "staff";
|
|
734
|
+
}
|
|
735
|
+
/* no rail column on a single-column stack — the rail has nothing to span */
|
|
736
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
|
|
737
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray { max-width: none; flex-direction: row; flex-wrap: wrap; }
|
|
738
|
+
}
|
|
739
|
+
/* inside a lane the people always flow right and wrap — the ".wrap" grid
|
|
740
|
+
(which grows downward) would otherwise stack a big department into a
|
|
741
|
+
one-card-wide column. Specificity here deliberately beats .oc-row.wrap. */
|
|
742
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row,
|
|
743
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row.wrap {
|
|
744
|
+
display: flex;
|
|
745
|
+
flex-direction: row;
|
|
746
|
+
flex-wrap: wrap;
|
|
747
|
+
justify-content: flex-start;
|
|
748
|
+
gap: 8px;
|
|
749
|
+
margin: 0;
|
|
750
|
+
padding: 0;
|
|
751
|
+
max-width: none;
|
|
752
|
+
border: 0;
|
|
753
|
+
border-radius: 0;
|
|
754
|
+
background: none;
|
|
755
|
+
align-content: flex-start;
|
|
756
|
+
}
|
|
757
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child { padding: 0; flex: 0 0 auto; }
|
|
758
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::before,
|
|
759
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::after { display: none; }
|
|
760
|
+
|
|
761
|
+
@media (max-width: 820px) {
|
|
762
|
+
.oc-row.lanes > .oc-child > .ocn { grid-template-columns: 1fr; gap: 12px; }
|
|
763
|
+
}
|
|
764
|
+
/* Connectors are drawn by the SVG rail plane now, not by pseudo-elements. The
|
|
765
|
+
old fan drop-lines/elbows here would double every rail. */
|
|
766
|
+
.oc-child { position: relative; padding: 0; display: flex; flex-direction: column; align-items: center; }
|
|
767
|
+
.oc-child::before, .oc-child::after { display: none; }
|
|
361
768
|
|
|
362
769
|
/* wide sibling sets — grouped subtrees that wrap to natural width (never crush a
|
|
363
770
|
branch into a fixed cell — that overflowed and collided with the next branch) */
|
|
@@ -370,78 +777,171 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
370
777
|
.oc-row.wrap > .oc-child { padding: 8px 4px 0; flex: 0 0 auto; }
|
|
371
778
|
.oc-row.wrap > .oc-child::before, .oc-row.wrap > .oc-child::after { display: none; }
|
|
372
779
|
|
|
373
|
-
/*
|
|
374
|
-
|
|
780
|
+
/* ── THE PLATE ────────────────────────────────────────────────────────────
|
|
781
|
+
An engraved plate on a key rack. Left edge is milled brass; rank reads
|
|
782
|
+
through FINISH and SIZE (polished → satin → dulled), never hue. Radius 4px
|
|
783
|
+
per CRAFT §0 (radii 2/3/6) — the old 15px rounded rect is what made this
|
|
784
|
+
read as a SaaS card. Content is left-aligned like a name plate, not
|
|
785
|
+
centred like a business card. */
|
|
786
|
+
/* ══ THE SOURCE CARD (variant B — "the spine") ══════════════════════════
|
|
787
|
+
This board is not a picture of the org: it IS the org file booboo_boot
|
|
788
|
+
reads. Rules inherit down it, bucket reach derives from it, every agent
|
|
789
|
+
boots against it. So the card states its CONSEQUENCE, not just its
|
|
790
|
+
attributes — what it declares, who it binds, what it boots on.
|
|
791
|
+
Health is the card's own left edge, so scanning nine stacked departments
|
|
792
|
+
you find the amber one peripherally, before reading a single word. */
|
|
375
793
|
.ag {
|
|
376
|
-
position: relative; display: flex; flex-direction: column; align-items:
|
|
377
|
-
width: calc(
|
|
378
|
-
padding: 13px
|
|
379
|
-
background:
|
|
380
|
-
border: 1px solid
|
|
381
|
-
|
|
382
|
-
box-shadow
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
794
|
+
position: relative; display: flex; flex-direction: column; align-items: stretch;
|
|
795
|
+
width: calc(300px - min(var(--d, 0), 2) * 22px); /* 300 / 278 / 256 by rank */
|
|
796
|
+
padding: 15px 18px 13px 21px; text-align: left;
|
|
797
|
+
background: var(--bg-1);
|
|
798
|
+
border: 1px solid var(--line); border-radius: 14px; cursor: grab; overflow: hidden;
|
|
799
|
+
box-shadow: var(--sh-2);
|
|
800
|
+
transition: box-shadow .22s var(--ease), transform .22s var(--ease), border-color .18s ease, opacity .18s ease;
|
|
801
|
+
animation: settle .45s var(--settle) both;
|
|
802
|
+
}
|
|
803
|
+
/* the spine — health as the card's own edge */
|
|
804
|
+
.ag::before {
|
|
805
|
+
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
|
|
806
|
+
background: var(--ink-3); transition: background .2s ease;
|
|
389
807
|
}
|
|
390
|
-
.ag::before {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
.ag.
|
|
397
|
-
|
|
808
|
+
.ag.h-ok::before { background: var(--ok); }
|
|
809
|
+
.ag.h-warn::before { background: var(--warn); }
|
|
810
|
+
.ag.h-fail::before { background: var(--fail); }
|
|
811
|
+
/* an unhealthy card also carries a wash, so the state survives a greyscale
|
|
812
|
+
print and never rests on hue alone */
|
|
813
|
+
.ag.h-warn { background: linear-gradient(180deg, var(--warn-wash), var(--bg-1) 52%); border-color: #ecd9ae; }
|
|
814
|
+
.ag.h-fail { background: linear-gradient(180deg, var(--fail-wash), var(--bg-1) 52%); border-color: #edc9bf; }
|
|
815
|
+
@keyframes settle { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
|
|
816
|
+
.ag:hover { transform: translateY(-2px); box-shadow: var(--sh-3); border-color: var(--line-2); }
|
|
817
|
+
.ag:active { transform: translateY(0) scale(.995); transition-duration: .08s; }
|
|
818
|
+
.ag.sel { border-color: var(--accent); box-shadow: var(--sh-3), 0 0 0 3px var(--accent-dim); }
|
|
398
819
|
.ag:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
399
|
-
.ag.over { border-color: var(--accent-2); box-shadow: 0 0 0
|
|
400
|
-
.ag.dragging { opacity: 0.
|
|
820
|
+
.ag.over { border-color: var(--accent-2); box-shadow: var(--sh-3), 0 0 0 4px var(--accent-dim); transform: scale(1.02); }
|
|
821
|
+
.ag.dragging { opacity: 0.4; cursor: grabbing; }
|
|
822
|
+
|
|
823
|
+
/* WHERE IT BELONGS — reads first, so a card lifted out of its lane still
|
|
824
|
+
knows its own place. */
|
|
825
|
+
.ag-eyebrow {
|
|
826
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
|
|
827
|
+
color: var(--accent); display: block; margin-bottom: 7px;
|
|
828
|
+
}
|
|
829
|
+
/* WHAT FLOWS OUT — the consequence line. Plain English, never glyphs. */
|
|
830
|
+
.ag-flows {
|
|
831
|
+
display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 10px;
|
|
832
|
+
font-variant-numeric: tabular-nums;
|
|
833
|
+
}
|
|
401
834
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
835
|
+
/* ── THE HEALTH LIGHT ──────────────────────────────────────────────────────
|
|
836
|
+
A real lamp: a lit core inside a soft halo, with a slow breath so a healthy
|
|
837
|
+
board is visibly ALIVE rather than merely static. The halo is a separate
|
|
838
|
+
::after ring that expands and fades — one pulse every 2.6s, offset per
|
|
839
|
+
bubble so nine departments never blink in unison (a synchronised board
|
|
840
|
+
reads as a screensaver; a staggered one reads as a building). */
|
|
841
|
+
.ag-light {
|
|
842
|
+
position: absolute; top: 13px; right: 14px; width: 8px; height: 8px; border-radius: 50%;
|
|
843
|
+
background: var(--ink-3);
|
|
844
|
+
}
|
|
845
|
+
.ag-light::after {
|
|
846
|
+
content: ""; position: absolute; inset: -3px; border-radius: 50%;
|
|
847
|
+
border: 1px solid currentColor; opacity: 0;
|
|
848
|
+
animation: lamp 2.6s var(--ease) infinite;
|
|
849
|
+
animation-delay: var(--lamp-delay, 0s);
|
|
850
|
+
}
|
|
851
|
+
.ag-light.ok { background: var(--ok); color: var(--ok); box-shadow: 0 0 0 3px var(--ok-wash); }
|
|
852
|
+
.ag-light.warn { background: var(--warn); color: var(--warn); box-shadow: 0 0 0 3px var(--warn-wash); }
|
|
853
|
+
.ag-light.fail { background: var(--fail); color: var(--fail); box-shadow: 0 0 0 3px var(--fail-wash); }
|
|
854
|
+
/* a failing lamp beats faster — urgency in the rhythm, not just the hue */
|
|
855
|
+
.ag-light.fail::after { animation-duration: 1.2s; }
|
|
856
|
+
@keyframes lamp {
|
|
857
|
+
0% { transform: scale(.7); opacity: .85; }
|
|
858
|
+
70%, 100% { transform: scale(1.9); opacity: 0; }
|
|
859
|
+
}
|
|
860
|
+
@media (prefers-reduced-motion: reduce) { .ag-light::after { animation: none; } }
|
|
406
861
|
|
|
407
|
-
.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
.ag.root .ag-name { font-size:
|
|
412
|
-
.ag.root::after {
|
|
413
|
-
content: ""; position: absolute; inset: 0; border-radius: 15px; pointer-events: none;
|
|
414
|
-
box-shadow: inset 0 0 26px rgba(79, 184, 160, 0.08); animation: ring 3.2s ease-in-out infinite;
|
|
415
|
-
}
|
|
416
|
-
@keyframes ring { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
|
|
862
|
+
/* rank I·II — the GM. Polished brass: the only plate that is lit rather than lit-upon. */
|
|
863
|
+
.ag.root { cursor: default; width: 268px; padding: 15px 17px;
|
|
864
|
+
background: var(--surface-lift);
|
|
865
|
+
border-color: var(--surface-lift-edge); box-shadow: var(--sh-3); }
|
|
866
|
+
.ag.root .ag-name { font-size: 14.5px; }
|
|
417
867
|
|
|
868
|
+
/* plate header: the mark sits BESIDE the name, not above it — a name plate,
|
|
869
|
+
not a business card. Staff plates carry no mark at all (see Panel.tsx): a
|
|
870
|
+
brass plaque does not have a cartoon on it, and 51 identical 🤖 was the
|
|
871
|
+
cheapest token on the board. */
|
|
872
|
+
.ag-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
|
418
873
|
.ag-ava {
|
|
419
|
-
width:
|
|
420
|
-
border-radius:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
.ag-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
874
|
+
width: 26px; height: 26px; flex: 0 0 26px; display: grid; place-items: center; font-size: 14px;
|
|
875
|
+
border-radius: 9px; background: var(--accent-dim); border: 1px solid #eadcc3;
|
|
876
|
+
}
|
|
877
|
+
.ag.root .ag-ava { width: 30px; height: 30px; flex-basis: 30px; font-size: 16px; border-color: rgba(239, 195, 137, 0.45); }
|
|
878
|
+
|
|
879
|
+
/* the name — engraved: a dark groove under, a hairline of light above */
|
|
880
|
+
.ag-name {
|
|
881
|
+
font-family: var(--display); font-size: 18px; font-weight: 600; letter-spacing: -0.015em;
|
|
882
|
+
line-height: 1.18; color: var(--ink);
|
|
883
|
+
white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
884
|
+
}
|
|
885
|
+
/* staff plates: quieter, lighter weight — rank in the type as well as the metal */
|
|
886
|
+
.ag.staff .ag-name { font-family: var(--font); font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); }
|
|
887
|
+
|
|
888
|
+
.ag-role {
|
|
889
|
+
font-size: 12.5px; line-height: 1.5; color: var(--ink-2); margin-top: 6px;
|
|
890
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
891
|
+
}
|
|
892
|
+
.ag-meta { display: flex; align-items: center; gap: 9px; margin-top: 7px; }
|
|
893
|
+
.ag-meta em { font-style: normal; font-family: var(--mono); font-size: 9px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
429
894
|
.ag-kids {
|
|
430
|
-
font-
|
|
431
|
-
padding: 2px
|
|
895
|
+
font-family: var(--mono); font-size: 8.5px; color: var(--brass); border: 1px solid rgba(217, 160, 91, 0.28);
|
|
896
|
+
border-radius: 3px; padding: 2px 7px; font-variant-numeric: tabular-nums; text-transform: uppercase; letter-spacing: 0.1em;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/* ── the plate face: health · bucket reach · rule count · last report ──────
|
|
900
|
+
Replaces a role line that used to just repeat the name ("Lift Engineer" /
|
|
901
|
+
"Lift Engineer — Engineering"). Four facts, left-aligned, mono, no chrome. */
|
|
902
|
+
.ag-facts {
|
|
903
|
+
display: flex; align-items: center; gap: 10px;
|
|
904
|
+
margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--line);
|
|
432
905
|
}
|
|
906
|
+
.ag-fact { font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--ink-3); }
|
|
907
|
+
.ag-fact-health.ok { color: var(--ok); }
|
|
908
|
+
.ag-fact-health.warn { color: var(--warn); }
|
|
909
|
+
.ag-fact-health.fail { color: var(--fail); }
|
|
910
|
+
.ag-fact-health.none { color: var(--ink-3); font-weight: 500; }
|
|
911
|
+
.fact-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--ink-3); flex: 0 0 auto; }
|
|
912
|
+
.ag-fact-health.ok .fact-dot { background: var(--ok); box-shadow: 0 0 6px rgba(139, 233, 176, 0.75); }
|
|
913
|
+
.ag-fact-health.warn .fact-dot { background: var(--brass); box-shadow: 0 0 6px rgba(217, 160, 91, 0.85); }
|
|
914
|
+
.ag-fact-health.fail .fact-dot { background: var(--fail); box-shadow: 0 0 7px rgba(224, 123, 106, 0.85); }
|
|
915
|
+
.ag-fact-health.ok, .ag-fact-health.warn, .ag-fact-health.fail { color: var(--ink-2); }
|
|
916
|
+
.ag-fact-report { margin-left: auto; color: var(--ink-3); font-weight: 400; font-family: var(--mono); font-size: 10px; }
|
|
917
|
+
/* the expected beat, quieter than the fact it qualifies — it is the ruler, not
|
|
918
|
+
the measurement, so it must never out-shout "reported 4d ago". */
|
|
919
|
+
.ag-cadence { font-weight: 400; color: var(--ink-3); opacity: .62; }
|
|
920
|
+
|
|
921
|
+
/* "show the law": the boot-order chain, printed on the plate only while the
|
|
922
|
+
toggle is on — the product's core idea, made literal per plate. */
|
|
923
|
+
.ag-law { display: none; }
|
|
924
|
+
.law-on .ag-law {
|
|
925
|
+
display: block; margin-top: 7px; padding-top: 7px; border-top: 1px dashed rgba(217, 160, 91, 0.32);
|
|
926
|
+
font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.03em; color: var(--brass-hi);
|
|
927
|
+
line-height: 1.6; word-break: break-word;
|
|
928
|
+
}
|
|
929
|
+
.ag-law-arrow { font-style: normal; color: var(--brass); margin: 0 3px; opacity: 0.85; }
|
|
930
|
+
.law-on.ag { box-shadow: 0 0 0 1px rgba(217, 160, 91, 0.3), 0 16px 34px -18px rgba(0, 0, 0, 0.85); }
|
|
433
931
|
|
|
434
932
|
/* the dossier */
|
|
435
933
|
.doss {
|
|
436
934
|
width: 340px; flex: 0 0 340px; border-left: 1px solid var(--line);
|
|
437
|
-
background:
|
|
935
|
+
background: var(--bg-1); backdrop-filter: blur(10px);
|
|
438
936
|
padding: 26px 24px 40px; overflow-y: auto; animation: slidein 0.25s ease both;
|
|
439
937
|
}
|
|
440
938
|
@keyframes slidein { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
|
|
441
|
-
.doss-head { display: flex; gap: 12px; align-items: center; margin-bottom:
|
|
442
|
-
.doss-emoji { font-size: 30px; }
|
|
443
|
-
.doss h2 { margin: 0; font-size: 18px; }
|
|
444
|
-
|
|
939
|
+
.doss-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; }
|
|
940
|
+
.doss-emoji { font-size: 30px; flex: none; }
|
|
941
|
+
.doss h2 { margin: 0; font-size: 18px; min-width: 0; }
|
|
942
|
+
/* full panel width: a body of four seats is a list, not a subtitle squeezed
|
|
943
|
+
beside three buttons. Middots become the wrap points. */
|
|
944
|
+
.doss-role { margin: 0 0 14px; color: var(--ink-2); font-size: 12.5px; line-height: 1.55; }
|
|
445
945
|
|
|
446
946
|
.doss-chain { font-size: 11px; color: var(--ink-3); margin-bottom: 18px; line-height: 1.7; }
|
|
447
947
|
.doss-chain i { font-style: normal; margin: 0 5px; }
|
|
@@ -462,8 +962,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
462
962
|
.doss-rules em { color: var(--ink-3); font-style: normal; font-size: 9.5px; margin-left: 5px; }
|
|
463
963
|
.doss-chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
464
964
|
.doss-reps { display: flex; flex-direction: column; gap: 7px; }
|
|
965
|
+
/* No side-stripe. It was a 2px brass border-left that said nothing the
|
|
966
|
+
status dot doesn't already say properly, and a coloured side-stripe on a
|
|
967
|
+
card is a banned pattern in the house design laws. Full 1px border; the
|
|
968
|
+
whole edge answers on hover. */
|
|
465
969
|
.doss-rep {
|
|
466
|
-
background: var(--bg-1); border: 1px solid var(--line);
|
|
970
|
+
background: var(--bg-1); border: 1px solid var(--line);
|
|
467
971
|
border-radius: 9px; padding: 9px 11px; animation: settle 0.35s ease both;
|
|
468
972
|
}
|
|
469
973
|
.doss-rep-top { display: flex; align-items: baseline; gap: 9px; }
|
|
@@ -471,7 +975,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
471
975
|
.doss-rep-label { font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
472
976
|
.doss-rep-sum { margin: 5px 0 0; font-size: 11.5px; color: var(--ink-2); line-height: 1.55; }
|
|
473
977
|
.doss-rep.tap { cursor: pointer; transition: border-color .12s ease, background .12s ease; }
|
|
474
|
-
.doss-rep.tap:hover { border-
|
|
978
|
+
.doss-rep.tap:hover { border-color: var(--accent); background: var(--accent-dim); }
|
|
475
979
|
.doss-rep.tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
476
980
|
.doss-rep-caret { margin-left: auto; font-size: 10px; color: var(--ink-2); align-self: center; }
|
|
477
981
|
.rep-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; align-self: center; }
|
|
@@ -479,7 +983,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
479
983
|
.rep-ok { color: var(--accent); } .rep-warn { color: var(--warn); } .rep-fail { color: #e07b6a; }
|
|
480
984
|
.doss-rep-detail { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; line-height: 1.7; color: var(--ink-2); font-family: var(--mono); word-break: break-word; }
|
|
481
985
|
.doss-rep-detail .doss-l { display: inline-block; min-width: 74px; opacity: .6; text-transform: uppercase; letter-spacing: .04em; font-size: 9.5px; }
|
|
482
|
-
.chip { font-size: 11px; background: var(--accent-dim); color: var(--accent-2); border-radius: 7px; padding: 4px 10px; }
|
|
986
|
+
.chip { transition: transform .16s var(--ease), background .18s ease; font-size: 11px; background: var(--accent-dim); color: var(--accent-2); border-radius: 7px; padding: 4px 10px; }
|
|
483
987
|
.chip.alt { background: var(--bg-2); color: var(--ink-2); }
|
|
484
988
|
.doss-boot { font-size: 12px; color: var(--ink-2); line-height: 1.6; margin: 0; white-space: pre-wrap; }
|
|
485
989
|
/* contract — one click to open, edit in place, save flows to the org file */
|
|
@@ -554,17 +1058,6 @@ var TABS = [
|
|
|
554
1058
|
{ id: "rules", glyph: "\xA7", label: "rules" },
|
|
555
1059
|
{ id: "graph", glyph: "\u25C9", label: "graph" }
|
|
556
1060
|
];
|
|
557
|
-
function relTime(iso) {
|
|
558
|
-
if (typeof iso !== "string") return "";
|
|
559
|
-
const t = new Date(iso).getTime();
|
|
560
|
-
if (!t) return "";
|
|
561
|
-
const s = (Date.now() - t) / 1e3;
|
|
562
|
-
if (s < 60) return "just now";
|
|
563
|
-
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
564
|
-
if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
|
|
565
|
-
if (s < 172800) return "yesterday";
|
|
566
|
-
return `${Math.floor(s / 86400)}d ago`;
|
|
567
|
-
}
|
|
568
1061
|
function nodeAt(n) {
|
|
569
1062
|
const d = n.data ?? {};
|
|
570
1063
|
for (const k of ["at", "ts", "time", "created_at", "date", "bst", "ts_bst", "finished_at"]) {
|
|
@@ -581,13 +1074,14 @@ function timeMs(at) {
|
|
|
581
1074
|
function buildHealthMap(nodes) {
|
|
582
1075
|
const m = /* @__PURE__ */ new Map();
|
|
583
1076
|
for (const r of nodes) {
|
|
584
|
-
|
|
1077
|
+
const who = reportAgentId(r);
|
|
1078
|
+
if (!who) continue;
|
|
585
1079
|
const d = r.data ?? {};
|
|
586
1080
|
if (typeof d.status !== "string") continue;
|
|
587
1081
|
const status = d.status;
|
|
588
1082
|
const at = nodeAt(r);
|
|
589
1083
|
const atMs = timeMs(at);
|
|
590
|
-
const cur = m.get(
|
|
1084
|
+
const cur = m.get(who) ?? { lastAt: "", lastMs: -Infinity, lastStatus: "", n: 0, fails: 0 };
|
|
591
1085
|
cur.n++;
|
|
592
1086
|
if (status === "fail") cur.fails++;
|
|
593
1087
|
if (Number.isFinite(atMs) && atMs >= cur.lastMs) {
|
|
@@ -595,7 +1089,7 @@ function buildHealthMap(nodes) {
|
|
|
595
1089
|
cur.lastAt = at;
|
|
596
1090
|
cur.lastStatus = status;
|
|
597
1091
|
}
|
|
598
|
-
m.set(
|
|
1092
|
+
m.set(who, cur);
|
|
599
1093
|
}
|
|
600
1094
|
return m;
|
|
601
1095
|
}
|
|
@@ -609,6 +1103,15 @@ function pulseFor(a, health) {
|
|
|
609
1103
|
}
|
|
610
1104
|
return best;
|
|
611
1105
|
}
|
|
1106
|
+
function everyN(h) {
|
|
1107
|
+
if (h <= 1) return "hourly";
|
|
1108
|
+
if (h < 24) return `every ${h}h`;
|
|
1109
|
+
if (h === 24) return "daily";
|
|
1110
|
+
if (h === 168) return "weekly";
|
|
1111
|
+
if (h >= 672 && h <= 744) return "monthly";
|
|
1112
|
+
if (h % 24 === 0) return `every ${h / 24} days`;
|
|
1113
|
+
return `every ${h}h`;
|
|
1114
|
+
}
|
|
612
1115
|
function lightFor(a, health) {
|
|
613
1116
|
const p = pulseFor(a, health);
|
|
614
1117
|
if (!p || !p.lastAt) return "none";
|
|
@@ -631,6 +1134,10 @@ function nodeSummary(n) {
|
|
|
631
1134
|
}
|
|
632
1135
|
return "";
|
|
633
1136
|
}
|
|
1137
|
+
function reportAgentId(n) {
|
|
1138
|
+
const d = n.data ?? {};
|
|
1139
|
+
return n.cluster || (typeof d.agent === "string" ? d.agent : "") || "";
|
|
1140
|
+
}
|
|
634
1141
|
async function fetchReports(api, cluster, limit = 500) {
|
|
635
1142
|
const q = async (t) => {
|
|
636
1143
|
try {
|
|
@@ -656,23 +1163,31 @@ function slugify(s) {
|
|
|
656
1163
|
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
657
1164
|
}
|
|
658
1165
|
var REDUCED = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
1166
|
+
var THEME_KEY = "booboo-theme-v2";
|
|
659
1167
|
function readTheme() {
|
|
660
1168
|
try {
|
|
661
|
-
|
|
1169
|
+
if (new URLSearchParams(window.location.search).get("embed")) return "light";
|
|
1170
|
+
return localStorage.getItem(THEME_KEY) === "dark" ? "dark" : "light";
|
|
662
1171
|
} catch {
|
|
663
|
-
return "
|
|
1172
|
+
return "light";
|
|
664
1173
|
}
|
|
665
1174
|
}
|
|
666
|
-
function useTheme() {
|
|
1175
|
+
function useTheme(pinned) {
|
|
667
1176
|
const [theme, setTheme] = useState(readTheme);
|
|
1177
|
+
const chosen = useRef(false);
|
|
668
1178
|
useEffect(() => {
|
|
669
|
-
|
|
1179
|
+
if (pinned) return;
|
|
1180
|
+
if (!chosen.current) return;
|
|
670
1181
|
try {
|
|
671
|
-
localStorage.setItem(
|
|
1182
|
+
localStorage.setItem(THEME_KEY, theme);
|
|
672
1183
|
} catch {
|
|
673
1184
|
}
|
|
674
|
-
}, [theme]);
|
|
675
|
-
|
|
1185
|
+
}, [theme, pinned]);
|
|
1186
|
+
if (pinned) return [pinned, null];
|
|
1187
|
+
return [theme, () => {
|
|
1188
|
+
chosen.current = true;
|
|
1189
|
+
setTheme((t) => t === "dark" ? "light" : "dark");
|
|
1190
|
+
}];
|
|
676
1191
|
}
|
|
677
1192
|
function useCountUp(target, ms = 900) {
|
|
678
1193
|
const [v, setV] = useState(REDUCED ? target : 0);
|
|
@@ -720,8 +1235,216 @@ function bucketHue(name) {
|
|
|
720
1235
|
for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;
|
|
721
1236
|
return h;
|
|
722
1237
|
}
|
|
1238
|
+
var SEALED_BUCKET = "guest-registry";
|
|
1239
|
+
function LedgerShelf({ org, lit }) {
|
|
1240
|
+
const buckets = useMemo(() => {
|
|
1241
|
+
const s = /* @__PURE__ */ new Set();
|
|
1242
|
+
for (const a of org.agents) for (const b of a.buckets ?? []) s.add(b);
|
|
1243
|
+
return [...s].sort();
|
|
1244
|
+
}, [org]);
|
|
1245
|
+
return /* @__PURE__ */ jsxs("div", { className: "ledger-shelf", role: "list", "aria-label": "the ledger \u2014 hover a role to see what it reaches", children: [
|
|
1246
|
+
/* @__PURE__ */ jsx("span", { className: "shelf-label", children: "the ledger" }),
|
|
1247
|
+
/* @__PURE__ */ jsxs("div", { className: "shelf-slots", children: [
|
|
1248
|
+
buckets.map((b) => /* @__PURE__ */ jsx("span", { className: `shelf-slot${lit.has(b) ? " lit" : ""}`, role: "listitem", children: b }, b)),
|
|
1249
|
+
/* @__PURE__ */ jsxs("span", { className: "shelf-slot sealed", role: "listitem", title: "ledger:guest-registry \u2014 sealed by wall. Visible, never emitted.", children: [
|
|
1250
|
+
"\u{1F512} ",
|
|
1251
|
+
SEALED_BUCKET
|
|
1252
|
+
] })
|
|
1253
|
+
] })
|
|
1254
|
+
] });
|
|
1255
|
+
}
|
|
1256
|
+
function Constellation() {
|
|
1257
|
+
const svg = useMemo(() => {
|
|
1258
|
+
const W = 1600, H = 1e3, N = 58;
|
|
1259
|
+
let s = 20260719;
|
|
1260
|
+
const rnd = () => (s = s * 1103515245 + 12345 & 2147483647) / 2147483647;
|
|
1261
|
+
const pts = [];
|
|
1262
|
+
for (let i = 0; i < N; i++) pts.push([rnd() * W, rnd() * H]);
|
|
1263
|
+
let d = "";
|
|
1264
|
+
for (let i = 0; i < N; i++) {
|
|
1265
|
+
for (let j = i + 1; j < N; j++) {
|
|
1266
|
+
const dist = Math.hypot(pts[i][0] - pts[j][0], pts[i][1] - pts[j][1]);
|
|
1267
|
+
if (dist < 200) d += `<line x1="${pts[i][0].toFixed(1)}" y1="${pts[i][1].toFixed(1)}" x2="${pts[j][0].toFixed(1)}" y2="${pts[j][1].toFixed(1)}" stroke="rgba(217,160,91,${((1 - dist / 200) * 0.14).toFixed(3)})" stroke-width="1"/>`;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
for (const [x, y] of pts) d += `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${(rnd() * 1.4 + 0.6).toFixed(2)}" fill="rgba(239,195,137,.45)"/>`;
|
|
1271
|
+
return d;
|
|
1272
|
+
}, []);
|
|
1273
|
+
return /* @__PURE__ */ jsx("svg", { className: "pnl-stars", viewBox: "0 0 1600 1000", preserveAspectRatio: "xMidYMid slice", "aria-hidden": true, dangerouslySetInnerHTML: { __html: svg } });
|
|
1274
|
+
}
|
|
1275
|
+
function LawPlate({ org, selected, onSelect }) {
|
|
1276
|
+
const root = org.agents.find((a) => a.id === org.root);
|
|
1277
|
+
const rules = root?.rules ?? [];
|
|
1278
|
+
if (!rules.length) return null;
|
|
1279
|
+
return /* @__PURE__ */ jsxs(
|
|
1280
|
+
"div",
|
|
1281
|
+
{
|
|
1282
|
+
className: `ag law-plate${selected ? " sel" : ""}`,
|
|
1283
|
+
"data-rail": "law",
|
|
1284
|
+
tabIndex: 0,
|
|
1285
|
+
title: "the law every agent in the house boots against",
|
|
1286
|
+
onClick: (e) => {
|
|
1287
|
+
e.stopPropagation();
|
|
1288
|
+
if (root) onSelect(root.id);
|
|
1289
|
+
},
|
|
1290
|
+
onKeyDown: (e) => {
|
|
1291
|
+
if ((e.key === "Enter" || e.key === " ") && root) {
|
|
1292
|
+
e.preventDefault();
|
|
1293
|
+
onSelect(root.id);
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
children: [
|
|
1297
|
+
/* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
|
|
1298
|
+
/* @__PURE__ */ jsx("span", { className: "ag-ava", children: "\xA7" }),
|
|
1299
|
+
/* @__PURE__ */ jsx("span", { className: "ag-name", children: "The House Standard" })
|
|
1300
|
+
] }),
|
|
1301
|
+
/* @__PURE__ */ jsx("span", { className: "ag-role", children: rules[0] }),
|
|
1302
|
+
/* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
|
|
1303
|
+
/* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
|
|
1304
|
+
"binds ",
|
|
1305
|
+
org.agents.length
|
|
1306
|
+
] }),
|
|
1307
|
+
/* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
|
|
1308
|
+
rules.length,
|
|
1309
|
+
" clause",
|
|
1310
|
+
rules.length === 1 ? "" : "s"
|
|
1311
|
+
] })
|
|
1312
|
+
] })
|
|
1313
|
+
]
|
|
1314
|
+
}
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
function boxIn(el, root) {
|
|
1318
|
+
let x = 0, y = 0;
|
|
1319
|
+
let cur = el;
|
|
1320
|
+
while (cur && cur !== root) {
|
|
1321
|
+
x += cur.offsetLeft;
|
|
1322
|
+
y += cur.offsetTop;
|
|
1323
|
+
cur = cur.offsetParent;
|
|
1324
|
+
}
|
|
1325
|
+
return { x, y, w: el.offsetWidth, h: el.offsetHeight, cy: y + el.offsetHeight / 2 };
|
|
1326
|
+
}
|
|
1327
|
+
function elbow(x1, y1, x2, y2) {
|
|
1328
|
+
if (Math.abs(y2 - y1) < 1.5) return `M${x1},${y1.toFixed(1)} H${x2}`;
|
|
1329
|
+
const mx = x1 + (x2 - x1) * 0.45;
|
|
1330
|
+
const r = Math.min(9, Math.abs(y2 - y1) / 2);
|
|
1331
|
+
const dir = y2 > y1 ? 1 : -1;
|
|
1332
|
+
return `M${x1},${y1.toFixed(1)} H${(mx - r).toFixed(1)} Q${mx},${y1.toFixed(1)} ${mx},${(y1 + r * dir).toFixed(1)} V${(y2 - r * dir).toFixed(1)} Q${mx},${y2.toFixed(1)} ${(mx + r).toFixed(1)},${y2.toFixed(1)} H${x2}`;
|
|
1333
|
+
}
|
|
1334
|
+
function CascadeRails({ chartRef, version }) {
|
|
1335
|
+
const [paths, setPaths] = useState([]);
|
|
1336
|
+
useLayoutEffect(() => {
|
|
1337
|
+
const root = chartRef.current;
|
|
1338
|
+
if (!root) return;
|
|
1339
|
+
const measure = () => {
|
|
1340
|
+
const pick = (sel) => Array.from(root.querySelectorAll(sel));
|
|
1341
|
+
const law = root.querySelector('[data-rail="law"]');
|
|
1342
|
+
const gm = root.querySelector('[data-rail="gm"]');
|
|
1343
|
+
const depts = pick('[data-rail="dept"]');
|
|
1344
|
+
const staff = pick('[data-rail="staff"]');
|
|
1345
|
+
const out = [];
|
|
1346
|
+
const gmB = gm ? boxIn(gm, root) : null;
|
|
1347
|
+
if (law && gmB) {
|
|
1348
|
+
const lawB = boxIn(law, root);
|
|
1349
|
+
out.push(elbow(lawB.x + lawB.w, lawB.cy, gmB.x, gmB.cy));
|
|
1350
|
+
}
|
|
1351
|
+
const deptBox = /* @__PURE__ */ new Map();
|
|
1352
|
+
for (const d of depts) {
|
|
1353
|
+
const b = boxIn(d, root);
|
|
1354
|
+
deptBox.set(d.dataset.id ?? "", b);
|
|
1355
|
+
if (gmB) out.push(elbow(gmB.x + gmB.w, gmB.cy, b.x, b.cy));
|
|
1356
|
+
}
|
|
1357
|
+
for (const s of staff) {
|
|
1358
|
+
const parent = deptBox.get(s.dataset.parent ?? "");
|
|
1359
|
+
if (!parent) continue;
|
|
1360
|
+
const b = boxIn(s, root);
|
|
1361
|
+
out.push(elbow(parent.x + parent.w, parent.cy, b.x, b.cy));
|
|
1362
|
+
}
|
|
1363
|
+
setPaths((prev) => prev.length === out.length && prev.every((p, i) => p === out[i]) ? prev : out);
|
|
1364
|
+
};
|
|
1365
|
+
measure();
|
|
1366
|
+
const ro = new ResizeObserver(measure);
|
|
1367
|
+
ro.observe(root);
|
|
1368
|
+
return () => ro.disconnect();
|
|
1369
|
+
}, [chartRef, version]);
|
|
1370
|
+
return /* @__PURE__ */ jsxs("svg", { className: "rails", "aria-hidden": true, children: [
|
|
1371
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: "railGrad", x1: "0", y1: "0", x2: "1", y2: "0", children: [
|
|
1372
|
+
/* @__PURE__ */ jsx("stop", { offset: "0", stopColor: "var(--brass)", stopOpacity: "0.3" }),
|
|
1373
|
+
/* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "var(--brass-hi)", stopOpacity: "0.85" })
|
|
1374
|
+
] }) }),
|
|
1375
|
+
paths.map((d, i) => /* @__PURE__ */ jsxs("g", { children: [
|
|
1376
|
+
/* @__PURE__ */ jsx("path", { className: "rail-line", d }),
|
|
1377
|
+
/* @__PURE__ */ jsx("path", { className: "rail-dash", d })
|
|
1378
|
+
] }, i))
|
|
1379
|
+
] });
|
|
1380
|
+
}
|
|
1381
|
+
function Lane({ laneId, isLane, dragId, onDropOn, children }) {
|
|
1382
|
+
const [over, setOver] = useState(false);
|
|
1383
|
+
if (!isLane) return /* @__PURE__ */ jsx("div", { className: "oc-child", children });
|
|
1384
|
+
return /* @__PURE__ */ jsx(
|
|
1385
|
+
"div",
|
|
1386
|
+
{
|
|
1387
|
+
className: `oc-child${over && dragId && dragId !== laneId ? " lane-over" : ""}`,
|
|
1388
|
+
onDragOver: (e) => {
|
|
1389
|
+
if (dragId && dragId !== laneId) {
|
|
1390
|
+
e.preventDefault();
|
|
1391
|
+
setOver(true);
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
onDragLeave: (e) => {
|
|
1395
|
+
if (!e.currentTarget.contains(e.relatedTarget)) setOver(false);
|
|
1396
|
+
},
|
|
1397
|
+
onDrop: (e) => {
|
|
1398
|
+
e.preventDefault();
|
|
1399
|
+
setOver(false);
|
|
1400
|
+
if (dragId && dragId !== laneId) onDropOn(laneId);
|
|
1401
|
+
},
|
|
1402
|
+
children
|
|
1403
|
+
}
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
var HEALTH_WORD = { ok: "Healthy", warn: "Needs a look", fail: "Failing", none: "No reports yet" };
|
|
1407
|
+
function consequenceOf(a, org, slice) {
|
|
1408
|
+
if (!slice) return "";
|
|
1409
|
+
const descendants = (id) => {
|
|
1410
|
+
const kids = org.agents.filter((x) => x.parent === id);
|
|
1411
|
+
return kids.reduce((n, k) => n + 1 + descendants(k.id), 0);
|
|
1412
|
+
};
|
|
1413
|
+
const declares = a.rules?.length ?? 0;
|
|
1414
|
+
const below = descendants(a.id);
|
|
1415
|
+
if (below === 0) return "";
|
|
1416
|
+
const parts = [];
|
|
1417
|
+
if (declares) parts.push(`declares ${declares} rule${declares === 1 ? "" : "s"}`);
|
|
1418
|
+
parts.push(`binds ${below} below`);
|
|
1419
|
+
return parts.join(" \xB7 ");
|
|
1420
|
+
}
|
|
1421
|
+
function AgentFacts({ a, org, health, showLaw }) {
|
|
1422
|
+
const slice = useMemo(() => orgBootSlice(org, a.id), [org, a.id]);
|
|
1423
|
+
const consequence = useMemo(() => consequenceOf(a, org, slice), [a, org, slice]);
|
|
1424
|
+
const pulse = pulseFor(a, health);
|
|
1425
|
+
const light = lightFor(a, health);
|
|
1426
|
+
if (!slice) return null;
|
|
1427
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1428
|
+
consequence && /* @__PURE__ */ jsx("span", { className: "ag-flows", title: "what flows out of this node \u2014 the org file is the source booboo_boot reads", children: consequence }),
|
|
1429
|
+
/* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
|
|
1430
|
+
/* @__PURE__ */ jsx("em", { className: `ag-fact ag-fact-health ${light}`, children: HEALTH_WORD[light] }),
|
|
1431
|
+
/* @__PURE__ */ jsxs("em", { className: "ag-fact ag-fact-report", title: typeof a.cadence === "number" ? `${pulse?.lastAt ? `last report filed ${relTime(pulse.lastAt)}` : "no report filed yet"} \xB7 expected ${everyN(a.cadence)}; amber past about twice that` : pulse?.lastAt ? `last report filed ${relTime(pulse.lastAt)}` : "no report filed yet", children: [
|
|
1432
|
+
pulse?.lastAt ? `reported ${relTime(pulse.lastAt)}` : "",
|
|
1433
|
+
typeof a.cadence === "number" && a.kind !== "automation" && /* @__PURE__ */ jsxs("b", { className: "ag-cadence", children: [
|
|
1434
|
+
" \xB7 ",
|
|
1435
|
+
everyN(a.cadence)
|
|
1436
|
+
] })
|
|
1437
|
+
] })
|
|
1438
|
+
] }),
|
|
1439
|
+
showLaw && /* @__PURE__ */ jsx("span", { className: "ag-law", title: "inheritance in boot order", children: slice.chain.map((c, i) => /* @__PURE__ */ jsxs("span", { children: [
|
|
1440
|
+
i > 0 && /* @__PURE__ */ jsx("i", { className: "ag-law-arrow", children: "\u2193" }),
|
|
1441
|
+
c.name
|
|
1442
|
+
] }, c.id)) })
|
|
1443
|
+
] });
|
|
1444
|
+
}
|
|
723
1445
|
function AgentCard({
|
|
724
1446
|
a,
|
|
1447
|
+
org,
|
|
725
1448
|
isRoot,
|
|
726
1449
|
depth,
|
|
727
1450
|
order,
|
|
@@ -731,16 +1454,19 @@ function AgentCard({
|
|
|
731
1454
|
onDragStart,
|
|
732
1455
|
onDropOn,
|
|
733
1456
|
childCount,
|
|
734
|
-
light = "none"
|
|
1457
|
+
light = "none",
|
|
1458
|
+
health = null,
|
|
1459
|
+
onHover,
|
|
1460
|
+
showLaw = false
|
|
735
1461
|
}) {
|
|
736
1462
|
const [over, setOver] = useState(false);
|
|
737
|
-
const nBuckets = a.buckets?.length ?? 0;
|
|
738
1463
|
const nSkills = a.skills?.length ?? 0;
|
|
1464
|
+
const parentName = a.parent ? org.agents.find((x) => x.id === a.parent)?.name : null;
|
|
739
1465
|
return /* @__PURE__ */ jsxs(
|
|
740
1466
|
"div",
|
|
741
1467
|
{
|
|
742
|
-
className: `ag${isRoot ? " root" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}`,
|
|
743
|
-
style: { ["--
|
|
1468
|
+
className: `ag${isRoot ? " root" : ""}${depth >= 2 ? " staff" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}${showLaw ? " law-on" : ""}${light !== "none" ? " h-" + light : ""}`,
|
|
1469
|
+
style: { ["--d"]: depth, animationDelay: `${Math.min(depth * 70 + order * 45, 600)}ms` },
|
|
744
1470
|
draggable: !isRoot,
|
|
745
1471
|
tabIndex: 0,
|
|
746
1472
|
onKeyDown: (e) => {
|
|
@@ -769,16 +1495,28 @@ function AgentCard({
|
|
|
769
1495
|
setOver(false);
|
|
770
1496
|
onDropOn(a.id);
|
|
771
1497
|
},
|
|
1498
|
+
onMouseEnter: () => onHover?.(a.id),
|
|
1499
|
+
onMouseLeave: () => onHover?.(null),
|
|
1500
|
+
"data-rail": isRoot ? "gm" : depth === 1 ? "dept" : "staff",
|
|
1501
|
+
"data-id": a.id,
|
|
1502
|
+
"data-parent": a.parent ?? "",
|
|
772
1503
|
children: [
|
|
773
|
-
light !== "none" && /* @__PURE__ */ jsx(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1504
|
+
light !== "none" && /* @__PURE__ */ jsx(
|
|
1505
|
+
"i",
|
|
1506
|
+
{
|
|
1507
|
+
className: `ag-light ${light}`,
|
|
1508
|
+
style: { ["--lamp-delay"]: `${bucketHue(a.id) % 26 / 10}s` },
|
|
1509
|
+
title: `health: ${light === "ok" ? "healthy" : light === "warn" ? "needs a look" : "failing"}`
|
|
1510
|
+
}
|
|
1511
|
+
),
|
|
1512
|
+
/* @__PURE__ */ jsx("span", { className: "ag-eyebrow", children: depth === 0 ? "The house \xB7 rank II" : depth === 1 ? "Department \xB7 rank III" : parentName ?? "Staff" }),
|
|
1513
|
+
/* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
|
|
1514
|
+
depth <= 1 && a.emoji && /* @__PURE__ */ jsx("span", { className: "ag-ava", children: a.emoji }),
|
|
1515
|
+
/* @__PURE__ */ jsx("span", { className: "ag-name", children: a.name })
|
|
1516
|
+
] }),
|
|
1517
|
+
a.role && /* @__PURE__ */ jsx("span", { className: "ag-role", children: a.role }),
|
|
1518
|
+
/* @__PURE__ */ jsx(AgentFacts, { a, org, health, showLaw }),
|
|
1519
|
+
(nSkills > 0 || childCount > 0) && /* @__PURE__ */ jsxs("span", { className: "ag-meta", children: [
|
|
782
1520
|
nSkills > 0 && /* @__PURE__ */ jsxs("em", { title: "skills", children: [
|
|
783
1521
|
"\u2726 ",
|
|
784
1522
|
nSkills
|
|
@@ -799,7 +1537,8 @@ function ChartNode({
|
|
|
799
1537
|
order,
|
|
800
1538
|
...cardProps
|
|
801
1539
|
}) {
|
|
802
|
-
const
|
|
1540
|
+
const kidsRaw = org.agents.filter((c) => c.parent === a.id && c.kind !== "automation");
|
|
1541
|
+
const kids = depth === 0 ? [...kidsRaw].sort((x, y) => x.id.localeCompare(y.id)) : kidsRaw;
|
|
803
1542
|
const machines = (() => {
|
|
804
1543
|
const out = [];
|
|
805
1544
|
const walk = (pid) => {
|
|
@@ -814,17 +1553,20 @@ function ChartNode({
|
|
|
814
1553
|
})();
|
|
815
1554
|
const lights = machines.map((m) => lightFor(m, cardProps.health));
|
|
816
1555
|
const trayLight = worst(lights);
|
|
1556
|
+
const isDept = depth === 1 && kids.length > 0;
|
|
1557
|
+
const folded = isDept && cardProps.collapsed?.has(a.id);
|
|
817
1558
|
const TRAY_MAX = 8;
|
|
818
1559
|
const hidden = machines.length - TRAY_MAX;
|
|
819
1560
|
const hiddenBad = machines.slice(TRAY_MAX).reduce(
|
|
820
1561
|
(n, m) => n + (lightFor(m, cardProps.health) !== "ok" && lightFor(m, cardProps.health) !== "none" ? 1 : 0),
|
|
821
1562
|
0
|
|
822
1563
|
);
|
|
823
|
-
|
|
1564
|
+
const cardAndRack = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
824
1565
|
/* @__PURE__ */ jsx(
|
|
825
1566
|
AgentCard,
|
|
826
1567
|
{
|
|
827
1568
|
a,
|
|
1569
|
+
org,
|
|
828
1570
|
isRoot: a.id === org.root,
|
|
829
1571
|
depth,
|
|
830
1572
|
order,
|
|
@@ -834,7 +1576,10 @@ function ChartNode({
|
|
|
834
1576
|
onDragStart: cardProps.onDragStart,
|
|
835
1577
|
onDropOn: cardProps.onDropOn,
|
|
836
1578
|
childCount: kids.length,
|
|
837
|
-
light: trayLight
|
|
1579
|
+
light: trayLight,
|
|
1580
|
+
health: cardProps.health,
|
|
1581
|
+
onHover: cardProps.onHover,
|
|
1582
|
+
showLaw: cardProps.showLaw
|
|
838
1583
|
}
|
|
839
1584
|
),
|
|
840
1585
|
machines.length > 0 && /* @__PURE__ */ jsxs("div", { className: `oc-tray ${trayLight}`, children: [
|
|
@@ -855,7 +1600,7 @@ function ChartNode({
|
|
|
855
1600
|
},
|
|
856
1601
|
children: [
|
|
857
1602
|
/* @__PURE__ */ jsx("i", { className: `mac-dot ${lightFor(m, cardProps.health)}${unstableFor(m, cardProps.health) ? " unstable" : ""}` }),
|
|
858
|
-
/* @__PURE__ */ jsx(
|
|
1603
|
+
/* @__PURE__ */ jsx(BrandMark, { agent: m }),
|
|
859
1604
|
/* @__PURE__ */ jsx("span", { className: "mac-name", children: m.name })
|
|
860
1605
|
]
|
|
861
1606
|
},
|
|
@@ -879,13 +1624,78 @@ function ChartNode({
|
|
|
879
1624
|
]
|
|
880
1625
|
}
|
|
881
1626
|
)
|
|
882
|
-
] })
|
|
1627
|
+
] })
|
|
1628
|
+
] });
|
|
1629
|
+
return /* @__PURE__ */ jsxs("div", { className: "ocn", children: [
|
|
1630
|
+
depth === 0 ? /* @__PURE__ */ jsx("div", { className: "casc-head", children: cardAndRack }) : cardAndRack,
|
|
883
1631
|
kids.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
884
|
-
/* @__PURE__ */ jsx(
|
|
885
|
-
|
|
1632
|
+
/* @__PURE__ */ jsx(
|
|
1633
|
+
"div",
|
|
1634
|
+
{
|
|
1635
|
+
className: `oc-down${isDept ? " foldable" : ""}${folded ? " folded" : ""}`,
|
|
1636
|
+
role: isDept ? "button" : void 0,
|
|
1637
|
+
tabIndex: isDept ? 0 : void 0,
|
|
1638
|
+
title: isDept ? folded ? `${kids.length} staff \u2014 click to expand (semantic zoom: department \u2192 role)` : "click to fold this department to head-only (semantic zoom: role \u2192 department)" : void 0,
|
|
1639
|
+
onClick: isDept ? (e) => {
|
|
1640
|
+
e.stopPropagation();
|
|
1641
|
+
cardProps.onToggleCollapse?.(a.id);
|
|
1642
|
+
} : void 0,
|
|
1643
|
+
onKeyDown: isDept ? (e) => {
|
|
1644
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1645
|
+
e.preventDefault();
|
|
1646
|
+
cardProps.onToggleCollapse?.(a.id);
|
|
1647
|
+
}
|
|
1648
|
+
} : void 0,
|
|
1649
|
+
children: isDept && /* @__PURE__ */ jsx("i", { className: "oc-fold-glyph", children: folded ? "\u25B8" : "\u25BE" })
|
|
1650
|
+
}
|
|
1651
|
+
),
|
|
1652
|
+
folded ? /* @__PURE__ */ jsxs(
|
|
1653
|
+
"button",
|
|
1654
|
+
{
|
|
1655
|
+
type: "button",
|
|
1656
|
+
className: "oc-folded-summary",
|
|
1657
|
+
onClick: (e) => {
|
|
1658
|
+
e.stopPropagation();
|
|
1659
|
+
cardProps.onToggleCollapse?.(a.id);
|
|
1660
|
+
},
|
|
1661
|
+
title: "click to expand \u2014 semantic zoom: department \u2192 role",
|
|
1662
|
+
children: [
|
|
1663
|
+
kids.length,
|
|
1664
|
+
" staff \xB7 folded to department level \u2014 click to expand"
|
|
1665
|
+
]
|
|
1666
|
+
}
|
|
1667
|
+
) : /* @__PURE__ */ jsx("div", { className: `oc-row${depth > 0 && kids.length > 3 ? " wrap" : ""}${depth === 0 ? " lanes" : ""}`, children: kids.map((k, i) => /* @__PURE__ */ jsx(
|
|
1668
|
+
Lane,
|
|
1669
|
+
{
|
|
1670
|
+
laneId: k.id,
|
|
1671
|
+
isLane: depth === 0,
|
|
1672
|
+
dragId: cardProps.dragId,
|
|
1673
|
+
onDropOn: cardProps.onDropOn,
|
|
1674
|
+
children: /* @__PURE__ */ jsx(ChartNode, { org, a: k, depth: depth + 1, order: i, ...cardProps })
|
|
1675
|
+
},
|
|
1676
|
+
k.id
|
|
1677
|
+
)) })
|
|
886
1678
|
] })
|
|
887
1679
|
] });
|
|
888
1680
|
}
|
|
1681
|
+
function BrandMark({ agent }) {
|
|
1682
|
+
const brand = (agent.data ?? {}).brand;
|
|
1683
|
+
const [failed, setFailed] = useState(false);
|
|
1684
|
+
if (typeof brand !== "string" || !brand || failed) {
|
|
1685
|
+
return /* @__PURE__ */ jsx("span", { className: "mac-emoji", children: agent.emoji || "\u2699\uFE0F" });
|
|
1686
|
+
}
|
|
1687
|
+
return /* @__PURE__ */ jsx(
|
|
1688
|
+
"img",
|
|
1689
|
+
{
|
|
1690
|
+
className: "mac-brand",
|
|
1691
|
+
src: `https://cdn.simpleicons.org/${brand}/000`,
|
|
1692
|
+
alt: "",
|
|
1693
|
+
loading: "lazy",
|
|
1694
|
+
draggable: false,
|
|
1695
|
+
onError: () => setFailed(true)
|
|
1696
|
+
}
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
889
1699
|
function Chip({ children, tone, onClick }) {
|
|
890
1700
|
return /* @__PURE__ */ jsx("span", { className: `chip${tone ? ` ${tone}` : ""}${onClick ? " tap" : ""}`, onClick, children });
|
|
891
1701
|
}
|
|
@@ -938,8 +1748,19 @@ function Dossier({
|
|
|
938
1748
|
)
|
|
939
1749
|
).then((counts) => setMemCount(counts.reduce((s, n) => s + n, 0)));
|
|
940
1750
|
fetchReports(api, id, 100).then(({ total, nodes }) => {
|
|
941
|
-
|
|
942
|
-
|
|
1751
|
+
if (total > 0) {
|
|
1752
|
+
setRepCount(total);
|
|
1753
|
+
setReports(nodes.slice(0, 4));
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
return fetchReports(api, null, 2e3).then(({ nodes: all }) => {
|
|
1757
|
+
const mine = all.filter((n) => reportAgentId(n) === id);
|
|
1758
|
+
setRepCount(mine.length);
|
|
1759
|
+
setReports(mine.slice(0, 4));
|
|
1760
|
+
});
|
|
1761
|
+
}).catch(() => {
|
|
1762
|
+
setRepCount(0);
|
|
1763
|
+
setReports([]);
|
|
943
1764
|
});
|
|
944
1765
|
}, [id, hasSnapshot, slice, api]);
|
|
945
1766
|
const mem = useCountUp(memCount ?? 0);
|
|
@@ -978,13 +1799,10 @@ function Dossier({
|
|
|
978
1799
|
return /* @__PURE__ */ jsxs("aside", { className: "doss", onClick: (e) => e.stopPropagation(), children: [
|
|
979
1800
|
/* @__PURE__ */ jsxs("div", { className: "doss-head", children: [
|
|
980
1801
|
/* @__PURE__ */ jsx("span", { className: "doss-emoji", children: a.emoji || "\u{1F916}" }),
|
|
981
|
-
/* @__PURE__ */ jsxs("
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
|
|
986
|
-
] }),
|
|
987
|
-
a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role })
|
|
1802
|
+
/* @__PURE__ */ jsxs("h2", { children: [
|
|
1803
|
+
a.name,
|
|
1804
|
+
" ",
|
|
1805
|
+
a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
|
|
988
1806
|
] }),
|
|
989
1807
|
/* @__PURE__ */ jsxs("div", { className: "doss-head-actions", children: [
|
|
990
1808
|
!edit && /* @__PURE__ */ jsx("button", { className: "doss-3d", title: "edit this agent", onClick: startEdit, children: "\u270E edit" }),
|
|
@@ -992,6 +1810,7 @@ function Dossier({
|
|
|
992
1810
|
/* @__PURE__ */ jsx("button", { className: "doss-close", title: "close the dossier", "aria-label": "close the dossier", onClick: onClose, children: "\u2715" })
|
|
993
1811
|
] })
|
|
994
1812
|
] }),
|
|
1813
|
+
a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role }),
|
|
995
1814
|
edit && /* @__PURE__ */ jsxs("div", { className: "doss-edit", children: [
|
|
996
1815
|
/* @__PURE__ */ jsxs("div", { className: "doss-edit-row2", children: [
|
|
997
1816
|
/* @__PURE__ */ jsxs("label", { children: [
|
|
@@ -1044,9 +1863,8 @@ function Dossier({
|
|
|
1044
1863
|
p.n
|
|
1045
1864
|
] }),
|
|
1046
1865
|
typeof a.cadence === "number" && /* @__PURE__ */ jsxs("span", { children: [
|
|
1047
|
-
"
|
|
1048
|
-
a.cadence
|
|
1049
|
-
"h"
|
|
1866
|
+
"reports ",
|
|
1867
|
+
everyN(a.cadence)
|
|
1050
1868
|
] })
|
|
1051
1869
|
] });
|
|
1052
1870
|
})(),
|
|
@@ -1217,7 +2035,8 @@ function OrgScreen({
|
|
|
1217
2035
|
hasSnapshot,
|
|
1218
2036
|
onUpdate,
|
|
1219
2037
|
onAdd,
|
|
1220
|
-
onRemove
|
|
2038
|
+
onRemove,
|
|
2039
|
+
showLaw
|
|
1221
2040
|
}) {
|
|
1222
2041
|
const api = useApi();
|
|
1223
2042
|
const [health, setHealth] = useState(null);
|
|
@@ -1226,10 +2045,27 @@ function OrgScreen({
|
|
|
1226
2045
|
fetchReports(api, null, 1e4).then(({ nodes }) => setHealth(buildHealthMap(nodes))).catch(() => {
|
|
1227
2046
|
});
|
|
1228
2047
|
}, [hasSnapshot, api]);
|
|
2048
|
+
const [hoverId, setHoverId] = useState(null);
|
|
2049
|
+
const litBuckets = useMemo(
|
|
2050
|
+
() => new Set(hoverId ? orgBootSlice(draft, hoverId)?.buckets ?? [] : []),
|
|
2051
|
+
[hoverId, draft]
|
|
2052
|
+
);
|
|
2053
|
+
const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
|
|
2054
|
+
const depts = useMemo(() => draft.agents.filter((a) => a.parent === draft.root), [draft]);
|
|
2055
|
+
const toggleCollapse = useCallback((id) => {
|
|
2056
|
+
setCollapsed((s) => {
|
|
2057
|
+
const n = new Set(s);
|
|
2058
|
+
n.has(id) ? n.delete(id) : n.add(id);
|
|
2059
|
+
return n;
|
|
2060
|
+
});
|
|
2061
|
+
}, []);
|
|
2062
|
+
const allFolded = depts.length > 0 && depts.every((d) => collapsed.has(d.id));
|
|
2063
|
+
const railVersion = `${draft.agents.length}:${[...collapsed].sort().join(",")}:${showLaw}:${health ? 1 : 0}:${selected ?? ""}`;
|
|
1229
2064
|
const fitRef = useRef(null);
|
|
1230
2065
|
const chartRef = useRef(null);
|
|
1231
2066
|
const [fit, setFit] = useState(1);
|
|
1232
2067
|
const [zoom, setZoom] = useState(null);
|
|
2068
|
+
const [nat, setNat] = useState({ w: 0, h: 0 });
|
|
1233
2069
|
const eff = zoom ?? fit;
|
|
1234
2070
|
useLayoutEffect(() => {
|
|
1235
2071
|
const vp = fitRef.current, chart = chartRef.current;
|
|
@@ -1239,15 +2075,18 @@ function OrgScreen({
|
|
|
1239
2075
|
const natW = chart.scrollWidth, natH = chart.scrollHeight;
|
|
1240
2076
|
const availW = vp.clientWidth - PAD * 2, availH = vp.clientHeight - PAD * 2;
|
|
1241
2077
|
if (natW <= 0 || natH <= 0) return;
|
|
1242
|
-
|
|
1243
|
-
|
|
2078
|
+
setNat((p) => p.w === natW && p.h === natH ? p : { w: natW, h: natH });
|
|
2079
|
+
const kW = availW / natW;
|
|
2080
|
+
const kH = availH / natH;
|
|
2081
|
+
const k = natH > availH * 1.35 ? kW : Math.min(kW, kH);
|
|
2082
|
+
setFit(Math.min(1, k > 0 ? k : 1));
|
|
1244
2083
|
};
|
|
1245
2084
|
measure();
|
|
1246
2085
|
const ro = new ResizeObserver(measure);
|
|
1247
2086
|
ro.observe(vp);
|
|
1248
2087
|
ro.observe(chart);
|
|
1249
2088
|
return () => ro.disconnect();
|
|
1250
|
-
}, [draft, health, selected]);
|
|
2089
|
+
}, [draft, health, selected, collapsed]);
|
|
1251
2090
|
const effRef = useRef(eff);
|
|
1252
2091
|
effRef.current = eff;
|
|
1253
2092
|
useEffect(() => {
|
|
@@ -1264,24 +2103,74 @@ function OrgScreen({
|
|
|
1264
2103
|
}, []);
|
|
1265
2104
|
const root = draft.agents.find((a) => a.id === draft.root);
|
|
1266
2105
|
return /* @__PURE__ */ jsxs("div", { className: "body", onClick: () => setSelected(null), children: [
|
|
1267
|
-
/* @__PURE__ */ jsxs("main", { className:
|
|
1268
|
-
/* @__PURE__ */ jsx("p", { className: "tree-hint", children: "drag
|
|
1269
|
-
/* @__PURE__ */
|
|
1270
|
-
|
|
2106
|
+
/* @__PURE__ */ jsxs("main", { className: `tree${showLaw ? " law-on" : ""}`, onClick: (e) => e.stopPropagation(), children: [
|
|
2107
|
+
/* @__PURE__ */ jsx("p", { className: "tree-hint", children: showLaw ? "the rails now trace authority \u2014 brass flows from the House Standard through every SOP to every role" : "drag a plate \u2014 or a machine \u2014 onto its new parent \xB7 click for its dossier \xB7 machine racks show live health" }),
|
|
2108
|
+
/* @__PURE__ */ jsxs("div", { className: "ranks", children: [
|
|
2109
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2110
|
+
/* @__PURE__ */ jsx("b", { children: "I" }),
|
|
2111
|
+
"The standard"
|
|
2112
|
+
] }),
|
|
2113
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2114
|
+
/* @__PURE__ */ jsx("b", { children: "II" }),
|
|
2115
|
+
"The executive"
|
|
2116
|
+
] }),
|
|
2117
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2118
|
+
/* @__PURE__ */ jsx("b", { children: "III" }),
|
|
2119
|
+
"Departments"
|
|
2120
|
+
] }),
|
|
2121
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2122
|
+
/* @__PURE__ */ jsx("b", { children: "IV" }),
|
|
2123
|
+
"Staff & machines"
|
|
2124
|
+
] })
|
|
2125
|
+
] }),
|
|
2126
|
+
/* @__PURE__ */ jsx("div", { className: `chart-fit${zoom !== null ? " zoomed" : ""}`, ref: fitRef, children: /* @__PURE__ */ jsx("div", { className: "chart-scaled", style: { width: nat.w * eff, height: nat.h * eff }, children: /* @__PURE__ */ jsxs(
|
|
2127
|
+
"div",
|
|
1271
2128
|
{
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
2129
|
+
className: "chart",
|
|
2130
|
+
ref: chartRef,
|
|
2131
|
+
style: {
|
|
2132
|
+
["--fit"]: eff,
|
|
2133
|
+
transform: zoom !== null ? `scale(${eff})` : `translateX(-50%) scale(${eff})`
|
|
2134
|
+
},
|
|
2135
|
+
children: [
|
|
2136
|
+
/* @__PURE__ */ jsx(CascadeRails, { chartRef, version: railVersion }),
|
|
2137
|
+
/* @__PURE__ */ jsxs("div", { className: "cascade", children: [
|
|
2138
|
+
/* @__PURE__ */ jsx(LawPlate, { org: draft, selected: selected === draft.root, onSelect: setSelected }),
|
|
2139
|
+
root && /* @__PURE__ */ jsx(
|
|
2140
|
+
ChartNode,
|
|
2141
|
+
{
|
|
2142
|
+
org: draft,
|
|
2143
|
+
a: root,
|
|
2144
|
+
depth: 0,
|
|
2145
|
+
order: 0,
|
|
2146
|
+
selected,
|
|
2147
|
+
dragId,
|
|
2148
|
+
health,
|
|
2149
|
+
onSelect: setSelected,
|
|
2150
|
+
onDragStart: setDragId,
|
|
2151
|
+
onDropOn: dropOn,
|
|
2152
|
+
onHover: setHoverId,
|
|
2153
|
+
showLaw,
|
|
2154
|
+
collapsed,
|
|
2155
|
+
onToggleCollapse: toggleCollapse
|
|
2156
|
+
}
|
|
2157
|
+
)
|
|
2158
|
+
] })
|
|
2159
|
+
]
|
|
1282
2160
|
}
|
|
1283
2161
|
) }) }),
|
|
1284
2162
|
/* @__PURE__ */ jsxs("div", { className: "zoomer", children: [
|
|
2163
|
+
/* @__PURE__ */ jsx(
|
|
2164
|
+
"button",
|
|
2165
|
+
{
|
|
2166
|
+
type: "button",
|
|
2167
|
+
className: allFolded ? "on" : "",
|
|
2168
|
+
title: allFolded ? "unfold every department \u2014 see roles again" : "fold every department to head-only \u2014 house \u2192 department",
|
|
2169
|
+
onClick: () => setCollapsed(allFolded ? /* @__PURE__ */ new Set() : new Set(depts.map((d) => d.id))),
|
|
2170
|
+
children: allFolded ? "\u2302 house" : "\u25BE roles"
|
|
2171
|
+
}
|
|
2172
|
+
),
|
|
2173
|
+
/* @__PURE__ */ jsx("span", { className: "zoomer-sep" }),
|
|
1285
2174
|
/* @__PURE__ */ jsx("button", { type: "button", title: "zoom out (ctrl+wheel)", onClick: () => setZoom(Math.max(0.2, eff / 1.25)), children: "\u2212" }),
|
|
1286
2175
|
/* @__PURE__ */ jsxs("span", { className: "zoomer-pct", children: [
|
|
1287
2176
|
Math.round(eff * 100),
|
|
@@ -1289,7 +2178,8 @@ function OrgScreen({
|
|
|
1289
2178
|
] }),
|
|
1290
2179
|
/* @__PURE__ */ jsx("button", { type: "button", title: "zoom in (ctrl+wheel)", onClick: () => setZoom(Math.min(2.5, eff * 1.25)), children: "\uFF0B" }),
|
|
1291
2180
|
/* @__PURE__ */ jsx("button", { type: "button", className: zoom === null ? "on" : "", title: "fit the whole org in view", onClick: () => setZoom(null), children: "fit" })
|
|
1292
|
-
] })
|
|
2181
|
+
] }),
|
|
2182
|
+
/* @__PURE__ */ jsx(LedgerShelf, { org: draft, lit: litBuckets })
|
|
1293
2183
|
] }),
|
|
1294
2184
|
selected && /* @__PURE__ */ jsx(
|
|
1295
2185
|
Dossier,
|
|
@@ -1418,8 +2308,8 @@ function ReportsScreen({ org, hasSnapshot }) {
|
|
|
1418
2308
|
if (!hasSnapshot) return;
|
|
1419
2309
|
fetchReports(api, null, 1e3).then(({ nodes }) => setRows(nodes)).catch(() => setRows([]));
|
|
1420
2310
|
}, [hasSnapshot, api]);
|
|
1421
|
-
const agents = useMemo(() => [...new Set((rows ?? []).map(
|
|
1422
|
-
const shown = (rows ?? []).filter((r) => !who || r
|
|
2311
|
+
const agents = useMemo(() => [...new Set((rows ?? []).map(reportAgentId))].filter(Boolean), [rows]);
|
|
2312
|
+
const shown = (rows ?? []).filter((r) => !who || reportAgentId(r) === who);
|
|
1423
2313
|
const total = useCountUp(shown.length);
|
|
1424
2314
|
if (!hasSnapshot)
|
|
1425
2315
|
return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
|
|
@@ -1450,15 +2340,16 @@ function ReportsScreen({ org, hasSnapshot }) {
|
|
|
1450
2340
|
")."
|
|
1451
2341
|
] }) : /* @__PURE__ */ jsxs("div", { className: "timeline", children: [
|
|
1452
2342
|
shown.slice(0, 100).map((r) => {
|
|
1453
|
-
const
|
|
2343
|
+
const filer = reportAgentId(r);
|
|
2344
|
+
const a = filer ? nameOf.get(filer) : void 0;
|
|
1454
2345
|
const when = relTime(nodeAt(r));
|
|
1455
2346
|
const sum = nodeSummary(r);
|
|
1456
|
-
return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(
|
|
2347
|
+
return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(filer || "x") }, children: [
|
|
1457
2348
|
/* @__PURE__ */ jsx("div", { className: "tl-dot" }),
|
|
1458
2349
|
/* @__PURE__ */ jsxs("div", { className: "tl-body", children: [
|
|
1459
2350
|
/* @__PURE__ */ jsxs("div", { className: "tl-top", children: [
|
|
1460
2351
|
/* @__PURE__ */ jsx("span", { className: "tl-ava", children: a?.emoji ?? "\u{1F916}" }),
|
|
1461
|
-
/* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ??
|
|
2352
|
+
/* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ?? filer ?? "unknown" }),
|
|
1462
2353
|
when && /* @__PURE__ */ jsx("span", { className: "tl-when", children: when })
|
|
1463
2354
|
] }),
|
|
1464
2355
|
/* @__PURE__ */ jsx("p", { className: "tl-sum", children: sum || r.label })
|
|
@@ -1543,7 +2434,7 @@ function GraphScreen({ hasSnapshot }) {
|
|
|
1543
2434
|
] });
|
|
1544
2435
|
return /* @__PURE__ */ jsx("div", { className: "graph-wrap", children: /* @__PURE__ */ jsx("iframe", { className: "graph-frame", src: "/view/?file=/snapshot.json", title: "Booboo 3D brain" }) });
|
|
1545
2436
|
}
|
|
1546
|
-
function App() {
|
|
2437
|
+
function App({ theme: pinnedTheme }) {
|
|
1547
2438
|
const api = useApi();
|
|
1548
2439
|
const [tab, param] = useRoute();
|
|
1549
2440
|
const [saved, setSaved] = useState(null);
|
|
@@ -1558,7 +2449,8 @@ function App() {
|
|
|
1558
2449
|
api("/org").then((o) => {
|
|
1559
2450
|
setSaved(o);
|
|
1560
2451
|
setDraft(o);
|
|
1561
|
-
|
|
2452
|
+
const embed = new URLSearchParams(window.location.search).get("embed");
|
|
2453
|
+
if (!embed) setSelected(o.root);
|
|
1562
2454
|
}).catch(() => setErr("Can't load the organigram \u2014 is `booboo panel --org` running?"));
|
|
1563
2455
|
api("/stats").then(setStats).catch(() => setStats(null));
|
|
1564
2456
|
Promise.all([
|
|
@@ -1576,15 +2468,32 @@ function App() {
|
|
|
1576
2468
|
for (const a of draft.agents) {
|
|
1577
2469
|
const b = before.get(a.id);
|
|
1578
2470
|
if (!b) {
|
|
1579
|
-
out.push(`\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}`);
|
|
2471
|
+
out.push({ key: `add:${a.id}`, id: a.id, kind: "added", label: `\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}` });
|
|
1580
2472
|
continue;
|
|
1581
2473
|
}
|
|
1582
|
-
if ((b.parent ?? null) !== (a.parent ?? null)) out.push(`${a.name} \u2192 now under ${a.parent ? name(a.parent) : "root"}`);
|
|
1583
|
-
if (JSON.stringify(noParent(b)) !== JSON.stringify(noParent(a))) out.push(`\u270E ${a.name} edited`);
|
|
2474
|
+
if ((b.parent ?? null) !== (a.parent ?? null)) out.push({ key: `move:${a.id}`, id: a.id, kind: "moved", label: `${a.name} \u2192 now under ${a.parent ? name(a.parent) : "root"}` });
|
|
2475
|
+
if (JSON.stringify(noParent(b)) !== JSON.stringify(noParent(a))) out.push({ key: `edit:${a.id}`, id: a.id, kind: "edited", label: `\u270E ${a.name} edited` });
|
|
1584
2476
|
}
|
|
1585
|
-
for (const b of saved.agents) if (!after.has(b.id)) out.push(`\u2212 ${b.name} removed`);
|
|
2477
|
+
for (const b of saved.agents) if (!after.has(b.id)) out.push({ key: `rm:${b.id}`, id: b.id, kind: "removed", label: `\u2212 ${b.name} removed` });
|
|
1586
2478
|
return out;
|
|
1587
2479
|
}, [saved, draft]);
|
|
2480
|
+
const revertOne = useCallback((c) => {
|
|
2481
|
+
if (!saved || !draft) return;
|
|
2482
|
+
const was = saved.agents.find((a) => a.id === c.id);
|
|
2483
|
+
setDraft((d) => {
|
|
2484
|
+
if (!d) return d;
|
|
2485
|
+
if (c.kind === "added") return { ...d, agents: d.agents.filter((a) => a.id !== c.id) };
|
|
2486
|
+
if (c.kind === "removed" && was) return { ...d, agents: [...d.agents, was] };
|
|
2487
|
+
if (!was) return d;
|
|
2488
|
+
return {
|
|
2489
|
+
...d,
|
|
2490
|
+
agents: d.agents.map((a) => {
|
|
2491
|
+
if (a.id !== c.id) return a;
|
|
2492
|
+
return c.kind === "moved" ? { ...a, parent: was.parent } : { ...was, parent: a.parent };
|
|
2493
|
+
})
|
|
2494
|
+
};
|
|
2495
|
+
});
|
|
2496
|
+
}, [saved, draft]);
|
|
1588
2497
|
const updateAgent = useCallback((id, patch) => {
|
|
1589
2498
|
setDraft((d) => d ? { ...d, agents: d.agents.map((a) => a.id === id ? { ...a, ...patch } : a) } : d);
|
|
1590
2499
|
}, []);
|
|
@@ -1656,11 +2565,13 @@ function App() {
|
|
|
1656
2565
|
const nodeCount = useCountUp(stats?.nodes ?? 0, 1200);
|
|
1657
2566
|
const memTotal = useCountUp(totals?.mem ?? 0, 1100);
|
|
1658
2567
|
const repTotal = useCountUp(totals?.rep ?? 0, 1300);
|
|
1659
|
-
const [theme, toggleTheme] = useTheme();
|
|
1660
|
-
|
|
1661
|
-
if (!draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal
|
|
1662
|
-
return /* @__PURE__ */
|
|
2568
|
+
const [theme, toggleTheme] = useTheme(pinnedTheme);
|
|
2569
|
+
const [showLaw, setShowLaw] = useState(false);
|
|
2570
|
+
if (err && !draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal", "data-theme": theme, children: err });
|
|
2571
|
+
if (!draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal calm", "data-theme": theme, children: "waking the organigram\u2026" });
|
|
2572
|
+
return /* @__PURE__ */ jsxs("div", { className: "pnl", "data-theme": theme, children: [
|
|
1663
2573
|
/* @__PURE__ */ jsx("div", { className: "pnl-aurora", "aria-hidden": true }),
|
|
2574
|
+
/* @__PURE__ */ jsx(Constellation, {}),
|
|
1664
2575
|
/* @__PURE__ */ jsxs("header", { className: "bar", children: [
|
|
1665
2576
|
/* @__PURE__ */ jsxs("div", { className: "bar-brand", children: [
|
|
1666
2577
|
"\u{1F43E} ",
|
|
@@ -1685,7 +2596,17 @@ function App() {
|
|
|
1685
2596
|
] })
|
|
1686
2597
|
] }),
|
|
1687
2598
|
/* @__PURE__ */ jsxs("div", { className: "bar-actions", children: [
|
|
1688
|
-
|
|
2599
|
+
tab === "org" && /* @__PURE__ */ jsx(
|
|
2600
|
+
"button",
|
|
2601
|
+
{
|
|
2602
|
+
className: `btn ghost law-toggle${showLaw ? " on" : ""}`,
|
|
2603
|
+
title: "trace rule inheritance \u2014 House Standard \u2192 SOP \u2192 role",
|
|
2604
|
+
"aria-pressed": showLaw,
|
|
2605
|
+
onClick: () => setShowLaw((v) => !v),
|
|
2606
|
+
children: "\u2696 show the law"
|
|
2607
|
+
}
|
|
2608
|
+
),
|
|
2609
|
+
toggleTheme && /* @__PURE__ */ jsx("button", { className: "btn ghost theme-toggle", title: "light / dark", "aria-label": "toggle light or dark theme", onClick: toggleTheme, children: theme === "dark" ? "\u2600" : "\u263E" }),
|
|
1689
2610
|
changes.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1690
2611
|
/* @__PURE__ */ jsxs("span", { className: "bar-draft", children: [
|
|
1691
2612
|
changes.length,
|
|
@@ -1702,7 +2623,23 @@ function App() {
|
|
|
1702
2623
|
" ",
|
|
1703
2624
|
t.label
|
|
1704
2625
|
] }, t.id)) }),
|
|
1705
|
-
changes.length > 0 && tab === "org" && /* @__PURE__ */
|
|
2626
|
+
changes.length > 0 && tab === "org" && /* @__PURE__ */ jsxs("div", { className: "pending", children: [
|
|
2627
|
+
/* @__PURE__ */ jsx("span", { className: "pending-lead", children: "unapplied" }),
|
|
2628
|
+
changes.map((c) => /* @__PURE__ */ jsxs(
|
|
2629
|
+
"button",
|
|
2630
|
+
{
|
|
2631
|
+
type: "button",
|
|
2632
|
+
className: "pending-item",
|
|
2633
|
+
title: "undo just this change",
|
|
2634
|
+
onClick: () => revertOne(c),
|
|
2635
|
+
children: [
|
|
2636
|
+
c.label,
|
|
2637
|
+
/* @__PURE__ */ jsx("i", { "aria-hidden": "true", children: "undo" })
|
|
2638
|
+
]
|
|
2639
|
+
},
|
|
2640
|
+
c.key
|
|
2641
|
+
))
|
|
2642
|
+
] }),
|
|
1706
2643
|
err && /* @__PURE__ */ jsx("div", { className: "pnl-err", children: err }),
|
|
1707
2644
|
/* @__PURE__ */ jsxs("div", { className: "content", children: [
|
|
1708
2645
|
tab === "org" && /* @__PURE__ */ jsx(
|
|
@@ -1717,7 +2654,8 @@ function App() {
|
|
|
1717
2654
|
hasSnapshot: !!stats,
|
|
1718
2655
|
onUpdate: updateAgent,
|
|
1719
2656
|
onAdd: addAgent,
|
|
1720
|
-
onRemove: removeAgent
|
|
2657
|
+
onRemove: removeAgent,
|
|
2658
|
+
showLaw
|
|
1721
2659
|
}
|
|
1722
2660
|
),
|
|
1723
2661
|
tab === "buckets" && /* @__PURE__ */ jsx(BucketsScreen, { org: draft, param, hasSnapshot: !!stats }),
|
|
@@ -1727,17 +2665,17 @@ function App() {
|
|
|
1727
2665
|
] }, tab + (param ?? ""))
|
|
1728
2666
|
] });
|
|
1729
2667
|
}
|
|
1730
|
-
function Panel({ api = defaultApi } = {}) {
|
|
2668
|
+
function Panel({ api = defaultApi, theme } = {}) {
|
|
1731
2669
|
return /* @__PURE__ */ jsxs(ApiCtx.Provider, { value: api, children: [
|
|
1732
2670
|
/* @__PURE__ */ jsx("style", { children: PANEL_CSS }),
|
|
1733
|
-
/* @__PURE__ */ jsx(App, {})
|
|
2671
|
+
/* @__PURE__ */ jsx(App, { theme })
|
|
1734
2672
|
] });
|
|
1735
2673
|
}
|
|
1736
2674
|
|
|
1737
2675
|
// package.json
|
|
1738
2676
|
var package_default = {
|
|
1739
2677
|
name: "@booboo-brain/panel",
|
|
1740
|
-
version: "0.5.
|
|
2678
|
+
version: "0.5.5",
|
|
1741
2679
|
description: "Booboo panel \u2014 the organigram. Run your agents like a company: a drag-drop hierarchy your agents boot from, with buckets, reports, rules and the 3D graph, over one org file + snapshot.",
|
|
1742
2680
|
license: "MIT",
|
|
1743
2681
|
repository: {
|