@vertebr/react 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.
@@ -0,0 +1,9 @@
1
+ import type { FieldSchema } from "./types";
2
+ type ConfigFormProps = {
3
+ schema: Record<string, FieldSchema>;
4
+ value: Record<string, unknown>;
5
+ onSave: (value: Record<string, unknown>) => Promise<void>;
6
+ };
7
+ export declare function ConfigForm({ schema, value, onSave }: ConfigFormProps): import("react").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=ConfigForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfigForm.d.ts","sourceRoot":"","sources":["../src/ConfigForm.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,KAAK,eAAe,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D,CAAC;AAgCF,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,eAAe,+BA4HpE"}
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { useForm, Controller } from "react-hook-form";
4
+ import { z } from "zod";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import * as Label from "@radix-ui/react-label";
7
+ import * as Switch from "@radix-ui/react-switch";
8
+ function buildZodSchema(schema) {
9
+ const shape = {};
10
+ for (const [key, field] of Object.entries(schema)) {
11
+ let zodField;
12
+ switch (field.type) {
13
+ case "number":
14
+ zodField = z.number();
15
+ if (field.min != null)
16
+ zodField = zodField.min(field.min);
17
+ if (field.max != null)
18
+ zodField = zodField.max(field.max);
19
+ break;
20
+ case "boolean":
21
+ zodField = z.boolean();
22
+ break;
23
+ default:
24
+ zodField = z.string();
25
+ }
26
+ if (!field.required) {
27
+ zodField = zodField.optional();
28
+ }
29
+ shape[key] = zodField;
30
+ }
31
+ return z.object(shape);
32
+ }
33
+ function spanFor(field) {
34
+ if (field.type === "boolean")
35
+ return "full";
36
+ if (field.type === "richtext")
37
+ return "wide";
38
+ return undefined;
39
+ }
40
+ export function ConfigForm({ schema, value, onSave }) {
41
+ const zodSchema = useMemo(() => buildZodSchema(schema), [schema]);
42
+ const [status, setStatus] = useState(null);
43
+ const [tone, setTone] = useState();
44
+ const form = useForm({
45
+ resolver: zodResolver(zodSchema),
46
+ defaultValues: value,
47
+ });
48
+ useEffect(() => {
49
+ form.reset(value);
50
+ }, [value, form]);
51
+ const onSubmit = form.handleSubmit(async (data) => {
52
+ setStatus("Saving…");
53
+ setTone(undefined);
54
+ try {
55
+ await onSave(data);
56
+ setStatus("Saved");
57
+ setTone("ok");
58
+ }
59
+ catch (error) {
60
+ setStatus(error instanceof Error ? error.message : "Save failed");
61
+ setTone("danger");
62
+ }
63
+ });
64
+ const isDirty = form.formState.isDirty;
65
+ useEffect(() => {
66
+ const onKeyDown = (event) => {
67
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
68
+ event.preventDefault();
69
+ void onSubmit();
70
+ }
71
+ };
72
+ window.addEventListener("keydown", onKeyDown);
73
+ return () => window.removeEventListener("keydown", onKeyDown);
74
+ }, [onSubmit]);
75
+ return (_jsxs("form", { className: "vertebr-form", onSubmit: onSubmit, children: [_jsx("div", { className: "vertebr-form-fields", children: Object.entries(schema).map(([key, field]) => {
76
+ if (field.type === "boolean") {
77
+ return (_jsxs("div", { className: "vertebr-switch-row", children: [_jsx(Label.Root, { htmlFor: key, children: field.label ?? key }), _jsx(Controller, { control: form.control, name: key, render: ({ field: controllerField }) => (_jsx(Switch.Root, { className: "vertebr-switch", id: key, checked: Boolean(controllerField.value), onCheckedChange: controllerField.onChange, children: _jsx(Switch.Thumb, { className: "vertebr-switch-thumb" }) })) })] }, key));
78
+ }
79
+ return (_jsxs("div", { className: "vertebr-field", "data-span": spanFor(field), children: [_jsx(Label.Root, { htmlFor: key, children: field.label ?? key }), field.type === "richtext" ? (_jsx("textarea", { id: key, rows: 3, readOnly: field.readOnly, ...form.register(key) })) : (_jsx("input", { id: key, type: field.type === "number" ? "number" : "text", readOnly: field.readOnly, ...form.register(key, {
80
+ valueAsNumber: field.type === "number",
81
+ }) }))] }, key));
82
+ }) }), _jsxs("div", { className: "vertebr-actions", children: [_jsx("button", { className: "vertebr-btn vertebr-btn-primary", type: "submit", disabled: form.formState.isSubmitting || !isDirty, children: form.formState.isSubmitting ? "Saving…" : "Save changes" }), isDirty ? (_jsx("button", { className: "vertebr-btn", type: "button", onClick: () => {
83
+ form.reset(value);
84
+ setStatus(null);
85
+ setTone(undefined);
86
+ }, children: "Discard" })) : null, isDirty ? (_jsx("span", { className: "vertebr-dirty", children: "Unsaved" })) : status ? (_jsx("span", { className: "vertebr-status", "data-tone": tone, children: status })) : null, _jsx("span", { className: "vertebr-actions-spacer" }), _jsx("span", { className: "vertebr-kbd", children: "\u2318S" })] })] }));
87
+ }
@@ -0,0 +1,9 @@
1
+ import type { AdminEnvironment } from "./types";
2
+ type EnvSelectProps = {
3
+ environments: AdminEnvironment[];
4
+ value: string;
5
+ onChange: (next: string) => void;
6
+ };
7
+ export declare function EnvSelect({ environments, value, onChange }: EnvSelectProps): import("react").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=EnvSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EnvSelect.d.ts","sourceRoot":"","sources":["../src/EnvSelect.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAEhD,KAAK,cAAc,GAAG;IACpB,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,wBAAgB,SAAS,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,cAAc,+BAqD1E"}
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as Select from "@radix-ui/react-select";
3
+ export function EnvSelect({ environments, value, onChange }) {
4
+ const active = environments.find((env) => env.id === value);
5
+ return (_jsxs(Select.Root, { value: value, onValueChange: onChange, children: [_jsxs(Select.Trigger, { className: "vertebr-select-trigger", "aria-label": "Environment", "data-tone": active?.tone ?? "neutral", children: [_jsxs("span", { className: "vertebr-select-value", children: [_jsx("span", { className: "vertebr-select-dot", "data-tone": active?.tone ?? "neutral" }), _jsx(Select.Value, { placeholder: "Select environment" })] }), _jsx(Select.Icon, { className: "vertebr-select-icon", children: _jsx(Chevron, {}) })] }), _jsx(Select.Portal, { children: _jsx(Select.Content, { className: "vertebr-select-content", position: "popper", sideOffset: 6, children: _jsx(Select.Viewport, { className: "vertebr-select-viewport", children: environments.map((env) => (_jsxs(Select.Item, { value: env.id, className: "vertebr-select-item", "data-tone": env.tone ?? "neutral", children: [_jsxs("span", { className: "vertebr-select-value", children: [_jsx("span", { className: "vertebr-select-dot", "data-tone": env.tone ?? "neutral" }), _jsx(Select.ItemText, { children: env.label })] }), env.auth?.required ? (_jsx("span", { className: "vertebr-env-lock", children: "auth" })) : null, _jsx(Select.ItemIndicator, { className: "vertebr-select-check", children: _jsx(Check, {}) })] }, env.id))) }) }) })] }));
6
+ }
7
+ function Chevron() {
8
+ return (_jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: _jsx("path", { d: "M2.5 4.5 6 8l3.5-3.5", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }));
9
+ }
10
+ function Check() {
11
+ return (_jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: _jsx("path", { d: "M2.5 6.2 4.8 8.5 9.5 3.8", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }) }));
12
+ }
@@ -0,0 +1,10 @@
1
+ import type { MetricCard } from "./types";
2
+ type MetricsBoardProps = {
3
+ label?: string;
4
+ metrics: MetricCard[];
5
+ onRefresh?: () => void;
6
+ loading?: boolean;
7
+ };
8
+ export declare function MetricsBoard({ label, metrics, onRefresh, loading, }: MetricsBoardProps): import("react").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=MetricsBoard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MetricsBoard.d.ts","sourceRoot":"","sources":["../src/MetricsBoard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,KAAkB,EAClB,OAAO,EACP,SAAS,EACT,OAAO,GACR,EAAE,iBAAiB,+BAqCnB"}
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function MetricsBoard({ label = "Overview", metrics, onRefresh, loading, }) {
3
+ return (_jsxs("div", { className: "vertebr-metrics", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: label }), onRefresh ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: onRefresh, disabled: loading, children: loading ? "Refreshing…" : "Refresh" })) : null] }), metrics.length === 0 ? (_jsx("div", { className: "vertebr-empty", children: "No metrics registered yet." })) : (_jsx("div", { className: "vertebr-metrics-grid", children: metrics.map((metric) => (_jsxs("article", { className: "vertebr-metric-card", "data-tone": metric.tone ?? "neutral", children: [_jsx("p", { className: "vertebr-metric-label", children: metric.label }), _jsx("p", { className: "vertebr-metric-value", children: metric.value }), metric.hint ? (_jsx("p", { className: "vertebr-metric-hint", children: metric.hint })) : null] }, metric.id))) }))] }));
4
+ }
@@ -0,0 +1,23 @@
1
+ import type { FieldSchema, ResourceActionMeta } from "./types";
2
+ type ResourcePanelProps = {
3
+ schema: Record<string, FieldSchema>;
4
+ items: Record<string, unknown>[];
5
+ permissions?: {
6
+ create?: boolean;
7
+ update?: boolean;
8
+ delete?: boolean;
9
+ };
10
+ actions?: Record<string, ResourceActionMeta>;
11
+ rowIdKeys?: string[];
12
+ onUpdate?: (rowId: string, value: Record<string, unknown>) => Promise<void>;
13
+ onDelete?: (rowId: string) => Promise<void>;
14
+ onAction?: (rowId: string, actionId: string) => Promise<void>;
15
+ };
16
+ export declare function ResourcePanel({ schema, items, permissions, actions, rowIdKeys, onUpdate, onDelete, onAction, }: ResourcePanelProps): import("react").JSX.Element;
17
+ /** @deprecated Prefer ResourcePanel */
18
+ export declare function ResourceTable(props: {
19
+ schema: Record<string, FieldSchema>;
20
+ items: Record<string, unknown>[];
21
+ }): import("react").JSX.Element;
22
+ export {};
23
+ //# sourceMappingURL=ResourceTable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResourceTable.d.ts","sourceRoot":"","sources":["../src/ResourceTable.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAE/D,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACjC,WAAW,CAAC,EAAE;QACZ,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,CACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,CAAC;AAmCF,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,KAAK,EACL,WAAW,EACX,OAAY,EACZ,SAA0C,EAC1C,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,kBAAkB,+BAqKpB;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;CAClC,+BAEA"}
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table";
4
+ import { ConfigForm } from "./ConfigForm";
5
+ function formatCell(value) {
6
+ if (value == null)
7
+ return "—";
8
+ if (typeof value === "boolean")
9
+ return value ? "Yes" : "No";
10
+ if (typeof value === "object") {
11
+ if (value &&
12
+ typeof value === "object" &&
13
+ "toDate" in value &&
14
+ typeof value.toDate === "function") {
15
+ try {
16
+ return value.toDate().toLocaleString();
17
+ }
18
+ catch {
19
+ // fall through
20
+ }
21
+ }
22
+ return JSON.stringify(value);
23
+ }
24
+ return String(value);
25
+ }
26
+ function resolveRowId(row, keys) {
27
+ for (const key of keys) {
28
+ const value = row[key];
29
+ if (typeof value === "string" && value.trim())
30
+ return value;
31
+ if (typeof value === "number")
32
+ return String(value);
33
+ }
34
+ return null;
35
+ }
36
+ export function ResourcePanel({ schema, items, permissions, actions = {}, rowIdKeys = ["id", "slug", "uid", "email"], onUpdate, onDelete, onAction, }) {
37
+ const [editing, setEditing] = useState(null);
38
+ const [busyAction, setBusyAction] = useState(null);
39
+ const canUpdate = Boolean(permissions?.update && onUpdate);
40
+ const canDelete = Boolean(permissions?.delete && onDelete);
41
+ const actionEntries = Object.entries(actions);
42
+ const columns = useMemo(() => {
43
+ const keys = Object.keys(schema);
44
+ const defs = keys.length === 0
45
+ ? [
46
+ {
47
+ id: "raw",
48
+ header: "Item",
49
+ cell: ({ row }) => formatCell(row.original),
50
+ },
51
+ ]
52
+ : keys.map((key) => ({
53
+ accessorKey: key,
54
+ header: schema[key]?.label ?? key,
55
+ cell: ({ getValue }) => formatCell(getValue()),
56
+ }));
57
+ if (canUpdate || canDelete || actionEntries.length > 0) {
58
+ defs.push({
59
+ id: "_actions",
60
+ header: "",
61
+ cell: ({ row }) => {
62
+ const rowId = resolveRowId(row.original, rowIdKeys);
63
+ if (!rowId)
64
+ return null;
65
+ return (_jsxs("div", { className: "vertebr-row-actions", children: [canUpdate ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: () => setEditing(row.original), children: "Edit" })) : null, actionEntries.map(([actionId, meta]) => (_jsx("button", { type: "button", className: "vertebr-btn", disabled: busyAction === `${rowId}:${actionId}`, onClick: () => {
66
+ if (!onAction)
67
+ return;
68
+ setBusyAction(`${rowId}:${actionId}`);
69
+ void onAction(rowId, actionId).finally(() => setBusyAction(null));
70
+ }, children: meta.label }, actionId))), canDelete ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: () => {
71
+ if (window.confirm("Delete this row? This cannot be undone.")) {
72
+ void onDelete?.(rowId);
73
+ }
74
+ }, children: "Delete" })) : null] }));
75
+ },
76
+ });
77
+ }
78
+ return defs;
79
+ }, [
80
+ schema,
81
+ canUpdate,
82
+ canDelete,
83
+ actionEntries,
84
+ busyAction,
85
+ onAction,
86
+ onDelete,
87
+ rowIdKeys,
88
+ ]);
89
+ const table = useReactTable({
90
+ data: items,
91
+ columns,
92
+ getCoreRowModel: getCoreRowModel(),
93
+ });
94
+ const editingId = editing ? resolveRowId(editing, rowIdKeys) : null;
95
+ if (editing && editingId && onUpdate) {
96
+ const editableSchema = Object.fromEntries(Object.entries(schema).filter(([, field]) => !field.readOnly));
97
+ return (_jsxs("div", { className: "vertebr-resource-edit", children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsxs("h2", { children: ["Edit ", editingId] }), _jsx("button", { type: "button", className: "vertebr-btn", onClick: () => setEditing(null), children: "Back" })] }), _jsx(ConfigForm, { schema: editableSchema, value: editing, onSave: async (next) => {
98
+ await onUpdate(editingId, next);
99
+ setEditing(null);
100
+ } })] }));
101
+ }
102
+ if (items.length === 0) {
103
+ return _jsx("div", { className: "vertebr-empty", children: "No rows yet." });
104
+ }
105
+ return (_jsx("div", { className: "vertebr-table-wrap", children: _jsxs("table", { className: "vertebr-table", children: [_jsx("thead", { children: table.getHeaderGroups().map((headerGroup) => (_jsx("tr", { children: headerGroup.headers.map((header) => (_jsx("th", { children: header.isPlaceholder
106
+ ? null
107
+ : flexRender(header.column.columnDef.header, header.getContext()) }, header.id))) }, headerGroup.id))) }), _jsx("tbody", { children: table.getRowModel().rows.map((row) => (_jsx("tr", { children: row.getVisibleCells().map((cell) => (_jsx("td", { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }, row.id))) })] }) }));
108
+ }
109
+ /** @deprecated Prefer ResourcePanel */
110
+ export function ResourceTable(props) {
111
+ return _jsx(ResourcePanel, { ...props });
112
+ }
@@ -0,0 +1,17 @@
1
+ export type VertebrAdminProps = {
2
+ /** Base URL for Vertebr API, e.g. `/admin/api` */
3
+ apiBase?: string;
4
+ title?: string;
5
+ /** localStorage key for remembering the selected environment */
6
+ envStorageKey?: string;
7
+ /** localStorage key for the access token */
8
+ tokenStorageKey?: string;
9
+ /** Optional host-provided token getter (Firebase ID token, etc.) */
10
+ getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
11
+ /** Signed-in identity shown in the dashboard. */
12
+ authUserLabel?: string;
13
+ /** Host-provided sign-out action. */
14
+ onSignOut?: () => void | Promise<void>;
15
+ };
16
+ export declare function VertebrAdmin({ apiBase, title, envStorageKey, tokenStorageKey, getAccessToken, authUserLabel, onSignOut, }: VertebrAdminProps): import("react").JSX.Element;
17
+ //# sourceMappingURL=VertebrAdmin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../src/VertebrAdmin.tsx"],"names":[],"mappings":"AAcA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACtF,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAgCF,wBAAgB,YAAY,CAAC,EAC3B,OAAsB,EACtB,KAAuB,EACvB,aAA6B,EAC7B,eAAiC,EACjC,cAAc,EACd,aAAa,EACb,SAAS,GACV,EAAE,iBAAiB,+BAgdnB"}
@@ -0,0 +1,269 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { createVertebrClient } from "./client";
4
+ import { ConfigForm } from "./ConfigForm";
5
+ import { EnvSelect } from "./EnvSelect";
6
+ import { MetricsBoard } from "./MetricsBoard";
7
+ import { ResourcePanel } from "./ResourceTable";
8
+ function readStored(key) {
9
+ try {
10
+ return localStorage.getItem(key);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function writeStored(key, value) {
17
+ try {
18
+ localStorage.setItem(key, value);
19
+ }
20
+ catch {
21
+ // ignore
22
+ }
23
+ }
24
+ function clearStored(key) {
25
+ try {
26
+ localStorage.removeItem(key);
27
+ }
28
+ catch {
29
+ // ignore
30
+ }
31
+ }
32
+ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane", envStorageKey = "vertebr:env", tokenStorageKey = "vertebr:token", getAccessToken, authUserLabel, onSignOut, }) {
33
+ const [environment, setEnvironment] = useState(() => readStored(envStorageKey) ?? "");
34
+ const [token, setToken] = useState(() => readStored(tokenStorageKey) ?? "");
35
+ const [tokenDraft, setTokenDraft] = useState(token);
36
+ const getEnvironment = useCallback(() => environment, [environment]);
37
+ const client = useMemo(() => createVertebrClient({
38
+ apiBase,
39
+ getEnvironment,
40
+ getAccessToken: getAccessToken ?? (() => token || null),
41
+ }), [apiBase, getEnvironment, getAccessToken, token]);
42
+ const [bootstrap, setBootstrap] = useState(null);
43
+ const [catalog, setCatalog] = useState(null);
44
+ const [selection, setSelection] = useState({ kind: "overview" });
45
+ const [resourceData, setResourceData] = useState(null);
46
+ const [configData, setConfigData] = useState(null);
47
+ const [metricsData, setMetricsData] = useState(null);
48
+ const [metricsLoading, setMetricsLoading] = useState(false);
49
+ const [error, setError] = useState(null);
50
+ const [loading, setLoading] = useState(true);
51
+ useEffect(() => {
52
+ let cancelled = false;
53
+ (async () => {
54
+ try {
55
+ const next = await client.getBootstrap();
56
+ if (cancelled)
57
+ return;
58
+ setBootstrap(next);
59
+ const known = next.environments.map((env) => env.id);
60
+ const preferred = (environment && known.includes(environment) && environment) ||
61
+ next.defaultEnvironment;
62
+ if (preferred !== environment) {
63
+ setEnvironment(preferred);
64
+ writeStored(envStorageKey, preferred);
65
+ }
66
+ }
67
+ catch (err) {
68
+ if (!cancelled) {
69
+ setError(err instanceof Error ? err.message : "Failed to load bootstrap");
70
+ }
71
+ }
72
+ })();
73
+ return () => {
74
+ cancelled = true;
75
+ };
76
+ }, [client, environment, envStorageKey]);
77
+ useEffect(() => {
78
+ if (!bootstrap || !environment)
79
+ return;
80
+ let cancelled = false;
81
+ (async () => {
82
+ try {
83
+ setLoading(true);
84
+ setError(null);
85
+ const next = await client.getCatalog();
86
+ if (cancelled)
87
+ return;
88
+ setCatalog(next);
89
+ setSelection((current) => {
90
+ if (current?.kind === "overview")
91
+ return current;
92
+ if (current?.kind === "resource") {
93
+ if (next.resources.some((r) => r.id === current.id))
94
+ return current;
95
+ }
96
+ if (current?.kind === "config") {
97
+ if (next.configs.some((c) => c.id === current.id))
98
+ return current;
99
+ }
100
+ if (next.metrics)
101
+ return { kind: "overview" };
102
+ const firstResource = next.resources[0];
103
+ if (firstResource)
104
+ return { kind: "resource", id: firstResource.id };
105
+ const firstConfig = next.configs[0];
106
+ if (firstConfig)
107
+ return { kind: "config", id: firstConfig.id };
108
+ return { kind: "overview" };
109
+ });
110
+ }
111
+ catch (err) {
112
+ if (!cancelled) {
113
+ setCatalog(null);
114
+ setError(err instanceof Error ? err.message : "Failed to load catalog");
115
+ }
116
+ }
117
+ finally {
118
+ if (!cancelled)
119
+ setLoading(false);
120
+ }
121
+ })();
122
+ return () => {
123
+ cancelled = true;
124
+ };
125
+ }, [client, bootstrap, environment]);
126
+ useEffect(() => {
127
+ if (!selection || !catalog)
128
+ return;
129
+ let cancelled = false;
130
+ (async () => {
131
+ try {
132
+ setError(null);
133
+ if (selection.kind === "overview") {
134
+ setMetricsLoading(true);
135
+ const data = await client.getMetrics();
136
+ if (!cancelled) {
137
+ setMetricsData(data);
138
+ setResourceData(null);
139
+ setConfigData(null);
140
+ }
141
+ }
142
+ else if (selection.kind === "resource") {
143
+ const data = await client.listResource(selection.id);
144
+ if (!cancelled) {
145
+ setResourceData(data);
146
+ setConfigData(null);
147
+ setMetricsData(null);
148
+ }
149
+ }
150
+ else {
151
+ const data = await client.getConfig(selection.id);
152
+ if (!cancelled) {
153
+ setConfigData(data);
154
+ setResourceData(null);
155
+ setMetricsData(null);
156
+ }
157
+ }
158
+ }
159
+ catch (err) {
160
+ if (!cancelled) {
161
+ setError(err instanceof Error ? err.message : "Failed to load panel");
162
+ }
163
+ }
164
+ finally {
165
+ if (!cancelled)
166
+ setMetricsLoading(false);
167
+ }
168
+ })();
169
+ return () => {
170
+ cancelled = true;
171
+ };
172
+ }, [client, selection, catalog, environment]);
173
+ const refreshResource = useCallback(async () => {
174
+ if (selection?.kind !== "resource")
175
+ return;
176
+ const data = await client.listResource(selection.id);
177
+ setResourceData(data);
178
+ }, [client, selection]);
179
+ const refreshMetrics = useCallback(async () => {
180
+ setMetricsLoading(true);
181
+ try {
182
+ const data = await client.getMetrics();
183
+ setMetricsData(data);
184
+ }
185
+ finally {
186
+ setMetricsLoading(false);
187
+ }
188
+ }, [client]);
189
+ const envList = catalog?.environments ?? bootstrap?.environments ?? [];
190
+ const activeEnv = envList.find((env) => env.id === environment);
191
+ const authRequired = Boolean(activeEnv?.auth?.required);
192
+ const switchEnvironment = (nextEnv) => {
193
+ if (nextEnv === environment)
194
+ return;
195
+ const meta = envList.find((env) => env.id === nextEnv);
196
+ if (meta?.tone === "danger") {
197
+ const ok = window.confirm(`Switch to ${meta.label}? Writes will affect this environment.`);
198
+ if (!ok)
199
+ return;
200
+ }
201
+ writeStored(envStorageKey, nextEnv);
202
+ setEnvironment(nextEnv);
203
+ setResourceData(null);
204
+ setConfigData(null);
205
+ };
206
+ const saveToken = () => {
207
+ const next = tokenDraft.trim();
208
+ setToken(next);
209
+ if (next)
210
+ writeStored(tokenStorageKey, next);
211
+ else
212
+ clearStored(tokenStorageKey);
213
+ };
214
+ const clearToken = () => {
215
+ setToken("");
216
+ setTokenDraft("");
217
+ clearStored(tokenStorageKey);
218
+ };
219
+ if (!bootstrap && loading) {
220
+ return _jsx("div", { className: "vertebr-loading", children: "Loading Vertebr\u2026" });
221
+ }
222
+ if (error && !bootstrap) {
223
+ return _jsx("div", { className: "vertebr-error", children: error });
224
+ }
225
+ const activeLabel = selection?.kind === "overview"
226
+ ? (catalog?.metrics?.label ?? "Overview")
227
+ : selection?.kind === "resource"
228
+ ? catalog?.resources.find((r) => r.id === selection.id)?.label
229
+ : selection?.kind === "config"
230
+ ? catalog?.configs.find((c) => c.id === selection.id)?.label
231
+ : null;
232
+ return (_jsxs("div", { className: "vertebr-root", "data-env": environment, children: [_jsxs("aside", { className: "vertebr-sidebar", children: [_jsxs("div", { className: "vertebr-sidebar-top", children: [_jsxs("div", { className: "vertebr-brand", children: [_jsx("strong", { children: bootstrap?.brand ?? catalog?.brand ?? "Vertebr" }), _jsx("span", { children: title })] }), envList.length > 0 ? (_jsxs("div", { className: "vertebr-env", children: [_jsx("div", { className: "vertebr-nav-label", children: "Environment" }), _jsx(EnvSelect, { environments: envList, value: environment, onChange: switchEnvironment })] })) : null, _jsxs("nav", { className: "vertebr-nav", children: [catalog?.metrics ? (_jsxs("div", { className: "vertebr-nav-group", children: [_jsx("div", { className: "vertebr-nav-label", children: "Analytics" }), _jsx("button", { type: "button", "data-active": selection?.kind === "overview", onClick: () => setSelection({ kind: "overview" }), children: catalog.metrics.label })] })) : null, catalog && catalog.resources.length > 0 ? (_jsxs("div", { className: "vertebr-nav-group", children: [_jsx("div", { className: "vertebr-nav-label", children: "Resources" }), catalog.resources.map((resource) => (_jsx("button", { type: "button", "data-active": selection?.kind === "resource" &&
233
+ selection.id === resource.id, onClick: () => setSelection({ kind: "resource", id: resource.id }), children: resource.label }, resource.id)))] })) : null, catalog && catalog.configs.length > 0 ? (_jsxs("div", { className: "vertebr-nav-group", children: [_jsx("div", { className: "vertebr-nav-label", children: "Config" }), catalog.configs.map((config) => (_jsx("button", { type: "button", "data-active": selection?.kind === "config" && selection.id === config.id, onClick: () => setSelection({ kind: "config", id: config.id }), children: config.label }, config.id)))] })) : null] })] }), _jsxs("div", { className: "vertebr-sidebar-bottom", children: [getAccessToken && authUserLabel ? (_jsxs("div", { className: "vertebr-auth", children: [_jsx("div", { className: "vertebr-nav-label", children: "Signed in" }), _jsx("p", { className: "vertebr-auth-hint", children: authUserLabel }), onSignOut ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: () => void onSignOut(), children: "Sign out" })) : null] })) : null, !getAccessToken && (authRequired || token) ? (_jsxs("div", { className: "vertebr-auth", children: [_jsx("div", { className: "vertebr-nav-label", children: "Access" }), _jsx("p", { className: "vertebr-auth-hint", children: authRequired
234
+ ? `This environment requires auth${activeEnv?.auth?.claimKey
235
+ ? ` (${activeEnv.auth.claimKey})`
236
+ : ""}.`
237
+ : "Optional access token for protected environments." }), _jsx("input", { className: "vertebr-auth-input", type: "password", placeholder: "Bearer token", value: tokenDraft, onChange: (event) => setTokenDraft(event.target.value), autoComplete: "off" }), _jsxs("div", { className: "vertebr-auth-actions", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", onClick: saveToken, children: "Use token" }), token ? (_jsx("button", { type: "button", className: "vertebr-btn", onClick: clearToken, children: "Clear" })) : null] })] })) : null] })] }), _jsxs("main", { className: "vertebr-main", children: [_jsxs("header", { className: "vertebr-header", children: [_jsxs("div", { children: [_jsxs("div", { className: "vertebr-heading-row", children: [_jsx("h1", { children: activeLabel ?? (catalog ? "Dashboard" : "Sign in") }), activeEnv ? (_jsx("span", { className: "vertebr-env-badge", "data-tone": activeEnv.tone ?? "neutral", children: activeEnv.label })) : null] }), _jsx("p", { children: !catalog && authRequired
238
+ ? "Provide an access token to load this environment."
239
+ : selection?.kind === "overview"
240
+ ? "Live metrics for the selected environment."
241
+ : selection?.kind === "resource"
242
+ ? "Manage records scoped to the selected environment."
243
+ : selection?.kind === "config"
244
+ ? "Owner-managed config for this environment."
245
+ : "Select a panel." })] }), error ? (_jsx("span", { className: "vertebr-status", "data-tone": "danger", children: error })) : null] }), _jsx("div", { className: "vertebr-content", children: catalog ? (_jsxs("section", { className: "vertebr-panel", children: [selection?.kind === "overview" ? (_jsx(MetricsBoard, { label: metricsData?.label ?? catalog.metrics?.label, metrics: metricsData?.metrics ?? [], loading: metricsLoading, onRefresh: () => void refreshMetrics() })) : null, selection?.kind === "resource" && resourceData ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Records" }), _jsx("span", { className: "vertebr-count", children: resourceData.items.length })] }), _jsx(ResourcePanel, { schema: resourceData.schema, items: resourceData.items, permissions: resourceData.permissions, actions: resourceData.actions, onUpdate: resourceData.permissions.update
246
+ ? async (rowId, value) => {
247
+ await client.updateResource(resourceData.id, rowId, value);
248
+ await refreshResource();
249
+ }
250
+ : undefined, onDelete: resourceData.permissions.delete
251
+ ? async (rowId) => {
252
+ await client.deleteResource(resourceData.id, rowId);
253
+ await refreshResource();
254
+ }
255
+ : undefined, onAction: async (rowId, actionId) => {
256
+ await client.runResourceAction(resourceData.id, rowId, actionId);
257
+ await refreshResource();
258
+ } })] })) : null, selection?.kind === "config" && configData ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Settings" }), _jsxs("span", { className: "vertebr-count", children: [Object.keys(configData.schema).length, " fields"] })] }), _jsx(ConfigForm, { schema: configData.schema, value: configData.value, onSave: async (next) => {
259
+ const saved = await client.setConfig(configData.id, next);
260
+ setConfigData({
261
+ ...configData,
262
+ value: saved.value,
263
+ });
264
+ } })] })) : null] })) : (_jsx("section", { className: "vertebr-panel", children: _jsx("div", { className: "vertebr-empty", children: authRequired
265
+ ? "Authenticated access required for this environment."
266
+ : loading
267
+ ? "Loading…"
268
+ : "Unable to load catalog." }) })) })] })] }));
269
+ }
@@ -0,0 +1,32 @@
1
+ import type { BootstrapResponse, CatalogResponse, ConfigResponse, MetricsResponse, ResourceListResponse } from "./types";
2
+ export declare const VERTEBR_ENV_HEADER = "x-vertebr-env";
3
+ export type VertebrClientOptions = {
4
+ apiBase: string;
5
+ getEnvironment: () => string;
6
+ getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
7
+ };
8
+ export type VertebrClient = {
9
+ getBootstrap: () => Promise<BootstrapResponse>;
10
+ getCatalog: () => Promise<CatalogResponse>;
11
+ getMetrics: () => Promise<MetricsResponse>;
12
+ listResource: (id: string) => Promise<ResourceListResponse>;
13
+ createResource: (id: string, value: Record<string, unknown>) => Promise<{
14
+ ok: boolean;
15
+ item: Record<string, unknown>;
16
+ }>;
17
+ updateResource: (id: string, rowId: string, value: Record<string, unknown>) => Promise<{
18
+ ok: boolean;
19
+ item: Record<string, unknown>;
20
+ }>;
21
+ deleteResource: (id: string, rowId: string) => Promise<{
22
+ ok: boolean;
23
+ }>;
24
+ runResourceAction: (id: string, rowId: string, actionId: string, input?: Record<string, unknown>) => Promise<{
25
+ ok: boolean;
26
+ result?: unknown;
27
+ }>;
28
+ getConfig: (id: string) => Promise<ConfigResponse>;
29
+ setConfig: (id: string, value: Record<string, unknown>) => Promise<ConfigResponse>;
30
+ };
31
+ export declare function createVertebrClient(apiBaseOrOptions: string | VertebrClientOptions, getEnvironment?: () => string): VertebrClient;
32
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,eAAe,EACf,oBAAoB,EACrB,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MACb,MAAM,GACN,IAAI,GACJ,SAAS,GACT,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC/C,UAAU,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C,UAAU,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;IAC3C,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC5D,cAAc,EAAE,CACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC7D,cAAc,EAAE,CACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IAC7D,cAAc,EAAE,CACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC9B,iBAAiB,EAAE,CACjB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAChD,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACnD,SAAS,EAAE,CACT,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC3B,OAAO,CAAC,cAAc,CAAC,CAAC;CAC9B,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,gBAAgB,EAAE,MAAM,GAAG,oBAAoB,EAC/C,cAAc,CAAC,EAAE,MAAM,MAAM,GAC5B,aAAa,CA4Ff"}