@fonderie/vue-admin-screens 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,593 @@
1
+ // src/screens/AdminShell.ts
2
+ import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/vue-config-admin-screens";
3
+ import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/vue-courier-admin-screens";
4
+ import { computed, defineComponent as defineComponent8, h as h9, ref } from "vue";
5
+
6
+ // src/styles.ts
7
+ var styles = {
8
+ shell: {
9
+ display: "flex",
10
+ minHeight: "100vh",
11
+ fontFamily: "system-ui, sans-serif",
12
+ color: "#111"
13
+ },
14
+ nav: { width: "220px", borderRight: "1px solid #eee", padding: "16px", flexShrink: 0 },
15
+ navGroup: {
16
+ fontSize: "11px",
17
+ textTransform: "uppercase",
18
+ letterSpacing: "0.6px",
19
+ color: "#888",
20
+ margin: "16px 0 6px"
21
+ },
22
+ navItem: {
23
+ display: "block",
24
+ width: "100%",
25
+ textAlign: "left",
26
+ background: "none",
27
+ border: "none",
28
+ borderRadius: "6px",
29
+ padding: "6px 8px",
30
+ cursor: "pointer",
31
+ fontSize: "14px",
32
+ color: "#111"
33
+ },
34
+ navItemActive: { background: "#f3f4f6", fontWeight: 600 },
35
+ main: { flex: 1, padding: "24px", maxWidth: "960px" },
36
+ container: { padding: "24px", maxWidth: "960px" },
37
+ title: { fontSize: "20px", fontWeight: 700, marginTop: 0, marginBottom: "12px" },
38
+ subtitle: { fontSize: "15px", fontWeight: 600, marginTop: "24px", marginBottom: "8px" },
39
+ status: { padding: "12px", color: "#666" },
40
+ error: { color: "#e11d48", marginBottom: "12px", fontSize: "14px" },
41
+ ok: { color: "#15803d", fontWeight: 600 },
42
+ bad: { color: "#e11d48", fontWeight: 600 },
43
+ advice: { color: "#b45309" },
44
+ muted: { color: "#666", fontSize: "13px" },
45
+ mono: { fontFamily: "ui-monospace, monospace", fontSize: "13px" },
46
+ table: { width: "100%", borderCollapse: "collapse", fontSize: "14px" },
47
+ th: {
48
+ textAlign: "left",
49
+ borderBottom: "1px solid #ddd",
50
+ padding: "6px 8px",
51
+ fontWeight: 600,
52
+ color: "#444"
53
+ },
54
+ td: { borderBottom: "1px solid #eee", padding: "6px 8px", verticalAlign: "top" },
55
+ list: { listStyle: "none", padding: 0, margin: 0 },
56
+ row: { borderBottom: "1px solid #eee", padding: "8px 0" },
57
+ badge: {
58
+ display: "inline-block",
59
+ borderRadius: "999px",
60
+ padding: "1px 8px",
61
+ fontSize: "12px",
62
+ background: "#f3f4f6"
63
+ },
64
+ button: {
65
+ background: "none",
66
+ border: "1px solid #ddd",
67
+ borderRadius: "8px",
68
+ padding: "4px 10px",
69
+ cursor: "pointer",
70
+ fontSize: "13px"
71
+ },
72
+ toolbar: { display: "flex", gap: "8px", alignItems: "center", marginBottom: "12px" }
73
+ };
74
+
75
+ // src/screens/AdminLogScreen.ts
76
+ import { useAdminLog } from "@fonderie/vue-admin";
77
+ import { defineComponent, h as h2 } from "vue";
78
+
79
+ // src/screens/common.ts
80
+ import { h } from "vue";
81
+ function page(title, state, content, toolbar = [], options = {}) {
82
+ const body = state.isLoading.value ? h("p", { style: styles.status }, options.loadingText ?? "Loading\u2026") : state.error.value ? h(
83
+ "p",
84
+ { style: styles.error, role: "alert" },
85
+ (options.errorText ?? ((e) => e.explanation))(state.error.value)
86
+ ) : content();
87
+ return h("div", { style: styles.container }, [
88
+ h("div", { style: styles.toolbar }, [
89
+ h("h1", { style: { ...styles.title, marginBottom: 0 } }, title),
90
+ ...toolbar
91
+ ]),
92
+ body
93
+ ]);
94
+ }
95
+ function table(headers, rows) {
96
+ return h("table", { style: styles.table }, [
97
+ h("thead", [
98
+ h(
99
+ "tr",
100
+ headers.map((t) => h("th", { style: styles.th }, t))
101
+ )
102
+ ]),
103
+ h("tbody", rows)
104
+ ]);
105
+ }
106
+ var td = (children, extra = {}) => h("td", { style: { ...styles.td, ...extra } }, children);
107
+ var refreshButton = (label, disabled, onClick) => h("button", { type: "button", style: styles.button, disabled, onClick }, label);
108
+
109
+ // src/screens/AdminLogScreen.ts
110
+ var AdminLogScreen = defineComponent({
111
+ name: "FonderieAdminLogScreen",
112
+ props: {
113
+ client: { type: Object, required: true },
114
+ pageSize: { type: Number, default: 50 }
115
+ },
116
+ setup(props) {
117
+ const { entries, hasMore, isLoading, error, refresh, loadMore } = useAdminLog(props.client, {
118
+ limit: props.pageSize
119
+ });
120
+ return () => h2("div", { style: styles.container }, [
121
+ h2("div", { style: styles.toolbar }, [
122
+ h2("h1", { style: { ...styles.title, marginBottom: 0 } }, "Admin log"),
123
+ refreshButton("Refresh", isLoading.value, () => void refresh())
124
+ ]),
125
+ error.value ? h2(
126
+ "p",
127
+ { style: styles.error, role: "alert" },
128
+ error.value.status === 404 ? "The admin log is off \u2014 give AdminModule a store." : error.value.explanation
129
+ ) : [
130
+ table(
131
+ ["When", "Actor", "Request", "Status", "Module"],
132
+ entries.value.map(
133
+ (e) => h2("tr", { key: e.id }, [
134
+ td(new Date(e.at).toLocaleString(), styles.muted),
135
+ td(e.actor),
136
+ td(`${e.method} ${e.path}`, styles.mono),
137
+ td([
138
+ h2(
139
+ "span",
140
+ { style: e.status >= 400 ? styles.bad : styles.ok },
141
+ String(e.status)
142
+ ),
143
+ " ",
144
+ h2("span", { style: styles.muted }, `${e.durationMs} ms`)
145
+ ]),
146
+ td(e.module, styles.muted)
147
+ ])
148
+ )
149
+ ),
150
+ isLoading.value ? h2("p", { style: styles.status }, "Loading\u2026") : null,
151
+ hasMore.value && !isLoading.value ? h2(
152
+ "button",
153
+ {
154
+ type: "button",
155
+ style: { ...styles.button, marginTop: "12px" },
156
+ onClick: () => void loadMore()
157
+ },
158
+ "Load more"
159
+ ) : null
160
+ ]
161
+ ]);
162
+ }
163
+ });
164
+
165
+ // src/screens/AttentionScreen.ts
166
+ import { useAttention } from "@fonderie/vue-admin";
167
+ import { defineComponent as defineComponent2, h as h3 } from "vue";
168
+ var AttentionScreen = defineComponent2({
169
+ name: "FonderieAttentionScreen",
170
+ props: { client: { type: Object, required: true } },
171
+ setup(props) {
172
+ const { attention, isLoading, error, refresh } = useAttention(props.client);
173
+ return () => page(
174
+ "Attention",
175
+ { isLoading, error },
176
+ () => {
177
+ const a = attention.value;
178
+ if (!a) return null;
179
+ if (a.items.length === 0)
180
+ return h3(
181
+ "p",
182
+ { style: styles.ok },
183
+ `Nothing needs you. Checked ${new Date(a.generatedAt).toLocaleString()}.`
184
+ );
185
+ return h3(
186
+ "ul",
187
+ { style: styles.list },
188
+ a.items.map(
189
+ (item) => h3("li", { key: `${item.source}:${item.message}`, style: styles.row }, [
190
+ h3(
191
+ "span",
192
+ { style: item.severity === "error" ? styles.bad : styles.advice },
193
+ item.severity
194
+ ),
195
+ " ",
196
+ h3("span", { style: styles.mono }, item.source),
197
+ ` \u2014 ${item.message}`
198
+ ])
199
+ )
200
+ );
201
+ },
202
+ [refreshButton("Refresh", isLoading.value, () => void refresh())],
203
+ { loadingText: "Running the checks\u2026" }
204
+ );
205
+ }
206
+ });
207
+
208
+ // src/screens/ConfigScreen.ts
209
+ import { useAdminConfig } from "@fonderie/vue-admin";
210
+ import { defineComponent as defineComponent3, h as h4 } from "vue";
211
+ var ConfigScreen = defineComponent3({
212
+ name: "FonderieConfigScreen",
213
+ props: { client: { type: Object, required: true } },
214
+ setup(props) {
215
+ const { report, isLoading, error } = useAdminConfig(props.client);
216
+ return () => page("Configuration", { isLoading, error }, () => {
217
+ const r = report.value;
218
+ if (!r) return null;
219
+ return [
220
+ h4("h2", { style: styles.subtitle }, "Readiness"),
221
+ h4(
222
+ "ul",
223
+ { style: styles.list },
224
+ r.modules.map(
225
+ (m) => h4("li", { key: m.name, style: styles.row }, [
226
+ h4("span", { style: styles.mono }, m.name),
227
+ " ",
228
+ ...m.problems.length === 0 ? [h4("span", { style: styles.ok }, "ok")] : m.problems.map(
229
+ (p) => h4(
230
+ "div",
231
+ {
232
+ key: p.message,
233
+ style: p.severity === "error" ? styles.bad : styles.advice
234
+ },
235
+ p.message
236
+ )
237
+ )
238
+ ])
239
+ )
240
+ ),
241
+ h4("h2", { style: styles.subtitle }, "Environment"),
242
+ r.env.length === 0 ? h4("p", { style: styles.muted }, "No variables declared \u2014 pass `env` to AdminModule.") : h4(
243
+ "ul",
244
+ { style: styles.list },
245
+ r.env.map(
246
+ (e) => h4("li", { key: e.name, style: styles.row }, [
247
+ h4("span", { style: styles.mono }, e.name),
248
+ " ",
249
+ h4("span", { style: e.set ? styles.ok : styles.bad }, e.set ? "set" : "missing")
250
+ ])
251
+ )
252
+ )
253
+ ];
254
+ });
255
+ }
256
+ });
257
+
258
+ // src/screens/DoctorScreen.ts
259
+ import { useDoctor } from "@fonderie/vue-admin";
260
+ import { defineComponent as defineComponent4, h as h5 } from "vue";
261
+ var DoctorScreen = defineComponent4({
262
+ name: "FonderieDoctorScreen",
263
+ props: { client: { type: Object, required: true } },
264
+ setup(props) {
265
+ const { report, isLoading, error, refresh } = useDoctor(props.client);
266
+ return () => page(
267
+ "Doctor",
268
+ { isLoading, error },
269
+ () => {
270
+ const r = report.value;
271
+ if (!r) return null;
272
+ return h5(
273
+ "ul",
274
+ { style: styles.list },
275
+ r.checks.map(
276
+ (c) => h5("li", { key: c.name, style: styles.row }, [
277
+ h5(
278
+ "span",
279
+ { style: c.skipped ? styles.muted : c.ok ? styles.ok : styles.bad },
280
+ c.skipped ? "skipped" : c.ok ? "ok" : "failed"
281
+ ),
282
+ " ",
283
+ h5("span", { style: styles.mono }, c.name),
284
+ " ",
285
+ h5("span", { style: styles.muted }, `${c.module} \xB7 ${c.durationMs} ms`),
286
+ c.skipped ? h5("div", { style: styles.muted }, c.skipped) : null,
287
+ ...c.findings.map(
288
+ (f) => h5("div", { key: f, style: c.ok ? styles.advice : styles.bad }, f)
289
+ )
290
+ ])
291
+ )
292
+ );
293
+ },
294
+ [
295
+ refreshButton("Run again", isLoading.value, () => void refresh()),
296
+ report.value && !isLoading.value ? h5(
297
+ "span",
298
+ { style: report.value.ok ? styles.ok : styles.bad },
299
+ report.value.ok ? "all checks pass" : "a check failed"
300
+ ) : h5("span")
301
+ ],
302
+ { loadingText: "Running the checks\u2026" }
303
+ );
304
+ }
305
+ });
306
+
307
+ // src/screens/ModulesScreen.ts
308
+ import { useManifest } from "@fonderie/vue-admin";
309
+ import { defineComponent as defineComponent5, h as h6 } from "vue";
310
+ var ModulesScreen = defineComponent5({
311
+ name: "FonderieModulesScreen",
312
+ props: { client: { type: Object, required: true } },
313
+ setup(props) {
314
+ const { manifest, isLoading, error } = useManifest(props.client);
315
+ return () => page("Modules", { isLoading, error }, () => {
316
+ const m = manifest.value;
317
+ if (!m) return null;
318
+ return [
319
+ h6(
320
+ "p",
321
+ { style: styles.muted },
322
+ `${m.env} \xB7 admin ${m.admin.version} \xB7 admin log ${m.admin.log ? "on" : "off"} \xB7 ${m.routes.length} routes`
323
+ ),
324
+ table(
325
+ ["Module", "Version", "Readiness", "Describes admin"],
326
+ m.modules.map(
327
+ (mod) => h6("tr", { key: mod.name }, [
328
+ td(mod.name, styles.mono),
329
+ td(mod.version ?? h6("span", { style: styles.muted }, "not reported")),
330
+ td([
331
+ h6(
332
+ "span",
333
+ { style: mod.readiness.ok ? styles.ok : styles.bad },
334
+ mod.readiness.ok ? "ok" : "error"
335
+ ),
336
+ ...mod.readiness.problems.map(
337
+ (p) => h6(
338
+ "div",
339
+ {
340
+ key: p.message,
341
+ style: p.severity === "error" ? styles.bad : styles.advice
342
+ },
343
+ p.message
344
+ )
345
+ )
346
+ ]),
347
+ td(mod.describesAdmin ? "yes" : h6("span", { style: styles.muted }, "no"))
348
+ ])
349
+ )
350
+ )
351
+ ];
352
+ });
353
+ }
354
+ });
355
+
356
+ // src/screens/RoutesScreen.ts
357
+ import { useAdminRoutes } from "@fonderie/vue-admin";
358
+ import { defineComponent as defineComponent6, h as h7 } from "vue";
359
+ var RoutesScreen = defineComponent6({
360
+ name: "FonderieRoutesScreen",
361
+ props: { client: { type: Object, required: true } },
362
+ setup(props) {
363
+ const { report, isLoading, error } = useAdminRoutes(props.client);
364
+ return () => page(
365
+ "Routes",
366
+ { isLoading, error },
367
+ () => table(
368
+ ["Method", "Path", "Guard", "Module"],
369
+ (report.value?.routes ?? []).map(
370
+ (r) => h7("tr", { key: `${r.method} ${r.path}` }, [
371
+ td(r.method, styles.mono),
372
+ td(r.path, styles.mono),
373
+ td(h7("span", { style: styles.badge }, r.guard)),
374
+ td(r.module ?? "application", styles.muted)
375
+ ])
376
+ )
377
+ )
378
+ );
379
+ }
380
+ });
381
+
382
+ // src/screens/TokensScreen.ts
383
+ import { useAdminTokens } from "@fonderie/vue-admin";
384
+ import { defineComponent as defineComponent7, h as h8 } from "vue";
385
+ var TokensScreen = defineComponent7({
386
+ name: "FonderieTokensScreen",
387
+ props: { client: { type: Object, required: true } },
388
+ setup(props) {
389
+ const { report, isLoading, error } = useAdminTokens(props.client);
390
+ return () => page("Access", { isLoading, error }, () => {
391
+ const r = report.value;
392
+ if (!r) return null;
393
+ return [
394
+ h8("h2", { style: styles.subtitle }, "Admin token"),
395
+ h8("p", [
396
+ h8(
397
+ "span",
398
+ { style: r.admin.ok ? styles.ok : styles.bad },
399
+ r.admin.ok ? "strong" : "weak"
400
+ ),
401
+ ...r.admin.problems.map(
402
+ (p) => h8("div", { key: p.message, style: styles.bad }, p.message)
403
+ )
404
+ ]),
405
+ h8("h2", { style: styles.subtitle }, "Legacy per-brick tokens"),
406
+ r.legacy.length === 0 ? h8(
407
+ "p",
408
+ { style: styles.muted },
409
+ "None \u2014 every brick's admin surface goes through this token."
410
+ ) : h8(
411
+ "ul",
412
+ { style: styles.list },
413
+ r.legacy.map(
414
+ (l) => h8("li", { key: l.module, style: styles.row }, [
415
+ h8("span", { style: styles.mono }, l.module),
416
+ " ",
417
+ h8(
418
+ "span",
419
+ { style: styles.muted },
420
+ "still registers its standalone routes with its own token (deprecated)"
421
+ )
422
+ ])
423
+ )
424
+ )
425
+ ];
426
+ });
427
+ }
428
+ });
429
+
430
+ // src/screens/AdminShell.ts
431
+ var NAV = [
432
+ { group: "Today", items: [{ page: "attention", label: "Attention" }] },
433
+ {
434
+ group: "System",
435
+ items: [
436
+ { page: "modules", label: "Modules" },
437
+ { page: "config", label: "Configuration" },
438
+ { page: "doctor", label: "Doctor" },
439
+ { page: "routes", label: "Routes" }
440
+ ]
441
+ },
442
+ { group: "Settings", items: [{ page: "settings", label: "Config & secrets", needs: "config" }] },
443
+ { group: "Messaging", items: [{ page: "templates", label: "Templates", needs: "courier" }] },
444
+ {
445
+ group: "Activity",
446
+ items: [
447
+ { page: "log", label: "Admin log" },
448
+ { page: "tokens", label: "Access" }
449
+ ]
450
+ }
451
+ ];
452
+ var AdminShell = defineComponent8({
453
+ name: "FonderieAdminShell",
454
+ props: {
455
+ client: { type: Object, required: true },
456
+ // Given ⇒ the Settings and Messaging pages appear. Construct them with
457
+ // `prefix: '/_admin'` to go through the one token.
458
+ configClient: { type: Object, default: void 0 },
459
+ courierClient: { type: Object, default: void 0 },
460
+ environment: { type: String, default: void 0 },
461
+ // Controlled navigation: pass `page` and listen to `navigate` to own the
462
+ // URL. Omit `page` and the shell keeps it itself.
463
+ page: { type: String, default: void 0 }
464
+ },
465
+ emits: { navigate: (_page) => true },
466
+ setup(props, { emit }) {
467
+ const own = ref("attention");
468
+ const current = computed(() => props.page ?? own.value);
469
+ const editing = ref(null);
470
+ const go = (p) => {
471
+ editing.value = null;
472
+ emit("navigate", p);
473
+ if (props.page === void 0) own.value = p;
474
+ };
475
+ const back = () => h9(
476
+ "button",
477
+ { type: "button", style: styles.button, onClick: () => editing.value = null },
478
+ "\u2190 Back"
479
+ );
480
+ const body = () => {
481
+ const c = props.client;
482
+ switch (current.value) {
483
+ case "attention":
484
+ return h9(AttentionScreen, { client: c });
485
+ case "modules":
486
+ return h9(ModulesScreen, { client: c });
487
+ case "doctor":
488
+ return h9(DoctorScreen, { client: c });
489
+ case "config":
490
+ return h9(ConfigScreen, { client: c });
491
+ case "routes":
492
+ return h9(RoutesScreen, { client: c });
493
+ case "tokens":
494
+ return h9(TokensScreen, { client: c });
495
+ case "log":
496
+ return h9(AdminLogScreen, { client: c });
497
+ case "settings": {
498
+ const cc = props.configClient;
499
+ if (!cc)
500
+ return h9(
501
+ "p",
502
+ { style: styles.status },
503
+ "Pass a ConfigAdminClient to manage config and secrets here."
504
+ );
505
+ const e = editing.value;
506
+ if (e && e.kind !== "template")
507
+ return [
508
+ back(),
509
+ h9(ConfigEditorScreen, {
510
+ client: cc,
511
+ kind: e.kind,
512
+ configKey: e.key,
513
+ ...props.environment !== void 0 ? { environment: props.environment } : {},
514
+ onSaved: () => editing.value = null
515
+ })
516
+ ];
517
+ return h9(ConfigListScreen, {
518
+ client: cc,
519
+ ...props.environment !== void 0 ? { environment: props.environment } : {},
520
+ "onSelect-config": (key) => editing.value = { kind: "config", key },
521
+ "onSelect-secret": (key) => editing.value = { kind: "secret", key }
522
+ });
523
+ }
524
+ case "templates": {
525
+ const kc = props.courierClient;
526
+ if (!kc)
527
+ return h9(
528
+ "p",
529
+ { style: styles.status },
530
+ "Pass a CourierAdminClient to manage templates here."
531
+ );
532
+ const e = editing.value;
533
+ if (e?.kind === "template")
534
+ return [
535
+ back(),
536
+ h9(TemplateEditorScreen, {
537
+ client: kc,
538
+ type: e.type,
539
+ onSaved: () => editing.value = null
540
+ })
541
+ ];
542
+ return h9(TemplateListScreen, {
543
+ client: kc,
544
+ "onSelect-template": (t) => editing.value = { kind: "template", type: t.type }
545
+ });
546
+ }
547
+ }
548
+ };
549
+ return () => h9("div", { style: styles.shell }, [
550
+ h9(
551
+ "nav",
552
+ { style: styles.nav, "aria-label": "Admin" },
553
+ NAV.map((g) => {
554
+ const items = g.items.filter(
555
+ (i) => !i.needs || (i.needs === "config" ? props.configClient : props.courierClient)
556
+ );
557
+ if (items.length === 0) return null;
558
+ return h9("div", { key: g.group }, [
559
+ h9("div", { style: styles.navGroup }, g.group),
560
+ ...items.map(
561
+ (i) => h9(
562
+ "button",
563
+ {
564
+ key: i.page,
565
+ type: "button",
566
+ style: {
567
+ ...styles.navItem,
568
+ ...current.value === i.page ? styles.navItemActive : {}
569
+ },
570
+ "aria-current": current.value === i.page ? "page" : void 0,
571
+ onClick: () => go(i.page)
572
+ },
573
+ i.label
574
+ )
575
+ )
576
+ ]);
577
+ })
578
+ ),
579
+ h9("main", { style: styles.main }, body())
580
+ ]);
581
+ }
582
+ });
583
+ export {
584
+ AdminLogScreen,
585
+ AdminShell,
586
+ AttentionScreen,
587
+ ConfigScreen,
588
+ DoctorScreen,
589
+ ModulesScreen,
590
+ RoutesScreen,
591
+ TokensScreen
592
+ };
593
+ //# sourceMappingURL=index.js.map