@vertebr/react 0.1.1 → 0.3.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.
Files changed (128) hide show
  1. package/dist/{VertebrAdmin.d.ts → components/admin/VertebrAdmin.d.ts} +3 -1
  2. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -0
  3. package/dist/components/admin/VertebrAdmin.js +375 -0
  4. package/dist/components/config/ConfigForm.d.ts +3 -0
  5. package/dist/components/config/ConfigForm.d.ts.map +1 -0
  6. package/dist/components/config/ConfigForm.js +1 -0
  7. package/dist/components/config/DescriptionInput.d.ts +18 -0
  8. package/dist/components/config/DescriptionInput.d.ts.map +1 -0
  9. package/dist/components/config/DescriptionInput.js +152 -0
  10. package/dist/components/config/ImagePickerModal.d.ts +18 -0
  11. package/dist/components/config/ImagePickerModal.d.ts.map +1 -0
  12. package/dist/components/config/ImagePickerModal.js +120 -0
  13. package/dist/components/config/PriceInput.d.ts +12 -0
  14. package/dist/components/config/PriceInput.d.ts.map +1 -0
  15. package/dist/components/config/PriceInput.js +20 -0
  16. package/dist/components/config/form/ConfigField.d.ts +21 -0
  17. package/dist/components/config/form/ConfigField.d.ts.map +1 -0
  18. package/dist/components/config/form/ConfigField.js +48 -0
  19. package/dist/components/config/form/ConfigForm.d.ts +4 -0
  20. package/dist/components/config/form/ConfigForm.d.ts.map +1 -0
  21. package/dist/components/config/form/ConfigForm.js +70 -0
  22. package/dist/components/config/form/ConfigFormActions.d.ts +10 -0
  23. package/dist/components/config/form/ConfigFormActions.d.ts.map +1 -0
  24. package/dist/components/config/form/ConfigFormActions.js +4 -0
  25. package/dist/components/config/form/RecordConfigForm.d.ts +21 -0
  26. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -0
  27. package/dist/components/config/form/RecordConfigForm.js +37 -0
  28. package/dist/components/config/form/SettingsConfigForm.d.ts +21 -0
  29. package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -0
  30. package/dist/components/config/form/SettingsConfigForm.js +7 -0
  31. package/dist/components/config/form/helpers.d.ts +16 -0
  32. package/dist/components/config/form/helpers.d.ts.map +1 -0
  33. package/dist/components/config/form/helpers.js +186 -0
  34. package/dist/components/config/form/types.d.ts +27 -0
  35. package/dist/components/config/form/types.d.ts.map +1 -0
  36. package/dist/{EnvSelect.d.ts → components/env/EnvSelect.d.ts} +1 -1
  37. package/dist/components/env/EnvSelect.d.ts.map +1 -0
  38. package/dist/components/gallery/GalleryBoard.d.ts +14 -0
  39. package/dist/components/gallery/GalleryBoard.d.ts.map +1 -0
  40. package/dist/components/gallery/GalleryBoard.js +14 -0
  41. package/dist/components/index.d.ts +9 -0
  42. package/dist/components/index.d.ts.map +1 -0
  43. package/dist/components/index.js +6 -0
  44. package/dist/{MetricsBoard.d.ts → components/metrics/MetricsBoard.d.ts} +1 -1
  45. package/dist/components/metrics/MetricsBoard.d.ts.map +1 -0
  46. package/dist/components/resources/BulkImageUploadModal.d.ts +17 -0
  47. package/dist/components/resources/BulkImageUploadModal.d.ts.map +1 -0
  48. package/dist/components/resources/BulkImageUploadModal.js +47 -0
  49. package/dist/components/resources/ColumnVisibilityMenu.d.ts +11 -0
  50. package/dist/components/resources/ColumnVisibilityMenu.d.ts.map +1 -0
  51. package/dist/components/resources/ColumnVisibilityMenu.js +40 -0
  52. package/dist/components/resources/ImageUploadControl.d.ts +10 -0
  53. package/dist/components/resources/ImageUploadControl.d.ts.map +1 -0
  54. package/dist/components/resources/ImageUploadControl.js +50 -0
  55. package/dist/components/resources/ResourceCell.d.ts +10 -0
  56. package/dist/components/resources/ResourceCell.d.ts.map +1 -0
  57. package/dist/components/resources/ResourceCell.js +100 -0
  58. package/dist/components/resources/ResourceTable.d.ts +34 -0
  59. package/dist/components/resources/ResourceTable.d.ts.map +1 -0
  60. package/dist/components/resources/ResourceTable.js +249 -0
  61. package/dist/components/resources/ResourceViewModal.d.ts +11 -0
  62. package/dist/components/resources/ResourceViewModal.d.ts.map +1 -0
  63. package/dist/components/resources/ResourceViewModal.js +44 -0
  64. package/dist/components/theme/ThemeToggle.d.ts +7 -0
  65. package/dist/components/theme/ThemeToggle.d.ts.map +1 -0
  66. package/dist/components/theme/ThemeToggle.js +10 -0
  67. package/dist/hooks/index.d.ts +5 -0
  68. package/dist/hooks/index.d.ts.map +1 -0
  69. package/dist/hooks/index.js +2 -0
  70. package/dist/{useVertebrControls.d.ts → hooks/useVertebrControls.d.ts} +2 -2
  71. package/dist/hooks/useVertebrControls.d.ts.map +1 -0
  72. package/dist/{useVertebrControls.js → hooks/useVertebrControls.js} +11 -21
  73. package/dist/hooks/useVertebrTheme.d.ts +7 -0
  74. package/dist/hooks/useVertebrTheme.d.ts.map +1 -0
  75. package/dist/hooks/useVertebrTheme.js +38 -0
  76. package/dist/index.d.ts +20 -10
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +14 -6
  79. package/dist/lib/description.d.ts +18 -0
  80. package/dist/lib/description.d.ts.map +1 -0
  81. package/dist/lib/description.js +85 -0
  82. package/dist/lib/images.d.ts +14 -0
  83. package/dist/lib/images.d.ts.map +1 -0
  84. package/dist/lib/images.js +63 -0
  85. package/dist/lib/price.d.ts +15 -0
  86. package/dist/lib/price.d.ts.map +1 -0
  87. package/dist/lib/price.js +50 -0
  88. package/dist/styles/auth.css +39 -0
  89. package/dist/styles/base.css +70 -0
  90. package/dist/styles/buttons.css +86 -0
  91. package/dist/styles/cells.css +178 -0
  92. package/dist/styles/columns.css +103 -0
  93. package/dist/styles/form.css +773 -0
  94. package/dist/styles/gallery.css +90 -0
  95. package/dist/styles/index.css +18 -0
  96. package/dist/styles/main.css +89 -0
  97. package/dist/styles/modal.css +225 -0
  98. package/dist/styles/nav.css +92 -0
  99. package/dist/styles/panel.css +215 -0
  100. package/dist/styles/select.css +151 -0
  101. package/dist/styles/sidebar.css +42 -0
  102. package/dist/styles/states.css +169 -0
  103. package/dist/styles/table.css +101 -0
  104. package/dist/styles/theme-toggle.css +87 -0
  105. package/dist/styles.css +2 -997
  106. package/dist/themes/dark.css +55 -0
  107. package/dist/themes/index.css +2 -0
  108. package/dist/themes/light.css +102 -0
  109. package/package.json +13 -3
  110. package/dist/ConfigForm.d.ts +0 -9
  111. package/dist/ConfigForm.d.ts.map +0 -1
  112. package/dist/ConfigForm.js +0 -87
  113. package/dist/EnvSelect.d.ts.map +0 -1
  114. package/dist/MetricsBoard.d.ts.map +0 -1
  115. package/dist/ResourceTable.d.ts +0 -23
  116. package/dist/ResourceTable.d.ts.map +0 -1
  117. package/dist/ResourceTable.js +0 -112
  118. package/dist/VertebrAdmin.d.ts.map +0 -1
  119. package/dist/VertebrAdmin.js +0 -269
  120. package/dist/client.d.ts +0 -32
  121. package/dist/client.d.ts.map +0 -1
  122. package/dist/client.js +0 -65
  123. package/dist/types.d.ts +0 -103
  124. package/dist/types.d.ts.map +0 -1
  125. package/dist/useVertebrControls.d.ts.map +0 -1
  126. /package/dist/{types.js → components/config/form/types.js} +0 -0
  127. /package/dist/{EnvSelect.js → components/env/EnvSelect.js} +0 -0
  128. /package/dist/{MetricsBoard.js → components/metrics/MetricsBoard.js} +0 -0
@@ -6,6 +6,8 @@ export type VertebrAdminProps = {
6
6
  envStorageKey?: string;
7
7
  /** localStorage key for the access token */
8
8
  tokenStorageKey?: string;
9
+ /** localStorage key for light/dark theme preference */
10
+ themeStorageKey?: string;
9
11
  /** Optional host-provided token getter (Firebase ID token, etc.) */
10
12
  getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;
11
13
  /** Signed-in identity shown in the dashboard. */
@@ -13,5 +15,5 @@ export type VertebrAdminProps = {
13
15
  /** Host-provided sign-out action. */
14
16
  onSignOut?: () => void | Promise<void>;
15
17
  };
16
- export declare function VertebrAdmin({ apiBase, title, envStorageKey, tokenStorageKey, getAccessToken, authUserLabel, onSignOut, }: VertebrAdminProps): import("react").JSX.Element;
18
+ export declare function VertebrAdmin({ apiBase, title, envStorageKey, tokenStorageKey, themeStorageKey, getAccessToken, authUserLabel, onSignOut, }: VertebrAdminProps): import("react").JSX.Element;
17
19
  //# sourceMappingURL=VertebrAdmin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../../../src/components/admin/VertebrAdmin.tsx"],"names":[],"mappings":"AAkBA,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,uDAAuD;IACvD,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;AAiCF,wBAAgB,YAAY,CAAC,EAC3B,OAAsB,EACtB,KAAuB,EACvB,aAA6B,EAC7B,eAAiC,EACjC,eAAiC,EACjC,cAAc,EACd,aAAa,EACb,SAAS,GACV,EAAE,iBAAiB,+BA+mBnB"}
@@ -0,0 +1,375 @@
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 "@vertebr/client";
4
+ import { ConfigForm } from "../config/ConfigForm";
5
+ import { EnvSelect } from "../env/EnvSelect";
6
+ import { GalleryBoard } from "../gallery/GalleryBoard";
7
+ import { MetricsBoard } from "../metrics/MetricsBoard";
8
+ import { ResourcePanel } from "../resources/ResourceTable";
9
+ import { ThemeToggle } from "../theme/ThemeToggle";
10
+ import { useVertebrTheme } from "../../hooks/useVertebrTheme";
11
+ function readStored(key) {
12
+ try {
13
+ return localStorage.getItem(key);
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function writeStored(key, value) {
20
+ try {
21
+ localStorage.setItem(key, value);
22
+ }
23
+ catch {
24
+ // ignore
25
+ }
26
+ }
27
+ function clearStored(key) {
28
+ try {
29
+ localStorage.removeItem(key);
30
+ }
31
+ catch {
32
+ // ignore
33
+ }
34
+ }
35
+ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane", envStorageKey = "vertebr:env", tokenStorageKey = "vertebr:token", themeStorageKey = "vertebr:theme", getAccessToken, authUserLabel, onSignOut, }) {
36
+ const { theme, setTheme } = useVertebrTheme(themeStorageKey);
37
+ const [environment, setEnvironment] = useState(() => readStored(envStorageKey) ?? "");
38
+ const [token, setToken] = useState(() => readStored(tokenStorageKey) ?? "");
39
+ const [tokenDraft, setTokenDraft] = useState(token);
40
+ const getEnvironment = useCallback(() => environment, [environment]);
41
+ const client = useMemo(() => createVertebrClient({
42
+ apiBase,
43
+ getEnvironment,
44
+ getAccessToken: getAccessToken ?? (() => token || null),
45
+ }), [apiBase, getEnvironment, getAccessToken, token]);
46
+ const [bootstrap, setBootstrap] = useState(null);
47
+ const [catalog, setCatalog] = useState(null);
48
+ const [selection, setSelection] = useState({ kind: "overview" });
49
+ const [resourceData, setResourceData] = useState(null);
50
+ const [configData, setConfigData] = useState(null);
51
+ const [metricsData, setMetricsData] = useState(null);
52
+ const [metricsLoading, setMetricsLoading] = useState(false);
53
+ const [galleryData, setGalleryData] = useState(null);
54
+ const [galleryLoading, setGalleryLoading] = useState(false);
55
+ const [error, setError] = useState(null);
56
+ const [loading, setLoading] = useState(true);
57
+ useEffect(() => {
58
+ let cancelled = false;
59
+ (async () => {
60
+ try {
61
+ const next = await client.getBootstrap();
62
+ if (cancelled)
63
+ return;
64
+ setBootstrap(next);
65
+ const known = next.environments.map((env) => env.id);
66
+ const preferred = (environment && known.includes(environment) && environment) ||
67
+ next.defaultEnvironment;
68
+ if (preferred !== environment) {
69
+ setEnvironment(preferred);
70
+ writeStored(envStorageKey, preferred);
71
+ }
72
+ }
73
+ catch (err) {
74
+ if (!cancelled) {
75
+ setError(err instanceof Error ? err.message : "Failed to load bootstrap");
76
+ }
77
+ }
78
+ })();
79
+ return () => {
80
+ cancelled = true;
81
+ };
82
+ }, [client, environment, envStorageKey]);
83
+ useEffect(() => {
84
+ if (!bootstrap || !environment)
85
+ return;
86
+ let cancelled = false;
87
+ (async () => {
88
+ try {
89
+ setLoading(true);
90
+ setError(null);
91
+ const next = await client.getCatalog();
92
+ if (cancelled)
93
+ return;
94
+ setCatalog(next);
95
+ setSelection((current) => {
96
+ if (current?.kind === "overview")
97
+ return current;
98
+ if (current?.kind === "gallery" && next.gallery)
99
+ return current;
100
+ if (current?.kind === "resource") {
101
+ if (next.resources.some((r) => r.id === current.id))
102
+ return current;
103
+ }
104
+ if (current?.kind === "config") {
105
+ if (next.configs.some((c) => c.id === current.id))
106
+ return current;
107
+ }
108
+ if (next.metrics)
109
+ return { kind: "overview" };
110
+ const firstResource = next.resources[0];
111
+ if (firstResource)
112
+ return { kind: "resource", id: firstResource.id };
113
+ if (next.gallery)
114
+ return { kind: "gallery" };
115
+ const firstConfig = next.configs[0];
116
+ if (firstConfig)
117
+ return { kind: "config", id: firstConfig.id };
118
+ return { kind: "overview" };
119
+ });
120
+ }
121
+ catch (err) {
122
+ if (!cancelled) {
123
+ setCatalog(null);
124
+ setError(err instanceof Error ? err.message : "Failed to load catalog");
125
+ }
126
+ }
127
+ finally {
128
+ if (!cancelled)
129
+ setLoading(false);
130
+ }
131
+ })();
132
+ return () => {
133
+ cancelled = true;
134
+ };
135
+ }, [client, bootstrap, environment]);
136
+ useEffect(() => {
137
+ if (!selection || !catalog)
138
+ return;
139
+ let cancelled = false;
140
+ (async () => {
141
+ try {
142
+ setError(null);
143
+ if (selection.kind === "overview") {
144
+ setMetricsLoading(true);
145
+ const data = await client.getMetrics();
146
+ if (!cancelled) {
147
+ setMetricsData(data);
148
+ setResourceData(null);
149
+ setConfigData(null);
150
+ setGalleryData(null);
151
+ }
152
+ }
153
+ else if (selection.kind === "gallery") {
154
+ setGalleryLoading(true);
155
+ const data = await client.getGallery();
156
+ if (!cancelled) {
157
+ setGalleryData(data);
158
+ setResourceData(null);
159
+ setConfigData(null);
160
+ setMetricsData(null);
161
+ }
162
+ }
163
+ else if (selection.kind === "resource") {
164
+ const data = await client.listResource(selection.id);
165
+ if (!cancelled) {
166
+ setResourceData(data);
167
+ setConfigData(null);
168
+ setMetricsData(null);
169
+ setGalleryData(null);
170
+ }
171
+ }
172
+ else {
173
+ const data = await client.getConfig(selection.id);
174
+ if (!cancelled) {
175
+ setConfigData(data);
176
+ setResourceData(null);
177
+ setMetricsData(null);
178
+ setGalleryData(null);
179
+ }
180
+ }
181
+ }
182
+ catch (err) {
183
+ if (!cancelled) {
184
+ setError(err instanceof Error ? err.message : "Failed to load panel");
185
+ }
186
+ }
187
+ finally {
188
+ if (!cancelled) {
189
+ setMetricsLoading(false);
190
+ setGalleryLoading(false);
191
+ }
192
+ }
193
+ })();
194
+ return () => {
195
+ cancelled = true;
196
+ };
197
+ }, [client, selection, catalog, environment]);
198
+ const refreshResource = useCallback(async () => {
199
+ if (selection?.kind !== "resource")
200
+ return;
201
+ const data = await client.listResource(selection.id);
202
+ setResourceData(data);
203
+ }, [client, selection]);
204
+ const refreshMetrics = useCallback(async () => {
205
+ setMetricsLoading(true);
206
+ try {
207
+ const data = await client.getMetrics();
208
+ setMetricsData(data);
209
+ }
210
+ finally {
211
+ setMetricsLoading(false);
212
+ }
213
+ }, [client]);
214
+ const refreshGallery = useCallback(async () => {
215
+ setGalleryLoading(true);
216
+ try {
217
+ const data = await client.getGallery({ fresh: true });
218
+ setGalleryData(data);
219
+ }
220
+ finally {
221
+ setGalleryLoading(false);
222
+ }
223
+ }, [client]);
224
+ const listGallery = useCallback(async () => {
225
+ const data = await client.getGallery();
226
+ return data.items;
227
+ }, [client]);
228
+ const uploadGallery = useCallback(async (files) => {
229
+ if (files.length === 1 && files[0]) {
230
+ const { item } = await client.uploadGalleryFile(files[0]);
231
+ return [item];
232
+ }
233
+ const { items } = await client.uploadGalleryFiles(files);
234
+ return items;
235
+ }, [client]);
236
+ const galleryPicker = catalog?.gallery
237
+ ? {
238
+ galleryUpload: {
239
+ accept: catalog.gallery.accept,
240
+ maxBytes: catalog.gallery.maxBytes,
241
+ },
242
+ onListGallery: listGallery,
243
+ onUploadGallery: uploadGallery,
244
+ }
245
+ : {};
246
+ const envList = catalog?.environments ?? bootstrap?.environments ?? [];
247
+ const activeEnv = envList.find((env) => env.id === environment);
248
+ const authRequired = Boolean(activeEnv?.auth?.required);
249
+ const activeConfig = selection?.kind === "config"
250
+ ? catalog?.configs.find((c) => c.id === selection.id)
251
+ : undefined;
252
+ const isPageSelection = activeConfig?.section === "page";
253
+ const switchEnvironment = (nextEnv) => {
254
+ if (nextEnv === environment)
255
+ return;
256
+ const meta = envList.find((env) => env.id === nextEnv);
257
+ if (meta?.tone === "danger") {
258
+ const ok = window.confirm(`Switch to ${meta.label}? Writes will affect this environment.`);
259
+ if (!ok)
260
+ return;
261
+ }
262
+ writeStored(envStorageKey, nextEnv);
263
+ setEnvironment(nextEnv);
264
+ setResourceData(null);
265
+ setConfigData(null);
266
+ setGalleryData(null);
267
+ };
268
+ const saveToken = () => {
269
+ const next = tokenDraft.trim();
270
+ setToken(next);
271
+ if (next)
272
+ writeStored(tokenStorageKey, next);
273
+ else
274
+ clearStored(tokenStorageKey);
275
+ };
276
+ const clearToken = () => {
277
+ setToken("");
278
+ setTokenDraft("");
279
+ clearStored(tokenStorageKey);
280
+ };
281
+ if (!bootstrap && loading) {
282
+ return _jsx("div", { className: "vertebr-loading", children: "Loading Vertebr\u2026" });
283
+ }
284
+ if (error && !bootstrap) {
285
+ return _jsx("div", { className: "vertebr-error", children: error });
286
+ }
287
+ const activeLabel = selection?.kind === "overview"
288
+ ? (catalog?.metrics?.label ?? "Overview")
289
+ : selection?.kind === "gallery"
290
+ ? (catalog?.gallery?.label ?? "Gallery")
291
+ : selection?.kind === "resource"
292
+ ? catalog?.resources.find((r) => r.id === selection.id)?.label
293
+ : selection?.kind === "config"
294
+ ? catalog?.configs.find((c) => c.id === selection.id)?.label
295
+ : null;
296
+ 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 &&
297
+ (catalog.resources.length > 0 || catalog.gallery) ? (_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" &&
298
+ selection.id === resource.id, onClick: () => setSelection({ kind: "resource", id: resource.id }), children: resource.label }, resource.id))), catalog.gallery ? (_jsx("button", { type: "button", "data-active": selection?.kind === "gallery", onClick: () => setSelection({ kind: "gallery" }), children: catalog.gallery.label })) : null] })) : null, catalog &&
299
+ catalog.configs.some((c) => (c.section ?? "config") !== "page") ? (_jsxs("div", { className: "vertebr-nav-group", children: [_jsx("div", { className: "vertebr-nav-label", children: "Config" }), catalog.configs
300
+ .filter((c) => (c.section ?? "config") !== "page")
301
+ .map((config) => (_jsx("button", { type: "button", "data-active": selection?.kind === "config" &&
302
+ selection.id === config.id, onClick: () => setSelection({ kind: "config", id: config.id }), children: config.label }, config.id)))] })) : null, catalog &&
303
+ catalog.configs.some((c) => c.section === "page") ? (_jsxs("div", { className: "vertebr-nav-group", children: [_jsx("div", { className: "vertebr-nav-label", children: "Pages" }), catalog.configs
304
+ .filter((c) => c.section === "page")
305
+ .map((config) => (_jsx("button", { type: "button", "data-active": selection?.kind === "config" &&
306
+ 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
307
+ ? `This environment requires auth${activeEnv?.auth?.claimKey
308
+ ? ` (${activeEnv.auth.claimKey})`
309
+ : ""}.`
310
+ : "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, _jsx(ThemeToggle, { theme: theme, onChange: setTheme })] })] }), _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
311
+ ? "Provide an access token to load this environment."
312
+ : selection?.kind === "overview"
313
+ ? "Live metrics for the selected environment."
314
+ : selection?.kind === "gallery"
315
+ ? "Browse and upload shared media assets."
316
+ : selection?.kind === "resource"
317
+ ? "Manage records scoped to the selected environment."
318
+ : selection?.kind === "config"
319
+ ? isPageSelection
320
+ ? "Edit content for this page."
321
+ : "Owner-managed config for this environment."
322
+ : "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 === "gallery" && galleryData ? (_jsx(GalleryBoard, { label: galleryData.label, items: galleryData.items, accept: galleryData.accept, maxBytes: galleryData.maxBytes, canDelete: galleryData.canDelete, loading: galleryLoading, onRefresh: () => void refreshGallery(), onUpload: async (files) => {
323
+ if (files.length === 1 && files[0]) {
324
+ await client.uploadGalleryFile(files[0]);
325
+ }
326
+ else {
327
+ await client.uploadGalleryFiles(files);
328
+ }
329
+ await refreshGallery();
330
+ }, onDelete: galleryData.canDelete
331
+ ? async (id) => {
332
+ await client.deleteGalleryItem(id);
333
+ await refreshGallery();
334
+ }
335
+ : undefined })) : null, selection?.kind === "resource" && resourceData ? (_jsx(ResourcePanel, { schema: resourceData.schema, items: resourceData.items, permissions: resourceData.permissions, actions: resourceData.actions, uploads: resourceData.uploads, storageKey: resourceData.id, onUpdate: resourceData.permissions.update
336
+ ? async (rowId, value) => {
337
+ await client.updateResource(resourceData.id, rowId, value);
338
+ await refreshResource();
339
+ }
340
+ : undefined, onCreate: resourceData.permissions.create
341
+ ? async (value) => {
342
+ await client.createResource(resourceData.id, value);
343
+ await refreshResource();
344
+ }
345
+ : undefined, onDelete: resourceData.permissions.delete
346
+ ? async (rowId) => {
347
+ await client.deleteResource(resourceData.id, rowId);
348
+ await refreshResource();
349
+ }
350
+ : undefined, onAction: async (rowId, actionId) => {
351
+ await client.runResourceAction(resourceData.id, rowId, actionId);
352
+ await refreshResource();
353
+ }, onUpload: resourceData.uploads
354
+ ? async (field, files) => {
355
+ if (files.length === 1 && files[0]) {
356
+ const result = await client.uploadResourceFile(resourceData.id, field, files[0]);
357
+ return [
358
+ { url: result.url, name: result.name },
359
+ ];
360
+ }
361
+ const result = await client.uploadResourceFiles(resourceData.id, field, files);
362
+ return result.files;
363
+ }
364
+ : undefined, ...galleryPicker })) : null, selection?.kind === "config" && configData ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: isPageSelection ? "Page content" : "Settings" }), _jsxs("span", { className: "vertebr-count", children: [Object.keys(configData.schema).length, " fields"] })] }), _jsx(ConfigForm, { schema: configData.schema, value: configData.value, onSave: async (next) => {
365
+ const saved = await client.setConfig(configData.id, next);
366
+ setConfigData({
367
+ ...configData,
368
+ value: saved.value,
369
+ });
370
+ }, ...galleryPicker })] })) : null] })) : (_jsx("section", { className: "vertebr-panel", children: _jsx("div", { className: "vertebr-empty", children: authRequired
371
+ ? "Authenticated access required for this environment."
372
+ : loading
373
+ ? "Loading…"
374
+ : "Unable to load catalog." }) })) })] })] }));
375
+ }
@@ -0,0 +1,3 @@
1
+ export { ConfigForm } from "./form/ConfigForm";
2
+ export type { ConfigFormProps, ConfigFormVariant } from "./form/types";
3
+ //# sourceMappingURL=ConfigForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfigForm.d.ts","sourceRoot":"","sources":["../../../src/components/config/ConfigForm.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1 @@
1
+ export { ConfigForm } from "./form/ConfigForm";
@@ -0,0 +1,18 @@
1
+ import type { GalleryItem, ResourceUploadMeta } from "@vertebr/client";
2
+ import type { RecordImage } from "../../lib/images";
3
+ export type DescriptionInputProps = {
4
+ id: string;
5
+ name?: string;
6
+ value: unknown;
7
+ readOnly?: boolean;
8
+ placeholder?: string;
9
+ recordImages?: RecordImage[];
10
+ galleryUpload?: Pick<ResourceUploadMeta, "accept" | "maxBytes">;
11
+ onListGallery?: () => Promise<GalleryItem[]>;
12
+ onUploadGallery?: (files: File[]) => Promise<GalleryItem[]>;
13
+ onBlur?: () => void;
14
+ onChange: (next: string) => void;
15
+ inputRef?: React.Ref<HTMLElement>;
16
+ };
17
+ export declare function DescriptionInput({ id, name, value, readOnly, placeholder, recordImages, galleryUpload, onListGallery, onUploadGallery, onBlur, onChange, inputRef, }: DescriptionInputProps): import("react").JSX.Element;
18
+ //# sourceMappingURL=DescriptionInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DescriptionInput.d.ts","sourceRoot":"","sources":["../../../src/components/config/DescriptionInput.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;CACnC,CAAC;AAoCF,wBAAgB,gBAAgB,CAAC,EAC/B,EAAE,EACF,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,WAAoC,EACpC,YAAY,EACZ,aAAa,EACb,aAAa,EACb,eAAe,EACf,MAAM,EACN,QAAQ,EACR,QAAQ,GACT,EAAE,qBAAqB,+BA6RvB"}
@@ -0,0 +1,152 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
4
+ import StarterKit from "@tiptap/starter-kit";
5
+ import { Markdown } from "@tiptap/markdown";
6
+ import { Placeholder } from "@tiptap/extensions";
7
+ import Image from "@tiptap/extension-image";
8
+ import { TableKit } from "@tiptap/extension-table";
9
+ import { descriptionStats } from "../../lib/description";
10
+ import { ImagePickerModal } from "./ImagePickerModal";
11
+ function assignRef(ref, node) {
12
+ if (!ref)
13
+ return;
14
+ if (typeof ref === "function")
15
+ ref(node);
16
+ else
17
+ ref.current = node;
18
+ }
19
+ function ToolbarButton({ active, disabled, title, onClick, children, }) {
20
+ return (_jsx("button", { type: "button", className: "vertebr-md-btn", "data-active": active || undefined, disabled: disabled, title: title, onMouseDown: (event) => event.preventDefault(), onClick: onClick, children: children }));
21
+ }
22
+ export function DescriptionInput({ id, name, value, readOnly, placeholder = "Write a description…", recordImages, galleryUpload, onListGallery, onUploadGallery, onBlur, onChange, inputRef, }) {
23
+ const text = typeof value === "string" ? value : "";
24
+ const onChangeRef = useRef(onChange);
25
+ const onBlurRef = useRef(onBlur);
26
+ const lastEmitted = useRef(text);
27
+ const [pickerOpen, setPickerOpen] = useState(false);
28
+ onChangeRef.current = onChange;
29
+ onBlurRef.current = onBlur;
30
+ const editor = useEditor({
31
+ immediatelyRender: false,
32
+ shouldRerenderOnTransaction: false,
33
+ content: text,
34
+ contentType: "markdown",
35
+ editable: !readOnly,
36
+ extensions: [
37
+ StarterKit.configure({
38
+ heading: { levels: [2, 3] },
39
+ underline: false,
40
+ link: {
41
+ openOnClick: false,
42
+ defaultProtocol: "https",
43
+ },
44
+ }),
45
+ Image.configure({ inline: false, allowBase64: false }),
46
+ TableKit.configure({
47
+ table: { resizable: false },
48
+ }),
49
+ Placeholder.configure({ placeholder }),
50
+ Markdown.configure({
51
+ markedOptions: { gfm: true },
52
+ }),
53
+ ],
54
+ editorProps: {
55
+ attributes: {
56
+ id,
57
+ class: "vertebr-md-content",
58
+ },
59
+ },
60
+ onUpdate: ({ editor: next }) => {
61
+ const markdown = next.getMarkdown();
62
+ lastEmitted.current = markdown;
63
+ onChangeRef.current(markdown);
64
+ },
65
+ onBlur: () => {
66
+ onBlurRef.current?.();
67
+ },
68
+ });
69
+ useEffect(() => {
70
+ if (!editor)
71
+ return;
72
+ assignRef(inputRef, editor.view.dom);
73
+ return () => assignRef(inputRef, null);
74
+ }, [editor, inputRef]);
75
+ useEffect(() => {
76
+ if (!editor)
77
+ return;
78
+ editor.setEditable(!readOnly);
79
+ }, [editor, readOnly]);
80
+ useEffect(() => {
81
+ if (!editor)
82
+ return;
83
+ if (text === lastEmitted.current)
84
+ return;
85
+ if (text === editor.getMarkdown()) {
86
+ lastEmitted.current = text;
87
+ return;
88
+ }
89
+ editor.commands.setContent(text, {
90
+ contentType: "markdown",
91
+ emitUpdate: false,
92
+ });
93
+ lastEmitted.current = text;
94
+ }, [editor, text]);
95
+ const marks = useEditorState({
96
+ editor,
97
+ selector: ({ editor: next }) => ({
98
+ bold: next?.isActive("bold") ?? false,
99
+ italic: next?.isActive("italic") ?? false,
100
+ strike: next?.isActive("strike") ?? false,
101
+ code: next?.isActive("code") ?? false,
102
+ codeBlock: next?.isActive("codeBlock") ?? false,
103
+ heading: next?.isActive("heading") ?? false,
104
+ quote: next?.isActive("blockquote") ?? false,
105
+ bullet: next?.isActive("bulletList") ?? false,
106
+ ordered: next?.isActive("orderedList") ?? false,
107
+ table: next?.isActive("table") ?? false,
108
+ link: next?.isActive("link") ?? false,
109
+ stats: descriptionStats(next?.getText() ?? text),
110
+ }),
111
+ });
112
+ const stats = marks?.stats ?? descriptionStats(text);
113
+ const locked = readOnly || !editor;
114
+ const setLink = () => {
115
+ if (!editor || readOnly)
116
+ return;
117
+ const previous = editor.getAttributes("link").href;
118
+ const href = window.prompt("Link URL", typeof previous === "string" && previous ? previous : "https://");
119
+ if (href === null)
120
+ return;
121
+ if (!href.trim()) {
122
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
123
+ return;
124
+ }
125
+ editor.chain().focus().extendMarkRange("link").setLink({ href: href.trim() }).run();
126
+ };
127
+ const insertImages = (images) => {
128
+ if (!editor || images.length === 0)
129
+ return;
130
+ editor
131
+ .chain()
132
+ .focus()
133
+ .insertContent(images.map((image) => ({
134
+ type: "image",
135
+ attrs: { src: image.url, alt: image.name },
136
+ })))
137
+ .run();
138
+ setPickerOpen(false);
139
+ };
140
+ return (_jsxs("div", { className: "vertebr-field-prose", onKeyDown: (event) => {
141
+ if (!(event.metaKey || event.ctrlKey))
142
+ return;
143
+ if (event.key.toLowerCase() !== "k")
144
+ return;
145
+ event.preventDefault();
146
+ setLink();
147
+ }, children: [name ? _jsx("input", { type: "hidden", name: name, value: text, readOnly: true }) : null, _jsxs("div", { className: "vertebr-md-toolbar", role: "toolbar", "aria-label": "Format", children: [_jsx(ToolbarButton, { active: marks?.bold, disabled: locked, title: "Bold", onClick: () => editor?.chain().focus().toggleBold().run(), children: _jsx("span", { className: "vertebr-md-glyph", children: "B" }) }), _jsx(ToolbarButton, { active: marks?.italic, disabled: locked, title: "Italic", onClick: () => editor?.chain().focus().toggleItalic().run(), children: _jsx("span", { className: "vertebr-md-glyph vertebr-md-italic", children: "I" }) }), _jsx(ToolbarButton, { active: marks?.strike, disabled: locked, title: "Strikethrough", onClick: () => editor?.chain().focus().toggleStrike().run(), children: _jsx("span", { className: "vertebr-md-glyph vertebr-md-strike", children: "S" }) }), _jsx(ToolbarButton, { active: marks?.code, disabled: locked, title: "Inline code", onClick: () => editor?.chain().focus().toggleCode().run(), children: "</>" }), _jsx(ToolbarButton, { active: marks?.codeBlock, disabled: locked, title: "Code block", onClick: () => editor?.chain().focus().toggleCodeBlock().run(), children: "{ }" }), _jsx("span", { className: "vertebr-md-sep", "aria-hidden": "true" }), _jsx(ToolbarButton, { active: marks?.heading, disabled: locked, title: "Heading", onClick: () => editor?.chain().focus().toggleHeading({ level: 2 }).run(), children: "H" }), _jsx(ToolbarButton, { active: marks?.quote, disabled: locked, title: "Quote", onClick: () => editor?.chain().focus().toggleBlockquote().run(), children: "\u201C" }), _jsx(ToolbarButton, { active: marks?.bullet, disabled: locked, title: "Bulleted list", onClick: () => editor?.chain().focus().toggleBulletList().run(), children: "\u2022" }), _jsx(ToolbarButton, { active: marks?.ordered, disabled: locked, title: "Numbered list", onClick: () => editor?.chain().focus().toggleOrderedList().run(), children: "1." }), _jsx("span", { className: "vertebr-md-sep", "aria-hidden": "true" }), _jsx(ToolbarButton, { active: marks?.table, disabled: locked, title: "Table", onClick: () => editor?.chain().focus().insertTable({
148
+ rows: 3,
149
+ cols: 3,
150
+ withHeaderRow: true,
151
+ }).run(), children: "Table" }), marks?.table ? (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { disabled: locked, title: "Add column", onClick: () => editor?.chain().focus().addColumnAfter().run(), children: "+Col" }), _jsx(ToolbarButton, { disabled: locked, title: "Add row", onClick: () => editor?.chain().focus().addRowAfter().run(), children: "+Row" }), _jsx(ToolbarButton, { disabled: locked, title: "Delete table", onClick: () => editor?.chain().focus().deleteTable().run(), children: "Del" })] })) : null, _jsx(ToolbarButton, { active: marks?.link, disabled: locked, title: "Link", onClick: setLink, children: "Link" }), _jsx(ToolbarButton, { disabled: locked, title: "Insert images", onClick: () => setPickerOpen(true), children: "Image" })] }), _jsx(EditorContent, { editor: editor }), _jsxs("p", { className: "vertebr-field-prose-meta", children: ["Markdown", _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), stats.words, " ", stats.words === 1 ? "word" : "words", _jsx("span", { "aria-hidden": "true", children: " \u00B7 " }), stats.characters, " ", stats.characters === 1 ? "character" : "characters"] }), pickerOpen ? (_jsx(ImagePickerModal, { recordImages: recordImages, galleryUpload: galleryUpload, onListGallery: onListGallery, onUploadGallery: onUploadGallery, onClose: () => setPickerOpen(false), onInsert: insertImages })) : null] }));
152
+ }
@@ -0,0 +1,18 @@
1
+ import type { GalleryItem, ResourceUploadMeta } from "@vertebr/client";
2
+ import type { RecordImage } from "../../lib/images";
3
+ export type PickerImage = {
4
+ id: string;
5
+ url: string;
6
+ name: string;
7
+ source: "record" | "gallery" | "url";
8
+ };
9
+ export type ImagePickerModalProps = {
10
+ recordImages?: RecordImage[];
11
+ galleryUpload?: Pick<ResourceUploadMeta, "accept" | "maxBytes">;
12
+ onListGallery?: () => Promise<GalleryItem[]>;
13
+ onUploadGallery?: (files: File[]) => Promise<GalleryItem[]>;
14
+ onClose: () => void;
15
+ onInsert: (images: PickerImage[]) => void;
16
+ };
17
+ export declare function ImagePickerModal({ recordImages, galleryUpload, onListGallery, onUploadGallery, onClose, onInsert, }: ImagePickerModalProps): import("react").JSX.Element;
18
+ //# sourceMappingURL=ImagePickerModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ImagePickerModal.d.ts","sourceRoot":"","sources":["../../../src/components/config/ImagePickerModal.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,YAAY,CAAC,EAAE,WAAW,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC,CAAC;IAChE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7C,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;CAC3C,CAAC;AAIF,wBAAgB,gBAAgB,CAAC,EAC/B,YAAiB,EACjB,aAAa,EACb,aAAa,EACb,eAAe,EACf,OAAO,EACP,QAAQ,GACT,EAAE,qBAAqB,+BA4PvB"}