@fonderie/vue-admin-screens 1.0.1 → 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.cjs CHANGED
@@ -39,7 +39,7 @@ module.exports = __toCommonJS(index_exports);
39
39
  // src/screens/AdminShell.ts
40
40
  var import_vue_config_admin_screens = require("@fonderie/vue-config-admin-screens");
41
41
  var import_vue_courier_admin_screens = require("@fonderie/vue-courier-admin-screens");
42
- var import_vue14 = require("vue");
42
+ var import_vue15 = require("vue");
43
43
 
44
44
  // src/styles.ts
45
45
  var t = {
@@ -61,7 +61,32 @@ var t = {
61
61
  trackingDisplay: "var(--fonderie-tracking-display,-0.05em)",
62
62
  shadowCard: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
63
63
  };
64
- var styles = {
64
+ var tint = (token, pct) => `color-mix(in srgb, ${token} ${pct}%, transparent)`;
65
+ var toneColor = {
66
+ ok: t.accentStrong,
67
+ bad: t.danger,
68
+ // Amber on white fails contrast; mixing in the text colour darkens it in
69
+ // light mode and lightens it in dark, from the same two tokens.
70
+ warn: `color-mix(in srgb, ${t.warning} 70%, ${t.text})`,
71
+ neutral: t.muted,
72
+ info: t.text
73
+ };
74
+ var control = {
75
+ display: "inline-flex",
76
+ alignItems: "center",
77
+ justifyContent: "center",
78
+ gap: 6,
79
+ height: 32,
80
+ boxSizing: "border-box",
81
+ borderRadius: 6,
82
+ padding: "0 12px",
83
+ fontSize: 13,
84
+ fontWeight: 500,
85
+ fontFamily: "inherit",
86
+ lineHeight: 1,
87
+ whiteSpace: "nowrap"
88
+ };
89
+ var raw = {
65
90
  shell: {
66
91
  display: "flex",
67
92
  minHeight: "100vh",
@@ -69,140 +94,571 @@ var styles = {
69
94
  color: t.text,
70
95
  background: t.bg
71
96
  },
72
- // The sidebar sits on the raised surface against the page canvas, which is
73
- // what separates the two without a heavy rule between them.
97
+ // Sticky, full height, scrolls on its own: the nav never scrolls away with
98
+ // a long table, and the footer (session controls) stays in reach.
74
99
  nav: {
75
- width: 220,
76
- borderRight: `1px solid ${t.border}`,
77
- padding: 16,
100
+ width: 248,
78
101
  flexShrink: 0,
79
- background: t.surface
102
+ boxSizing: "border-box",
103
+ borderRight: `1px solid ${t.border}`,
104
+ background: t.surface,
105
+ position: "sticky",
106
+ top: 0,
107
+ height: "100vh",
108
+ display: "flex",
109
+ flexDirection: "column"
110
+ },
111
+ navBrand: {
112
+ display: "flex",
113
+ alignItems: "center",
114
+ gap: 10,
115
+ padding: "18px 16px 14px",
116
+ borderBottom: `1px solid ${t.borderLight}`
117
+ },
118
+ navMark: {
119
+ width: 28,
120
+ height: 28,
121
+ borderRadius: 7,
122
+ background: t.text,
123
+ color: t.surface,
124
+ display: "grid",
125
+ placeItems: "center",
126
+ flexShrink: 0
127
+ },
128
+ navBrandTitle: {
129
+ fontSize: 14,
130
+ fontWeight: 600,
131
+ letterSpacing: t.trackingDisplay,
132
+ lineHeight: 1.2,
133
+ overflow: "hidden",
134
+ textOverflow: "ellipsis",
135
+ whiteSpace: "nowrap"
80
136
  },
137
+ navScroll: { flex: 1, overflowY: "auto", padding: "8px 10px 16px" },
81
138
  navGroup: {
82
139
  fontSize: 11,
83
140
  textTransform: "uppercase",
84
141
  letterSpacing: 0.6,
85
142
  color: t.muted,
86
- margin: "16px 0 6px",
143
+ margin: "16px 8px 4px",
87
144
  fontWeight: 600
88
145
  },
89
146
  navItem: {
90
- display: "block",
147
+ display: "flex",
148
+ alignItems: "center",
149
+ gap: 10,
91
150
  width: "100%",
92
151
  textAlign: "left",
93
152
  background: "none",
94
153
  border: "none",
95
- borderRadius: t.radius,
96
- padding: "6px 8px",
154
+ borderRadius: 6,
155
+ padding: "7px 8px",
97
156
  cursor: "pointer",
98
- fontSize: 14,
99
- color: t.text,
100
- fontFamily: "inherit"
157
+ fontSize: 13.5,
158
+ fontWeight: 500,
159
+ color: t.muted,
160
+ fontFamily: "inherit",
161
+ position: "relative"
162
+ },
163
+ navItemActive: { background: tint(t.accent, 12), color: t.text, fontWeight: 600 },
164
+ navIconActive: { color: t.accentStrong },
165
+ navFooter: {
166
+ borderTop: `1px solid ${t.borderLight}`,
167
+ padding: 12,
168
+ display: "flex",
169
+ flexDirection: "column",
170
+ gap: 10
171
+ },
172
+ // Phone layout: a top bar with the menu button replaces the sidebar, which
173
+ // opens as a drawer over the content.
174
+ topbar: {
175
+ position: "sticky",
176
+ top: 0,
177
+ zIndex: 20,
178
+ display: "flex",
179
+ alignItems: "center",
180
+ gap: 10,
181
+ padding: "10px 16px",
182
+ background: t.surface,
183
+ borderBottom: `1px solid ${t.border}`
184
+ },
185
+ drawer: {
186
+ position: "fixed",
187
+ inset: 0,
188
+ zIndex: 30,
189
+ display: "flex"
190
+ },
191
+ scrim: { flex: 1, background: "rgba(0,0,0,.35)" },
192
+ main: { flex: 1, minWidth: 0 },
193
+ container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
194
+ // ── page header ──
195
+ header: {
196
+ display: "flex",
197
+ alignItems: "flex-start",
198
+ justifyContent: "space-between",
199
+ gap: 16,
200
+ marginBottom: 24,
201
+ flexWrap: "wrap"
101
202
  },
102
- navItemActive: { background: t.surfaceAlt, fontWeight: 600, color: t.accentStrong },
103
- main: { flex: 1, padding: 24, maxWidth: 960 },
104
- container: { padding: 24, maxWidth: 960 },
105
203
  title: {
106
- fontSize: 20,
204
+ fontSize: 22,
107
205
  fontWeight: 600,
108
- marginTop: 0,
109
- marginBottom: 12,
110
- letterSpacing: t.trackingDisplay
206
+ margin: 0,
207
+ letterSpacing: t.trackingDisplay,
208
+ lineHeight: 1.25
111
209
  },
210
+ lead: { color: t.muted, fontSize: 13.5, margin: "4px 0 0", lineHeight: 1.5 },
211
+ actions: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
112
212
  subtitle: {
113
213
  fontSize: 15,
114
214
  fontWeight: 600,
115
- marginTop: 24,
116
- marginBottom: 8,
117
- letterSpacing: t.trackingDisplay
215
+ marginTop: 32,
216
+ marginBottom: 10,
217
+ letterSpacing: t.trackingDisplay,
218
+ display: "flex",
219
+ alignItems: "center",
220
+ gap: 8
221
+ },
222
+ // ── feedback ──
223
+ status: { padding: "24px 0", color: t.muted, fontSize: 13.5 },
224
+ error: {
225
+ color: t.danger,
226
+ background: tint(t.danger, 8),
227
+ border: `1px solid ${tint(t.danger, 25)}`,
228
+ borderRadius: t.radiusLg,
229
+ padding: "10px 14px",
230
+ marginBottom: 16,
231
+ fontSize: 13.5
232
+ },
233
+ notice: {
234
+ color: t.text,
235
+ background: tint(t.warning, 10),
236
+ border: `1px solid ${tint(t.warning, 35)}`,
237
+ borderRadius: t.radiusLg,
238
+ padding: "10px 14px",
239
+ marginBottom: 16,
240
+ fontSize: 13.5
118
241
  },
119
- status: { padding: 12, color: t.muted },
120
- error: { color: t.danger, marginBottom: 12, fontSize: 14 },
121
242
  ok: { color: t.accentStrong, fontWeight: 600 },
122
243
  bad: { color: t.danger, fontWeight: 600 },
123
244
  advice: { color: t.warning },
124
245
  muted: { color: t.muted, fontSize: 13 },
125
- mono: { fontFamily: t.mono, fontSize: 13 },
126
- table: { width: "100%", borderCollapse: "collapse", fontSize: 14 },
246
+ mono: { fontFamily: t.mono, fontSize: 12.5 },
247
+ code: {
248
+ fontFamily: t.mono,
249
+ fontSize: 12,
250
+ background: t.surfaceAlt,
251
+ border: `1px solid ${t.borderLight}`,
252
+ borderRadius: 4,
253
+ padding: "1px 5px"
254
+ },
255
+ // ── surfaces ──
256
+ // Tables and lists are cards themselves, so every screen gets the framed,
257
+ // rounded look without wrapping each one. The last row's rule is removed in
258
+ // the shell stylesheet (a pseudo-selector); embedded without the shell it
259
+ // simply doubles against the frame.
260
+ table: {
261
+ width: "100%",
262
+ borderCollapse: "separate",
263
+ borderSpacing: 0,
264
+ fontSize: 13.5,
265
+ background: t.surface,
266
+ border: `1px solid ${t.border}`,
267
+ borderRadius: t.radiusLg,
268
+ overflow: "hidden",
269
+ boxShadow: t.shadowCard
270
+ },
127
271
  th: {
128
272
  textAlign: "left",
129
273
  borderBottom: `1px solid ${t.border}`,
130
- padding: "6px 8px",
274
+ padding: "9px 14px",
275
+ fontWeight: 600,
276
+ fontSize: 11.5,
277
+ textTransform: "uppercase",
278
+ letterSpacing: 0.4,
279
+ color: t.muted,
280
+ background: t.surfaceAlt,
281
+ whiteSpace: "nowrap"
282
+ },
283
+ td: {
284
+ borderBottom: `1px solid ${t.borderLight}`,
285
+ padding: "11px 14px",
286
+ verticalAlign: "top"
287
+ },
288
+ list: {
289
+ listStyle: "none",
290
+ padding: 0,
291
+ margin: 0,
292
+ background: t.surface,
293
+ border: `1px solid ${t.border}`,
294
+ borderRadius: t.radiusLg,
295
+ overflow: "hidden",
296
+ boxShadow: t.shadowCard
297
+ },
298
+ row: { borderBottom: `1px solid ${t.borderLight}`, padding: "12px 16px" },
299
+ card: {
300
+ background: t.surface,
301
+ border: `1px solid ${t.border}`,
302
+ borderRadius: t.radiusLg,
303
+ padding: 16,
304
+ boxShadow: t.shadowCard
305
+ },
306
+ grid: {
307
+ display: "grid",
308
+ gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))",
309
+ gap: 12,
310
+ marginBottom: 24
311
+ },
312
+ statLabel: { fontSize: 12, color: t.muted, fontWeight: 500 },
313
+ statValue: {
314
+ fontSize: 26,
131
315
  fontWeight: 600,
132
- color: t.muted
316
+ letterSpacing: t.trackingDisplay,
317
+ lineHeight: 1.2,
318
+ marginTop: 6
319
+ },
320
+ statHint: { fontSize: 12, color: t.muted, marginTop: 2 },
321
+ empty: {
322
+ background: t.surface,
323
+ border: `1px dashed ${t.border}`,
324
+ borderRadius: t.radiusLg,
325
+ padding: "40px 24px",
326
+ textAlign: "center",
327
+ color: t.muted,
328
+ fontSize: 13.5
329
+ },
330
+ emptyTitle: { color: t.text, fontWeight: 600, fontSize: 14.5, margin: "10px 0 4px" },
331
+ dl: { display: "grid", gridTemplateColumns: "160px 1fr", margin: 0 },
332
+ dt: {
333
+ color: t.muted,
334
+ fontSize: 13,
335
+ padding: "10px 16px",
336
+ borderBottom: `1px solid ${t.borderLight}`
337
+ },
338
+ dd: {
339
+ margin: 0,
340
+ padding: "10px 16px",
341
+ borderBottom: `1px solid ${t.borderLight}`,
342
+ fontSize: 13.5,
343
+ minWidth: 0
133
344
  },
134
- td: { borderBottom: `1px solid ${t.borderLight}`, padding: "6px 8px", verticalAlign: "top" },
135
- list: { listStyle: "none", padding: 0, margin: 0 },
136
- row: { borderBottom: `1px solid ${t.borderLight}`, padding: "8px 0" },
137
345
  badge: {
138
- display: "inline-block",
346
+ display: "inline-flex",
347
+ alignItems: "center",
348
+ gap: 5,
139
349
  borderRadius: 999,
140
- padding: "1px 8px",
141
- fontSize: 12,
350
+ padding: "2px 8px",
351
+ fontSize: 11.5,
352
+ fontWeight: 500,
353
+ lineHeight: 1.5,
142
354
  background: t.surfaceAlt,
143
355
  border: `1px solid ${t.border}`,
144
- color: t.muted
356
+ color: t.muted,
357
+ whiteSpace: "nowrap"
145
358
  },
359
+ pill: {
360
+ display: "inline-flex",
361
+ alignItems: "center",
362
+ gap: 6,
363
+ borderRadius: 999,
364
+ padding: "2px 9px 2px 8px",
365
+ fontSize: 12,
366
+ fontWeight: 600,
367
+ lineHeight: 1.5,
368
+ whiteSpace: "nowrap",
369
+ verticalAlign: "baseline"
370
+ },
371
+ dot: { width: 6, height: 6, borderRadius: 999, flexShrink: 0, background: "currentColor" },
372
+ // ── controls ──
146
373
  button: {
374
+ ...control,
147
375
  background: t.surface,
148
376
  border: `1px solid ${t.border}`,
149
- borderRadius: t.radius,
150
- padding: "4px 10px",
151
- cursor: "pointer",
152
- fontSize: 13,
153
377
  color: t.text,
154
- fontFamily: "inherit",
378
+ cursor: "pointer",
155
379
  boxShadow: t.shadowCard
156
380
  },
157
- toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 12 },
158
- // Surfaces the screens compose themselves, kept here so a card looks the
159
- // same on every page rather than being re-invented per screen.
160
- card: {
381
+ buttonPrimary: {
382
+ ...control,
383
+ background: t.text,
384
+ border: `1px solid ${t.text}`,
385
+ color: t.surface,
386
+ cursor: "pointer"
387
+ },
388
+ buttonDanger: {
389
+ ...control,
161
390
  background: t.surface,
391
+ border: `1px solid ${tint(t.danger, 40)}`,
392
+ color: t.danger,
393
+ cursor: "pointer"
394
+ },
395
+ buttonGhost: {
396
+ ...control,
397
+ background: "none",
398
+ border: "1px solid transparent",
399
+ color: t.muted,
400
+ cursor: "pointer",
401
+ padding: "0 8px"
402
+ },
403
+ link: {
404
+ background: "none",
405
+ border: "none",
406
+ padding: 0,
407
+ color: t.text,
408
+ fontFamily: "inherit",
409
+ fontSize: "inherit",
410
+ fontWeight: 500,
411
+ cursor: "pointer",
412
+ textAlign: "left"
413
+ },
414
+ input: {
415
+ height: 32,
416
+ boxSizing: "border-box",
162
417
  border: `1px solid ${t.border}`,
163
- borderRadius: t.radiusLg,
164
- padding: 16,
165
- boxShadow: t.shadowCard
418
+ borderRadius: 6,
419
+ padding: "0 10px",
420
+ fontSize: 13.5,
421
+ fontFamily: "inherit",
422
+ background: t.surface,
423
+ color: t.text,
424
+ minWidth: 0
166
425
  },
426
+ toolbar: { display: "flex", gap: 8, alignItems: "center", marginBottom: 16, flexWrap: "wrap" },
167
427
  accent: { color: t.accent }
168
428
  };
429
+ var UNITLESS = /* @__PURE__ */ new Set([
430
+ "fontWeight",
431
+ "lineHeight",
432
+ "opacity",
433
+ "zIndex",
434
+ "flex",
435
+ "flexGrow",
436
+ "flexShrink",
437
+ "order"
438
+ ]);
439
+ function withUnits(sheet) {
440
+ const out = {};
441
+ for (const [name, rule] of Object.entries(sheet)) {
442
+ const fixed = {};
443
+ for (const [prop, value] of Object.entries(rule)) {
444
+ fixed[prop] = typeof value === "number" && !UNITLESS.has(prop) ? `${value}px` : value;
445
+ }
446
+ out[name] = fixed;
447
+ }
448
+ return out;
449
+ }
450
+ var styles = withUnits(raw);
451
+
452
+ // src/ui.ts
453
+ var import_vue = require("vue");
454
+
455
+ // src/icons.ts
456
+ var ICONS = {
457
+ attention: ["M22 12h-4l-3 9L9 3l-3 9H2"],
458
+ modules: [
459
+ "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",
460
+ "M3.3 7 12 12l8.7-5",
461
+ "M12 22V12"
462
+ ],
463
+ environment: [
464
+ "M4 21v-7",
465
+ "M4 10V3",
466
+ "M12 21v-9",
467
+ "M12 8V3",
468
+ "M20 21v-5",
469
+ "M20 12V3",
470
+ "M1 14h6",
471
+ "M9 8h6",
472
+ "M17 16h6"
473
+ ],
474
+ doctor: [
475
+ "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"
476
+ ],
477
+ routes: [
478
+ "M6 3v12",
479
+ "M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
480
+ "M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
481
+ "M18 9a9 9 0 0 1-9 9"
482
+ ],
483
+ users: [
484
+ "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",
485
+ "M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z",
486
+ "M22 21v-2a4 4 0 0 0-3-3.87",
487
+ "M16 3.13a4 4 0 0 1 0 7.75"
488
+ ],
489
+ catalog: [
490
+ "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",
491
+ "M7.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
492
+ ],
493
+ subscriber: ["M2 7h20v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z", "M2 11h20", "M6 15h4"],
494
+ settings: [
495
+ "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",
496
+ "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
497
+ ],
498
+ templates: [
499
+ "M22 7l-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7",
500
+ "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"
501
+ ],
502
+ audit: ["M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z", "M9 12l2 2 4-4"],
503
+ log: ["M12 8v4l3 3", "M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5"],
504
+ tokens: [
505
+ "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",
506
+ "M16.5 8a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"
507
+ ],
508
+ migrations: [
509
+ "M12 22c4.97 0 9-1.79 9-4V6",
510
+ "M3 6v12c0 2.21 4.03 4 9 4",
511
+ "M12 10c4.97 0 9-1.79 9-4s-4.03-4-9-4-9 1.79-9 4 4.03 4 9 4z",
512
+ "M3 12c0 2.21 4.03 4 9 4s9-1.79 9-4"
513
+ ],
514
+ menu: ["M4 6h16", "M4 12h16", "M4 18h16"],
515
+ close: ["M18 6 6 18", "m6 6 12 12"],
516
+ refresh: [
517
+ "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",
518
+ "M21 3v5h-5",
519
+ "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",
520
+ "M8 16H3v5"
521
+ ],
522
+ check: ["M20 6 9 17l-5-5"],
523
+ alert: [
524
+ "M12 9v4",
525
+ "M12 17h.01",
526
+ "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"
527
+ ],
528
+ search: ["M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", "m21 21-4.3-4.3"],
529
+ back: ["m12 19-7-7 7-7", "M19 12H5"],
530
+ logout: ["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", "m16 17 5-5-5-5", "M21 12H9"],
531
+ lock: [
532
+ "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",
533
+ "M7 11V7a5 5 0 0 1 10 0v4"
534
+ ],
535
+ plus: ["M12 5v14", "M5 12h14"],
536
+ mark: [
537
+ "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",
538
+ "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",
539
+ "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",
540
+ "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"
541
+ ]
542
+ };
543
+
544
+ // src/ui.ts
545
+ function icon(name, size = 16, style) {
546
+ return (0, import_vue.h)(
547
+ "svg",
548
+ {
549
+ width: size,
550
+ height: size,
551
+ viewBox: "0 0 24 24",
552
+ fill: "none",
553
+ stroke: "currentColor",
554
+ "stroke-width": 2,
555
+ "stroke-linecap": "round",
556
+ "stroke-linejoin": "round",
557
+ "aria-hidden": "true",
558
+ style: { flexShrink: 0, ...style }
559
+ },
560
+ ICONS[name].map((d) => (0, import_vue.h)("path", { key: d, d }))
561
+ );
562
+ }
563
+ function pageHeader(title, lead, actions) {
564
+ return (0, import_vue.h)("div", { style: styles.header }, [
565
+ (0, import_vue.h)("div", { style: { minWidth: 0 } }, [
566
+ (0, import_vue.h)("h1", { style: styles.title }, title),
567
+ lead ? (0, import_vue.h)("p", { style: styles.lead }, lead) : null
568
+ ]),
569
+ actions?.filter(Boolean).length ? (0, import_vue.h)("div", { style: styles.actions }, actions) : null
570
+ ]);
571
+ }
572
+ function pill(tone, text, dot = true) {
573
+ const color = toneColor[tone];
574
+ return (0, import_vue.h)(
575
+ "span",
576
+ { style: { ...styles.pill, color, background: tint(color, tone === "neutral" ? 10 : 13) } },
577
+ [dot ? (0, import_vue.h)("span", { style: styles.dot }) : null, text]
578
+ );
579
+ }
580
+ function stat(label, value, hint, tone) {
581
+ return (0, import_vue.h)("div", { style: styles.card }, [
582
+ (0, import_vue.h)("div", { style: styles.statLabel }, label),
583
+ (0, import_vue.h)(
584
+ "div",
585
+ { style: { ...styles.statValue, ...tone ? { color: toneColor[tone] } : {} } },
586
+ value
587
+ ),
588
+ hint ? (0, import_vue.h)("div", { style: styles.statHint }, hint) : null
589
+ ]);
590
+ }
591
+ function empty(title, body, iconName = "check") {
592
+ return (0, import_vue.h)("div", { style: styles.empty }, [
593
+ icon(iconName, 22),
594
+ (0, import_vue.h)("div", { style: styles.emptyTitle }, title),
595
+ body ? (0, import_vue.h)("div", body) : null
596
+ ]);
597
+ }
598
+ function methodChip(method) {
599
+ const tone = method === "GET" ? "ok" : method === "DELETE" ? "bad" : method === "POST" ? "info" : "warn";
600
+ const color = toneColor[tone];
601
+ return (0, import_vue.h)(
602
+ "span",
603
+ {
604
+ style: {
605
+ ...styles.mono,
606
+ fontSize: "11px",
607
+ fontWeight: 700,
608
+ color,
609
+ background: tint(color, 12),
610
+ borderRadius: "4px",
611
+ padding: "2px 6px",
612
+ display: "inline-block",
613
+ minWidth: "44px",
614
+ textAlign: "center"
615
+ }
616
+ },
617
+ method
618
+ );
619
+ }
169
620
 
170
621
  // src/screens/AdminLogScreen.ts
171
622
  var import_vue_admin = require("@fonderie/vue-admin");
172
- var import_vue2 = require("vue");
623
+ var import_vue3 = require("vue");
173
624
 
174
625
  // src/screens/common.ts
175
- var import_vue = require("vue");
626
+ var import_vue2 = require("vue");
176
627
  function page(title, state, content, toolbar = [], options = {}) {
177
- const body = state.isLoading.value ? (0, import_vue.h)("p", { style: styles.status }, options.loadingText ?? "Loading\u2026") : state.error.value ? (0, import_vue.h)(
628
+ const body = state.isLoading.value ? (0, import_vue2.h)("p", { style: styles.status }, options.loadingText ?? "Loading\u2026") : state.error.value ? (0, import_vue2.h)(
178
629
  "p",
179
630
  { style: styles.error, role: "alert" },
180
631
  (options.errorText ?? ((e) => e.explanation))(state.error.value)
181
632
  ) : content();
182
- return (0, import_vue.h)("div", { style: styles.container }, [
183
- (0, import_vue.h)("div", { style: styles.toolbar }, [
184
- (0, import_vue.h)("h1", { style: { ...styles.title, marginBottom: 0 } }, title),
185
- ...toolbar
186
- ]),
187
- body
188
- ]);
633
+ return (0, import_vue2.h)("div", { style: styles.container }, [pageHeader(title, options.lead, toolbar), body]);
189
634
  }
190
635
  function table(headers, rows) {
191
- return (0, import_vue.h)("table", { style: styles.table }, [
192
- (0, import_vue.h)("thead", [
193
- (0, import_vue.h)(
636
+ return (0, import_vue2.h)("table", { style: styles.table }, [
637
+ (0, import_vue2.h)("thead", [
638
+ (0, import_vue2.h)(
194
639
  "tr",
195
- headers.map((t2) => (0, import_vue.h)("th", { style: styles.th }, t2))
640
+ headers.map(
641
+ (t2) => typeof t2 === "string" ? (0, import_vue2.h)("th", { style: styles.th }, t2) : (0, import_vue2.h)("th", { style: { ...styles.th, ...t2.style } }, t2.label)
642
+ )
196
643
  )
197
644
  ]),
198
- (0, import_vue.h)("tbody", rows)
645
+ (0, import_vue2.h)("tbody", rows)
199
646
  ]);
200
647
  }
201
- var td = (children, extra = {}) => (0, import_vue.h)("td", { style: { ...styles.td, ...extra } }, children);
202
- var refreshButton = (label, disabled, onClick) => (0, import_vue.h)("button", { type: "button", style: styles.button, disabled, onClick }, label);
648
+ var td = (children, extra = {}) => (0, import_vue2.h)("td", { style: { ...styles.td, ...extra } }, children);
649
+ var refreshButton = (label, disabled, onClick) => (0, import_vue2.h)("button", { type: "button", style: styles.button, disabled, onClick }, [
650
+ icon("refresh", 14),
651
+ disabled ? "Working\u2026" : label
652
+ ]);
653
+ var loadMoreButton = (onClick) => (0, import_vue2.h)(
654
+ "button",
655
+ { type: "button", style: { ...styles.button, marginTop: "12px" }, onClick },
656
+ "Load more"
657
+ );
658
+ var actionButton = (label, disabled, onClick, style = styles.button) => (0, import_vue2.h)("button", { type: "button", style, disabled, onClick }, label);
203
659
 
204
660
  // src/screens/AdminLogScreen.ts
205
- var AdminLogScreen = (0, import_vue2.defineComponent)({
661
+ var AdminLogScreen = (0, import_vue3.defineComponent)({
206
662
  name: "FonderieAdminLogScreen",
207
663
  props: {
208
664
  client: { type: Object, required: true },
@@ -212,46 +668,48 @@ var AdminLogScreen = (0, import_vue2.defineComponent)({
212
668
  const { entries, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin.useAdminLog)(props.client, {
213
669
  limit: props.pageSize
214
670
  });
215
- return () => (0, import_vue2.h)("div", { style: styles.container }, [
216
- (0, import_vue2.h)("div", { style: styles.toolbar }, [
217
- (0, import_vue2.h)("h1", { style: { ...styles.title, marginBottom: 0 } }, "Admin log"),
218
- refreshButton("Refresh", isLoading.value, () => void refresh())
219
- ]),
220
- error.value ? (0, import_vue2.h)(
671
+ return () => (0, import_vue3.h)("div", { style: styles.container }, [
672
+ pageHeader(
673
+ "Admin log",
674
+ "Every request made through this surface, newest first \u2014 refused ones included.",
675
+ [refreshButton("Refresh", isLoading.value, () => void refresh())]
676
+ ),
677
+ error.value ? (0, import_vue3.h)(
221
678
  "p",
222
679
  { style: styles.error, role: "alert" },
223
680
  error.value.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.value.explanation
224
- ) : [
681
+ ) : entries.value.length === 0 && !isLoading.value ? empty("No requests yet", void 0, "log") : [
225
682
  table(
226
683
  ["When", "Actor", "Request", "Status", "Module"],
227
684
  entries.value.map(
228
- (e) => (0, import_vue2.h)("tr", { key: e.id }, [
229
- td(new Date(e.at).toLocaleString(), styles.muted),
685
+ (e) => (0, import_vue3.h)("tr", { key: e.id }, [
686
+ td(new Date(e.at).toLocaleString(), {
687
+ ...styles.muted,
688
+ whiteSpace: "nowrap"
689
+ }),
230
690
  td(e.actor),
231
- td(`${e.method} ${e.path}`, styles.mono),
232
691
  td([
233
- (0, import_vue2.h)(
234
- "span",
235
- { style: e.status >= 400 ? styles.bad : styles.ok },
236
- String(e.status)
237
- ),
692
+ methodChip(e.method),
238
693
  " ",
239
- (0, import_vue2.h)("span", { style: styles.muted }, `${e.durationMs} ms`)
694
+ (0, import_vue3.h)("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.path)
240
695
  ]),
696
+ td(
697
+ [
698
+ pill(
699
+ e.status >= 500 ? "bad" : e.status >= 400 ? "warn" : "ok",
700
+ String(e.status)
701
+ ),
702
+ " ",
703
+ (0, import_vue3.h)("span", { style: styles.muted }, `${e.durationMs} ms`)
704
+ ],
705
+ { whiteSpace: "nowrap" }
706
+ ),
241
707
  td(e.module, styles.muted)
242
708
  ])
243
709
  )
244
710
  ),
245
- isLoading.value ? (0, import_vue2.h)("p", { style: styles.status }, "Loading\u2026") : null,
246
- hasMore.value && !isLoading.value ? (0, import_vue2.h)(
247
- "button",
248
- {
249
- type: "button",
250
- style: { ...styles.button, marginTop: "12px" },
251
- onClick: () => void loadMore()
252
- },
253
- "Load more"
254
- ) : null
711
+ isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, "Loading\u2026") : null,
712
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
255
713
  ]
256
714
  ]);
257
715
  }
@@ -259,224 +717,414 @@ var AdminLogScreen = (0, import_vue2.defineComponent)({
259
717
 
260
718
  // src/screens/AttentionScreen.ts
261
719
  var import_vue_admin2 = require("@fonderie/vue-admin");
262
- var import_vue3 = require("vue");
263
- var AttentionScreen = (0, import_vue3.defineComponent)({
720
+ var import_vue4 = require("vue");
721
+ var AttentionScreen = (0, import_vue4.defineComponent)({
264
722
  name: "FonderieAttentionScreen",
265
723
  props: { client: { type: Object, required: true } },
266
724
  setup(props) {
267
725
  const { attention, isLoading, error, refresh } = (0, import_vue_admin2.useAttention)(props.client);
268
- return () => page(
269
- "Attention",
270
- { isLoading, error },
271
- () => {
272
- const a = attention.value;
273
- if (!a) return null;
274
- if (a.items.length === 0)
275
- return (0, import_vue3.h)(
276
- "p",
277
- { style: styles.ok },
278
- `Nothing needs you. Checked ${new Date(a.generatedAt).toLocaleString()}.`
279
- );
280
- return (0, import_vue3.h)(
281
- "ul",
282
- { style: styles.list },
283
- a.items.map(
284
- (item) => (0, import_vue3.h)("li", { key: `${item.source}:${item.message}`, style: styles.row }, [
285
- (0, import_vue3.h)(
286
- "span",
287
- { style: item.severity === "error" ? styles.bad : styles.advice },
288
- item.severity
289
- ),
290
- " ",
291
- (0, import_vue3.h)("span", { style: styles.mono }, item.source),
292
- ` \u2014 ${item.message}`
293
- ])
294
- )
295
- );
296
- },
297
- [refreshButton("Refresh", isLoading.value, () => void refresh())],
298
- { loadingText: "Running the checks\u2026" }
299
- );
726
+ const { manifest } = (0, import_vue_admin2.useManifest)(props.client);
727
+ return () => {
728
+ const a = attention.value;
729
+ const m = manifest.value;
730
+ const errors = a?.items.filter((i) => i.severity === "error") ?? [];
731
+ const advice = a?.items.filter((i) => i.severity !== "error") ?? [];
732
+ const ready = m?.modules.filter((x) => x.readiness.ok).length ?? 0;
733
+ const total = m?.modules.length ?? 0;
734
+ const tiles = (0, import_vue4.h)("div", { style: styles.grid }, [
735
+ stat(
736
+ "Needs action",
737
+ a ? errors.length : "\u2014",
738
+ "errors to fix",
739
+ errors.length ? "bad" : a ? "ok" : void 0
740
+ ),
741
+ stat("Advice", a ? advice.length : "\u2014", "worth a look", advice.length ? "warn" : void 0),
742
+ stat(
743
+ "Modules ready",
744
+ m ? `${ready}/${total}` : "\u2014",
745
+ m ? `admin ${m.admin.version}` : void 0,
746
+ m && ready < total ? "warn" : m ? "ok" : void 0
747
+ ),
748
+ stat("Routes", m ? m.routes.length : "\u2014", m ? m.env : void 0)
749
+ ]);
750
+ return page(
751
+ "Attention",
752
+ // Tiles show while the checks run; only the list waits.
753
+ { isLoading: { value: false }, error },
754
+ () => {
755
+ if (isLoading.value && !a)
756
+ return [tiles, (0, import_vue4.h)("p", { style: styles.status }, "Running the checks\u2026")];
757
+ if (!a) return tiles;
758
+ if (a.items.length === 0)
759
+ return [
760
+ tiles,
761
+ empty(
762
+ "Nothing needs you",
763
+ `Every check passes. Checked ${new Date(a.generatedAt).toLocaleString()}.`
764
+ )
765
+ ];
766
+ return [
767
+ tiles,
768
+ (0, import_vue4.h)(
769
+ "ul",
770
+ { style: styles.list },
771
+ [...errors, ...advice].map(
772
+ (item) => (0, import_vue4.h)(
773
+ "li",
774
+ {
775
+ key: `${item.source}:${item.message}`,
776
+ style: {
777
+ ...styles.row,
778
+ display: "flex",
779
+ gap: "12px",
780
+ alignItems: "flex-start"
781
+ }
782
+ },
783
+ [
784
+ icon("alert", 16, {
785
+ marginTop: "3px",
786
+ color: item.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-warning,#f5a623)"
787
+ }),
788
+ (0, import_vue4.h)("div", { style: { minWidth: 0, flex: 1 } }, [
789
+ (0, import_vue4.h)(
790
+ "div",
791
+ {
792
+ style: {
793
+ display: "flex",
794
+ gap: "8px",
795
+ alignItems: "center",
796
+ flexWrap: "wrap",
797
+ marginBottom: "2px"
798
+ }
799
+ },
800
+ [
801
+ pill(
802
+ item.severity === "error" ? "bad" : "warn",
803
+ item.severity === "error" ? "error" : "advice"
804
+ ),
805
+ (0, import_vue4.h)("span", { style: styles.mono }, item.source)
806
+ ]
807
+ ),
808
+ (0, import_vue4.h)("div", { style: { lineHeight: 1.55 } }, item.message)
809
+ ])
810
+ ]
811
+ )
812
+ )
813
+ )
814
+ ];
815
+ },
816
+ [refreshButton("Refresh", isLoading.value, () => void refresh())],
817
+ {
818
+ lead: a ? `What needs you on this deployment \xB7 checked ${new Date(a.generatedAt).toLocaleTimeString()}` : "What needs you on this deployment"
819
+ }
820
+ );
821
+ };
300
822
  }
301
823
  });
302
824
 
303
825
  // src/screens/EnvironmentScreen.ts
304
826
  var import_vue_admin3 = require("@fonderie/vue-admin");
305
- var import_vue4 = require("vue");
306
- var EnvironmentScreen = (0, import_vue4.defineComponent)({
827
+ var import_vue5 = require("vue");
828
+ var EnvironmentScreen = (0, import_vue5.defineComponent)({
307
829
  name: "FonderieEnvironmentScreen",
308
830
  props: { client: { type: Object, required: true } },
309
831
  setup(props) {
310
832
  const { report, isLoading, error } = (0, import_vue_admin3.useAdminEnvironment)(props.client);
311
- return () => page("Configuration", { isLoading, error }, () => {
833
+ return () => {
312
834
  const r = report.value;
313
- if (!r) return null;
314
- return [
315
- (0, import_vue4.h)("h2", { style: styles.subtitle }, "Readiness"),
316
- (0, import_vue4.h)(
317
- "ul",
318
- { style: styles.list },
319
- r.modules.map(
320
- (m) => (0, import_vue4.h)("li", { key: m.name, style: styles.row }, [
321
- (0, import_vue4.h)("span", { style: styles.mono }, m.name),
322
- " ",
323
- ...m.problems.length === 0 ? [(0, import_vue4.h)("span", { style: styles.ok }, "ok")] : m.problems.map(
324
- (p) => (0, import_vue4.h)(
835
+ const missing = r?.env.filter((e) => !e.set).length ?? 0;
836
+ return page(
837
+ "Environment",
838
+ { isLoading, error },
839
+ () => {
840
+ if (!r) return null;
841
+ return [
842
+ (0, import_vue5.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Variables"),
843
+ r.env.length === 0 ? (0, import_vue5.h)(
844
+ "p",
845
+ { style: styles.muted },
846
+ "No variables declared \u2014 pass `env` to AdminModule."
847
+ ) : (0, import_vue5.h)(
848
+ "div",
849
+ {
850
+ style: {
851
+ ...styles.grid,
852
+ gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
853
+ gap: "8px"
854
+ }
855
+ },
856
+ r.env.map(
857
+ (e) => (0, import_vue5.h)(
325
858
  "div",
326
859
  {
327
- key: p.message,
328
- style: p.severity === "error" ? styles.bad : styles.advice
860
+ key: e.name,
861
+ style: {
862
+ ...styles.card,
863
+ padding: "10px 12px",
864
+ display: "flex",
865
+ alignItems: "center",
866
+ justifyContent: "space-between",
867
+ gap: "8px"
868
+ }
329
869
  },
330
- p.message
870
+ [
871
+ (0, import_vue5.h)("span", { style: { ...styles.mono, wordBreak: "break-all" } }, e.name),
872
+ pill(e.set ? "ok" : "bad", e.set ? "set" : "missing")
873
+ ]
331
874
  )
332
875
  )
333
- ])
334
- )
335
- ),
336
- (0, import_vue4.h)("h2", { style: styles.subtitle }, "Environment"),
337
- r.env.length === 0 ? (0, import_vue4.h)("p", { style: styles.muted }, "No variables declared \u2014 pass `env` to AdminModule.") : (0, import_vue4.h)(
338
- "ul",
339
- { style: styles.list },
340
- r.env.map(
341
- (e) => (0, import_vue4.h)("li", { key: e.name, style: styles.row }, [
342
- (0, import_vue4.h)("span", { style: styles.mono }, e.name),
343
- " ",
344
- (0, import_vue4.h)("span", { style: e.set ? styles.ok : styles.bad }, e.set ? "set" : "missing")
345
- ])
346
- )
347
- )
348
- ];
349
- });
876
+ ),
877
+ (0, import_vue5.h)("h2", { style: styles.subtitle }, "Module readiness"),
878
+ (0, import_vue5.h)(
879
+ "ul",
880
+ { style: styles.list },
881
+ r.modules.map(
882
+ (m) => (0, import_vue5.h)("li", { key: m.name, style: styles.row }, [
883
+ (0, import_vue5.h)(
884
+ "div",
885
+ {
886
+ style: {
887
+ display: "flex",
888
+ justifyContent: "space-between",
889
+ gap: "12px",
890
+ alignItems: "center"
891
+ }
892
+ },
893
+ [
894
+ (0, import_vue5.h)("span", { style: { ...styles.mono, fontWeight: 600 } }, m.name),
895
+ pill(
896
+ m.problems.length === 0 ? "ok" : m.problems.some((p) => p.severity === "error") ? "bad" : "warn",
897
+ m.problems.length === 0 ? "ready" : `${m.problems.length} problem${m.problems.length === 1 ? "" : "s"}`
898
+ )
899
+ ]
900
+ ),
901
+ ...m.problems.map(
902
+ (p) => (0, import_vue5.h)(
903
+ "div",
904
+ {
905
+ key: p.message,
906
+ style: {
907
+ marginTop: "6px",
908
+ fontSize: "13px",
909
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
910
+ }
911
+ },
912
+ p.message
913
+ )
914
+ )
915
+ ])
916
+ )
917
+ )
918
+ ];
919
+ },
920
+ [
921
+ r && r.env.length > 0 ? pill(missing ? "bad" : "ok", missing ? `${missing} missing` : "all set") : null
922
+ ],
923
+ {
924
+ lead: "Whether each module is configured and each variable the app reads is set. Values are never shown."
925
+ }
926
+ );
927
+ };
350
928
  }
351
929
  });
352
930
 
353
931
  // src/screens/DoctorScreen.ts
354
932
  var import_vue_admin4 = require("@fonderie/vue-admin");
355
- var import_vue5 = require("vue");
356
- var DoctorScreen = (0, import_vue5.defineComponent)({
933
+ var import_vue6 = require("vue");
934
+ var DoctorScreen = (0, import_vue6.defineComponent)({
357
935
  name: "FonderieDoctorScreen",
358
936
  props: { client: { type: Object, required: true } },
359
937
  setup(props) {
360
938
  const { report, isLoading, error, refresh } = (0, import_vue_admin4.useDoctor)(props.client);
361
939
  return () => page(
362
940
  "Doctor",
363
- { isLoading, error },
941
+ { isLoading: { value: isLoading.value && !report.value }, error },
364
942
  () => {
365
943
  const r = report.value;
366
944
  if (!r) return null;
367
- return (0, import_vue5.h)(
368
- "ul",
369
- { style: styles.list },
945
+ return table(
946
+ [
947
+ { label: "Result", style: { width: "110px" } },
948
+ "Check",
949
+ { label: "Time", style: { textAlign: "right", width: "90px" } }
950
+ ],
370
951
  r.checks.map(
371
- (c) => (0, import_vue5.h)("li", { key: c.name, style: styles.row }, [
372
- (0, import_vue5.h)(
373
- "span",
374
- { style: c.skipped ? styles.muted : c.ok ? styles.ok : styles.bad },
375
- c.skipped ? "skipped" : c.ok ? "ok" : "failed"
952
+ (c) => (0, import_vue6.h)("tr", { key: c.name }, [
953
+ td(
954
+ pill(
955
+ c.skipped ? "neutral" : c.ok ? c.findings.length ? "warn" : "ok" : "bad",
956
+ c.skipped ? "skipped" : c.ok ? c.findings.length ? "advice" : "ok" : "failed"
957
+ )
376
958
  ),
377
- " ",
378
- (0, import_vue5.h)("span", { style: styles.mono }, c.name),
379
- " ",
380
- (0, import_vue5.h)("span", { style: styles.muted }, `${c.module} \xB7 ${c.durationMs} ms`),
381
- c.skipped ? (0, import_vue5.h)("div", { style: styles.muted }, c.skipped) : null,
382
- ...c.findings.map(
383
- (f) => (0, import_vue5.h)("div", { key: f, style: c.ok ? styles.advice : styles.bad }, f)
384
- )
959
+ td([
960
+ (0, import_vue6.h)("div", [
961
+ (0, import_vue6.h)("span", { style: { ...styles.mono, fontWeight: 600 } }, c.name),
962
+ " ",
963
+ (0, import_vue6.h)("span", { style: styles.muted }, c.module)
964
+ ]),
965
+ c.skipped ? (0, import_vue6.h)("div", { style: { ...styles.muted, marginTop: "4px" } }, c.skipped) : null,
966
+ ...c.findings.map(
967
+ (f) => (0, import_vue6.h)(
968
+ "div",
969
+ {
970
+ key: f,
971
+ style: {
972
+ marginTop: "4px",
973
+ fontSize: "13px",
974
+ lineHeight: 1.55,
975
+ color: c.ok ? "var(--fonderie-text,#171717)" : "var(--fonderie-danger,#e00)"
976
+ }
977
+ },
978
+ f
979
+ )
980
+ )
981
+ ]),
982
+ td(`${c.durationMs} ms`, {
983
+ ...styles.muted,
984
+ textAlign: "right",
985
+ whiteSpace: "nowrap"
986
+ })
385
987
  ])
386
988
  )
387
989
  );
388
990
  },
389
991
  [
390
- refreshButton("Run again", isLoading.value, () => void refresh()),
391
- report.value && !isLoading.value ? (0, import_vue5.h)(
392
- "span",
393
- { style: report.value.ok ? styles.ok : styles.bad },
992
+ report.value && !isLoading.value ? pill(
993
+ report.value.ok ? "ok" : "bad",
394
994
  report.value.ok ? "all checks pass" : "a check failed"
395
- ) : (0, import_vue5.h)("span")
995
+ ) : null,
996
+ refreshButton("Run again", isLoading.value, () => void refresh())
396
997
  ],
397
- { loadingText: "Running the checks\u2026" }
998
+ {
999
+ loadingText: "Running the checks\u2026",
1000
+ lead: "Every reconciliation check across the installed modules, run on demand."
1001
+ }
398
1002
  );
399
1003
  }
400
1004
  });
401
1005
 
402
1006
  // src/screens/ModulesScreen.ts
403
1007
  var import_vue_admin5 = require("@fonderie/vue-admin");
404
- var import_vue6 = require("vue");
405
- var ModulesScreen = (0, import_vue6.defineComponent)({
1008
+ var import_vue7 = require("vue");
1009
+ var ModulesScreen = (0, import_vue7.defineComponent)({
406
1010
  name: "FonderieModulesScreen",
407
1011
  props: { client: { type: Object, required: true } },
408
1012
  setup(props) {
409
1013
  const { manifest, isLoading, error } = (0, import_vue_admin5.useManifest)(props.client);
410
- return () => page("Modules", { isLoading, error }, () => {
1014
+ return () => {
411
1015
  const m = manifest.value;
412
- if (!m) return null;
413
- return [
414
- (0, import_vue6.h)(
415
- "p",
416
- { style: styles.muted },
417
- `${m.env} \xB7 admin ${m.admin.version} \xB7 admin log ${m.admin.log ? "on" : "off"} \xB7 ${m.routes.length} routes`
418
- ),
419
- table(
420
- ["Module", "Version", "Readiness", "Describes admin"],
421
- m.modules.map(
422
- (mod) => (0, import_vue6.h)("tr", { key: mod.name }, [
423
- td(mod.name, styles.mono),
424
- td(mod.version ?? (0, import_vue6.h)("span", { style: styles.muted }, "not reported")),
425
- td([
426
- (0, import_vue6.h)(
427
- "span",
428
- { style: mod.readiness.ok ? styles.ok : styles.bad },
429
- mod.readiness.ok ? "ok" : "error"
430
- ),
431
- ...mod.readiness.problems.map(
432
- (p) => (0, import_vue6.h)(
433
- "div",
434
- {
435
- key: p.message,
436
- style: p.severity === "error" ? styles.bad : styles.advice
437
- },
438
- p.message
1016
+ return page(
1017
+ "Modules",
1018
+ { isLoading, error },
1019
+ () => {
1020
+ if (!m) return null;
1021
+ return table(
1022
+ ["Module", "Version", "Readiness", "Admin"],
1023
+ m.modules.map(
1024
+ (mod) => (0, import_vue7.h)("tr", { key: mod.name }, [
1025
+ td(mod.name, { ...styles.mono, fontWeight: 600 }),
1026
+ td(mod.version ?? (0, import_vue7.h)("span", { style: styles.muted }, "not reported"), styles.mono),
1027
+ td([
1028
+ pill(mod.readiness.ok ? "ok" : "bad", mod.readiness.ok ? "ready" : "error"),
1029
+ ...mod.readiness.problems.map(
1030
+ (p) => (0, import_vue7.h)(
1031
+ "div",
1032
+ {
1033
+ key: p.message,
1034
+ style: {
1035
+ marginTop: "6px",
1036
+ fontSize: "13px",
1037
+ color: p.severity === "error" ? "var(--fonderie-danger,#e00)" : "var(--fonderie-text-muted,#5c5c5c)"
1038
+ }
1039
+ },
1040
+ p.message
1041
+ )
439
1042
  )
1043
+ ]),
1044
+ td(
1045
+ mod.describesAdmin ? (0, import_vue7.h)("span", { style: styles.badge }, "describes admin") : (0, import_vue7.h)("span", { style: styles.muted }, "\u2014")
440
1046
  )
441
- ]),
442
- td(mod.describesAdmin ? "yes" : (0, import_vue6.h)("span", { style: styles.muted }, "no"))
443
- ])
444
- )
445
- )
446
- ];
447
- });
1047
+ ])
1048
+ )
1049
+ );
1050
+ },
1051
+ [],
1052
+ {
1053
+ 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."
1054
+ }
1055
+ );
1056
+ };
448
1057
  }
449
1058
  });
450
1059
 
451
1060
  // src/screens/RoutesScreen.ts
452
1061
  var import_vue_admin6 = require("@fonderie/vue-admin");
453
- var import_vue7 = require("vue");
454
- var RoutesScreen = (0, import_vue7.defineComponent)({
1062
+ var import_vue8 = require("vue");
1063
+ var RoutesScreen = (0, import_vue8.defineComponent)({
455
1064
  name: "FonderieRoutesScreen",
456
1065
  props: { client: { type: Object, required: true } },
457
1066
  setup(props) {
458
1067
  const { report, isLoading, error } = (0, import_vue_admin6.useAdminRoutes)(props.client);
459
- return () => page(
460
- "Routes",
461
- { isLoading, error },
462
- () => table(
463
- ["Method", "Path", "Guard", "Module"],
464
- (report.value?.routes ?? []).map(
465
- (r) => (0, import_vue7.h)("tr", { key: `${r.method} ${r.path}` }, [
466
- td(r.method, styles.mono),
467
- td(r.path, styles.mono),
468
- td((0, import_vue7.h)("span", { style: styles.badge }, r.guard)),
469
- td(r.module ?? "application", styles.muted)
470
- ])
471
- )
472
- )
473
- );
1068
+ const q = (0, import_vue8.ref)("");
1069
+ return () => {
1070
+ const needle = q.value.trim().toLowerCase();
1071
+ const routes = (report.value?.routes ?? []).filter(
1072
+ (r) => !needle || `${r.method} ${r.path} ${r.module ?? "application"} ${r.guard}`.toLowerCase().includes(needle)
1073
+ );
1074
+ return page(
1075
+ "Routes",
1076
+ { isLoading, error },
1077
+ () => table(
1078
+ [{ label: "Method", style: { width: "80px" } }, "Path", "Guard", "Module"],
1079
+ [
1080
+ ...routes.map(
1081
+ (r) => (0, import_vue8.h)("tr", { key: `${r.method} ${r.path}` }, [
1082
+ td(methodChip(r.method)),
1083
+ td(r.path, { ...styles.mono, wordBreak: "break-all" }),
1084
+ td(
1085
+ pill(
1086
+ r.guard === "admin" ? "ok" : r.guard === "probe" ? "neutral" : "info",
1087
+ r.guard,
1088
+ false
1089
+ )
1090
+ ),
1091
+ td(r.module ?? "application", styles.muted)
1092
+ ])
1093
+ ),
1094
+ ...routes.length === 0 ? [
1095
+ (0, import_vue8.h)("tr", [
1096
+ (0, import_vue8.h)(
1097
+ "td",
1098
+ { style: { ...styles.td, ...styles.muted }, colspan: 4 },
1099
+ `No route matches \u201C${q.value}\u201D.`
1100
+ )
1101
+ ])
1102
+ ] : []
1103
+ ]
1104
+ ),
1105
+ [
1106
+ (0, import_vue8.h)("input", {
1107
+ type: "search",
1108
+ value: q.value,
1109
+ placeholder: "Filter by path, method, module\u2026",
1110
+ style: { ...styles.input, width: "280px" },
1111
+ "aria-label": "Filter routes",
1112
+ onInput: (e) => {
1113
+ q.value = e.target.value;
1114
+ }
1115
+ })
1116
+ ],
1117
+ {
1118
+ 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."
1119
+ }
1120
+ );
1121
+ };
474
1122
  }
475
1123
  });
476
1124
 
477
1125
  // src/screens/MigrationsScreen.ts
478
1126
  var import_vue_admin7 = require("@fonderie/vue-admin");
479
- var import_vue8 = require("vue");
1127
+ var import_vue9 = require("vue");
480
1128
  function why(m, everApplied) {
481
1129
  if (m.pending.length === 0) return null;
482
1130
  if (m.blockedBy) return `Apply "${m.blockedBy}" first \u2014 it runs before this one and is behind.`;
@@ -484,7 +1132,7 @@ function why(m, everApplied) {
484
1132
  return "Contains a migration that deletes data. No down-migration brings it back \u2014 apply this one through CI or `npm run migrate`.";
485
1133
  return null;
486
1134
  }
487
- var MigrationsScreen = (0, import_vue8.defineComponent)({
1135
+ var MigrationsScreen = (0, import_vue9.defineComponent)({
488
1136
  name: "FonderieMigrationsScreen",
489
1137
  props: { client: { type: Object, required: true } },
490
1138
  setup(props) {
@@ -492,31 +1140,31 @@ var MigrationsScreen = (0, import_vue8.defineComponent)({
492
1140
  const section = (m, everApplied) => {
493
1141
  const blocked = why(m, everApplied);
494
1142
  const files = m.pending.map((p) => p.file);
495
- return (0, import_vue8.h)("section", { style: { marginBottom: "24px" } }, [
496
- (0, import_vue8.h)("h2", { style: styles.subtitle }, `${m.name} \u2014 ${m.pending.length} pending`),
497
- (0, import_vue8.h)(
1143
+ return (0, import_vue9.h)("section", { style: { ...styles.card, marginBottom: "16px" } }, [
1144
+ (0, import_vue9.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, [
1145
+ (0, import_vue9.h)("span", { style: styles.mono }, m.name),
1146
+ pill("warn", `${m.pending.length} pending`)
1147
+ ]),
1148
+ (0, import_vue9.h)(
498
1149
  "ul",
1150
+ { style: { margin: "0 0 12px", paddingLeft: "18px", lineHeight: 1.9 } },
499
1151
  m.pending.map(
500
- (p) => (0, import_vue8.h)("li", [
501
- (0, import_vue8.h)("code", p.file),
1152
+ (p) => (0, import_vue9.h)("li", [
1153
+ (0, import_vue9.h)("code", { style: styles.code }, p.file),
502
1154
  " ",
503
- (0, import_vue8.h)(
504
- "span",
505
- { style: p.impact === "destructive" ? styles.bad : styles.advice },
506
- p.impact
507
- ),
508
- p.destructive.length > 0 ? (0, import_vue8.h)(
1155
+ p.impact === "destructive" ? pill("bad", "destructive") : pill("neutral", "additive"),
1156
+ p.destructive.length > 0 ? (0, import_vue9.h)(
509
1157
  "ul",
510
- p.destructive.map((s) => (0, import_vue8.h)("li", [(0, import_vue8.h)("code", s.slice(0, 120))]))
1158
+ p.destructive.map((s) => (0, import_vue9.h)("li", [(0, import_vue9.h)("code", s.slice(0, 120))]))
511
1159
  ) : null
512
1160
  ])
513
1161
  )
514
1162
  ),
515
- blocked ? (0, import_vue8.h)("p", { style: styles.advice }, blocked) : (0, import_vue8.h)(
1163
+ blocked ? (0, import_vue9.h)("p", { style: { ...styles.notice, marginBottom: 0 } }, blocked) : (0, import_vue9.h)(
516
1164
  "button",
517
1165
  {
518
1166
  type: "button",
519
- style: styles.button,
1167
+ style: styles.buttonPrimary,
520
1168
  disabled: isLoading.value || !m.appliable,
521
1169
  onClick: () => {
522
1170
  if (window.confirm(
@@ -537,11 +1185,12 @@ var MigrationsScreen = (0, import_vue8.defineComponent)({
537
1185
  const r = report.value;
538
1186
  if (!r) return null;
539
1187
  const behind = r.modules.filter((m) => m.pending.length > 0);
540
- if (behind.length === 0) return (0, import_vue8.h)("p", { style: styles.ok }, "Every module is up to date.");
1188
+ if (behind.length === 0)
1189
+ return empty("Every module is up to date", "No pending migrations.", "migrations");
541
1190
  return [
542
- r.everApplied ? null : (0, import_vue8.h)(
1191
+ r.everApplied ? null : (0, import_vue9.h)(
543
1192
  "p",
544
- { style: styles.advice },
1193
+ { style: styles.notice },
545
1194
  "This database has never been migrated \u2014 treating it as a first install, so nothing is held back."
546
1195
  ),
547
1196
  ...behind.map((m) => section(m, r.everApplied))
@@ -549,6 +1198,7 @@ var MigrationsScreen = (0, import_vue8.defineComponent)({
549
1198
  },
550
1199
  [refreshButton("Refresh", isLoading.value, () => void refresh())],
551
1200
  {
1201
+ lead: "Schema changes each module ships, and whether this database has them.",
552
1202
  errorText: (e) => e.status === 403 ? "Applying migrations needs a token with the write scope." : e.explanation
553
1203
  }
554
1204
  );
@@ -557,50 +1207,89 @@ var MigrationsScreen = (0, import_vue8.defineComponent)({
557
1207
 
558
1208
  // src/screens/TokensScreen.ts
559
1209
  var import_vue_admin8 = require("@fonderie/vue-admin");
560
- var import_vue9 = require("vue");
1210
+ var import_vue10 = require("vue");
561
1211
  var ALL = ["read", "write", "secrets"];
562
- var TokensScreen = (0, import_vue9.defineComponent)({
1212
+ var TokensScreen = (0, import_vue10.defineComponent)({
563
1213
  name: "FonderieTokensScreen",
564
1214
  props: { client: { type: Object, required: true } },
565
1215
  setup(props) {
566
1216
  const { report, isLoading, error, issue, revoke } = (0, import_vue_admin8.useAdminTokens)(props.client);
567
- const name = (0, import_vue9.ref)("");
568
- const scopes = (0, import_vue9.ref)(["read"]);
569
- const days = (0, import_vue9.ref)("");
570
- const minted = (0, import_vue9.ref)(null);
1217
+ const name = (0, import_vue10.ref)("");
1218
+ const scopes = (0, import_vue10.ref)(["read"]);
1219
+ const days = (0, import_vue10.ref)("");
1220
+ const minted = (0, import_vue10.ref)(null);
571
1221
  const toggle = (s) => scopes.value = scopes.value.includes(s) ? scopes.value.filter((x) => x !== s) : [...scopes.value, s];
572
1222
  return () => {
573
1223
  const r = report.value;
574
- return (0, import_vue9.h)("div", { style: styles.container }, [
575
- (0, import_vue9.h)("h1", { style: styles.title }, "Access"),
576
- error.value ? (0, import_vue9.h)(
1224
+ return (0, import_vue10.h)("div", { style: styles.container }, [
1225
+ pageHeader(
1226
+ "Access",
1227
+ "Who can open this surface: the root token's strength, scoped tokens you have issued, and any legacy per-module tokens."
1228
+ ),
1229
+ error.value ? (0, import_vue10.h)(
577
1230
  "p",
578
1231
  { style: styles.error, role: "alert" },
579
1232
  error.value.status === 401 ? "Issuing and revoking need the root token (the one in your deployment config)." : error.value.explanation
580
1233
  ) : null,
581
- (0, import_vue9.h)("h2", { style: styles.subtitle }, "Root token"),
582
- r ? (0, import_vue9.h)("p", [
583
- (0, import_vue9.h)(
584
- "span",
585
- { style: r.admin.ok ? styles.ok : styles.bad },
586
- r.admin.ok ? "strong" : "weak"
587
- ),
588
- ...r.admin.problems.map(
589
- (p) => (0, import_vue9.h)("div", { key: p.message, style: styles.bad }, p.message)
590
- )
591
- ]) : null,
592
- (0, import_vue9.h)("h2", { style: styles.subtitle }, "Issued tokens"),
593
- r?.issued === null ? (0, import_vue9.h)(
1234
+ (0, import_vue10.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Root token"),
1235
+ r ? (0, import_vue10.h)(
1236
+ "div",
1237
+ { style: { ...styles.card, display: "flex", gap: "12px", alignItems: "flex-start" } },
1238
+ [
1239
+ icon("lock", 18, { marginTop: "2px" }),
1240
+ (0, import_vue10.h)("div", [
1241
+ pill(r.admin.ok ? "ok" : "bad", r.admin.ok ? "strong" : "weak"),
1242
+ ...r.admin.problems.map(
1243
+ (p) => (0, import_vue10.h)(
1244
+ "div",
1245
+ {
1246
+ key: p.message,
1247
+ style: {
1248
+ marginTop: "6px",
1249
+ color: "var(--fonderie-danger,#e00)",
1250
+ fontSize: "13px"
1251
+ }
1252
+ },
1253
+ p.message
1254
+ )
1255
+ )
1256
+ ])
1257
+ ]
1258
+ ) : null,
1259
+ (0, import_vue10.h)("h2", { style: styles.subtitle }, "Issued tokens"),
1260
+ r?.issued === null ? (0, import_vue10.h)(
594
1261
  "p",
595
1262
  { style: styles.muted },
596
1263
  "Issuing is off \u2014 give AdminModule a store and run its migrations."
597
1264
  ) : [
598
- minted.value ? (0, import_vue9.h)("p", { style: styles.ok }, [
599
- "Copy this now \u2014 it is never shown again. ",
600
- (0, import_vue9.h)("span", { style: styles.mono }, minted.value.token),
601
- ` (${minted.value.name})`
602
- ]) : null,
603
- (0, import_vue9.h)(
1265
+ minted.value ? (0, import_vue10.h)(
1266
+ "div",
1267
+ {
1268
+ style: {
1269
+ ...styles.notice,
1270
+ background: "color-mix(in srgb, var(--fonderie-accent,#00d294) 10%, transparent)",
1271
+ borderColor: "color-mix(in srgb, var(--fonderie-accent,#00d294) 35%, transparent)"
1272
+ }
1273
+ },
1274
+ [
1275
+ (0, import_vue10.h)("strong", "Copy this now \u2014 it is never shown again."),
1276
+ ` (${minted.value.name})`,
1277
+ (0, import_vue10.h)(
1278
+ "div",
1279
+ {
1280
+ style: {
1281
+ ...styles.code,
1282
+ display: "block",
1283
+ marginTop: "8px",
1284
+ padding: "8px 10px",
1285
+ wordBreak: "break-all"
1286
+ }
1287
+ },
1288
+ minted.value.token
1289
+ )
1290
+ ]
1291
+ ) : null,
1292
+ (0, import_vue10.h)(
604
1293
  "form",
605
1294
  {
606
1295
  style: styles.toolbar,
@@ -620,49 +1309,60 @@ var TokensScreen = (0, import_vue9.defineComponent)({
620
1309
  }
621
1310
  },
622
1311
  [
623
- (0, import_vue9.h)("input", {
1312
+ (0, import_vue10.h)("input", {
624
1313
  value: name.value,
625
1314
  onInput: (e) => name.value = e.target.value,
626
1315
  placeholder: "name",
627
- style: { ...styles.button, cursor: "text" },
1316
+ style: styles.input,
628
1317
  "aria-label": "Name"
629
1318
  }),
630
1319
  ...ALL.map(
631
- (s) => (0, import_vue9.h)("label", { key: s, style: { ...styles.badge, cursor: "pointer" } }, [
632
- (0, import_vue9.h)("input", {
633
- type: "checkbox",
634
- checked: scopes.value.includes(s),
635
- onChange: () => toggle(s)
636
- }),
637
- ` ${s}`
638
- ])
1320
+ (s) => (0, import_vue10.h)(
1321
+ "label",
1322
+ {
1323
+ key: s,
1324
+ style: {
1325
+ ...styles.badge,
1326
+ cursor: "pointer",
1327
+ height: "32px",
1328
+ boxSizing: "border-box",
1329
+ padding: "0 10px",
1330
+ fontSize: "13px"
1331
+ }
1332
+ },
1333
+ [
1334
+ (0, import_vue10.h)("input", {
1335
+ type: "checkbox",
1336
+ checked: scopes.value.includes(s),
1337
+ onChange: () => toggle(s)
1338
+ }),
1339
+ ` ${s}`
1340
+ ]
1341
+ )
639
1342
  ),
640
- (0, import_vue9.h)("input", {
1343
+ (0, import_vue10.h)("input", {
641
1344
  value: days.value,
642
1345
  onInput: (e) => days.value = e.target.value,
643
1346
  placeholder: "days (optional)",
644
- style: { ...styles.button, cursor: "text", width: "140px" },
1347
+ style: { ...styles.input, width: "140px" },
645
1348
  "aria-label": "Days"
646
1349
  }),
647
- (0, import_vue9.h)(
1350
+ (0, import_vue10.h)(
648
1351
  "button",
649
1352
  {
650
1353
  type: "submit",
651
- style: styles.button,
1354
+ style: styles.buttonPrimary,
652
1355
  disabled: !name.value.trim() || scopes.value.length === 0 || isLoading.value
653
1356
  },
654
- "Issue"
1357
+ [icon("plus", 14), "Issue token"]
655
1358
  )
656
1359
  ]
657
1360
  ),
658
1361
  r?.issued && r.issued.length > 0 ? table(
659
1362
  ["Name", "Scopes", "Created", "Expires", "Last used", ""],
660
1363
  r.issued.map(
661
- (t2) => (0, import_vue9.h)("tr", { key: t2.id }, [
662
- td([
663
- t2.name,
664
- t2.revokedAt ? (0, import_vue9.h)("span", { style: styles.badge }, " revoked") : ""
665
- ]),
1364
+ (t2) => (0, import_vue10.h)("tr", { key: t2.id }, [
1365
+ td([t2.name, t2.revokedAt ? [" ", pill("neutral", "revoked")] : ""]),
666
1366
  td(t2.scopes.join(", "), styles.mono),
667
1367
  td(
668
1368
  `${new Date(t2.createdAt).toLocaleDateString()} \xB7 ${t2.createdBy}`,
@@ -677,11 +1377,11 @@ var TokensScreen = (0, import_vue9.defineComponent)({
677
1377
  styles.muted
678
1378
  ),
679
1379
  td(
680
- t2.revokedAt ? "" : (0, import_vue9.h)(
1380
+ t2.revokedAt ? "" : (0, import_vue10.h)(
681
1381
  "button",
682
1382
  {
683
1383
  type: "button",
684
- style: styles.button,
1384
+ style: styles.buttonDanger,
685
1385
  onClick: () => {
686
1386
  if (window.confirm(
687
1387
  `Revoke "${t2.name}"? Anything using it stops working immediately.`
@@ -695,21 +1395,25 @@ var TokensScreen = (0, import_vue9.defineComponent)({
695
1395
  )
696
1396
  ])
697
1397
  )
698
- ) : (0, import_vue9.h)("p", { style: styles.muted }, "None issued.")
1398
+ ) : empty(
1399
+ "No scoped tokens yet",
1400
+ "Issue a read-only token for a dashboard or a teammate instead of sharing the root.",
1401
+ "tokens"
1402
+ )
699
1403
  ],
700
- (0, import_vue9.h)("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
701
- r && r.legacy.length === 0 ? (0, import_vue9.h)(
1404
+ (0, import_vue10.h)("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
1405
+ r && r.legacy.length === 0 ? (0, import_vue10.h)(
702
1406
  "p",
703
1407
  { style: styles.muted },
704
1408
  "None \u2014 every brick's admin surface goes through this token."
705
- ) : (0, import_vue9.h)(
1409
+ ) : (0, import_vue10.h)(
706
1410
  "ul",
707
1411
  { style: styles.list },
708
1412
  (r?.legacy ?? []).map(
709
- (l) => (0, import_vue9.h)("li", { key: l.module, style: styles.row }, [
710
- (0, import_vue9.h)("span", { style: styles.mono }, l.module),
1413
+ (l) => (0, import_vue10.h)("li", { key: l.module, style: styles.row }, [
1414
+ (0, import_vue10.h)("span", { style: styles.mono }, l.module),
711
1415
  " ",
712
- (0, import_vue9.h)(
1416
+ (0, import_vue10.h)(
713
1417
  "span",
714
1418
  { style: styles.muted },
715
1419
  "still registers its standalone routes with its own token (deprecated)"
@@ -725,28 +1429,28 @@ var TokensScreen = (0, import_vue9.defineComponent)({
725
1429
  // src/screens/UsersScreen.ts
726
1430
  var import_client = require("@fonderie/client");
727
1431
  var import_vue_admin9 = require("@fonderie/vue-admin");
728
- var import_vue10 = require("vue");
729
- var UsersScreen = (0, import_vue10.defineComponent)({
1432
+ var import_vue11 = require("vue");
1433
+ var UsersScreen = (0, import_vue11.defineComponent)({
730
1434
  name: "FonderieUsersScreen",
731
1435
  props: {
732
1436
  client: { type: Object, required: true },
733
1437
  pageSize: { type: Number, default: 50 }
734
1438
  },
735
1439
  setup(props) {
736
- const input = (0, import_vue10.ref)("");
737
- const email = (0, import_vue10.ref)("");
738
- const selectedId = (0, import_vue10.ref)("");
1440
+ const input = (0, import_vue11.ref)("");
1441
+ const email = (0, import_vue11.ref)("");
1442
+ const selectedId = (0, import_vue11.ref)("");
739
1443
  const list = (0, import_vue_admin9.useAdminUsers)(props.client, { limit: props.pageSize });
740
1444
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = (0, import_vue_admin9.useAdminUser)(
741
1445
  props.client,
742
1446
  { email, id: selectedId }
743
1447
  );
744
- const userId = (0, import_vue10.computed)(() => user.value?.id ?? null);
1448
+ const userId = (0, import_vue11.computed)(() => user.value?.id ?? null);
745
1449
  const sessions = (0, import_vue_admin9.useAdminUserSessions)(props.client, userId);
746
1450
  const history = (0, import_vue_admin9.useAdminLoginHistory)(props.client, userId, { limit: 20 });
747
- const showList = (0, import_vue10.computed)(() => !email.value && !selectedId.value);
748
- const yesNo = (v) => (0, import_vue10.h)("span", { style: v ? styles.ok : styles.muted }, v ? "yes" : "no");
749
- const row = (k, v) => (0, import_vue10.h)("tr", [td(k), td(v)]);
1451
+ const showList = (0, import_vue11.computed)(() => !email.value && !selectedId.value);
1452
+ const yesNo = (v) => v ? pill("ok", "yes") : pill("neutral", "no");
1453
+ const row = (k, v) => (0, import_vue11.h)("tr", [td(k), td(v)]);
750
1454
  const clear = () => {
751
1455
  email.value = "";
752
1456
  selectedId.value = "";
@@ -754,9 +1458,12 @@ var UsersScreen = (0, import_vue10.defineComponent)({
754
1458
  };
755
1459
  return () => {
756
1460
  const u = user.value;
757
- return (0, import_vue10.h)("div", { style: styles.container }, [
758
- (0, import_vue10.h)("h1", { style: styles.title }, "Users"),
759
- (0, import_vue10.h)(
1461
+ return (0, import_vue11.h)("div", { style: styles.container }, [
1462
+ pageHeader(
1463
+ "Users",
1464
+ "Everyone signed up. Open an account for its sessions, sign-ins and the suspend and sign-out controls."
1465
+ ),
1466
+ (0, import_vue11.h)(
760
1467
  "form",
761
1468
  {
762
1469
  style: styles.toolbar,
@@ -767,75 +1474,67 @@ var UsersScreen = (0, import_vue10.defineComponent)({
767
1474
  }
768
1475
  },
769
1476
  [
770
- (0, import_vue10.h)("input", {
1477
+ (0, import_vue11.h)("input", {
771
1478
  type: "email",
772
1479
  value: input.value,
773
1480
  onInput: (e) => input.value = e.target.value,
774
1481
  placeholder: "email address",
775
- style: { ...styles.button, cursor: "text", minWidth: "280px" },
1482
+ style: { ...styles.input, minWidth: "280px" },
776
1483
  "aria-label": "Email"
777
1484
  }),
778
- (0, import_vue10.h)(
1485
+ (0, import_vue11.h)(
779
1486
  "button",
780
- { type: "submit", style: styles.button, disabled: isLoading.value },
781
- "Look up"
1487
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1488
+ [icon("search", 14), "Look up"]
782
1489
  ),
783
- showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : refreshButton("\u2190 All users", false, clear)
1490
+ showList.value ? refreshButton("Refresh", list.isLoading.value, () => void list.refresh()) : actionButton([icon("back", 14), "All users"], false, clear, styles.buttonGhost)
784
1491
  ]
785
1492
  ),
786
- showList.value ? (0, import_vue10.h)("div", [
787
- list.error.value ? (0, import_vue10.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
788
- list.users.value.length === 0 && !list.isLoading.value ? (0, import_vue10.h)("p", { style: styles.muted }, "No users yet.") : table(
1493
+ showList.value ? (0, import_vue11.h)("div", [
1494
+ list.error.value ? (0, import_vue11.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1495
+ list.users.value.length === 0 && !list.isLoading.value ? empty("No users yet", "Sign-ups appear here as they happen.", "users") : table(
789
1496
  ["Email", "Name", "Created", "Status"],
790
1497
  list.users.value.map(
791
- (u2) => (0, import_vue10.h)("tr", { key: u2.id }, [
1498
+ (u2) => (0, import_vue11.h)("tr", { key: u2.id }, [
792
1499
  td(
793
- (0, import_vue10.h)(
1500
+ (0, import_vue11.h)(
794
1501
  "button",
795
1502
  {
796
1503
  type: "button",
797
- style: { ...styles.navItem, padding: 0 },
1504
+ style: styles.link,
798
1505
  onClick: () => selectedId.value = u2.id
799
1506
  },
800
1507
  u2.email
801
1508
  )
802
1509
  ),
803
1510
  td(
804
- `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue10.h)("span", { style: styles.muted }, "\u2014")
1511
+ `${u2.firstName ?? ""} ${u2.lastName ?? ""}`.trim() || (0, import_vue11.h)("span", { style: styles.muted }, "\u2014")
805
1512
  ),
806
1513
  td(new Date(u2.createdAt).toLocaleDateString()),
807
1514
  td(
808
- u2.suspended ? (0, import_vue10.h)("span", { style: styles.badge }, "suspended") : u2.deletedAt ? (0, import_vue10.h)("span", { style: styles.badge }, "deleted") : (0, import_vue10.h)("span", { style: styles.muted }, "active")
1515
+ u2.suspended ? pill("warn", "suspended") : u2.deletedAt ? pill("neutral", "deleted") : pill("ok", "active")
809
1516
  )
810
1517
  ])
811
1518
  )
812
1519
  ),
813
- list.isLoading.value ? (0, import_vue10.h)("p", { style: styles.status }, "Loading\u2026") : null,
814
- list.hasMore.value && !list.isLoading.value ? (0, import_vue10.h)(
815
- "button",
816
- {
817
- type: "button",
818
- style: { ...styles.button, marginTop: "8px" },
819
- onClick: () => void list.loadMore()
820
- },
821
- "Load more"
822
- ) : null
1520
+ list.isLoading.value ? (0, import_vue11.h)("p", { style: styles.status }, "Loading\u2026") : null,
1521
+ list.hasMore.value && !list.isLoading.value ? loadMoreButton(() => void list.loadMore()) : null
823
1522
  ]) : null,
824
- error.value ? (0, import_vue10.h)(
1523
+ error.value ? (0, import_vue11.h)(
825
1524
  "p",
826
1525
  { style: styles.error, role: "alert" },
827
1526
  error.value.status === 404 ? "No user with that email." : error.value.explanation
828
1527
  ) : null,
829
1528
  u ? [
830
- (0, import_vue10.h)("h2", { style: styles.subtitle }, [
1529
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, [
831
1530
  u.firstName || u.lastName ? `${u.firstName} ${u.lastName}`.trim() : u.email,
832
1531
  " ",
833
- u.suspended ? (0, import_vue10.h)("span", { style: styles.badge }, "suspended") : null,
834
- u.deletedAt ? (0, import_vue10.h)("span", { style: styles.badge }, "deleted") : null
1532
+ u.suspended ? pill("warn", "suspended") : null,
1533
+ u.deletedAt ? pill("neutral", "deleted") : null
835
1534
  ]),
836
- (0, import_vue10.h)("table", { style: styles.table }, [
837
- (0, import_vue10.h)("tbody", [
838
- row("id", (0, import_vue10.h)("span", { style: styles.mono }, u.id)),
1535
+ (0, import_vue11.h)("table", { style: styles.table }, [
1536
+ (0, import_vue11.h)("tbody", [
1537
+ row("id", (0, import_vue11.h)("span", { style: styles.mono }, u.id)),
839
1538
  row("email", [u.email, " \xB7 verified ", yesNo(u.isEmailVerified)]),
840
1539
  row("MFA", yesNo(u.mfaEnabled)),
841
1540
  row(
@@ -844,32 +1543,37 @@ var UsersScreen = (0, import_vue10.defineComponent)({
844
1543
  ),
845
1544
  row(
846
1545
  "last login",
847
- u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue10.h)("span", { style: styles.muted }, "never")
1546
+ u.lastLogin ? new Date(u.lastLogin).toLocaleString() : (0, import_vue11.h)("span", { style: styles.muted }, "never")
848
1547
  ),
849
1548
  row("created", new Date(u.createdAt).toLocaleString())
850
1549
  ])
851
1550
  ]),
852
- (0, import_vue10.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
853
- u.suspended ? refreshButton("Unsuspend", isLoading.value, () => void unsuspend()) : refreshButton("Suspend", isLoading.value, () => void suspend()),
854
- refreshButton(
1551
+ (0, import_vue11.h)("div", { style: { ...styles.toolbar, marginTop: "12px" } }, [
1552
+ u.suspended ? actionButton("Unsuspend", isLoading.value, () => void unsuspend()) : actionButton(
1553
+ "Suspend",
1554
+ isLoading.value,
1555
+ () => void suspend(),
1556
+ styles.buttonDanger
1557
+ ),
1558
+ actionButton(
855
1559
  "Sign out everywhere",
856
1560
  isLoading.value,
857
1561
  () => void revokeSessions().then(() => sessions.refresh())
858
1562
  )
859
1563
  ]),
860
- (0, import_vue10.h)("h2", { style: styles.subtitle }, "Live sessions"),
861
- sessions.sessions.value.length === 0 ? (0, import_vue10.h)("p", { style: styles.muted }, "None.") : (0, import_vue10.h)(
1564
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, "Live sessions"),
1565
+ sessions.sessions.value.length === 0 ? (0, import_vue11.h)("p", { style: styles.muted }, "None.") : (0, import_vue11.h)(
862
1566
  "ul",
863
1567
  { style: styles.list },
864
1568
  sessions.sessions.value.map(
865
- (s) => (0, import_vue10.h)("li", { key: s.id, style: styles.row }, [
866
- (0, import_vue10.h)("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
1569
+ (s) => (0, import_vue11.h)("li", { key: s.id, style: styles.row }, [
1570
+ (0, import_vue11.h)("span", { style: styles.mono }, s.ipAddress ?? "\u2014"),
867
1571
  " ",
868
- (0, import_vue10.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
1572
+ (0, import_vue11.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(s.location) ?? ""),
869
1573
  " ",
870
- (0, import_vue10.h)("span", { style: styles.muted }, s.userAgent ?? ""),
1574
+ (0, import_vue11.h)("span", { style: styles.muted }, s.userAgent ?? ""),
871
1575
  " ",
872
- (0, import_vue10.h)(
1576
+ (0, import_vue11.h)(
873
1577
  "span",
874
1578
  { style: styles.muted },
875
1579
  `since ${new Date(s.createdAt).toLocaleString()}`
@@ -877,38 +1581,26 @@ var UsersScreen = (0, import_vue10.defineComponent)({
877
1581
  ])
878
1582
  )
879
1583
  ),
880
- (0, import_vue10.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
881
- history.events.value.length === 0 && !history.isLoading.value ? (0, import_vue10.h)("p", { style: styles.muted }, "None recorded.") : (0, import_vue10.h)(
1584
+ (0, import_vue11.h)("h2", { style: styles.subtitle }, "Recent sign-ins"),
1585
+ history.events.value.length === 0 && !history.isLoading.value ? (0, import_vue11.h)("p", { style: styles.muted }, "None recorded.") : (0, import_vue11.h)(
882
1586
  "ul",
883
1587
  { style: styles.list },
884
1588
  history.events.value.map(
885
- (e) => (0, import_vue10.h)("li", { key: e.id, style: styles.row }, [
886
- (0, import_vue10.h)(
887
- "span",
888
- { style: e.outcome === "success" ? styles.ok : styles.bad },
889
- e.outcome
890
- ),
1589
+ (e) => (0, import_vue11.h)("li", { key: e.id, style: styles.row }, [
1590
+ pill(e.outcome === "success" ? "ok" : "bad", e.outcome),
891
1591
  " ",
892
- (0, import_vue10.h)("span", { style: styles.muted }, e.method),
1592
+ (0, import_vue11.h)("span", { style: styles.muted }, e.method),
893
1593
  " ",
894
- (0, import_vue10.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
1594
+ (0, import_vue11.h)("span", { style: styles.mono }, e.ipAddress ?? "\u2014"),
895
1595
  " ",
896
- (0, import_vue10.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
897
- e.location?.proxy || e.location?.hosting ? (0, import_vue10.h)("span", { style: styles.bad }, e.location.proxy ? " proxy/VPN" : " hosting") : "",
1596
+ (0, import_vue11.h)("span", { style: styles.muted }, (0, import_client.describeLocation)(e.location) ?? ""),
1597
+ e.location?.proxy || e.location?.hosting ? [" ", pill("warn", e.location.proxy ? "proxy/VPN" : "hosting")] : "",
898
1598
  " ",
899
- (0, import_vue10.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
1599
+ (0, import_vue11.h)("span", { style: styles.muted }, new Date(e.createdAt).toLocaleString())
900
1600
  ])
901
1601
  )
902
1602
  ),
903
- history.hasMore.value && !history.isLoading.value ? (0, import_vue10.h)(
904
- "button",
905
- {
906
- type: "button",
907
- style: { ...styles.button, marginTop: "8px" },
908
- onClick: () => void history.loadMore()
909
- },
910
- "Load more"
911
- ) : null
1603
+ history.hasMore.value && !history.isLoading.value ? loadMoreButton(() => void history.loadMore()) : null
912
1604
  ] : null
913
1605
  ]);
914
1606
  };
@@ -917,8 +1609,8 @@ var UsersScreen = (0, import_vue10.defineComponent)({
917
1609
 
918
1610
  // src/screens/CatalogScreen.ts
919
1611
  var import_vue_admin10 = require("@fonderie/vue-admin");
920
- var import_vue11 = require("vue");
921
- var CatalogScreen = (0, import_vue11.defineComponent)({
1612
+ var import_vue12 = require("vue");
1613
+ var CatalogScreen = (0, import_vue12.defineComponent)({
922
1614
  name: "FonderieCatalogScreen",
923
1615
  props: { client: { type: Object, required: true } },
924
1616
  setup(props) {
@@ -926,35 +1618,42 @@ var CatalogScreen = (0, import_vue11.defineComponent)({
926
1618
  const money = (v) => v == null ? "\u2014" : (v / 100).toFixed(2);
927
1619
  return () => {
928
1620
  const c = catalog.value;
929
- return (0, import_vue11.h)("div", { style: styles.container }, [
930
- (0, import_vue11.h)("div", { style: styles.toolbar }, [
931
- (0, import_vue11.h)("h1", { style: { ...styles.title, marginBottom: 0 } }, "Catalog"),
932
- refreshButton("Refresh", isLoading.value, () => void refresh())
933
- ]),
934
- error.value ? (0, import_vue11.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
935
- isLoading.value && !c ? (0, import_vue11.h)("p", { style: styles.status }, "Loading\u2026") : null,
1621
+ return (0, import_vue12.h)("div", { style: styles.container }, [
1622
+ pageHeader(
1623
+ "Catalog",
1624
+ "What you sell: plans as configured in code, and as stored in the database, side by side.",
1625
+ [refreshButton("Refresh", isLoading.value, () => void refresh())]
1626
+ ),
1627
+ error.value ? (0, import_vue12.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1628
+ isLoading.value && !c ? (0, import_vue12.h)("p", { style: styles.status }, "Loading\u2026") : null,
936
1629
  c ? [
937
- (0, import_vue11.h)("h2", { style: styles.subtitle }, "Configured (code)"),
938
- (0, import_vue11.h)(
1630
+ (0, import_vue12.h)("h2", { style: { ...styles.subtitle, marginTop: 0 } }, "Configured (code)"),
1631
+ (0, import_vue12.h)(
939
1632
  "pre",
940
1633
  {
941
1634
  style: {
1635
+ ...styles.card,
942
1636
  ...styles.mono,
943
- background: "var(--fonderie-surface-alt,#fafafa)",
944
- padding: "12px",
945
- overflowX: "auto"
1637
+ margin: 0,
1638
+ overflowX: "auto",
1639
+ maxHeight: "360px",
1640
+ lineHeight: 1.6
946
1641
  }
947
1642
  },
948
1643
  JSON.stringify(c.configured, null, 2)
949
1644
  ),
950
- (0, import_vue11.h)("h2", { style: styles.subtitle }, "Stored (database)"),
951
- c.stored.length === 0 ? (0, import_vue11.h)("p", { style: styles.muted }, "No stored plans.") : table(
1645
+ (0, import_vue12.h)("h2", { style: styles.subtitle }, "Stored (database)"),
1646
+ c.stored.length === 0 ? empty(
1647
+ "No stored plans",
1648
+ "Plans are configured in code; nothing has been written to the database.",
1649
+ "catalog"
1650
+ ) : table(
952
1651
  ["Plan", "Tier", "Seats", "Monthly", "Yearly", "Trial", ""],
953
1652
  c.stored.map(
954
- (p) => (0, import_vue11.h)("tr", { key: p.id }, [
1653
+ (p) => (0, import_vue12.h)("tr", { key: p.id }, [
955
1654
  td([
956
- (0, import_vue11.h)("strong", p.name),
957
- p.description ? (0, import_vue11.h)("div", { style: styles.muted }, p.description) : ""
1655
+ (0, import_vue12.h)("strong", p.name),
1656
+ p.description ? (0, import_vue12.h)("div", { style: styles.muted }, p.description) : ""
958
1657
  ]),
959
1658
  td(String(p.tier)),
960
1659
  td(p.seats == null ? "\u221E" : String(p.seats)),
@@ -962,11 +1661,11 @@ var CatalogScreen = (0, import_vue11.defineComponent)({
962
1661
  td(money(p.pricing?.yearly)),
963
1662
  td(p.trialDays ? `${p.trialDays} d` : "\u2014"),
964
1663
  td(
965
- (0, import_vue11.h)(
1664
+ (0, import_vue12.h)(
966
1665
  "button",
967
1666
  {
968
1667
  type: "button",
969
- style: styles.button,
1668
+ style: styles.buttonDanger,
970
1669
  onClick: () => {
971
1670
  if (window.confirm(`Delete stored plan "${p.name}"?`))
972
1671
  void deletePlan(p.id).catch(() => {
@@ -987,17 +1686,17 @@ var CatalogScreen = (0, import_vue11.defineComponent)({
987
1686
 
988
1687
  // src/screens/SubscriberScreen.ts
989
1688
  var import_vue_admin11 = require("@fonderie/vue-admin");
990
- var import_vue12 = require("vue");
991
- var SubscriberScreen = (0, import_vue12.defineComponent)({
1689
+ var import_vue13 = require("vue");
1690
+ var SubscriberScreen = (0, import_vue13.defineComponent)({
992
1691
  name: "FonderieSubscriberScreen",
993
1692
  props: {
994
1693
  client: { type: Object, required: true },
995
1694
  pageSize: { type: Number, default: 50 }
996
1695
  },
997
1696
  setup(props) {
998
- const type = (0, import_vue12.ref)("user");
999
- const idInput = (0, import_vue12.ref)("");
1000
- const subscriber = (0, import_vue12.ref)(null);
1697
+ const type = (0, import_vue13.ref)("user");
1698
+ const idInput = (0, import_vue13.ref)("");
1699
+ const subscriber = (0, import_vue13.ref)(null);
1001
1700
  const list = (0, import_vue_admin11.useAdminSubscribers)(props.client, { limit: props.pageSize });
1002
1701
  const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = (0, import_vue_admin11.useAdminSubscriber)(props.client, subscriber, { limit: 20 });
1003
1702
  const open = (t2, id) => {
@@ -1006,22 +1705,25 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1006
1705
  idInput.value = id;
1007
1706
  subscriber.value = { type: t2, id };
1008
1707
  };
1009
- const amount = (0, import_vue12.ref)("");
1010
- const note = (0, import_vue12.ref)("");
1011
- const granted = (0, import_vue12.ref)(null);
1012
- const input = (model, placeholder, extra = {}) => (0, import_vue12.h)("input", {
1708
+ const amount = (0, import_vue13.ref)("");
1709
+ const note = (0, import_vue13.ref)("");
1710
+ const granted = (0, import_vue13.ref)(null);
1711
+ const input = (model, placeholder, extra = {}) => (0, import_vue13.h)("input", {
1013
1712
  value: model.value,
1014
1713
  onInput: (e) => model.value = e.target.value,
1015
1714
  placeholder,
1016
- style: { ...styles.button, cursor: "text", ...extra },
1715
+ style: { ...styles.input, ...extra },
1017
1716
  "aria-label": placeholder
1018
1717
  });
1019
1718
  return () => {
1020
1719
  const s = subscription.value;
1021
1720
  const w = wallet.value;
1022
- return (0, import_vue12.h)("div", { style: styles.container }, [
1023
- (0, import_vue12.h)("h1", { style: styles.title }, "Subscriber"),
1024
- (0, import_vue12.h)(
1721
+ return (0, import_vue13.h)("div", { style: styles.container }, [
1722
+ pageHeader(
1723
+ "Subscribers",
1724
+ "Who is subscribed. Open one for their plan, wallet and ledger, and to grant credit."
1725
+ ),
1726
+ (0, import_vue13.h)(
1025
1727
  "form",
1026
1728
  {
1027
1729
  style: styles.toolbar,
@@ -1032,26 +1734,26 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1032
1734
  }
1033
1735
  },
1034
1736
  [
1035
- (0, import_vue12.h)(
1737
+ (0, import_vue13.h)(
1036
1738
  "select",
1037
1739
  {
1038
1740
  value: type.value,
1039
1741
  onChange: (e) => type.value = e.target.value,
1040
- style: styles.button,
1742
+ style: styles.input,
1041
1743
  "aria-label": "Type"
1042
1744
  },
1043
1745
  [
1044
- (0, import_vue12.h)("option", { value: "user" }, "user"),
1045
- (0, import_vue12.h)("option", { value: "workspace" }, "workspace")
1746
+ (0, import_vue13.h)("option", { value: "user" }, "user"),
1747
+ (0, import_vue13.h)("option", { value: "workspace" }, "workspace")
1046
1748
  ]
1047
1749
  ),
1048
1750
  input(idInput, "subscriber id", { minWidth: "280px" }),
1049
- (0, import_vue12.h)(
1751
+ (0, import_vue13.h)(
1050
1752
  "button",
1051
- { type: "submit", style: styles.button, disabled: isLoading.value },
1052
- "Look up"
1753
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1754
+ [icon("search", 14), "Look up"]
1053
1755
  ),
1054
- subscriber.value ? (0, import_vue12.h)(
1756
+ subscriber.value ? (0, import_vue13.h)(
1055
1757
  "button",
1056
1758
  {
1057
1759
  type: "button",
@@ -1062,8 +1764,8 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1062
1764
  granted.value = null;
1063
1765
  }
1064
1766
  },
1065
- "\u2190 All subscribers"
1066
- ) : (0, import_vue12.h)(
1767
+ [icon("back", 14), "All subscribers"]
1768
+ ) : (0, import_vue13.h)(
1067
1769
  "button",
1068
1770
  {
1069
1771
  type: "button",
@@ -1075,35 +1777,43 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1075
1777
  )
1076
1778
  ]
1077
1779
  ),
1078
- !subscriber.value ? (0, import_vue12.h)("div", [
1079
- list.error.value ? (0, import_vue12.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1080
- list.subscriptions.value.length === 0 && !list.isLoading.value ? (0, import_vue12.h)("p", { style: styles.muted }, "No subscribers yet.") : table(
1780
+ !subscriber.value ? (0, import_vue13.h)("div", [
1781
+ list.error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, list.error.value.explanation) : null,
1782
+ list.subscriptions.value.length === 0 && !list.isLoading.value ? empty(
1783
+ "No subscribers yet",
1784
+ "Subscriptions appear here once someone checks out.",
1785
+ "subscriber"
1786
+ ) : table(
1081
1787
  ["Subscriber", "Plan", "Status", "Renews"],
1082
1788
  list.subscriptions.value.map(
1083
- (sub) => (0, import_vue12.h)("tr", { key: sub.id }, [
1789
+ (sub) => (0, import_vue13.h)("tr", { key: sub.id }, [
1084
1790
  td(
1085
- (0, import_vue12.h)(
1791
+ (0, import_vue13.h)(
1086
1792
  "button",
1087
1793
  {
1088
1794
  type: "button",
1089
- style: { ...styles.navItem, padding: 0, ...styles.mono },
1795
+ style: { ...styles.link, ...styles.mono },
1090
1796
  onClick: () => open(sub.subscriberType, sub.subscriberId)
1091
1797
  },
1092
1798
  `${sub.subscriberType}/${sub.subscriberId}`
1093
1799
  )
1094
1800
  ),
1095
1801
  td(`${sub.plan} \xB7 ${sub.interval}`),
1802
+ td([
1803
+ pill(
1804
+ sub.status === "active" ? "ok" : sub.status === "trialing" ? "info" : sub.status === "past_due" ? "warn" : "neutral",
1805
+ sub.status
1806
+ ),
1807
+ sub.cancelAtPeriodEnd ? [" ", pill("warn", "cancels", false)] : null
1808
+ ]),
1096
1809
  td(
1097
- sub.cancelAtPeriodEnd ? [sub.status, " ", (0, import_vue12.h)("span", { style: styles.badge }, "cancels")] : sub.status
1098
- ),
1099
- td(
1100
- sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : (0, import_vue12.h)("span", { style: styles.muted }, "\u2014")
1810
+ sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd).toLocaleDateString() : (0, import_vue13.h)("span", { style: styles.muted }, "\u2014")
1101
1811
  )
1102
1812
  ])
1103
1813
  )
1104
1814
  ),
1105
- list.isLoading.value ? (0, import_vue12.h)("p", { style: styles.status }, "Loading\u2026") : null,
1106
- list.hasMore.value && !list.isLoading.value ? (0, import_vue12.h)(
1815
+ list.isLoading.value ? (0, import_vue13.h)("p", { style: styles.status }, "Loading\u2026") : null,
1816
+ list.hasMore.value && !list.isLoading.value ? (0, import_vue13.h)(
1107
1817
  "button",
1108
1818
  {
1109
1819
  type: "button",
@@ -1113,37 +1823,58 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1113
1823
  "Load more"
1114
1824
  ) : null
1115
1825
  ]) : null,
1116
- error.value ? (0, import_vue12.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1826
+ error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1117
1827
  subscriber.value && !isLoading.value ? [
1118
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Subscription"),
1119
- s ? (0, import_vue12.h)("table", { style: styles.table }, [
1120
- (0, import_vue12.h)("tbody", [
1121
- (0, import_vue12.h)("tr", [td("plan"), td([(0, import_vue12.h)("strong", s.plan), ` \xB7 ${s.interval}`])]),
1122
- (0, import_vue12.h)("tr", [
1828
+ (0, import_vue13.h)("h2", { style: styles.subtitle }, "Subscription"),
1829
+ s ? (0, import_vue13.h)("table", { style: styles.table }, [
1830
+ (0, import_vue13.h)("tbody", [
1831
+ (0, import_vue13.h)("tr", [td("plan"), td([(0, import_vue13.h)("strong", s.plan), ` \xB7 ${s.interval}`])]),
1832
+ (0, import_vue13.h)("tr", [
1123
1833
  td("status"),
1124
1834
  td(`${s.status}${s.cancelAtPeriodEnd ? " \xB7 cancels at period end" : ""}`)
1125
1835
  ]),
1126
- (0, import_vue12.h)("tr", [
1836
+ (0, import_vue13.h)("tr", [
1127
1837
  td("period"),
1128
1838
  td(
1129
1839
  `${s.currentPeriodStart ? new Date(s.currentPeriodStart).toLocaleDateString() : "\u2014"} \u2192 ${s.currentPeriodEnd ? new Date(s.currentPeriodEnd).toLocaleDateString() : "\u2014"}`
1130
1840
  )
1131
1841
  ]),
1132
- (0, import_vue12.h)("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
1842
+ (0, import_vue13.h)("tr", [td("provider"), td(s.providerSubscriptionId ?? "\u2014", styles.mono)])
1133
1843
  ])
1134
- ]) : (0, import_vue12.h)("p", { style: styles.muted }, "No subscription."),
1135
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Wallet"),
1844
+ ]) : (0, import_vue13.h)("p", { style: styles.muted }, "No subscription."),
1845
+ (0, import_vue13.h)("h2", { style: styles.subtitle }, "Wallet"),
1136
1846
  w ? [
1137
- (0, import_vue12.h)("p", [
1138
- (0, import_vue12.h)("strong", w.balance),
1139
- ` ${w.currency} `,
1140
- (0, import_vue12.h)(
1141
- "span",
1142
- { style: styles.muted },
1143
- `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
1144
- )
1145
- ]),
1146
- (0, import_vue12.h)(
1847
+ (0, import_vue13.h)(
1848
+ "p",
1849
+ {
1850
+ style: {
1851
+ ...styles.card,
1852
+ display: "flex",
1853
+ alignItems: "baseline",
1854
+ gap: "8px",
1855
+ flexWrap: "wrap"
1856
+ }
1857
+ },
1858
+ [
1859
+ (0, import_vue13.h)(
1860
+ "strong",
1861
+ {
1862
+ style: {
1863
+ fontSize: "22px",
1864
+ letterSpacing: "var(--fonderie-tracking-display,-0.05em)"
1865
+ }
1866
+ },
1867
+ w.balance
1868
+ ),
1869
+ ` ${w.currency} `,
1870
+ (0, import_vue13.h)(
1871
+ "span",
1872
+ { style: styles.muted },
1873
+ `(granted ${w.granted ?? "0"} \xB7 purchased ${w.purchased ?? "0"}) \xB7 minor units, precision ${w.precision}`
1874
+ )
1875
+ ]
1876
+ ),
1877
+ (0, import_vue13.h)(
1147
1878
  "form",
1148
1879
  {
1149
1880
  style: styles.toolbar,
@@ -1168,23 +1899,23 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1168
1899
  [
1169
1900
  input(amount, "amount (minor units)"),
1170
1901
  input(note, "reason (optional)", { minWidth: "220px" }),
1171
- (0, import_vue12.h)(
1902
+ (0, import_vue13.h)(
1172
1903
  "button",
1173
1904
  {
1174
1905
  type: "submit",
1175
- style: styles.button,
1906
+ style: styles.buttonPrimary,
1176
1907
  disabled: !/^\d+$/.test(amount.value.trim())
1177
1908
  },
1178
- "Grant"
1909
+ [icon("plus", 14), "Grant"]
1179
1910
  ),
1180
- granted.value ? (0, import_vue12.h)("span", { style: styles.ok }, granted.value) : null
1911
+ granted.value ? (0, import_vue13.h)("span", { style: styles.ok }, granted.value) : null
1181
1912
  ]
1182
1913
  ),
1183
- (0, import_vue12.h)("h2", { style: styles.subtitle }, "Ledger"),
1184
- ledger.value.length === 0 ? (0, import_vue12.h)("p", { style: styles.muted }, "Nothing moved yet.") : table(
1914
+ (0, import_vue13.h)("h2", { style: styles.subtitle }, "Ledger"),
1915
+ ledger.value.length === 0 ? empty("Nothing moved yet", void 0, "subscriber") : table(
1185
1916
  ["When", "Kind", "Amount", "Note"],
1186
1917
  ledger.value.map(
1187
- (t2) => (0, import_vue12.h)("tr", { key: t2.id }, [
1918
+ (t2) => (0, import_vue13.h)("tr", { key: t2.id }, [
1188
1919
  td(new Date(t2.createdAt).toLocaleString(), styles.muted),
1189
1920
  td(t2.type),
1190
1921
  td(t2.amount, styles.mono),
@@ -1192,7 +1923,7 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1192
1923
  ])
1193
1924
  )
1194
1925
  ),
1195
- hasMoreLedger.value ? (0, import_vue12.h)(
1926
+ hasMoreLedger.value ? (0, import_vue13.h)(
1196
1927
  "button",
1197
1928
  {
1198
1929
  type: "button",
@@ -1201,7 +1932,7 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1201
1932
  },
1202
1933
  "Load more"
1203
1934
  ) : null
1204
- ] : (0, import_vue12.h)(
1935
+ ] : (0, import_vue13.h)(
1205
1936
  "p",
1206
1937
  { style: styles.muted },
1207
1938
  "No wallet \u2014 billing has no wallet configuration."
@@ -1214,30 +1945,33 @@ var SubscriberScreen = (0, import_vue12.defineComponent)({
1214
1945
 
1215
1946
  // src/screens/AuditScreen.ts
1216
1947
  var import_vue_admin12 = require("@fonderie/vue-admin");
1217
- var import_vue13 = require("vue");
1218
- var AuditScreen = (0, import_vue13.defineComponent)({
1948
+ var import_vue14 = require("vue");
1949
+ var AuditScreen = (0, import_vue14.defineComponent)({
1219
1950
  name: "FonderieAuditScreen",
1220
1951
  props: {
1221
1952
  client: { type: Object, required: true },
1222
1953
  pageSize: { type: Number, default: 50 }
1223
1954
  },
1224
1955
  setup(props) {
1225
- const draft = (0, import_vue13.ref)({ workspaceId: "", type: "", actorId: "" });
1226
- const query = (0, import_vue13.ref)({ limit: props.pageSize });
1956
+ const draft = (0, import_vue14.ref)({ workspaceId: "", type: "", actorId: "" });
1957
+ const query = (0, import_vue14.ref)({ limit: props.pageSize });
1227
1958
  const { events, hasMore, isLoading, error, refresh, loadMore } = (0, import_vue_admin12.useAdminAudit)(
1228
1959
  props.client,
1229
1960
  query
1230
1961
  );
1231
- const field = (key, placeholder) => (0, import_vue13.h)("input", {
1962
+ const field = (key, placeholder) => (0, import_vue14.h)("input", {
1232
1963
  value: draft.value[key],
1233
1964
  onInput: (e) => draft.value = { ...draft.value, [key]: e.target.value },
1234
1965
  placeholder,
1235
- style: { ...styles.button, cursor: "text" },
1966
+ style: styles.input,
1236
1967
  "aria-label": placeholder
1237
1968
  });
1238
- return () => (0, import_vue13.h)("div", { style: styles.container }, [
1239
- (0, import_vue13.h)("h1", { style: styles.title }, "Audit"),
1240
- (0, import_vue13.h)(
1969
+ return () => (0, import_vue14.h)("div", { style: styles.container }, [
1970
+ pageHeader(
1971
+ "Audit",
1972
+ "What happened, across every workspace unless you name one. The chain's integrity verdict is on the Doctor page."
1973
+ ),
1974
+ (0, import_vue14.h)(
1241
1975
  "form",
1242
1976
  {
1243
1977
  style: styles.toolbar,
@@ -1256,19 +1990,19 @@ var AuditScreen = (0, import_vue13.defineComponent)({
1256
1990
  field("workspaceId", "workspace id (all if empty)"),
1257
1991
  field("type", "event type"),
1258
1992
  field("actorId", "actor id"),
1259
- (0, import_vue13.h)(
1993
+ (0, import_vue14.h)(
1260
1994
  "button",
1261
- { type: "submit", style: styles.button, disabled: isLoading.value },
1262
- "Filter"
1995
+ { type: "submit", style: styles.buttonPrimary, disabled: isLoading.value },
1996
+ [icon("search", 14), "Filter"]
1263
1997
  ),
1264
1998
  refreshButton("Refresh", isLoading.value, () => void refresh())
1265
1999
  ]
1266
2000
  ),
1267
- error.value ? (0, import_vue13.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
1268
- events.value.length === 0 && !isLoading.value ? (0, import_vue13.h)("p", { style: styles.muted }, "Nothing recorded for this filter.") : table(
2001
+ error.value ? (0, import_vue14.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : null,
2002
+ events.value.length === 0 && !isLoading.value ? empty("Nothing recorded for this filter", void 0, "audit") : table(
1269
2003
  ["When", "Type", "Workspace", "Actor", "Request"],
1270
2004
  events.value.map(
1271
- (e) => (0, import_vue13.h)("tr", { key: e.id }, [
2005
+ (e) => (0, import_vue14.h)("tr", { key: e.id }, [
1272
2006
  td(new Date(e.createdAt).toLocaleString(), styles.muted),
1273
2007
  td(e.type, styles.mono),
1274
2008
  td(String(e.payload["workspaceId"] ?? "\u2014"), styles.mono),
@@ -1277,16 +2011,8 @@ var AuditScreen = (0, import_vue13.defineComponent)({
1277
2011
  ])
1278
2012
  )
1279
2013
  ),
1280
- isLoading.value ? (0, import_vue13.h)("p", { style: styles.status }, "Loading\u2026") : null,
1281
- hasMore.value && !isLoading.value ? (0, import_vue13.h)(
1282
- "button",
1283
- {
1284
- type: "button",
1285
- style: { ...styles.button, marginTop: "8px" },
1286
- onClick: () => void loadMore()
1287
- },
1288
- "Load more"
1289
- ) : null
2014
+ isLoading.value ? (0, import_vue14.h)("p", { style: styles.status }, "Loading\u2026") : null,
2015
+ hasMore.value && !isLoading.value ? loadMoreButton(() => void loadMore()) : null
1290
2016
  ]);
1291
2017
  }
1292
2018
  });
@@ -1323,7 +2049,24 @@ var NAV = [
1323
2049
  ]
1324
2050
  }
1325
2051
  ];
1326
- var AdminShell = (0, import_vue14.defineComponent)({
2052
+ var ICON = {
2053
+ attention: "attention",
2054
+ modules: "modules",
2055
+ environment: "environment",
2056
+ doctor: "doctor",
2057
+ routes: "routes",
2058
+ users: "users",
2059
+ catalog: "catalog",
2060
+ subscriber: "subscriber",
2061
+ settings: "settings",
2062
+ templates: "templates",
2063
+ audit: "audit",
2064
+ log: "log",
2065
+ tokens: "tokens",
2066
+ migrations: "migrations"
2067
+ };
2068
+ var NARROW = "(max-width: 820px)";
2069
+ var AdminShell = (0, import_vue15.defineComponent)({
1327
2070
  name: "FonderieAdminShell",
1328
2071
  props: {
1329
2072
  client: { type: Object, required: true },
@@ -1340,75 +2083,75 @@ var AdminShell = (0, import_vue14.defineComponent)({
1340
2083
  environment: { type: String, default: void 0 },
1341
2084
  // Controlled navigation: pass `page` and listen to `navigate` to own the
1342
2085
  // URL. Omit `page` and the shell keeps it itself.
1343
- page: { type: String, default: void 0 }
2086
+ page: { type: String, default: void 0 },
2087
+ // Shown at the top of the sidebar. Defaults to "Admin".
2088
+ appName: { type: String, default: "Admin" },
2089
+ // A badge beside the name — the deployment's environment ("production").
2090
+ // Production is tinted so an operator always knows where their clicks land.
2091
+ envLabel: { type: String, default: void 0 }
1344
2092
  },
2093
+ // `footer` slot: pinned to the bottom of the sidebar — session controls
2094
+ // (theme, sign out). The React shell takes the same thing as a `footer` prop.
1345
2095
  emits: { navigate: (_page) => true },
1346
- setup(props, { emit }) {
1347
- const own = (0, import_vue14.ref)("attention");
1348
- const current = (0, import_vue14.computed)(() => props.page ?? own.value);
1349
- const editing = (0, import_vue14.ref)(null);
2096
+ setup(props, { emit, slots }) {
2097
+ const query = typeof window !== "undefined" && window.matchMedia ? window.matchMedia(NARROW) : null;
2098
+ const narrow = (0, import_vue15.ref)(query?.matches ?? false);
2099
+ const drawer = (0, import_vue15.ref)(false);
2100
+ const onMedia = (e) => narrow.value = e.matches;
2101
+ (0, import_vue15.onMounted)(() => query?.addEventListener("change", onMedia));
2102
+ (0, import_vue15.onBeforeUnmount)(() => query?.removeEventListener("change", onMedia));
2103
+ const own = (0, import_vue15.ref)("attention");
2104
+ const current = (0, import_vue15.computed)(() => props.page ?? own.value);
2105
+ const editing = (0, import_vue15.ref)(null);
1350
2106
  const go = (p) => {
1351
2107
  editing.value = null;
1352
2108
  emit("navigate", p);
1353
2109
  if (props.page === void 0) own.value = p;
2110
+ drawer.value = false;
1354
2111
  };
1355
- const back = () => (0, import_vue14.h)(
1356
- "button",
1357
- { type: "button", style: styles.button, onClick: () => editing.value = null },
1358
- "\u2190 Back"
1359
- );
2112
+ const back = (label) => (0, import_vue15.h)("div", { style: { padding: "24px 40px 0" } }, [
2113
+ (0, import_vue15.h)(
2114
+ "button",
2115
+ { type: "button", style: styles.buttonGhost, onClick: () => editing.value = null },
2116
+ [icon("back", 14), label]
2117
+ )
2118
+ ]);
2119
+ const missing = (text) => (0, import_vue15.h)("p", { style: { ...styles.status, padding: "32px 40px" } }, text);
1360
2120
  const body = () => {
1361
2121
  const c = props.client;
1362
2122
  switch (current.value) {
1363
2123
  case "attention":
1364
- return (0, import_vue14.h)(AttentionScreen, { client: c });
2124
+ return (0, import_vue15.h)(AttentionScreen, { client: c });
1365
2125
  case "modules":
1366
- return (0, import_vue14.h)(ModulesScreen, { client: c });
2126
+ return (0, import_vue15.h)(ModulesScreen, { client: c });
1367
2127
  case "doctor":
1368
- return (0, import_vue14.h)(DoctorScreen, { client: c });
2128
+ return (0, import_vue15.h)(DoctorScreen, { client: c });
1369
2129
  case "environment":
1370
- return (0, import_vue14.h)(EnvironmentScreen, { client: c });
2130
+ return (0, import_vue15.h)(EnvironmentScreen, { client: c });
1371
2131
  case "routes":
1372
- return (0, import_vue14.h)(RoutesScreen, { client: c });
2132
+ return (0, import_vue15.h)(RoutesScreen, { client: c });
1373
2133
  case "tokens":
1374
- return (0, import_vue14.h)(TokensScreen, { client: c });
2134
+ return (0, import_vue15.h)(TokensScreen, { client: c });
1375
2135
  case "migrations":
1376
- return (0, import_vue14.h)(MigrationsScreen, { client: c });
2136
+ return (0, import_vue15.h)(MigrationsScreen, { client: c });
1377
2137
  case "log":
1378
- return (0, import_vue14.h)(AdminLogScreen, { client: c });
2138
+ return (0, import_vue15.h)(AdminLogScreen, { client: c });
1379
2139
  case "catalog":
1380
- return props.billingClient ? (0, import_vue14.h)(CatalogScreen, { client: props.billingClient }) : (0, import_vue14.h)(
1381
- "p",
1382
- { style: styles.status },
1383
- "Pass a BillingAdminClient to see the catalog here."
1384
- );
2140
+ return props.billingClient ? (0, import_vue15.h)(CatalogScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to see the catalog here.");
1385
2141
  case "subscriber":
1386
- return props.billingClient ? (0, import_vue14.h)(SubscriberScreen, { client: props.billingClient }) : (0, import_vue14.h)(
1387
- "p",
1388
- { style: styles.status },
1389
- "Pass a BillingAdminClient to look up subscribers here."
1390
- );
2142
+ return props.billingClient ? (0, import_vue15.h)(SubscriberScreen, { client: props.billingClient }) : missing("Pass a BillingAdminClient to look up subscribers here.");
1391
2143
  case "audit":
1392
- return props.auditClient ? (0, import_vue14.h)(AuditScreen, { client: props.auditClient }) : (0, import_vue14.h)(
1393
- "p",
1394
- { style: styles.status },
1395
- "Pass an AuditAdminClient to see the audit trail here."
1396
- );
2144
+ return props.auditClient ? (0, import_vue15.h)(AuditScreen, { client: props.auditClient }) : missing("Pass an AuditAdminClient to see the audit trail here.");
1397
2145
  case "users":
1398
- return props.authClient ? (0, import_vue14.h)(UsersScreen, { client: props.authClient }) : (0, import_vue14.h)("p", { style: styles.status }, "Pass an AuthAdminClient to look up users here.");
2146
+ return props.authClient ? (0, import_vue15.h)(UsersScreen, { client: props.authClient }) : missing("Pass an AuthAdminClient to look up users here.");
1399
2147
  case "settings": {
1400
2148
  const cc = props.configClient;
1401
- if (!cc)
1402
- return (0, import_vue14.h)(
1403
- "p",
1404
- { style: styles.status },
1405
- "Pass a ConfigAdminClient to manage config and secrets here."
1406
- );
2149
+ if (!cc) return missing("Pass a ConfigAdminClient to manage config and secrets here.");
1407
2150
  const e = editing.value;
1408
2151
  if (e && e.kind !== "template")
1409
2152
  return [
1410
- back(),
1411
- (0, import_vue14.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
2153
+ back("Config & secrets"),
2154
+ (0, import_vue15.h)(import_vue_config_admin_screens.ConfigEditorScreen, {
1412
2155
  client: cc,
1413
2156
  kind: e.kind,
1414
2157
  configKey: e.key,
@@ -1416,70 +2159,147 @@ var AdminShell = (0, import_vue14.defineComponent)({
1416
2159
  onSaved: () => editing.value = null
1417
2160
  })
1418
2161
  ];
1419
- return (0, import_vue14.h)(import_vue_config_admin_screens.ConfigListScreen, {
2162
+ return (0, import_vue15.h)(import_vue_config_admin_screens.ConfigListScreen, {
1420
2163
  client: cc,
1421
2164
  ...props.environment !== void 0 ? { environment: props.environment } : {},
1422
2165
  "onSelect-config": (key) => editing.value = { kind: "config", key },
1423
- "onSelect-secret": (key) => editing.value = { kind: "secret", key }
2166
+ "onSelect-secret": (key) => editing.value = { kind: "secret", key },
2167
+ allowCreate: true,
2168
+ "onCreate-config": () => editing.value = { kind: "config", key: "" },
2169
+ "onCreate-secret": () => editing.value = { kind: "secret", key: "" }
1424
2170
  });
1425
2171
  }
1426
2172
  case "templates": {
1427
2173
  const kc = props.courierClient;
1428
- if (!kc)
1429
- return (0, import_vue14.h)(
1430
- "p",
1431
- { style: styles.status },
1432
- "Pass a CourierAdminClient to manage templates here."
1433
- );
2174
+ if (!kc) return missing("Pass a CourierAdminClient to manage templates here.");
1434
2175
  const e = editing.value;
1435
2176
  if (e?.kind === "template")
1436
2177
  return [
1437
- back(),
1438
- (0, import_vue14.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
2178
+ back("Templates"),
2179
+ (0, import_vue15.h)(import_vue_courier_admin_screens.TemplateEditorScreen, {
1439
2180
  client: kc,
1440
2181
  type: e.type,
1441
2182
  onSaved: () => editing.value = null
1442
2183
  })
1443
2184
  ];
1444
- return (0, import_vue14.h)(import_vue_courier_admin_screens.TemplateListScreen, {
2185
+ return (0, import_vue15.h)(import_vue_courier_admin_screens.TemplateListScreen, {
1445
2186
  client: kc,
1446
2187
  "onSelect-template": (t2) => editing.value = { kind: "template", type: t2.type }
1447
2188
  });
1448
2189
  }
1449
2190
  }
1450
2191
  };
1451
- return () => (0, import_vue14.h)("div", { style: styles.shell }, [
1452
- (0, import_vue14.h)(
1453
- "nav",
1454
- { style: styles.nav, "aria-label": "Admin" },
1455
- NAV.map((g) => {
1456
- const items = g.items.filter(
1457
- (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)
1458
- );
1459
- if (items.length === 0) return null;
1460
- return (0, import_vue14.h)("div", { key: g.group }, [
1461
- (0, import_vue14.h)("div", { style: styles.navGroup }, g.group),
1462
- ...items.map(
1463
- (i) => (0, import_vue14.h)(
2192
+ 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);
2193
+ const brand = () => {
2194
+ const production = props.envLabel ? /^prod/i.test(props.envLabel) : false;
2195
+ return (0, import_vue15.h)("div", { style: styles.navBrand }, [
2196
+ (0, import_vue15.h)("div", { style: styles.navMark }, [icon("mark", 15)]),
2197
+ (0, import_vue15.h)("div", { style: { minWidth: 0, flex: 1 } }, [
2198
+ (0, import_vue15.h)("div", { style: styles.navBrandTitle }, props.appName),
2199
+ props.envLabel ? (0, import_vue15.h)(
2200
+ "span",
2201
+ {
2202
+ style: {
2203
+ ...styles.badge,
2204
+ marginTop: "3px",
2205
+ padding: "0 7px",
2206
+ fontSize: "11px",
2207
+ ...production ? {
2208
+ color: "var(--fonderie-danger,#e00)",
2209
+ borderColor: "color-mix(in srgb, var(--fonderie-danger,#e00) 35%, transparent)"
2210
+ } : {}
2211
+ }
2212
+ },
2213
+ props.envLabel
2214
+ ) : null
2215
+ ]),
2216
+ narrow.value ? (0, import_vue15.h)(
2217
+ "button",
2218
+ {
2219
+ type: "button",
2220
+ style: styles.buttonGhost,
2221
+ "aria-label": "Close menu",
2222
+ onClick: () => drawer.value = false
2223
+ },
2224
+ [icon("close")]
2225
+ ) : null
2226
+ ]);
2227
+ };
2228
+ const nav = () => (0, import_vue15.h)(
2229
+ "nav",
2230
+ {
2231
+ style: {
2232
+ ...styles.nav,
2233
+ ...narrow.value ? { width: "280px", maxWidth: "85vw", boxShadow: "0 10px 40px rgba(0,0,0,.25)" } : {}
2234
+ },
2235
+ "aria-label": "Admin"
2236
+ },
2237
+ [
2238
+ brand(),
2239
+ (0, import_vue15.h)(
2240
+ "div",
2241
+ { style: styles.navScroll },
2242
+ NAV.map((g) => {
2243
+ const items = g.items.filter(visible);
2244
+ if (items.length === 0) return null;
2245
+ return (0, import_vue15.h)("div", { key: g.group }, [
2246
+ (0, import_vue15.h)("div", { style: styles.navGroup }, g.group),
2247
+ ...items.map((i) => {
2248
+ const active = current.value === i.page;
2249
+ return (0, import_vue15.h)(
2250
+ "button",
2251
+ {
2252
+ key: i.page,
2253
+ type: "button",
2254
+ class: "fonderie-admin-nav-item",
2255
+ style: { ...styles.navItem, ...active ? styles.navItemActive : {} },
2256
+ "aria-current": active ? "page" : void 0,
2257
+ onClick: () => go(i.page)
2258
+ },
2259
+ [icon(ICON[i.page], 16, active ? styles.navIconActive : void 0), i.label]
2260
+ );
2261
+ })
2262
+ ]);
2263
+ })
2264
+ ),
2265
+ slots["footer"] ? (0, import_vue15.h)("div", { style: styles.navFooter }, slots["footer"]()) : null
2266
+ ]
2267
+ );
2268
+ return () => {
2269
+ const title = NAV.flatMap((g) => g.items).find((i) => i.page === current.value)?.label ?? "";
2270
+ return (0, import_vue15.h)(
2271
+ "div",
2272
+ { style: { ...styles.shell, ...narrow.value ? { flexDirection: "column" } : {} } },
2273
+ [
2274
+ ...narrow.value ? [
2275
+ (0, import_vue15.h)("div", { style: styles.topbar }, [
2276
+ (0, import_vue15.h)(
1464
2277
  "button",
1465
2278
  {
1466
- key: i.page,
1467
2279
  type: "button",
1468
- style: {
1469
- ...styles.navItem,
1470
- ...current.value === i.page ? styles.navItemActive : {}
1471
- },
1472
- "aria-current": current.value === i.page ? "page" : void 0,
1473
- onClick: () => go(i.page)
2280
+ style: styles.buttonGhost,
2281
+ "aria-label": "Open menu",
2282
+ "aria-expanded": drawer.value,
2283
+ onClick: () => drawer.value = true
1474
2284
  },
1475
- i.label
1476
- )
1477
- )
1478
- ]);
1479
- })
1480
- ),
1481
- (0, import_vue14.h)("main", { style: styles.main }, body())
1482
- ]);
2285
+ [icon("menu")]
2286
+ ),
2287
+ (0, import_vue15.h)("span", { style: { fontWeight: 600, fontSize: "14px" } }, title),
2288
+ props.envLabel ? (0, import_vue15.h)("span", { style: { ...styles.badge, marginLeft: "auto" } }, props.envLabel) : null
2289
+ ]),
2290
+ drawer.value ? (0, import_vue15.h)("div", { style: styles.drawer }, [
2291
+ nav(),
2292
+ (0, import_vue15.h)("div", {
2293
+ style: styles.scrim,
2294
+ "aria-hidden": "true",
2295
+ onClick: () => drawer.value = false
2296
+ })
2297
+ ]) : null
2298
+ ] : [nav()],
2299
+ (0, import_vue15.h)("main", { class: "fonderie-admin-main", style: styles.main }, body())
2300
+ ]
2301
+ );
2302
+ };
1483
2303
  }
1484
2304
  });
1485
2305
  // Annotate the CommonJS export names for ESM import in node: