@fonderie/vue-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.ts
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
3
3
  import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
4
- import { computed as computed2, defineComponent as defineComponent13, h as h14, ref as ref5 } from "vue";
4
+ import { computed as computed2, defineComponent as defineComponent14, h as h16, onBeforeUnmount, onMounted, ref as ref7 } from "vue";
5
5
 
6
6
  // src/styles.ts
7
7
  var t = {
@@ -23,7 +23,32 @@ 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 styles = {
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
+ };
51
+ var raw = {
27
52
  shell: {
28
53
  display: "flex",
29
54
  minHeight: "100vh",
@@ -31,137 +56,568 @@ 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"
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"
42
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"
107
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"
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
  };
391
+ var UNITLESS = /* @__PURE__ */ new Set([
392
+ "fontWeight",
393
+ "lineHeight",
394
+ "opacity",
395
+ "zIndex",
396
+ "flex",
397
+ "flexGrow",
398
+ "flexShrink",
399
+ "order"
400
+ ]);
401
+ function withUnits(sheet) {
402
+ const out = {};
403
+ for (const [name, rule] of Object.entries(sheet)) {
404
+ const fixed = {};
405
+ for (const [prop, value] of Object.entries(rule)) {
406
+ fixed[prop] = typeof value === "number" && !UNITLESS.has(prop) ? `${value}px` : value;
407
+ }
408
+ out[name] = fixed;
409
+ }
410
+ return out;
411
+ }
412
+ var styles = withUnits(raw);
413
+
414
+ // src/ui.ts
415
+ import { h } from "vue";
416
+
417
+ // src/icons.ts
418
+ var ICONS = {
419
+ attention: ["M22 12h-4l-3 9L9 3l-3 9H2"],
420
+ modules: [
421
+ "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",
422
+ "M3.3 7 12 12l8.7-5",
423
+ "M12 22V12"
424
+ ],
425
+ environment: [
426
+ "M4 21v-7",
427
+ "M4 10V3",
428
+ "M12 21v-9",
429
+ "M12 8V3",
430
+ "M20 21v-5",
431
+ "M20 12V3",
432
+ "M1 14h6",
433
+ "M9 8h6",
434
+ "M17 16h6"
435
+ ],
436
+ doctor: [
437
+ "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"
438
+ ],
439
+ routes: [
440
+ "M6 3v12",
441
+ "M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
442
+ "M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
443
+ "M18 9a9 9 0 0 1-9 9"
444
+ ],
445
+ users: [
446
+ "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",
447
+ "M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z",
448
+ "M22 21v-2a4 4 0 0 0-3-3.87",
449
+ "M16 3.13a4 4 0 0 1 0 7.75"
450
+ ],
451
+ catalog: [
452
+ "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",
453
+ "M7.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
454
+ ],
455
+ subscriber: ["M2 7h20v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z", "M2 11h20", "M6 15h4"],
456
+ settings: [
457
+ "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",
458
+ "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
459
+ ],
460
+ templates: [
461
+ "M22 7l-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7",
462
+ "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"
463
+ ],
464
+ audit: ["M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z", "M9 12l2 2 4-4"],
465
+ log: ["M12 8v4l3 3", "M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5"],
466
+ tokens: [
467
+ "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",
468
+ "M16.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
469
+ ],
470
+ migrations: [
471
+ "M12 22c4.97 0 9-1.79 9-4V6",
472
+ "M3 6v12c0 2.21 4.03 4 9 4",
473
+ "M12 10c4.97 0 9-1.79 9-4s-4.03-4-9-4-9 1.79-9 4 4.03 4 9 4z",
474
+ "M3 12c0 2.21 4.03 4 9 4s9-1.79 9-4"
475
+ ],
476
+ menu: ["M4 6h16", "M4 12h16", "M4 18h16"],
477
+ close: ["M18 6 6 18", "m6 6 12 12"],
478
+ refresh: [
479
+ "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
480
+ "M21 3v5h-5",
481
+ "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
482
+ "M8 16H3v5"
483
+ ],
484
+ check: ["M20 6 9 17l-5-5"],
485
+ alert: [
486
+ "M12 9v4",
487
+ "M12 17h.01",
488
+ "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"
489
+ ],
490
+ search: ["M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", "m21 21-4.3-4.3"],
491
+ back: ["m12 19-7-7 7-7", "M19 12H5"],
492
+ logout: ["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", "m16 17 5-5-5-5", "M21 12H9"],
493
+ lock: [
494
+ "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",
495
+ "M7 11V7a5 5 0 0 1 10 0v4"
496
+ ],
497
+ plus: ["M12 5v14", "M5 12h14"],
498
+ mark: [
499
+ "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",
500
+ "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",
501
+ "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",
502
+ "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"
503
+ ]
504
+ };
505
+
506
+ // src/ui.ts
507
+ function icon(name, size = 16, style) {
508
+ return h(
509
+ "svg",
510
+ {
511
+ width: size,
512
+ height: size,
513
+ viewBox: "0 0 24 24",
514
+ fill: "none",
515
+ stroke: "currentColor",
516
+ "stroke-width": 2,
517
+ "stroke-linecap": "round",
518
+ "stroke-linejoin": "round",
519
+ "aria-hidden": "true",
520
+ style: { flexShrink: 0, ...style }
521
+ },
522
+ ICONS[name].map((d) => h("path", { key: d, d }))
523
+ );
524
+ }
525
+ function pageHeader(title, lead, actions) {
526
+ return h("div", { style: styles.header }, [
527
+ h("div", { style: { minWidth: 0 } }, [
528
+ h("h1", { style: styles.title }, title),
529
+ lead ? h("p", { style: styles.lead }, lead) : null
530
+ ]),
531
+ actions?.filter(Boolean).length ? h("div", { style: styles.actions }, actions) : null
532
+ ]);
533
+ }
534
+ function pill(tone, text, dot = true) {
535
+ const color = toneColor[tone];
536
+ return h(
537
+ "span",
538
+ { style: { ...styles.pill, color, background: tint(color, tone === "neutral" ? 10 : 13) } },
539
+ [dot ? h("span", { style: styles.dot }) : null, text]
540
+ );
541
+ }
542
+ function stat(label, value, hint, tone) {
543
+ return h("div", { style: styles.card }, [
544
+ h("div", { style: styles.statLabel }, label),
545
+ h(
546
+ "div",
547
+ { style: { ...styles.statValue, ...tone ? { color: toneColor[tone] } : {} } },
548
+ value
549
+ ),
550
+ hint ? h("div", { style: styles.statHint }, hint) : null
551
+ ]);
552
+ }
553
+ function empty(title, body, iconName = "check") {
554
+ return h("div", { style: styles.empty }, [
555
+ icon(iconName, 22),
556
+ h("div", { style: styles.emptyTitle }, title),
557
+ body ? h("div", body) : null
558
+ ]);
559
+ }
560
+ function methodChip(method) {
561
+ const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
562
+ const color = toneColor[tone];
563
+ return h(
564
+ "span",
565
+ {
566
+ style: {
567
+ ...styles.mono,
568
+ fontSize: "11px",
569
+ fontWeight: 700,
570
+ color,
571
+ background: tint(color, 12),
572
+ borderRadius: "4px",
573
+ padding: "2px 6px",
574
+ display: "inline-block",
575
+ minWidth: "44px",
576
+ textAlign: "center"
577
+ }
578
+ },
579
+ method
580
+ );
581
+ }
131
582
 
132
583
  // src/screens/AdminLogScreen.ts
133
584
  import { useAdminLog } from "@fonderie/vue-admin";
134
- import { defineComponent, h as h2 } from "vue";
585
+ import { defineComponent, h as h3 } from "vue";
135
586
 
136
587
  // src/screens/common.ts
137
- import { h } from "vue";
588
+ import { h as h2 } from "vue";
138
589
  function page(title, state, content, toolbar = [], options = {}) {
139
- const body = state.isLoading.value ? h("p", { style: styles.status }, options.loadingText ?? "Loading\u2026") : state.error.value ? h(
590
+ const body = state.isLoading.value ? h2("p", { style: styles.status }, options.loadingText ?? "Loading\u2026") : state.error.value ? h2(
140
591
  "p",
141
592
  { style: styles.error, role: "alert" },
142
593
  (options.errorText ?? ((e) => e.explanation))(state.error.value)
143
594
  ) : content();
144
- return h("div", { style: styles.container }, [
145
- h("div", { style: styles.toolbar }, [
146
- h("h1", { style: { ...styles.title, marginBottom: 0 } }, title),
147
- ...toolbar
148
- ]),
149
- body
150
- ]);
595
+ return h2("div", { style: styles.container }, [pageHeader(title, options.lead, toolbar), body]);
151
596
  }
152
597
  function table(headers, rows) {
153
- return h("table", { style: styles.table }, [
154
- h("thead", [
155
- h(
598
+ return h2("table", { style: styles.table }, [
599
+ h2("thead", [
600
+ h2(
156
601
  "tr",
157
- headers.map((t2) => h("th", { style: styles.th }, t2))
602
+ headers.map(
603
+ (t2) => typeof t2 === "string" ? h2("th", { style: styles.th }, t2) : h2("th", { style: { ...styles.th, ...t2.style } }, t2.label)
604
+ )
158
605
  )
159
606
  ]),
160
- h("tbody", rows)
607
+ h2("tbody", rows)
161
608
  ]);
162
609
  }
163
- var td = (children, extra = {}) => h("td", { style: { ...styles.td, ...extra } }, children);
164
- var refreshButton = (label, disabled, onClick) => h("button", { type: "button", style: styles.button, disabled, onClick }, label);
610
+ var td = (children, extra = {}) => h2("td", { style: { ...styles.td, ...extra } }, children);
611
+ var refreshButton = (label, disabled, onClick) => h2("button", { type: "button", style: styles.button, disabled, onClick }, [
612
+ icon("refresh", 14),
613
+ disabled ? "Working\u2026" : label
614
+ ]);
615
+ var loadMoreButton = (onClick) => h2(
616
+ "button",
617
+ { type: "button", style: { ...styles.button, marginTop: "12px" }, onClick },
618
+ "Load more"
619
+ );
620
+ var actionButton = (label, disabled, onClick, style = styles.button) => h2("button", { type: "button", style, disabled, onClick }, label);
165
621
 
166
622
  // src/screens/AdminLogScreen.ts
167
623
  var AdminLogScreen = defineComponent({
@@ -174,147 +630,269 @@ var AdminLogScreen = defineComponent({
174
630
  const { entries, hasMore, isLoading, error, refresh, loadMore } = useAdminLog(props.client, {
175
631
  limit: props.pageSize
176
632
  });
177
- return () => h2("div", { style: styles.container }, [
178
- h2("div", { style: styles.toolbar }, [
179
- h2("h1", { style: { ...styles.title, marginBottom: 0 } }, "Admin log"),
180
- refreshButton("Refresh", isLoading.value, () => void refresh())
181
- ]),
182
- error.value ? h2(
633
+ return () => h3("div", { style: styles.container }, [
634
+ pageHeader(
635
+ "Admin log",
636
+ "Every request made through this surface, newest first \u2014 refused ones included.",
637
+ [refreshButton("Refresh", isLoading.value, () => void refresh())]
638
+ ),
639
+ error.value ? h3(
183
640
  "p",
184
641
  { style: styles.error, role: "alert" },
185
642
  error.value.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.value.explanation
186
- ) : [
643
+ ) : entries.value.length === 0 && !isLoading.value ? empty("No requests yet", void 0, "log") : [
187
644
  table(
188
645
  ["When", "Actor", "Request", "Status", "Module"],
189
646
  entries.value.map(
190
- (e) => h2("tr", { key: e.id }, [
191
- td(new Date(e.at).toLocaleString(), styles.muted),
647
+ (e) => h3("tr", { key: e.id }, [
648
+ td(new Date(e.at).toLocaleString(), {
649
+ ...styles.muted,
650
+ whiteSpace: "nowrap"
651
+ }),
192
652
  td(e.actor),
193
- td(`${e.method} ${e.path}`, styles.mono),
194
653
  td([
195
- h2(
196
- "span",
197
- { style: e.status >= 400 ? styles.bad : styles.ok },
198
- String(e.status)
199
- ),
654
+ methodChip(e.method),
200
655
  " ",
201
- h2("span", { style: styles.muted }, `${e.durationMs} ms`)
656
+ h3("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.path)
202
657
  ]),
658
+ td(
659
+ [
660
+ pill(
661
+ e.status >= 500 ? "bad" : e.status >= 400 ? "warn" : "ok",
662
+ String(e.status)
663
+ ),
664
+ " ",
665
+ h3("span", { style: styles.muted }, `${e.durationMs} ms`)
666
+ ],
667
+ { whiteSpace: "nowrap" }
668
+ ),
203
669
  td(e.module, styles.muted)
204
670
  ])
205
671
  )
206
672
  ),
207
- isLoading.value ? h2("p", { style: styles.status }, "Loading\u2026") : null,
208
- hasMore.value && !isLoading.value ? h2(
209
- "button",
210
- {
211
- type: "button",
212
- style: { ...styles.button, marginTop: "12px" },
213
- onClick: () => void loadMore()
214
- },
215
- "Load more"
216
- ) : null
673
+ isLoading.value ? h3("p", { style: styles.status }, "Loading\u2026") : null,
674
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
217
675
  ]
218
676
  ]);
219
677
  }
220
678
  });
221
679
 
222
680
  // src/screens/AttentionScreen.ts
223
- import { useAttention } from "@fonderie/vue-admin";
224
- import { defineComponent as defineComponent2, h as h3 } from "vue";
681
+ import { useAttention, useManifest } from "@fonderie/vue-admin";
682
+ import { defineComponent as defineComponent2, h as h4 } from "vue";
225
683
  var AttentionScreen = defineComponent2({
226
684
  name: "FonderieAttentionScreen",
227
685
  props: { client: { type: Object, required: true } },
228
686
  setup(props) {
229
687
  const { attention, isLoading, error, refresh } = useAttention(props.client);
230
- return () => page(
231
- "Attention",
232
- { isLoading, error },
233
- () => {
234
- const a = attention.value;
235
- if (!a) return null;
236
- if (a.items.length === 0)
237
- return h3(
238
- "p",
239
- { style: styles.ok },
240
- `Nothing needs you. Checked ${new Date(a.generatedAt).toLocaleString()}.`
241
- );
242
- return h3(
243
- "ul",
244
- { style: styles.list },
245
- a.items.map(
246
- (item) => h3("li", { key: `${item.source}:${item.message}`, style: styles.row }, [
247
- h3(
248
- "span",
249
- { style: item.severity === "error" ? styles.bad : styles.advice },
250
- item.severity
251
- ),
252
- " ",
253
- h3("span", { style: styles.mono }, item.source),
254
- ` \u2014 ${item.message}`
255
- ])
256
- )
257
- );
258
- },
259
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
260
- { loadingText: "Running the checks\u2026" }
261
- );
688
+ const { manifest } = useManifest(props.client);
689
+ return () => {
690
+ const a = attention.value;
691
+ const m = manifest.value;
692
+ const errors = a?.items.filter((i) => i.severity === "error") ?? [];
693
+ const advice = a?.items.filter((i) => i.severity !== "error") ?? [];
694
+ const ready = m?.modules.filter((x) => x.readiness.ok).length ?? 0;
695
+ const total = m?.modules.length ?? 0;
696
+ const tiles = h4("div", { style: styles.grid }, [
697
+ stat(
698
+ "Needs action",
699
+ a ? errors.length : "\u2014",
700
+ "errors to fix",
701
+ errors.length ? "bad" : a ? "ok" : void 0
702
+ ),
703
+ stat("Advice", a ? advice.length : "\u2014", "worth a look", advice.length ? "warn" : void 0),
704
+ stat(
705
+ "Modules ready",
706
+ m ? `${ready}/${total}` : "\u2014",
707
+ m ? `admin ${m.admin.version}` : void 0,
708
+ m && ready < total ? "warn" : m ? "ok" : void 0
709
+ ),
710
+ stat("Routes", m ? m.routes.length : "\u2014", m ? m.env : void 0)
711
+ ]);
712
+ return page(
713
+ "Attention",
714
+ // Tiles show while the checks run; only the list waits.
715
+ { isLoading: { value: false }, error },
716
+ () => {
717
+ if (isLoading.value && !a)
718
+ return [tiles, h4("p", { style: styles.status }, "Running the checks\u2026")];
719
+ if (!a) return tiles;
720
+ if (a.items.length === 0)
721
+ return [
722
+ tiles,
723
+ empty(
724
+ "Nothing needs you",
725
+ `Every check passes. Checked ${new Date(a.generatedAt).toLocaleString()}.`
726
+ )
727
+ ];
728
+ return [
729
+ tiles,
730
+ h4(
731
+ "ul",
732
+ { style: styles.list },
733
+ [...errors, ...advice].map(
734
+ (item) => h4(
735
+ "li",
736
+ {
737
+ key: `${item.source}:${item.message}`,
738
+ style: {
739
+ ...styles.row,
740
+ display: "flex",
741
+ gap: "12px",
742
+ alignItems: "flex-start"
743
+ }
744
+ },
745
+ [
746
+ icon("alert", 16, {
747
+ marginTop: "3px",
748
+ color: item.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-warning,#f5a623)"
749
+ }),
750
+ h4("div", { style: { minWidth: 0, flex: 1 } }, [
751
+ h4(
752
+ "div",
753
+ {
754
+ style: {
755
+ display: "flex",
756
+ gap: "8px",
757
+ alignItems: "center",
758
+ flexWrap: "wrap",
759
+ marginBottom: "2px"
760
+ }
761
+ },
762
+ [
763
+ pill(
764
+ item.severity === "error" ? "bad" : "warn",
765
+ item.severity === "error" ? "error" : "advice"
766
+ ),
767
+ h4("span", { style: styles.mono }, item.source)
768
+ ]
769
+ ),
770
+ h4("div", { style: { lineHeight: 1.55 } }, item.message)
771
+ ])
772
+ ]
773
+ )
774
+ )
775
+ )
776
+ ];
777
+ },
778
+ [refreshButton("Refresh", isLoading.value, () => void refresh())],
779
+ {
780
+ lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
781
+ }
782
+ );
783
+ };
262
784
  }
263
785
  });
264
786
 
265
787
  // src/screens/EnvironmentScreen.ts
266
788
  import { useAdminEnvironment } from "@fonderie/vue-admin";
267
- import { defineComponent as defineComponent3, h as h4 } from "vue";
789
+ import { defineComponent as defineComponent3, h as h5 } from "vue";
268
790
  var EnvironmentScreen = defineComponent3({
269
791
  name: "FonderieEnvironmentScreen",
270
792
  props: { client: { type: Object, required: true } },
271
793
  setup(props) {
272
794
  const { report, isLoading, error } = useAdminEnvironment(props.client);
273
- return () => page("Configuration", { isLoading, error }, () => {
795
+ return () => {
274
796
  const r = report.value;
275
- if (!r) return null;
276
- return [
277
- h4("h2", { style: styles.subtitle }, "Readiness"),
278
- h4(
279
- "ul",
280
- { style: styles.list },
281
- r.modules.map(
282
- (m) => h4("li", { key: m.name, style: styles.row }, [
283
- h4("span", { style: styles.mono }, m.name),
284
- " ",
285
- ...m.problems.length === 0 ? [h4("span", { style: styles.ok }, "ok")] : m.problems.map(
286
- (p) => h4(
797
+ const missing = r?.env.filter((e) => !e.set).length ?? 0;
798
+ return page(
799
+ "Environment",
800
+ { isLoading, error },
801
+ () => {
802
+ if (!r) return null;
803
+ return [
804
+ h5("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Variables"),
805
+ r.env.length === 0 ? h5(
806
+ "p",
807
+ { style: styles.muted },
808
+ "No variables declared \u2014 pass `env` to AdminModule."
809
+ ) : h5(
810
+ "div",
811
+ {
812
+ style: {
813
+ ...styles.grid,
814
+ gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
815
+ gap: "8px"
816
+ }
817
+ },
818
+ r.env.map(
819
+ (e) => h5(
287
820
  "div",
288
821
  {
289
- key: p.message,
290
- style: p.severity === "error" ? styles.bad : styles.advice
822
+ key: e.name,
823
+ style: {
824
+ ...styles.card,
825
+ padding: "10px 12px",
826
+ display: "flex",
827
+ alignItems: "center",
828
+ justifyContent: "space-between",
829
+ gap: "8px"
830
+ }
291
831
  },
292
- p.message
832
+ [
833
+ h5("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.name),
834
+ pill(e.set ? "ok" : "bad", e.set ? "set" : "missing")
835
+ ]
293
836
  )
294
837
  )
295
- ])
296
- )
297
- ),
298
- h4("h2", { style: styles.subtitle }, "Environment"),
299
- r.env.length === 0 ? h4("p", { style: styles.muted }, "No variables declared \u2014 pass `env` to AdminModule.") : h4(
300
- "ul",
301
- { style: styles.list },
302
- r.env.map(
303
- (e) => h4("li", { key: e.name, style: styles.row }, [
304
- h4("span", { style: styles.mono }, e.name),
305
- " ",
306
- h4("span", { style: e.set ? styles.ok : styles.bad }, e.set ? "set" : "missing")
307
- ])
308
- )
309
- )
310
- ];
311
- });
838
+ ),
839
+ h5("h2", { style: styles.subtitle }, "Module readiness"),
840
+ h5(
841
+ "ul",
842
+ { style: styles.list },
843
+ r.modules.map(
844
+ (m) => h5("li", { key: m.name, style: styles.row }, [
845
+ h5(
846
+ "div",
847
+ {
848
+ style: {
849
+ display: "flex",
850
+ justifyContent: "space-between",
851
+ gap: "12px",
852
+ alignItems: "center"
853
+ }
854
+ },
855
+ [
856
+ h5("span", { style: { ...styles.mono, fontWeight: 600 } }, m.name),
857
+ pill(
858
+ m.problems.length === 0 ? "ok" : m.problems.some((p) => p.severity === "error") ? "bad" : "warn",
859
+ m.problems.length === 0 ? "ready" : `${m.problems.length} problem${m.problems.length === 1 ? "" : "s"}`
860
+ )
861
+ ]
862
+ ),
863
+ ...m.problems.map(
864
+ (p) => h5(
865
+ "div",
866
+ {
867
+ key: p.message,
868
+ style: {
869
+ marginTop: "6px",
870
+ fontSize: "13px",
871
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
872
+ }
873
+ },
874
+ p.message
875
+ )
876
+ )
877
+ ])
878
+ )
879
+ )
880
+ ];
881
+ },
882
+ [
883
+ r && r.env.length > 0 ? pill(missing ? "bad" : "ok", missing ? `${missing} missing` : "all set") : null
884
+ ],
885
+ {
886
+ lead: "Whether each module is configured and each variable the app reads is set. Values are never shown."
887
+ }
888
+ );
889
+ };
312
890
  }
313
891
  });
314
892
 
315
893
  // src/screens/DoctorScreen.ts
316
894
  import { useDoctor } from "@fonderie/vue-admin";
317
- import { defineComponent as defineComponent4, h as h5 } from "vue";
895
+ import { defineComponent as defineComponent4, h as h6 } from "vue";
318
896
  var DoctorScreen = defineComponent4({
319
897
  name: "FonderieDoctorScreen",
320
898
  props: { client: { type: Object, required: true } },
@@ -322,123 +900,193 @@ var DoctorScreen = defineComponent4({
322
900
  const { report, isLoading, error, refresh } = useDoctor(props.client);
323
901
  return () => page(
324
902
  "Doctor",
325
- { isLoading, error },
903
+ { isLoading: { value: isLoading.value && !report.value }, error },
326
904
  () => {
327
905
  const r = report.value;
328
906
  if (!r) return null;
329
- return h5(
330
- "ul",
331
- { style: styles.list },
907
+ return table(
908
+ [
909
+ { label: "Result", style: { width: "110px" } },
910
+ "Check",
911
+ { label: "Time", style: { textAlign: "right", width: "90px" } }
912
+ ],
332
913
  r.checks.map(
333
- (c) => h5("li", { key: c.name, style: styles.row }, [
334
- h5(
335
- "span",
336
- { style: c.skipped ? styles.muted : c.ok ? styles.ok : styles.bad },
337
- c.skipped ? "skipped" : c.ok ? "ok" : "failed"
914
+ (c) => h6("tr", { key: c.name }, [
915
+ td(
916
+ pill(
917
+ c.skipped ? "neutral" : c.ok ? c.findings.length ? "warn" : "ok" : "bad",
918
+ c.skipped ? "skipped" : c.ok ? c.findings.length ? "advice" : "ok" : "failed"
919
+ )
338
920
  ),
339
- " ",
340
- h5("span", { style: styles.mono }, c.name),
341
- " ",
342
- h5("span", { style: styles.muted }, `${c.module} \xB7 ${c.durationMs} ms`),
343
- c.skipped ? h5("div", { style: styles.muted }, c.skipped) : null,
344
- ...c.findings.map(
345
- (f) => h5("div", { key: f, style: c.ok ? styles.advice : styles.bad }, f)
346
- )
921
+ td([
922
+ h6("div", [
923
+ h6("span", { style: { ...styles.mono, fontWeight: 600 } }, c.name),
924
+ " ",
925
+ h6("span", { style: styles.muted }, c.module)
926
+ ]),
927
+ c.skipped ? h6("div", { style: { ...styles.muted, marginTop: "4px" } }, c.skipped) : null,
928
+ ...c.findings.map(
929
+ (f) => h6(
930
+ "div",
931
+ {
932
+ key: f,
933
+ style: {
934
+ marginTop: "4px",
935
+ fontSize: "13px",
936
+ lineHeight: 1.55,
937
+ color: c.ok ? "var(--fonderie-text,#171717)" : "var(--fonderie-danger,#e00)"
938
+ }
939
+ },
940
+ f
941
+ )
942
+ )
943
+ ]),
944
+ td(`${c.durationMs} ms`, {
945
+ ...styles.muted,
946
+ textAlign: "right",
947
+ whiteSpace: "nowrap"
948
+ })
347
949
  ])
348
950
  )
349
951
  );
350
952
  },
351
953
  [
352
- refreshButton("Run again", isLoading.value, () => void refresh()),
353
- report.value && !isLoading.value ? h5(
354
- "span",
355
- { style: report.value.ok ? styles.ok : styles.bad },
954
+ report.value && !isLoading.value ? pill(
955
+ report.value.ok ? "ok" : "bad",
356
956
  report.value.ok ? "all checks pass" : "a check failed"
357
- ) : h5("span")
957
+ ) : null,
958
+ refreshButton("Run again", isLoading.value, () => void refresh())
358
959
  ],
359
- { loadingText: "Running the checks\u2026" }
960
+ {
961
+ loadingText: "Running the checks\u2026",
962
+ lead: "Every reconciliation check across the installed modules, run on demand."
963
+ }
360
964
  );
361
965
  }
362
966
  });
363
967
 
364
968
  // src/screens/ModulesScreen.ts
365
- import { useManifest } from "@fonderie/vue-admin";
366
- import { defineComponent as defineComponent5, h as h6 } from "vue";
969
+ import { useManifest as useManifest2 } from "@fonderie/vue-admin";
970
+ import { defineComponent as defineComponent5, h as h7 } from "vue";
367
971
  var ModulesScreen = defineComponent5({
368
972
  name: "FonderieModulesScreen",
369
973
  props: { client: { type: Object, required: true } },
370
974
  setup(props) {
371
- const { manifest, isLoading, error } = useManifest(props.client);
372
- return () => page("Modules", { isLoading, error }, () => {
975
+ const { manifest, isLoading, error } = useManifest2(props.client);
976
+ return () => {
373
977
  const m = manifest.value;
374
- if (!m) return null;
375
- return [
376
- h6(
377
- "p",
378
- { style: styles.muted },
379
- `${m.env} \xB7 admin ${m.admin.version} \xB7 admin log ${m.admin.log ? "on" : "off"} \xB7 ${m.routes.length} routes`
380
- ),
381
- table(
382
- ["Module", "Version", "Readiness", "Describes admin"],
383
- m.modules.map(
384
- (mod) => h6("tr", { key: mod.name }, [
385
- td(mod.name, styles.mono),
386
- td(mod.version ?? h6("span", { style: styles.muted }, "not reported")),
387
- td([
388
- h6(
389
- "span",
390
- { style: mod.readiness.ok ? styles.ok : styles.bad },
391
- mod.readiness.ok ? "ok" : "error"
392
- ),
393
- ...mod.readiness.problems.map(
394
- (p) => h6(
395
- "div",
396
- {
397
- key: p.message,
398
- style: p.severity === "error" ? styles.bad : styles.advice
399
- },
400
- p.message
978
+ return page(
979
+ "Modules",
980
+ { isLoading, error },
981
+ () => {
982
+ if (!m) return null;
983
+ return table(
984
+ ["Module", "Version", "Readiness", "Admin"],
985
+ m.modules.map(
986
+ (mod) => h7("tr", { key: mod.name }, [
987
+ td(mod.name, { ...styles.mono, fontWeight: 600 }),
988
+ td(mod.version ?? h7("span", { style: styles.muted }, "not reported"), styles.mono),
989
+ td([
990
+ pill(mod.readiness.ok ? "ok" : "bad", mod.readiness.ok ? "ready" : "error"),
991
+ ...mod.readiness.problems.map(
992
+ (p) => h7(
993
+ "div",
994
+ {
995
+ key: p.message,
996
+ style: {
997
+ marginTop: "6px",
998
+ fontSize: "13px",
999
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
1000
+ }
1001
+ },
1002
+ p.message
1003
+ )
401
1004
  )
1005
+ ]),
1006
+ td(
1007
+ mod.describesAdmin ? h7("span", { style: styles.badge }, "describes admin") : h7("span", { style: styles.muted }, "\u2014")
402
1008
  )
403
- ]),
404
- td(mod.describesAdmin ? "yes" : h6("span", { style: styles.muted }, "no"))
405
- ])
406
- )
407
- )
408
- ];
409
- });
1009
+ ])
1010
+ )
1011
+ );
1012
+ },
1013
+ [],
1014
+ {
1015
+ lead: m ? `${m.env} \xB7 admin ${m.admin.version} \xB7 admin log ${m.admin.log ? "on" : "off"} \xB7 ${m.routes.length} routes` : "Every installed module, its version and whether it is ready."
1016
+ }
1017
+ );
1018
+ };
410
1019
  }
411
1020
  });
412
1021
 
413
1022
  // src/screens/RoutesScreen.ts
414
1023
  import { useAdminRoutes } from "@fonderie/vue-admin";
415
- import { defineComponent as defineComponent6, h as h7 } from "vue";
1024
+ import { defineComponent as defineComponent6, h as h8, ref } from "vue";
416
1025
  var RoutesScreen = defineComponent6({
417
1026
  name: "FonderieRoutesScreen",
418
1027
  props: { client: { type: Object, required: true } },
419
1028
  setup(props) {
420
1029
  const { report, isLoading, error } = useAdminRoutes(props.client);
421
- return () => page(
422
- "Routes",
423
- { isLoading, error },
424
- () => table(
425
- ["Method", "Path", "Guard", "Module"],
426
- (report.value?.routes ?? []).map(
427
- (r) => h7("tr", { key: `${r.method} ${r.path}` }, [
428
- td(r.method, styles.mono),
429
- td(r.path, styles.mono),
430
- td(h7("span", { style: styles.badge }, r.guard)),
431
- td(r.module ?? "application", styles.muted)
432
- ])
433
- )
434
- )
435
- );
1030
+ const q = ref("");
1031
+ return () => {
1032
+ const needle = q.value.trim().toLowerCase();
1033
+ const routes = (report.value?.routes ?? []).filter(
1034
+ (r) => !needle || `${r.method} ${r.path} ${r.module ?? "application"} ${r.guard}`.toLowerCase().includes(needle)
1035
+ );
1036
+ return page(
1037
+ "Routes",
1038
+ { isLoading, error },
1039
+ () => table(
1040
+ [{ label: "Method", style: { width: "80px" } }, "Path", "Guard", "Module"],
1041
+ [
1042
+ ...routes.map(
1043
+ (r) => h8("tr", { key: `${r.method} ${r.path}` }, [
1044
+ td(methodChip(r.method)),
1045
+ td(r.path, { ...styles.mono, wordBreak: "break-all" }),
1046
+ td(
1047
+ pill(
1048
+ r.guard === "admin" ? "ok" : r.guard === "probe" ? "neutral" : "info",
1049
+ r.guard,
1050
+ false
1051
+ )
1052
+ ),
1053
+ td(r.module ?? "application", styles.muted)
1054
+ ])
1055
+ ),
1056
+ ...routes.length === 0 ? [
1057
+ h8("tr", [
1058
+ h8(
1059
+ "td",
1060
+ { style: { ...styles.td, ...styles.muted }, colspan: 4 },
1061
+ `No route matches \u201C${q.value}\u201D.`
1062
+ )
1063
+ ])
1064
+ ] : []
1065
+ ]
1066
+ ),
1067
+ [
1068
+ h8("input", {
1069
+ type: "search",
1070
+ value: q.value,
1071
+ placeholder: "Filter by path, method, module\u2026",
1072
+ style: { ...styles.input, width: "280px" },
1073
+ "aria-label": "Filter routes",
1074
+ onInput: (e) => {
1075
+ q.value = e.target.value;
1076
+ }
1077
+ })
1078
+ ],
1079
+ {
1080
+ lead: report.value ? `${report.value.routes.length} routes exposed by this deployment, with the guard in front of each.` : "Every exposed route, with its guard."
1081
+ }
1082
+ );
1083
+ };
436
1084
  }
437
1085
  });
438
1086
 
439
1087
  // src/screens/MigrationsScreen.ts
440
1088
  import { useAdminMigrations } from "@fonderie/vue-admin";
441
- import { defineComponent as defineComponent7, h as h8 } from "vue";
1089
+ import { defineComponent as defineComponent7, h as h9 } from "vue";
442
1090
  function why(m, everApplied) {
443
1091
  if (m.pending.length === 0) return null;
444
1092
  if (m.blockedBy) return `Apply "${m.blockedBy}" first \u2014 it runs before this one and is behind.`;
@@ -454,31 +1102,31 @@ var MigrationsScreen = defineComponent7({
454
1102
  const section = (m, everApplied) => {
455
1103
  const blocked = why(m, everApplied);
456
1104
  const files = m.pending.map((p) => p.file);
457
- return h8("section", { style: { marginBottom: "24px" } }, [
458
- h8("h2", { style: styles.subtitle }, `${m.name} \u2014 ${m.pending.length} pending`),
459
- h8(
1105
+ return h9("section", { style: { ...styles.card, marginBottom: "16px" } }, [
1106
+ h9("h2", { style: { ...styles.subtitle, marginTop: 0 } }, [
1107
+ h9("span", { style: styles.mono }, m.name),
1108
+ pill("warn", `${m.pending.length} pending`)
1109
+ ]),
1110
+ h9(
460
1111
  "ul",
1112
+ { style: { margin: "0 0 12px", paddingLeft: "18px", lineHeight: 1.9 } },
461
1113
  m.pending.map(
462
- (p) => h8("li", [
463
- h8("code", p.file),
1114
+ (p) => h9("li", [
1115
+ h9("code", { style: styles.code }, p.file),
464
1116
  " ",
465
- h8(
466
- "span",
467
- { style: p.impact === "destructive" ? styles.bad : styles.advice },
468
- p.impact
469
- ),
470
- p.destructive.length > 0 ? h8(
1117
+ p.impact === "destructive" ? pill("bad", "destructive") : pill("neutral", "additive"),
1118
+ p.destructive.length > 0 ? h9(
471
1119
  "ul",
472
- p.destructive.map((s) => h8("li", [h8("code", s.slice(0, 120))]))
1120
+ p.destructive.map((s) => h9("li", [h9("code", s.slice(0, 120))]))
473
1121
  ) : null
474
1122
  ])
475
1123
  )
476
1124
  ),
477
- blocked ? h8("p", { style: styles.advice }, blocked) : h8(
1125
+ blocked ? h9("p", { style: { ...styles.notice, marginBottom: 0 } }, blocked) : h9(
478
1126
  "button",
479
1127
  {
480
1128
  type: "button",
481
- style: styles.button,
1129
+ style: styles.buttonPrimary,
482
1130
  disabled: isLoading.value || !m.appliable,
483
1131
  onClick: () => {
484
1132
  if (window.confirm(
@@ -488,81 +1136,399 @@ var MigrationsScreen = defineComponent7({
488
1136
  });
489
1137
  }
490
1138
  },
491
- `Apply ${m.pending.length} migration${m.pending.length === 1 ? "" : "s"}`
492
- )
1139
+ `Apply ${m.pending.length} migration${m.pending.length === 1 ? "" : "s"}`
1140
+ )
1141
+ ]);
1142
+ };
1143
+ return () => page(
1144
+ "Migrations",
1145
+ { isLoading, error },
1146
+ () => {
1147
+ const r = report.value;
1148
+ if (!r) return null;
1149
+ const behind = r.modules.filter((m) => m.pending.length > 0);
1150
+ if (behind.length === 0)
1151
+ return empty("Every module is up to date", "No pending migrations.", "migrations");
1152
+ return [
1153
+ r.everApplied ? null : h9(
1154
+ "p",
1155
+ { style: styles.notice },
1156
+ "This database has never been migrated \u2014 treating it as a first install, so nothing is held back."
1157
+ ),
1158
+ ...behind.map((m) => section(m, r.everApplied))
1159
+ ].filter(Boolean);
1160
+ },
1161
+ [refreshButton("Refresh", isLoading.value, () => void refresh())],
1162
+ {
1163
+ lead: "Schema changes each module ships, and whether this database has them.",
1164
+ errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
1165
+ }
1166
+ );
1167
+ }
1168
+ });
1169
+
1170
+ // src/screens/OperatorsScreen.ts
1171
+ import { useAdminOperators } from "@fonderie/vue-admin";
1172
+ import { defineComponent as defineComponent8, h as h10, ref as ref2 } from "vue";
1173
+ var LEVELS = [
1174
+ { label: "Read only", scopes: ["read"], hint: "look around, change nothing" },
1175
+ { label: "Editor", scopes: ["read", "write"], hint: "change config, templates, users" },
1176
+ {
1177
+ label: "Owner",
1178
+ scopes: ["read", "write", "secrets"],
1179
+ hint: "secrets, tokens and operators too"
1180
+ }
1181
+ ];
1182
+ var levelOf = (scopes) => scopes.includes("secrets") ? "Owner" : scopes.includes("write") ? "Editor" : "Read only";
1183
+ var scopesFor = (label) => LEVELS.find((l) => l.label === label)?.scopes ?? ["read"];
1184
+ var small = { height: "28px" };
1185
+ var OperatorsScreen = defineComponent8({
1186
+ name: "FonderieOperatorsScreen",
1187
+ props: {
1188
+ client: { type: Object, required: true },
1189
+ // The signed-in operator's email: their own row offers no disable or
1190
+ // recovery (the server refuses both for yourself anyway).
1191
+ me: { type: String, default: void 0 }
1192
+ },
1193
+ setup(props) {
1194
+ const { report, isLoading, error, invite, recover, update, revokeLink } = useAdminOperators(
1195
+ props.client
1196
+ );
1197
+ const email = ref2("");
1198
+ const level = ref2("Editor");
1199
+ const minted = ref2(null);
1200
+ const copied = ref2(false);
1201
+ const status = (o) => o.disabledAt ? pill("neutral", "disabled") : o.locked ? pill("warn", "locked") : !o.enrolled ? pill("warn", "setting up") : pill("ok", "active");
1202
+ const mintedNotice = (link) => {
1203
+ const url = `${window.location.origin}${link.url}`;
1204
+ return h10(
1205
+ "div",
1206
+ {
1207
+ style: {
1208
+ ...styles.notice,
1209
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1210
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1211
+ }
1212
+ },
1213
+ [
1214
+ h10("strong", `Send this link to ${link.email}. It is shown once.`),
1215
+ ` It works once and expires ${new Date(link.expiresAt).toLocaleString()}.`,
1216
+ h10(
1217
+ "div",
1218
+ { style: { display: "flex", gap: "8px", marginTop: "8px", alignItems: "center" } },
1219
+ [
1220
+ h10(
1221
+ "code",
1222
+ { style: { ...styles.code, flex: 1, padding: "7px 10px", wordBreak: "break-all" } },
1223
+ url
1224
+ ),
1225
+ h10(
1226
+ "button",
1227
+ {
1228
+ type: "button",
1229
+ style: styles.button,
1230
+ onClick: () => {
1231
+ void navigator.clipboard?.writeText(url);
1232
+ copied.value = true;
1233
+ }
1234
+ },
1235
+ copied.value ? "Copied" : "Copy"
1236
+ ),
1237
+ h10(
1238
+ "button",
1239
+ {
1240
+ type: "button",
1241
+ style: styles.buttonGhost,
1242
+ "aria-label": "Dismiss",
1243
+ onClick: () => {
1244
+ minted.value = null;
1245
+ copied.value = false;
1246
+ }
1247
+ },
1248
+ [icon("close", 14)]
1249
+ )
1250
+ ]
1251
+ )
1252
+ ]
1253
+ );
1254
+ };
1255
+ const setMinted = (l) => {
1256
+ copied.value = false;
1257
+ minted.value = l;
1258
+ };
1259
+ return () => {
1260
+ const r = report.value;
1261
+ const e = error.value;
1262
+ return h10("div", { style: styles.container }, [
1263
+ pageHeader(
1264
+ "Operators",
1265
+ "The people who can sign in to this console. Each one uses a password and an authenticator app; there is no sign-up."
1266
+ ),
1267
+ e ? h10(
1268
+ "p",
1269
+ { style: styles.error, role: "alert" },
1270
+ e.status === 403 && e.reason === "FORBIDDEN" ? "Managing operators needs the Owner level." : e.explanation
1271
+ ) : null,
1272
+ minted.value ? mintedNotice(minted.value) : null,
1273
+ h10(
1274
+ "form",
1275
+ {
1276
+ style: {
1277
+ ...styles.card,
1278
+ display: "flex",
1279
+ gap: "8px",
1280
+ alignItems: "center",
1281
+ flexWrap: "wrap",
1282
+ marginBottom: "20px"
1283
+ },
1284
+ onSubmit: (ev) => {
1285
+ ev.preventDefault();
1286
+ void invite({ email: email.value.trim(), scopes: scopesFor(level.value) }).then((l) => {
1287
+ setMinted(l);
1288
+ email.value = "";
1289
+ }).catch(() => {
1290
+ });
1291
+ }
1292
+ },
1293
+ [
1294
+ icon("users", 16),
1295
+ h10("strong", { style: { fontSize: "13.5px", marginRight: "4px" } }, "Invite"),
1296
+ h10("input", {
1297
+ type: "email",
1298
+ required: true,
1299
+ value: email.value,
1300
+ onInput: (ev) => email.value = ev.target.value,
1301
+ placeholder: "teammate@company.com",
1302
+ style: { ...styles.input, flex: 1, minWidth: "220px" },
1303
+ "aria-label": "Email to invite"
1304
+ }),
1305
+ h10(
1306
+ "select",
1307
+ {
1308
+ value: level.value,
1309
+ onChange: (ev) => level.value = ev.target.value,
1310
+ style: styles.input,
1311
+ "aria-label": "Access level"
1312
+ },
1313
+ LEVELS.map(
1314
+ (l) => h10("option", { key: l.label, value: l.label }, `${l.label} \u2014 ${l.hint}`)
1315
+ )
1316
+ ),
1317
+ h10(
1318
+ "button",
1319
+ { type: "submit", style: styles.buttonPrimary, disabled: !email.value.trim() },
1320
+ [icon("plus", 14), "Create invite link"]
1321
+ )
1322
+ ]
1323
+ ),
1324
+ isLoading.value && !r ? h10("p", { style: styles.status }, "Loading\u2026") : r && r.operators.length === 0 ? empty("No operators yet", void 0, "users") : r ? table(
1325
+ ["Operator", "Access", "Status", "Last sign-in", ""],
1326
+ r.operators.map((o) => {
1327
+ const mine = o.email === props.me;
1328
+ return h10("tr", { key: o.id }, [
1329
+ td([
1330
+ h10("div", { style: { fontWeight: 600 } }, o.name || o.email),
1331
+ o.name ? h10("div", { style: styles.muted }, o.email) : null
1332
+ ]),
1333
+ td(
1334
+ h10(
1335
+ "select",
1336
+ {
1337
+ value: levelOf(o.scopes),
1338
+ disabled: mine,
1339
+ onChange: (ev) => void update(o.id, {
1340
+ scopes: scopesFor(ev.target.value)
1341
+ }).catch(() => {
1342
+ }),
1343
+ style: { ...styles.input, ...small, fontSize: "12.5px" },
1344
+ "aria-label": `Access level for ${o.email}`
1345
+ },
1346
+ LEVELS.map((l) => h10("option", { key: l.label, value: l.label }, l.label))
1347
+ )
1348
+ ),
1349
+ td([
1350
+ status(o),
1351
+ o.enrolled && o.backupCodesLeft <= 2 && !o.disabledAt ? h10(
1352
+ "div",
1353
+ { style: { ...styles.muted, marginTop: "4px" } },
1354
+ `${o.backupCodesLeft} backup code(s) left`
1355
+ ) : null
1356
+ ]),
1357
+ td(o.lastLoginAt ? new Date(o.lastLoginAt).toLocaleString() : "never", {
1358
+ ...styles.muted,
1359
+ whiteSpace: "nowrap"
1360
+ }),
1361
+ td(
1362
+ mine ? h10("span", { style: styles.muted }, "you") : [
1363
+ h10(
1364
+ "button",
1365
+ {
1366
+ type: "button",
1367
+ style: { ...styles.button, ...small, marginRight: "6px" },
1368
+ onClick: () => {
1369
+ if (window.confirm(
1370
+ `Create a recovery link for ${o.email}? It signs them out everywhere; the link sets a new password and a new authenticator.`
1371
+ ))
1372
+ void recover(o.id).then(setMinted).catch(() => {
1373
+ });
1374
+ }
1375
+ },
1376
+ "Recovery link"
1377
+ ),
1378
+ h10(
1379
+ "button",
1380
+ {
1381
+ type: "button",
1382
+ style: {
1383
+ ...o.disabledAt ? styles.button : styles.buttonDanger,
1384
+ ...small
1385
+ },
1386
+ onClick: () => {
1387
+ if (o.disabledAt || window.confirm(
1388
+ `Disable ${o.email}? They are signed out immediately.`
1389
+ ))
1390
+ void update(o.id, { disabled: !o.disabledAt }).catch(
1391
+ () => {
1392
+ }
1393
+ );
1394
+ }
1395
+ },
1396
+ o.disabledAt ? "Enable" : "Disable"
1397
+ )
1398
+ ],
1399
+ { textAlign: "right", whiteSpace: "nowrap" }
1400
+ )
1401
+ ]);
1402
+ })
1403
+ ) : null,
1404
+ r && r.links.length > 0 ? [
1405
+ h10("h2", { style: styles.subtitle }, "Pending links"),
1406
+ h10(
1407
+ "ul",
1408
+ { style: styles.list },
1409
+ r.links.map(
1410
+ (l) => h10(
1411
+ "li",
1412
+ {
1413
+ key: l.id,
1414
+ style: { ...styles.row, display: "flex", gap: "12px", alignItems: "center" }
1415
+ },
1416
+ [
1417
+ pill(l.kind === "invite" ? "info" : "warn", l.kind, false),
1418
+ h10("span", { style: { flex: 1 } }, [
1419
+ l.email,
1420
+ l.kind === "invite" ? h10("span", { style: styles.muted }, ` \xB7 ${levelOf(l.scopes)}`) : null
1421
+ ]),
1422
+ h10(
1423
+ "span",
1424
+ { style: styles.muted },
1425
+ `expires ${new Date(l.expiresAt).toLocaleString()}`
1426
+ ),
1427
+ h10(
1428
+ "button",
1429
+ {
1430
+ type: "button",
1431
+ style: { ...styles.buttonDanger, ...small },
1432
+ onClick: () => void revokeLink(l.id).catch(() => {
1433
+ })
1434
+ },
1435
+ "Revoke"
1436
+ )
1437
+ ]
1438
+ )
1439
+ )
1440
+ )
1441
+ ] : null
493
1442
  ]);
494
1443
  };
495
- return () => page(
496
- "Migrations",
497
- { isLoading, error },
498
- () => {
499
- const r = report.value;
500
- if (!r) return null;
501
- const behind = r.modules.filter((m) => m.pending.length > 0);
502
- if (behind.length === 0) return h8("p", { style: styles.ok }, "Every module is up to date.");
503
- return [
504
- r.everApplied ? null : h8(
505
- "p",
506
- { style: styles.advice },
507
- "This database has never been migrated \u2014 treating it as a first install, so nothing is held back."
508
- ),
509
- ...behind.map((m) => section(m, r.everApplied))
510
- ].filter(Boolean);
511
- },
512
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
513
- {
514
- errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
515
- }
516
- );
517
1444
  }
518
1445
  });
519
1446
 
520
1447
  // src/screens/TokensScreen.ts
521
1448
  import { useAdminTokens } from "@fonderie/vue-admin";
522
- import { defineComponent as defineComponent8, h as h9, ref } from "vue";
1449
+ import { defineComponent as defineComponent9, h as h11, ref as ref3 } from "vue";
523
1450
  var ALL = ["read", "write", "secrets"];
524
- var TokensScreen = defineComponent8({
1451
+ var TokensScreen = defineComponent9({
525
1452
  name: "FonderieTokensScreen",
526
1453
  props: { client: { type: Object, required: true } },
527
1454
  setup(props) {
528
1455
  const { report, isLoading, error, issue, revoke } = useAdminTokens(props.client);
529
- const name = ref("");
530
- const scopes = ref(["read"]);
531
- const days = ref("");
532
- const minted = ref(null);
1456
+ const name = ref3("");
1457
+ const scopes = ref3(["read"]);
1458
+ const days = ref3("");
1459
+ const minted = ref3(null);
533
1460
  const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
534
1461
  return () => {
535
1462
  const r = report.value;
536
- return h9("div", { style: styles.container }, [
537
- h9("h1", { style: styles.title }, "Access"),
538
- error.value ? h9(
1463
+ return h11("div", { style: styles.container }, [
1464
+ pageHeader(
1465
+ "Tokens",
1466
+ "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."
1467
+ ),
1468
+ error.value ? h11(
539
1469
  "p",
540
1470
  { style: styles.error, role: "alert" },
541
1471
  error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
542
1472
  ) : null,
543
- h9("h2", { style: styles.subtitle }, "Root token"),
544
- r ? h9("p", [
545
- h9(
546
- "span",
547
- { style: r.admin.ok ? styles.ok : styles.bad },
548
- r.admin.ok ? "strong" : "weak"
549
- ),
550
- ...r.admin.problems.map(
551
- (p) => h9("div", { key: p.message, style: styles.bad }, p.message)
552
- )
553
- ]) : null,
554
- h9("h2", { style: styles.subtitle }, "Issued tokens"),
555
- r?.issued === null ? h9(
1473
+ h11("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
1474
+ r ? h11(
1475
+ "div",
1476
+ { style: { ...styles.card, display: "flex", gap: "12px", alignItems: "flex-start" } },
1477
+ [
1478
+ icon("lock", 18, { marginTop: "2px" }),
1479
+ h11("div", [
1480
+ pill(r.admin.ok ? "ok" : "bad", r.admin.ok ? "strong" : "weak"),
1481
+ ...r.admin.problems.map(
1482
+ (p) => h11(
1483
+ "div",
1484
+ {
1485
+ key: p.message,
1486
+ style: {
1487
+ marginTop: "6px",
1488
+ color: "var(--fonderie-danger,#e00)",
1489
+ fontSize: "13px"
1490
+ }
1491
+ },
1492
+ p.message
1493
+ )
1494
+ )
1495
+ ])
1496
+ ]
1497
+ ) : null,
1498
+ h11("h2", { style: styles.subtitle }, "Issued tokens"),
1499
+ r?.issued === null ? h11(
556
1500
  "p",
557
1501
  { style: styles.muted },
558
1502
  "Issuing is off \u2014 give AdminModule a store and run its migrations."
559
1503
  ) : [
560
- minted.value ? h9("p", { style: styles.ok }, [
561
- "Copy this now \u2014 it is never shown again. ",
562
- h9("span", { style: styles.mono }, minted.value.token),
563
- ` (${minted.value.name})`
564
- ]) : null,
565
- h9(
1504
+ minted.value ? h11(
1505
+ "div",
1506
+ {
1507
+ style: {
1508
+ ...styles.notice,
1509
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1510
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1511
+ }
1512
+ },
1513
+ [
1514
+ h11("strong", "Copy this now \u2014 it is never shown again."),
1515
+ ` (${minted.value.name})`,
1516
+ h11(
1517
+ "div",
1518
+ {
1519
+ style: {
1520
+ ...styles.code,
1521
+ display: "block",
1522
+ marginTop: "8px",
1523
+ padding: "8px 10px",
1524
+ wordBreak: "break-all"
1525
+ }
1526
+ },
1527
+ minted.value.token
1528
+ )
1529
+ ]
1530
+ ) : null,
1531
+ h11(
566
1532
  "form",
567
1533
  {
568
1534
  style: styles.toolbar,
@@ -582,49 +1548,60 @@ var TokensScreen = defineComponent8({
582
1548
  }
583
1549
  },
584
1550
  [
585
- h9("input", {
1551
+ h11("input", {
586
1552
  value: name.value,
587
1553
  onInput: (e) => name.value = e.target.value,
588
1554
  placeholder: "name",
589
- style: { ...styles.button, cursor: "text" },
1555
+ style: styles.input,
590
1556
  "aria-label": "Name"
591
1557
  }),
592
1558
  ...ALL.map(
593
- (s) => h9("label", { key: s, style: { ...styles.badge, cursor: "pointer" } }, [
594
- h9("input", {
595
- type: "checkbox",
596
- checked: scopes.value.includes(s),
597
- onChange: () => toggle(s)
598
- }),
599
- ` ${s}`
600
- ])
1559
+ (s) => h11(
1560
+ "label",
1561
+ {
1562
+ key: s,
1563
+ style: {
1564
+ ...styles.badge,
1565
+ cursor: "pointer",
1566
+ height: "32px",
1567
+ boxSizing: "border-box",
1568
+ padding: "0 10px",
1569
+ fontSize: "13px"
1570
+ }
1571
+ },
1572
+ [
1573
+ h11("input", {
1574
+ type: "checkbox",
1575
+ checked: scopes.value.includes(s),
1576
+ onChange: () => toggle(s)
1577
+ }),
1578
+ ` ${s}`
1579
+ ]
1580
+ )
601
1581
  ),
602
- h9("input", {
1582
+ h11("input", {
603
1583
  value: days.value,
604
1584
  onInput: (e) => days.value = e.target.value,
605
1585
  placeholder: "days (optional)",
606
- style: { ...styles.button, cursor: "text", width: "140px" },
1586
+ style: { ...styles.input, width: "140px" },
607
1587
  "aria-label": "Days"
608
1588
  }),
609
- h9(
1589
+ h11(
610
1590
  "button",
611
1591
  {
612
1592
  type: "submit",
613
- style: styles.button,
1593
+ style: styles.buttonPrimary,
614
1594
  disabled: !name.value.trim() || scopes.value.length === 0 || isLoading.value
615
1595
  },
616
- "Issue"
1596
+ [icon("plus", 14), "Issue token"]
617
1597
  )
618
1598
  ]
619
1599
  ),
620
1600
  r?.issued && r.issued.length > 0 ? table(
621
1601
  ["Name", "Scopes", "Created", "Expires", "Last used", ""],
622
1602
  r.issued.map(
623
- (t2) => h9("tr", { key: t2.id }, [
624
- td([
625
- t2.name,
626
- t2.revokedAt ? h9("span", { style: styles.badge }, " revoked") : ""
627
- ]),
1603
+ (t2) => h11("tr", { key: t2.id }, [
1604
+ td([t2.name, t2.revokedAt ? [" ", pill("neutral", "revoked")] : ""]),
628
1605
  td(t2.scopes.join(", "), styles.mono),
629
1606
  td(
630
1607
  `${new Date(t2.createdAt).toLocaleDateString()} \xB7 ${t2.createdBy}`,
@@ -639,11 +1616,11 @@ var TokensScreen = defineComponent8({
639
1616
  styles.muted
640
1617
  ),
641
1618
  td(
642
- t2.revokedAt ? "" : h9(
1619
+ t2.revokedAt ? "" : h11(
643
1620
  "button",
644
1621
  {
645
1622
  type: "button",
646
- style: styles.button,
1623
+ style: styles.buttonDanger,
647
1624
  onClick: () => {
648
1625
  if (window.confirm(
649
1626
  `Revoke "${t2.name}"? Anything using it stops working immediately.`
@@ -657,21 +1634,25 @@ var TokensScreen = defineComponent8({
657
1634
  )
658
1635
  ])
659
1636
  )
660
- ) : h9("p", { style: styles.muted }, "None issued.")
1637
+ ) : empty(
1638
+ "No scoped tokens yet",
1639
+ "Issue a read-only token for a dashboard or a teammate instead of sharing the root.",
1640
+ "tokens"
1641
+ )
661
1642
  ],
662
- h9("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
663
- r && r.legacy.length === 0 ? h9(
1643
+ h11("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
1644
+ r && r.legacy.length === 0 ? h11(
664
1645
  "p",
665
1646
  { style: styles.muted },
666
1647
  "None \u2014 every brick's admin surface goes through this token."
667
- ) : h9(
1648
+ ) : h11(
668
1649
  "ul",
669
1650
  { style: styles.list },
670
1651
  (r?.legacy ?? []).map(
671
- (l) => h9("li", { key: l.module, style: styles.row }, [
672
- h9("span", { style: styles.mono }, l.module),
1652
+ (l) => h11("li", { key: l.module, style: styles.row }, [
1653
+ h11("span", { style: styles.mono }, l.module),
673
1654
  " ",
674
- h9(
1655
+ h11(
675
1656
  "span",
676
1657
  { style: styles.muted },
677
1658
  "still registers its standalone routes with its own token (deprecated)"
@@ -692,17 +1673,17 @@ import {
692
1673
  useAdminUserSessions,
693
1674
  useAdminUsers
694
1675
  } from "@fonderie/vue-admin";
695
- import { computed, defineComponent as defineComponent9, h as h10, ref as ref2 } from "vue";
696
- var UsersScreen = defineComponent9({
1676
+ import { computed, defineComponent as defineComponent10, h as h12, ref as ref4 } from "vue";
1677
+ var UsersScreen = defineComponent10({
697
1678
  name: "FonderieUsersScreen",
698
1679
  props: {
699
1680
  client: { type: Object, required: true },
700
1681
  pageSize: { type: Number, default: 50 }
701
1682
  },
702
1683
  setup(props) {
703
- const input = ref2("");
704
- const email = ref2("");
705
- const selectedId = ref2("");
1684
+ const input = ref4("");
1685
+ const email = ref4("");
1686
+ const selectedId = ref4("");
706
1687
  const list = useAdminUsers(props.client, { limit: props.pageSize });
707
1688
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
708
1689
  props.client,
@@ -712,8 +1693,8 @@ var UsersScreen = defineComponent9({
712
1693
  const sessions = useAdminUserSessions(props.client, userId);
713
1694
  const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
714
1695
  const showList = computed(() => !email.value && !selectedId.value);
715
- const yesNo = (v) => h10("span", { style: v ? styles.ok : styles.muted }, v ? "yes" : "no");
716
- const row = (k, v) => h10("tr", [td(k), td(v)]);
1696
+ const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
1697
+ const row = (k, v) => h12("tr", [td(k), td(v)]);
717
1698
  const clear = () => {
718
1699
  email.value = "";
719
1700
  selectedId.value = "";
@@ -721,9 +1702,12 @@ var UsersScreen = defineComponent9({
721
1702
  };
722
1703
  return () => {
723
1704
  const u = user.value;
724
- return h10("div", { style: styles.container }, [
725
- h10("h1", { style: styles.title }, "Users"),
726
- h10(
1705
+ return h12("div", { style: styles.container }, [
1706
+ pageHeader(
1707
+ "Users",
1708
+ "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1709
+ ),
1710
+ h12(
727
1711
  "form",
728
1712
  {
729
1713
  style: styles.toolbar,
@@ -734,75 +1718,67 @@ var UsersScreen = defineComponent9({
734
1718
  }
735
1719
  },
736
1720
  [
737
- h10("input", {
1721
+ h12("input", {
738
1722
  type: "email",
739
1723
  value: input.value,
740
1724
  onInput: (e) => input.value = e.target.value,
741
1725
  placeholder: "email address",
742
- style: { ...styles.button, cursor: "text", minWidth: "280px" },
1726
+ style: { ...styles.input, minWidth: "280px" },
743
1727
  "aria-label": "Email"
744
1728
  }),
745
- h10(
1729
+ h12(
746
1730
  "button",
747
- { type: "submit", style: styles.button, disabled: isLoading.value },
748
- "Look up"
1731
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1732
+ [icon("search", 14), "Look up"]
749
1733
  ),
750
- showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : refreshButton("\u2190 All users", false, clear)
1734
+ showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
751
1735
  ]
752
1736
  ),
753
- showList.value ? h10("div", [
754
- list.error.value ? h10("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
755
- list.users.value.length === 0 && !list.isLoading.value ? h10("p", { style: styles.muted }, "No users yet.") : table(
1737
+ showList.value ? h12("div", [
1738
+ list.error.value ? h12("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1739
+ list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
756
1740
  ["Email", "Name", "Created", "Status"],
757
1741
  list.users.value.map(
758
- (u2) => h10("tr", { key: u2.id }, [
1742
+ (u2) => h12("tr", { key: u2.id }, [
759
1743
  td(
760
- h10(
1744
+ h12(
761
1745
  "button",
762
1746
  {
763
1747
  type: "button",
764
- style: { ...styles.navItem, padding: 0 },
1748
+ style: styles.link,
765
1749
  onClick: () => selectedId.value = u2.id
766
1750
  },
767
1751
  u2.email
768
1752
  )
769
1753
  ),
770
1754
  td(
771
- `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h10("span", { style: styles.muted }, "\u2014")
1755
+ `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h12("span", { style: styles.muted }, "\u2014")
772
1756
  ),
773
1757
  td(new Date(u2.createdAt).toLocaleDateString()),
774
1758
  td(
775
- u2.suspended ? h10("span", { style: styles.badge }, "suspended") : u2.deletedAt ? h10("span", { style: styles.badge }, "deleted") : h10("span", { style: styles.muted }, "active")
1759
+ u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
776
1760
  )
777
1761
  ])
778
1762
  )
779
1763
  ),
780
- list.isLoading.value ? h10("p", { style: styles.status }, "Loading\u2026") : null,
781
- list.hasMore.value && !list.isLoading.value ? h10(
782
- "button",
783
- {
784
- type: "button",
785
- style: { ...styles.button, marginTop: "8px" },
786
- onClick: () => void list.loadMore()
787
- },
788
- "Load more"
789
- ) : null
1764
+ list.isLoading.value ? h12("p", { style: styles.status }, "Loading\u2026") : null,
1765
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
790
1766
  ]) : null,
791
- error.value ? h10(
1767
+ error.value ? h12(
792
1768
  "p",
793
1769
  { style: styles.error, role: "alert" },
794
1770
  error.value.status === 404 ? "No user with that email." : error.value.explanation
795
1771
  ) : null,
796
1772
  u ? [
797
- h10("h2", { style: styles.subtitle }, [
1773
+ h12("h2", { style: styles.subtitle }, [
798
1774
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
799
1775
  " ",
800
- u.suspended ? h10("span", { style: styles.badge }, "suspended") : null,
801
- u.deletedAt ? h10("span", { style: styles.badge }, "deleted") : null
1776
+ u.suspended ? pill("warn", "suspended") : null,
1777
+ u.deletedAt ? pill("neutral", "deleted") : null
802
1778
  ]),
803
- h10("table", { style: styles.table }, [
804
- h10("tbody", [
805
- row("id", h10("span", { style: styles.mono }, u.id)),
1779
+ h12("table", { style: styles.table }, [
1780
+ h12("tbody", [
1781
+ row("id", h12("span", { style: styles.mono }, u.id)),
806
1782
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
807
1783
  row("MFA", yesNo(u.mfaEnabled)),
808
1784
  row(
@@ -811,32 +1787,37 @@ var UsersScreen = defineComponent9({
811
1787
  ),
812
1788
  row(
813
1789
  "last login",
814
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h10("span", { style: styles.muted }, "never")
1790
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h12("span", { style: styles.muted }, "never")
815
1791
  ),
816
1792
  row("created", new Date(u.createdAt).toLocaleString())
817
1793
  ])
818
1794
  ]),
819
- h10("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
820
- u.suspended ? refreshButton("Unsuspend", isLoading.value, () => void unsuspend()) : refreshButton("Suspend", isLoading.value, () => void suspend()),
821
- refreshButton(
1795
+ h12("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1796
+ u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1797
+ "Suspend",
1798
+ isLoading.value,
1799
+ () => void suspend(),
1800
+ styles.buttonDanger
1801
+ ),
1802
+ actionButton(
822
1803
  "Sign out everywhere",
823
1804
  isLoading.value,
824
1805
  () => void revokeSessions().then(() => sessions.refresh())
825
1806
  )
826
1807
  ]),
827
- h10("h2", { style: styles.subtitle }, "Live sessions"),
828
- sessions.sessions.value.length === 0 ? h10("p", { style: styles.muted }, "None.") : h10(
1808
+ h12("h2", { style: styles.subtitle }, "Live sessions"),
1809
+ sessions.sessions.value.length === 0 ? h12("p", { style: styles.muted }, "None.") : h12(
829
1810
  "ul",
830
1811
  { style: styles.list },
831
1812
  sessions.sessions.value.map(
832
- (s) => h10("li", { key: s.id, style: styles.row }, [
833
- h10("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1813
+ (s) => h12("li", { key: s.id, style: styles.row }, [
1814
+ h12("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
834
1815
  " ",
835
- h10("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
1816
+ h12("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
836
1817
  " ",
837
- h10("span", { style: styles.muted }, s.userAgent ?? ""),
1818
+ h12("span", { style: styles.muted }, s.userAgent ?? ""),
838
1819
  " ",
839
- h10(
1820
+ h12(
840
1821
  "span",
841
1822
  { style: styles.muted },
842
1823
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -844,38 +1825,26 @@ var UsersScreen = defineComponent9({
844
1825
  ])
845
1826
  )
846
1827
  ),
847
- h10("h2", { style: styles.subtitle }, "Recent sign-ins"),
848
- history.events.value.length === 0 && !history.isLoading.value ? h10("p", { style: styles.muted }, "None recorded.") : h10(
1828
+ h12("h2", { style: styles.subtitle }, "Recent sign-ins"),
1829
+ history.events.value.length === 0 && !history.isLoading.value ? h12("p", { style: styles.muted }, "None recorded.") : h12(
849
1830
  "ul",
850
1831
  { style: styles.list },
851
1832
  history.events.value.map(
852
- (e) => h10("li", { key: e.id, style: styles.row }, [
853
- h10(
854
- "span",
855
- { style: e.outcome === "success" ? styles.ok : styles.bad },
856
- e.outcome
857
- ),
1833
+ (e) => h12("li", { key: e.id, style: styles.row }, [
1834
+ pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
858
1835
  " ",
859
- h10("span", { style: styles.muted }, e.method),
1836
+ h12("span", { style: styles.muted }, e.method),
860
1837
  " ",
861
- h10("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1838
+ h12("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
862
1839
  " ",
863
- h10("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
864
- e.location?.proxy || e.location?.hosting ? h10("span", { style: styles.bad }, e.location.proxy ? " proxy/VPN" : " hosting") : "",
1840
+ h12("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
1841
+ e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
865
1842
  " ",
866
- h10("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1843
+ h12("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
867
1844
  ])
868
1845
  )
869
1846
  ),
870
- history.hasMore.value && !history.isLoading.value ? h10(
871
- "button",
872
- {
873
- type: "button",
874
- style: { ...styles.button, marginTop: "8px" },
875
- onClick: () => void history.loadMore()
876
- },
877
- "Load more"
878
- ) : null
1847
+ history.hasMore.value && !history.isLoading.value ? loadMoreButton(() => void history.loadMore()) : null
879
1848
  ] : null
880
1849
  ]);
881
1850
  };
@@ -884,8 +1853,8 @@ var UsersScreen = defineComponent9({
884
1853
 
885
1854
  // src/screens/CatalogScreen.ts
886
1855
  import { useAdminCatalog } from "@fonderie/vue-admin";
887
- import { defineComponent as defineComponent10, h as h11 } from "vue";
888
- var CatalogScreen = defineComponent10({
1856
+ import { defineComponent as defineComponent11, h as h13 } from "vue";
1857
+ var CatalogScreen = defineComponent11({
889
1858
  name: "FonderieCatalogScreen",
890
1859
  props: { client: { type: Object, required: true } },
891
1860
  setup(props) {
@@ -893,35 +1862,42 @@ var CatalogScreen = defineComponent10({
893
1862
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
894
1863
  return () => {
895
1864
  const c = catalog.value;
896
- return h11("div", { style: styles.container }, [
897
- h11("div", { style: styles.toolbar }, [
898
- h11("h1", { style: { ...styles.title, marginBottom: 0 } }, "Catalog"),
899
- refreshButton("Refresh", isLoading.value, () => void refresh())
900
- ]),
901
- error.value ? h11("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
902
- isLoading.value && !c ? h11("p", { style: styles.status }, "Loading\u2026") : null,
1865
+ return h13("div", { style: styles.container }, [
1866
+ pageHeader(
1867
+ "Catalog",
1868
+ "What you sell: plans as configured in code, and as stored in the database, side by side.",
1869
+ [refreshButton("Refresh", isLoading.value, () => void refresh())]
1870
+ ),
1871
+ error.value ? h13("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1872
+ isLoading.value && !c ? h13("p", { style: styles.status }, "Loading\u2026") : null,
903
1873
  c ? [
904
- h11("h2", { style: styles.subtitle }, "Configured (code)"),
905
- h11(
1874
+ h13("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1875
+ h13(
906
1876
  "pre",
907
1877
  {
908
1878
  style: {
1879
+ ...styles.card,
909
1880
  ...styles.mono,
910
- background: "var(--fonderie-surface-alt,#fafafa)",
911
- padding: "12px",
912
- overflowX: "auto"
1881
+ margin: 0,
1882
+ overflowX: "auto",
1883
+ maxHeight: "360px",
1884
+ lineHeight: 1.6
913
1885
  }
914
1886
  },
915
1887
  JSON.stringify(c.configured, null, 2)
916
1888
  ),
917
- h11("h2", { style: styles.subtitle }, "Stored (database)"),
918
- c.stored.length === 0 ? h11("p", { style: styles.muted }, "No stored plans.") : table(
1889
+ h13("h2", { style: styles.subtitle }, "Stored (database)"),
1890
+ c.stored.length === 0 ? empty(
1891
+ "No stored plans",
1892
+ "Plans are configured in code; nothing has been written to the database.",
1893
+ "catalog"
1894
+ ) : table(
919
1895
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
920
1896
  c.stored.map(
921
- (p) => h11("tr", { key: p.id }, [
1897
+ (p) => h13("tr", { key: p.id }, [
922
1898
  td([
923
- h11("strong", p.name),
924
- p.description ? h11("div", { style: styles.muted }, p.description) : ""
1899
+ h13("strong", p.name),
1900
+ p.description ? h13("div", { style: styles.muted }, p.description) : ""
925
1901
  ]),
926
1902
  td(String(p.tier)),
927
1903
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -929,11 +1905,11 @@ var CatalogScreen = defineComponent10({
929
1905
  td(money(p.pricing?.yearly)),
930
1906
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
931
1907
  td(
932
- h11(
1908
+ h13(
933
1909
  "button",
934
1910
  {
935
1911
  type: "button",
936
- style: styles.button,
1912
+ style: styles.buttonDanger,
937
1913
  onClick: () => {
938
1914
  if (window.confirm(`Delete stored plan "${p.name}"?`))
939
1915
  void deletePlan(p.id).catch(() => {
@@ -954,17 +1930,17 @@ var CatalogScreen = defineComponent10({
954
1930
 
955
1931
  // src/screens/SubscriberScreen.ts
956
1932
  import { useAdminSubscriber, useAdminSubscribers } from "@fonderie/vue-admin";
957
- import { defineComponent as defineComponent11, h as h12, ref as ref3 } from "vue";
958
- var SubscriberScreen = defineComponent11({
1933
+ import { defineComponent as defineComponent12, h as h14, ref as ref5 } from "vue";
1934
+ var SubscriberScreen = defineComponent12({
959
1935
  name: "FonderieSubscriberScreen",
960
1936
  props: {
961
1937
  client: { type: Object, required: true },
962
1938
  pageSize: { type: Number, default: 50 }
963
1939
  },
964
1940
  setup(props) {
965
- const type = ref3("user");
966
- const idInput = ref3("");
967
- const subscriber = ref3(null);
1941
+ const type = ref5("user");
1942
+ const idInput = ref5("");
1943
+ const subscriber = ref5(null);
968
1944
  const list = useAdminSubscribers(props.client, { limit: props.pageSize });
969
1945
  const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
970
1946
  const open = (t2, id) => {
@@ -973,22 +1949,25 @@ var SubscriberScreen = defineComponent11({
973
1949
  idInput.value = id;
974
1950
  subscriber.value = { type: t2, id };
975
1951
  };
976
- const amount = ref3("");
977
- const note = ref3("");
978
- const granted = ref3(null);
979
- const input = (model, placeholder, extra = {}) => h12("input", {
1952
+ const amount = ref5("");
1953
+ const note = ref5("");
1954
+ const granted = ref5(null);
1955
+ const input = (model, placeholder, extra = {}) => h14("input", {
980
1956
  value: model.value,
981
1957
  onInput: (e) => model.value = e.target.value,
982
1958
  placeholder,
983
- style: { ...styles.button, cursor: "text", ...extra },
1959
+ style: { ...styles.input, ...extra },
984
1960
  "aria-label": placeholder
985
1961
  });
986
1962
  return () => {
987
1963
  const s = subscription.value;
988
1964
  const w = wallet.value;
989
- return h12("div", { style: styles.container }, [
990
- h12("h1", { style: styles.title }, "Subscriber"),
991
- h12(
1965
+ return h14("div", { style: styles.container }, [
1966
+ pageHeader(
1967
+ "Subscribers",
1968
+ "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
1969
+ ),
1970
+ h14(
992
1971
  "form",
993
1972
  {
994
1973
  style: styles.toolbar,
@@ -999,26 +1978,26 @@ var SubscriberScreen = defineComponent11({
999
1978
  }
1000
1979
  },
1001
1980
  [
1002
- h12(
1981
+ h14(
1003
1982
  "select",
1004
1983
  {
1005
1984
  value: type.value,
1006
1985
  onChange: (e) => type.value = e.target.value,
1007
- style: styles.button,
1986
+ style: styles.input,
1008
1987
  "aria-label": "Type"
1009
1988
  },
1010
1989
  [
1011
- h12("option", { value: "user" }, "user"),
1012
- h12("option", { value: "workspace" }, "workspace")
1990
+ h14("option", { value: "user" }, "user"),
1991
+ h14("option", { value: "workspace" }, "workspace")
1013
1992
  ]
1014
1993
  ),
1015
1994
  input(idInput, "subscriber id", { minWidth: "280px" }),
1016
- h12(
1995
+ h14(
1017
1996
  "button",
1018
- { type: "submit", style: styles.button, disabled: isLoading.value },
1019
- "Look up"
1997
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1998
+ [icon("search", 14), "Look up"]
1020
1999
  ),
1021
- subscriber.value ? h12(
2000
+ subscriber.value ? h14(
1022
2001
  "button",
1023
2002
  {
1024
2003
  type: "button",
@@ -1029,8 +2008,8 @@ var SubscriberScreen = defineComponent11({
1029
2008
  granted.value = null;
1030
2009
  }
1031
2010
  },
1032
- "\u2190 All subscribers"
1033
- ) : h12(
2011
+ [icon("back", 14), "All subscribers"]
2012
+ ) : h14(
1034
2013
  "button",
1035
2014
  {
1036
2015
  type: "button",
@@ -1042,35 +2021,43 @@ var SubscriberScreen = defineComponent11({
1042
2021
  )
1043
2022
  ]
1044
2023
  ),
1045
- !subscriber.value ? h12("div", [
1046
- list.error.value ? h12("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1047
- list.subscriptions.value.length === 0 && !list.isLoading.value ? h12("p", { style: styles.muted }, "No subscribers yet.") : table(
2024
+ !subscriber.value ? h14("div", [
2025
+ list.error.value ? h14("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
2026
+ list.subscriptions.value.length === 0 && !list.isLoading.value ? empty(
2027
+ "No subscribers yet",
2028
+ "Subscriptions appear here once someone checks out.",
2029
+ "subscriber"
2030
+ ) : table(
1048
2031
  ["Subscriber", "Plan", "Status", "Renews"],
1049
2032
  list.subscriptions.value.map(
1050
- (sub) => h12("tr", { key: sub.id }, [
2033
+ (sub) => h14("tr", { key: sub.id }, [
1051
2034
  td(
1052
- h12(
2035
+ h14(
1053
2036
  "button",
1054
2037
  {
1055
2038
  type: "button",
1056
- style: { ...styles.navItem, padding: 0, ...styles.mono },
2039
+ style: { ...styles.link, ...styles.mono },
1057
2040
  onClick: () => open(sub.subscriberType, sub.subscriberId)
1058
2041
  },
1059
2042
  `${sub.subscriberType}/${sub.subscriberId}`
1060
2043
  )
1061
2044
  ),
1062
2045
  td(`${sub.plan} \xB7 ${sub.interval}`),
2046
+ td([
2047
+ pill(
2048
+ sub.status === "active" ? "ok" : sub.status === "trialing" ? "info" : sub.status === "past_due" ? "warn" : "neutral",
2049
+ sub.status
2050
+ ),
2051
+ sub.cancelAtPeriodEnd ? [" ", pill("warn", "cancels", false)] : null
2052
+ ]),
1063
2053
  td(
1064
- sub.cancelAtPeriodEnd ? [sub.status, " ", h12("span", { style: styles.badge }, "cancels")] : sub.status
1065
- ),
1066
- td(
1067
- sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : h12("span", { style: styles.muted }, "\u2014")
2054
+ sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : h14("span", { style: styles.muted }, "\u2014")
1068
2055
  )
1069
2056
  ])
1070
2057
  )
1071
2058
  ),
1072
- list.isLoading.value ? h12("p", { style: styles.status }, "Loading\u2026") : null,
1073
- list.hasMore.value && !list.isLoading.value ? h12(
2059
+ list.isLoading.value ? h14("p", { style: styles.status }, "Loading\u2026") : null,
2060
+ list.hasMore.value && !list.isLoading.value ? h14(
1074
2061
  "button",
1075
2062
  {
1076
2063
  type: "button",
@@ -1080,37 +2067,58 @@ var SubscriberScreen = defineComponent11({
1080
2067
  "Load more"
1081
2068
  ) : null
1082
2069
  ]) : null,
1083
- error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2070
+ error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1084
2071
  subscriber.value && !isLoading.value ? [
1085
- h12("h2", { style: styles.subtitle }, "Subscription"),
1086
- s ? h12("table", { style: styles.table }, [
1087
- h12("tbody", [
1088
- h12("tr", [td("plan"), td([h12("strong", s.plan), ` \xB7 ${s.interval}`])]),
1089
- h12("tr", [
2072
+ h14("h2", { style: styles.subtitle }, "Subscription"),
2073
+ s ? h14("table", { style: styles.table }, [
2074
+ h14("tbody", [
2075
+ h14("tr", [td("plan"), td([h14("strong", s.plan), ` \xB7 ${s.interval}`])]),
2076
+ h14("tr", [
1090
2077
  td("status"),
1091
2078
  td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
1092
2079
  ]),
1093
- h12("tr", [
2080
+ h14("tr", [
1094
2081
  td("period"),
1095
2082
  td(
1096
2083
  `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
1097
2084
  )
1098
2085
  ]),
1099
- h12("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
2086
+ h14("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
1100
2087
  ])
1101
- ]) : h12("p", { style: styles.muted }, "No subscription."),
1102
- h12("h2", { style: styles.subtitle }, "Wallet"),
2088
+ ]) : h14("p", { style: styles.muted }, "No subscription."),
2089
+ h14("h2", { style: styles.subtitle }, "Wallet"),
1103
2090
  w ? [
1104
- h12("p", [
1105
- h12("strong", w.balance),
1106
- ` ${w.currency} `,
1107
- h12(
1108
- "span",
1109
- { style: styles.muted },
1110
- `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
1111
- )
1112
- ]),
1113
- h12(
2091
+ h14(
2092
+ "p",
2093
+ {
2094
+ style: {
2095
+ ...styles.card,
2096
+ display: "flex",
2097
+ alignItems: "baseline",
2098
+ gap: "8px",
2099
+ flexWrap: "wrap"
2100
+ }
2101
+ },
2102
+ [
2103
+ h14(
2104
+ "strong",
2105
+ {
2106
+ style: {
2107
+ fontSize: "22px",
2108
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
2109
+ }
2110
+ },
2111
+ w.balance
2112
+ ),
2113
+ ` ${w.currency} `,
2114
+ h14(
2115
+ "span",
2116
+ { style: styles.muted },
2117
+ `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
2118
+ )
2119
+ ]
2120
+ ),
2121
+ h14(
1114
2122
  "form",
1115
2123
  {
1116
2124
  style: styles.toolbar,
@@ -1135,23 +2143,23 @@ var SubscriberScreen = defineComponent11({
1135
2143
  [
1136
2144
  input(amount, "amount (minor units)"),
1137
2145
  input(note, "reason (optional)", { minWidth: "220px" }),
1138
- h12(
2146
+ h14(
1139
2147
  "button",
1140
2148
  {
1141
2149
  type: "submit",
1142
- style: styles.button,
2150
+ style: styles.buttonPrimary,
1143
2151
  disabled: !/^\d+$/.test(amount.value.trim())
1144
2152
  },
1145
- "Grant"
2153
+ [icon("plus", 14), "Grant"]
1146
2154
  ),
1147
- granted.value ? h12("span", { style: styles.ok }, granted.value) : null
2155
+ granted.value ? h14("span", { style: styles.ok }, granted.value) : null
1148
2156
  ]
1149
2157
  ),
1150
- h12("h2", { style: styles.subtitle }, "Ledger"),
1151
- ledger.value.length === 0 ? h12("p", { style: styles.muted }, "Nothing moved yet.") : table(
2158
+ h14("h2", { style: styles.subtitle }, "Ledger"),
2159
+ ledger.value.length === 0 ? empty("Nothing moved yet", void 0, "subscriber") : table(
1152
2160
  ["When", "Kind", "Amount", "Note"],
1153
2161
  ledger.value.map(
1154
- (t2) => h12("tr", { key: t2.id }, [
2162
+ (t2) => h14("tr", { key: t2.id }, [
1155
2163
  td(new Date(t2.createdAt).toLocaleString(), styles.muted),
1156
2164
  td(t2.type),
1157
2165
  td(t2.amount, styles.mono),
@@ -1159,7 +2167,7 @@ var SubscriberScreen = defineComponent11({
1159
2167
  ])
1160
2168
  )
1161
2169
  ),
1162
- hasMoreLedger.value ? h12(
2170
+ hasMoreLedger.value ? h14(
1163
2171
  "button",
1164
2172
  {
1165
2173
  type: "button",
@@ -1168,7 +2176,7 @@ var SubscriberScreen = defineComponent11({
1168
2176
  },
1169
2177
  "Load more"
1170
2178
  ) : null
1171
- ] : h12(
2179
+ ] : h14(
1172
2180
  "p",
1173
2181
  { style: styles.muted },
1174
2182
  "No wallet \u2014 billing has no wallet configuration."
@@ -1181,30 +2189,33 @@ var SubscriberScreen = defineComponent11({
1181
2189
 
1182
2190
  // src/screens/AuditScreen.ts
1183
2191
  import { useAdminAudit } from "@fonderie/vue-admin";
1184
- import { defineComponent as defineComponent12, h as h13, ref as ref4 } from "vue";
1185
- var AuditScreen = defineComponent12({
2192
+ import { defineComponent as defineComponent13, h as h15, ref as ref6 } from "vue";
2193
+ var AuditScreen = defineComponent13({
1186
2194
  name: "FonderieAuditScreen",
1187
2195
  props: {
1188
2196
  client: { type: Object, required: true },
1189
2197
  pageSize: { type: Number, default: 50 }
1190
2198
  },
1191
2199
  setup(props) {
1192
- const draft = ref4({ workspaceId: "", type: "", actorId: "" });
1193
- const query = ref4({ limit: props.pageSize });
2200
+ const draft = ref6({ workspaceId: "", type: "", actorId: "" });
2201
+ const query = ref6({ limit: props.pageSize });
1194
2202
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
1195
2203
  props.client,
1196
2204
  query
1197
2205
  );
1198
- const field = (key, placeholder) => h13("input", {
2206
+ const field = (key, placeholder) => h15("input", {
1199
2207
  value: draft.value[key],
1200
2208
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
1201
2209
  placeholder,
1202
- style: { ...styles.button, cursor: "text" },
2210
+ style: styles.input,
1203
2211
  "aria-label": placeholder
1204
2212
  });
1205
- return () => h13("div", { style: styles.container }, [
1206
- h13("h1", { style: styles.title }, "Audit"),
1207
- h13(
2213
+ return () => h15("div", { style: styles.container }, [
2214
+ pageHeader(
2215
+ "Audit",
2216
+ "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
2217
+ ),
2218
+ h15(
1208
2219
  "form",
1209
2220
  {
1210
2221
  style: styles.toolbar,
@@ -1223,19 +2234,19 @@ var AuditScreen = defineComponent12({
1223
2234
  field("workspaceId", "workspace id (all if empty)"),
1224
2235
  field("type", "event type"),
1225
2236
  field("actorId", "actor id"),
1226
- h13(
2237
+ h15(
1227
2238
  "button",
1228
- { type: "submit", style: styles.button, disabled: isLoading.value },
1229
- "Filter"
2239
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
2240
+ [icon("search", 14), "Filter"]
1230
2241
  ),
1231
2242
  refreshButton("Refresh", isLoading.value, () => void refresh())
1232
2243
  ]
1233
2244
  ),
1234
- error.value ? h13("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1235
- events.value.length === 0 && !isLoading.value ? h13("p", { style: styles.muted }, "Nothing recorded for this filter.") : table(
2245
+ error.value ? h15("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2246
+ events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
1236
2247
  ["When", "Type", "Workspace", "Actor", "Request"],
1237
2248
  events.value.map(
1238
- (e) => h13("tr", { key: e.id }, [
2249
+ (e) => h15("tr", { key: e.id }, [
1239
2250
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
1240
2251
  td(e.type, styles.mono),
1241
2252
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -1244,16 +2255,8 @@ var AuditScreen = defineComponent12({
1244
2255
  ])
1245
2256
  )
1246
2257
  ),
1247
- isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
1248
- hasMore.value && !isLoading.value ? h13(
1249
- "button",
1250
- {
1251
- type: "button",
1252
- style: { ...styles.button, marginTop: "8px" },
1253
- onClick: () => void loadMore()
1254
- },
1255
- "Load more"
1256
- ) : null
2258
+ isLoading.value ? h15("p", { style: styles.status }, "Loading\u2026") : null,
2259
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
1257
2260
  ]);
1258
2261
  }
1259
2262
  });
@@ -1285,12 +2288,31 @@ var NAV = [
1285
2288
  items: [
1286
2289
  { page: "audit", label: "Audit", needs: "audit" },
1287
2290
  { page: "log", label: "Admin log" },
1288
- { page: "tokens", label: "Access" },
2291
+ { page: "operators", label: "Operators", needs: "operators" },
2292
+ { page: "tokens", label: "Tokens" },
1289
2293
  { page: "migrations", label: "Migrations" }
1290
2294
  ]
1291
2295
  }
1292
2296
  ];
1293
- var AdminShell = defineComponent13({
2297
+ var ICON = {
2298
+ attention: "attention",
2299
+ modules: "modules",
2300
+ environment: "environment",
2301
+ doctor: "doctor",
2302
+ routes: "routes",
2303
+ users: "users",
2304
+ catalog: "catalog",
2305
+ subscriber: "subscriber",
2306
+ settings: "settings",
2307
+ templates: "templates",
2308
+ audit: "audit",
2309
+ log: "log",
2310
+ tokens: "tokens",
2311
+ operators: "users",
2312
+ migrations: "migrations"
2313
+ };
2314
+ var NARROW = "(max-width: 820px)";
2315
+ var AdminShell = defineComponent14({
1294
2316
  name: "FonderieAdminShell",
1295
2317
  props: {
1296
2318
  client: { type: Object, required: true },
@@ -1307,75 +2329,85 @@ var AdminShell = defineComponent13({
1307
2329
  environment: { type: String, default: void 0 },
1308
2330
  // Controlled navigation: pass `page` and listen to `navigate` to own the
1309
2331
  // URL. Omit `page` and the shell keeps it itself.
1310
- page: { type: String, default: void 0 }
2332
+ page: { type: String, default: void 0 },
2333
+ // Shown at the top of the sidebar. Defaults to "Admin".
2334
+ appName: { type: String, default: "Admin" },
2335
+ // A badge beside the name — the deployment's environment ("production").
2336
+ // Production is tinted so an operator always knows where their clicks land.
2337
+ envLabel: { type: String, default: void 0 },
2338
+ // The deployment has operator accounts (@fonderie/admin with a store):
2339
+ // shows the Operators page.
2340
+ operators: { type: Boolean, default: false },
2341
+ // The signed-in operator's email, when a person (not a token) is signed in.
2342
+ currentOperator: { type: String, default: void 0 }
1311
2343
  },
2344
+ // `footer` slot: pinned to the bottom of the sidebar — session controls
2345
+ // (theme, sign out). The React shell takes the same thing as a `footer` prop.
1312
2346
  emits: { navigate: (_page) => true },
1313
- setup(props, { emit }) {
1314
- const own = ref5("attention");
2347
+ setup(props, { emit, slots }) {
2348
+ const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2349
+ const narrow = ref7(query?.matches ?? false);
2350
+ const drawer = ref7(false);
2351
+ const onMedia = (e) => narrow.value = e.matches;
2352
+ onMounted(() => query?.addEventListener("change", onMedia));
2353
+ onBeforeUnmount(() => query?.removeEventListener("change", onMedia));
2354
+ const own = ref7("attention");
1315
2355
  const current = computed2(() => props.page ?? own.value);
1316
- const editing = ref5(null);
2356
+ const editing = ref7(null);
1317
2357
  const go = (p) => {
1318
2358
  editing.value = null;
1319
2359
  emit("navigate", p);
1320
2360
  if (props.page === void 0) own.value = p;
2361
+ drawer.value = false;
1321
2362
  };
1322
- const back = () => h14(
1323
- "button",
1324
- { type: "button", style: styles.button, onClick: () => editing.value = null },
1325
- "\u2190 Back"
1326
- );
2363
+ const back = (label) => h16("div", { style: { padding: "24px 40px 0" } }, [
2364
+ h16(
2365
+ "button",
2366
+ { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2367
+ [icon("back", 14), label]
2368
+ )
2369
+ ]);
2370
+ const missing = (text) => h16("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
1327
2371
  const body = () => {
1328
2372
  const c = props.client;
1329
2373
  switch (current.value) {
1330
2374
  case "attention":
1331
- return h14(AttentionScreen, { client: c });
2375
+ return h16(AttentionScreen, { client: c });
1332
2376
  case "modules":
1333
- return h14(ModulesScreen, { client: c });
2377
+ return h16(ModulesScreen, { client: c });
1334
2378
  case "doctor":
1335
- return h14(DoctorScreen, { client: c });
2379
+ return h16(DoctorScreen, { client: c });
1336
2380
  case "environment":
1337
- return h14(EnvironmentScreen, { client: c });
2381
+ return h16(EnvironmentScreen, { client: c });
1338
2382
  case "routes":
1339
- return h14(RoutesScreen, { client: c });
2383
+ return h16(RoutesScreen, { client: c });
1340
2384
  case "tokens":
1341
- return h14(TokensScreen, { client: c });
2385
+ return h16(TokensScreen, { client: c });
2386
+ case "operators":
2387
+ return h16(OperatorsScreen, {
2388
+ client: c,
2389
+ ...props.currentOperator ? { me: props.currentOperator } : {}
2390
+ });
1342
2391
  case "migrations":
1343
- return h14(MigrationsScreen, { client: c });
2392
+ return h16(MigrationsScreen, { client: c });
1344
2393
  case "log":
1345
- return h14(AdminLogScreen, { client: c });
2394
+ return h16(AdminLogScreen, { client: c });
1346
2395
  case "catalog":
1347
- return props.billingClient ? h14(CatalogScreen, { client: props.billingClient }) : h14(
1348
- "p",
1349
- { style: styles.status },
1350
- "Pass a BillingAdminClient to see the catalog here."
1351
- );
2396
+ return props.billingClient ? h16(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
1352
2397
  case "subscriber":
1353
- return props.billingClient ? h14(SubscriberScreen, { client: props.billingClient }) : h14(
1354
- "p",
1355
- { style: styles.status },
1356
- "Pass a BillingAdminClient to look up subscribers here."
1357
- );
2398
+ return props.billingClient ? h16(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
1358
2399
  case "audit":
1359
- return props.auditClient ? h14(AuditScreen, { client: props.auditClient }) : h14(
1360
- "p",
1361
- { style: styles.status },
1362
- "Pass an AuditAdminClient to see the audit trail here."
1363
- );
2400
+ return props.auditClient ? h16(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
1364
2401
  case "users":
1365
- return props.authClient ? h14(UsersScreen, { client: props.authClient }) : h14("p", { style: styles.status }, "Pass an AuthAdminClient to look up users here.");
2402
+ return props.authClient ? h16(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
1366
2403
  case "settings": {
1367
2404
  const cc = props.configClient;
1368
- if (!cc)
1369
- return h14(
1370
- "p",
1371
- { style: styles.status },
1372
- "Pass a ConfigAdminClient to manage config and secrets here."
1373
- );
2405
+ if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
1374
2406
  const e = editing.value;
1375
2407
  if (e && e.kind !== "template")
1376
2408
  return [
1377
- back(),
1378
- h14(ConfigEditorScreen, {
2409
+ back("Config & secrets"),
2410
+ h16(ConfigEditorScreen, {
1379
2411
  client: cc,
1380
2412
  kind: e.kind,
1381
2413
  configKey: e.key,
@@ -1383,7 +2415,7 @@ var AdminShell = defineComponent13({
1383
2415
  onSaved: () => editing.value = null
1384
2416
  })
1385
2417
  ];
1386
- return h14(ConfigListScreen, {
2418
+ return h16(ConfigListScreen, {
1387
2419
  client: cc,
1388
2420
  ...props.environment !== void 0 ? { environment: props.environment } : {},
1389
2421
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -1395,61 +2427,135 @@ var AdminShell = defineComponent13({
1395
2427
  }
1396
2428
  case "templates": {
1397
2429
  const kc = props.courierClient;
1398
- if (!kc)
1399
- return h14(
1400
- "p",
1401
- { style: styles.status },
1402
- "Pass a CourierAdminClient to manage templates here."
1403
- );
2430
+ if (!kc) return missing("Pass a CourierAdminClient to manage templates here.");
1404
2431
  const e = editing.value;
1405
2432
  if (e?.kind === "template")
1406
2433
  return [
1407
- back(),
1408
- h14(TemplateEditorScreen, {
2434
+ back("Templates"),
2435
+ h16(TemplateEditorScreen, {
1409
2436
  client: kc,
1410
2437
  type: e.type,
1411
2438
  onSaved: () => editing.value = null
1412
2439
  })
1413
2440
  ];
1414
- return h14(TemplateListScreen, {
2441
+ return h16(TemplateListScreen, {
1415
2442
  client: kc,
1416
2443
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
1417
2444
  });
1418
2445
  }
1419
2446
  }
1420
2447
  };
1421
- return () => h14("div", { style: styles.shell }, [
1422
- h14(
1423
- "nav",
1424
- { style: styles.nav, "aria-label": "Admin" },
1425
- NAV.map((g) => {
1426
- const items = g.items.filter(
1427
- (i) => !i.needs || (i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient)
1428
- );
1429
- if (items.length === 0) return null;
1430
- return h14("div", { key: g.group }, [
1431
- h14("div", { style: styles.navGroup }, g.group),
1432
- ...items.map(
1433
- (i) => h14(
2448
+ const visible = (i) => !i.needs || (i.needs === "operators" ? props.operators : i.needs === "config" ? props.configClient : i.needs === "courier" ? props.courierClient : i.needs === "auth" ? props.authClient : i.needs === "billing" ? props.billingClient : props.auditClient);
2449
+ const brand = () => {
2450
+ const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
2451
+ return h16("div", { style: styles.navBrand }, [
2452
+ h16("div", { style: styles.navMark }, [icon("mark", 15)]),
2453
+ h16("div", { style: { minWidth: 0, flex: 1 } }, [
2454
+ h16("div", { style: styles.navBrandTitle }, props.appName),
2455
+ props.envLabel ? h16(
2456
+ "span",
2457
+ {
2458
+ style: {
2459
+ ...styles.badge,
2460
+ marginTop: "3px",
2461
+ padding: "0 7px",
2462
+ fontSize: "11px",
2463
+ ...production ? {
2464
+ color: "var(--fonderie-danger,#e00)",
2465
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 35%, transparent)"
2466
+ } : {}
2467
+ }
2468
+ },
2469
+ props.envLabel
2470
+ ) : null
2471
+ ]),
2472
+ narrow.value ? h16(
2473
+ "button",
2474
+ {
2475
+ type: "button",
2476
+ style: styles.buttonGhost,
2477
+ "aria-label": "Close menu",
2478
+ onClick: () => drawer.value = false
2479
+ },
2480
+ [icon("close")]
2481
+ ) : null
2482
+ ]);
2483
+ };
2484
+ const nav = () => h16(
2485
+ "nav",
2486
+ {
2487
+ style: {
2488
+ ...styles.nav,
2489
+ ...narrow.value ? { width: "280px", maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2490
+ },
2491
+ "aria-label": "Admin"
2492
+ },
2493
+ [
2494
+ brand(),
2495
+ h16(
2496
+ "div",
2497
+ { style: styles.navScroll },
2498
+ NAV.map((g) => {
2499
+ const items = g.items.filter(visible);
2500
+ if (items.length === 0) return null;
2501
+ return h16("div", { key: g.group }, [
2502
+ h16("div", { style: styles.navGroup }, g.group),
2503
+ ...items.map((i) => {
2504
+ const active = current.value === i.page;
2505
+ return h16(
2506
+ "button",
2507
+ {
2508
+ key: i.page,
2509
+ type: "button",
2510
+ class: "fonderie-admin-nav-item",
2511
+ style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2512
+ "aria-current": active ? "page" : void 0,
2513
+ onClick: () => go(i.page)
2514
+ },
2515
+ [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2516
+ );
2517
+ })
2518
+ ]);
2519
+ })
2520
+ ),
2521
+ slots["footer"] ? h16("div", { style: styles.navFooter }, slots["footer"]()) : null
2522
+ ]
2523
+ );
2524
+ return () => {
2525
+ const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2526
+ return h16(
2527
+ "div",
2528
+ { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2529
+ [
2530
+ ...narrow.value ? [
2531
+ h16("div", { style: styles.topbar }, [
2532
+ h16(
1434
2533
  "button",
1435
2534
  {
1436
- key: i.page,
1437
2535
  type: "button",
1438
- style: {
1439
- ...styles.navItem,
1440
- ...current.value === i.page ? styles.navItemActive : {}
1441
- },
1442
- "aria-current": current.value === i.page ? "page" : void 0,
1443
- onClick: () => go(i.page)
2536
+ style: styles.buttonGhost,
2537
+ "aria-label": "Open menu",
2538
+ "aria-expanded": drawer.value,
2539
+ onClick: () => drawer.value = true
1444
2540
  },
1445
- i.label
1446
- )
1447
- )
1448
- ]);
1449
- })
1450
- ),
1451
- h14("main", { style: styles.main }, body())
1452
- ]);
2541
+ [icon("menu")]
2542
+ ),
2543
+ h16("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2544
+ props.envLabel ? h16("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2545
+ ]),
2546
+ drawer.value ? h16("div", { style: styles.drawer }, [
2547
+ nav(),
2548
+ h16("div", {
2549
+ style: styles.scrim,
2550
+ "aria-hidden": "true",
2551
+ onClick: () => drawer.value = false
2552
+ })
2553
+ ]) : null
2554
+ ] : [nav()],
2555
+ h16("main", { class: "fonderie-admin-main", style: styles.main }, body())
2556
+ ]
2557
+ );
2558
+ };
1453
2559
  }
1454
2560
  });
1455
2561
  export {
@@ -1462,6 +2568,7 @@ export {
1462
2568
  EnvironmentScreen,
1463
2569
  MigrationsScreen,
1464
2570
  ModulesScreen,
2571
+ OperatorsScreen,
1465
2572
  RoutesScreen,
1466
2573
  SubscriberScreen,
1467
2574
  TokensScreen,