@booboo-brain/panel 0.5.2 → 0.5.4
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/README.md +52 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1219 -234
- package/dist-app/assets/index-CgJZeTDV.js +1079 -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,171 @@
|
|
|
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
|
+
:root {
|
|
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
62
|
:root {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
/* THE STAFF BOARD — a master-key rack, not a dashboard.
|
|
64
|
+
Structure comes from two real artefacts: a master-key hierarchy (passe
|
|
65
|
+
général → passe partiel → cylinder, which is literally how a hotel is
|
|
66
|
+
keyed, and exactly how authority cascades here) and a ministry organigram
|
|
67
|
+
(rank is NAMED in column headers, never inferred from indentation).
|
|
68
|
+
Material comes from the approved FHQ hero designs — wine-black ground,
|
|
69
|
+
brass rails, real vendor marks on white chips.
|
|
70
|
+
Raw values are generated into app/tokens.css from design/tokens.json
|
|
71
|
+
(scripts/gen-tokens.mjs); these are the panel's semantic aliases over them.
|
|
72
|
+
RANK READS THROUGH FINISH, NEVER HUE. The old board coloured each card by
|
|
73
|
+
hash(agent.id) — nine departments, nine arbitrary neon hues — which is the
|
|
74
|
+
loudest "generated by a dev tool" signal there is, and it disagreed with
|
|
75
|
+
the cosmos's own gold/silver/bronze ladder. */
|
|
76
|
+
/* LIGHT IS THE DEFAULT (Jesse, 2026-07-19: "design for everyone — a white
|
|
77
|
+
background, very light bubbles, beautiful refined display"). A whole hotel
|
|
78
|
+
uses this, not a room of developers; a stranger should not land in a
|
|
79
|
+
terminal. Dark remains, one toggle away, below.
|
|
80
|
+
The ground is a WARM white, never flat #fff — a lit room, not a sheet of
|
|
81
|
+
paper. Depth comes from layered shadow, so ink stays the only dark thing
|
|
82
|
+
on screen and brass drops from material to ~5% accent. */
|
|
83
|
+
--bg: #fcfbf9;
|
|
84
|
+
--bg-1: #fffefb; /* tinted toward the brass hue, never flat #fff */
|
|
85
|
+
--bg-2: #f6f4f0;
|
|
86
|
+
--line: #e9e5df;
|
|
87
|
+
--line-soft: #f0ece6;
|
|
88
|
+
--line-2: #d9d3c9;
|
|
89
|
+
--surface-tray: rgba(255, 255, 255, .5);
|
|
90
|
+
/* the ground-plane dot. White-on-white rendered NOTHING on the light board
|
|
91
|
+
— the texture was there in the stylesheet and absent on screen. */
|
|
92
|
+
--dot: rgba(27, 24, 21, 0.055);
|
|
93
|
+
--surface-lift: linear-gradient(180deg, #fffdf8, #ffffff);
|
|
94
|
+
--surface-lift-edge: #e8d9bd;
|
|
95
|
+
--ink: #1b1815;
|
|
96
|
+
--ink-2: #6b6459;
|
|
97
|
+
--ink-3: #9a9287;
|
|
98
|
+
--accent: #a9753a;
|
|
99
|
+
--accent-2: #c9954f;
|
|
100
|
+
--accent-dim: #f7f0e4;
|
|
101
|
+
--warn: #b8862c;
|
|
102
|
+
--warn-wash: #fbf3e3;
|
|
103
|
+
--ok: #3f9c6d;
|
|
104
|
+
--ok-wash: #eaf5ef;
|
|
105
|
+
--fail: #c0553f;
|
|
106
|
+
--fail-wash: #fbeeea;
|
|
107
|
+
/* one shadow set, three depths — the only source of elevation */
|
|
108
|
+
--sh-1: 0 1px 2px rgba(27, 24, 21, .04), 0 1px 1px rgba(27, 24, 21, .03);
|
|
109
|
+
--sh-2: 0 2px 4px rgba(27, 24, 21, .04), 0 6px 16px -6px rgba(27, 24, 21, .10);
|
|
110
|
+
--sh-3: 0 4px 8px rgba(27, 24, 21, .05), 0 16px 34px -12px rgba(27, 24, 21, .16);
|
|
111
|
+
/* motion — slight, never showy. Everything eases on these two. */
|
|
112
|
+
--ease: cubic-bezier(.2, .7, .25, 1);
|
|
113
|
+
--settle: cubic-bezier(.16, 1, .3, 1);
|
|
114
|
+
--font: "DM Sans", -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
|
|
115
|
+
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
116
|
+
--display: "Fraunces", ui-serif, Georgia, serif;
|
|
117
|
+
--brass-line: linear-gradient(180deg, var(--brass-hi), var(--brass) 55%, var(--brass-deep));
|
|
25
118
|
}
|
|
26
119
|
|
|
27
|
-
/*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
--bg
|
|
31
|
-
--bg-
|
|
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
|
-
[data-theme="
|
|
57
|
-
[data-theme="
|
|
120
|
+
/* DARK — the wine-and-brass board, now one toggle away instead of the default.
|
|
121
|
+
Same structure, same shadows-as-depth logic, inverted ground. */
|
|
122
|
+
:root[data-theme="dark"] {
|
|
123
|
+
--bg: var(--wine);
|
|
124
|
+
--bg-1: var(--plate);
|
|
125
|
+
--bg-2: var(--wine2);
|
|
126
|
+
--line: rgba(244, 238, 226, 0.13);
|
|
127
|
+
--line-soft: rgba(244, 238, 226, 0.08);
|
|
128
|
+
--line-2: rgba(244, 238, 226, 0.22);
|
|
129
|
+
--surface-tray: rgba(11, 5, 9, .4);
|
|
130
|
+
--dot: rgba(255, 255, 255, 0.035);
|
|
131
|
+
--surface-lift: linear-gradient(168deg, #3a2a16, #1d1410);
|
|
132
|
+
--surface-lift-edge: rgba(239, 195, 137, .4);
|
|
133
|
+
--ink: var(--cream);
|
|
134
|
+
--ink-2: var(--cream-dim);
|
|
135
|
+
--ink-3: var(--cream-faint);
|
|
136
|
+
--accent: var(--brass);
|
|
137
|
+
--accent-2: var(--brass-hi);
|
|
138
|
+
--accent-dim: rgba(217, 160, 91, 0.12);
|
|
139
|
+
--warn: var(--brass);
|
|
140
|
+
--warn-wash: rgba(217, 160, 91, 0.10);
|
|
141
|
+
--ok: var(--mint);
|
|
142
|
+
--ok-wash: rgba(139, 233, 176, 0.10);
|
|
143
|
+
--fail: #e07b6a;
|
|
144
|
+
--fail-wash: rgba(224, 123, 106, 0.10);
|
|
145
|
+
--sh-1: 0 1px 2px rgba(0, 0, 0, .5);
|
|
146
|
+
--sh-2: 0 2px 4px rgba(0, 0, 0, .5), 0 16px 34px -18px rgba(0, 0, 0, .85);
|
|
147
|
+
--sh-3: 0 4px 8px rgba(0, 0, 0, .5), 0 22px 48px -18px rgba(0, 0, 0, .9);
|
|
148
|
+
}
|
|
149
|
+
[data-theme="dark"] body { background: var(--wine); }
|
|
150
|
+
[data-theme="dark"] .ag { background: linear-gradient(168deg, var(--plate-lift), #180e16); }
|
|
151
|
+
[data-theme="dark"] .bar, [data-theme="dark"] .tabs { background: rgba(11, 5, 9, 0.72); }
|
|
152
|
+
[data-theme="dark"] .rack .wt { background: #fff; border-color: transparent; }
|
|
153
|
+
[data-theme="dark"] .pnl-stars { opacity: .45; }
|
|
58
154
|
|
|
59
155
|
* { box-sizing: border-box; }
|
|
60
156
|
body { font-family: var(--font); color: var(--ink); background: var(--bg); overflow-x: hidden; }
|
|
61
157
|
|
|
62
158
|
.pnl { display: flex; flex-direction: column; position: relative; overflow: hidden; }
|
|
63
159
|
|
|
64
|
-
/* the
|
|
160
|
+
/* the ground — warm wine light pooling from the top-left, exactly as the hero
|
|
161
|
+
grades it. NO GRAIN/NOISE LAYER: the mockup carried a feTurbulence overlay
|
|
162
|
+
and it read as a blurry filter smeared over everything (Jesse, 2026-07-19).
|
|
163
|
+
The atmosphere is gradient + constellation only. */
|
|
65
164
|
.pnl-aurora {
|
|
66
|
-
position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity:
|
|
165
|
+
position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity: 1;
|
|
67
166
|
background:
|
|
68
|
-
radial-gradient(
|
|
69
|
-
radial-gradient(
|
|
70
|
-
radial-gradient(24% 22% at 55% 15%, rgba(79, 184, 160, 0.06), transparent 70%);
|
|
167
|
+
radial-gradient(55% 45% at 12% 0%, rgba(201, 149, 79, .055), transparent 62%),
|
|
168
|
+
radial-gradient(45% 40% at 95% 100%, rgba(63, 156, 109, .035), transparent 62%);
|
|
71
169
|
animation: aurora 48s ease-in-out infinite alternate;
|
|
72
170
|
}
|
|
73
171
|
@keyframes aurora {
|
|
@@ -87,12 +185,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
87
185
|
/* top bar */
|
|
88
186
|
.bar {
|
|
89
187
|
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(
|
|
188
|
+
padding: 14px 26px; border-bottom: 1px solid var(--line-soft);
|
|
189
|
+
background: rgba(252, 251, 249, 0.82); backdrop-filter: blur(10px);
|
|
92
190
|
flex-wrap: wrap;
|
|
93
191
|
}
|
|
94
|
-
.bar-brand { font-size:
|
|
95
|
-
.bar-brand b { font-weight:
|
|
192
|
+
.bar-brand { font-size: 19px; font-family: var(--display); font-weight: 600; letter-spacing: -0.01em; }
|
|
193
|
+
.bar-brand b { font-weight: 600; color: var(--accent-2); }
|
|
96
194
|
.bar-stats { display: flex; gap: 18px; color: var(--ink-2); font-size: 12.5px; }
|
|
97
195
|
.bar-stats b { color: var(--ink); font-variant-numeric: tabular-nums; font-size: 14px; }
|
|
98
196
|
.bar-stats .tap { cursor: pointer; transition: color 0.15s; }
|
|
@@ -106,22 +204,23 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
106
204
|
.btn {
|
|
107
205
|
border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
|
|
108
206
|
border-radius: 9px; padding: 8px 15px; font-size: 12.5px; cursor: pointer;
|
|
109
|
-
transition: border-color
|
|
207
|
+
transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
|
|
110
208
|
}
|
|
111
209
|
.btn:hover { border-color: var(--accent); }
|
|
112
210
|
.btn:active { transform: scale(0.97); }
|
|
113
|
-
.btn.primary { background: var(--accent); border-color: var(--accent); color: #
|
|
211
|
+
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 650; }
|
|
114
212
|
.btn.primary:disabled { opacity: 0.5; cursor: wait; }
|
|
115
213
|
.btn.ghost { background: transparent; color: var(--ink-2); }
|
|
116
214
|
|
|
117
215
|
/* tabs */
|
|
118
216
|
.tabs {
|
|
119
217
|
position: relative; z-index: 2; display: flex; gap: 4px; padding: 0 22px;
|
|
120
|
-
border-bottom: 1px solid var(--line); background: rgba(
|
|
218
|
+
border-bottom: 1px solid var(--line); background: rgba(252, 251, 249, 0.7); backdrop-filter: blur(8px);
|
|
121
219
|
overflow-x: auto;
|
|
122
220
|
}
|
|
123
221
|
.tab {
|
|
124
222
|
background: none; border: none; border-bottom: 2px solid transparent; color: var(--ink-2);
|
|
223
|
+
transition: color .18s var(--ease), border-color .22s var(--ease);
|
|
125
224
|
font-size: 12.5px; padding: 12px 15px 11px; cursor: pointer; white-space: nowrap;
|
|
126
225
|
transition: color 0.15s, border-color 0.2s;
|
|
127
226
|
}
|
|
@@ -197,7 +296,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
197
296
|
|
|
198
297
|
/* rules */
|
|
199
298
|
.rule-list { display: flex; flex-direction: column; gap: 9px; }
|
|
200
|
-
.rule-card { background: var(--bg-1); border: 1px solid var(--line); border-
|
|
299
|
+
.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
300
|
.rule-card:hover { border-color: var(--accent); }
|
|
202
301
|
.rule-top { display: flex; align-items: baseline; gap: 10px; margin-bottom: 5px; }
|
|
203
302
|
.rule-ref { font-family: var(--mono); font-size: 12.5px; }
|
|
@@ -229,19 +328,17 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
229
328
|
/* the machine rack — a recessed slot flush under the owner's card */
|
|
230
329
|
.oc-tray {
|
|
231
330
|
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);
|
|
331
|
+
max-width: 210px; margin-top: -4px; padding: 10px 8px 7px;
|
|
332
|
+
background: transparent; border: 0; padding: 8px 0 0;
|
|
236
333
|
}
|
|
237
|
-
.oc-tray.fail { border-color: rgba(224, 123, 106, 0.
|
|
238
|
-
.oc-tray.warn { border-color: rgba(
|
|
334
|
+
.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); }
|
|
335
|
+
.oc-tray.warn { border-color: rgba(217, 160, 91, 0.42); }
|
|
239
336
|
.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
|
|
337
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
338
|
+
background: var(--bg-2); border: 1px solid var(--line);
|
|
339
|
+
border-radius: 8px; padding: 5px 9px; cursor: pointer;
|
|
340
|
+
font-family: var(--mono); font-size: 9.5px; color: var(--ink-2);
|
|
341
|
+
transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
|
|
245
342
|
}
|
|
246
343
|
.oc-mac:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
|
|
247
344
|
.oc-mac.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--ink); }
|
|
@@ -249,6 +346,21 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
249
346
|
.oc-mac.dragging { opacity: 0.45; cursor: grabbing; }
|
|
250
347
|
.oc-mac.more:hover { color: var(--accent); }
|
|
251
348
|
.mac-emoji { font-size: 11px; }
|
|
349
|
+
/* real vendor marks, tinted to the house so twelve logos read as one system.
|
|
350
|
+
Full brand colour on hover — recognition when you want it, calm when you
|
|
351
|
+
don't. */
|
|
352
|
+
/* the hero's treatment: a real vendor mark on a WHITE chip. Twelve tinted
|
|
353
|
+
monochrome logos read as a sticker album; twelve white chips read as a rack
|
|
354
|
+
of real systems — and it is the one thing on the old board that already
|
|
355
|
+
looked genuine, so everything else was raised to it rather than it lowered. */
|
|
356
|
+
.mac-brand {
|
|
357
|
+
width: 24px; height: 24px; flex: 0 0 24px; display: block;
|
|
358
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 7px;
|
|
359
|
+
padding: 4px; box-sizing: border-box;
|
|
360
|
+
transition: transform .2s ease, box-shadow .2s ease;
|
|
361
|
+
}
|
|
362
|
+
.oc-mac:hover .mac-brand,
|
|
363
|
+
.oc-mac.sel .mac-brand { transform: scale(1.06); box-shadow: 0 5px 14px -5px rgba(0, 0, 0, 0.85); }
|
|
252
364
|
.mac-name { white-space: nowrap; max-width: 76px; overflow: hidden; text-overflow: ellipsis; }
|
|
253
365
|
.mac-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; display: inline-block; }
|
|
254
366
|
.mac-dot.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
|
|
@@ -285,10 +397,35 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
285
397
|
.bk-agents.unassigned { color: var(--ink-3); font-style: italic; }
|
|
286
398
|
|
|
287
399
|
.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:
|
|
400
|
+
position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
|
|
401
|
+
padding: 10px 26px; border-bottom: 1px solid var(--line); background: var(--warn-wash);
|
|
402
|
+
}
|
|
403
|
+
.pending-lead {
|
|
404
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
|
|
405
|
+
color: var(--warn); margin-right: 2px;
|
|
406
|
+
}
|
|
407
|
+
/* every pending change is its own undo button. A global discard makes one
|
|
408
|
+
mis-drop cost every other edit, so people stop dragging entirely. */
|
|
409
|
+
.pending-item {
|
|
410
|
+
display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
|
|
411
|
+
font-size: 11.5px; font-family: var(--mono); color: var(--ink-2);
|
|
412
|
+
background: var(--bg-1); border: 1px solid var(--line); border-radius: 100px;
|
|
413
|
+
padding: 4px 6px 4px 12px;
|
|
414
|
+
transition: border-color .18s var(--ease), color .18s ease, transform .16s var(--ease);
|
|
415
|
+
}
|
|
416
|
+
.pending-item i {
|
|
417
|
+
font-style: normal; font-size: 8.5px; letter-spacing: .12em; text-transform: uppercase;
|
|
418
|
+
color: var(--warn); border: 1px solid rgba(184,134,44,.3); border-radius: 100px; padding: 2px 7px;
|
|
419
|
+
transition: background .18s ease, color .18s ease;
|
|
420
|
+
}
|
|
421
|
+
.pending-item:hover { border-color: var(--warn); color: var(--ink); transform: translateY(-1px); }
|
|
422
|
+
.pending-item:hover i { background: var(--warn); color: var(--bg-1); }
|
|
423
|
+
|
|
424
|
+
/* the lane lights up as a drop target, so a near-miss is impossible */
|
|
425
|
+
.oc-row.lanes > .oc-child.lane-over {
|
|
426
|
+
border-color: var(--accent); background: var(--accent-dim);
|
|
427
|
+
box-shadow: 0 0 0 3px var(--accent-dim), var(--sh-2);
|
|
290
428
|
}
|
|
291
|
-
.pending-item { font-size: 11.5px; color: var(--warn); font-family: var(--mono); }
|
|
292
429
|
.pnl-err { position: relative; z-index: 2; padding: 10px 26px; color: #e07b6a; font-size: 12.5px; }
|
|
293
430
|
|
|
294
431
|
/* layout — 100% so a HOST container decides the height (embedded panel);
|
|
@@ -301,17 +438,59 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
301
438
|
+ a verdigris pool anchoring the eye at the root. Static — capture-safe. */
|
|
302
439
|
background:
|
|
303
440
|
radial-gradient(900px 420px at 50% 120px, rgba(79, 184, 160, 0.05), transparent 70%),
|
|
304
|
-
radial-gradient(circle at 1px 1px,
|
|
441
|
+
radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 1.5px);
|
|
305
442
|
background-size: auto, 28px 28px;
|
|
306
443
|
}
|
|
307
|
-
.tree-hint { color: var(--ink-3); font-size:
|
|
444
|
+
.tree-hint { color: var(--ink-3); font-family: var(--mono); font-size: 10px; letter-spacing: .05em; margin: 0 0 14px; text-align: center; }
|
|
445
|
+
|
|
446
|
+
/* ── RANK HEADERS (from the ministry organigram) ───────────────────────────
|
|
447
|
+
Rank is NAMED, never inferred from indentation. Roman numerals + a rule
|
|
448
|
+
under each, spanning the columns the cascade actually occupies. */
|
|
449
|
+
/* four headers over the four real cascade columns: law · gm · departments ·
|
|
450
|
+
staff. Widths mirror .cascade so a header sits over the column it names —
|
|
451
|
+
headers labelling columns that do not exist is worse than no headers. */
|
|
452
|
+
.ranks {
|
|
453
|
+
display: grid;
|
|
454
|
+
grid-template-columns: 232px var(--rail-w, 56px) 268px var(--rail-w, 56px) minmax(232px, 300px) var(--rail-w, 56px) 1fr;
|
|
455
|
+
margin: 0 0 16px; padding: 0 16px; flex: 0 0 auto;
|
|
456
|
+
}
|
|
457
|
+
.rank:nth-child(1) { grid-column: 1; }
|
|
458
|
+
.rank:nth-child(2) { grid-column: 3; }
|
|
459
|
+
.rank:nth-child(3) { grid-column: 5; }
|
|
460
|
+
.rank:nth-child(4) { grid-column: 7; }
|
|
461
|
+
.rank {
|
|
462
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: .2em; text-transform: uppercase;
|
|
463
|
+
color: var(--ink-3); padding-bottom: 8px; border-bottom: 1px solid rgba(217, 160, 91, 0.16);
|
|
464
|
+
position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
465
|
+
}
|
|
466
|
+
.rank b { color: var(--brass); font-weight: 500; margin-right: 7px; }
|
|
467
|
+
.rank::after { content: ""; position: absolute; left: 0; bottom: -1px; width: 24px; height: 1px; background: var(--brass); }
|
|
468
|
+
|
|
469
|
+
/* the constellation — the hero's own atmosphere, drawn once behind the rack.
|
|
470
|
+
Gradient + points only; NO grain/noise overlay (it read as a blurry filter
|
|
471
|
+
smeared across the board). */
|
|
472
|
+
.pnl-stars { position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0; width: 100%; height: 100%; }
|
|
308
473
|
|
|
309
474
|
/* THE COMPANY CHART — root on top, branches fanning beneath. Connectors are
|
|
310
475
|
crafted: curved elbows at the row's ends, branch-hued drops, a glowing
|
|
311
476
|
junction node where the parent's line meets the bar. Pure CSS. */
|
|
312
477
|
/* the measuring viewport — flex:1 + min-height:0 lets it consume the height
|
|
313
478
|
.tree gives it; align/justify center the scaled block for narrow orgs. */
|
|
314
|
-
|
|
479
|
+
/* Always scrollable. It used to be overflow:hidden unless the user manually
|
|
480
|
+
zoomed — safe only while auto-fit guaranteed the whole board fit one screen.
|
|
481
|
+
A lane board grows downward, so that assumption sealed every lane below the
|
|
482
|
+
fold behind an invisible wall. */
|
|
483
|
+
.chart-fit { position: relative; flex: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: center; overflow: auto; }
|
|
484
|
+
/* transform:scale() does not change layout size, so the scaled block gets a
|
|
485
|
+
spacer sized to its VISUAL footprint — otherwise the scroll extent is the
|
|
486
|
+
unscaled height and you scroll into empty space. */
|
|
487
|
+
/* overflow:hidden clips the UNSCALED layout box of the absolutely-positioned
|
|
488
|
+
chart, which otherwise adds its full natural height to the scroll extent —
|
|
489
|
+
~900px of empty scroll past the last lane. The visual (scaled) content is
|
|
490
|
+
exactly the spacer's size, so nothing real is clipped. */
|
|
491
|
+
.chart-scaled { position: relative; flex: 0 0 auto; overflow: hidden; }
|
|
492
|
+
.chart-scaled > .chart { position: absolute; top: 0; left: 50%; transform-origin: top center; }
|
|
493
|
+
.chart-fit.zoomed .chart-scaled > .chart { left: 0; transform-origin: top left; }
|
|
315
494
|
/* user-zoomed: native scroll is the pan; top-left origin keeps the scaled
|
|
316
495
|
overflow fully reachable (top/center origin would clip the left half). */
|
|
317
496
|
.chart-fit.zoomed { overflow: auto; justify-content: flex-start; }
|
|
@@ -329,35 +508,252 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
329
508
|
.zoomer button:hover { background: var(--accent-dim); color: var(--ink); }
|
|
330
509
|
.zoomer button.on { color: var(--accent); }
|
|
331
510
|
.zoomer-pct { min-width: 40px; text-align: center; color: var(--ink-3); }
|
|
511
|
+
.zoomer-sep { width: 1px; height: 16px; background: var(--line); margin: 0 2px; }
|
|
512
|
+
|
|
513
|
+
/* ── the ledger shelf: every bucket as a pigeonhole, hover a role to see its
|
|
514
|
+
reach. Docked under the chart, always visible — the memory half of the
|
|
515
|
+
two pillars (structure above, memory below), never a separate tab. */
|
|
516
|
+
.ledger-shelf {
|
|
517
|
+
position: relative; z-index: 2; flex: 0 0 auto; margin-top: 14px;
|
|
518
|
+
padding: 10px 16px 12px; border-radius: 10px;
|
|
519
|
+
border: 1px solid var(--line); background: var(--bg-1); box-shadow: var(--sh-2);
|
|
520
|
+
width: min(1180px, 92vw);
|
|
521
|
+
}
|
|
522
|
+
.shelf-label {
|
|
523
|
+
display: block; font-family: var(--mono); font-size: 9px; letter-spacing: 0.14em;
|
|
524
|
+
text-transform: uppercase; color: var(--brass); margin-bottom: 8px;
|
|
525
|
+
}
|
|
526
|
+
.shelf-slots { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
527
|
+
.shelf-slot {
|
|
528
|
+
font-family: var(--mono); font-size: 10px; color: var(--ink-3);
|
|
529
|
+
background: var(--bg-2); border: 1px solid var(--line); border-radius: 5px;
|
|
530
|
+
padding: 4px 9px; transition: border-color .18s ease, color .18s ease, box-shadow .25s ease, transform .18s ease;
|
|
531
|
+
}
|
|
532
|
+
.shelf-slot.lit {
|
|
533
|
+
color: var(--brass-hi); border-color: var(--brass); transform: translateY(-1px);
|
|
534
|
+
box-shadow: 0 0 0 1px rgba(201, 160, 74, 0.3), 0 6px 16px -6px rgba(201, 160, 74, 0.5);
|
|
535
|
+
background: rgba(201, 160, 74, 0.1);
|
|
536
|
+
}
|
|
537
|
+
.shelf-slot.sealed { color: var(--ink-3); font-style: italic; border-style: dashed; opacity: 0.75; }
|
|
538
|
+
.shelf-slot.sealed.lit { color: var(--ink-3); border-color: var(--line); background: var(--bg-2); box-shadow: none; transform: none; }
|
|
539
|
+
@media (prefers-reduced-motion: reduce) { .shelf-slot { transition: none; } }
|
|
540
|
+
|
|
541
|
+
/* the law toggle in the top bar */
|
|
542
|
+
.law-toggle { font-family: var(--mono); letter-spacing: 0.02em; }
|
|
543
|
+
.law-toggle.on { color: var(--brass-hi); border-color: var(--brass); background: rgba(201, 160, 74, 0.12); }
|
|
332
544
|
/* min-width:max-content keeps connectors geometrically correct at natural size
|
|
333
545
|
BEFORE scaling; the measured --fit shrinks the whole block toward its head. */
|
|
546
|
+
/* transform is set inline by Panel.tsx (see the note there) — only origin,
|
|
547
|
+
sizing and the easing live here. */
|
|
334
548
|
.chart {
|
|
335
549
|
min-width: max-content; padding: 8px 14px 20px;
|
|
336
|
-
transform:
|
|
550
|
+
transform-origin: top center; position: relative; /* the rail plane's origin */
|
|
337
551
|
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); will-change: transform;
|
|
338
552
|
}
|
|
553
|
+
/* plates sit above the rail plane */
|
|
554
|
+
.chart .cascade { position: relative; z-index: 1; }
|
|
555
|
+
|
|
556
|
+
/* rank I — the law. Brass-edged like the GM but unfilled: it is a document,
|
|
557
|
+
not an operator. */
|
|
558
|
+
.law-plate { width: 232px; cursor: pointer; }
|
|
559
|
+
.law-plate .ag-ava { font-family: var(--display); color: var(--accent); }
|
|
560
|
+
.law-plate .ag-name { color: var(--accent); font-size: 13px; }
|
|
561
|
+
.law-plate .ag-role { word-break: break-all; }
|
|
562
|
+
/* ── THE CASCADE (master-key structure) ───────────────────────────────────
|
|
563
|
+
Rank runs LEFT TO RIGHT, one column each, exactly as a master-key chart
|
|
564
|
+
reads: THE STANDARD → GM → departments → staff. Two nested grids give the
|
|
565
|
+
four columns; the brass elbows between them are drawn by CascadeRails as
|
|
566
|
+
one measured SVG (orthogonal geometry between arbitrary boxes is not a
|
|
567
|
+
thing CSS pseudo-elements can do honestly). */
|
|
568
|
+
.cascade {
|
|
569
|
+
display: grid;
|
|
570
|
+
grid-template-columns: max-content var(--rail-w, 56px) 1fr;
|
|
571
|
+
/* START, not centre. Centring is right for a 3-branch key chart; with nine
|
|
572
|
+
departments the stack is ~2,200px tall, so it buried the master key and the
|
|
573
|
+
GM below the fold — the two plates that must be seen first. Top-aligned
|
|
574
|
+
also matches the ministry chart's reading order. */
|
|
575
|
+
align-items: start;
|
|
576
|
+
}
|
|
577
|
+
.cascade > .law-plate { grid-column: 1; }
|
|
578
|
+
.cascade > .ocn { grid-column: 3; }
|
|
579
|
+
|
|
580
|
+
/* rank II–IV live inside the root .ocn, which is itself a cascade */
|
|
581
|
+
.cascade > .ocn {
|
|
582
|
+
display: grid;
|
|
583
|
+
/* the lanes column is BOUNDED so staff wrap inside their lane. Left at 1fr
|
|
584
|
+
under .chart's max-content sizing it grew to fit every staff plate on one
|
|
585
|
+
row — 2,400px natural width, which fit-to-width then scaled to 52% and made
|
|
586
|
+
every plate unreadable. Rank IV wraps; the board stays legible. */
|
|
587
|
+
grid-template-columns: max-content var(--rail-w, 56px) minmax(420px, 940px);
|
|
588
|
+
grid-template-areas: "gm rail lanes";
|
|
589
|
+
align-items: start;
|
|
590
|
+
}
|
|
591
|
+
.cascade > .ocn > .casc-head { grid-area: gm; display: flex; flex-direction: column; align-items: stretch; }
|
|
592
|
+
.cascade > .ocn > .oc-down { grid-area: rail; }
|
|
593
|
+
.cascade > .ocn > .oc-row.lanes { grid-area: lanes; }
|
|
594
|
+
|
|
595
|
+
/* the SVG rail plane — sits under the plates, never intercepts a click */
|
|
596
|
+
.rails { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: visible; width: 100%; height: 100%; }
|
|
597
|
+
|
|
339
598
|
.ocn { display: flex; flex-direction: column; align-items: center; }
|
|
340
|
-
|
|
341
|
-
|
|
599
|
+
/* the connective tissue reads brass everywhere — a company chart drawn with
|
|
600
|
+
the house's own material, not a generic diagram line. */
|
|
601
|
+
.oc-down { width: 2px; height: 18px; position: relative; background: var(--brass-line); opacity: 0.75; }
|
|
342
602
|
.oc-down::after { content: ""; position: absolute; bottom: -2px; left: 50%;
|
|
343
603
|
width: 5px; height: 5px; transform: translateX(-50%); border-radius: 50%;
|
|
344
|
-
background: var(--
|
|
604
|
+
background: var(--brass-hi); box-shadow: 0 0 8px rgba(201, 160, 74, 0.55); }
|
|
345
605
|
.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
606
|
|
|
357
|
-
/*
|
|
358
|
-
|
|
359
|
-
.
|
|
360
|
-
|
|
607
|
+
/* ── THE STAFF BOARD: departments as lanes, stacking DOWNWARD ──────────────
|
|
608
|
+
Nine departments fanned sideways forced a canvas nobody could read at 17%
|
|
609
|
+
zoom. Each lane is a row of the board: a brass rail down the left, the
|
|
610
|
+
head's card, then its people flowing right and wrapping inside the lane.
|
|
611
|
+
The board scrolls like a document; every card stays at legible size. */
|
|
612
|
+
/* the lanes are rank III+IV: one shelf per department, stacked. Width now comes
|
|
613
|
+
from the cascade's 1fr column, not a hardcoded viewport clamp. */
|
|
614
|
+
.oc-row.lanes {
|
|
615
|
+
flex-direction: column;
|
|
616
|
+
align-items: stretch;
|
|
617
|
+
gap: 14px;
|
|
618
|
+
padding-top: 0;
|
|
619
|
+
width: 100%;
|
|
620
|
+
}
|
|
621
|
+
/* a lane is a SHELF on the rack — a hairline tray, not a coloured panel. The
|
|
622
|
+
old lane had a hash-hue left border and tint per department; the rack is one
|
|
623
|
+
material throughout and the department is named, not coloured. */
|
|
624
|
+
.oc-row.lanes > .oc-child {
|
|
625
|
+
flex: 1 1 auto;
|
|
626
|
+
position: relative;
|
|
627
|
+
border: 1px solid var(--line);
|
|
628
|
+
border-radius: 18px;
|
|
629
|
+
background: var(--surface-tray);
|
|
630
|
+
padding: 16px 18px 18px;
|
|
631
|
+
transition: border-color .25s ease, box-shadow .35s var(--ease);
|
|
632
|
+
}
|
|
633
|
+
.oc-row.lanes > .oc-child:hover { border-color: var(--line-2, #d9d3c9); box-shadow: var(--sh-2); }
|
|
634
|
+
@media (prefers-reduced-motion: reduce) {
|
|
635
|
+
.oc-row.lanes > .oc-child:hover { transform: none; }
|
|
636
|
+
}
|
|
637
|
+
/* lanes own their connectors — kill the fan's drop-lines inside them */
|
|
638
|
+
.oc-row.lanes > .oc-child::before,
|
|
639
|
+
.oc-row.lanes > .oc-child::after { display: none; }
|
|
640
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
|
|
641
|
+
|
|
642
|
+
/* inside a lane: head card top-left, its service tray beneath the head, a
|
|
643
|
+
brass rail column, its people filling the right. Explicit areas — an
|
|
644
|
+
implicit 2-column grid put the tray in the staff slot the moment every
|
|
645
|
+
department gained services, which shunted the people into a one-card
|
|
646
|
+
column. The rail column is the elbow HANDOFF.md flagged as hidden: the
|
|
647
|
+
department head commanding its people, drawn once, orthogonally. */
|
|
648
|
+
.oc-row.lanes > .oc-child > .ocn {
|
|
649
|
+
display: grid;
|
|
650
|
+
grid-template-columns: minmax(278px, 300px) var(--rail-w, 56px) 1fr;
|
|
651
|
+
grid-template-areas:
|
|
652
|
+
"head rail staff"
|
|
653
|
+
"macs rail staff";
|
|
654
|
+
grid-template-rows: auto 1fr;
|
|
655
|
+
align-items: start;
|
|
656
|
+
gap: 10px 0;
|
|
657
|
+
}
|
|
658
|
+
.oc-row.lanes > .oc-child > .ocn > .ag { grid-area: head; }
|
|
659
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray { grid-area: macs; }
|
|
660
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row { grid-area: staff; margin-left: 4px; }
|
|
661
|
+
|
|
662
|
+
/* the rail: a vertical brass spine for the department, capped where it meets
|
|
663
|
+
the head, with one orthogonal branch reaching into the staff block. This
|
|
664
|
+
is the ".oc-down" element — hidden below (line ~490 in the old cascade),
|
|
665
|
+
here it's re-purposed as the rail instead of the plain tree drop-line. */
|
|
666
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-down {
|
|
667
|
+
display: block; grid-area: rail; position: relative; width: 100%; height: 100%;
|
|
668
|
+
min-height: 44px;
|
|
669
|
+
/* the base .oc-down is a 2px-wide FILLED brass drop-line. Widening it to the
|
|
670
|
+
rail column without clearing its background painted a solid brass slab down
|
|
671
|
+
every lane — the rail is drawn by ::before/::after, not by the box itself. */
|
|
672
|
+
background: none;
|
|
673
|
+
}
|
|
674
|
+
/* No ::before/::after brass here any more — the SVG rail plane draws every
|
|
675
|
+
connector from measured box positions. Leaving these would double each rail. */
|
|
676
|
+
|
|
677
|
+
/* semantic zoom lever: the rail itself is the fold/unfold control at the
|
|
678
|
+
department level — house → department → role, one click at a time. */
|
|
679
|
+
.oc-down.foldable { cursor: pointer; }
|
|
680
|
+
.oc-down.foldable:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; border-radius: 4px; }
|
|
681
|
+
.oc-fold-glyph {
|
|
682
|
+
position: absolute; left: 50%; top: 22px; transform: translate(-50%, -50%);
|
|
683
|
+
font-style: normal; font-size: 10px; line-height: 1; width: 15px; height: 15px;
|
|
684
|
+
display: grid; place-items: center; border-radius: 50%;
|
|
685
|
+
background: var(--bg-1); border: 1px solid var(--line-2); color: var(--accent);
|
|
686
|
+
transition: transform .15s ease, background .15s ease;
|
|
687
|
+
}
|
|
688
|
+
.oc-down.foldable:hover .oc-fold-glyph { background: rgba(201, 160, 74, 0.18); transform: translate(-50%, -50%) scale(1.15); }
|
|
689
|
+
.oc-down.folded .oc-fold-glyph { color: var(--ink-2); border-color: var(--line); }
|
|
690
|
+
|
|
691
|
+
/* the folded department: head visible, staff summarised to one line — the
|
|
692
|
+
"department" rung of house → department → role. */
|
|
693
|
+
.oc-folded-summary {
|
|
694
|
+
grid-area: staff; align-self: center; justify-self: start; text-align: left;
|
|
695
|
+
background: none; border: 1px dashed var(--line); border-radius: 8px;
|
|
696
|
+
padding: 9px 13px; margin-left: 4px; cursor: pointer;
|
|
697
|
+
font-family: var(--mono); font-size: 11px; color: var(--ink-3);
|
|
698
|
+
transition: border-color .15s ease, color .15s ease;
|
|
699
|
+
}
|
|
700
|
+
.oc-folded-summary:hover { border-color: var(--brass); color: var(--brass-hi); }
|
|
701
|
+
/* "show the law": the SAME rails, re-read as authority. The SVG plane swaps to
|
|
702
|
+
a travelling dashed brass so the law is seen flowing down the cascade. */
|
|
703
|
+
.rail-line { stroke: var(--line-2, #d9d3c9); stroke-width: 1.5; stroke-linecap: round; fill: none; }
|
|
704
|
+
.rail-dash { stroke: var(--accent-2); stroke-width: 1; fill: none; stroke-dasharray: 2 9; opacity: 0; }
|
|
705
|
+
.law-on .rail-line { stroke: var(--brass-hi); stroke-width: 1.9; opacity: .9; }
|
|
706
|
+
.law-on .rail-dash { opacity: .95; animation: rail-flow 700ms linear infinite; }
|
|
707
|
+
@keyframes rail-flow { to { stroke-dashoffset: -11; } }
|
|
708
|
+
@media (prefers-reduced-motion: reduce) { .law-on .rail-dash { animation: none; } }
|
|
709
|
+
|
|
710
|
+
/* the tray is the department's stack of real services — one per line, so the
|
|
711
|
+
vendor marks read as a list of systems rather than a scatter of chips */
|
|
712
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray {
|
|
713
|
+
display: flex; flex-direction: column; align-items: stretch;
|
|
714
|
+
gap: 4px; margin: 0; padding: 0; max-width: 232px;
|
|
715
|
+
}
|
|
716
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac { justify-content: flex-start; width: 100%; }
|
|
717
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac .mac-name { max-width: none; }
|
|
718
|
+
|
|
719
|
+
@media (max-width: 820px) {
|
|
720
|
+
.oc-row.lanes > .oc-child > .ocn {
|
|
721
|
+
grid-template-columns: 1fr;
|
|
722
|
+
grid-template-areas: "head" "macs" "staff";
|
|
723
|
+
}
|
|
724
|
+
/* no rail column on a single-column stack — the rail has nothing to span */
|
|
725
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
|
|
726
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-tray { max-width: none; flex-direction: row; flex-wrap: wrap; }
|
|
727
|
+
}
|
|
728
|
+
/* inside a lane the people always flow right and wrap — the ".wrap" grid
|
|
729
|
+
(which grows downward) would otherwise stack a big department into a
|
|
730
|
+
one-card-wide column. Specificity here deliberately beats .oc-row.wrap. */
|
|
731
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row,
|
|
732
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row.wrap {
|
|
733
|
+
display: flex;
|
|
734
|
+
flex-direction: row;
|
|
735
|
+
flex-wrap: wrap;
|
|
736
|
+
justify-content: flex-start;
|
|
737
|
+
gap: 8px;
|
|
738
|
+
margin: 0;
|
|
739
|
+
padding: 0;
|
|
740
|
+
max-width: none;
|
|
741
|
+
border: 0;
|
|
742
|
+
border-radius: 0;
|
|
743
|
+
background: none;
|
|
744
|
+
align-content: flex-start;
|
|
745
|
+
}
|
|
746
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child { padding: 0; flex: 0 0 auto; }
|
|
747
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::before,
|
|
748
|
+
.oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::after { display: none; }
|
|
749
|
+
|
|
750
|
+
@media (max-width: 820px) {
|
|
751
|
+
.oc-row.lanes > .oc-child > .ocn { grid-template-columns: 1fr; gap: 12px; }
|
|
752
|
+
}
|
|
753
|
+
/* Connectors are drawn by the SVG rail plane now, not by pseudo-elements. The
|
|
754
|
+
old fan drop-lines/elbows here would double every rail. */
|
|
755
|
+
.oc-child { position: relative; padding: 0; display: flex; flex-direction: column; align-items: center; }
|
|
756
|
+
.oc-child::before, .oc-child::after { display: none; }
|
|
361
757
|
|
|
362
758
|
/* wide sibling sets — grouped subtrees that wrap to natural width (never crush a
|
|
363
759
|
branch into a fixed cell — that overflowed and collided with the next branch) */
|
|
@@ -370,78 +766,171 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
370
766
|
.oc-row.wrap > .oc-child { padding: 8px 4px 0; flex: 0 0 auto; }
|
|
371
767
|
.oc-row.wrap > .oc-child::before, .oc-row.wrap > .oc-child::after { display: none; }
|
|
372
768
|
|
|
373
|
-
/*
|
|
374
|
-
|
|
769
|
+
/* ── THE PLATE ────────────────────────────────────────────────────────────
|
|
770
|
+
An engraved plate on a key rack. Left edge is milled brass; rank reads
|
|
771
|
+
through FINISH and SIZE (polished → satin → dulled), never hue. Radius 4px
|
|
772
|
+
per CRAFT §0 (radii 2/3/6) — the old 15px rounded rect is what made this
|
|
773
|
+
read as a SaaS card. Content is left-aligned like a name plate, not
|
|
774
|
+
centred like a business card. */
|
|
775
|
+
/* ══ THE SOURCE CARD (variant B — "the spine") ══════════════════════════
|
|
776
|
+
This board is not a picture of the org: it IS the org file booboo_boot
|
|
777
|
+
reads. Rules inherit down it, bucket reach derives from it, every agent
|
|
778
|
+
boots against it. So the card states its CONSEQUENCE, not just its
|
|
779
|
+
attributes — what it declares, who it binds, what it boots on.
|
|
780
|
+
Health is the card's own left edge, so scanning nine stacked departments
|
|
781
|
+
you find the amber one peripherally, before reading a single word. */
|
|
375
782
|
.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
|
-
|
|
783
|
+
position: relative; display: flex; flex-direction: column; align-items: stretch;
|
|
784
|
+
width: calc(300px - min(var(--d, 0), 2) * 22px); /* 300 / 278 / 256 by rank */
|
|
785
|
+
padding: 15px 18px 13px 21px; text-align: left;
|
|
786
|
+
background: var(--bg-1);
|
|
787
|
+
border: 1px solid var(--line); border-radius: 14px; cursor: grab; overflow: hidden;
|
|
788
|
+
box-shadow: var(--sh-2);
|
|
789
|
+
transition: box-shadow .22s var(--ease), transform .22s var(--ease), border-color .18s ease, opacity .18s ease;
|
|
790
|
+
animation: settle .45s var(--settle) both;
|
|
791
|
+
}
|
|
792
|
+
/* the spine — health as the card's own edge */
|
|
793
|
+
.ag::before {
|
|
794
|
+
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
|
|
795
|
+
background: var(--ink-3); transition: background .2s ease;
|
|
389
796
|
}
|
|
390
|
-
.ag::before {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
.ag.
|
|
397
|
-
|
|
797
|
+
.ag.h-ok::before { background: var(--ok); }
|
|
798
|
+
.ag.h-warn::before { background: var(--warn); }
|
|
799
|
+
.ag.h-fail::before { background: var(--fail); }
|
|
800
|
+
/* an unhealthy card also carries a wash, so the state survives a greyscale
|
|
801
|
+
print and never rests on hue alone */
|
|
802
|
+
.ag.h-warn { background: linear-gradient(180deg, var(--warn-wash), var(--bg-1) 52%); border-color: #ecd9ae; }
|
|
803
|
+
.ag.h-fail { background: linear-gradient(180deg, var(--fail-wash), var(--bg-1) 52%); border-color: #edc9bf; }
|
|
804
|
+
@keyframes settle { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
|
|
805
|
+
.ag:hover { transform: translateY(-2px); box-shadow: var(--sh-3); border-color: var(--line-2); }
|
|
806
|
+
.ag:active { transform: translateY(0) scale(.995); transition-duration: .08s; }
|
|
807
|
+
.ag.sel { border-color: var(--accent); box-shadow: var(--sh-3), 0 0 0 3px var(--accent-dim); }
|
|
398
808
|
.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.
|
|
809
|
+
.ag.over { border-color: var(--accent-2); box-shadow: var(--sh-3), 0 0 0 4px var(--accent-dim); transform: scale(1.02); }
|
|
810
|
+
.ag.dragging { opacity: 0.4; cursor: grabbing; }
|
|
811
|
+
|
|
812
|
+
/* WHERE IT BELONGS — reads first, so a card lifted out of its lane still
|
|
813
|
+
knows its own place. */
|
|
814
|
+
.ag-eyebrow {
|
|
815
|
+
font-family: var(--mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
|
|
816
|
+
color: var(--accent); display: block; margin-bottom: 7px;
|
|
817
|
+
}
|
|
818
|
+
/* WHAT FLOWS OUT — the consequence line. Plain English, never glyphs. */
|
|
819
|
+
.ag-flows {
|
|
820
|
+
display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 10px;
|
|
821
|
+
font-variant-numeric: tabular-nums;
|
|
822
|
+
}
|
|
401
823
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
824
|
+
/* ── THE HEALTH LIGHT ──────────────────────────────────────────────────────
|
|
825
|
+
A real lamp: a lit core inside a soft halo, with a slow breath so a healthy
|
|
826
|
+
board is visibly ALIVE rather than merely static. The halo is a separate
|
|
827
|
+
::after ring that expands and fades — one pulse every 2.6s, offset per
|
|
828
|
+
bubble so nine departments never blink in unison (a synchronised board
|
|
829
|
+
reads as a screensaver; a staggered one reads as a building). */
|
|
830
|
+
.ag-light {
|
|
831
|
+
position: absolute; top: 13px; right: 14px; width: 8px; height: 8px; border-radius: 50%;
|
|
832
|
+
background: var(--ink-3);
|
|
833
|
+
}
|
|
834
|
+
.ag-light::after {
|
|
835
|
+
content: ""; position: absolute; inset: -3px; border-radius: 50%;
|
|
836
|
+
border: 1px solid currentColor; opacity: 0;
|
|
837
|
+
animation: lamp 2.6s var(--ease) infinite;
|
|
838
|
+
animation-delay: var(--lamp-delay, 0s);
|
|
839
|
+
}
|
|
840
|
+
.ag-light.ok { background: var(--ok); color: var(--ok); box-shadow: 0 0 0 3px var(--ok-wash); }
|
|
841
|
+
.ag-light.warn { background: var(--warn); color: var(--warn); box-shadow: 0 0 0 3px var(--warn-wash); }
|
|
842
|
+
.ag-light.fail { background: var(--fail); color: var(--fail); box-shadow: 0 0 0 3px var(--fail-wash); }
|
|
843
|
+
/* a failing lamp beats faster — urgency in the rhythm, not just the hue */
|
|
844
|
+
.ag-light.fail::after { animation-duration: 1.2s; }
|
|
845
|
+
@keyframes lamp {
|
|
846
|
+
0% { transform: scale(.7); opacity: .85; }
|
|
847
|
+
70%, 100% { transform: scale(1.9); opacity: 0; }
|
|
848
|
+
}
|
|
849
|
+
@media (prefers-reduced-motion: reduce) { .ag-light::after { animation: none; } }
|
|
406
850
|
|
|
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; } }
|
|
851
|
+
/* rank I·II — the GM. Polished brass: the only plate that is lit rather than lit-upon. */
|
|
852
|
+
.ag.root { cursor: default; width: 268px; padding: 15px 17px;
|
|
853
|
+
background: var(--surface-lift);
|
|
854
|
+
border-color: var(--surface-lift-edge); box-shadow: var(--sh-3); }
|
|
855
|
+
.ag.root .ag-name { font-size: 14.5px; }
|
|
417
856
|
|
|
857
|
+
/* plate header: the mark sits BESIDE the name, not above it — a name plate,
|
|
858
|
+
not a business card. Staff plates carry no mark at all (see Panel.tsx): a
|
|
859
|
+
brass plaque does not have a cartoon on it, and 51 identical 🤖 was the
|
|
860
|
+
cheapest token on the board. */
|
|
861
|
+
.ag-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
|
418
862
|
.ag-ava {
|
|
419
|
-
width:
|
|
420
|
-
border-radius:
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
.ag-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
863
|
+
width: 26px; height: 26px; flex: 0 0 26px; display: grid; place-items: center; font-size: 14px;
|
|
864
|
+
border-radius: 9px; background: var(--accent-dim); border: 1px solid #eadcc3;
|
|
865
|
+
}
|
|
866
|
+
.ag.root .ag-ava { width: 30px; height: 30px; flex-basis: 30px; font-size: 16px; border-color: rgba(239, 195, 137, 0.45); }
|
|
867
|
+
|
|
868
|
+
/* the name — engraved: a dark groove under, a hairline of light above */
|
|
869
|
+
.ag-name {
|
|
870
|
+
font-family: var(--display); font-size: 18px; font-weight: 600; letter-spacing: -0.015em;
|
|
871
|
+
line-height: 1.18; color: var(--ink);
|
|
872
|
+
white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
873
|
+
}
|
|
874
|
+
/* staff plates: quieter, lighter weight — rank in the type as well as the metal */
|
|
875
|
+
.ag.staff .ag-name { font-family: var(--font); font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); }
|
|
876
|
+
|
|
877
|
+
.ag-role {
|
|
878
|
+
font-size: 12.5px; line-height: 1.5; color: var(--ink-2); margin-top: 6px;
|
|
879
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
880
|
+
}
|
|
881
|
+
.ag-meta { display: flex; align-items: center; gap: 9px; margin-top: 7px; }
|
|
882
|
+
.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
883
|
.ag-kids {
|
|
430
|
-
font-
|
|
431
|
-
padding: 2px
|
|
884
|
+
font-family: var(--mono); font-size: 8.5px; color: var(--brass); border: 1px solid rgba(217, 160, 91, 0.28);
|
|
885
|
+
border-radius: 3px; padding: 2px 7px; font-variant-numeric: tabular-nums; text-transform: uppercase; letter-spacing: 0.1em;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/* ── the plate face: health · bucket reach · rule count · last report ──────
|
|
889
|
+
Replaces a role line that used to just repeat the name ("Lift Engineer" /
|
|
890
|
+
"Lift Engineer — Engineering"). Four facts, left-aligned, mono, no chrome. */
|
|
891
|
+
.ag-facts {
|
|
892
|
+
display: flex; align-items: center; gap: 10px;
|
|
893
|
+
margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--line);
|
|
432
894
|
}
|
|
895
|
+
.ag-fact { font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--ink-3); }
|
|
896
|
+
.ag-fact-health.ok { color: var(--ok); }
|
|
897
|
+
.ag-fact-health.warn { color: var(--warn); }
|
|
898
|
+
.ag-fact-health.fail { color: var(--fail); }
|
|
899
|
+
.ag-fact-health.none { color: var(--ink-3); font-weight: 500; }
|
|
900
|
+
.fact-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--ink-3); flex: 0 0 auto; }
|
|
901
|
+
.ag-fact-health.ok .fact-dot { background: var(--ok); box-shadow: 0 0 6px rgba(139, 233, 176, 0.75); }
|
|
902
|
+
.ag-fact-health.warn .fact-dot { background: var(--brass); box-shadow: 0 0 6px rgba(217, 160, 91, 0.85); }
|
|
903
|
+
.ag-fact-health.fail .fact-dot { background: var(--fail); box-shadow: 0 0 7px rgba(224, 123, 106, 0.85); }
|
|
904
|
+
.ag-fact-health.ok, .ag-fact-health.warn, .ag-fact-health.fail { color: var(--ink-2); }
|
|
905
|
+
.ag-fact-report { margin-left: auto; color: var(--ink-3); font-weight: 400; font-family: var(--mono); font-size: 10px; }
|
|
906
|
+
/* the expected beat, quieter than the fact it qualifies — it is the ruler, not
|
|
907
|
+
the measurement, so it must never out-shout "reported 4d ago". */
|
|
908
|
+
.ag-cadence { font-weight: 400; color: var(--ink-3); opacity: .62; }
|
|
909
|
+
|
|
910
|
+
/* "show the law": the boot-order chain, printed on the plate only while the
|
|
911
|
+
toggle is on — the product's core idea, made literal per plate. */
|
|
912
|
+
.ag-law { display: none; }
|
|
913
|
+
.law-on .ag-law {
|
|
914
|
+
display: block; margin-top: 7px; padding-top: 7px; border-top: 1px dashed rgba(217, 160, 91, 0.32);
|
|
915
|
+
font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.03em; color: var(--brass-hi);
|
|
916
|
+
line-height: 1.6; word-break: break-word;
|
|
917
|
+
}
|
|
918
|
+
.ag-law-arrow { font-style: normal; color: var(--brass); margin: 0 3px; opacity: 0.85; }
|
|
919
|
+
.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
920
|
|
|
434
921
|
/* the dossier */
|
|
435
922
|
.doss {
|
|
436
923
|
width: 340px; flex: 0 0 340px; border-left: 1px solid var(--line);
|
|
437
|
-
background:
|
|
924
|
+
background: var(--bg-1); backdrop-filter: blur(10px);
|
|
438
925
|
padding: 26px 24px 40px; overflow-y: auto; animation: slidein 0.25s ease both;
|
|
439
926
|
}
|
|
440
927
|
@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
|
-
|
|
928
|
+
.doss-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; }
|
|
929
|
+
.doss-emoji { font-size: 30px; flex: none; }
|
|
930
|
+
.doss h2 { margin: 0; font-size: 18px; min-width: 0; }
|
|
931
|
+
/* full panel width: a body of four seats is a list, not a subtitle squeezed
|
|
932
|
+
beside three buttons. Middots become the wrap points. */
|
|
933
|
+
.doss-role { margin: 0 0 14px; color: var(--ink-2); font-size: 12.5px; line-height: 1.55; }
|
|
445
934
|
|
|
446
935
|
.doss-chain { font-size: 11px; color: var(--ink-3); margin-bottom: 18px; line-height: 1.7; }
|
|
447
936
|
.doss-chain i { font-style: normal; margin: 0 5px; }
|
|
@@ -462,8 +951,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
462
951
|
.doss-rules em { color: var(--ink-3); font-style: normal; font-size: 9.5px; margin-left: 5px; }
|
|
463
952
|
.doss-chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
464
953
|
.doss-reps { display: flex; flex-direction: column; gap: 7px; }
|
|
954
|
+
/* No side-stripe. It was a 2px brass border-left that said nothing the
|
|
955
|
+
status dot doesn't already say properly, and a coloured side-stripe on a
|
|
956
|
+
card is a banned pattern in the house design laws. Full 1px border; the
|
|
957
|
+
whole edge answers on hover. */
|
|
465
958
|
.doss-rep {
|
|
466
|
-
background: var(--bg-1); border: 1px solid var(--line);
|
|
959
|
+
background: var(--bg-1); border: 1px solid var(--line);
|
|
467
960
|
border-radius: 9px; padding: 9px 11px; animation: settle 0.35s ease both;
|
|
468
961
|
}
|
|
469
962
|
.doss-rep-top { display: flex; align-items: baseline; gap: 9px; }
|
|
@@ -471,7 +964,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
471
964
|
.doss-rep-label { font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
472
965
|
.doss-rep-sum { margin: 5px 0 0; font-size: 11.5px; color: var(--ink-2); line-height: 1.55; }
|
|
473
966
|
.doss-rep.tap { cursor: pointer; transition: border-color .12s ease, background .12s ease; }
|
|
474
|
-
.doss-rep.tap:hover { border-
|
|
967
|
+
.doss-rep.tap:hover { border-color: var(--accent); background: var(--accent-dim); }
|
|
475
968
|
.doss-rep.tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
476
969
|
.doss-rep-caret { margin-left: auto; font-size: 10px; color: var(--ink-2); align-self: center; }
|
|
477
970
|
.rep-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; align-self: center; }
|
|
@@ -479,7 +972,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
|
|
|
479
972
|
.rep-ok { color: var(--accent); } .rep-warn { color: var(--warn); } .rep-fail { color: #e07b6a; }
|
|
480
973
|
.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
974
|
.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; }
|
|
975
|
+
.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
976
|
.chip.alt { background: var(--bg-2); color: var(--ink-2); }
|
|
484
977
|
.doss-boot { font-size: 12px; color: var(--ink-2); line-height: 1.6; margin: 0; white-space: pre-wrap; }
|
|
485
978
|
/* contract — one click to open, edit in place, save flows to the org file */
|
|
@@ -554,17 +1047,6 @@ var TABS = [
|
|
|
554
1047
|
{ id: "rules", glyph: "\xA7", label: "rules" },
|
|
555
1048
|
{ id: "graph", glyph: "\u25C9", label: "graph" }
|
|
556
1049
|
];
|
|
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
1050
|
function nodeAt(n) {
|
|
569
1051
|
const d = n.data ?? {};
|
|
570
1052
|
for (const k of ["at", "ts", "time", "created_at", "date", "bst", "ts_bst", "finished_at"]) {
|
|
@@ -581,13 +1063,14 @@ function timeMs(at) {
|
|
|
581
1063
|
function buildHealthMap(nodes) {
|
|
582
1064
|
const m = /* @__PURE__ */ new Map();
|
|
583
1065
|
for (const r of nodes) {
|
|
584
|
-
|
|
1066
|
+
const who = reportAgentId(r);
|
|
1067
|
+
if (!who) continue;
|
|
585
1068
|
const d = r.data ?? {};
|
|
586
1069
|
if (typeof d.status !== "string") continue;
|
|
587
1070
|
const status = d.status;
|
|
588
1071
|
const at = nodeAt(r);
|
|
589
1072
|
const atMs = timeMs(at);
|
|
590
|
-
const cur = m.get(
|
|
1073
|
+
const cur = m.get(who) ?? { lastAt: "", lastMs: -Infinity, lastStatus: "", n: 0, fails: 0 };
|
|
591
1074
|
cur.n++;
|
|
592
1075
|
if (status === "fail") cur.fails++;
|
|
593
1076
|
if (Number.isFinite(atMs) && atMs >= cur.lastMs) {
|
|
@@ -595,7 +1078,7 @@ function buildHealthMap(nodes) {
|
|
|
595
1078
|
cur.lastAt = at;
|
|
596
1079
|
cur.lastStatus = status;
|
|
597
1080
|
}
|
|
598
|
-
m.set(
|
|
1081
|
+
m.set(who, cur);
|
|
599
1082
|
}
|
|
600
1083
|
return m;
|
|
601
1084
|
}
|
|
@@ -609,6 +1092,15 @@ function pulseFor(a, health) {
|
|
|
609
1092
|
}
|
|
610
1093
|
return best;
|
|
611
1094
|
}
|
|
1095
|
+
function everyN(h) {
|
|
1096
|
+
if (h <= 1) return "hourly";
|
|
1097
|
+
if (h < 24) return `every ${h}h`;
|
|
1098
|
+
if (h === 24) return "daily";
|
|
1099
|
+
if (h === 168) return "weekly";
|
|
1100
|
+
if (h >= 672 && h <= 744) return "monthly";
|
|
1101
|
+
if (h % 24 === 0) return `every ${h / 24} days`;
|
|
1102
|
+
return `every ${h}h`;
|
|
1103
|
+
}
|
|
612
1104
|
function lightFor(a, health) {
|
|
613
1105
|
const p = pulseFor(a, health);
|
|
614
1106
|
if (!p || !p.lastAt) return "none";
|
|
@@ -631,6 +1123,10 @@ function nodeSummary(n) {
|
|
|
631
1123
|
}
|
|
632
1124
|
return "";
|
|
633
1125
|
}
|
|
1126
|
+
function reportAgentId(n) {
|
|
1127
|
+
const d = n.data ?? {};
|
|
1128
|
+
return n.cluster || (typeof d.agent === "string" ? d.agent : "") || "";
|
|
1129
|
+
}
|
|
634
1130
|
async function fetchReports(api, cluster, limit = 500) {
|
|
635
1131
|
const q = async (t) => {
|
|
636
1132
|
try {
|
|
@@ -656,23 +1152,30 @@ function slugify(s) {
|
|
|
656
1152
|
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
657
1153
|
}
|
|
658
1154
|
var REDUCED = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
1155
|
+
var THEME_KEY = "booboo-theme-v2";
|
|
659
1156
|
function readTheme() {
|
|
660
1157
|
try {
|
|
661
|
-
|
|
1158
|
+
if (new URLSearchParams(window.location.search).get("embed")) return "light";
|
|
1159
|
+
return localStorage.getItem(THEME_KEY) === "dark" ? "dark" : "light";
|
|
662
1160
|
} catch {
|
|
663
|
-
return "
|
|
1161
|
+
return "light";
|
|
664
1162
|
}
|
|
665
1163
|
}
|
|
666
1164
|
function useTheme() {
|
|
667
1165
|
const [theme, setTheme] = useState(readTheme);
|
|
1166
|
+
const chosen = useRef(false);
|
|
668
1167
|
useEffect(() => {
|
|
669
1168
|
document.documentElement.setAttribute("data-theme", theme);
|
|
1169
|
+
if (!chosen.current) return;
|
|
670
1170
|
try {
|
|
671
|
-
localStorage.setItem(
|
|
1171
|
+
localStorage.setItem(THEME_KEY, theme);
|
|
672
1172
|
} catch {
|
|
673
1173
|
}
|
|
674
1174
|
}, [theme]);
|
|
675
|
-
return [theme, () =>
|
|
1175
|
+
return [theme, () => {
|
|
1176
|
+
chosen.current = true;
|
|
1177
|
+
setTheme((t) => t === "dark" ? "light" : "dark");
|
|
1178
|
+
}];
|
|
676
1179
|
}
|
|
677
1180
|
function useCountUp(target, ms = 900) {
|
|
678
1181
|
const [v, setV] = useState(REDUCED ? target : 0);
|
|
@@ -720,8 +1223,216 @@ function bucketHue(name) {
|
|
|
720
1223
|
for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;
|
|
721
1224
|
return h;
|
|
722
1225
|
}
|
|
1226
|
+
var SEALED_BUCKET = "guest-registry";
|
|
1227
|
+
function LedgerShelf({ org, lit }) {
|
|
1228
|
+
const buckets = useMemo(() => {
|
|
1229
|
+
const s = /* @__PURE__ */ new Set();
|
|
1230
|
+
for (const a of org.agents) for (const b of a.buckets ?? []) s.add(b);
|
|
1231
|
+
return [...s].sort();
|
|
1232
|
+
}, [org]);
|
|
1233
|
+
return /* @__PURE__ */ jsxs("div", { className: "ledger-shelf", role: "list", "aria-label": "the ledger \u2014 hover a role to see what it reaches", children: [
|
|
1234
|
+
/* @__PURE__ */ jsx("span", { className: "shelf-label", children: "the ledger" }),
|
|
1235
|
+
/* @__PURE__ */ jsxs("div", { className: "shelf-slots", children: [
|
|
1236
|
+
buckets.map((b) => /* @__PURE__ */ jsx("span", { className: `shelf-slot${lit.has(b) ? " lit" : ""}`, role: "listitem", children: b }, b)),
|
|
1237
|
+
/* @__PURE__ */ jsxs("span", { className: "shelf-slot sealed", role: "listitem", title: "ledger:guest-registry \u2014 sealed by wall. Visible, never emitted.", children: [
|
|
1238
|
+
"\u{1F512} ",
|
|
1239
|
+
SEALED_BUCKET
|
|
1240
|
+
] })
|
|
1241
|
+
] })
|
|
1242
|
+
] });
|
|
1243
|
+
}
|
|
1244
|
+
function Constellation() {
|
|
1245
|
+
const svg = useMemo(() => {
|
|
1246
|
+
const W = 1600, H = 1e3, N = 58;
|
|
1247
|
+
let s = 20260719;
|
|
1248
|
+
const rnd = () => (s = s * 1103515245 + 12345 & 2147483647) / 2147483647;
|
|
1249
|
+
const pts = [];
|
|
1250
|
+
for (let i = 0; i < N; i++) pts.push([rnd() * W, rnd() * H]);
|
|
1251
|
+
let d = "";
|
|
1252
|
+
for (let i = 0; i < N; i++) {
|
|
1253
|
+
for (let j = i + 1; j < N; j++) {
|
|
1254
|
+
const dist = Math.hypot(pts[i][0] - pts[j][0], pts[i][1] - pts[j][1]);
|
|
1255
|
+
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"/>`;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
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)"/>`;
|
|
1259
|
+
return d;
|
|
1260
|
+
}, []);
|
|
1261
|
+
return /* @__PURE__ */ jsx("svg", { className: "pnl-stars", viewBox: "0 0 1600 1000", preserveAspectRatio: "xMidYMid slice", "aria-hidden": true, dangerouslySetInnerHTML: { __html: svg } });
|
|
1262
|
+
}
|
|
1263
|
+
function LawPlate({ org, selected, onSelect }) {
|
|
1264
|
+
const root = org.agents.find((a) => a.id === org.root);
|
|
1265
|
+
const rules = root?.rules ?? [];
|
|
1266
|
+
if (!rules.length) return null;
|
|
1267
|
+
return /* @__PURE__ */ jsxs(
|
|
1268
|
+
"div",
|
|
1269
|
+
{
|
|
1270
|
+
className: `ag law-plate${selected ? " sel" : ""}`,
|
|
1271
|
+
"data-rail": "law",
|
|
1272
|
+
tabIndex: 0,
|
|
1273
|
+
title: "the law every agent in the house boots against",
|
|
1274
|
+
onClick: (e) => {
|
|
1275
|
+
e.stopPropagation();
|
|
1276
|
+
if (root) onSelect(root.id);
|
|
1277
|
+
},
|
|
1278
|
+
onKeyDown: (e) => {
|
|
1279
|
+
if ((e.key === "Enter" || e.key === " ") && root) {
|
|
1280
|
+
e.preventDefault();
|
|
1281
|
+
onSelect(root.id);
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
children: [
|
|
1285
|
+
/* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
|
|
1286
|
+
/* @__PURE__ */ jsx("span", { className: "ag-ava", children: "\xA7" }),
|
|
1287
|
+
/* @__PURE__ */ jsx("span", { className: "ag-name", children: "The House Standard" })
|
|
1288
|
+
] }),
|
|
1289
|
+
/* @__PURE__ */ jsx("span", { className: "ag-role", children: rules[0] }),
|
|
1290
|
+
/* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
|
|
1291
|
+
/* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
|
|
1292
|
+
"binds ",
|
|
1293
|
+
org.agents.length
|
|
1294
|
+
] }),
|
|
1295
|
+
/* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
|
|
1296
|
+
rules.length,
|
|
1297
|
+
" clause",
|
|
1298
|
+
rules.length === 1 ? "" : "s"
|
|
1299
|
+
] })
|
|
1300
|
+
] })
|
|
1301
|
+
]
|
|
1302
|
+
}
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
function boxIn(el, root) {
|
|
1306
|
+
let x = 0, y = 0;
|
|
1307
|
+
let cur = el;
|
|
1308
|
+
while (cur && cur !== root) {
|
|
1309
|
+
x += cur.offsetLeft;
|
|
1310
|
+
y += cur.offsetTop;
|
|
1311
|
+
cur = cur.offsetParent;
|
|
1312
|
+
}
|
|
1313
|
+
return { x, y, w: el.offsetWidth, h: el.offsetHeight, cy: y + el.offsetHeight / 2 };
|
|
1314
|
+
}
|
|
1315
|
+
function elbow(x1, y1, x2, y2) {
|
|
1316
|
+
if (Math.abs(y2 - y1) < 1.5) return `M${x1},${y1.toFixed(1)} H${x2}`;
|
|
1317
|
+
const mx = x1 + (x2 - x1) * 0.45;
|
|
1318
|
+
const r = Math.min(9, Math.abs(y2 - y1) / 2);
|
|
1319
|
+
const dir = y2 > y1 ? 1 : -1;
|
|
1320
|
+
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}`;
|
|
1321
|
+
}
|
|
1322
|
+
function CascadeRails({ chartRef, version }) {
|
|
1323
|
+
const [paths, setPaths] = useState([]);
|
|
1324
|
+
useLayoutEffect(() => {
|
|
1325
|
+
const root = chartRef.current;
|
|
1326
|
+
if (!root) return;
|
|
1327
|
+
const measure = () => {
|
|
1328
|
+
const pick = (sel) => Array.from(root.querySelectorAll(sel));
|
|
1329
|
+
const law = root.querySelector('[data-rail="law"]');
|
|
1330
|
+
const gm = root.querySelector('[data-rail="gm"]');
|
|
1331
|
+
const depts = pick('[data-rail="dept"]');
|
|
1332
|
+
const staff = pick('[data-rail="staff"]');
|
|
1333
|
+
const out = [];
|
|
1334
|
+
const gmB = gm ? boxIn(gm, root) : null;
|
|
1335
|
+
if (law && gmB) {
|
|
1336
|
+
const lawB = boxIn(law, root);
|
|
1337
|
+
out.push(elbow(lawB.x + lawB.w, lawB.cy, gmB.x, gmB.cy));
|
|
1338
|
+
}
|
|
1339
|
+
const deptBox = /* @__PURE__ */ new Map();
|
|
1340
|
+
for (const d of depts) {
|
|
1341
|
+
const b = boxIn(d, root);
|
|
1342
|
+
deptBox.set(d.dataset.id ?? "", b);
|
|
1343
|
+
if (gmB) out.push(elbow(gmB.x + gmB.w, gmB.cy, b.x, b.cy));
|
|
1344
|
+
}
|
|
1345
|
+
for (const s of staff) {
|
|
1346
|
+
const parent = deptBox.get(s.dataset.parent ?? "");
|
|
1347
|
+
if (!parent) continue;
|
|
1348
|
+
const b = boxIn(s, root);
|
|
1349
|
+
out.push(elbow(parent.x + parent.w, parent.cy, b.x, b.cy));
|
|
1350
|
+
}
|
|
1351
|
+
setPaths((prev) => prev.length === out.length && prev.every((p, i) => p === out[i]) ? prev : out);
|
|
1352
|
+
};
|
|
1353
|
+
measure();
|
|
1354
|
+
const ro = new ResizeObserver(measure);
|
|
1355
|
+
ro.observe(root);
|
|
1356
|
+
return () => ro.disconnect();
|
|
1357
|
+
}, [chartRef, version]);
|
|
1358
|
+
return /* @__PURE__ */ jsxs("svg", { className: "rails", "aria-hidden": true, children: [
|
|
1359
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: "railGrad", x1: "0", y1: "0", x2: "1", y2: "0", children: [
|
|
1360
|
+
/* @__PURE__ */ jsx("stop", { offset: "0", stopColor: "var(--brass)", stopOpacity: "0.3" }),
|
|
1361
|
+
/* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "var(--brass-hi)", stopOpacity: "0.85" })
|
|
1362
|
+
] }) }),
|
|
1363
|
+
paths.map((d, i) => /* @__PURE__ */ jsxs("g", { children: [
|
|
1364
|
+
/* @__PURE__ */ jsx("path", { className: "rail-line", d }),
|
|
1365
|
+
/* @__PURE__ */ jsx("path", { className: "rail-dash", d })
|
|
1366
|
+
] }, i))
|
|
1367
|
+
] });
|
|
1368
|
+
}
|
|
1369
|
+
function Lane({ laneId, isLane, dragId, onDropOn, children }) {
|
|
1370
|
+
const [over, setOver] = useState(false);
|
|
1371
|
+
if (!isLane) return /* @__PURE__ */ jsx("div", { className: "oc-child", children });
|
|
1372
|
+
return /* @__PURE__ */ jsx(
|
|
1373
|
+
"div",
|
|
1374
|
+
{
|
|
1375
|
+
className: `oc-child${over && dragId && dragId !== laneId ? " lane-over" : ""}`,
|
|
1376
|
+
onDragOver: (e) => {
|
|
1377
|
+
if (dragId && dragId !== laneId) {
|
|
1378
|
+
e.preventDefault();
|
|
1379
|
+
setOver(true);
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
onDragLeave: (e) => {
|
|
1383
|
+
if (!e.currentTarget.contains(e.relatedTarget)) setOver(false);
|
|
1384
|
+
},
|
|
1385
|
+
onDrop: (e) => {
|
|
1386
|
+
e.preventDefault();
|
|
1387
|
+
setOver(false);
|
|
1388
|
+
if (dragId && dragId !== laneId) onDropOn(laneId);
|
|
1389
|
+
},
|
|
1390
|
+
children
|
|
1391
|
+
}
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
var HEALTH_WORD = { ok: "Healthy", warn: "Needs a look", fail: "Failing", none: "No reports yet" };
|
|
1395
|
+
function consequenceOf(a, org, slice) {
|
|
1396
|
+
if (!slice) return "";
|
|
1397
|
+
const descendants = (id) => {
|
|
1398
|
+
const kids = org.agents.filter((x) => x.parent === id);
|
|
1399
|
+
return kids.reduce((n, k) => n + 1 + descendants(k.id), 0);
|
|
1400
|
+
};
|
|
1401
|
+
const declares = a.rules?.length ?? 0;
|
|
1402
|
+
const below = descendants(a.id);
|
|
1403
|
+
if (below === 0) return "";
|
|
1404
|
+
const parts = [];
|
|
1405
|
+
if (declares) parts.push(`declares ${declares} rule${declares === 1 ? "" : "s"}`);
|
|
1406
|
+
parts.push(`binds ${below} below`);
|
|
1407
|
+
return parts.join(" \xB7 ");
|
|
1408
|
+
}
|
|
1409
|
+
function AgentFacts({ a, org, health, showLaw }) {
|
|
1410
|
+
const slice = useMemo(() => orgBootSlice(org, a.id), [org, a.id]);
|
|
1411
|
+
const consequence = useMemo(() => consequenceOf(a, org, slice), [a, org, slice]);
|
|
1412
|
+
const pulse = pulseFor(a, health);
|
|
1413
|
+
const light = lightFor(a, health);
|
|
1414
|
+
if (!slice) return null;
|
|
1415
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1416
|
+
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 }),
|
|
1417
|
+
/* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
|
|
1418
|
+
/* @__PURE__ */ jsx("em", { className: `ag-fact ag-fact-health ${light}`, children: HEALTH_WORD[light] }),
|
|
1419
|
+
/* @__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: [
|
|
1420
|
+
pulse?.lastAt ? `reported ${relTime(pulse.lastAt)}` : "",
|
|
1421
|
+
typeof a.cadence === "number" && a.kind !== "automation" && /* @__PURE__ */ jsxs("b", { className: "ag-cadence", children: [
|
|
1422
|
+
" \xB7 ",
|
|
1423
|
+
everyN(a.cadence)
|
|
1424
|
+
] })
|
|
1425
|
+
] })
|
|
1426
|
+
] }),
|
|
1427
|
+
showLaw && /* @__PURE__ */ jsx("span", { className: "ag-law", title: "inheritance in boot order", children: slice.chain.map((c, i) => /* @__PURE__ */ jsxs("span", { children: [
|
|
1428
|
+
i > 0 && /* @__PURE__ */ jsx("i", { className: "ag-law-arrow", children: "\u2193" }),
|
|
1429
|
+
c.name
|
|
1430
|
+
] }, c.id)) })
|
|
1431
|
+
] });
|
|
1432
|
+
}
|
|
723
1433
|
function AgentCard({
|
|
724
1434
|
a,
|
|
1435
|
+
org,
|
|
725
1436
|
isRoot,
|
|
726
1437
|
depth,
|
|
727
1438
|
order,
|
|
@@ -731,16 +1442,19 @@ function AgentCard({
|
|
|
731
1442
|
onDragStart,
|
|
732
1443
|
onDropOn,
|
|
733
1444
|
childCount,
|
|
734
|
-
light = "none"
|
|
1445
|
+
light = "none",
|
|
1446
|
+
health = null,
|
|
1447
|
+
onHover,
|
|
1448
|
+
showLaw = false
|
|
735
1449
|
}) {
|
|
736
1450
|
const [over, setOver] = useState(false);
|
|
737
|
-
const nBuckets = a.buckets?.length ?? 0;
|
|
738
1451
|
const nSkills = a.skills?.length ?? 0;
|
|
1452
|
+
const parentName = a.parent ? org.agents.find((x) => x.id === a.parent)?.name : null;
|
|
739
1453
|
return /* @__PURE__ */ jsxs(
|
|
740
1454
|
"div",
|
|
741
1455
|
{
|
|
742
|
-
className: `ag${isRoot ? " root" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}`,
|
|
743
|
-
style: { ["--
|
|
1456
|
+
className: `ag${isRoot ? " root" : ""}${depth >= 2 ? " staff" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}${showLaw ? " law-on" : ""}${light !== "none" ? " h-" + light : ""}`,
|
|
1457
|
+
style: { ["--d"]: depth, animationDelay: `${Math.min(depth * 70 + order * 45, 600)}ms` },
|
|
744
1458
|
draggable: !isRoot,
|
|
745
1459
|
tabIndex: 0,
|
|
746
1460
|
onKeyDown: (e) => {
|
|
@@ -769,16 +1483,28 @@ function AgentCard({
|
|
|
769
1483
|
setOver(false);
|
|
770
1484
|
onDropOn(a.id);
|
|
771
1485
|
},
|
|
1486
|
+
onMouseEnter: () => onHover?.(a.id),
|
|
1487
|
+
onMouseLeave: () => onHover?.(null),
|
|
1488
|
+
"data-rail": isRoot ? "gm" : depth === 1 ? "dept" : "staff",
|
|
1489
|
+
"data-id": a.id,
|
|
1490
|
+
"data-parent": a.parent ?? "",
|
|
772
1491
|
children: [
|
|
773
|
-
light !== "none" && /* @__PURE__ */ jsx(
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1492
|
+
light !== "none" && /* @__PURE__ */ jsx(
|
|
1493
|
+
"i",
|
|
1494
|
+
{
|
|
1495
|
+
className: `ag-light ${light}`,
|
|
1496
|
+
style: { ["--lamp-delay"]: `${bucketHue(a.id) % 26 / 10}s` },
|
|
1497
|
+
title: `health: ${light === "ok" ? "healthy" : light === "warn" ? "needs a look" : "failing"}`
|
|
1498
|
+
}
|
|
1499
|
+
),
|
|
1500
|
+
/* @__PURE__ */ jsx("span", { className: "ag-eyebrow", children: depth === 0 ? "The house \xB7 rank II" : depth === 1 ? "Department \xB7 rank III" : parentName ?? "Staff" }),
|
|
1501
|
+
/* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
|
|
1502
|
+
depth <= 1 && a.emoji && /* @__PURE__ */ jsx("span", { className: "ag-ava", children: a.emoji }),
|
|
1503
|
+
/* @__PURE__ */ jsx("span", { className: "ag-name", children: a.name })
|
|
1504
|
+
] }),
|
|
1505
|
+
a.role && /* @__PURE__ */ jsx("span", { className: "ag-role", children: a.role }),
|
|
1506
|
+
/* @__PURE__ */ jsx(AgentFacts, { a, org, health, showLaw }),
|
|
1507
|
+
(nSkills > 0 || childCount > 0) && /* @__PURE__ */ jsxs("span", { className: "ag-meta", children: [
|
|
782
1508
|
nSkills > 0 && /* @__PURE__ */ jsxs("em", { title: "skills", children: [
|
|
783
1509
|
"\u2726 ",
|
|
784
1510
|
nSkills
|
|
@@ -799,7 +1525,8 @@ function ChartNode({
|
|
|
799
1525
|
order,
|
|
800
1526
|
...cardProps
|
|
801
1527
|
}) {
|
|
802
|
-
const
|
|
1528
|
+
const kidsRaw = org.agents.filter((c) => c.parent === a.id && c.kind !== "automation");
|
|
1529
|
+
const kids = depth === 0 ? [...kidsRaw].sort((x, y) => x.id.localeCompare(y.id)) : kidsRaw;
|
|
803
1530
|
const machines = (() => {
|
|
804
1531
|
const out = [];
|
|
805
1532
|
const walk = (pid) => {
|
|
@@ -814,17 +1541,20 @@ function ChartNode({
|
|
|
814
1541
|
})();
|
|
815
1542
|
const lights = machines.map((m) => lightFor(m, cardProps.health));
|
|
816
1543
|
const trayLight = worst(lights);
|
|
1544
|
+
const isDept = depth === 1 && kids.length > 0;
|
|
1545
|
+
const folded = isDept && cardProps.collapsed?.has(a.id);
|
|
817
1546
|
const TRAY_MAX = 8;
|
|
818
1547
|
const hidden = machines.length - TRAY_MAX;
|
|
819
1548
|
const hiddenBad = machines.slice(TRAY_MAX).reduce(
|
|
820
1549
|
(n, m) => n + (lightFor(m, cardProps.health) !== "ok" && lightFor(m, cardProps.health) !== "none" ? 1 : 0),
|
|
821
1550
|
0
|
|
822
1551
|
);
|
|
823
|
-
|
|
1552
|
+
const cardAndRack = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
824
1553
|
/* @__PURE__ */ jsx(
|
|
825
1554
|
AgentCard,
|
|
826
1555
|
{
|
|
827
1556
|
a,
|
|
1557
|
+
org,
|
|
828
1558
|
isRoot: a.id === org.root,
|
|
829
1559
|
depth,
|
|
830
1560
|
order,
|
|
@@ -834,7 +1564,10 @@ function ChartNode({
|
|
|
834
1564
|
onDragStart: cardProps.onDragStart,
|
|
835
1565
|
onDropOn: cardProps.onDropOn,
|
|
836
1566
|
childCount: kids.length,
|
|
837
|
-
light: trayLight
|
|
1567
|
+
light: trayLight,
|
|
1568
|
+
health: cardProps.health,
|
|
1569
|
+
onHover: cardProps.onHover,
|
|
1570
|
+
showLaw: cardProps.showLaw
|
|
838
1571
|
}
|
|
839
1572
|
),
|
|
840
1573
|
machines.length > 0 && /* @__PURE__ */ jsxs("div", { className: `oc-tray ${trayLight}`, children: [
|
|
@@ -855,7 +1588,7 @@ function ChartNode({
|
|
|
855
1588
|
},
|
|
856
1589
|
children: [
|
|
857
1590
|
/* @__PURE__ */ jsx("i", { className: `mac-dot ${lightFor(m, cardProps.health)}${unstableFor(m, cardProps.health) ? " unstable" : ""}` }),
|
|
858
|
-
/* @__PURE__ */ jsx(
|
|
1591
|
+
/* @__PURE__ */ jsx(BrandMark, { agent: m }),
|
|
859
1592
|
/* @__PURE__ */ jsx("span", { className: "mac-name", children: m.name })
|
|
860
1593
|
]
|
|
861
1594
|
},
|
|
@@ -879,13 +1612,78 @@ function ChartNode({
|
|
|
879
1612
|
]
|
|
880
1613
|
}
|
|
881
1614
|
)
|
|
882
|
-
] })
|
|
1615
|
+
] })
|
|
1616
|
+
] });
|
|
1617
|
+
return /* @__PURE__ */ jsxs("div", { className: "ocn", children: [
|
|
1618
|
+
depth === 0 ? /* @__PURE__ */ jsx("div", { className: "casc-head", children: cardAndRack }) : cardAndRack,
|
|
883
1619
|
kids.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
884
|
-
/* @__PURE__ */ jsx(
|
|
885
|
-
|
|
1620
|
+
/* @__PURE__ */ jsx(
|
|
1621
|
+
"div",
|
|
1622
|
+
{
|
|
1623
|
+
className: `oc-down${isDept ? " foldable" : ""}${folded ? " folded" : ""}`,
|
|
1624
|
+
role: isDept ? "button" : void 0,
|
|
1625
|
+
tabIndex: isDept ? 0 : void 0,
|
|
1626
|
+
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,
|
|
1627
|
+
onClick: isDept ? (e) => {
|
|
1628
|
+
e.stopPropagation();
|
|
1629
|
+
cardProps.onToggleCollapse?.(a.id);
|
|
1630
|
+
} : void 0,
|
|
1631
|
+
onKeyDown: isDept ? (e) => {
|
|
1632
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1633
|
+
e.preventDefault();
|
|
1634
|
+
cardProps.onToggleCollapse?.(a.id);
|
|
1635
|
+
}
|
|
1636
|
+
} : void 0,
|
|
1637
|
+
children: isDept && /* @__PURE__ */ jsx("i", { className: "oc-fold-glyph", children: folded ? "\u25B8" : "\u25BE" })
|
|
1638
|
+
}
|
|
1639
|
+
),
|
|
1640
|
+
folded ? /* @__PURE__ */ jsxs(
|
|
1641
|
+
"button",
|
|
1642
|
+
{
|
|
1643
|
+
type: "button",
|
|
1644
|
+
className: "oc-folded-summary",
|
|
1645
|
+
onClick: (e) => {
|
|
1646
|
+
e.stopPropagation();
|
|
1647
|
+
cardProps.onToggleCollapse?.(a.id);
|
|
1648
|
+
},
|
|
1649
|
+
title: "click to expand \u2014 semantic zoom: department \u2192 role",
|
|
1650
|
+
children: [
|
|
1651
|
+
kids.length,
|
|
1652
|
+
" staff \xB7 folded to department level \u2014 click to expand"
|
|
1653
|
+
]
|
|
1654
|
+
}
|
|
1655
|
+
) : /* @__PURE__ */ jsx("div", { className: `oc-row${depth > 0 && kids.length > 3 ? " wrap" : ""}${depth === 0 ? " lanes" : ""}`, children: kids.map((k, i) => /* @__PURE__ */ jsx(
|
|
1656
|
+
Lane,
|
|
1657
|
+
{
|
|
1658
|
+
laneId: k.id,
|
|
1659
|
+
isLane: depth === 0,
|
|
1660
|
+
dragId: cardProps.dragId,
|
|
1661
|
+
onDropOn: cardProps.onDropOn,
|
|
1662
|
+
children: /* @__PURE__ */ jsx(ChartNode, { org, a: k, depth: depth + 1, order: i, ...cardProps })
|
|
1663
|
+
},
|
|
1664
|
+
k.id
|
|
1665
|
+
)) })
|
|
886
1666
|
] })
|
|
887
1667
|
] });
|
|
888
1668
|
}
|
|
1669
|
+
function BrandMark({ agent }) {
|
|
1670
|
+
const brand = (agent.data ?? {}).brand;
|
|
1671
|
+
const [failed, setFailed] = useState(false);
|
|
1672
|
+
if (typeof brand !== "string" || !brand || failed) {
|
|
1673
|
+
return /* @__PURE__ */ jsx("span", { className: "mac-emoji", children: agent.emoji || "\u2699\uFE0F" });
|
|
1674
|
+
}
|
|
1675
|
+
return /* @__PURE__ */ jsx(
|
|
1676
|
+
"img",
|
|
1677
|
+
{
|
|
1678
|
+
className: "mac-brand",
|
|
1679
|
+
src: `https://cdn.simpleicons.org/${brand}/000`,
|
|
1680
|
+
alt: "",
|
|
1681
|
+
loading: "lazy",
|
|
1682
|
+
draggable: false,
|
|
1683
|
+
onError: () => setFailed(true)
|
|
1684
|
+
}
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
889
1687
|
function Chip({ children, tone, onClick }) {
|
|
890
1688
|
return /* @__PURE__ */ jsx("span", { className: `chip${tone ? ` ${tone}` : ""}${onClick ? " tap" : ""}`, onClick, children });
|
|
891
1689
|
}
|
|
@@ -938,8 +1736,19 @@ function Dossier({
|
|
|
938
1736
|
)
|
|
939
1737
|
).then((counts) => setMemCount(counts.reduce((s, n) => s + n, 0)));
|
|
940
1738
|
fetchReports(api, id, 100).then(({ total, nodes }) => {
|
|
941
|
-
|
|
942
|
-
|
|
1739
|
+
if (total > 0) {
|
|
1740
|
+
setRepCount(total);
|
|
1741
|
+
setReports(nodes.slice(0, 4));
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
return fetchReports(api, null, 2e3).then(({ nodes: all }) => {
|
|
1745
|
+
const mine = all.filter((n) => reportAgentId(n) === id);
|
|
1746
|
+
setRepCount(mine.length);
|
|
1747
|
+
setReports(mine.slice(0, 4));
|
|
1748
|
+
});
|
|
1749
|
+
}).catch(() => {
|
|
1750
|
+
setRepCount(0);
|
|
1751
|
+
setReports([]);
|
|
943
1752
|
});
|
|
944
1753
|
}, [id, hasSnapshot, slice, api]);
|
|
945
1754
|
const mem = useCountUp(memCount ?? 0);
|
|
@@ -978,13 +1787,10 @@ function Dossier({
|
|
|
978
1787
|
return /* @__PURE__ */ jsxs("aside", { className: "doss", onClick: (e) => e.stopPropagation(), children: [
|
|
979
1788
|
/* @__PURE__ */ jsxs("div", { className: "doss-head", children: [
|
|
980
1789
|
/* @__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 })
|
|
1790
|
+
/* @__PURE__ */ jsxs("h2", { children: [
|
|
1791
|
+
a.name,
|
|
1792
|
+
" ",
|
|
1793
|
+
a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
|
|
988
1794
|
] }),
|
|
989
1795
|
/* @__PURE__ */ jsxs("div", { className: "doss-head-actions", children: [
|
|
990
1796
|
!edit && /* @__PURE__ */ jsx("button", { className: "doss-3d", title: "edit this agent", onClick: startEdit, children: "\u270E edit" }),
|
|
@@ -992,6 +1798,7 @@ function Dossier({
|
|
|
992
1798
|
/* @__PURE__ */ jsx("button", { className: "doss-close", title: "close the dossier", "aria-label": "close the dossier", onClick: onClose, children: "\u2715" })
|
|
993
1799
|
] })
|
|
994
1800
|
] }),
|
|
1801
|
+
a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role }),
|
|
995
1802
|
edit && /* @__PURE__ */ jsxs("div", { className: "doss-edit", children: [
|
|
996
1803
|
/* @__PURE__ */ jsxs("div", { className: "doss-edit-row2", children: [
|
|
997
1804
|
/* @__PURE__ */ jsxs("label", { children: [
|
|
@@ -1044,9 +1851,8 @@ function Dossier({
|
|
|
1044
1851
|
p.n
|
|
1045
1852
|
] }),
|
|
1046
1853
|
typeof a.cadence === "number" && /* @__PURE__ */ jsxs("span", { children: [
|
|
1047
|
-
"
|
|
1048
|
-
a.cadence
|
|
1049
|
-
"h"
|
|
1854
|
+
"reports ",
|
|
1855
|
+
everyN(a.cadence)
|
|
1050
1856
|
] })
|
|
1051
1857
|
] });
|
|
1052
1858
|
})(),
|
|
@@ -1217,7 +2023,8 @@ function OrgScreen({
|
|
|
1217
2023
|
hasSnapshot,
|
|
1218
2024
|
onUpdate,
|
|
1219
2025
|
onAdd,
|
|
1220
|
-
onRemove
|
|
2026
|
+
onRemove,
|
|
2027
|
+
showLaw
|
|
1221
2028
|
}) {
|
|
1222
2029
|
const api = useApi();
|
|
1223
2030
|
const [health, setHealth] = useState(null);
|
|
@@ -1226,10 +2033,27 @@ function OrgScreen({
|
|
|
1226
2033
|
fetchReports(api, null, 1e4).then(({ nodes }) => setHealth(buildHealthMap(nodes))).catch(() => {
|
|
1227
2034
|
});
|
|
1228
2035
|
}, [hasSnapshot, api]);
|
|
2036
|
+
const [hoverId, setHoverId] = useState(null);
|
|
2037
|
+
const litBuckets = useMemo(
|
|
2038
|
+
() => new Set(hoverId ? orgBootSlice(draft, hoverId)?.buckets ?? [] : []),
|
|
2039
|
+
[hoverId, draft]
|
|
2040
|
+
);
|
|
2041
|
+
const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
|
|
2042
|
+
const depts = useMemo(() => draft.agents.filter((a) => a.parent === draft.root), [draft]);
|
|
2043
|
+
const toggleCollapse = useCallback((id) => {
|
|
2044
|
+
setCollapsed((s) => {
|
|
2045
|
+
const n = new Set(s);
|
|
2046
|
+
n.has(id) ? n.delete(id) : n.add(id);
|
|
2047
|
+
return n;
|
|
2048
|
+
});
|
|
2049
|
+
}, []);
|
|
2050
|
+
const allFolded = depts.length > 0 && depts.every((d) => collapsed.has(d.id));
|
|
2051
|
+
const railVersion = `${draft.agents.length}:${[...collapsed].sort().join(",")}:${showLaw}:${health ? 1 : 0}:${selected ?? ""}`;
|
|
1229
2052
|
const fitRef = useRef(null);
|
|
1230
2053
|
const chartRef = useRef(null);
|
|
1231
2054
|
const [fit, setFit] = useState(1);
|
|
1232
2055
|
const [zoom, setZoom] = useState(null);
|
|
2056
|
+
const [nat, setNat] = useState({ w: 0, h: 0 });
|
|
1233
2057
|
const eff = zoom ?? fit;
|
|
1234
2058
|
useLayoutEffect(() => {
|
|
1235
2059
|
const vp = fitRef.current, chart = chartRef.current;
|
|
@@ -1239,15 +2063,18 @@ function OrgScreen({
|
|
|
1239
2063
|
const natW = chart.scrollWidth, natH = chart.scrollHeight;
|
|
1240
2064
|
const availW = vp.clientWidth - PAD * 2, availH = vp.clientHeight - PAD * 2;
|
|
1241
2065
|
if (natW <= 0 || natH <= 0) return;
|
|
1242
|
-
|
|
1243
|
-
|
|
2066
|
+
setNat((p) => p.w === natW && p.h === natH ? p : { w: natW, h: natH });
|
|
2067
|
+
const kW = availW / natW;
|
|
2068
|
+
const kH = availH / natH;
|
|
2069
|
+
const k = natH > availH * 1.35 ? kW : Math.min(kW, kH);
|
|
2070
|
+
setFit(Math.min(1, k > 0 ? k : 1));
|
|
1244
2071
|
};
|
|
1245
2072
|
measure();
|
|
1246
2073
|
const ro = new ResizeObserver(measure);
|
|
1247
2074
|
ro.observe(vp);
|
|
1248
2075
|
ro.observe(chart);
|
|
1249
2076
|
return () => ro.disconnect();
|
|
1250
|
-
}, [draft, health, selected]);
|
|
2077
|
+
}, [draft, health, selected, collapsed]);
|
|
1251
2078
|
const effRef = useRef(eff);
|
|
1252
2079
|
effRef.current = eff;
|
|
1253
2080
|
useEffect(() => {
|
|
@@ -1264,24 +2091,74 @@ function OrgScreen({
|
|
|
1264
2091
|
}, []);
|
|
1265
2092
|
const root = draft.agents.find((a) => a.id === draft.root);
|
|
1266
2093
|
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
|
-
|
|
2094
|
+
/* @__PURE__ */ jsxs("main", { className: `tree${showLaw ? " law-on" : ""}`, onClick: (e) => e.stopPropagation(), children: [
|
|
2095
|
+
/* @__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" }),
|
|
2096
|
+
/* @__PURE__ */ jsxs("div", { className: "ranks", children: [
|
|
2097
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2098
|
+
/* @__PURE__ */ jsx("b", { children: "I" }),
|
|
2099
|
+
"The standard"
|
|
2100
|
+
] }),
|
|
2101
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2102
|
+
/* @__PURE__ */ jsx("b", { children: "II" }),
|
|
2103
|
+
"The executive"
|
|
2104
|
+
] }),
|
|
2105
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2106
|
+
/* @__PURE__ */ jsx("b", { children: "III" }),
|
|
2107
|
+
"Departments"
|
|
2108
|
+
] }),
|
|
2109
|
+
/* @__PURE__ */ jsxs("div", { className: "rank", children: [
|
|
2110
|
+
/* @__PURE__ */ jsx("b", { children: "IV" }),
|
|
2111
|
+
"Staff & machines"
|
|
2112
|
+
] })
|
|
2113
|
+
] }),
|
|
2114
|
+
/* @__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(
|
|
2115
|
+
"div",
|
|
1271
2116
|
{
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
2117
|
+
className: "chart",
|
|
2118
|
+
ref: chartRef,
|
|
2119
|
+
style: {
|
|
2120
|
+
["--fit"]: eff,
|
|
2121
|
+
transform: zoom !== null ? `scale(${eff})` : `translateX(-50%) scale(${eff})`
|
|
2122
|
+
},
|
|
2123
|
+
children: [
|
|
2124
|
+
/* @__PURE__ */ jsx(CascadeRails, { chartRef, version: railVersion }),
|
|
2125
|
+
/* @__PURE__ */ jsxs("div", { className: "cascade", children: [
|
|
2126
|
+
/* @__PURE__ */ jsx(LawPlate, { org: draft, selected: selected === draft.root, onSelect: setSelected }),
|
|
2127
|
+
root && /* @__PURE__ */ jsx(
|
|
2128
|
+
ChartNode,
|
|
2129
|
+
{
|
|
2130
|
+
org: draft,
|
|
2131
|
+
a: root,
|
|
2132
|
+
depth: 0,
|
|
2133
|
+
order: 0,
|
|
2134
|
+
selected,
|
|
2135
|
+
dragId,
|
|
2136
|
+
health,
|
|
2137
|
+
onSelect: setSelected,
|
|
2138
|
+
onDragStart: setDragId,
|
|
2139
|
+
onDropOn: dropOn,
|
|
2140
|
+
onHover: setHoverId,
|
|
2141
|
+
showLaw,
|
|
2142
|
+
collapsed,
|
|
2143
|
+
onToggleCollapse: toggleCollapse
|
|
2144
|
+
}
|
|
2145
|
+
)
|
|
2146
|
+
] })
|
|
2147
|
+
]
|
|
1282
2148
|
}
|
|
1283
2149
|
) }) }),
|
|
1284
2150
|
/* @__PURE__ */ jsxs("div", { className: "zoomer", children: [
|
|
2151
|
+
/* @__PURE__ */ jsx(
|
|
2152
|
+
"button",
|
|
2153
|
+
{
|
|
2154
|
+
type: "button",
|
|
2155
|
+
className: allFolded ? "on" : "",
|
|
2156
|
+
title: allFolded ? "unfold every department \u2014 see roles again" : "fold every department to head-only \u2014 house \u2192 department",
|
|
2157
|
+
onClick: () => setCollapsed(allFolded ? /* @__PURE__ */ new Set() : new Set(depts.map((d) => d.id))),
|
|
2158
|
+
children: allFolded ? "\u2302 house" : "\u25BE roles"
|
|
2159
|
+
}
|
|
2160
|
+
),
|
|
2161
|
+
/* @__PURE__ */ jsx("span", { className: "zoomer-sep" }),
|
|
1285
2162
|
/* @__PURE__ */ jsx("button", { type: "button", title: "zoom out (ctrl+wheel)", onClick: () => setZoom(Math.max(0.2, eff / 1.25)), children: "\u2212" }),
|
|
1286
2163
|
/* @__PURE__ */ jsxs("span", { className: "zoomer-pct", children: [
|
|
1287
2164
|
Math.round(eff * 100),
|
|
@@ -1289,7 +2166,8 @@ function OrgScreen({
|
|
|
1289
2166
|
] }),
|
|
1290
2167
|
/* @__PURE__ */ jsx("button", { type: "button", title: "zoom in (ctrl+wheel)", onClick: () => setZoom(Math.min(2.5, eff * 1.25)), children: "\uFF0B" }),
|
|
1291
2168
|
/* @__PURE__ */ jsx("button", { type: "button", className: zoom === null ? "on" : "", title: "fit the whole org in view", onClick: () => setZoom(null), children: "fit" })
|
|
1292
|
-
] })
|
|
2169
|
+
] }),
|
|
2170
|
+
/* @__PURE__ */ jsx(LedgerShelf, { org: draft, lit: litBuckets })
|
|
1293
2171
|
] }),
|
|
1294
2172
|
selected && /* @__PURE__ */ jsx(
|
|
1295
2173
|
Dossier,
|
|
@@ -1418,8 +2296,8 @@ function ReportsScreen({ org, hasSnapshot }) {
|
|
|
1418
2296
|
if (!hasSnapshot) return;
|
|
1419
2297
|
fetchReports(api, null, 1e3).then(({ nodes }) => setRows(nodes)).catch(() => setRows([]));
|
|
1420
2298
|
}, [hasSnapshot, api]);
|
|
1421
|
-
const agents = useMemo(() => [...new Set((rows ?? []).map(
|
|
1422
|
-
const shown = (rows ?? []).filter((r) => !who || r
|
|
2299
|
+
const agents = useMemo(() => [...new Set((rows ?? []).map(reportAgentId))].filter(Boolean), [rows]);
|
|
2300
|
+
const shown = (rows ?? []).filter((r) => !who || reportAgentId(r) === who);
|
|
1423
2301
|
const total = useCountUp(shown.length);
|
|
1424
2302
|
if (!hasSnapshot)
|
|
1425
2303
|
return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
|
|
@@ -1450,15 +2328,16 @@ function ReportsScreen({ org, hasSnapshot }) {
|
|
|
1450
2328
|
")."
|
|
1451
2329
|
] }) : /* @__PURE__ */ jsxs("div", { className: "timeline", children: [
|
|
1452
2330
|
shown.slice(0, 100).map((r) => {
|
|
1453
|
-
const
|
|
2331
|
+
const filer = reportAgentId(r);
|
|
2332
|
+
const a = filer ? nameOf.get(filer) : void 0;
|
|
1454
2333
|
const when = relTime(nodeAt(r));
|
|
1455
2334
|
const sum = nodeSummary(r);
|
|
1456
|
-
return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(
|
|
2335
|
+
return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(filer || "x") }, children: [
|
|
1457
2336
|
/* @__PURE__ */ jsx("div", { className: "tl-dot" }),
|
|
1458
2337
|
/* @__PURE__ */ jsxs("div", { className: "tl-body", children: [
|
|
1459
2338
|
/* @__PURE__ */ jsxs("div", { className: "tl-top", children: [
|
|
1460
2339
|
/* @__PURE__ */ jsx("span", { className: "tl-ava", children: a?.emoji ?? "\u{1F916}" }),
|
|
1461
|
-
/* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ??
|
|
2340
|
+
/* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ?? filer ?? "unknown" }),
|
|
1462
2341
|
when && /* @__PURE__ */ jsx("span", { className: "tl-when", children: when })
|
|
1463
2342
|
] }),
|
|
1464
2343
|
/* @__PURE__ */ jsx("p", { className: "tl-sum", children: sum || r.label })
|
|
@@ -1558,7 +2437,8 @@ function App() {
|
|
|
1558
2437
|
api("/org").then((o) => {
|
|
1559
2438
|
setSaved(o);
|
|
1560
2439
|
setDraft(o);
|
|
1561
|
-
|
|
2440
|
+
const embed = new URLSearchParams(window.location.search).get("embed");
|
|
2441
|
+
if (!embed) setSelected(o.root);
|
|
1562
2442
|
}).catch(() => setErr("Can't load the organigram \u2014 is `booboo panel --org` running?"));
|
|
1563
2443
|
api("/stats").then(setStats).catch(() => setStats(null));
|
|
1564
2444
|
Promise.all([
|
|
@@ -1576,15 +2456,32 @@ function App() {
|
|
|
1576
2456
|
for (const a of draft.agents) {
|
|
1577
2457
|
const b = before.get(a.id);
|
|
1578
2458
|
if (!b) {
|
|
1579
|
-
out.push(`\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}`);
|
|
2459
|
+
out.push({ key: `add:${a.id}`, id: a.id, kind: "added", label: `\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}` });
|
|
1580
2460
|
continue;
|
|
1581
2461
|
}
|
|
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`);
|
|
2462
|
+
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"}` });
|
|
2463
|
+
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
2464
|
}
|
|
1585
|
-
for (const b of saved.agents) if (!after.has(b.id)) out.push(`\u2212 ${b.name} removed`);
|
|
2465
|
+
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
2466
|
return out;
|
|
1587
2467
|
}, [saved, draft]);
|
|
2468
|
+
const revertOne = useCallback((c) => {
|
|
2469
|
+
if (!saved || !draft) return;
|
|
2470
|
+
const was = saved.agents.find((a) => a.id === c.id);
|
|
2471
|
+
setDraft((d) => {
|
|
2472
|
+
if (!d) return d;
|
|
2473
|
+
if (c.kind === "added") return { ...d, agents: d.agents.filter((a) => a.id !== c.id) };
|
|
2474
|
+
if (c.kind === "removed" && was) return { ...d, agents: [...d.agents, was] };
|
|
2475
|
+
if (!was) return d;
|
|
2476
|
+
return {
|
|
2477
|
+
...d,
|
|
2478
|
+
agents: d.agents.map((a) => {
|
|
2479
|
+
if (a.id !== c.id) return a;
|
|
2480
|
+
return c.kind === "moved" ? { ...a, parent: was.parent } : { ...was, parent: a.parent };
|
|
2481
|
+
})
|
|
2482
|
+
};
|
|
2483
|
+
});
|
|
2484
|
+
}, [saved, draft]);
|
|
1588
2485
|
const updateAgent = useCallback((id, patch) => {
|
|
1589
2486
|
setDraft((d) => d ? { ...d, agents: d.agents.map((a) => a.id === id ? { ...a, ...patch } : a) } : d);
|
|
1590
2487
|
}, []);
|
|
@@ -1657,10 +2554,12 @@ function App() {
|
|
|
1657
2554
|
const memTotal = useCountUp(totals?.mem ?? 0, 1100);
|
|
1658
2555
|
const repTotal = useCountUp(totals?.rep ?? 0, 1300);
|
|
1659
2556
|
const [theme, toggleTheme] = useTheme();
|
|
2557
|
+
const [showLaw, setShowLaw] = useState(false);
|
|
1660
2558
|
if (err && !draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal", children: err });
|
|
1661
2559
|
if (!draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal calm", children: "waking the organigram\u2026" });
|
|
1662
2560
|
return /* @__PURE__ */ jsxs("div", { className: "pnl", children: [
|
|
1663
2561
|
/* @__PURE__ */ jsx("div", { className: "pnl-aurora", "aria-hidden": true }),
|
|
2562
|
+
/* @__PURE__ */ jsx(Constellation, {}),
|
|
1664
2563
|
/* @__PURE__ */ jsxs("header", { className: "bar", children: [
|
|
1665
2564
|
/* @__PURE__ */ jsxs("div", { className: "bar-brand", children: [
|
|
1666
2565
|
"\u{1F43E} ",
|
|
@@ -1685,6 +2584,16 @@ function App() {
|
|
|
1685
2584
|
] })
|
|
1686
2585
|
] }),
|
|
1687
2586
|
/* @__PURE__ */ jsxs("div", { className: "bar-actions", children: [
|
|
2587
|
+
tab === "org" && /* @__PURE__ */ jsx(
|
|
2588
|
+
"button",
|
|
2589
|
+
{
|
|
2590
|
+
className: `btn ghost law-toggle${showLaw ? " on" : ""}`,
|
|
2591
|
+
title: "trace rule inheritance \u2014 House Standard \u2192 SOP \u2192 role",
|
|
2592
|
+
"aria-pressed": showLaw,
|
|
2593
|
+
onClick: () => setShowLaw((v) => !v),
|
|
2594
|
+
children: "\u2696 show the law"
|
|
2595
|
+
}
|
|
2596
|
+
),
|
|
1688
2597
|
/* @__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
2598
|
changes.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1690
2599
|
/* @__PURE__ */ jsxs("span", { className: "bar-draft", children: [
|
|
@@ -1702,7 +2611,23 @@ function App() {
|
|
|
1702
2611
|
" ",
|
|
1703
2612
|
t.label
|
|
1704
2613
|
] }, t.id)) }),
|
|
1705
|
-
changes.length > 0 && tab === "org" && /* @__PURE__ */
|
|
2614
|
+
changes.length > 0 && tab === "org" && /* @__PURE__ */ jsxs("div", { className: "pending", children: [
|
|
2615
|
+
/* @__PURE__ */ jsx("span", { className: "pending-lead", children: "unapplied" }),
|
|
2616
|
+
changes.map((c) => /* @__PURE__ */ jsxs(
|
|
2617
|
+
"button",
|
|
2618
|
+
{
|
|
2619
|
+
type: "button",
|
|
2620
|
+
className: "pending-item",
|
|
2621
|
+
title: "undo just this change",
|
|
2622
|
+
onClick: () => revertOne(c),
|
|
2623
|
+
children: [
|
|
2624
|
+
c.label,
|
|
2625
|
+
/* @__PURE__ */ jsx("i", { "aria-hidden": "true", children: "undo" })
|
|
2626
|
+
]
|
|
2627
|
+
},
|
|
2628
|
+
c.key
|
|
2629
|
+
))
|
|
2630
|
+
] }),
|
|
1706
2631
|
err && /* @__PURE__ */ jsx("div", { className: "pnl-err", children: err }),
|
|
1707
2632
|
/* @__PURE__ */ jsxs("div", { className: "content", children: [
|
|
1708
2633
|
tab === "org" && /* @__PURE__ */ jsx(
|
|
@@ -1717,7 +2642,8 @@ function App() {
|
|
|
1717
2642
|
hasSnapshot: !!stats,
|
|
1718
2643
|
onUpdate: updateAgent,
|
|
1719
2644
|
onAdd: addAgent,
|
|
1720
|
-
onRemove: removeAgent
|
|
2645
|
+
onRemove: removeAgent,
|
|
2646
|
+
showLaw
|
|
1721
2647
|
}
|
|
1722
2648
|
),
|
|
1723
2649
|
tab === "buckets" && /* @__PURE__ */ jsx(BucketsScreen, { org: draft, param, hasSnapshot: !!stats }),
|
|
@@ -1734,8 +2660,67 @@ function Panel({ api = defaultApi } = {}) {
|
|
|
1734
2660
|
] });
|
|
1735
2661
|
}
|
|
1736
2662
|
|
|
2663
|
+
// package.json
|
|
2664
|
+
var package_default = {
|
|
2665
|
+
name: "@booboo-brain/panel",
|
|
2666
|
+
version: "0.5.4",
|
|
2667
|
+
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.",
|
|
2668
|
+
license: "MIT",
|
|
2669
|
+
repository: {
|
|
2670
|
+
type: "git",
|
|
2671
|
+
url: "git+https://github.com/jessedu29260-netizen/booboo.git",
|
|
2672
|
+
directory: "packages/panel"
|
|
2673
|
+
},
|
|
2674
|
+
bugs: {
|
|
2675
|
+
url: "https://github.com/jessedu29260-netizen/booboo/issues"
|
|
2676
|
+
},
|
|
2677
|
+
homepage: "https://github.com/jessedu29260-netizen/booboo#readme",
|
|
2678
|
+
type: "module",
|
|
2679
|
+
main: "./dist/index.js",
|
|
2680
|
+
module: "./dist/index.js",
|
|
2681
|
+
types: "./dist/index.d.ts",
|
|
2682
|
+
exports: {
|
|
2683
|
+
".": {
|
|
2684
|
+
types: "./dist/index.d.ts",
|
|
2685
|
+
import: "./dist/index.js"
|
|
2686
|
+
},
|
|
2687
|
+
"./package.json": "./package.json"
|
|
2688
|
+
},
|
|
2689
|
+
files: [
|
|
2690
|
+
"dist",
|
|
2691
|
+
"dist-app"
|
|
2692
|
+
],
|
|
2693
|
+
scripts: {
|
|
2694
|
+
build: "tsup src/index.ts --format esm --dts --clean --external react,react-dom,@booboo-brain/spec && vite build --config vite.app.config.ts",
|
|
2695
|
+
dev: "vite"
|
|
2696
|
+
},
|
|
2697
|
+
peerDependencies: {
|
|
2698
|
+
react: ">=18",
|
|
2699
|
+
"react-dom": ">=18"
|
|
2700
|
+
},
|
|
2701
|
+
dependencies: {
|
|
2702
|
+
"@booboo-brain/spec": "workspace:*"
|
|
2703
|
+
},
|
|
2704
|
+
devDependencies: {
|
|
2705
|
+
react: "^19.0.0",
|
|
2706
|
+
"react-dom": "^19.0.0",
|
|
2707
|
+
"@types/react": "^19.0.0",
|
|
2708
|
+
"@types/react-dom": "^19.0.0",
|
|
2709
|
+
vite: "^6.0.0",
|
|
2710
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
2711
|
+
tsup: "^8",
|
|
2712
|
+
typescript: "^5"
|
|
2713
|
+
},
|
|
2714
|
+
publishConfig: {
|
|
2715
|
+
access: "public"
|
|
2716
|
+
},
|
|
2717
|
+
engines: {
|
|
2718
|
+
node: ">=18"
|
|
2719
|
+
}
|
|
2720
|
+
};
|
|
2721
|
+
|
|
1737
2722
|
// src/index.ts
|
|
1738
|
-
var PANEL_VERSION =
|
|
2723
|
+
var PANEL_VERSION = package_default.version;
|
|
1739
2724
|
export {
|
|
1740
2725
|
PANEL_VERSION,
|
|
1741
2726
|
Panel
|