@fonderie/vue-admin-screens 1.0.2 → 1.1.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 defineComponent13, h as h15, onBeforeUnmount, onMounted, ref as ref6 } 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"
42
72
  },
73
+ navBrand: {
74
+ display: "flex",
75
+ alignItems: "center",
76
+ gap: 10,
77
+ padding: "18px 16px 14px",
78
+ borderBottom: `1px solid ${t.borderLight}`
79
+ },
80
+ navMark: {
81
+ width: 28,
82
+ height: 28,
83
+ borderRadius: 7,
84
+ background: t.text,
85
+ color: t.surface,
86
+ display: "grid",
87
+ placeItems: "center",
88
+ flexShrink: 0
89
+ },
90
+ navBrandTitle: {
91
+ fontSize: 14,
92
+ fontWeight: 600,
93
+ letterSpacing: t.trackingDisplay,
94
+ lineHeight: 1.2,
95
+ overflow: "hidden",
96
+ textOverflow: "ellipsis",
97
+ whiteSpace: "nowrap"
98
+ },
99
+ navScroll: { flex: 1, overflowY: "auto", padding: "8px 10px 16px" },
43
100
  navGroup: {
44
101
  fontSize: 11,
45
102
  textTransform: "uppercase",
46
103
  letterSpacing: 0.6,
47
104
  color: t.muted,
48
- margin: "16px 0 6px",
105
+ margin: "16px 8px 4px",
49
106
  fontWeight: 600
50
107
  },
51
108
  navItem: {
52
- display: "block",
109
+ display: "flex",
110
+ alignItems: "center",
111
+ gap: 10,
53
112
  width: "100%",
54
113
  textAlign: "left",
55
114
  background: "none",
56
115
  border: "none",
57
- borderRadius: t.radius,
58
- padding: "6px 8px",
116
+ borderRadius: 6,
117
+ padding: "7px 8px",
59
118
  cursor: "pointer",
60
- fontSize: 14,
61
- color: t.text,
62
- fontFamily: "inherit"
119
+ fontSize: 13.5,
120
+ fontWeight: 500,
121
+ color: t.muted,
122
+ fontFamily: "inherit",
123
+ position: "relative"
124
+ },
125
+ navItemActive: { background: tint(t.accent, 12), color: t.text, fontWeight: 600 },
126
+ navIconActive: { color: t.accentStrong },
127
+ navFooter: {
128
+ borderTop: `1px solid ${t.borderLight}`,
129
+ padding: 12,
130
+ display: "flex",
131
+ flexDirection: "column",
132
+ gap: 10
133
+ },
134
+ // Phone layout: a top bar with the menu button replaces the sidebar, which
135
+ // opens as a drawer over the content.
136
+ topbar: {
137
+ position: "sticky",
138
+ top: 0,
139
+ zIndex: 20,
140
+ display: "flex",
141
+ alignItems: "center",
142
+ gap: 10,
143
+ padding: "10px 16px",
144
+ background: t.surface,
145
+ borderBottom: `1px solid ${t.border}`
146
+ },
147
+ drawer: {
148
+ position: "fixed",
149
+ inset: 0,
150
+ zIndex: 30,
151
+ display: "flex"
152
+ },
153
+ scrim: { flex: 1, background: "rgba(0,0,0,.35)" },
154
+ main: { flex: 1, minWidth: 0 },
155
+ container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
156
+ // ── page header ──
157
+ header: {
158
+ display: "flex",
159
+ alignItems: "flex-start",
160
+ justifyContent: "space-between",
161
+ gap: 16,
162
+ marginBottom: 24,
163
+ flexWrap: "wrap"
63
164
  },
64
- navItemActive: { background: t.surfaceAlt, fontWeight: 600, color: t.accentStrong },
65
- main: { flex: 1, padding: 24, maxWidth: 960 },
66
- container: { padding: 24, maxWidth: 960 },
67
165
  title: {
68
- fontSize: 20,
166
+ fontSize: 22,
69
167
  fontWeight: 600,
70
- marginTop: 0,
71
- marginBottom: 12,
72
- letterSpacing: t.trackingDisplay
168
+ margin: 0,
169
+ letterSpacing: t.trackingDisplay,
170
+ lineHeight: 1.25
73
171
  },
172
+ lead: { color: t.muted, fontSize: 13.5, margin: "4px 0 0", lineHeight: 1.5 },
173
+ actions: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
74
174
  subtitle: {
75
175
  fontSize: 15,
76
176
  fontWeight: 600,
77
- marginTop: 24,
78
- marginBottom: 8,
79
- letterSpacing: t.trackingDisplay
177
+ marginTop: 32,
178
+ marginBottom: 10,
179
+ letterSpacing: t.trackingDisplay,
180
+ display: "flex",
181
+ alignItems: "center",
182
+ gap: 8
183
+ },
184
+ // ── feedback ──
185
+ status: { padding: "24px 0", color: t.muted, fontSize: 13.5 },
186
+ error: {
187
+ color: t.danger,
188
+ background: tint(t.danger, 8),
189
+ border: `1px solid ${tint(t.danger, 25)}`,
190
+ borderRadius: t.radiusLg,
191
+ padding: "10px 14px",
192
+ marginBottom: 16,
193
+ fontSize: 13.5
194
+ },
195
+ notice: {
196
+ color: t.text,
197
+ background: tint(t.warning, 10),
198
+ border: `1px solid ${tint(t.warning, 35)}`,
199
+ borderRadius: t.radiusLg,
200
+ padding: "10px 14px",
201
+ marginBottom: 16,
202
+ fontSize: 13.5
80
203
  },
81
- status: { padding: 12, color: t.muted },
82
- error: { color: t.danger, marginBottom: 12, fontSize: 14 },
83
204
  ok: { color: t.accentStrong, fontWeight: 600 },
84
205
  bad: { color: t.danger, fontWeight: 600 },
85
206
  advice: { color: t.warning },
86
207
  muted: { color: t.muted, fontSize: 13 },
87
- mono: { fontFamily: t.mono, fontSize: 13 },
88
- table: { width: "100%", borderCollapse: "collapse", fontSize: 14 },
208
+ mono: { fontFamily: t.mono, fontSize: 12.5 },
209
+ code: {
210
+ fontFamily: t.mono,
211
+ fontSize: 12,
212
+ background: t.surfaceAlt,
213
+ border: `1px solid ${t.borderLight}`,
214
+ borderRadius: 4,
215
+ padding: "1px 5px"
216
+ },
217
+ // ── surfaces ──
218
+ // Tables and lists are cards themselves, so every screen gets the framed,
219
+ // rounded look without wrapping each one. The last row's rule is removed in
220
+ // the shell stylesheet (a pseudo-selector); embedded without the shell it
221
+ // simply doubles against the frame.
222
+ table: {
223
+ width: "100%",
224
+ borderCollapse: "separate",
225
+ borderSpacing: 0,
226
+ fontSize: 13.5,
227
+ background: t.surface,
228
+ border: `1px solid ${t.border}`,
229
+ borderRadius: t.radiusLg,
230
+ overflow: "hidden",
231
+ boxShadow: t.shadowCard
232
+ },
89
233
  th: {
90
234
  textAlign: "left",
91
235
  borderBottom: `1px solid ${t.border}`,
92
- padding: "6px 8px",
236
+ padding: "9px 14px",
237
+ fontWeight: 600,
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,
93
277
  fontWeight: 600,
94
- color: t.muted
278
+ letterSpacing: t.trackingDisplay,
279
+ lineHeight: 1.2,
280
+ marginTop: 6
281
+ },
282
+ statHint: { fontSize: 12, color: t.muted, marginTop: 2 },
283
+ empty: {
284
+ background: t.surface,
285
+ border: `1px dashed ${t.border}`,
286
+ borderRadius: t.radiusLg,
287
+ padding: "40px 24px",
288
+ textAlign: "center",
289
+ color: t.muted,
290
+ fontSize: 13.5
291
+ },
292
+ emptyTitle: { color: t.text, fontWeight: 600, fontSize: 14.5, margin: "10px 0 4px" },
293
+ dl: { display: "grid", gridTemplateColumns: "160px 1fr", margin: 0 },
294
+ dt: {
295
+ color: t.muted,
296
+ fontSize: 13,
297
+ padding: "10px 16px",
298
+ borderBottom: `1px solid ${t.borderLight}`
299
+ },
300
+ dd: {
301
+ margin: 0,
302
+ padding: "10px 16px",
303
+ borderBottom: `1px solid ${t.borderLight}`,
304
+ fontSize: 13.5,
305
+ minWidth: 0
95
306
  },
96
- td: { borderBottom: `1px solid ${t.borderLight}`, padding: "6px 8px", verticalAlign: "top" },
97
- list: { listStyle: "none", padding: 0, margin: 0 },
98
- row: { borderBottom: `1px solid ${t.borderLight}`, padding: "8px 0" },
99
307
  badge: {
100
- display: "inline-block",
308
+ display: "inline-flex",
309
+ alignItems: "center",
310
+ gap: 5,
101
311
  borderRadius: 999,
102
- padding: "1px 8px",
103
- fontSize: 12,
312
+ padding: "2px 8px",
313
+ fontSize: 11.5,
314
+ fontWeight: 500,
315
+ lineHeight: 1.5,
104
316
  background: t.surfaceAlt,
105
317
  border: `1px solid ${t.border}`,
106
- color: t.muted
318
+ color: t.muted,
319
+ whiteSpace: "nowrap"
320
+ },
321
+ pill: {
322
+ display: "inline-flex",
323
+ alignItems: "center",
324
+ gap: 6,
325
+ borderRadius: 999,
326
+ padding: "2px 9px 2px 8px",
327
+ fontSize: 12,
328
+ fontWeight: 600,
329
+ lineHeight: 1.5,
330
+ whiteSpace: "nowrap",
331
+ verticalAlign: "baseline"
107
332
  },
333
+ dot: { width: 6, height: 6, borderRadius: 999, flexShrink: 0, background: "currentColor" },
334
+ // ── controls ──
108
335
  button: {
336
+ ...control,
109
337
  background: t.surface,
110
338
  border: `1px solid ${t.border}`,
111
- borderRadius: t.radius,
112
- padding: "4px 10px",
113
- cursor: "pointer",
114
- fontSize: 13,
115
339
  color: t.text,
116
- fontFamily: "inherit",
340
+ cursor: "pointer",
117
341
  boxShadow: t.shadowCard
118
342
  },
119
- toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 12 },
120
- // Surfaces the screens compose themselves, kept here so a card looks the
121
- // same on every page rather than being re-invented per screen.
122
- card: {
343
+ buttonPrimary: {
344
+ ...control,
345
+ background: t.text,
346
+ border: `1px solid ${t.text}`,
347
+ color: t.surface,
348
+ cursor: "pointer"
349
+ },
350
+ buttonDanger: {
351
+ ...control,
123
352
  background: t.surface,
353
+ border: `1px solid ${tint(t.danger, 40)}`,
354
+ color: t.danger,
355
+ cursor: "pointer"
356
+ },
357
+ buttonGhost: {
358
+ ...control,
359
+ background: "none",
360
+ border: "1px solid transparent",
361
+ color: t.muted,
362
+ cursor: "pointer",
363
+ padding: "0 8px"
364
+ },
365
+ link: {
366
+ background: "none",
367
+ border: "none",
368
+ padding: 0,
369
+ color: t.text,
370
+ fontFamily: "inherit",
371
+ fontSize: "inherit",
372
+ fontWeight: 500,
373
+ cursor: "pointer",
374
+ textAlign: "left"
375
+ },
376
+ input: {
377
+ height: 32,
378
+ boxSizing: "border-box",
124
379
  border: `1px solid ${t.border}`,
125
- borderRadius: t.radiusLg,
126
- padding: 16,
127
- boxShadow: t.shadowCard
380
+ borderRadius: 6,
381
+ padding: "0 10px",
382
+ fontSize: 13.5,
383
+ fontFamily: "inherit",
384
+ background: t.surface,
385
+ color: t.text,
386
+ minWidth: 0
128
387
  },
388
+ toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 16, flexWrap: "wrap" },
129
389
  accent: { color: t.accent }
130
390
  };
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(
@@ -499,11 +1147,12 @@ var MigrationsScreen = defineComponent7({
499
1147
  const r = report.value;
500
1148
  if (!r) return null;
501
1149
  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.");
1150
+ if (behind.length === 0)
1151
+ return empty("Every module is up to date", "No pending migrations.", "migrations");
503
1152
  return [
504
- r.everApplied ? null : h8(
1153
+ r.everApplied ? null : h9(
505
1154
  "p",
506
- { style: styles.advice },
1155
+ { style: styles.notice },
507
1156
  "This database has never been migrated \u2014 treating it as a first install, so nothing is held back."
508
1157
  ),
509
1158
  ...behind.map((m) => section(m, r.everApplied))
@@ -511,6 +1160,7 @@ var MigrationsScreen = defineComponent7({
511
1160
  },
512
1161
  [refreshButton("Refresh", isLoading.value, () => void refresh())],
513
1162
  {
1163
+ lead: "Schema changes each module ships, and whether this database has them.",
514
1164
  errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
515
1165
  }
516
1166
  );
@@ -519,50 +1169,89 @@ var MigrationsScreen = defineComponent7({
519
1169
 
520
1170
  // src/screens/TokensScreen.ts
521
1171
  import { useAdminTokens } from "@fonderie/vue-admin";
522
- import { defineComponent as defineComponent8, h as h9, ref } from "vue";
1172
+ import { defineComponent as defineComponent8, h as h10, ref as ref2 } from "vue";
523
1173
  var ALL = ["read", "write", "secrets"];
524
1174
  var TokensScreen = defineComponent8({
525
1175
  name: "FonderieTokensScreen",
526
1176
  props: { client: { type: Object, required: true } },
527
1177
  setup(props) {
528
1178
  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);
1179
+ const name = ref2("");
1180
+ const scopes = ref2(["read"]);
1181
+ const days = ref2("");
1182
+ const minted = ref2(null);
533
1183
  const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
534
1184
  return () => {
535
1185
  const r = report.value;
536
- return h9("div", { style: styles.container }, [
537
- h9("h1", { style: styles.title }, "Access"),
538
- error.value ? h9(
1186
+ return h10("div", { style: styles.container }, [
1187
+ pageHeader(
1188
+ "Access",
1189
+ "Who can open this surface: the root token's strength, scoped tokens you have issued, and any legacy per-module tokens."
1190
+ ),
1191
+ error.value ? h10(
539
1192
  "p",
540
1193
  { style: styles.error, role: "alert" },
541
1194
  error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
542
1195
  ) : 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(
1196
+ h10("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
1197
+ r ? h10(
1198
+ "div",
1199
+ { style: { ...styles.card, display: "flex", gap: "12px", alignItems: "flex-start" } },
1200
+ [
1201
+ icon("lock", 18, { marginTop: "2px" }),
1202
+ h10("div", [
1203
+ pill(r.admin.ok ? "ok" : "bad", r.admin.ok ? "strong" : "weak"),
1204
+ ...r.admin.problems.map(
1205
+ (p) => h10(
1206
+ "div",
1207
+ {
1208
+ key: p.message,
1209
+ style: {
1210
+ marginTop: "6px",
1211
+ color: "var(--fonderie-danger,#e00)",
1212
+ fontSize: "13px"
1213
+ }
1214
+ },
1215
+ p.message
1216
+ )
1217
+ )
1218
+ ])
1219
+ ]
1220
+ ) : null,
1221
+ h10("h2", { style: styles.subtitle }, "Issued tokens"),
1222
+ r?.issued === null ? h10(
556
1223
  "p",
557
1224
  { style: styles.muted },
558
1225
  "Issuing is off \u2014 give AdminModule a store and run its migrations."
559
1226
  ) : [
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(
1227
+ minted.value ? h10(
1228
+ "div",
1229
+ {
1230
+ style: {
1231
+ ...styles.notice,
1232
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1233
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1234
+ }
1235
+ },
1236
+ [
1237
+ h10("strong", "Copy this now \u2014 it is never shown again."),
1238
+ ` (${minted.value.name})`,
1239
+ h10(
1240
+ "div",
1241
+ {
1242
+ style: {
1243
+ ...styles.code,
1244
+ display: "block",
1245
+ marginTop: "8px",
1246
+ padding: "8px 10px",
1247
+ wordBreak: "break-all"
1248
+ }
1249
+ },
1250
+ minted.value.token
1251
+ )
1252
+ ]
1253
+ ) : null,
1254
+ h10(
566
1255
  "form",
567
1256
  {
568
1257
  style: styles.toolbar,
@@ -582,49 +1271,60 @@ var TokensScreen = defineComponent8({
582
1271
  }
583
1272
  },
584
1273
  [
585
- h9("input", {
1274
+ h10("input", {
586
1275
  value: name.value,
587
1276
  onInput: (e) => name.value = e.target.value,
588
1277
  placeholder: "name",
589
- style: { ...styles.button, cursor: "text" },
1278
+ style: styles.input,
590
1279
  "aria-label": "Name"
591
1280
  }),
592
1281
  ...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
- ])
1282
+ (s) => h10(
1283
+ "label",
1284
+ {
1285
+ key: s,
1286
+ style: {
1287
+ ...styles.badge,
1288
+ cursor: "pointer",
1289
+ height: "32px",
1290
+ boxSizing: "border-box",
1291
+ padding: "0 10px",
1292
+ fontSize: "13px"
1293
+ }
1294
+ },
1295
+ [
1296
+ h10("input", {
1297
+ type: "checkbox",
1298
+ checked: scopes.value.includes(s),
1299
+ onChange: () => toggle(s)
1300
+ }),
1301
+ ` ${s}`
1302
+ ]
1303
+ )
601
1304
  ),
602
- h9("input", {
1305
+ h10("input", {
603
1306
  value: days.value,
604
1307
  onInput: (e) => days.value = e.target.value,
605
1308
  placeholder: "days (optional)",
606
- style: { ...styles.button, cursor: "text", width: "140px" },
1309
+ style: { ...styles.input, width: "140px" },
607
1310
  "aria-label": "Days"
608
1311
  }),
609
- h9(
1312
+ h10(
610
1313
  "button",
611
1314
  {
612
1315
  type: "submit",
613
- style: styles.button,
1316
+ style: styles.buttonPrimary,
614
1317
  disabled: !name.value.trim() || scopes.value.length === 0 || isLoading.value
615
1318
  },
616
- "Issue"
1319
+ [icon("plus", 14), "Issue token"]
617
1320
  )
618
1321
  ]
619
1322
  ),
620
1323
  r?.issued && r.issued.length > 0 ? table(
621
1324
  ["Name", "Scopes", "Created", "Expires", "Last used", ""],
622
1325
  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
- ]),
1326
+ (t2) => h10("tr", { key: t2.id }, [
1327
+ td([t2.name, t2.revokedAt ? [" ", pill("neutral", "revoked")] : ""]),
628
1328
  td(t2.scopes.join(", "), styles.mono),
629
1329
  td(
630
1330
  `${new Date(t2.createdAt).toLocaleDateString()} \xB7 ${t2.createdBy}`,
@@ -639,11 +1339,11 @@ var TokensScreen = defineComponent8({
639
1339
  styles.muted
640
1340
  ),
641
1341
  td(
642
- t2.revokedAt ? "" : h9(
1342
+ t2.revokedAt ? "" : h10(
643
1343
  "button",
644
1344
  {
645
1345
  type: "button",
646
- style: styles.button,
1346
+ style: styles.buttonDanger,
647
1347
  onClick: () => {
648
1348
  if (window.confirm(
649
1349
  `Revoke "${t2.name}"? Anything using it stops working immediately.`
@@ -657,21 +1357,25 @@ var TokensScreen = defineComponent8({
657
1357
  )
658
1358
  ])
659
1359
  )
660
- ) : h9("p", { style: styles.muted }, "None issued.")
1360
+ ) : empty(
1361
+ "No scoped tokens yet",
1362
+ "Issue a read-only token for a dashboard or a teammate instead of sharing the root.",
1363
+ "tokens"
1364
+ )
661
1365
  ],
662
- h9("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
663
- r && r.legacy.length === 0 ? h9(
1366
+ h10("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
1367
+ r && r.legacy.length === 0 ? h10(
664
1368
  "p",
665
1369
  { style: styles.muted },
666
1370
  "None \u2014 every brick's admin surface goes through this token."
667
- ) : h9(
1371
+ ) : h10(
668
1372
  "ul",
669
1373
  { style: styles.list },
670
1374
  (r?.legacy ?? []).map(
671
- (l) => h9("li", { key: l.module, style: styles.row }, [
672
- h9("span", { style: styles.mono }, l.module),
1375
+ (l) => h10("li", { key: l.module, style: styles.row }, [
1376
+ h10("span", { style: styles.mono }, l.module),
673
1377
  " ",
674
- h9(
1378
+ h10(
675
1379
  "span",
676
1380
  { style: styles.muted },
677
1381
  "still registers its standalone routes with its own token (deprecated)"
@@ -692,7 +1396,7 @@ import {
692
1396
  useAdminUserSessions,
693
1397
  useAdminUsers
694
1398
  } from "@fonderie/vue-admin";
695
- import { computed, defineComponent as defineComponent9, h as h10, ref as ref2 } from "vue";
1399
+ import { computed, defineComponent as defineComponent9, h as h11, ref as ref3 } from "vue";
696
1400
  var UsersScreen = defineComponent9({
697
1401
  name: "FonderieUsersScreen",
698
1402
  props: {
@@ -700,9 +1404,9 @@ var UsersScreen = defineComponent9({
700
1404
  pageSize: { type: Number, default: 50 }
701
1405
  },
702
1406
  setup(props) {
703
- const input = ref2("");
704
- const email = ref2("");
705
- const selectedId = ref2("");
1407
+ const input = ref3("");
1408
+ const email = ref3("");
1409
+ const selectedId = ref3("");
706
1410
  const list = useAdminUsers(props.client, { limit: props.pageSize });
707
1411
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
708
1412
  props.client,
@@ -712,8 +1416,8 @@ var UsersScreen = defineComponent9({
712
1416
  const sessions = useAdminUserSessions(props.client, userId);
713
1417
  const history = useAdminLoginHistory(props.client, userId, { limit: 20 });
714
1418
  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)]);
1419
+ const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
1420
+ const row = (k, v) => h11("tr", [td(k), td(v)]);
717
1421
  const clear = () => {
718
1422
  email.value = "";
719
1423
  selectedId.value = "";
@@ -721,9 +1425,12 @@ var UsersScreen = defineComponent9({
721
1425
  };
722
1426
  return () => {
723
1427
  const u = user.value;
724
- return h10("div", { style: styles.container }, [
725
- h10("h1", { style: styles.title }, "Users"),
726
- h10(
1428
+ return h11("div", { style: styles.container }, [
1429
+ pageHeader(
1430
+ "Users",
1431
+ "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1432
+ ),
1433
+ h11(
727
1434
  "form",
728
1435
  {
729
1436
  style: styles.toolbar,
@@ -734,75 +1441,67 @@ var UsersScreen = defineComponent9({
734
1441
  }
735
1442
  },
736
1443
  [
737
- h10("input", {
1444
+ h11("input", {
738
1445
  type: "email",
739
1446
  value: input.value,
740
1447
  onInput: (e) => input.value = e.target.value,
741
1448
  placeholder: "email address",
742
- style: { ...styles.button, cursor: "text", minWidth: "280px" },
1449
+ style: { ...styles.input, minWidth: "280px" },
743
1450
  "aria-label": "Email"
744
1451
  }),
745
- h10(
1452
+ h11(
746
1453
  "button",
747
- { type: "submit", style: styles.button, disabled: isLoading.value },
748
- "Look up"
1454
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1455
+ [icon("search", 14), "Look up"]
749
1456
  ),
750
- showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : refreshButton("\u2190 All users", false, clear)
1457
+ showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
751
1458
  ]
752
1459
  ),
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(
1460
+ showList.value ? h11("div", [
1461
+ list.error.value ? h11("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1462
+ list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
756
1463
  ["Email", "Name", "Created", "Status"],
757
1464
  list.users.value.map(
758
- (u2) => h10("tr", { key: u2.id }, [
1465
+ (u2) => h11("tr", { key: u2.id }, [
759
1466
  td(
760
- h10(
1467
+ h11(
761
1468
  "button",
762
1469
  {
763
1470
  type: "button",
764
- style: { ...styles.navItem, padding: 0 },
1471
+ style: styles.link,
765
1472
  onClick: () => selectedId.value = u2.id
766
1473
  },
767
1474
  u2.email
768
1475
  )
769
1476
  ),
770
1477
  td(
771
- `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h10("span", { style: styles.muted }, "\u2014")
1478
+ `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || h11("span", { style: styles.muted }, "\u2014")
772
1479
  ),
773
1480
  td(new Date(u2.createdAt).toLocaleDateString()),
774
1481
  td(
775
- u2.suspended ? h10("span", { style: styles.badge }, "suspended") : u2.deletedAt ? h10("span", { style: styles.badge }, "deleted") : h10("span", { style: styles.muted }, "active")
1482
+ u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
776
1483
  )
777
1484
  ])
778
1485
  )
779
1486
  ),
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
1487
+ list.isLoading.value ? h11("p", { style: styles.status }, "Loading\u2026") : null,
1488
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
790
1489
  ]) : null,
791
- error.value ? h10(
1490
+ error.value ? h11(
792
1491
  "p",
793
1492
  { style: styles.error, role: "alert" },
794
1493
  error.value.status === 404 ? "No user with that email." : error.value.explanation
795
1494
  ) : null,
796
1495
  u ? [
797
- h10("h2", { style: styles.subtitle }, [
1496
+ h11("h2", { style: styles.subtitle }, [
798
1497
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
799
1498
  " ",
800
- u.suspended ? h10("span", { style: styles.badge }, "suspended") : null,
801
- u.deletedAt ? h10("span", { style: styles.badge }, "deleted") : null
1499
+ u.suspended ? pill("warn", "suspended") : null,
1500
+ u.deletedAt ? pill("neutral", "deleted") : null
802
1501
  ]),
803
- h10("table", { style: styles.table }, [
804
- h10("tbody", [
805
- row("id", h10("span", { style: styles.mono }, u.id)),
1502
+ h11("table", { style: styles.table }, [
1503
+ h11("tbody", [
1504
+ row("id", h11("span", { style: styles.mono }, u.id)),
806
1505
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
807
1506
  row("MFA", yesNo(u.mfaEnabled)),
808
1507
  row(
@@ -811,32 +1510,37 @@ var UsersScreen = defineComponent9({
811
1510
  ),
812
1511
  row(
813
1512
  "last login",
814
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h10("span", { style: styles.muted }, "never")
1513
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : h11("span", { style: styles.muted }, "never")
815
1514
  ),
816
1515
  row("created", new Date(u.createdAt).toLocaleString())
817
1516
  ])
818
1517
  ]),
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(
1518
+ h11("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1519
+ u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1520
+ "Suspend",
1521
+ isLoading.value,
1522
+ () => void suspend(),
1523
+ styles.buttonDanger
1524
+ ),
1525
+ actionButton(
822
1526
  "Sign out everywhere",
823
1527
  isLoading.value,
824
1528
  () => void revokeSessions().then(() => sessions.refresh())
825
1529
  )
826
1530
  ]),
827
- h10("h2", { style: styles.subtitle }, "Live sessions"),
828
- sessions.sessions.value.length === 0 ? h10("p", { style: styles.muted }, "None.") : h10(
1531
+ h11("h2", { style: styles.subtitle }, "Live sessions"),
1532
+ sessions.sessions.value.length === 0 ? h11("p", { style: styles.muted }, "None.") : h11(
829
1533
  "ul",
830
1534
  { style: styles.list },
831
1535
  sessions.sessions.value.map(
832
- (s) => h10("li", { key: s.id, style: styles.row }, [
833
- h10("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1536
+ (s) => h11("li", { key: s.id, style: styles.row }, [
1537
+ h11("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
834
1538
  " ",
835
- h10("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
1539
+ h11("span", { style: styles.muted }, describeLocation(s.location) ?? ""),
836
1540
  " ",
837
- h10("span", { style: styles.muted }, s.userAgent ?? ""),
1541
+ h11("span", { style: styles.muted }, s.userAgent ?? ""),
838
1542
  " ",
839
- h10(
1543
+ h11(
840
1544
  "span",
841
1545
  { style: styles.muted },
842
1546
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -844,38 +1548,26 @@ var UsersScreen = defineComponent9({
844
1548
  ])
845
1549
  )
846
1550
  ),
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(
1551
+ h11("h2", { style: styles.subtitle }, "Recent sign-ins"),
1552
+ history.events.value.length === 0 && !history.isLoading.value ? h11("p", { style: styles.muted }, "None recorded.") : h11(
849
1553
  "ul",
850
1554
  { style: styles.list },
851
1555
  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
- ),
1556
+ (e) => h11("li", { key: e.id, style: styles.row }, [
1557
+ pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
858
1558
  " ",
859
- h10("span", { style: styles.muted }, e.method),
1559
+ h11("span", { style: styles.muted }, e.method),
860
1560
  " ",
861
- h10("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1561
+ h11("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
862
1562
  " ",
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") : "",
1563
+ h11("span", { style: styles.muted }, describeLocation(e.location) ?? ""),
1564
+ e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
865
1565
  " ",
866
- h10("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1566
+ h11("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
867
1567
  ])
868
1568
  )
869
1569
  ),
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
1570
+ history.hasMore.value && !history.isLoading.value ? loadMoreButton(() => void history.loadMore()) : null
879
1571
  ] : null
880
1572
  ]);
881
1573
  };
@@ -884,7 +1576,7 @@ var UsersScreen = defineComponent9({
884
1576
 
885
1577
  // src/screens/CatalogScreen.ts
886
1578
  import { useAdminCatalog } from "@fonderie/vue-admin";
887
- import { defineComponent as defineComponent10, h as h11 } from "vue";
1579
+ import { defineComponent as defineComponent10, h as h12 } from "vue";
888
1580
  var CatalogScreen = defineComponent10({
889
1581
  name: "FonderieCatalogScreen",
890
1582
  props: { client: { type: Object, required: true } },
@@ -893,35 +1585,42 @@ var CatalogScreen = defineComponent10({
893
1585
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
894
1586
  return () => {
895
1587
  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,
1588
+ return h12("div", { style: styles.container }, [
1589
+ pageHeader(
1590
+ "Catalog",
1591
+ "What you sell: plans as configured in code, and as stored in the database, side by side.",
1592
+ [refreshButton("Refresh", isLoading.value, () => void refresh())]
1593
+ ),
1594
+ error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1595
+ isLoading.value && !c ? h12("p", { style: styles.status }, "Loading\u2026") : null,
903
1596
  c ? [
904
- h11("h2", { style: styles.subtitle }, "Configured (code)"),
905
- h11(
1597
+ h12("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1598
+ h12(
906
1599
  "pre",
907
1600
  {
908
1601
  style: {
1602
+ ...styles.card,
909
1603
  ...styles.mono,
910
- background: "var(--fonderie-surface-alt,#fafafa)",
911
- padding: "12px",
912
- overflowX: "auto"
1604
+ margin: 0,
1605
+ overflowX: "auto",
1606
+ maxHeight: "360px",
1607
+ lineHeight: 1.6
913
1608
  }
914
1609
  },
915
1610
  JSON.stringify(c.configured, null, 2)
916
1611
  ),
917
- h11("h2", { style: styles.subtitle }, "Stored (database)"),
918
- c.stored.length === 0 ? h11("p", { style: styles.muted }, "No stored plans.") : table(
1612
+ h12("h2", { style: styles.subtitle }, "Stored (database)"),
1613
+ c.stored.length === 0 ? empty(
1614
+ "No stored plans",
1615
+ "Plans are configured in code; nothing has been written to the database.",
1616
+ "catalog"
1617
+ ) : table(
919
1618
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
920
1619
  c.stored.map(
921
- (p) => h11("tr", { key: p.id }, [
1620
+ (p) => h12("tr", { key: p.id }, [
922
1621
  td([
923
- h11("strong", p.name),
924
- p.description ? h11("div", { style: styles.muted }, p.description) : ""
1622
+ h12("strong", p.name),
1623
+ p.description ? h12("div", { style: styles.muted }, p.description) : ""
925
1624
  ]),
926
1625
  td(String(p.tier)),
927
1626
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -929,11 +1628,11 @@ var CatalogScreen = defineComponent10({
929
1628
  td(money(p.pricing?.yearly)),
930
1629
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
931
1630
  td(
932
- h11(
1631
+ h12(
933
1632
  "button",
934
1633
  {
935
1634
  type: "button",
936
- style: styles.button,
1635
+ style: styles.buttonDanger,
937
1636
  onClick: () => {
938
1637
  if (window.confirm(`Delete stored plan "${p.name}"?`))
939
1638
  void deletePlan(p.id).catch(() => {
@@ -954,7 +1653,7 @@ var CatalogScreen = defineComponent10({
954
1653
 
955
1654
  // src/screens/SubscriberScreen.ts
956
1655
  import { useAdminSubscriber, useAdminSubscribers } from "@fonderie/vue-admin";
957
- import { defineComponent as defineComponent11, h as h12, ref as ref3 } from "vue";
1656
+ import { defineComponent as defineComponent11, h as h13, ref as ref4 } from "vue";
958
1657
  var SubscriberScreen = defineComponent11({
959
1658
  name: "FonderieSubscriberScreen",
960
1659
  props: {
@@ -962,9 +1661,9 @@ var SubscriberScreen = defineComponent11({
962
1661
  pageSize: { type: Number, default: 50 }
963
1662
  },
964
1663
  setup(props) {
965
- const type = ref3("user");
966
- const idInput = ref3("");
967
- const subscriber = ref3(null);
1664
+ const type = ref4("user");
1665
+ const idInput = ref4("");
1666
+ const subscriber = ref4(null);
968
1667
  const list = useAdminSubscribers(props.client, { limit: props.pageSize });
969
1668
  const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(props.client, subscriber, { limit: 20 });
970
1669
  const open = (t2, id) => {
@@ -973,22 +1672,25 @@ var SubscriberScreen = defineComponent11({
973
1672
  idInput.value = id;
974
1673
  subscriber.value = { type: t2, id };
975
1674
  };
976
- const amount = ref3("");
977
- const note = ref3("");
978
- const granted = ref3(null);
979
- const input = (model, placeholder, extra = {}) => h12("input", {
1675
+ const amount = ref4("");
1676
+ const note = ref4("");
1677
+ const granted = ref4(null);
1678
+ const input = (model, placeholder, extra = {}) => h13("input", {
980
1679
  value: model.value,
981
1680
  onInput: (e) => model.value = e.target.value,
982
1681
  placeholder,
983
- style: { ...styles.button, cursor: "text", ...extra },
1682
+ style: { ...styles.input, ...extra },
984
1683
  "aria-label": placeholder
985
1684
  });
986
1685
  return () => {
987
1686
  const s = subscription.value;
988
1687
  const w = wallet.value;
989
- return h12("div", { style: styles.container }, [
990
- h12("h1", { style: styles.title }, "Subscriber"),
991
- h12(
1688
+ return h13("div", { style: styles.container }, [
1689
+ pageHeader(
1690
+ "Subscribers",
1691
+ "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
1692
+ ),
1693
+ h13(
992
1694
  "form",
993
1695
  {
994
1696
  style: styles.toolbar,
@@ -999,26 +1701,26 @@ var SubscriberScreen = defineComponent11({
999
1701
  }
1000
1702
  },
1001
1703
  [
1002
- h12(
1704
+ h13(
1003
1705
  "select",
1004
1706
  {
1005
1707
  value: type.value,
1006
1708
  onChange: (e) => type.value = e.target.value,
1007
- style: styles.button,
1709
+ style: styles.input,
1008
1710
  "aria-label": "Type"
1009
1711
  },
1010
1712
  [
1011
- h12("option", { value: "user" }, "user"),
1012
- h12("option", { value: "workspace" }, "workspace")
1713
+ h13("option", { value: "user" }, "user"),
1714
+ h13("option", { value: "workspace" }, "workspace")
1013
1715
  ]
1014
1716
  ),
1015
1717
  input(idInput, "subscriber id", { minWidth: "280px" }),
1016
- h12(
1718
+ h13(
1017
1719
  "button",
1018
- { type: "submit", style: styles.button, disabled: isLoading.value },
1019
- "Look up"
1720
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1721
+ [icon("search", 14), "Look up"]
1020
1722
  ),
1021
- subscriber.value ? h12(
1723
+ subscriber.value ? h13(
1022
1724
  "button",
1023
1725
  {
1024
1726
  type: "button",
@@ -1029,8 +1731,8 @@ var SubscriberScreen = defineComponent11({
1029
1731
  granted.value = null;
1030
1732
  }
1031
1733
  },
1032
- "\u2190 All subscribers"
1033
- ) : h12(
1734
+ [icon("back", 14), "All subscribers"]
1735
+ ) : h13(
1034
1736
  "button",
1035
1737
  {
1036
1738
  type: "button",
@@ -1042,35 +1744,43 @@ var SubscriberScreen = defineComponent11({
1042
1744
  )
1043
1745
  ]
1044
1746
  ),
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(
1747
+ !subscriber.value ? h13("div", [
1748
+ list.error.value ? h13("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1749
+ list.subscriptions.value.length === 0 && !list.isLoading.value ? empty(
1750
+ "No subscribers yet",
1751
+ "Subscriptions appear here once someone checks out.",
1752
+ "subscriber"
1753
+ ) : table(
1048
1754
  ["Subscriber", "Plan", "Status", "Renews"],
1049
1755
  list.subscriptions.value.map(
1050
- (sub) => h12("tr", { key: sub.id }, [
1756
+ (sub) => h13("tr", { key: sub.id }, [
1051
1757
  td(
1052
- h12(
1758
+ h13(
1053
1759
  "button",
1054
1760
  {
1055
1761
  type: "button",
1056
- style: { ...styles.navItem, padding: 0, ...styles.mono },
1762
+ style: { ...styles.link, ...styles.mono },
1057
1763
  onClick: () => open(sub.subscriberType, sub.subscriberId)
1058
1764
  },
1059
1765
  `${sub.subscriberType}/${sub.subscriberId}`
1060
1766
  )
1061
1767
  ),
1062
1768
  td(`${sub.plan} \xB7 ${sub.interval}`),
1769
+ td([
1770
+ pill(
1771
+ sub.status === "active" ? "ok" : sub.status === "trialing" ? "info" : sub.status === "past_due" ? "warn" : "neutral",
1772
+ sub.status
1773
+ ),
1774
+ sub.cancelAtPeriodEnd ? [" ", pill("warn", "cancels", false)] : null
1775
+ ]),
1063
1776
  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")
1777
+ sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : h13("span", { style: styles.muted }, "\u2014")
1068
1778
  )
1069
1779
  ])
1070
1780
  )
1071
1781
  ),
1072
- list.isLoading.value ? h12("p", { style: styles.status }, "Loading\u2026") : null,
1073
- list.hasMore.value && !list.isLoading.value ? h12(
1782
+ list.isLoading.value ? h13("p", { style: styles.status }, "Loading\u2026") : null,
1783
+ list.hasMore.value && !list.isLoading.value ? h13(
1074
1784
  "button",
1075
1785
  {
1076
1786
  type: "button",
@@ -1080,37 +1790,58 @@ var SubscriberScreen = defineComponent11({
1080
1790
  "Load more"
1081
1791
  ) : null
1082
1792
  ]) : null,
1083
- error.value ? h12("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1793
+ error.value ? h13("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1084
1794
  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", [
1795
+ h13("h2", { style: styles.subtitle }, "Subscription"),
1796
+ s ? h13("table", { style: styles.table }, [
1797
+ h13("tbody", [
1798
+ h13("tr", [td("plan"), td([h13("strong", s.plan), ` \xB7 ${s.interval}`])]),
1799
+ h13("tr", [
1090
1800
  td("status"),
1091
1801
  td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
1092
1802
  ]),
1093
- h12("tr", [
1803
+ h13("tr", [
1094
1804
  td("period"),
1095
1805
  td(
1096
1806
  `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
1097
1807
  )
1098
1808
  ]),
1099
- h12("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
1809
+ h13("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
1100
1810
  ])
1101
- ]) : h12("p", { style: styles.muted }, "No subscription."),
1102
- h12("h2", { style: styles.subtitle }, "Wallet"),
1811
+ ]) : h13("p", { style: styles.muted }, "No subscription."),
1812
+ h13("h2", { style: styles.subtitle }, "Wallet"),
1103
1813
  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(
1814
+ h13(
1815
+ "p",
1816
+ {
1817
+ style: {
1818
+ ...styles.card,
1819
+ display: "flex",
1820
+ alignItems: "baseline",
1821
+ gap: "8px",
1822
+ flexWrap: "wrap"
1823
+ }
1824
+ },
1825
+ [
1826
+ h13(
1827
+ "strong",
1828
+ {
1829
+ style: {
1830
+ fontSize: "22px",
1831
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
1832
+ }
1833
+ },
1834
+ w.balance
1835
+ ),
1836
+ ` ${w.currency} `,
1837
+ h13(
1838
+ "span",
1839
+ { style: styles.muted },
1840
+ `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
1841
+ )
1842
+ ]
1843
+ ),
1844
+ h13(
1114
1845
  "form",
1115
1846
  {
1116
1847
  style: styles.toolbar,
@@ -1135,23 +1866,23 @@ var SubscriberScreen = defineComponent11({
1135
1866
  [
1136
1867
  input(amount, "amount (minor units)"),
1137
1868
  input(note, "reason (optional)", { minWidth: "220px" }),
1138
- h12(
1869
+ h13(
1139
1870
  "button",
1140
1871
  {
1141
1872
  type: "submit",
1142
- style: styles.button,
1873
+ style: styles.buttonPrimary,
1143
1874
  disabled: !/^\d+$/.test(amount.value.trim())
1144
1875
  },
1145
- "Grant"
1876
+ [icon("plus", 14), "Grant"]
1146
1877
  ),
1147
- granted.value ? h12("span", { style: styles.ok }, granted.value) : null
1878
+ granted.value ? h13("span", { style: styles.ok }, granted.value) : null
1148
1879
  ]
1149
1880
  ),
1150
- h12("h2", { style: styles.subtitle }, "Ledger"),
1151
- ledger.value.length === 0 ? h12("p", { style: styles.muted }, "Nothing moved yet.") : table(
1881
+ h13("h2", { style: styles.subtitle }, "Ledger"),
1882
+ ledger.value.length === 0 ? empty("Nothing moved yet", void 0, "subscriber") : table(
1152
1883
  ["When", "Kind", "Amount", "Note"],
1153
1884
  ledger.value.map(
1154
- (t2) => h12("tr", { key: t2.id }, [
1885
+ (t2) => h13("tr", { key: t2.id }, [
1155
1886
  td(new Date(t2.createdAt).toLocaleString(), styles.muted),
1156
1887
  td(t2.type),
1157
1888
  td(t2.amount, styles.mono),
@@ -1159,7 +1890,7 @@ var SubscriberScreen = defineComponent11({
1159
1890
  ])
1160
1891
  )
1161
1892
  ),
1162
- hasMoreLedger.value ? h12(
1893
+ hasMoreLedger.value ? h13(
1163
1894
  "button",
1164
1895
  {
1165
1896
  type: "button",
@@ -1168,7 +1899,7 @@ var SubscriberScreen = defineComponent11({
1168
1899
  },
1169
1900
  "Load more"
1170
1901
  ) : null
1171
- ] : h12(
1902
+ ] : h13(
1172
1903
  "p",
1173
1904
  { style: styles.muted },
1174
1905
  "No wallet \u2014 billing has no wallet configuration."
@@ -1181,7 +1912,7 @@ var SubscriberScreen = defineComponent11({
1181
1912
 
1182
1913
  // src/screens/AuditScreen.ts
1183
1914
  import { useAdminAudit } from "@fonderie/vue-admin";
1184
- import { defineComponent as defineComponent12, h as h13, ref as ref4 } from "vue";
1915
+ import { defineComponent as defineComponent12, h as h14, ref as ref5 } from "vue";
1185
1916
  var AuditScreen = defineComponent12({
1186
1917
  name: "FonderieAuditScreen",
1187
1918
  props: {
@@ -1189,22 +1920,25 @@ var AuditScreen = defineComponent12({
1189
1920
  pageSize: { type: Number, default: 50 }
1190
1921
  },
1191
1922
  setup(props) {
1192
- const draft = ref4({ workspaceId: "", type: "", actorId: "" });
1193
- const query = ref4({ limit: props.pageSize });
1923
+ const draft = ref5({ workspaceId: "", type: "", actorId: "" });
1924
+ const query = ref5({ limit: props.pageSize });
1194
1925
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(
1195
1926
  props.client,
1196
1927
  query
1197
1928
  );
1198
- const field = (key, placeholder) => h13("input", {
1929
+ const field = (key, placeholder) => h14("input", {
1199
1930
  value: draft.value[key],
1200
1931
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
1201
1932
  placeholder,
1202
- style: { ...styles.button, cursor: "text" },
1933
+ style: styles.input,
1203
1934
  "aria-label": placeholder
1204
1935
  });
1205
- return () => h13("div", { style: styles.container }, [
1206
- h13("h1", { style: styles.title }, "Audit"),
1207
- h13(
1936
+ return () => h14("div", { style: styles.container }, [
1937
+ pageHeader(
1938
+ "Audit",
1939
+ "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
1940
+ ),
1941
+ h14(
1208
1942
  "form",
1209
1943
  {
1210
1944
  style: styles.toolbar,
@@ -1223,19 +1957,19 @@ var AuditScreen = defineComponent12({
1223
1957
  field("workspaceId", "workspace id (all if empty)"),
1224
1958
  field("type", "event type"),
1225
1959
  field("actorId", "actor id"),
1226
- h13(
1960
+ h14(
1227
1961
  "button",
1228
- { type: "submit", style: styles.button, disabled: isLoading.value },
1229
- "Filter"
1962
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1963
+ [icon("search", 14), "Filter"]
1230
1964
  ),
1231
1965
  refreshButton("Refresh", isLoading.value, () => void refresh())
1232
1966
  ]
1233
1967
  ),
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(
1968
+ error.value ? h14("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1969
+ events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
1236
1970
  ["When", "Type", "Workspace", "Actor", "Request"],
1237
1971
  events.value.map(
1238
- (e) => h13("tr", { key: e.id }, [
1972
+ (e) => h14("tr", { key: e.id }, [
1239
1973
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
1240
1974
  td(e.type, styles.mono),
1241
1975
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -1244,16 +1978,8 @@ var AuditScreen = defineComponent12({
1244
1978
  ])
1245
1979
  )
1246
1980
  ),
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
1981
+ isLoading.value ? h14("p", { style: styles.status }, "Loading\u2026") : null,
1982
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
1257
1983
  ]);
1258
1984
  }
1259
1985
  });
@@ -1290,6 +2016,23 @@ var NAV = [
1290
2016
  ]
1291
2017
  }
1292
2018
  ];
2019
+ var ICON = {
2020
+ attention: "attention",
2021
+ modules: "modules",
2022
+ environment: "environment",
2023
+ doctor: "doctor",
2024
+ routes: "routes",
2025
+ users: "users",
2026
+ catalog: "catalog",
2027
+ subscriber: "subscriber",
2028
+ settings: "settings",
2029
+ templates: "templates",
2030
+ audit: "audit",
2031
+ log: "log",
2032
+ tokens: "tokens",
2033
+ migrations: "migrations"
2034
+ };
2035
+ var NARROW = "(max-width: 820px)";
1293
2036
  var AdminShell = defineComponent13({
1294
2037
  name: "FonderieAdminShell",
1295
2038
  props: {
@@ -1307,75 +2050,75 @@ var AdminShell = defineComponent13({
1307
2050
  environment: { type: String, default: void 0 },
1308
2051
  // Controlled navigation: pass `page` and listen to `navigate` to own the
1309
2052
  // URL. Omit `page` and the shell keeps it itself.
1310
- page: { type: String, default: void 0 }
2053
+ page: { type: String, default: void 0 },
2054
+ // Shown at the top of the sidebar. Defaults to "Admin".
2055
+ appName: { type: String, default: "Admin" },
2056
+ // A badge beside the name — the deployment's environment ("production").
2057
+ // Production is tinted so an operator always knows where their clicks land.
2058
+ envLabel: { type: String, default: void 0 }
1311
2059
  },
2060
+ // `footer` slot: pinned to the bottom of the sidebar — session controls
2061
+ // (theme, sign out). The React shell takes the same thing as a `footer` prop.
1312
2062
  emits: { navigate: (_page) => true },
1313
- setup(props, { emit }) {
1314
- const own = ref5("attention");
2063
+ setup(props, { emit, slots }) {
2064
+ const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2065
+ const narrow = ref6(query?.matches ?? false);
2066
+ const drawer = ref6(false);
2067
+ const onMedia = (e) => narrow.value = e.matches;
2068
+ onMounted(() => query?.addEventListener("change", onMedia));
2069
+ onBeforeUnmount(() => query?.removeEventListener("change", onMedia));
2070
+ const own = ref6("attention");
1315
2071
  const current = computed2(() => props.page ?? own.value);
1316
- const editing = ref5(null);
2072
+ const editing = ref6(null);
1317
2073
  const go = (p) => {
1318
2074
  editing.value = null;
1319
2075
  emit("navigate", p);
1320
2076
  if (props.page === void 0) own.value = p;
2077
+ drawer.value = false;
1321
2078
  };
1322
- const back = () => h14(
1323
- "button",
1324
- { type: "button", style: styles.button, onClick: () => editing.value = null },
1325
- "\u2190 Back"
1326
- );
2079
+ const back = (label) => h15("div", { style: { padding: "24px 40px 0" } }, [
2080
+ h15(
2081
+ "button",
2082
+ { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2083
+ [icon("back", 14), label]
2084
+ )
2085
+ ]);
2086
+ const missing = (text) => h15("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
1327
2087
  const body = () => {
1328
2088
  const c = props.client;
1329
2089
  switch (current.value) {
1330
2090
  case "attention":
1331
- return h14(AttentionScreen, { client: c });
2091
+ return h15(AttentionScreen, { client: c });
1332
2092
  case "modules":
1333
- return h14(ModulesScreen, { client: c });
2093
+ return h15(ModulesScreen, { client: c });
1334
2094
  case "doctor":
1335
- return h14(DoctorScreen, { client: c });
2095
+ return h15(DoctorScreen, { client: c });
1336
2096
  case "environment":
1337
- return h14(EnvironmentScreen, { client: c });
2097
+ return h15(EnvironmentScreen, { client: c });
1338
2098
  case "routes":
1339
- return h14(RoutesScreen, { client: c });
2099
+ return h15(RoutesScreen, { client: c });
1340
2100
  case "tokens":
1341
- return h14(TokensScreen, { client: c });
2101
+ return h15(TokensScreen, { client: c });
1342
2102
  case "migrations":
1343
- return h14(MigrationsScreen, { client: c });
2103
+ return h15(MigrationsScreen, { client: c });
1344
2104
  case "log":
1345
- return h14(AdminLogScreen, { client: c });
2105
+ return h15(AdminLogScreen, { client: c });
1346
2106
  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
- );
2107
+ return props.billingClient ? h15(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
1352
2108
  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
- );
2109
+ return props.billingClient ? h15(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
1358
2110
  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
- );
2111
+ return props.auditClient ? h15(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
1364
2112
  case "users":
1365
- return props.authClient ? h14(UsersScreen, { client: props.authClient }) : h14("p", { style: styles.status }, "Pass an AuthAdminClient to look up users here.");
2113
+ return props.authClient ? h15(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
1366
2114
  case "settings": {
1367
2115
  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
- );
2116
+ if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
1374
2117
  const e = editing.value;
1375
2118
  if (e && e.kind !== "template")
1376
2119
  return [
1377
- back(),
1378
- h14(ConfigEditorScreen, {
2120
+ back("Config & secrets"),
2121
+ h15(ConfigEditorScreen, {
1379
2122
  client: cc,
1380
2123
  kind: e.kind,
1381
2124
  configKey: e.key,
@@ -1383,7 +2126,7 @@ var AdminShell = defineComponent13({
1383
2126
  onSaved: () => editing.value = null
1384
2127
  })
1385
2128
  ];
1386
- return h14(ConfigListScreen, {
2129
+ return h15(ConfigListScreen, {
1387
2130
  client: cc,
1388
2131
  ...props.environment !== void 0 ? { environment: props.environment } : {},
1389
2132
  "onSelect-config": (key) => editing.value = { kind: "config", key },
@@ -1395,61 +2138,135 @@ var AdminShell = defineComponent13({
1395
2138
  }
1396
2139
  case "templates": {
1397
2140
  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
- );
2141
+ if (!kc) return missing("Pass a CourierAdminClient to manage templates here.");
1404
2142
  const e = editing.value;
1405
2143
  if (e?.kind === "template")
1406
2144
  return [
1407
- back(),
1408
- h14(TemplateEditorScreen, {
2145
+ back("Templates"),
2146
+ h15(TemplateEditorScreen, {
1409
2147
  client: kc,
1410
2148
  type: e.type,
1411
2149
  onSaved: () => editing.value = null
1412
2150
  })
1413
2151
  ];
1414
- return h14(TemplateListScreen, {
2152
+ return h15(TemplateListScreen, {
1415
2153
  client: kc,
1416
2154
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
1417
2155
  });
1418
2156
  }
1419
2157
  }
1420
2158
  };
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(
2159
+ const visible = (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);
2160
+ const brand = () => {
2161
+ const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
2162
+ return h15("div", { style: styles.navBrand }, [
2163
+ h15("div", { style: styles.navMark }, [icon("mark", 15)]),
2164
+ h15("div", { style: { minWidth: 0, flex: 1 } }, [
2165
+ h15("div", { style: styles.navBrandTitle }, props.appName),
2166
+ props.envLabel ? h15(
2167
+ "span",
2168
+ {
2169
+ style: {
2170
+ ...styles.badge,
2171
+ marginTop: "3px",
2172
+ padding: "0 7px",
2173
+ fontSize: "11px",
2174
+ ...production ? {
2175
+ color: "var(--fonderie-danger,#e00)",
2176
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 35%, transparent)"
2177
+ } : {}
2178
+ }
2179
+ },
2180
+ props.envLabel
2181
+ ) : null
2182
+ ]),
2183
+ narrow.value ? h15(
2184
+ "button",
2185
+ {
2186
+ type: "button",
2187
+ style: styles.buttonGhost,
2188
+ "aria-label": "Close menu",
2189
+ onClick: () => drawer.value = false
2190
+ },
2191
+ [icon("close")]
2192
+ ) : null
2193
+ ]);
2194
+ };
2195
+ const nav = () => h15(
2196
+ "nav",
2197
+ {
2198
+ style: {
2199
+ ...styles.nav,
2200
+ ...narrow.value ? { width: "280px", maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2201
+ },
2202
+ "aria-label": "Admin"
2203
+ },
2204
+ [
2205
+ brand(),
2206
+ h15(
2207
+ "div",
2208
+ { style: styles.navScroll },
2209
+ NAV.map((g) => {
2210
+ const items = g.items.filter(visible);
2211
+ if (items.length === 0) return null;
2212
+ return h15("div", { key: g.group }, [
2213
+ h15("div", { style: styles.navGroup }, g.group),
2214
+ ...items.map((i) => {
2215
+ const active = current.value === i.page;
2216
+ return h15(
2217
+ "button",
2218
+ {
2219
+ key: i.page,
2220
+ type: "button",
2221
+ class: "fonderie-admin-nav-item",
2222
+ style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2223
+ "aria-current": active ? "page" : void 0,
2224
+ onClick: () => go(i.page)
2225
+ },
2226
+ [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2227
+ );
2228
+ })
2229
+ ]);
2230
+ })
2231
+ ),
2232
+ slots["footer"] ? h15("div", { style: styles.navFooter }, slots["footer"]()) : null
2233
+ ]
2234
+ );
2235
+ return () => {
2236
+ const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2237
+ return h15(
2238
+ "div",
2239
+ { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2240
+ [
2241
+ ...narrow.value ? [
2242
+ h15("div", { style: styles.topbar }, [
2243
+ h15(
1434
2244
  "button",
1435
2245
  {
1436
- key: i.page,
1437
2246
  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)
2247
+ style: styles.buttonGhost,
2248
+ "aria-label": "Open menu",
2249
+ "aria-expanded": drawer.value,
2250
+ onClick: () => drawer.value = true
1444
2251
  },
1445
- i.label
1446
- )
1447
- )
1448
- ]);
1449
- })
1450
- ),
1451
- h14("main", { style: styles.main }, body())
1452
- ]);
2252
+ [icon("menu")]
2253
+ ),
2254
+ h15("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2255
+ props.envLabel ? h15("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2256
+ ]),
2257
+ drawer.value ? h15("div", { style: styles.drawer }, [
2258
+ nav(),
2259
+ h15("div", {
2260
+ style: styles.scrim,
2261
+ "aria-hidden": "true",
2262
+ onClick: () => drawer.value = false
2263
+ })
2264
+ ]) : null
2265
+ ] : [nav()],
2266
+ h15("main", { class: "fonderie-admin-main", style: styles.main }, body())
2267
+ ]
2268
+ );
2269
+ };
1453
2270
  }
1454
2271
  });
1455
2272
  export {