@fonderie/react-admin-screens 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/screens/AdminShell.tsx
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/react-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/react-courier-admin-screens";
4
- import { useState as useState5 } from "react";
4
+ import { useEffect, useState as useState7 } from "react";
5
5
 
6
6
  // src/styles.ts
7
7
  var t = {
@@ -23,6 +23,31 @@ var t = {
23
23
  trackingDisplay: "var(--fonderie-tracking-display,-0.05em)",
24
24
  shadowCard: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
25
25
  };
26
+ var tint = (token, pct) => `color-mix(in srgb, ${token} ${pct}%, transparent)`;
27
+ var toneColor = {
28
+ ok: t.accentStrong,
29
+ bad: t.danger,
30
+ // Amber on white fails contrast; mixing in the text colour darkens it in
31
+ // light mode and lightens it in dark, from the same two tokens.
32
+ warn: `color-mix(in srgb, ${t.warning} 70%, ${t.text})`,
33
+ neutral: t.muted,
34
+ info: t.text
35
+ };
36
+ var control = {
37
+ display: "inline-flex",
38
+ alignItems: "center",
39
+ justifyContent: "center",
40
+ gap: 6,
41
+ height: 32,
42
+ boxSizing: "border-box",
43
+ borderRadius: 6,
44
+ padding: "0 12px",
45
+ fontSize: 13,
46
+ fontWeight: 500,
47
+ fontFamily: "inherit",
48
+ lineHeight: 1,
49
+ whiteSpace: "nowrap"
50
+ };
26
51
  var styles = {
27
52
  shell: {
28
53
  display: "flex",
@@ -31,155 +56,578 @@ var styles = {
31
56
  color: t.text,
32
57
  background: t.bg
33
58
  },
34
- // The sidebar sits on the raised surface against the page canvas, which is
35
- // what separates the two without a heavy rule between them.
59
+ // Sticky, full height, scrolls on its own: the nav never scrolls away with
60
+ // a long table, and the footer (session controls) stays in reach.
36
61
  nav: {
37
- width: 220,
38
- borderRight: `1px solid ${t.border}`,
39
- padding: 16,
62
+ width: 248,
40
63
  flexShrink: 0,
41
- background: t.surface
64
+ boxSizing: "border-box",
65
+ borderRight: `1px solid ${t.border}`,
66
+ background: t.surface,
67
+ position: "sticky",
68
+ top: 0,
69
+ height: "100vh",
70
+ display: "flex",
71
+ flexDirection: "column"
42
72
  },
73
+ navBrand: {
74
+ display: "flex",
75
+ alignItems: "center",
76
+ gap: 10,
77
+ padding: "18px 16px 14px",
78
+ borderBottom: `1px solid ${t.borderLight}`
79
+ },
80
+ navMark: {
81
+ width: 28,
82
+ height: 28,
83
+ borderRadius: 7,
84
+ background: t.text,
85
+ color: t.surface,
86
+ display: "grid",
87
+ placeItems: "center",
88
+ flexShrink: 0
89
+ },
90
+ navBrandTitle: {
91
+ fontSize: 14,
92
+ fontWeight: 600,
93
+ letterSpacing: t.trackingDisplay,
94
+ lineHeight: 1.2,
95
+ overflow: "hidden",
96
+ textOverflow: "ellipsis",
97
+ whiteSpace: "nowrap"
98
+ },
99
+ navScroll: { flex: 1, overflowY: "auto", padding: "8px 10px 16px" },
43
100
  navGroup: {
44
101
  fontSize: 11,
45
102
  textTransform: "uppercase",
46
103
  letterSpacing: 0.6,
47
104
  color: t.muted,
48
- margin: "16px 0 6px",
105
+ margin: "16px 8px 4px",
49
106
  fontWeight: 600
50
107
  },
51
108
  navItem: {
52
- display: "block",
109
+ display: "flex",
110
+ alignItems: "center",
111
+ gap: 10,
53
112
  width: "100%",
54
113
  textAlign: "left",
55
114
  background: "none",
56
115
  border: "none",
57
- borderRadius: t.radius,
58
- padding: "6px 8px",
116
+ borderRadius: 6,
117
+ padding: "7px 8px",
59
118
  cursor: "pointer",
60
- fontSize: 14,
61
- color: t.text,
62
- fontFamily: "inherit"
119
+ fontSize: 13.5,
120
+ fontWeight: 500,
121
+ color: t.muted,
122
+ fontFamily: "inherit",
123
+ position: "relative"
124
+ },
125
+ navItemActive: { background: tint(t.accent, 12), color: t.text, fontWeight: 600 },
126
+ navIconActive: { color: t.accentStrong },
127
+ navFooter: {
128
+ borderTop: `1px solid ${t.borderLight}`,
129
+ padding: 12,
130
+ display: "flex",
131
+ flexDirection: "column",
132
+ gap: 10
133
+ },
134
+ // Phone layout: a top bar with the menu button replaces the sidebar, which
135
+ // opens as a drawer over the content.
136
+ topbar: {
137
+ position: "sticky",
138
+ top: 0,
139
+ zIndex: 20,
140
+ display: "flex",
141
+ alignItems: "center",
142
+ gap: 10,
143
+ padding: "10px 16px",
144
+ background: t.surface,
145
+ borderBottom: `1px solid ${t.border}`
146
+ },
147
+ drawer: {
148
+ position: "fixed",
149
+ inset: 0,
150
+ zIndex: 30,
151
+ display: "flex"
152
+ },
153
+ scrim: { flex: 1, background: "rgba(0,0,0,.35)" },
154
+ main: { flex: 1, minWidth: 0 },
155
+ container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
156
+ // ── page header ──
157
+ header: {
158
+ display: "flex",
159
+ alignItems: "flex-start",
160
+ justifyContent: "space-between",
161
+ gap: 16,
162
+ marginBottom: 24,
163
+ flexWrap: "wrap"
63
164
  },
64
- navItemActive: { background: t.surfaceAlt, fontWeight: 600, color: t.accentStrong },
65
- main: { flex: 1, padding: 24, maxWidth: 960 },
66
- container: { padding: 24, maxWidth: 960 },
67
165
  title: {
68
- fontSize: 20,
166
+ fontSize: 22,
69
167
  fontWeight: 600,
70
- marginTop: 0,
71
- marginBottom: 12,
72
- letterSpacing: t.trackingDisplay
168
+ margin: 0,
169
+ letterSpacing: t.trackingDisplay,
170
+ lineHeight: 1.25
73
171
  },
172
+ lead: { color: t.muted, fontSize: 13.5, margin: "4px 0 0", lineHeight: 1.5 },
173
+ actions: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
74
174
  subtitle: {
75
175
  fontSize: 15,
76
176
  fontWeight: 600,
77
- marginTop: 24,
78
- marginBottom: 8,
79
- letterSpacing: t.trackingDisplay
177
+ marginTop: 32,
178
+ marginBottom: 10,
179
+ letterSpacing: t.trackingDisplay,
180
+ display: "flex",
181
+ alignItems: "center",
182
+ gap: 8
183
+ },
184
+ // ── feedback ──
185
+ status: { padding: "24px 0", color: t.muted, fontSize: 13.5 },
186
+ error: {
187
+ color: t.danger,
188
+ background: tint(t.danger, 8),
189
+ border: `1px solid ${tint(t.danger, 25)}`,
190
+ borderRadius: t.radiusLg,
191
+ padding: "10px 14px",
192
+ marginBottom: 16,
193
+ fontSize: 13.5
194
+ },
195
+ notice: {
196
+ color: t.text,
197
+ background: tint(t.warning, 10),
198
+ border: `1px solid ${tint(t.warning, 35)}`,
199
+ borderRadius: t.radiusLg,
200
+ padding: "10px 14px",
201
+ marginBottom: 16,
202
+ fontSize: 13.5
80
203
  },
81
- status: { padding: 12, color: t.muted },
82
- error: { color: t.danger, marginBottom: 12, fontSize: 14 },
83
204
  ok: { color: t.accentStrong, fontWeight: 600 },
84
205
  bad: { color: t.danger, fontWeight: 600 },
85
206
  advice: { color: t.warning },
86
207
  muted: { color: t.muted, fontSize: 13 },
87
- mono: { fontFamily: t.mono, fontSize: 13 },
88
- table: { width: "100%", borderCollapse: "collapse", fontSize: 14 },
208
+ mono: { fontFamily: t.mono, fontSize: 12.5 },
209
+ code: {
210
+ fontFamily: t.mono,
211
+ fontSize: 12,
212
+ background: t.surfaceAlt,
213
+ border: `1px solid ${t.borderLight}`,
214
+ borderRadius: 4,
215
+ padding: "1px 5px"
216
+ },
217
+ // ── surfaces ──
218
+ // Tables and lists are cards themselves, so every screen gets the framed,
219
+ // rounded look without wrapping each one. The last row's rule is removed in
220
+ // the shell stylesheet (a pseudo-selector); embedded without the shell it
221
+ // simply doubles against the frame.
222
+ table: {
223
+ width: "100%",
224
+ borderCollapse: "separate",
225
+ borderSpacing: 0,
226
+ fontSize: 13.5,
227
+ background: t.surface,
228
+ border: `1px solid ${t.border}`,
229
+ borderRadius: t.radiusLg,
230
+ overflow: "hidden",
231
+ boxShadow: t.shadowCard
232
+ },
89
233
  th: {
90
234
  textAlign: "left",
91
235
  borderBottom: `1px solid ${t.border}`,
92
- padding: "6px 8px",
236
+ padding: "9px 14px",
93
237
  fontWeight: 600,
94
- color: t.muted
238
+ fontSize: 11.5,
239
+ textTransform: "uppercase",
240
+ letterSpacing: 0.4,
241
+ color: t.muted,
242
+ background: t.surfaceAlt,
243
+ whiteSpace: "nowrap"
244
+ },
245
+ td: {
246
+ borderBottom: `1px solid ${t.borderLight}`,
247
+ padding: "11px 14px",
248
+ verticalAlign: "top"
249
+ },
250
+ list: {
251
+ listStyle: "none",
252
+ padding: 0,
253
+ margin: 0,
254
+ background: t.surface,
255
+ border: `1px solid ${t.border}`,
256
+ borderRadius: t.radiusLg,
257
+ overflow: "hidden",
258
+ boxShadow: t.shadowCard
259
+ },
260
+ row: { borderBottom: `1px solid ${t.borderLight}`, padding: "12px 16px" },
261
+ card: {
262
+ background: t.surface,
263
+ border: `1px solid ${t.border}`,
264
+ borderRadius: t.radiusLg,
265
+ padding: 16,
266
+ boxShadow: t.shadowCard
267
+ },
268
+ grid: {
269
+ display: "grid",
270
+ gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))",
271
+ gap: 12,
272
+ marginBottom: 24
273
+ },
274
+ statLabel: { fontSize: 12, color: t.muted, fontWeight: 500 },
275
+ statValue: {
276
+ fontSize: 26,
277
+ fontWeight: 600,
278
+ letterSpacing: t.trackingDisplay,
279
+ lineHeight: 1.2,
280
+ marginTop: 6
281
+ },
282
+ statHint: { fontSize: 12, color: t.muted, marginTop: 2 },
283
+ empty: {
284
+ background: t.surface,
285
+ border: `1px dashed ${t.border}`,
286
+ borderRadius: t.radiusLg,
287
+ padding: "40px 24px",
288
+ textAlign: "center",
289
+ color: t.muted,
290
+ fontSize: 13.5
291
+ },
292
+ emptyTitle: { color: t.text, fontWeight: 600, fontSize: 14.5, margin: "10px 0 4px" },
293
+ dl: { display: "grid", gridTemplateColumns: "160px 1fr", margin: 0 },
294
+ dt: {
295
+ color: t.muted,
296
+ fontSize: 13,
297
+ padding: "10px 16px",
298
+ borderBottom: `1px solid ${t.borderLight}`
299
+ },
300
+ dd: {
301
+ margin: 0,
302
+ padding: "10px 16px",
303
+ borderBottom: `1px solid ${t.borderLight}`,
304
+ fontSize: 13.5,
305
+ minWidth: 0
95
306
  },
96
- td: { borderBottom: `1px solid ${t.borderLight}`, padding: "6px 8px", verticalAlign: "top" },
97
- list: { listStyle: "none", padding: 0, margin: 0 },
98
- row: { borderBottom: `1px solid ${t.borderLight}`, padding: "8px 0" },
99
307
  badge: {
100
- display: "inline-block",
308
+ display: "inline-flex",
309
+ alignItems: "center",
310
+ gap: 5,
101
311
  borderRadius: 999,
102
- padding: "1px 8px",
103
- fontSize: 12,
312
+ padding: "2px 8px",
313
+ fontSize: 11.5,
314
+ fontWeight: 500,
315
+ lineHeight: 1.5,
104
316
  background: t.surfaceAlt,
105
317
  border: `1px solid ${t.border}`,
106
- color: t.muted
318
+ color: t.muted,
319
+ whiteSpace: "nowrap"
320
+ },
321
+ pill: {
322
+ display: "inline-flex",
323
+ alignItems: "center",
324
+ gap: 6,
325
+ borderRadius: 999,
326
+ padding: "2px 9px 2px 8px",
327
+ fontSize: 12,
328
+ fontWeight: 600,
329
+ lineHeight: 1.5,
330
+ whiteSpace: "nowrap",
331
+ verticalAlign: "baseline"
107
332
  },
333
+ dot: { width: 6, height: 6, borderRadius: 999, flexShrink: 0, background: "currentColor" },
334
+ // ── controls ──
108
335
  button: {
336
+ ...control,
109
337
  background: t.surface,
110
338
  border: `1px solid ${t.border}`,
111
- borderRadius: t.radius,
112
- padding: "4px 10px",
113
- cursor: "pointer",
114
- fontSize: 13,
115
339
  color: t.text,
116
- fontFamily: "inherit",
340
+ cursor: "pointer",
117
341
  boxShadow: t.shadowCard
118
342
  },
119
- toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 12 },
120
- // Surfaces the screens compose themselves, kept here so a card looks the
121
- // same on every page rather than being re-invented per screen.
122
- card: {
343
+ buttonPrimary: {
344
+ ...control,
345
+ background: t.text,
346
+ border: `1px solid ${t.text}`,
347
+ color: t.surface,
348
+ cursor: "pointer"
349
+ },
350
+ buttonDanger: {
351
+ ...control,
123
352
  background: t.surface,
353
+ border: `1px solid ${tint(t.danger, 40)}`,
354
+ color: t.danger,
355
+ cursor: "pointer"
356
+ },
357
+ buttonGhost: {
358
+ ...control,
359
+ background: "none",
360
+ border: "1px solid transparent",
361
+ color: t.muted,
362
+ cursor: "pointer",
363
+ padding: "0 8px"
364
+ },
365
+ link: {
366
+ background: "none",
367
+ border: "none",
368
+ padding: 0,
369
+ color: t.text,
370
+ fontFamily: "inherit",
371
+ fontSize: "inherit",
372
+ fontWeight: 500,
373
+ cursor: "pointer",
374
+ textAlign: "left"
375
+ },
376
+ input: {
377
+ height: 32,
378
+ boxSizing: "border-box",
124
379
  border: `1px solid ${t.border}`,
125
- borderRadius: t.radiusLg,
126
- padding: 16,
127
- boxShadow: t.shadowCard
380
+ borderRadius: 6,
381
+ padding: "0 10px",
382
+ fontSize: 13.5,
383
+ fontFamily: "inherit",
384
+ background: t.surface,
385
+ color: t.text,
386
+ minWidth: 0
128
387
  },
388
+ toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 16, flexWrap: "wrap" },
129
389
  accent: { color: t.accent }
130
390
  };
131
391
 
392
+ // src/icons.ts
393
+ var ICONS = {
394
+ attention: ["M22 12h-4l-3 9L9 3l-3 9H2"],
395
+ modules: [
396
+ "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
397
+ "M3.3 7 12 12l8.7-5",
398
+ "M12 22V12"
399
+ ],
400
+ environment: [
401
+ "M4 21v-7",
402
+ "M4 10V3",
403
+ "M12 21v-9",
404
+ "M12 8V3",
405
+ "M20 21v-5",
406
+ "M20 12V3",
407
+ "M1 14h6",
408
+ "M9 8h6",
409
+ "M17 16h6"
410
+ ],
411
+ doctor: [
412
+ "M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"
413
+ ],
414
+ routes: [
415
+ "M6 3v12",
416
+ "M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
417
+ "M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
418
+ "M18 9a9 9 0 0 1-9 9"
419
+ ],
420
+ users: [
421
+ "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",
422
+ "M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z",
423
+ "M22 21v-2a4 4 0 0 0-3-3.87",
424
+ "M16 3.13a4 4 0 0 1 0 7.75"
425
+ ],
426
+ catalog: [
427
+ "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z",
428
+ "M7.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
429
+ ],
430
+ subscriber: ["M2 7h20v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z", "M2 11h20", "M6 15h4"],
431
+ settings: [
432
+ "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",
433
+ "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
434
+ ],
435
+ templates: [
436
+ "M22 7l-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7",
437
+ "M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"
438
+ ],
439
+ audit: ["M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z", "M9 12l2 2 4-4"],
440
+ log: ["M12 8v4l3 3", "M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5"],
441
+ tokens: [
442
+ "M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z",
443
+ "M16.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
444
+ ],
445
+ migrations: [
446
+ "M12 22c4.97 0 9-1.79 9-4V6",
447
+ "M3 6v12c0 2.21 4.03 4 9 4",
448
+ "M12 10c4.97 0 9-1.79 9-4s-4.03-4-9-4-9 1.79-9 4 4.03 4 9 4z",
449
+ "M3 12c0 2.21 4.03 4 9 4s9-1.79 9-4"
450
+ ],
451
+ menu: ["M4 6h16", "M4 12h16", "M4 18h16"],
452
+ close: ["M18 6 6 18", "m6 6 12 12"],
453
+ refresh: [
454
+ "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
455
+ "M21 3v5h-5",
456
+ "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
457
+ "M8 16H3v5"
458
+ ],
459
+ check: ["M20 6 9 17l-5-5"],
460
+ alert: [
461
+ "M12 9v4",
462
+ "M12 17h.01",
463
+ "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
464
+ ],
465
+ search: ["M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", "m21 21-4.3-4.3"],
466
+ back: ["m12 19-7-7 7-7", "M19 12H5"],
467
+ logout: ["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", "m16 17 5-5-5-5", "M21 12H9"],
468
+ lock: [
469
+ "M5 11h14a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2z",
470
+ "M7 11V7a5 5 0 0 1 10 0v4"
471
+ ],
472
+ plus: ["M12 5v14", "M5 12h14"],
473
+ mark: [
474
+ "M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z",
475
+ "M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z",
476
+ "M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z",
477
+ "M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z"
478
+ ]
479
+ };
480
+
481
+ // src/ui.tsx
482
+ import { jsx, jsxs } from "react/jsx-runtime";
483
+ function Icon({
484
+ name,
485
+ size = 16,
486
+ style
487
+ }) {
488
+ return /* @__PURE__ */ jsx(
489
+ "svg",
490
+ {
491
+ width: size,
492
+ height: size,
493
+ viewBox: "0 0 24 24",
494
+ fill: "none",
495
+ stroke: "currentColor",
496
+ strokeWidth: 2,
497
+ strokeLinecap: "round",
498
+ strokeLinejoin: "round",
499
+ "aria-hidden": "true",
500
+ style: { flexShrink: 0, ...style },
501
+ children: ICONS[name].map((d) => /* @__PURE__ */ jsx("path", { d }, d))
502
+ }
503
+ );
504
+ }
505
+ function PageHeader({
506
+ title,
507
+ lead,
508
+ actions
509
+ }) {
510
+ return /* @__PURE__ */ jsxs("div", { style: styles.header, children: [
511
+ /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
512
+ /* @__PURE__ */ jsx("h1", { style: styles.title, children: title }),
513
+ lead ? /* @__PURE__ */ jsx("p", { style: styles.lead, children: lead }) : null
514
+ ] }),
515
+ actions ? /* @__PURE__ */ jsx("div", { style: styles.actions, children: actions }) : null
516
+ ] });
517
+ }
518
+ function Pill({
519
+ tone,
520
+ children,
521
+ dot = true
522
+ }) {
523
+ const color = toneColor[tone];
524
+ return /* @__PURE__ */ jsxs("span", { style: { ...styles.pill, color, background: tint(color, tone === "neutral" ? 10 : 13) }, children: [
525
+ dot ? /* @__PURE__ */ jsx("span", { style: styles.dot }) : null,
526
+ children
527
+ ] });
528
+ }
529
+ function Stat({
530
+ label,
531
+ value,
532
+ hint,
533
+ tone
534
+ }) {
535
+ return /* @__PURE__ */ jsxs("div", { style: styles.card, children: [
536
+ /* @__PURE__ */ jsx("div", { style: styles.statLabel, children: label }),
537
+ /* @__PURE__ */ jsx("div", { style: { ...styles.statValue, ...tone ? { color: toneColor[tone] } : {} }, children: value }),
538
+ hint ? /* @__PURE__ */ jsx("div", { style: styles.statHint, children: hint }) : null
539
+ ] });
540
+ }
541
+ function Empty({
542
+ icon = "check",
543
+ title,
544
+ children
545
+ }) {
546
+ return /* @__PURE__ */ jsxs("div", { style: styles.empty, children: [
547
+ /* @__PURE__ */ jsx(Icon, { name: icon, size: 22 }),
548
+ /* @__PURE__ */ jsx("div", { style: styles.emptyTitle, children: title }),
549
+ children ? /* @__PURE__ */ jsx("div", { children }) : null
550
+ ] });
551
+ }
552
+ function RefreshButton({
553
+ onClick,
554
+ busy,
555
+ label = "Refresh"
556
+ }) {
557
+ return /* @__PURE__ */ jsxs("button", { type: "button", style: styles.button, onClick, disabled: busy, children: [
558
+ /* @__PURE__ */ jsx(Icon, { name: "refresh", size: 14 }),
559
+ busy ? "Working\u2026" : label
560
+ ] });
561
+ }
562
+ function MethodChip({ method }) {
563
+ const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
564
+ const color = toneColor[tone];
565
+ return /* @__PURE__ */ jsx(
566
+ "span",
567
+ {
568
+ style: {
569
+ ...styles.mono,
570
+ fontSize: 11,
571
+ fontWeight: 700,
572
+ color,
573
+ background: tint(color, 12),
574
+ borderRadius: 4,
575
+ padding: "2px 6px",
576
+ display: "inline-block",
577
+ minWidth: 44,
578
+ textAlign: "center"
579
+ },
580
+ children: method
581
+ }
582
+ );
583
+ }
584
+
132
585
  // src/screens/AdminLogScreen.tsx
133
586
  import { useAdminLog } from "@fonderie/react-admin";
134
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
587
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
135
588
  function AdminLogScreen({ client, pageSize = 50 }) {
136
589
  const { entries, hasMore, isLoading, error, refresh, loadMore } = useAdminLog(client, {
137
590
  limit: pageSize
138
591
  });
139
- return /* @__PURE__ */ jsxs("div", { style: styles.container, children: [
140
- /* @__PURE__ */ jsxs("div", { style: styles.toolbar, children: [
141
- /* @__PURE__ */ jsx("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Admin log" }),
142
- /* @__PURE__ */ jsx(
143
- "button",
144
- {
145
- type: "button",
146
- style: styles.button,
147
- onClick: () => void refresh(),
148
- disabled: isLoading,
149
- children: "Refresh"
150
- }
151
- )
152
- ] }),
153
- error ? /* @__PURE__ */ jsx("p", { style: styles.error, role: "alert", children: error.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.explanation }) : /* @__PURE__ */ jsxs(Fragment, { children: [
154
- /* @__PURE__ */ jsxs("table", { style: styles.table, children: [
155
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
156
- /* @__PURE__ */ jsx("th", { style: styles.th, children: "When" }),
157
- /* @__PURE__ */ jsx("th", { style: styles.th, children: "Actor" }),
158
- /* @__PURE__ */ jsx("th", { style: styles.th, children: "Request" }),
159
- /* @__PURE__ */ jsx("th", { style: styles.th, children: "Status" }),
160
- /* @__PURE__ */ jsx("th", { style: styles.th, children: "Module" })
592
+ return /* @__PURE__ */ jsxs2("div", { style: styles.container, children: [
593
+ /* @__PURE__ */ jsx2(
594
+ PageHeader,
595
+ {
596
+ title: "Admin log",
597
+ lead: "Every request made through this surface, newest first \u2014 refused ones included.",
598
+ actions: /* @__PURE__ */ jsx2(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
599
+ }
600
+ ),
601
+ error ? /* @__PURE__ */ jsx2("p", { style: styles.error, role: "alert", children: error.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.explanation }) : entries.length === 0 && !isLoading ? /* @__PURE__ */ jsx2(Empty, { icon: "log", title: "No requests yet" }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
602
+ /* @__PURE__ */ jsxs2("table", { style: styles.table, children: [
603
+ /* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
604
+ /* @__PURE__ */ jsx2("th", { style: styles.th, children: "When" }),
605
+ /* @__PURE__ */ jsx2("th", { style: styles.th, children: "Actor" }),
606
+ /* @__PURE__ */ jsx2("th", { style: styles.th, children: "Request" }),
607
+ /* @__PURE__ */ jsx2("th", { style: styles.th, children: "Status" }),
608
+ /* @__PURE__ */ jsx2("th", { style: styles.th, children: "Module" })
161
609
  ] }) }),
162
- /* @__PURE__ */ jsx("tbody", { children: entries.map((e) => /* @__PURE__ */ jsxs("tr", { children: [
163
- /* @__PURE__ */ jsx("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.at).toLocaleString() }),
164
- /* @__PURE__ */ jsx("td", { style: styles.td, children: e.actor }),
165
- /* @__PURE__ */ jsxs("td", { style: { ...styles.td, ...styles.mono }, children: [
166
- e.method,
610
+ /* @__PURE__ */ jsx2("tbody", { children: entries.map((e) => /* @__PURE__ */ jsxs2("tr", { children: [
611
+ /* @__PURE__ */ jsx2("td", { style: { ...styles.td, ...styles.muted, whiteSpace: "nowrap" }, children: new Date(e.at).toLocaleString() }),
612
+ /* @__PURE__ */ jsx2("td", { style: styles.td, children: e.actor }),
613
+ /* @__PURE__ */ jsxs2("td", { style: styles.td, children: [
614
+ /* @__PURE__ */ jsx2(MethodChip, { method: e.method }),
167
615
  " ",
168
- e.path
616
+ /* @__PURE__ */ jsx2("span", { style: { ...styles.mono, wordBreak: "break-all" }, children: e.path })
169
617
  ] }),
170
- /* @__PURE__ */ jsxs("td", { style: styles.td, children: [
171
- /* @__PURE__ */ jsx("span", { style: e.status >= 400 ? styles.bad : styles.ok, children: e.status }),
618
+ /* @__PURE__ */ jsxs2("td", { style: { ...styles.td, whiteSpace: "nowrap" }, children: [
619
+ /* @__PURE__ */ jsx2(Pill, { tone: e.status >= 500 ? "bad" : e.status >= 400 ? "warn" : "ok", children: e.status }),
172
620
  " ",
173
- /* @__PURE__ */ jsxs("span", { style: styles.muted, children: [
621
+ /* @__PURE__ */ jsxs2("span", { style: styles.muted, children: [
174
622
  e.durationMs,
175
623
  " ms"
176
624
  ] })
177
625
  ] }),
178
- /* @__PURE__ */ jsx("td", { style: { ...styles.td, ...styles.muted }, children: e.module })
626
+ /* @__PURE__ */ jsx2("td", { style: { ...styles.td, ...styles.muted }, children: e.module })
179
627
  ] }, e.id)) })
180
628
  ] }),
181
- isLoading ? /* @__PURE__ */ jsx("p", { style: styles.status, children: "Loading\u2026" }) : null,
182
- hasMore && !isLoading ? /* @__PURE__ */ jsx(
629
+ isLoading ? /* @__PURE__ */ jsx2("p", { style: styles.status, children: "Loading\u2026" }) : null,
630
+ hasMore && !isLoading ? /* @__PURE__ */ jsx2(
183
631
  "button",
184
632
  {
185
633
  type: "button",
@@ -193,180 +641,373 @@ function AdminLogScreen({ client, pageSize = 50 }) {
193
641
  }
194
642
 
195
643
  // src/screens/AttentionScreen.tsx
196
- import { useAttention } from "@fonderie/react-admin";
197
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
644
+ import { useAttention, useManifest } from "@fonderie/react-admin";
645
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
198
646
  function AttentionScreen({ client }) {
199
647
  const { attention, isLoading, error, refresh } = useAttention(client);
200
- return /* @__PURE__ */ jsxs2("div", { style: styles.container, children: [
201
- /* @__PURE__ */ jsxs2("div", { style: styles.toolbar, children: [
202
- /* @__PURE__ */ jsx2("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Attention" }),
203
- /* @__PURE__ */ jsx2(
204
- "button",
648
+ const { manifest } = useManifest(client);
649
+ const errors = attention?.items.filter((i) => i.severity === "error") ?? [];
650
+ const advice = attention?.items.filter((i) => i.severity !== "error") ?? [];
651
+ const ready = manifest?.modules.filter((m) => m.readiness.ok).length ?? 0;
652
+ const total = manifest?.modules.length ?? 0;
653
+ return /* @__PURE__ */ jsxs3("div", { style: styles.container, children: [
654
+ /* @__PURE__ */ jsx3(
655
+ PageHeader,
656
+ {
657
+ title: "Attention",
658
+ lead: attention ? `What needs you on this deployment \xB7 checked ${new Date(attention.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment",
659
+ actions: /* @__PURE__ */ jsx3(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
660
+ }
661
+ ),
662
+ error ? /* @__PURE__ */ jsx3("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
663
+ /* @__PURE__ */ jsxs3("div", { style: styles.grid, children: [
664
+ /* @__PURE__ */ jsx3(
665
+ Stat,
205
666
  {
206
- type: "button",
207
- style: styles.button,
208
- onClick: () => void refresh(),
209
- disabled: isLoading,
210
- children: "Refresh"
667
+ label: "Needs action",
668
+ value: attention ? errors.length : "\u2014",
669
+ tone: errors.length ? "bad" : attention ? "ok" : void 0,
670
+ hint: "errors to fix"
671
+ }
672
+ ),
673
+ /* @__PURE__ */ jsx3(
674
+ Stat,
675
+ {
676
+ label: "Advice",
677
+ value: attention ? advice.length : "\u2014",
678
+ tone: advice.length ? "warn" : void 0,
679
+ hint: "worth a look"
680
+ }
681
+ ),
682
+ /* @__PURE__ */ jsx3(
683
+ Stat,
684
+ {
685
+ label: "Modules ready",
686
+ value: manifest ? `${ready}/${total}` : "\u2014",
687
+ tone: manifest && ready < total ? "warn" : manifest ? "ok" : void 0,
688
+ hint: manifest ? `admin ${manifest.admin.version}` : void 0
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsx3(
692
+ Stat,
693
+ {
694
+ label: "Routes",
695
+ value: manifest ? manifest.routes.length : "\u2014",
696
+ hint: manifest ? manifest.env : void 0
211
697
  }
212
698
  )
213
699
  ] }),
214
- isLoading && !attention ? /* @__PURE__ */ jsx2("p", { style: styles.status, children: "Running the checks\u2026" }) : error ? /* @__PURE__ */ jsx2("p", { style: styles.error, role: "alert", children: error.explanation }) : attention?.items.length === 0 ? /* @__PURE__ */ jsxs2("p", { style: styles.ok, children: [
215
- "Nothing needs you. Checked ",
700
+ isLoading && !attention ? /* @__PURE__ */ jsx3("p", { style: styles.status, children: "Running the checks\u2026" }) : attention && attention.items.length === 0 ? /* @__PURE__ */ jsxs3(Empty, { title: "Nothing needs you", children: [
701
+ "Every check passes. Checked ",
216
702
  new Date(attention.generatedAt).toLocaleString(),
217
703
  "."
218
- ] }) : /* @__PURE__ */ jsx2("ul", { style: styles.list, children: attention?.items.map((item) => /* @__PURE__ */ jsxs2("li", { style: styles.row, children: [
219
- /* @__PURE__ */ jsx2("span", { style: item.severity === "error" ? styles.bad : styles.advice, children: item.severity === "error" ? "error" : "advice" }),
220
- " ",
221
- /* @__PURE__ */ jsx2("span", { style: styles.mono, children: item.source }),
222
- " \u2014 ",
223
- item.message
224
- ] }, `${item.source}:${item.message}`)) })
704
+ ] }) : /* @__PURE__ */ jsx3("ul", { style: styles.list, children: [...errors, ...advice].map((item) => /* @__PURE__ */ jsxs3(
705
+ "li",
706
+ {
707
+ style: { ...styles.row, display: "flex", gap: 12, alignItems: "flex-start" },
708
+ children: [
709
+ /* @__PURE__ */ jsx3(
710
+ Icon,
711
+ {
712
+ name: "alert",
713
+ size: 16,
714
+ style: {
715
+ marginTop: 3,
716
+ color: item.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-warning,#f5a623)"
717
+ }
718
+ }
719
+ ),
720
+ /* @__PURE__ */ jsxs3("div", { style: { minWidth: 0, flex: 1 }, children: [
721
+ /* @__PURE__ */ jsxs3(
722
+ "div",
723
+ {
724
+ style: {
725
+ display: "flex",
726
+ gap: 8,
727
+ alignItems: "center",
728
+ flexWrap: "wrap",
729
+ marginBottom: 2
730
+ },
731
+ children: [
732
+ /* @__PURE__ */ jsx3(Pill, { tone: item.severity === "error" ? "bad" : "warn", children: item.severity === "error" ? "error" : "advice" }),
733
+ /* @__PURE__ */ jsx3("span", { style: styles.mono, children: item.source })
734
+ ]
735
+ }
736
+ ),
737
+ /* @__PURE__ */ jsx3("div", { style: { lineHeight: 1.55 }, children: item.message })
738
+ ] })
739
+ ]
740
+ },
741
+ `${item.source}:${item.message}`
742
+ )) })
225
743
  ] });
226
744
  }
227
745
 
228
746
  // src/screens/EnvironmentScreen.tsx
229
747
  import { useAdminEnvironment } from "@fonderie/react-admin";
230
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
748
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
231
749
  function EnvironmentScreen({ client }) {
232
750
  const { report, isLoading, error } = useAdminEnvironment(client);
233
- return /* @__PURE__ */ jsxs3("div", { style: styles.container, children: [
234
- /* @__PURE__ */ jsx3("h1", { style: styles.title, children: "Configuration" }),
235
- isLoading ? /* @__PURE__ */ jsx3("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ jsx3("p", { style: styles.error, role: "alert", children: error.explanation }) : report ? /* @__PURE__ */ jsxs3(Fragment2, { children: [
236
- /* @__PURE__ */ jsx3("h2", { style: styles.subtitle, children: "Readiness" }),
237
- /* @__PURE__ */ jsx3("ul", { style: styles.list, children: report.modules.map((m) => /* @__PURE__ */ jsxs3("li", { style: styles.row, children: [
238
- /* @__PURE__ */ jsx3("span", { style: styles.mono, children: m.name }),
239
- " ",
240
- m.problems.length === 0 ? /* @__PURE__ */ jsx3("span", { style: styles.ok, children: "ok" }) : m.problems.map((p) => /* @__PURE__ */ jsx3(
751
+ const missing = report?.env.filter((e) => !e.set).length ?? 0;
752
+ return /* @__PURE__ */ jsxs4("div", { style: styles.container, children: [
753
+ /* @__PURE__ */ jsx4(
754
+ PageHeader,
755
+ {
756
+ title: "Environment",
757
+ lead: "Whether each module is configured and each variable the app reads is set. Values are never shown.",
758
+ actions: report && report.env.length > 0 ? /* @__PURE__ */ jsx4(Pill, { tone: missing ? "bad" : "ok", children: missing ? `${missing} missing` : "all set" }) : null
759
+ }
760
+ ),
761
+ isLoading ? /* @__PURE__ */ jsx4("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ jsx4("p", { style: styles.error, role: "alert", children: error.explanation }) : report ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
762
+ /* @__PURE__ */ jsx4("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Variables" }),
763
+ report.env.length === 0 ? /* @__PURE__ */ jsx4("p", { style: styles.muted, children: "No variables declared \u2014 pass `env` to AdminModule." }) : /* @__PURE__ */ jsx4(
764
+ "div",
765
+ {
766
+ style: {
767
+ ...styles.grid,
768
+ gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
769
+ gap: 8
770
+ },
771
+ children: report.env.map((e) => /* @__PURE__ */ jsxs4(
772
+ "div",
773
+ {
774
+ style: {
775
+ ...styles.card,
776
+ padding: "10px 12px",
777
+ display: "flex",
778
+ alignItems: "center",
779
+ justifyContent: "space-between",
780
+ gap: 8
781
+ },
782
+ children: [
783
+ /* @__PURE__ */ jsx4("span", { style: { ...styles.mono, wordBreak: "break-all" }, children: e.name }),
784
+ /* @__PURE__ */ jsx4(Pill, { tone: e.set ? "ok" : "bad", children: e.set ? "set" : "missing" })
785
+ ]
786
+ },
787
+ e.name
788
+ ))
789
+ }
790
+ ),
791
+ /* @__PURE__ */ jsx4("h2", { style: styles.subtitle, children: "Module readiness" }),
792
+ /* @__PURE__ */ jsx4("ul", { style: styles.list, children: report.modules.map((m) => /* @__PURE__ */ jsxs4("li", { style: styles.row, children: [
793
+ /* @__PURE__ */ jsxs4(
241
794
  "div",
242
795
  {
243
- style: p.severity === "error" ? styles.bad : styles.advice,
796
+ style: {
797
+ display: "flex",
798
+ justifyContent: "space-between",
799
+ gap: 12,
800
+ alignItems: "center"
801
+ },
802
+ children: [
803
+ /* @__PURE__ */ jsx4("span", { style: { ...styles.mono, fontWeight: 600 }, children: m.name }),
804
+ /* @__PURE__ */ jsx4(
805
+ Pill,
806
+ {
807
+ tone: m.problems.length === 0 ? "ok" : m.problems.some((p) => p.severity === "error") ? "bad" : "warn",
808
+ children: m.problems.length === 0 ? "ready" : `${m.problems.length} problem${m.problems.length === 1 ? "" : "s"}`
809
+ }
810
+ )
811
+ ]
812
+ }
813
+ ),
814
+ m.problems.map((p) => /* @__PURE__ */ jsx4(
815
+ "div",
816
+ {
817
+ style: {
818
+ marginTop: 6,
819
+ fontSize: 13,
820
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
821
+ },
244
822
  children: p.message
245
823
  },
246
824
  p.message
247
825
  ))
248
- ] }, m.name)) }),
249
- /* @__PURE__ */ jsx3("h2", { style: styles.subtitle, children: "Environment" }),
250
- report.env.length === 0 ? /* @__PURE__ */ jsx3("p", { style: styles.muted, children: "No variables declared \u2014 pass `env` to AdminModule." }) : /* @__PURE__ */ jsx3("ul", { style: styles.list, children: report.env.map((e) => /* @__PURE__ */ jsxs3("li", { style: styles.row, children: [
251
- /* @__PURE__ */ jsx3("span", { style: styles.mono, children: e.name }),
252
- " ",
253
- e.set ? /* @__PURE__ */ jsx3("span", { style: styles.ok, children: "set" }) : /* @__PURE__ */ jsx3("span", { style: styles.bad, children: "missing" })
254
- ] }, e.name)) })
826
+ ] }, m.name)) })
255
827
  ] }) : null
256
828
  ] });
257
829
  }
258
830
 
259
831
  // src/screens/DoctorScreen.tsx
260
832
  import { useDoctor } from "@fonderie/react-admin";
261
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
833
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
262
834
  function DoctorScreen({ client }) {
263
835
  const { report, isLoading, error, refresh } = useDoctor(client);
264
- return /* @__PURE__ */ jsxs4("div", { style: styles.container, children: [
265
- /* @__PURE__ */ jsxs4("div", { style: styles.toolbar, children: [
266
- /* @__PURE__ */ jsx4("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Doctor" }),
267
- /* @__PURE__ */ jsx4(
268
- "button",
269
- {
270
- type: "button",
271
- style: styles.button,
272
- onClick: () => void refresh(),
273
- disabled: isLoading,
274
- children: "Run again"
275
- }
276
- ),
277
- report && !isLoading ? /* @__PURE__ */ jsx4("span", { style: report.ok ? styles.ok : styles.bad, children: report.ok ? "all checks pass" : "a check failed" }) : null
278
- ] }),
279
- isLoading && !report ? /* @__PURE__ */ jsx4("p", { style: styles.status, children: "Running the checks\u2026" }) : error ? /* @__PURE__ */ jsx4("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ jsx4("ul", { style: styles.list, children: report?.checks.map((c) => /* @__PURE__ */ jsxs4("li", { style: styles.row, children: [
280
- /* @__PURE__ */ jsx4("span", { style: c.skipped ? styles.muted : c.ok ? styles.ok : styles.bad, children: c.skipped ? "skipped" : c.ok ? "ok" : "failed" }),
281
- " ",
282
- /* @__PURE__ */ jsx4("span", { style: styles.mono, children: c.name }),
283
- " ",
284
- /* @__PURE__ */ jsxs4("span", { style: styles.muted, children: [
285
- c.module,
286
- " \xB7 ",
287
- c.durationMs,
288
- " ms"
289
- ] }),
290
- c.skipped ? /* @__PURE__ */ jsx4("div", { style: styles.muted, children: c.skipped }) : null,
291
- c.findings.map((f) => /* @__PURE__ */ jsx4("div", { style: c.ok ? styles.advice : styles.bad, children: f }, f))
292
- ] }, c.name)) })
836
+ return /* @__PURE__ */ jsxs5("div", { style: styles.container, children: [
837
+ /* @__PURE__ */ jsx5(
838
+ PageHeader,
839
+ {
840
+ title: "Doctor",
841
+ lead: "Every reconciliation check across the installed modules, run on demand.",
842
+ actions: /* @__PURE__ */ jsxs5(Fragment3, { children: [
843
+ report && !isLoading ? /* @__PURE__ */ jsx5(Pill, { tone: report.ok ? "ok" : "bad", children: report.ok ? "all checks pass" : "a check failed" }) : null,
844
+ /* @__PURE__ */ jsx5(RefreshButton, { onClick: () => void refresh(), busy: isLoading, label: "Run again" })
845
+ ] })
846
+ }
847
+ ),
848
+ isLoading && !report ? /* @__PURE__ */ jsx5("p", { style: styles.status, children: "Running the checks\u2026" }) : error ? /* @__PURE__ */ jsx5("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ jsxs5("table", { style: styles.table, children: [
849
+ /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
850
+ /* @__PURE__ */ jsx5("th", { style: { ...styles.th, width: 110 }, children: "Result" }),
851
+ /* @__PURE__ */ jsx5("th", { style: styles.th, children: "Check" }),
852
+ /* @__PURE__ */ jsx5("th", { style: { ...styles.th, textAlign: "right", width: 90 }, children: "Time" })
853
+ ] }) }),
854
+ /* @__PURE__ */ jsx5("tbody", { children: report?.checks.map((c) => /* @__PURE__ */ jsxs5("tr", { children: [
855
+ /* @__PURE__ */ jsx5("td", { style: styles.td, children: /* @__PURE__ */ jsx5(
856
+ Pill,
857
+ {
858
+ tone: c.skipped ? "neutral" : c.ok ? c.findings.length ? "warn" : "ok" : "bad",
859
+ children: c.skipped ? "skipped" : c.ok ? c.findings.length ? "advice" : "ok" : "failed"
860
+ }
861
+ ) }),
862
+ /* @__PURE__ */ jsxs5("td", { style: styles.td, children: [
863
+ /* @__PURE__ */ jsxs5("div", { children: [
864
+ /* @__PURE__ */ jsx5("span", { style: { ...styles.mono, fontWeight: 600 }, children: c.name }),
865
+ " ",
866
+ /* @__PURE__ */ jsx5("span", { style: styles.muted, children: c.module })
867
+ ] }),
868
+ c.skipped ? /* @__PURE__ */ jsx5("div", { style: { ...styles.muted, marginTop: 4 }, children: c.skipped }) : null,
869
+ c.findings.map((f) => /* @__PURE__ */ jsx5(
870
+ "div",
871
+ {
872
+ style: {
873
+ marginTop: 4,
874
+ fontSize: 13,
875
+ lineHeight: 1.55,
876
+ color: c.ok ? "var(--fonderie-text,#171717)" : "var(--fonderie-danger,#e00)"
877
+ },
878
+ children: f
879
+ },
880
+ f
881
+ ))
882
+ ] }),
883
+ /* @__PURE__ */ jsxs5(
884
+ "td",
885
+ {
886
+ style: {
887
+ ...styles.td,
888
+ ...styles.muted,
889
+ textAlign: "right",
890
+ whiteSpace: "nowrap"
891
+ },
892
+ children: [
893
+ c.durationMs,
894
+ " ms"
895
+ ]
896
+ }
897
+ )
898
+ ] }, c.name)) })
899
+ ] })
293
900
  ] });
294
901
  }
295
902
 
296
903
  // src/screens/ModulesScreen.tsx
297
- import { useManifest } from "@fonderie/react-admin";
298
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
904
+ import { useManifest as useManifest2 } from "@fonderie/react-admin";
905
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
299
906
  function ModulesScreen({ client }) {
300
- const { manifest, isLoading, error } = useManifest(client);
301
- return /* @__PURE__ */ jsxs5("div", { style: styles.container, children: [
302
- /* @__PURE__ */ jsx5("h1", { style: styles.title, children: "Modules" }),
303
- isLoading ? /* @__PURE__ */ jsx5("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ jsx5("p", { style: styles.error, role: "alert", children: error.explanation }) : manifest ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
304
- /* @__PURE__ */ jsxs5("p", { style: styles.muted, children: [
305
- manifest.env,
306
- " \xB7 admin ",
307
- manifest.admin.version,
308
- " \xB7 admin log",
309
- " ",
310
- manifest.admin.log ? "on" : "off",
311
- " \xB7 ",
312
- manifest.routes.length,
313
- " routes"
314
- ] }),
315
- /* @__PURE__ */ jsxs5("table", { style: styles.table, children: [
316
- /* @__PURE__ */ jsx5("thead", { children: /* @__PURE__ */ jsxs5("tr", { children: [
317
- /* @__PURE__ */ jsx5("th", { style: styles.th, children: "Module" }),
318
- /* @__PURE__ */ jsx5("th", { style: styles.th, children: "Version" }),
319
- /* @__PURE__ */ jsx5("th", { style: styles.th, children: "Readiness" }),
320
- /* @__PURE__ */ jsx5("th", { style: styles.th, children: "Describes admin" })
321
- ] }) }),
322
- /* @__PURE__ */ jsx5("tbody", { children: manifest.modules.map((m) => /* @__PURE__ */ jsxs5("tr", { children: [
323
- /* @__PURE__ */ jsx5("td", { style: { ...styles.td, ...styles.mono }, children: m.name }),
324
- /* @__PURE__ */ jsx5("td", { style: styles.td, children: m.version ?? /* @__PURE__ */ jsx5("span", { style: styles.muted, children: "not reported" }) }),
325
- /* @__PURE__ */ jsxs5("td", { style: styles.td, children: [
326
- m.readiness.ok ? /* @__PURE__ */ jsx5("span", { style: styles.ok, children: "ok" }) : /* @__PURE__ */ jsx5("span", { style: styles.bad, children: "error" }),
327
- m.readiness.problems.map((p) => /* @__PURE__ */ jsx5(
328
- "div",
329
- {
330
- style: p.severity === "error" ? styles.bad : styles.advice,
331
- children: p.message
907
+ const { manifest, isLoading, error } = useManifest2(client);
908
+ return /* @__PURE__ */ jsxs6("div", { style: styles.container, children: [
909
+ /* @__PURE__ */ jsx6(
910
+ PageHeader,
911
+ {
912
+ title: "Modules",
913
+ lead: manifest ? `${manifest.env} \xB7 admin ${manifest.admin.version} \xB7 admin log ${manifest.admin.log ? "on" : "off"} \xB7 ${manifest.routes.length} routes` : "Every installed module, its version and whether it is ready."
914
+ }
915
+ ),
916
+ isLoading ? /* @__PURE__ */ jsx6("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ jsx6("p", { style: styles.error, role: "alert", children: error.explanation }) : manifest ? /* @__PURE__ */ jsxs6("table", { style: styles.table, children: [
917
+ /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
918
+ /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Module" }),
919
+ /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Version" }),
920
+ /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Readiness" }),
921
+ /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Admin" })
922
+ ] }) }),
923
+ /* @__PURE__ */ jsx6("tbody", { children: manifest.modules.map((m) => /* @__PURE__ */ jsxs6("tr", { children: [
924
+ /* @__PURE__ */ jsx6("td", { style: { ...styles.td, ...styles.mono, fontWeight: 600 }, children: m.name }),
925
+ /* @__PURE__ */ jsx6("td", { style: { ...styles.td, ...styles.mono }, children: m.version ?? /* @__PURE__ */ jsx6("span", { style: styles.muted, children: "not reported" }) }),
926
+ /* @__PURE__ */ jsxs6("td", { style: styles.td, children: [
927
+ /* @__PURE__ */ jsx6(Pill, { tone: m.readiness.ok ? "ok" : "bad", children: m.readiness.ok ? "ready" : "error" }),
928
+ m.readiness.problems.map((p) => /* @__PURE__ */ jsx6(
929
+ "div",
930
+ {
931
+ style: {
932
+ marginTop: 6,
933
+ fontSize: 13,
934
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
332
935
  },
333
- p.message
334
- ))
335
- ] }),
336
- /* @__PURE__ */ jsx5("td", { style: styles.td, children: m.describesAdmin ? "yes" : /* @__PURE__ */ jsx5("span", { style: styles.muted, children: "no" }) })
337
- ] }, m.name)) })
338
- ] })
936
+ children: p.message
937
+ },
938
+ p.message
939
+ ))
940
+ ] }),
941
+ /* @__PURE__ */ jsx6("td", { style: styles.td, children: m.describesAdmin ? /* @__PURE__ */ jsx6("span", { style: styles.badge, children: "describes admin" }) : /* @__PURE__ */ jsx6("span", { style: styles.muted, children: "\u2014" }) })
942
+ ] }, m.name)) })
339
943
  ] }) : null
340
944
  ] });
341
945
  }
342
946
 
343
947
  // src/screens/RoutesScreen.tsx
344
948
  import { useAdminRoutes } from "@fonderie/react-admin";
345
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
949
+ import { useState } from "react";
950
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
346
951
  function RoutesScreen({ client }) {
347
952
  const { report, isLoading, error } = useAdminRoutes(client);
348
- return /* @__PURE__ */ jsxs6("div", { style: styles.container, children: [
349
- /* @__PURE__ */ jsx6("h1", { style: styles.title, children: "Routes" }),
350
- isLoading ? /* @__PURE__ */ jsx6("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ jsx6("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ jsxs6("table", { style: styles.table, children: [
351
- /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
352
- /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Method" }),
353
- /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Path" }),
354
- /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Guard" }),
355
- /* @__PURE__ */ jsx6("th", { style: styles.th, children: "Module" })
953
+ const [q, setQ] = useState("");
954
+ const needle = q.trim().toLowerCase();
955
+ const routes = (report?.routes ?? []).filter(
956
+ (r) => !needle || `${r.method} ${r.path} ${r.module ?? "application"} ${r.guard}`.toLowerCase().includes(needle)
957
+ );
958
+ return /* @__PURE__ */ jsxs7("div", { style: styles.container, children: [
959
+ /* @__PURE__ */ jsx7(
960
+ PageHeader,
961
+ {
962
+ title: "Routes",
963
+ lead: report ? `${report.routes.length} routes exposed by this deployment, with the guard in front of each.` : "Every exposed route, with its guard.",
964
+ actions: /* @__PURE__ */ jsx7(
965
+ "input",
966
+ {
967
+ type: "search",
968
+ value: q,
969
+ onChange: (e) => setQ(e.target.value),
970
+ placeholder: "Filter by path, method, module\u2026",
971
+ style: { ...styles.input, width: 280 },
972
+ "aria-label": "Filter routes"
973
+ }
974
+ )
975
+ }
976
+ ),
977
+ isLoading ? /* @__PURE__ */ jsx7("p", { style: styles.status, children: "Loading\u2026" }) : error ? /* @__PURE__ */ jsx7("p", { style: styles.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ jsxs7("table", { style: styles.table, children: [
978
+ /* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
979
+ /* @__PURE__ */ jsx7("th", { style: { ...styles.th, width: 80 }, children: "Method" }),
980
+ /* @__PURE__ */ jsx7("th", { style: styles.th, children: "Path" }),
981
+ /* @__PURE__ */ jsx7("th", { style: styles.th, children: "Guard" }),
982
+ /* @__PURE__ */ jsx7("th", { style: styles.th, children: "Module" })
356
983
  ] }) }),
357
- /* @__PURE__ */ jsx6("tbody", { children: report?.routes.map((r) => /* @__PURE__ */ jsxs6("tr", { children: [
358
- /* @__PURE__ */ jsx6("td", { style: { ...styles.td, ...styles.mono }, children: r.method }),
359
- /* @__PURE__ */ jsx6("td", { style: { ...styles.td, ...styles.mono }, children: r.path }),
360
- /* @__PURE__ */ jsx6("td", { style: styles.td, children: /* @__PURE__ */ jsx6("span", { style: styles.badge, children: r.guard }) }),
361
- /* @__PURE__ */ jsx6("td", { style: { ...styles.td, ...styles.muted }, children: r.module ?? "application" })
362
- ] }, `${r.method} ${r.path}`)) })
984
+ /* @__PURE__ */ jsxs7("tbody", { children: [
985
+ routes.map((r) => /* @__PURE__ */ jsxs7("tr", { children: [
986
+ /* @__PURE__ */ jsx7("td", { style: styles.td, children: /* @__PURE__ */ jsx7(MethodChip, { method: r.method }) }),
987
+ /* @__PURE__ */ jsx7("td", { style: { ...styles.td, ...styles.mono, wordBreak: "break-all" }, children: r.path }),
988
+ /* @__PURE__ */ jsx7("td", { style: styles.td, children: /* @__PURE__ */ jsx7(
989
+ Pill,
990
+ {
991
+ tone: r.guard === "admin" ? "ok" : r.guard === "probe" ? "neutral" : "info",
992
+ dot: false,
993
+ children: r.guard
994
+ }
995
+ ) }),
996
+ /* @__PURE__ */ jsx7("td", { style: { ...styles.td, ...styles.muted }, children: r.module ?? "application" })
997
+ ] }, `${r.method} ${r.path}`)),
998
+ routes.length === 0 ? /* @__PURE__ */ jsx7("tr", { children: /* @__PURE__ */ jsxs7("td", { style: { ...styles.td, ...styles.muted }, colSpan: 4, children: [
999
+ "No route matches \u201C",
1000
+ q,
1001
+ "\u201D."
1002
+ ] }) }) : null
1003
+ ] })
363
1004
  ] })
364
1005
  ] });
365
1006
  }
366
1007
 
367
1008
  // src/screens/MigrationsScreen.tsx
368
1009
  import { useAdminMigrations } from "@fonderie/react-admin";
369
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1010
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
370
1011
  function why(m, everApplied) {
371
1012
  if (m.pending.length === 0) return null;
372
1013
  if (m.blockedBy) return `Apply "${m.blockedBy}" first \u2014 it runs before this one and is behind.`;
@@ -377,36 +1018,40 @@ function why(m, everApplied) {
377
1018
  function MigrationsScreen({ client }) {
378
1019
  const { report, isLoading, error, refresh, apply } = useAdminMigrations(client);
379
1020
  const behind = report?.modules.filter((m) => m.pending.length > 0) ?? [];
380
- return /* @__PURE__ */ jsxs7("div", { style: styles.container, children: [
381
- /* @__PURE__ */ jsx7("h1", { style: styles.title, children: "Migrations" }),
382
- error ? /* @__PURE__ */ jsx7("p", { style: styles.error, role: "alert", children: error.status === 403 ? "Applying migrations needs a token with the write scope." : error.explanation }) : null,
383
- /* @__PURE__ */ jsxs7("p", { children: [
384
- /* @__PURE__ */ jsx7("button", { type: "button", style: styles.button, onClick: () => void refresh(), disabled: isLoading, children: isLoading ? "Checking\u2026" : "Refresh" }),
385
- " ",
386
- report && !report.everApplied ? /* @__PURE__ */ jsx7("span", { style: styles.advice, children: "This database has never been migrated \u2014 treating it as a first install, so nothing is held back." }) : null
387
- ] }),
388
- report && behind.length === 0 ? /* @__PURE__ */ jsx7("p", { style: styles.ok, children: "Every module is up to date." }) : null,
1021
+ return /* @__PURE__ */ jsxs8("div", { style: styles.container, children: [
1022
+ /* @__PURE__ */ jsx8(
1023
+ PageHeader,
1024
+ {
1025
+ title: "Migrations",
1026
+ lead: "Schema changes each module ships, and whether this database has them.",
1027
+ actions: /* @__PURE__ */ jsx8(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
1028
+ }
1029
+ ),
1030
+ error ? /* @__PURE__ */ jsx8("p", { style: styles.error, role: "alert", children: error.status === 403 ? "Applying migrations needs a token with the write scope." : error.explanation }) : null,
1031
+ report && !report.everApplied ? /* @__PURE__ */ jsx8("p", { style: styles.notice, children: "This database has never been migrated \u2014 treating it as a first install, so nothing is held back." }) : null,
1032
+ report && behind.length === 0 ? /* @__PURE__ */ jsx8(Empty, { icon: "migrations", title: "Every module is up to date", children: "No pending migrations." }) : null,
389
1033
  behind.map((m) => {
390
1034
  const blocked = why(m, report?.everApplied ?? false);
391
1035
  const files = m.pending.map((p) => p.file);
392
- return /* @__PURE__ */ jsxs7("section", { style: { marginBottom: 24 }, children: [
393
- /* @__PURE__ */ jsxs7("h2", { style: styles.subtitle, children: [
394
- m.name,
395
- " \u2014 ",
396
- m.pending.length,
397
- " pending"
1036
+ return /* @__PURE__ */ jsxs8("section", { style: { ...styles.card, marginBottom: 16 }, children: [
1037
+ /* @__PURE__ */ jsxs8("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: [
1038
+ /* @__PURE__ */ jsx8("span", { style: styles.mono, children: m.name }),
1039
+ /* @__PURE__ */ jsxs8(Pill, { tone: "warn", children: [
1040
+ m.pending.length,
1041
+ " pending"
1042
+ ] })
398
1043
  ] }),
399
- /* @__PURE__ */ jsx7("ul", { children: m.pending.map((p) => /* @__PURE__ */ jsxs7("li", { children: [
400
- /* @__PURE__ */ jsx7("code", { children: p.file }),
1044
+ /* @__PURE__ */ jsx8("ul", { style: { margin: "0 0 12px", paddingLeft: 18, lineHeight: 1.9 }, children: m.pending.map((p) => /* @__PURE__ */ jsxs8("li", { children: [
1045
+ /* @__PURE__ */ jsx8("code", { style: styles.code, children: p.file }),
401
1046
  " ",
402
- p.impact === "destructive" ? /* @__PURE__ */ jsx7("span", { style: styles.bad, children: "destructive" }) : /* @__PURE__ */ jsx7("span", { style: styles.advice, children: "additive" }),
403
- p.destructive.length > 0 ? /* @__PURE__ */ jsx7("ul", { children: p.destructive.map((s) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsx7("code", { children: s.slice(0, 120) }) }, s)) }) : null
1047
+ p.impact === "destructive" ? /* @__PURE__ */ jsx8(Pill, { tone: "bad", children: "destructive" }) : /* @__PURE__ */ jsx8(Pill, { tone: "neutral", children: "additive" }),
1048
+ p.destructive.length > 0 ? /* @__PURE__ */ jsx8("ul", { children: p.destructive.map((s) => /* @__PURE__ */ jsx8("li", { children: /* @__PURE__ */ jsx8("code", { children: s.slice(0, 120) }) }, s)) }) : null
404
1049
  ] }, p.file)) }),
405
- blocked ? /* @__PURE__ */ jsx7("p", { style: styles.advice, children: blocked }) : /* @__PURE__ */ jsxs7(
1050
+ blocked ? /* @__PURE__ */ jsx8("p", { style: { ...styles.notice, marginBottom: 0 }, children: blocked }) : /* @__PURE__ */ jsxs8(
406
1051
  "button",
407
1052
  {
408
1053
  type: "button",
409
- style: styles.button,
1054
+ style: styles.buttonPrimary,
410
1055
  disabled: isLoading || !m.appliable,
411
1056
  onClick: () => {
412
1057
  if (window.confirm(
@@ -428,37 +1073,306 @@ function MigrationsScreen({ client }) {
428
1073
  ] });
429
1074
  }
430
1075
 
1076
+ // src/screens/OperatorsScreen.tsx
1077
+ import { useAdminOperators } from "@fonderie/react-admin";
1078
+ import { useState as useState2 } from "react";
1079
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1080
+ var LEVELS = [
1081
+ { label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
1082
+ { label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
1083
+ {
1084
+ label: "Owner",
1085
+ scopes: ["read", "write", "secrets"],
1086
+ hint: "secrets, tokens and operators too"
1087
+ }
1088
+ ];
1089
+ var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
1090
+ function Minted({ link, onClose }) {
1091
+ const [copied, setCopied] = useState2(false);
1092
+ const url = `${window.location.origin}${link.url}`;
1093
+ return /* @__PURE__ */ jsxs9(
1094
+ "div",
1095
+ {
1096
+ style: {
1097
+ ...styles.notice,
1098
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1099
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1100
+ },
1101
+ children: [
1102
+ /* @__PURE__ */ jsxs9("strong", { children: [
1103
+ "Send this link to ",
1104
+ link.email,
1105
+ ". It is shown once."
1106
+ ] }),
1107
+ " It works once and expires",
1108
+ " ",
1109
+ new Date(link.expiresAt).toLocaleString(),
1110
+ ".",
1111
+ /* @__PURE__ */ jsxs9("div", { style: { display: "flex", gap: 8, marginTop: 8, alignItems: "center" }, children: [
1112
+ /* @__PURE__ */ jsx9("code", { style: { ...styles.code, flex: 1, padding: "7px 10px", wordBreak: "break-all" }, children: url }),
1113
+ /* @__PURE__ */ jsx9(
1114
+ "button",
1115
+ {
1116
+ type: "button",
1117
+ style: styles.button,
1118
+ onClick: () => {
1119
+ void navigator.clipboard?.writeText(url);
1120
+ setCopied(true);
1121
+ },
1122
+ children: copied ? "Copied" : "Copy"
1123
+ }
1124
+ ),
1125
+ /* @__PURE__ */ jsx9("button", { type: "button", style: styles.buttonGhost, onClick: onClose, "aria-label": "Dismiss", children: /* @__PURE__ */ jsx9(Icon, { name: "close", size: 14 }) })
1126
+ ] })
1127
+ ]
1128
+ }
1129
+ );
1130
+ }
1131
+ function OperatorsScreen({ client, me }) {
1132
+ const { report, isLoading, error, invite, recover, update, revokeLink } = useAdminOperators(client);
1133
+ const [email, setEmail] = useState2("");
1134
+ const [level, setLevel] = useState2("Editor");
1135
+ const [minted, setMinted] = useState2(null);
1136
+ const scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
1137
+ const status = (o) => o.disabledAt ? /* @__PURE__ */ jsx9(Pill, { tone: "neutral", children: "disabled" }) : o.locked ? /* @__PURE__ */ jsx9(Pill, { tone: "warn", children: "locked" }) : !o.enrolled ? /* @__PURE__ */ jsx9(Pill, { tone: "warn", children: "setting up" }) : /* @__PURE__ */ jsx9(Pill, { tone: "ok", children: "active" });
1138
+ return /* @__PURE__ */ jsxs9("div", { style: styles.container, children: [
1139
+ /* @__PURE__ */ jsx9(
1140
+ PageHeader,
1141
+ {
1142
+ title: "Operators",
1143
+ lead: "The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
1144
+ }
1145
+ ),
1146
+ error ? /* @__PURE__ */ jsx9("p", { style: styles.error, role: "alert", children: error.status === 403 && error.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : error.explanation }) : null,
1147
+ minted ? /* @__PURE__ */ jsx9(Minted, { link: minted, onClose: () => setMinted(null) }) : null,
1148
+ /* @__PURE__ */ jsxs9(
1149
+ "form",
1150
+ {
1151
+ style: {
1152
+ ...styles.card,
1153
+ display: "flex",
1154
+ gap: 8,
1155
+ alignItems: "center",
1156
+ flexWrap: "wrap",
1157
+ marginBottom: 20
1158
+ },
1159
+ onSubmit: (e) => {
1160
+ e.preventDefault();
1161
+ void invite({ email: email.trim(), scopes: scopesFor(level) }).then((l) => {
1162
+ setMinted(l);
1163
+ setEmail("");
1164
+ }).catch(() => {
1165
+ });
1166
+ },
1167
+ children: [
1168
+ /* @__PURE__ */ jsx9(Icon, { name: "users", size: 16 }),
1169
+ /* @__PURE__ */ jsx9("strong", { style: { fontSize: 13.5, marginRight: 4 }, children: "Invite" }),
1170
+ /* @__PURE__ */ jsx9(
1171
+ "input",
1172
+ {
1173
+ type: "email",
1174
+ required: true,
1175
+ value: email,
1176
+ onChange: (e) => setEmail(e.target.value),
1177
+ placeholder: "teammate@company.com",
1178
+ style: { ...styles.input, flex: 1, minWidth: 220 },
1179
+ "aria-label": "Email to invite"
1180
+ }
1181
+ ),
1182
+ /* @__PURE__ */ jsx9(
1183
+ "select",
1184
+ {
1185
+ value: level,
1186
+ onChange: (e) => setLevel(e.target.value),
1187
+ style: styles.input,
1188
+ "aria-label": "Access level",
1189
+ children: LEVELS.map((l) => /* @__PURE__ */ jsxs9("option", { value: l.label, children: [
1190
+ l.label,
1191
+ " \u2014 ",
1192
+ l.hint
1193
+ ] }, l.label))
1194
+ }
1195
+ ),
1196
+ /* @__PURE__ */ jsxs9("button", { type: "submit", style: styles.buttonPrimary, disabled: !email.trim(), children: [
1197
+ /* @__PURE__ */ jsx9(Icon, { name: "plus", size: 14 }),
1198
+ "Create invite link"
1199
+ ] })
1200
+ ]
1201
+ }
1202
+ ),
1203
+ isLoading && !report ? /* @__PURE__ */ jsx9("p", { style: styles.status, children: "Loading\u2026" }) : report && report.operators.length === 0 ? /* @__PURE__ */ jsx9(Empty, { icon: "users", title: "No operators yet" }) : report ? /* @__PURE__ */ jsxs9("table", { style: styles.table, children: [
1204
+ /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsxs9("tr", { children: [
1205
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Operator" }),
1206
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Access" }),
1207
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Status" }),
1208
+ /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Last sign-in" }),
1209
+ /* @__PURE__ */ jsx9("th", { style: styles.th })
1210
+ ] }) }),
1211
+ /* @__PURE__ */ jsx9("tbody", { children: report.operators.map((o) => /* @__PURE__ */ jsxs9("tr", { children: [
1212
+ /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
1213
+ /* @__PURE__ */ jsx9("div", { style: { fontWeight: 600 }, children: o.name || o.email }),
1214
+ o.name ? /* @__PURE__ */ jsx9("div", { style: styles.muted, children: o.email }) : null
1215
+ ] }),
1216
+ /* @__PURE__ */ jsx9("td", { style: styles.td, children: /* @__PURE__ */ jsx9(
1217
+ "select",
1218
+ {
1219
+ value: levelOf(o.scopes),
1220
+ onChange: (e) => void update(o.id, { scopes: scopesFor(e.target.value) }).catch(() => {
1221
+ }),
1222
+ style: { ...styles.input, height: 28, fontSize: 12.5 },
1223
+ disabled: o.email === me,
1224
+ "aria-label": `Access level for ${o.email}`,
1225
+ children: LEVELS.map((l) => /* @__PURE__ */ jsx9("option", { value: l.label, children: l.label }, l.label))
1226
+ }
1227
+ ) }),
1228
+ /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
1229
+ status(o),
1230
+ o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? /* @__PURE__ */ jsxs9("div", { style: { ...styles.muted, marginTop: 4 }, children: [
1231
+ o.backupCodesLeft,
1232
+ " backup code(s) left"
1233
+ ] }) : null
1234
+ ] }),
1235
+ /* @__PURE__ */ jsx9("td", { style: { ...styles.td, ...styles.muted, whiteSpace: "nowrap" }, children: o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never" }),
1236
+ /* @__PURE__ */ jsx9("td", { style: { ...styles.td, textAlign: "right", whiteSpace: "nowrap" }, children: o.email === me ? /* @__PURE__ */ jsx9("span", { style: styles.muted, children: "you" }) : /* @__PURE__ */ jsxs9(Fragment4, { children: [
1237
+ /* @__PURE__ */ jsx9(
1238
+ "button",
1239
+ {
1240
+ type: "button",
1241
+ style: { ...styles.button, height: 28, marginRight: 6 },
1242
+ onClick: () => {
1243
+ if (window.confirm(
1244
+ `Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
1245
+ ))
1246
+ void recover(o.id).then(setMinted).catch(() => {
1247
+ });
1248
+ },
1249
+ children: "Recovery link"
1250
+ }
1251
+ ),
1252
+ /* @__PURE__ */ jsx9(
1253
+ "button",
1254
+ {
1255
+ type: "button",
1256
+ style: {
1257
+ ...o.disabledAt ? styles.button : styles.buttonDanger,
1258
+ height: 28
1259
+ },
1260
+ onClick: () => {
1261
+ if (o.disabledAt || window.confirm(`Disable ${o.email}? They are signed out immediately.`))
1262
+ void update(o.id, { disabled: !o.disabledAt }).catch(() => {
1263
+ });
1264
+ },
1265
+ children: o.disabledAt ? "Enable" : "Disable"
1266
+ }
1267
+ )
1268
+ ] }) })
1269
+ ] }, o.id)) })
1270
+ ] }) : null,
1271
+ report && report.links.length > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1272
+ /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Pending links" }),
1273
+ /* @__PURE__ */ jsx9("ul", { style: styles.list, children: report.links.map((l) => /* @__PURE__ */ jsxs9(
1274
+ "li",
1275
+ {
1276
+ style: { ...styles.row, display: "flex", gap: 12, alignItems: "center" },
1277
+ children: [
1278
+ /* @__PURE__ */ jsx9(Pill, { tone: l.kind === "invite" ? "info" : "warn", dot: false, children: l.kind }),
1279
+ /* @__PURE__ */ jsxs9("span", { style: { flex: 1 }, children: [
1280
+ l.email,
1281
+ l.kind === "invite" ? /* @__PURE__ */ jsxs9("span", { style: styles.muted, children: [
1282
+ " \xB7 ",
1283
+ levelOf(l.scopes)
1284
+ ] }) : null
1285
+ ] }),
1286
+ /* @__PURE__ */ jsxs9("span", { style: styles.muted, children: [
1287
+ "expires ",
1288
+ new Date(l.expiresAt).toLocaleString()
1289
+ ] }),
1290
+ /* @__PURE__ */ jsx9(
1291
+ "button",
1292
+ {
1293
+ type: "button",
1294
+ style: { ...styles.buttonDanger, height: 28 },
1295
+ onClick: () => void revokeLink(l.id).catch(() => {
1296
+ }),
1297
+ children: "Revoke"
1298
+ }
1299
+ )
1300
+ ]
1301
+ },
1302
+ l.id
1303
+ )) })
1304
+ ] }) : null
1305
+ ] });
1306
+ }
1307
+
431
1308
  // src/screens/TokensScreen.tsx
432
1309
  import { useAdminTokens } from "@fonderie/react-admin";
433
- import { useState } from "react";
434
- import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1310
+ import { useState as useState3 } from "react";
1311
+ import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
435
1312
  var ALL = ["read", "write", "secrets"];
436
1313
  function TokensScreen({ client }) {
437
1314
  const { report, isLoading, error, issue, revoke } = useAdminTokens(client);
438
- const [name, setName] = useState("");
439
- const [scopes, setScopes] = useState(["read"]);
440
- const [days, setDays] = useState("");
441
- const [minted, setMinted] = useState(null);
1315
+ const [name, setName] = useState3("");
1316
+ const [scopes, setScopes] = useState3(["read"]);
1317
+ const [days, setDays] = useState3("");
1318
+ const [minted, setMinted] = useState3(null);
442
1319
  const toggle = (s) => setScopes((prev) => prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]);
443
- return /* @__PURE__ */ jsxs8("div", { style: styles.container, children: [
444
- /* @__PURE__ */ jsx8("h1", { style: styles.title, children: "Access" }),
445
- error ? /* @__PURE__ */ jsx8("p", { style: styles.error, role: "alert", children: error.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.explanation }) : null,
446
- /* @__PURE__ */ jsx8("h2", { style: styles.subtitle, children: "Root token" }),
447
- report ? /* @__PURE__ */ jsxs8("p", { children: [
448
- report.admin.ok ? /* @__PURE__ */ jsx8("span", { style: styles.ok, children: "strong" }) : /* @__PURE__ */ jsx8("span", { style: styles.bad, children: "weak" }),
449
- report.admin.problems.map((p) => /* @__PURE__ */ jsx8("div", { style: styles.bad, children: p.message }, p.message))
1320
+ return /* @__PURE__ */ jsxs10("div", { style: styles.container, children: [
1321
+ /* @__PURE__ */ jsx10(
1322
+ PageHeader,
1323
+ {
1324
+ title: "Tokens",
1325
+ lead: "Credentials for machines: the root token's strength, scoped tokens for the CLI and CI, and any legacy per-module tokens. People sign in as operators."
1326
+ }
1327
+ ),
1328
+ error ? /* @__PURE__ */ jsx10("p", { style: styles.error, role: "alert", children: error.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.explanation }) : null,
1329
+ /* @__PURE__ */ jsx10("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Root token" }),
1330
+ report ? /* @__PURE__ */ jsxs10("div", { style: { ...styles.card, display: "flex", gap: 12, alignItems: "flex-start" }, children: [
1331
+ /* @__PURE__ */ jsx10(Icon, { name: "lock", size: 18, style: { marginTop: 2 } }),
1332
+ /* @__PURE__ */ jsxs10("div", { children: [
1333
+ /* @__PURE__ */ jsx10(Pill, { tone: report.admin.ok ? "ok" : "bad", children: report.admin.ok ? "strong" : "weak" }),
1334
+ report.admin.problems.map((p) => /* @__PURE__ */ jsx10(
1335
+ "div",
1336
+ {
1337
+ style: { marginTop: 6, color: "var(--fonderie-danger,#e00)", fontSize: 13 },
1338
+ children: p.message
1339
+ },
1340
+ p.message
1341
+ ))
1342
+ ] })
450
1343
  ] }) : null,
451
- /* @__PURE__ */ jsx8("h2", { style: styles.subtitle, children: "Issued tokens" }),
452
- report?.issued === null ? /* @__PURE__ */ jsx8("p", { style: styles.muted, children: "Issuing is off \u2014 give AdminModule a store and run its migrations." }) : /* @__PURE__ */ jsxs8(Fragment4, { children: [
453
- minted ? /* @__PURE__ */ jsxs8("p", { style: styles.ok, children: [
454
- "Copy this now \u2014 it is never shown again.",
455
- " ",
456
- /* @__PURE__ */ jsx8("span", { style: styles.mono, children: minted.token }),
457
- " (",
458
- minted.name,
459
- ")"
460
- ] }) : null,
461
- /* @__PURE__ */ jsxs8(
1344
+ /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Issued tokens" }),
1345
+ report?.issued === null ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "Issuing is off \u2014 give AdminModule a store and run its migrations." }) : /* @__PURE__ */ jsxs10(Fragment5, { children: [
1346
+ minted ? /* @__PURE__ */ jsxs10(
1347
+ "div",
1348
+ {
1349
+ style: {
1350
+ ...styles.notice,
1351
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1352
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1353
+ },
1354
+ children: [
1355
+ /* @__PURE__ */ jsx10("strong", { children: "Copy this now \u2014 it is never shown again." }),
1356
+ " (",
1357
+ minted.name,
1358
+ ")",
1359
+ /* @__PURE__ */ jsx10(
1360
+ "div",
1361
+ {
1362
+ style: {
1363
+ ...styles.code,
1364
+ display: "block",
1365
+ marginTop: 8,
1366
+ padding: "8px 10px",
1367
+ wordBreak: "break-all"
1368
+ },
1369
+ children: minted.token
1370
+ }
1371
+ )
1372
+ ]
1373
+ }
1374
+ ) : null,
1375
+ /* @__PURE__ */ jsxs10(
462
1376
  "form",
463
1377
  {
464
1378
  style: styles.toolbar,
@@ -477,70 +1391,90 @@ function TokensScreen({ client }) {
477
1391
  });
478
1392
  },
479
1393
  children: [
480
- /* @__PURE__ */ jsx8(
1394
+ /* @__PURE__ */ jsx10(
481
1395
  "input",
482
1396
  {
483
1397
  value: name,
484
1398
  onChange: (e) => setName(e.target.value),
485
1399
  placeholder: "name",
486
- style: { ...styles.button, cursor: "text" },
1400
+ style: styles.input,
487
1401
  "aria-label": "Name"
488
1402
  }
489
1403
  ),
490
- ALL.map((s) => /* @__PURE__ */ jsxs8("label", { style: { ...styles.badge, cursor: "pointer" }, children: [
491
- /* @__PURE__ */ jsx8("input", { type: "checkbox", checked: scopes.includes(s), onChange: () => toggle(s) }),
492
- " ",
1404
+ ALL.map((s) => /* @__PURE__ */ jsxs10(
1405
+ "label",
1406
+ {
1407
+ style: {
1408
+ ...styles.badge,
1409
+ cursor: "pointer",
1410
+ height: 32,
1411
+ boxSizing: "border-box",
1412
+ padding: "0 10px",
1413
+ fontSize: 13
1414
+ },
1415
+ children: [
1416
+ /* @__PURE__ */ jsx10("input", { type: "checkbox", checked: scopes.includes(s), onChange: () => toggle(s) }),
1417
+ " ",
1418
+ s
1419
+ ]
1420
+ },
493
1421
  s
494
- ] }, s)),
495
- /* @__PURE__ */ jsx8(
1422
+ )),
1423
+ /* @__PURE__ */ jsx10(
496
1424
  "input",
497
1425
  {
498
1426
  value: days,
499
1427
  onChange: (e) => setDays(e.target.value),
500
1428
  placeholder: "days (optional)",
501
- style: { ...styles.button, cursor: "text", width: 140 },
1429
+ style: { ...styles.input, width: 140 },
502
1430
  "aria-label": "Days"
503
1431
  }
504
1432
  ),
505
- /* @__PURE__ */ jsx8(
1433
+ /* @__PURE__ */ jsxs10(
506
1434
  "button",
507
1435
  {
508
1436
  type: "submit",
509
- style: styles.button,
1437
+ style: styles.buttonPrimary,
510
1438
  disabled: !name.trim() || scopes.length === 0 || isLoading,
511
- children: "Issue"
1439
+ children: [
1440
+ /* @__PURE__ */ jsx10(Icon, { name: "plus", size: 14 }),
1441
+ "Issue token"
1442
+ ]
512
1443
  }
513
1444
  )
514
1445
  ]
515
1446
  }
516
1447
  ),
517
- report?.issued && report.issued.length > 0 ? /* @__PURE__ */ jsxs8("table", { style: styles.table, children: [
518
- /* @__PURE__ */ jsx8("thead", { children: /* @__PURE__ */ jsxs8("tr", { children: [
519
- /* @__PURE__ */ jsx8("th", { style: styles.th, children: "Name" }),
520
- /* @__PURE__ */ jsx8("th", { style: styles.th, children: "Scopes" }),
521
- /* @__PURE__ */ jsx8("th", { style: styles.th, children: "Created" }),
522
- /* @__PURE__ */ jsx8("th", { style: styles.th, children: "Expires" }),
523
- /* @__PURE__ */ jsx8("th", { style: styles.th, children: "Last used" }),
524
- /* @__PURE__ */ jsx8("th", { style: styles.th })
1448
+ report?.issued && report.issued.length > 0 ? /* @__PURE__ */ jsxs10("table", { style: styles.table, children: [
1449
+ /* @__PURE__ */ jsx10("thead", { children: /* @__PURE__ */ jsxs10("tr", { children: [
1450
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Name" }),
1451
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Scopes" }),
1452
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Created" }),
1453
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Expires" }),
1454
+ /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Last used" }),
1455
+ /* @__PURE__ */ jsx10("th", { style: styles.th })
525
1456
  ] }) }),
526
- /* @__PURE__ */ jsx8("tbody", { children: report.issued.map((t2) => /* @__PURE__ */ jsxs8("tr", { children: [
527
- /* @__PURE__ */ jsxs8("td", { style: styles.td, children: [
1457
+ /* @__PURE__ */ jsx10("tbody", { children: report.issued.map((t2) => /* @__PURE__ */ jsxs10("tr", { children: [
1458
+ /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
528
1459
  t2.name,
529
- t2.revokedAt ? /* @__PURE__ */ jsx8("span", { style: styles.badge, children: " revoked" }) : null
1460
+ t2.revokedAt ? /* @__PURE__ */ jsxs10(Fragment5, { children: [
1461
+ " ",
1462
+ /* @__PURE__ */ jsx10(Pill, { tone: "neutral", children: "revoked" })
1463
+ ] }) : null
530
1464
  ] }),
531
- /* @__PURE__ */ jsx8("td", { style: { ...styles.td, ...styles.mono }, children: t2.scopes.join(", ") }),
532
- /* @__PURE__ */ jsxs8("td", { style: { ...styles.td, ...styles.muted }, children: [
1465
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.mono }, children: t2.scopes.join(", ") }),
1466
+ /* @__PURE__ */ jsxs10("td", { style: { ...styles.td, ...styles.muted }, children: [
533
1467
  new Date(t2.createdAt).toLocaleDateString(),
534
1468
  " \xB7 ",
535
1469
  t2.createdBy
536
1470
  ] }),
537
- /* @__PURE__ */ jsx8("td", { style: { ...styles.td, ...styles.muted }, children: t2.expiresAt ? new Date(t2.expiresAt).toLocaleDateString() : "never" }),
538
- /* @__PURE__ */ jsx8("td", { style: { ...styles.td, ...styles.muted }, children: t2.lastUsedAt ? new Date(t2.lastUsedAt).toLocaleString() : "never" }),
539
- /* @__PURE__ */ jsx8("td", { style: styles.td, children: t2.revokedAt ? null : /* @__PURE__ */ jsx8(
1471
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.muted }, children: t2.expiresAt ? new Date(t2.expiresAt).toLocaleDateString() : "never" }),
1472
+ /* @__PURE__ */ jsx10("td", { style: { ...styles.td, ...styles.muted }, children: t2.lastUsedAt ? new Date(t2.lastUsedAt).toLocaleString() : "never" }),
1473
+ /* @__PURE__ */ jsx10("td", { style: styles.td, children: t2.revokedAt ? null : /* @__PURE__ */ jsx10(
540
1474
  "button",
541
1475
  {
542
1476
  type: "button",
543
- style: styles.button,
1477
+ style: styles.buttonDanger,
544
1478
  onClick: () => {
545
1479
  if (window.confirm(
546
1480
  `Revoke "${t2.name}"? Anything using it stops working immediately.`
@@ -552,13 +1486,13 @@ function TokensScreen({ client }) {
552
1486
  }
553
1487
  ) })
554
1488
  ] }, t2.id)) })
555
- ] }) : /* @__PURE__ */ jsx8("p", { style: styles.muted, children: "None issued." })
1489
+ ] }) : /* @__PURE__ */ jsx10(Empty, { icon: "tokens", title: "No scoped tokens yet", children: "Issue a read-only token for a dashboard or a teammate instead of sharing the root." })
556
1490
  ] }),
557
- /* @__PURE__ */ jsx8("h2", { style: styles.subtitle, children: "Legacy per-brick tokens" }),
558
- report && report.legacy.length === 0 ? /* @__PURE__ */ jsx8("p", { style: styles.muted, children: "None \u2014 every brick's admin surface goes through this token." }) : /* @__PURE__ */ jsx8("ul", { style: styles.list, children: report?.legacy.map((l) => /* @__PURE__ */ jsxs8("li", { style: styles.row, children: [
559
- /* @__PURE__ */ jsx8("span", { style: styles.mono, children: l.module }),
1491
+ /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Legacy per-brick tokens" }),
1492
+ report && report.legacy.length === 0 ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "None \u2014 every brick's admin surface goes through this token." }) : /* @__PURE__ */ jsx10("ul", { style: styles.list, children: report?.legacy.map((l) => /* @__PURE__ */ jsxs10("li", { style: styles.row, children: [
1493
+ /* @__PURE__ */ jsx10("span", { style: styles.mono, children: l.module }),
560
1494
  " ",
561
- /* @__PURE__ */ jsx8("span", { style: styles.muted, children: "still registers its standalone routes with its own token (deprecated)" })
1495
+ /* @__PURE__ */ jsx10("span", { style: styles.muted, children: "still registers its standalone routes with its own token (deprecated)" })
562
1496
  ] }, l.module)) })
563
1497
  ] });
564
1498
  }
@@ -571,11 +1505,11 @@ import {
571
1505
  useAdminUserSessions,
572
1506
  useAdminUsers
573
1507
  } from "@fonderie/react-admin";
574
- import { useState as useState2 } from "react";
575
- import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1508
+ import { useState as useState4 } from "react";
1509
+ import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
576
1510
  function UsersScreen({ client, pageSize = 50 }) {
577
- const [input, setInput] = useState2("");
578
- const [selected, setSelected] = useState2(null);
1511
+ const [input, setInput] = useState4("");
1512
+ const [selected, setSelected] = useState4(null);
579
1513
  const list = useAdminUsers(client, { limit: pageSize });
580
1514
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
581
1515
  client,
@@ -584,14 +1518,20 @@ function UsersScreen({ client, pageSize = 50 }) {
584
1518
  const userId = user?.id ?? null;
585
1519
  const sessions = useAdminUserSessions(client, userId);
586
1520
  const history = useAdminLoginHistory(client, userId, { limit: 20 });
587
- const yesNo = (v) => v ? /* @__PURE__ */ jsx9("span", { style: styles.ok, children: "yes" }) : /* @__PURE__ */ jsx9("span", { style: styles.muted, children: "no" });
1521
+ const yesNo = (v) => v ? /* @__PURE__ */ jsx11(Pill, { tone: "ok", children: "yes" }) : /* @__PURE__ */ jsx11(Pill, { tone: "neutral", children: "no" });
588
1522
  const clear = () => {
589
1523
  setSelected(null);
590
1524
  setInput("");
591
1525
  };
592
- return /* @__PURE__ */ jsxs9("div", { style: styles.container, children: [
593
- /* @__PURE__ */ jsx9("h1", { style: styles.title, children: "Users" }),
594
- /* @__PURE__ */ jsxs9(
1526
+ return /* @__PURE__ */ jsxs11("div", { style: styles.container, children: [
1527
+ /* @__PURE__ */ jsx11(
1528
+ PageHeader,
1529
+ {
1530
+ title: "Users",
1531
+ lead: "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1532
+ }
1533
+ ),
1534
+ /* @__PURE__ */ jsxs11(
595
1535
  "form",
596
1536
  {
597
1537
  style: styles.toolbar,
@@ -601,19 +1541,25 @@ function UsersScreen({ client, pageSize = 50 }) {
601
1541
  setSelected(email ? { email } : null);
602
1542
  },
603
1543
  children: [
604
- /* @__PURE__ */ jsx9(
1544
+ /* @__PURE__ */ jsx11(
605
1545
  "input",
606
1546
  {
607
1547
  type: "email",
608
1548
  value: input,
609
1549
  onChange: (e) => setInput(e.target.value),
610
1550
  placeholder: "email address",
611
- style: { ...styles.button, cursor: "text", minWidth: 280 },
1551
+ style: { ...styles.input, minWidth: 280 },
612
1552
  "aria-label": "Email"
613
1553
  }
614
1554
  ),
615
- /* @__PURE__ */ jsx9("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Look up" }),
616
- selected ? /* @__PURE__ */ jsx9("button", { type: "button", style: styles.button, onClick: clear, children: "\u2190 All users" }) : /* @__PURE__ */ jsx9(
1555
+ /* @__PURE__ */ jsxs11("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1556
+ /* @__PURE__ */ jsx11(Icon, { name: "search", size: 14 }),
1557
+ "Look up"
1558
+ ] }),
1559
+ selected ? /* @__PURE__ */ jsxs11("button", { type: "button", style: styles.buttonGhost, onClick: clear, children: [
1560
+ /* @__PURE__ */ jsx11(Icon, { name: "back", size: 14 }),
1561
+ "All users"
1562
+ ] }) : /* @__PURE__ */ jsx11(
617
1563
  "button",
618
1564
  {
619
1565
  type: "button",
@@ -626,37 +1572,37 @@ function UsersScreen({ client, pageSize = 50 }) {
626
1572
  ]
627
1573
  }
628
1574
  ),
629
- error ? /* @__PURE__ */ jsx9("p", { style: styles.error, role: "alert", children: error.status === 404 ? "No user with that email." : error.explanation }) : null,
630
- !selected ? /* @__PURE__ */ jsxs9(Fragment5, { children: [
631
- list.error ? /* @__PURE__ */ jsx9("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
632
- list.users.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx9("p", { style: styles.muted, children: "No users yet." }) : /* @__PURE__ */ jsxs9("table", { style: styles.table, children: [
633
- /* @__PURE__ */ jsx9("thead", { children: /* @__PURE__ */ jsxs9("tr", { children: [
634
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Email" }),
635
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Name" }),
636
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Created" }),
637
- /* @__PURE__ */ jsx9("th", { style: styles.th, children: "Status" })
1575
+ error ? /* @__PURE__ */ jsx11("p", { style: styles.error, role: "alert", children: error.status === 404 ? "No user with that email." : error.explanation }) : null,
1576
+ !selected ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
1577
+ list.error ? /* @__PURE__ */ jsx11("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
1578
+ list.users.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx11(Empty, { icon: "users", title: "No users yet", children: "Sign-ups appear here as they happen." }) : /* @__PURE__ */ jsxs11("table", { style: styles.table, children: [
1579
+ /* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsxs11("tr", { children: [
1580
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Email" }),
1581
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Name" }),
1582
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Created" }),
1583
+ /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Status" })
638
1584
  ] }) }),
639
- /* @__PURE__ */ jsx9("tbody", { children: list.users.map((u) => /* @__PURE__ */ jsxs9("tr", { children: [
640
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: /* @__PURE__ */ jsx9(
1585
+ /* @__PURE__ */ jsx11("tbody", { children: list.users.map((u) => /* @__PURE__ */ jsxs11("tr", { children: [
1586
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: /* @__PURE__ */ jsx11(
641
1587
  "button",
642
1588
  {
643
1589
  type: "button",
644
- style: { ...styles.navItem, padding: 0 },
1590
+ style: styles.link,
645
1591
  onClick: () => setSelected({ id: u.id }),
646
1592
  children: u.email
647
1593
  }
648
1594
  ) }),
649
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim() || /* @__PURE__ */ jsx9("span", { style: styles.muted, children: "\u2014" }) }),
650
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: new Date(u.createdAt).toLocaleDateString() }),
651
- /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
652
- u.suspended ? /* @__PURE__ */ jsx9("span", { style: styles.badge, children: "suspended" }) : null,
653
- u.deletedAt ? /* @__PURE__ */ jsx9("span", { style: styles.badge, children: "deleted" }) : null,
654
- !u.suspended && !u.deletedAt ? /* @__PURE__ */ jsx9("span", { style: styles.muted, children: "active" }) : null
1595
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: `${u.firstName ?? ""} ${u.lastName ?? ""}`.trim() || /* @__PURE__ */ jsx11("span", { style: styles.muted, children: "\u2014" }) }),
1596
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: new Date(u.createdAt).toLocaleDateString() }),
1597
+ /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
1598
+ u.suspended ? /* @__PURE__ */ jsx11(Pill, { tone: "warn", children: "suspended" }) : null,
1599
+ u.deletedAt ? /* @__PURE__ */ jsx11(Pill, { tone: "neutral", children: "deleted" }) : null,
1600
+ !u.suspended && !u.deletedAt ? /* @__PURE__ */ jsx11(Pill, { tone: "ok", children: "active" }) : null
655
1601
  ] })
656
1602
  ] }, u.id)) })
657
1603
  ] }),
658
- list.isLoading ? /* @__PURE__ */ jsx9("p", { style: styles.status, children: "Loading\u2026" }) : null,
659
- list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx9(
1604
+ list.isLoading ? /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Loading\u2026" }) : null,
1605
+ list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx11(
660
1606
  "button",
661
1607
  {
662
1608
  type: "button",
@@ -666,48 +1612,48 @@ function UsersScreen({ client, pageSize = 50 }) {
666
1612
  }
667
1613
  ) : null
668
1614
  ] }) : null,
669
- user ? /* @__PURE__ */ jsxs9(Fragment5, { children: [
670
- /* @__PURE__ */ jsxs9("h2", { style: styles.subtitle, children: [
1615
+ user ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
1616
+ /* @__PURE__ */ jsxs11("h2", { style: styles.subtitle, children: [
671
1617
  user.firstName || user.lastName ? `${user.firstName} ${user.lastName}`.trim() : user.email,
672
1618
  " ",
673
- user.suspended ? /* @__PURE__ */ jsx9("span", { style: styles.badge, children: "suspended" }) : null,
674
- user.deletedAt ? /* @__PURE__ */ jsx9("span", { style: styles.badge, children: "deleted" }) : null
1619
+ user.suspended ? /* @__PURE__ */ jsx11(Pill, { tone: "warn", children: "suspended" }) : null,
1620
+ user.deletedAt ? /* @__PURE__ */ jsx11(Pill, { tone: "neutral", children: "deleted" }) : null
675
1621
  ] }),
676
- /* @__PURE__ */ jsx9("table", { style: styles.table, children: /* @__PURE__ */ jsxs9("tbody", { children: [
677
- /* @__PURE__ */ jsxs9("tr", { children: [
678
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: "id" }),
679
- /* @__PURE__ */ jsx9("td", { style: { ...styles.td, ...styles.mono }, children: user.id })
1622
+ /* @__PURE__ */ jsx11("table", { style: styles.table, children: /* @__PURE__ */ jsxs11("tbody", { children: [
1623
+ /* @__PURE__ */ jsxs11("tr", { children: [
1624
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: "id" }),
1625
+ /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.mono }, children: user.id })
680
1626
  ] }),
681
- /* @__PURE__ */ jsxs9("tr", { children: [
682
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: "email" }),
683
- /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
1627
+ /* @__PURE__ */ jsxs11("tr", { children: [
1628
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: "email" }),
1629
+ /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
684
1630
  user.email,
685
1631
  " \xB7 verified ",
686
1632
  yesNo(user.isEmailVerified)
687
1633
  ] })
688
1634
  ] }),
689
- /* @__PURE__ */ jsxs9("tr", { children: [
690
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: "MFA" }),
691
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: yesNo(user.mfaEnabled) })
1635
+ /* @__PURE__ */ jsxs11("tr", { children: [
1636
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: "MFA" }),
1637
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: yesNo(user.mfaEnabled) })
692
1638
  ] }),
693
- /* @__PURE__ */ jsxs9("tr", { children: [
694
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: "provider" }),
695
- /* @__PURE__ */ jsxs9("td", { style: styles.td, children: [
1639
+ /* @__PURE__ */ jsxs11("tr", { children: [
1640
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: "provider" }),
1641
+ /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
696
1642
  user.provider || "password",
697
1643
  user.hasPassword ? "" : " \xB7 no password set"
698
1644
  ] })
699
1645
  ] }),
700
- /* @__PURE__ */ jsxs9("tr", { children: [
701
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: "last login" }),
702
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: user.lastLogin ? new Date(user.lastLogin).toLocaleString() : /* @__PURE__ */ jsx9("span", { style: styles.muted, children: "never" }) })
1646
+ /* @__PURE__ */ jsxs11("tr", { children: [
1647
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: "last login" }),
1648
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: user.lastLogin ? new Date(user.lastLogin).toLocaleString() : /* @__PURE__ */ jsx11("span", { style: styles.muted, children: "never" }) })
703
1649
  ] }),
704
- /* @__PURE__ */ jsxs9("tr", { children: [
705
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: "created" }),
706
- /* @__PURE__ */ jsx9("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
1650
+ /* @__PURE__ */ jsxs11("tr", { children: [
1651
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: "created" }),
1652
+ /* @__PURE__ */ jsx11("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
707
1653
  ] })
708
1654
  ] }) }),
709
- /* @__PURE__ */ jsxs9("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
710
- user.suspended ? /* @__PURE__ */ jsx9(
1655
+ /* @__PURE__ */ jsxs11("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
1656
+ user.suspended ? /* @__PURE__ */ jsx11(
711
1657
  "button",
712
1658
  {
713
1659
  type: "button",
@@ -716,17 +1662,17 @@ function UsersScreen({ client, pageSize = 50 }) {
716
1662
  disabled: isLoading,
717
1663
  children: "Unsuspend"
718
1664
  }
719
- ) : /* @__PURE__ */ jsx9(
1665
+ ) : /* @__PURE__ */ jsx11(
720
1666
  "button",
721
1667
  {
722
1668
  type: "button",
723
- style: styles.button,
1669
+ style: styles.buttonDanger,
724
1670
  onClick: () => void suspend(),
725
1671
  disabled: isLoading,
726
1672
  children: "Suspend"
727
1673
  }
728
1674
  ),
729
- /* @__PURE__ */ jsx9(
1675
+ /* @__PURE__ */ jsx11(
730
1676
  "button",
731
1677
  {
732
1678
  type: "button",
@@ -737,38 +1683,38 @@ function UsersScreen({ client, pageSize = 50 }) {
737
1683
  }
738
1684
  )
739
1685
  ] }),
740
- /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Live sessions" }),
741
- sessions.sessions.length === 0 ? /* @__PURE__ */ jsx9("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ jsx9("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ jsxs9("li", { style: styles.row, children: [
742
- /* @__PURE__ */ jsx9("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
1686
+ /* @__PURE__ */ jsx11("h2", { style: styles.subtitle, children: "Live sessions" }),
1687
+ sessions.sessions.length === 0 ? /* @__PURE__ */ jsx11("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ jsx11("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ jsxs11("li", { style: styles.row, children: [
1688
+ /* @__PURE__ */ jsx11("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
743
1689
  " ",
744
- describeLocation(s.location) ? /* @__PURE__ */ jsxs9("span", { style: styles.muted, children: [
1690
+ describeLocation(s.location) ? /* @__PURE__ */ jsxs11("span", { style: styles.muted, children: [
745
1691
  describeLocation(s.location),
746
1692
  " "
747
1693
  ] }) : null,
748
- /* @__PURE__ */ jsx9("span", { style: styles.muted, children: s.userAgent ?? "" }),
1694
+ /* @__PURE__ */ jsx11("span", { style: styles.muted, children: s.userAgent ?? "" }),
749
1695
  " ",
750
- /* @__PURE__ */ jsxs9("span", { style: styles.muted, children: [
1696
+ /* @__PURE__ */ jsxs11("span", { style: styles.muted, children: [
751
1697
  "since ",
752
1698
  new Date(s.createdAt).toLocaleString()
753
1699
  ] })
754
1700
  ] }, s.id)) }),
755
- /* @__PURE__ */ jsx9("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
756
- history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ jsx9("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ jsx9("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ jsxs9("li", { style: styles.row, children: [
757
- /* @__PURE__ */ jsx9("span", { style: e.outcome === "success" ? styles.ok : styles.bad, children: e.outcome }),
1701
+ /* @__PURE__ */ jsx11("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
1702
+ history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ jsx11("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ jsx11("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ jsxs11("li", { style: styles.row, children: [
1703
+ /* @__PURE__ */ jsx11(Pill, { tone: e.outcome === "success" ? "ok" : "bad", children: e.outcome }),
758
1704
  " ",
759
- /* @__PURE__ */ jsx9("span", { style: styles.muted, children: e.method }),
1705
+ /* @__PURE__ */ jsx11("span", { style: styles.muted, children: e.method }),
760
1706
  " ",
761
- /* @__PURE__ */ jsx9("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
1707
+ /* @__PURE__ */ jsx11("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
762
1708
  " ",
763
- describeLocation(e.location) ? /* @__PURE__ */ jsx9("span", { style: styles.muted, children: describeLocation(e.location) }) : null,
764
- e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ jsxs9("span", { style: styles.bad, children: [
1709
+ describeLocation(e.location) ? /* @__PURE__ */ jsx11("span", { style: styles.muted, children: describeLocation(e.location) }) : null,
1710
+ e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
765
1711
  " ",
766
- e.location.proxy ? "proxy/VPN" : "hosting"
1712
+ /* @__PURE__ */ jsx11(Pill, { tone: "warn", children: e.location.proxy ? "proxy/VPN" : "hosting" })
767
1713
  ] }) : null,
768
1714
  " ",
769
- /* @__PURE__ */ jsx9("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
1715
+ /* @__PURE__ */ jsx11("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
770
1716
  ] }, e.id)) }),
771
- history.hasMore && !history.isLoading ? /* @__PURE__ */ jsx9(
1717
+ history.hasMore && !history.isLoading ? /* @__PURE__ */ jsx11(
772
1718
  "button",
773
1719
  {
774
1720
  type: "button",
@@ -783,59 +1729,67 @@ function UsersScreen({ client, pageSize = 50 }) {
783
1729
 
784
1730
  // src/screens/CatalogScreen.tsx
785
1731
  import { useAdminCatalog } from "@fonderie/react-admin";
786
- import { Fragment as Fragment6, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1732
+ import { Fragment as Fragment7, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
787
1733
  function CatalogScreen({ client }) {
788
1734
  const { catalog, isLoading, error, refresh, deletePlan } = useAdminCatalog(client);
789
1735
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
790
- return /* @__PURE__ */ jsxs10("div", { style: styles.container, children: [
791
- /* @__PURE__ */ jsxs10("div", { style: styles.toolbar, children: [
792
- /* @__PURE__ */ jsx10("h1", { style: { ...styles.title, marginBottom: 0 }, children: "Catalog" }),
793
- /* @__PURE__ */ jsx10(
794
- "button",
1736
+ return /* @__PURE__ */ jsxs12("div", { style: styles.container, children: [
1737
+ /* @__PURE__ */ jsx12(
1738
+ PageHeader,
1739
+ {
1740
+ title: "Catalog",
1741
+ lead: "What you sell: plans as configured in code, and as stored in the database, side by side.",
1742
+ actions: /* @__PURE__ */ jsx12(RefreshButton, { onClick: () => void refresh(), busy: isLoading })
1743
+ }
1744
+ ),
1745
+ error ? /* @__PURE__ */ jsx12("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1746
+ isLoading && !catalog ? /* @__PURE__ */ jsx12("p", { style: styles.status, children: "Loading\u2026" }) : null,
1747
+ catalog ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
1748
+ /* @__PURE__ */ jsx12("h2", { style: { ...styles.subtitle, marginTop: 0 }, children: "Configured (code)" }),
1749
+ /* @__PURE__ */ jsx12(
1750
+ "pre",
795
1751
  {
796
- type: "button",
797
- style: styles.button,
798
- onClick: () => void refresh(),
799
- disabled: isLoading,
800
- children: "Refresh"
1752
+ style: {
1753
+ ...styles.card,
1754
+ ...styles.mono,
1755
+ margin: 0,
1756
+ overflowX: "auto",
1757
+ maxHeight: 360,
1758
+ lineHeight: 1.6
1759
+ },
1760
+ children: JSON.stringify(catalog.configured, null, 2)
801
1761
  }
802
- )
803
- ] }),
804
- error ? /* @__PURE__ */ jsx10("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
805
- isLoading && !catalog ? /* @__PURE__ */ jsx10("p", { style: styles.status, children: "Loading\u2026" }) : null,
806
- catalog ? /* @__PURE__ */ jsxs10(Fragment6, { children: [
807
- /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Configured (code)" }),
808
- /* @__PURE__ */ jsx10("pre", { style: { ...styles.mono, background: "var(--fonderie-surface-alt,#fafafa)", padding: 12, overflowX: "auto" }, children: JSON.stringify(catalog.configured, null, 2) }),
809
- /* @__PURE__ */ jsx10("h2", { style: styles.subtitle, children: "Stored (database)" }),
810
- catalog.stored.length === 0 ? /* @__PURE__ */ jsx10("p", { style: styles.muted, children: "No stored plans." }) : /* @__PURE__ */ jsxs10("table", { style: styles.table, children: [
811
- /* @__PURE__ */ jsx10("thead", { children: /* @__PURE__ */ jsxs10("tr", { children: [
812
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Plan" }),
813
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Tier" }),
814
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Seats" }),
815
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Monthly" }),
816
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Yearly" }),
817
- /* @__PURE__ */ jsx10("th", { style: styles.th, children: "Trial" }),
818
- /* @__PURE__ */ jsx10("th", { style: styles.th })
1762
+ ),
1763
+ /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Stored (database)" }),
1764
+ catalog.stored.length === 0 ? /* @__PURE__ */ jsx12(Empty, { icon: "catalog", title: "No stored plans", children: "Plans are configured in code; nothing has been written to the database." }) : /* @__PURE__ */ jsxs12("table", { style: styles.table, children: [
1765
+ /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsxs12("tr", { children: [
1766
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Plan" }),
1767
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Tier" }),
1768
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Seats" }),
1769
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Monthly" }),
1770
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Yearly" }),
1771
+ /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Trial" }),
1772
+ /* @__PURE__ */ jsx12("th", { style: styles.th })
819
1773
  ] }) }),
820
- /* @__PURE__ */ jsx10("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ jsxs10("tr", { children: [
821
- /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
822
- /* @__PURE__ */ jsx10("strong", { children: p.name }),
823
- p.description ? /* @__PURE__ */ jsx10("div", { style: styles.muted, children: p.description }) : null
1774
+ /* @__PURE__ */ jsx12("tbody", { children: catalog.stored.map((p) => /* @__PURE__ */ jsxs12("tr", { children: [
1775
+ /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
1776
+ /* @__PURE__ */ jsx12("strong", { children: p.name }),
1777
+ p.description ? /* @__PURE__ */ jsx12("div", { style: styles.muted, children: p.description }) : null
824
1778
  ] }),
825
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: p.tier }),
826
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: p.seats ?? "\u221E" }),
827
- /* @__PURE__ */ jsxs10("td", { style: styles.td, children: [
1779
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: p.tier }),
1780
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: p.seats ?? "\u221E" }),
1781
+ /* @__PURE__ */ jsxs12("td", { style: styles.td, children: [
828
1782
  money(p.pricing?.monthly),
829
1783
  " ",
830
1784
  p.pricing?.currency ?? ""
831
1785
  ] }),
832
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: money(p.pricing?.yearly) }),
833
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
834
- /* @__PURE__ */ jsx10("td", { style: styles.td, children: /* @__PURE__ */ jsx10(
1786
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: money(p.pricing?.yearly) }),
1787
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: p.trialDays ? `${p.trialDays} d` : "\u2014" }),
1788
+ /* @__PURE__ */ jsx12("td", { style: styles.td, children: /* @__PURE__ */ jsx12(
835
1789
  "button",
836
1790
  {
837
1791
  type: "button",
838
- style: styles.button,
1792
+ style: styles.buttonDanger,
839
1793
  onClick: () => {
840
1794
  if (window.confirm(`Delete stored plan "${p.name}"?`))
841
1795
  void deletePlan(p.id).catch(() => {
@@ -852,26 +1806,32 @@ function CatalogScreen({ client }) {
852
1806
 
853
1807
  // src/screens/SubscriberScreen.tsx
854
1808
  import { useAdminSubscriber, useAdminSubscribers } from "@fonderie/react-admin";
855
- import { useState as useState3 } from "react";
856
- import { Fragment as Fragment7, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1809
+ import { useState as useState5 } from "react";
1810
+ import { Fragment as Fragment8, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
857
1811
  function SubscriberScreen({ client, pageSize = 50 }) {
858
- const [type, setType] = useState3("user");
859
- const [idInput, setIdInput] = useState3("");
860
- const [subscriber, setSubscriber] = useState3(null);
1812
+ const [type, setType] = useState5("user");
1813
+ const [idInput, setIdInput] = useState5("");
1814
+ const [subscriber, setSubscriber] = useState5(null);
861
1815
  const list = useAdminSubscribers(client, { limit: pageSize });
862
1816
  const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(client, subscriber, { limit: 20 });
863
- const [amount, setAmount] = useState3("");
864
- const [note, setNote] = useState3("");
865
- const [granted, setGranted] = useState3(null);
1817
+ const [amount, setAmount] = useState5("");
1818
+ const [note, setNote] = useState5("");
1819
+ const [granted, setGranted] = useState5(null);
866
1820
  const open = (t2, id) => {
867
1821
  setGranted(null);
868
1822
  setType(t2);
869
1823
  setIdInput(id);
870
1824
  setSubscriber({ type: t2, id });
871
1825
  };
872
- return /* @__PURE__ */ jsxs11("div", { style: styles.container, children: [
873
- /* @__PURE__ */ jsx11("h1", { style: styles.title, children: "Subscriber" }),
874
- /* @__PURE__ */ jsxs11(
1826
+ return /* @__PURE__ */ jsxs13("div", { style: styles.container, children: [
1827
+ /* @__PURE__ */ jsx13(
1828
+ PageHeader,
1829
+ {
1830
+ title: "Subscribers",
1831
+ lead: "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
1832
+ }
1833
+ ),
1834
+ /* @__PURE__ */ jsxs13(
875
1835
  "form",
876
1836
  {
877
1837
  style: styles.toolbar,
@@ -881,31 +1841,34 @@ function SubscriberScreen({ client, pageSize = 50 }) {
881
1841
  setSubscriber(idInput.trim() ? { type, id: idInput.trim() } : null);
882
1842
  },
883
1843
  children: [
884
- /* @__PURE__ */ jsxs11(
1844
+ /* @__PURE__ */ jsxs13(
885
1845
  "select",
886
1846
  {
887
1847
  value: type,
888
1848
  onChange: (e) => setType(e.target.value),
889
- style: styles.button,
1849
+ style: styles.input,
890
1850
  "aria-label": "Type",
891
1851
  children: [
892
- /* @__PURE__ */ jsx11("option", { value: "user", children: "user" }),
893
- /* @__PURE__ */ jsx11("option", { value: "workspace", children: "workspace" })
1852
+ /* @__PURE__ */ jsx13("option", { value: "user", children: "user" }),
1853
+ /* @__PURE__ */ jsx13("option", { value: "workspace", children: "workspace" })
894
1854
  ]
895
1855
  }
896
1856
  ),
897
- /* @__PURE__ */ jsx11(
1857
+ /* @__PURE__ */ jsx13(
898
1858
  "input",
899
1859
  {
900
1860
  value: idInput,
901
1861
  onChange: (e) => setIdInput(e.target.value),
902
1862
  placeholder: "subscriber id",
903
- style: { ...styles.button, cursor: "text", minWidth: 280 },
1863
+ style: { ...styles.input, minWidth: 280 },
904
1864
  "aria-label": "Subscriber id"
905
1865
  }
906
1866
  ),
907
- /* @__PURE__ */ jsx11("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Look up" }),
908
- subscriber ? /* @__PURE__ */ jsx11(
1867
+ /* @__PURE__ */ jsxs13("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
1868
+ /* @__PURE__ */ jsx13(Icon, { name: "search", size: 14 }),
1869
+ "Look up"
1870
+ ] }),
1871
+ subscriber ? /* @__PURE__ */ jsxs13(
909
1872
  "button",
910
1873
  {
911
1874
  type: "button",
@@ -915,9 +1878,12 @@ function SubscriberScreen({ client, pageSize = 50 }) {
915
1878
  setIdInput("");
916
1879
  setGranted(null);
917
1880
  },
918
- children: "\u2190 All subscribers"
1881
+ children: [
1882
+ /* @__PURE__ */ jsx13(Icon, { name: "back", size: 14 }),
1883
+ "All subscribers"
1884
+ ]
919
1885
  }
920
- ) : /* @__PURE__ */ jsx11(
1886
+ ) : /* @__PURE__ */ jsx13(
921
1887
  "button",
922
1888
  {
923
1889
  type: "button",
@@ -930,22 +1896,22 @@ function SubscriberScreen({ client, pageSize = 50 }) {
930
1896
  ]
931
1897
  }
932
1898
  ),
933
- error ? /* @__PURE__ */ jsx11("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
934
- !subscriber ? /* @__PURE__ */ jsxs11(Fragment7, { children: [
935
- list.error ? /* @__PURE__ */ jsx11("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
936
- list.subscriptions.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx11("p", { style: styles.muted, children: "No subscribers yet." }) : /* @__PURE__ */ jsxs11("table", { style: styles.table, children: [
937
- /* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsxs11("tr", { children: [
938
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Subscriber" }),
939
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Plan" }),
940
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Status" }),
941
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Renews" })
1899
+ error ? /* @__PURE__ */ jsx13("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1900
+ !subscriber ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1901
+ list.error ? /* @__PURE__ */ jsx13("p", { style: styles.error, role: "alert", children: list.error.explanation }) : null,
1902
+ list.subscriptions.length === 0 && !list.isLoading ? /* @__PURE__ */ jsx13(Empty, { icon: "subscriber", title: "No subscribers yet", children: "Subscriptions appear here once someone checks out." }) : /* @__PURE__ */ jsxs13("table", { style: styles.table, children: [
1903
+ /* @__PURE__ */ jsx13("thead", { children: /* @__PURE__ */ jsxs13("tr", { children: [
1904
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Subscriber" }),
1905
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Plan" }),
1906
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Status" }),
1907
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Renews" })
942
1908
  ] }) }),
943
- /* @__PURE__ */ jsx11("tbody", { children: list.subscriptions.map((s) => /* @__PURE__ */ jsxs11("tr", { children: [
944
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: /* @__PURE__ */ jsxs11(
1909
+ /* @__PURE__ */ jsx13("tbody", { children: list.subscriptions.map((s) => /* @__PURE__ */ jsxs13("tr", { children: [
1910
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: /* @__PURE__ */ jsxs13(
945
1911
  "button",
946
1912
  {
947
1913
  type: "button",
948
- style: { ...styles.navItem, padding: 0, ...styles.mono },
1914
+ style: { ...styles.link, ...styles.mono },
949
1915
  onClick: () => open(s.subscriberType, s.subscriberId),
950
1916
  children: [
951
1917
  s.subscriberType,
@@ -954,20 +1920,29 @@ function SubscriberScreen({ client, pageSize = 50 }) {
954
1920
  ]
955
1921
  }
956
1922
  ) }),
957
- /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
958
- /* @__PURE__ */ jsx11("strong", { children: s.plan }),
1923
+ /* @__PURE__ */ jsxs13("td", { style: styles.td, children: [
1924
+ /* @__PURE__ */ jsx13("strong", { children: s.plan }),
959
1925
  " \xB7 ",
960
1926
  s.interval
961
1927
  ] }),
962
- /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
963
- s.status,
964
- s.cancelAtPeriodEnd ? /* @__PURE__ */ jsx11("span", { style: styles.badge, children: "cancels" }) : null
1928
+ /* @__PURE__ */ jsxs13("td", { style: styles.td, children: [
1929
+ /* @__PURE__ */ jsx13(
1930
+ Pill,
1931
+ {
1932
+ tone: s.status === "active" ? "ok" : s.status === "trialing" ? "info" : s.status === "past_due" ? "warn" : "neutral",
1933
+ children: s.status
1934
+ }
1935
+ ),
1936
+ s.cancelAtPeriodEnd ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1937
+ " ",
1938
+ /* @__PURE__ */ jsx13(Pill, { tone: "warn", dot: false, children: "cancels" })
1939
+ ] }) : null
965
1940
  ] }),
966
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : /* @__PURE__ */ jsx11("span", { style: styles.muted, children: "\u2014" }) })
1941
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : /* @__PURE__ */ jsx13("span", { style: styles.muted, children: "\u2014" }) })
967
1942
  ] }, s.id)) })
968
1943
  ] }),
969
- list.isLoading ? /* @__PURE__ */ jsx11("p", { style: styles.status, children: "Loading\u2026" }) : null,
970
- list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx11(
1944
+ list.isLoading ? /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Loading\u2026" }) : null,
1945
+ list.hasMore && !list.isLoading ? /* @__PURE__ */ jsx13(
971
1946
  "button",
972
1947
  {
973
1948
  type: "button",
@@ -977,27 +1952,27 @@ function SubscriberScreen({ client, pageSize = 50 }) {
977
1952
  }
978
1953
  ) : null
979
1954
  ] }) : null,
980
- subscriber && !isLoading ? /* @__PURE__ */ jsxs11(Fragment7, { children: [
981
- /* @__PURE__ */ jsx11("h2", { style: styles.subtitle, children: "Subscription" }),
982
- subscription ? /* @__PURE__ */ jsx11("table", { style: styles.table, children: /* @__PURE__ */ jsxs11("tbody", { children: [
983
- /* @__PURE__ */ jsxs11("tr", { children: [
984
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: "plan" }),
985
- /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
986
- /* @__PURE__ */ jsx11("strong", { children: subscription.plan }),
1955
+ subscriber && !isLoading ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1956
+ /* @__PURE__ */ jsx13("h2", { style: styles.subtitle, children: "Subscription" }),
1957
+ subscription ? /* @__PURE__ */ jsx13("table", { style: styles.table, children: /* @__PURE__ */ jsxs13("tbody", { children: [
1958
+ /* @__PURE__ */ jsxs13("tr", { children: [
1959
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: "plan" }),
1960
+ /* @__PURE__ */ jsxs13("td", { style: styles.td, children: [
1961
+ /* @__PURE__ */ jsx13("strong", { children: subscription.plan }),
987
1962
  " \xB7 ",
988
1963
  subscription.interval
989
1964
  ] })
990
1965
  ] }),
991
- /* @__PURE__ */ jsxs11("tr", { children: [
992
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: "status" }),
993
- /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
1966
+ /* @__PURE__ */ jsxs13("tr", { children: [
1967
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: "status" }),
1968
+ /* @__PURE__ */ jsxs13("td", { style: styles.td, children: [
994
1969
  subscription.status,
995
1970
  subscription.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""
996
1971
  ] })
997
1972
  ] }),
998
- /* @__PURE__ */ jsxs11("tr", { children: [
999
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: "period" }),
1000
- /* @__PURE__ */ jsxs11("td", { style: styles.td, children: [
1973
+ /* @__PURE__ */ jsxs13("tr", { children: [
1974
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: "period" }),
1975
+ /* @__PURE__ */ jsxs13("td", { style: styles.td, children: [
1001
1976
  subscription.currentPeriodStart ? new Date(subscription.currentPeriodStart).toLocaleDateString() : "\u2014",
1002
1977
  " ",
1003
1978
  "\u2192",
@@ -1005,28 +1980,49 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1005
1980
  subscription.currentPeriodEnd ? new Date(subscription.currentPeriodEnd).toLocaleDateString() : "\u2014"
1006
1981
  ] })
1007
1982
  ] }),
1008
- /* @__PURE__ */ jsxs11("tr", { children: [
1009
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: "provider" }),
1010
- /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.mono }, children: subscription.providerSubscriptionId ?? "\u2014" })
1983
+ /* @__PURE__ */ jsxs13("tr", { children: [
1984
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: "provider" }),
1985
+ /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.mono }, children: subscription.providerSubscriptionId ?? "\u2014" })
1011
1986
  ] })
1012
- ] }) }) : /* @__PURE__ */ jsx11("p", { style: styles.muted, children: "No subscription." }),
1013
- /* @__PURE__ */ jsx11("h2", { style: styles.subtitle, children: "Wallet" }),
1014
- wallet ? /* @__PURE__ */ jsxs11(Fragment7, { children: [
1015
- /* @__PURE__ */ jsxs11("p", { children: [
1016
- /* @__PURE__ */ jsx11("strong", { children: wallet.balance }),
1017
- " ",
1018
- wallet.currency,
1019
- " ",
1020
- /* @__PURE__ */ jsxs11("span", { style: styles.muted, children: [
1021
- "(granted ",
1022
- wallet.granted ?? "0",
1023
- " \xB7 purchased ",
1024
- wallet.purchased ?? "0",
1025
- ") \xB7 minor units, precision ",
1026
- wallet.precision
1027
- ] })
1028
- ] }),
1029
- /* @__PURE__ */ jsxs11(
1987
+ ] }) }) : /* @__PURE__ */ jsx13("p", { style: styles.muted, children: "No subscription." }),
1988
+ /* @__PURE__ */ jsx13("h2", { style: styles.subtitle, children: "Wallet" }),
1989
+ wallet ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1990
+ /* @__PURE__ */ jsxs13(
1991
+ "p",
1992
+ {
1993
+ style: {
1994
+ ...styles.card,
1995
+ display: "flex",
1996
+ alignItems: "baseline",
1997
+ gap: 8,
1998
+ flexWrap: "wrap"
1999
+ },
2000
+ children: [
2001
+ /* @__PURE__ */ jsx13(
2002
+ "strong",
2003
+ {
2004
+ style: {
2005
+ fontSize: 22,
2006
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
2007
+ },
2008
+ children: wallet.balance
2009
+ }
2010
+ ),
2011
+ " ",
2012
+ wallet.currency,
2013
+ " ",
2014
+ /* @__PURE__ */ jsxs13("span", { style: styles.muted, children: [
2015
+ "(granted ",
2016
+ wallet.granted ?? "0",
2017
+ " \xB7 purchased ",
2018
+ wallet.purchased ?? "0",
2019
+ ") \xB7 minor units, precision ",
2020
+ wallet.precision
2021
+ ] })
2022
+ ]
2023
+ }
2024
+ ),
2025
+ /* @__PURE__ */ jsxs13(
1030
2026
  "form",
1031
2027
  {
1032
2028
  style: styles.toolbar,
@@ -1045,47 +2041,58 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1045
2041
  });
1046
2042
  },
1047
2043
  children: [
1048
- /* @__PURE__ */ jsx11(
2044
+ /* @__PURE__ */ jsx13(
1049
2045
  "input",
1050
2046
  {
1051
2047
  value: amount,
1052
2048
  onChange: (e) => setAmount(e.target.value),
1053
2049
  placeholder: "amount (minor units)",
1054
- style: { ...styles.button, cursor: "text" },
2050
+ style: styles.input,
1055
2051
  "aria-label": "Amount"
1056
2052
  }
1057
2053
  ),
1058
- /* @__PURE__ */ jsx11(
2054
+ /* @__PURE__ */ jsx13(
1059
2055
  "input",
1060
2056
  {
1061
2057
  value: note,
1062
2058
  onChange: (e) => setNote(e.target.value),
1063
2059
  placeholder: "reason (optional)",
1064
- style: { ...styles.button, cursor: "text", minWidth: 220 },
2060
+ style: { ...styles.input, minWidth: 220 },
1065
2061
  "aria-label": "Reason"
1066
2062
  }
1067
2063
  ),
1068
- /* @__PURE__ */ jsx11("button", { type: "submit", style: styles.button, disabled: !/^\d+$/.test(amount.trim()), children: "Grant" }),
1069
- granted ? /* @__PURE__ */ jsx11("span", { style: styles.ok, children: granted }) : null
2064
+ /* @__PURE__ */ jsxs13(
2065
+ "button",
2066
+ {
2067
+ type: "submit",
2068
+ style: styles.buttonPrimary,
2069
+ disabled: !/^\d+$/.test(amount.trim()),
2070
+ children: [
2071
+ /* @__PURE__ */ jsx13(Icon, { name: "plus", size: 14 }),
2072
+ "Grant"
2073
+ ]
2074
+ }
2075
+ ),
2076
+ granted ? /* @__PURE__ */ jsx13("span", { style: styles.ok, children: granted }) : null
1070
2077
  ]
1071
2078
  }
1072
2079
  ),
1073
- /* @__PURE__ */ jsx11("h2", { style: styles.subtitle, children: "Ledger" }),
1074
- ledger.length === 0 ? /* @__PURE__ */ jsx11("p", { style: styles.muted, children: "Nothing moved yet." }) : /* @__PURE__ */ jsxs11("table", { style: styles.table, children: [
1075
- /* @__PURE__ */ jsx11("thead", { children: /* @__PURE__ */ jsxs11("tr", { children: [
1076
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "When" }),
1077
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Kind" }),
1078
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Amount" }),
1079
- /* @__PURE__ */ jsx11("th", { style: styles.th, children: "Note" })
2080
+ /* @__PURE__ */ jsx13("h2", { style: styles.subtitle, children: "Ledger" }),
2081
+ ledger.length === 0 ? /* @__PURE__ */ jsx13(Empty, { icon: "subscriber", title: "Nothing moved yet" }) : /* @__PURE__ */ jsxs13("table", { style: styles.table, children: [
2082
+ /* @__PURE__ */ jsx13("thead", { children: /* @__PURE__ */ jsxs13("tr", { children: [
2083
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "When" }),
2084
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Kind" }),
2085
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Amount" }),
2086
+ /* @__PURE__ */ jsx13("th", { style: styles.th, children: "Note" })
1080
2087
  ] }) }),
1081
- /* @__PURE__ */ jsx11("tbody", { children: ledger.map((t2) => /* @__PURE__ */ jsxs11("tr", { children: [
1082
- /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.muted }, children: new Date(t2.createdAt).toLocaleString() }),
1083
- /* @__PURE__ */ jsx11("td", { style: styles.td, children: t2.type }),
1084
- /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.mono }, children: t2.amount }),
1085
- /* @__PURE__ */ jsx11("td", { style: { ...styles.td, ...styles.muted }, children: t2.description ?? "" })
2088
+ /* @__PURE__ */ jsx13("tbody", { children: ledger.map((t2) => /* @__PURE__ */ jsxs13("tr", { children: [
2089
+ /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.muted }, children: new Date(t2.createdAt).toLocaleString() }),
2090
+ /* @__PURE__ */ jsx13("td", { style: styles.td, children: t2.type }),
2091
+ /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.mono }, children: t2.amount }),
2092
+ /* @__PURE__ */ jsx13("td", { style: { ...styles.td, ...styles.muted }, children: t2.description ?? "" })
1086
2093
  ] }, t2.id)) })
1087
2094
  ] }),
1088
- hasMoreLedger ? /* @__PURE__ */ jsx11(
2095
+ hasMoreLedger ? /* @__PURE__ */ jsx13(
1089
2096
  "button",
1090
2097
  {
1091
2098
  type: "button",
@@ -1094,37 +2101,43 @@ function SubscriberScreen({ client, pageSize = 50 }) {
1094
2101
  children: "Load more"
1095
2102
  }
1096
2103
  ) : null
1097
- ] }) : /* @__PURE__ */ jsx11("p", { style: styles.muted, children: "No wallet \u2014 billing has no wallet configuration." })
2104
+ ] }) : /* @__PURE__ */ jsx13("p", { style: styles.muted, children: "No wallet \u2014 billing has no wallet configuration." })
1098
2105
  ] }) : null
1099
2106
  ] });
1100
2107
  }
1101
2108
 
1102
2109
  // src/screens/AuditScreen.tsx
1103
2110
  import { useAdminAudit } from "@fonderie/react-admin";
1104
- import { useState as useState4 } from "react";
1105
- import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2111
+ import { useState as useState6 } from "react";
2112
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1106
2113
  function AuditScreen({ client, pageSize = 50 }) {
1107
- const [draft, setDraft] = useState4({ workspaceId: "", type: "", actorId: "" });
1108
- const [filter, setFilter] = useState4(
2114
+ const [draft, setDraft] = useState6({ workspaceId: "", type: "", actorId: "" });
2115
+ const [filter, setFilter] = useState6(
1109
2116
  {}
1110
2117
  );
1111
2118
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(client, {
1112
2119
  ...filter,
1113
2120
  limit: pageSize
1114
2121
  });
1115
- const field = (key, placeholder) => /* @__PURE__ */ jsx12(
2122
+ const field = (key, placeholder) => /* @__PURE__ */ jsx14(
1116
2123
  "input",
1117
2124
  {
1118
2125
  value: draft[key],
1119
2126
  onChange: (e) => setDraft({ ...draft, [key]: e.target.value }),
1120
2127
  placeholder,
1121
- style: { ...styles.button, cursor: "text" },
2128
+ style: styles.input,
1122
2129
  "aria-label": placeholder
1123
2130
  }
1124
2131
  );
1125
- return /* @__PURE__ */ jsxs12("div", { style: styles.container, children: [
1126
- /* @__PURE__ */ jsx12("h1", { style: styles.title, children: "Audit" }),
1127
- /* @__PURE__ */ jsxs12(
2132
+ return /* @__PURE__ */ jsxs14("div", { style: styles.container, children: [
2133
+ /* @__PURE__ */ jsx14(
2134
+ PageHeader,
2135
+ {
2136
+ title: "Audit",
2137
+ lead: "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2138
+ }
2139
+ ),
2140
+ /* @__PURE__ */ jsxs14(
1128
2141
  "form",
1129
2142
  {
1130
2143
  style: styles.toolbar,
@@ -1140,8 +2153,11 @@ function AuditScreen({ client, pageSize = 50 }) {
1140
2153
  field("workspaceId", "workspace id (all if empty)"),
1141
2154
  field("type", "event type"),
1142
2155
  field("actorId", "actor id"),
1143
- /* @__PURE__ */ jsx12("button", { type: "submit", style: styles.button, disabled: isLoading, children: "Filter" }),
1144
- /* @__PURE__ */ jsx12(
2156
+ /* @__PURE__ */ jsxs14("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
2157
+ /* @__PURE__ */ jsx14(Icon, { name: "search", size: 14 }),
2158
+ "Filter"
2159
+ ] }),
2160
+ /* @__PURE__ */ jsx14(
1145
2161
  "button",
1146
2162
  {
1147
2163
  type: "button",
@@ -1154,25 +2170,25 @@ function AuditScreen({ client, pageSize = 50 }) {
1154
2170
  ]
1155
2171
  }
1156
2172
  ),
1157
- error ? /* @__PURE__ */ jsx12("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
1158
- events.length === 0 && !isLoading ? /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "Nothing recorded for this filter." }) : /* @__PURE__ */ jsxs12("table", { style: styles.table, children: [
1159
- /* @__PURE__ */ jsx12("thead", { children: /* @__PURE__ */ jsxs12("tr", { children: [
1160
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "When" }),
1161
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Type" }),
1162
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Workspace" }),
1163
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Actor" }),
1164
- /* @__PURE__ */ jsx12("th", { style: styles.th, children: "Request" })
2173
+ error ? /* @__PURE__ */ jsx14("p", { style: styles.error, role: "alert", children: error.explanation }) : null,
2174
+ events.length === 0 && !isLoading ? /* @__PURE__ */ jsx14(Empty, { icon: "audit", title: "Nothing recorded for this filter" }) : /* @__PURE__ */ jsxs14("table", { style: styles.table, children: [
2175
+ /* @__PURE__ */ jsx14("thead", { children: /* @__PURE__ */ jsxs14("tr", { children: [
2176
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "When" }),
2177
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Type" }),
2178
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Workspace" }),
2179
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Actor" }),
2180
+ /* @__PURE__ */ jsx14("th", { style: styles.th, children: "Request" })
1165
2181
  ] }) }),
1166
- /* @__PURE__ */ jsx12("tbody", { children: events.map((e) => /* @__PURE__ */ jsxs12("tr", { children: [
1167
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.createdAt).toLocaleString() }),
1168
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.mono }, children: e.type }),
1169
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.mono }, children: String(e.payload["workspaceId"] ?? "\u2014") }),
1170
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.mono }, children: e.actorId ?? "\u2014" }),
1171
- /* @__PURE__ */ jsx12("td", { style: { ...styles.td, ...styles.muted }, children: e.requestId ?? "" })
2182
+ /* @__PURE__ */ jsx14("tbody", { children: events.map((e) => /* @__PURE__ */ jsxs14("tr", { children: [
2183
+ /* @__PURE__ */ jsx14("td", { style: { ...styles.td, ...styles.muted }, children: new Date(e.createdAt).toLocaleString() }),
2184
+ /* @__PURE__ */ jsx14("td", { style: { ...styles.td, ...styles.mono }, children: e.type }),
2185
+ /* @__PURE__ */ jsx14("td", { style: { ...styles.td, ...styles.mono }, children: String(e.payload["workspaceId"] ?? "\u2014") }),
2186
+ /* @__PURE__ */ jsx14("td", { style: { ...styles.td, ...styles.mono }, children: e.actorId ?? "\u2014" }),
2187
+ /* @__PURE__ */ jsx14("td", { style: { ...styles.td, ...styles.muted }, children: e.requestId ?? "" })
1172
2188
  ] }, e.id)) })
1173
2189
  ] }),
1174
- isLoading ? /* @__PURE__ */ jsx12("p", { style: styles.status, children: "Loading\u2026" }) : null,
1175
- hasMore && !isLoading ? /* @__PURE__ */ jsx12(
2190
+ isLoading ? /* @__PURE__ */ jsx14("p", { style: styles.status, children: "Loading\u2026" }) : null,
2191
+ hasMore && !isLoading ? /* @__PURE__ */ jsx14(
1176
2192
  "button",
1177
2193
  {
1178
2194
  type: "button",
@@ -1185,7 +2201,36 @@ function AuditScreen({ client, pageSize = 50 }) {
1185
2201
  }
1186
2202
 
1187
2203
  // src/screens/AdminShell.tsx
1188
- import { Fragment as Fragment8, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2204
+ import { Fragment as Fragment9, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
2205
+ var ICON = {
2206
+ attention: "attention",
2207
+ modules: "modules",
2208
+ environment: "environment",
2209
+ doctor: "doctor",
2210
+ routes: "routes",
2211
+ users: "users",
2212
+ catalog: "catalog",
2213
+ subscriber: "subscriber",
2214
+ settings: "settings",
2215
+ templates: "templates",
2216
+ audit: "audit",
2217
+ log: "log",
2218
+ tokens: "tokens",
2219
+ operators: "users",
2220
+ migrations: "migrations"
2221
+ };
2222
+ var NARROW = "(max-width: 820px)";
2223
+ function useNarrow() {
2224
+ const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2225
+ const [narrow, setNarrow] = useState7(() => query?.matches ?? false);
2226
+ useEffect(() => {
2227
+ if (!query) return;
2228
+ const on = (e) => setNarrow(e.matches);
2229
+ query.addEventListener("change", on);
2230
+ return () => query.removeEventListener("change", on);
2231
+ }, [query]);
2232
+ return narrow;
2233
+ }
1189
2234
  var NAV = [
1190
2235
  { group: "Today", items: [{ page: "attention", label: "Attention" }] },
1191
2236
  {
@@ -1212,7 +2257,8 @@ var NAV = [
1212
2257
  items: [
1213
2258
  { page: "audit", label: "Audit", needs: "audit" },
1214
2259
  { page: "log", label: "Admin log" },
1215
- { page: "tokens", label: "Access" },
2260
+ { page: "operators", label: "Operators", needs: "operators" },
2261
+ { page: "tokens", label: "Tokens" },
1216
2262
  { page: "migrations", label: "Migrations" }
1217
2263
  ]
1218
2264
  }
@@ -1226,64 +2272,79 @@ function AdminShell({
1226
2272
  auditClient,
1227
2273
  environment,
1228
2274
  page,
1229
- onNavigate
2275
+ onNavigate,
2276
+ appName = "Admin",
2277
+ envLabel,
2278
+ footer,
2279
+ operators = false,
2280
+ currentOperator
1230
2281
  }) {
1231
- const [own, setOwn] = useState5("attention");
2282
+ const narrow = useNarrow();
2283
+ const [drawer, setDrawer] = useState7(false);
2284
+ const [own, setOwn] = useState7("attention");
1232
2285
  const current = page ?? own;
1233
2286
  const go = (p) => {
1234
2287
  if (onNavigate) onNavigate(p);
1235
2288
  if (page === void 0) setOwn(p);
2289
+ setDrawer(false);
1236
2290
  };
1237
- const [editing, setEditing] = useState5(null);
2291
+ const [editing, setEditing] = useState7(null);
1238
2292
  const has = {
1239
2293
  config: Boolean(configClient),
1240
2294
  courier: Boolean(courierClient),
1241
2295
  auth: Boolean(authClient),
1242
2296
  billing: Boolean(billingClient),
1243
- audit: Boolean(auditClient)
2297
+ audit: Boolean(auditClient),
2298
+ operators
1244
2299
  };
1245
2300
  let body;
1246
2301
  switch (current) {
1247
2302
  case "attention":
1248
- body = /* @__PURE__ */ jsx13(AttentionScreen, { client });
2303
+ body = /* @__PURE__ */ jsx15(AttentionScreen, { client });
1249
2304
  break;
1250
2305
  case "modules":
1251
- body = /* @__PURE__ */ jsx13(ModulesScreen, { client });
2306
+ body = /* @__PURE__ */ jsx15(ModulesScreen, { client });
1252
2307
  break;
1253
2308
  case "doctor":
1254
- body = /* @__PURE__ */ jsx13(DoctorScreen, { client });
2309
+ body = /* @__PURE__ */ jsx15(DoctorScreen, { client });
1255
2310
  break;
1256
2311
  case "environment":
1257
- body = /* @__PURE__ */ jsx13(EnvironmentScreen, { client });
2312
+ body = /* @__PURE__ */ jsx15(EnvironmentScreen, { client });
1258
2313
  break;
1259
2314
  case "routes":
1260
- body = /* @__PURE__ */ jsx13(RoutesScreen, { client });
2315
+ body = /* @__PURE__ */ jsx15(RoutesScreen, { client });
1261
2316
  break;
1262
2317
  case "tokens":
1263
- body = /* @__PURE__ */ jsx13(TokensScreen, { client });
2318
+ body = /* @__PURE__ */ jsx15(TokensScreen, { client });
2319
+ break;
2320
+ case "operators":
2321
+ body = /* @__PURE__ */ jsx15(OperatorsScreen, { client, me: currentOperator });
1264
2322
  break;
1265
2323
  case "migrations":
1266
- body = /* @__PURE__ */ jsx13(MigrationsScreen, { client });
2324
+ body = /* @__PURE__ */ jsx15(MigrationsScreen, { client });
1267
2325
  break;
1268
2326
  case "log":
1269
- body = /* @__PURE__ */ jsx13(AdminLogScreen, { client });
2327
+ body = /* @__PURE__ */ jsx15(AdminLogScreen, { client });
1270
2328
  break;
1271
2329
  case "catalog":
1272
- body = billingClient ? /* @__PURE__ */ jsx13(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Pass a BillingAdminClient to see the catalog here." });
2330
+ body = billingClient ? /* @__PURE__ */ jsx15(CatalogScreen, { client: billingClient }) : /* @__PURE__ */ jsx15("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to see the catalog here." });
1273
2331
  break;
1274
2332
  case "subscriber":
1275
- body = billingClient ? /* @__PURE__ */ jsx13(SubscriberScreen, { client: billingClient }) : /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Pass a BillingAdminClient to look up subscribers here." });
2333
+ body = billingClient ? /* @__PURE__ */ jsx15(SubscriberScreen, { client: billingClient }) : /* @__PURE__ */ jsx15("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a BillingAdminClient to look up subscribers here." });
1276
2334
  break;
1277
2335
  case "audit":
1278
- body = auditClient ? /* @__PURE__ */ jsx13(AuditScreen, { client: auditClient }) : /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Pass an AuditAdminClient to see the audit trail here." });
2336
+ body = auditClient ? /* @__PURE__ */ jsx15(AuditScreen, { client: auditClient }) : /* @__PURE__ */ jsx15("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuditAdminClient to see the audit trail here." });
1279
2337
  break;
1280
2338
  case "users":
1281
- body = authClient ? /* @__PURE__ */ jsx13(UsersScreen, { client: authClient }) : /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Pass an AuthAdminClient to look up users here." });
2339
+ body = authClient ? /* @__PURE__ */ jsx15(UsersScreen, { client: authClient }) : /* @__PURE__ */ jsx15("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuthAdminClient to look up users here." });
1282
2340
  break;
1283
2341
  case "settings":
1284
- body = !configClient ? /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1285
- /* @__PURE__ */ jsx13("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
1286
- /* @__PURE__ */ jsx13(
2342
+ body = !configClient ? /* @__PURE__ */ jsx15("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
2343
+ /* @__PURE__ */ jsx15("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs15("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2344
+ /* @__PURE__ */ jsx15(Icon, { name: "back", size: 14 }),
2345
+ "Config & secrets"
2346
+ ] }) }),
2347
+ /* @__PURE__ */ jsx15(
1287
2348
  ConfigEditorScreen,
1288
2349
  {
1289
2350
  client: configClient,
@@ -1293,7 +2354,7 @@ function AdminShell({
1293
2354
  onSaved: () => setEditing(null)
1294
2355
  }
1295
2356
  )
1296
- ] }) : /* @__PURE__ */ jsx13(
2357
+ ] }) : /* @__PURE__ */ jsx15(
1297
2358
  ConfigListScreen,
1298
2359
  {
1299
2360
  client: configClient,
@@ -1306,9 +2367,12 @@ function AdminShell({
1306
2367
  );
1307
2368
  break;
1308
2369
  case "templates":
1309
- body = !courierClient ? /* @__PURE__ */ jsx13("p", { style: styles.status, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs13(Fragment8, { children: [
1310
- /* @__PURE__ */ jsx13("button", { type: "button", style: styles.button, onClick: () => setEditing(null), children: "\u2190 Back" }),
1311
- /* @__PURE__ */ jsx13(
2370
+ body = !courierClient ? /* @__PURE__ */ jsx15("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
2371
+ /* @__PURE__ */ jsx15("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs15("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2372
+ /* @__PURE__ */ jsx15(Icon, { name: "back", size: 14 }),
2373
+ "Templates"
2374
+ ] }) }),
2375
+ /* @__PURE__ */ jsx15(
1312
2376
  TemplateEditorScreen,
1313
2377
  {
1314
2378
  client: courierClient,
@@ -1316,7 +2380,7 @@ function AdminShell({
1316
2380
  onSaved: () => setEditing(null)
1317
2381
  }
1318
2382
  )
1319
- ] }) : /* @__PURE__ */ jsx13(
2383
+ ] }) : /* @__PURE__ */ jsx15(
1320
2384
  TemplateListScreen,
1321
2385
  {
1322
2386
  client: courierClient,
@@ -1325,29 +2389,105 @@ function AdminShell({
1325
2389
  );
1326
2390
  break;
1327
2391
  }
1328
- return /* @__PURE__ */ jsxs13("div", { style: styles.shell, children: [
1329
- /* @__PURE__ */ jsx13("nav", { style: styles.nav, "aria-label": "Admin", children: NAV.map((g) => {
1330
- const items = g.items.filter((i) => !i.needs || has[i.needs]);
1331
- if (items.length === 0) return null;
1332
- return /* @__PURE__ */ jsxs13("div", { children: [
1333
- /* @__PURE__ */ jsx13("div", { style: styles.navGroup, children: g.group }),
1334
- items.map((i) => /* @__PURE__ */ jsx13(
2392
+ const production = envLabel ? /^prod/i.test(envLabel) : false;
2393
+ const brand = /* @__PURE__ */ jsxs15("div", { style: styles.navBrand, children: [
2394
+ /* @__PURE__ */ jsx15("div", { style: styles.navMark, children: /* @__PURE__ */ jsx15(Icon, { name: "mark", size: 15 }) }),
2395
+ /* @__PURE__ */ jsxs15("div", { style: { minWidth: 0, flex: 1 }, children: [
2396
+ /* @__PURE__ */ jsx15("div", { style: styles.navBrandTitle, children: appName }),
2397
+ envLabel ? /* @__PURE__ */ jsx15(
2398
+ "span",
2399
+ {
2400
+ style: {
2401
+ ...styles.badge,
2402
+ marginTop: 3,
2403
+ padding: "0 7px",
2404
+ fontSize: 11,
2405
+ ...production ? {
2406
+ color: "var(--fonderie-danger,#e00)",
2407
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 35%, transparent)"
2408
+ } : {}
2409
+ },
2410
+ children: envLabel
2411
+ }
2412
+ ) : null
2413
+ ] }),
2414
+ narrow ? /* @__PURE__ */ jsx15(
2415
+ "button",
2416
+ {
2417
+ type: "button",
2418
+ style: styles.buttonGhost,
2419
+ "aria-label": "Close menu",
2420
+ onClick: () => setDrawer(false),
2421
+ children: /* @__PURE__ */ jsx15(Icon, { name: "close" })
2422
+ }
2423
+ ) : null
2424
+ ] });
2425
+ const nav = /* @__PURE__ */ jsxs15(
2426
+ "nav",
2427
+ {
2428
+ style: {
2429
+ ...styles.nav,
2430
+ ...narrow ? { width: 280, maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2431
+ },
2432
+ "aria-label": "Admin",
2433
+ children: [
2434
+ brand,
2435
+ /* @__PURE__ */ jsx15("div", { style: styles.navScroll, children: NAV.map((g) => {
2436
+ const items = g.items.filter((i) => !i.needs || has[i.needs]);
2437
+ if (items.length === 0) return null;
2438
+ return /* @__PURE__ */ jsxs15("div", { children: [
2439
+ /* @__PURE__ */ jsx15("div", { style: styles.navGroup, children: g.group }),
2440
+ items.map((i) => {
2441
+ const active = current === i.page;
2442
+ return /* @__PURE__ */ jsxs15(
2443
+ "button",
2444
+ {
2445
+ type: "button",
2446
+ className: "fonderie-admin-nav-item",
2447
+ style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2448
+ "aria-current": active ? "page" : void 0,
2449
+ onClick: () => {
2450
+ setEditing(null);
2451
+ go(i.page);
2452
+ },
2453
+ children: [
2454
+ /* @__PURE__ */ jsx15(Icon, { name: ICON[i.page], style: active ? styles.navIconActive : void 0 }),
2455
+ i.label
2456
+ ]
2457
+ },
2458
+ i.page
2459
+ );
2460
+ })
2461
+ ] }, g.group);
2462
+ }) }),
2463
+ footer ? /* @__PURE__ */ jsx15("div", { style: styles.navFooter, children: footer }) : null
2464
+ ]
2465
+ }
2466
+ );
2467
+ const title = NAV.flatMap((g) => g.items).find((i) => i.page === current)?.label ?? "";
2468
+ return /* @__PURE__ */ jsxs15("div", { style: { ...styles.shell, ...narrow ? { flexDirection: "column" } : {} }, children: [
2469
+ narrow ? /* @__PURE__ */ jsxs15(Fragment9, { children: [
2470
+ /* @__PURE__ */ jsxs15("div", { style: styles.topbar, children: [
2471
+ /* @__PURE__ */ jsx15(
1335
2472
  "button",
1336
2473
  {
1337
2474
  type: "button",
1338
- style: { ...styles.navItem, ...current === i.page ? styles.navItemActive : {} },
1339
- "aria-current": current === i.page ? "page" : void 0,
1340
- onClick: () => {
1341
- setEditing(null);
1342
- go(i.page);
1343
- },
1344
- children: i.label
1345
- },
1346
- i.page
1347
- ))
1348
- ] }, g.group);
1349
- }) }),
1350
- /* @__PURE__ */ jsx13("main", { style: styles.main, children: body })
2475
+ style: styles.buttonGhost,
2476
+ "aria-label": "Open menu",
2477
+ "aria-expanded": drawer,
2478
+ onClick: () => setDrawer(true),
2479
+ children: /* @__PURE__ */ jsx15(Icon, { name: "menu" })
2480
+ }
2481
+ ),
2482
+ /* @__PURE__ */ jsx15("span", { style: { fontWeight: 600, fontSize: 14 }, children: title }),
2483
+ envLabel ? /* @__PURE__ */ jsx15("span", { style: { ...styles.badge, marginLeft: "auto" }, children: envLabel }) : null
2484
+ ] }),
2485
+ drawer ? /* @__PURE__ */ jsxs15("div", { style: styles.drawer, children: [
2486
+ nav,
2487
+ /* @__PURE__ */ jsx15("div", { style: styles.scrim, onClick: () => setDrawer(false), "aria-hidden": "true" })
2488
+ ] }) : null
2489
+ ] }) : nav,
2490
+ /* @__PURE__ */ jsx15("main", { className: "fonderie-admin-main", style: styles.main, children: body })
1351
2491
  ] });
1352
2492
  }
1353
2493
  export {
@@ -1360,6 +2500,7 @@ export {
1360
2500
  EnvironmentScreen,
1361
2501
  MigrationsScreen,
1362
2502
  ModulesScreen,
2503
+ OperatorsScreen,
1363
2504
  RoutesScreen,
1364
2505
  SubscriberScreen,
1365
2506
  TokensScreen,