@vertebr/react 0.5.0 → 0.6.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 (75) hide show
  1. package/dist/components/admin/AdminContent.d.ts +11 -2
  2. package/dist/components/admin/AdminContent.d.ts.map +1 -1
  3. package/dist/components/admin/AdminContent.js +35 -10
  4. package/dist/components/admin/AdminHeader.d.ts +3 -1
  5. package/dist/components/admin/AdminHeader.d.ts.map +1 -1
  6. package/dist/components/admin/AdminHeader.js +12 -3
  7. package/dist/components/admin/PagePreviewPane.d.ts +14 -0
  8. package/dist/components/admin/PagePreviewPane.d.ts.map +1 -0
  9. package/dist/components/admin/PagePreviewPane.js +121 -0
  10. package/dist/components/admin/VertebrAdmin.d.ts +1 -1
  11. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  12. package/dist/components/admin/VertebrAdmin.js +123 -41
  13. package/dist/components/admin/routes.d.ts +21 -0
  14. package/dist/components/admin/routes.d.ts.map +1 -0
  15. package/dist/components/admin/routes.js +170 -0
  16. package/dist/components/admin/types.d.ts +7 -0
  17. package/dist/components/admin/types.d.ts.map +1 -1
  18. package/dist/components/config/VisibilityEye.d.ts +12 -0
  19. package/dist/components/config/VisibilityEye.d.ts.map +1 -1
  20. package/dist/components/config/VisibilityEye.js +12 -4
  21. package/dist/components/config/form/ConfigField.d.ts +5 -3
  22. package/dist/components/config/form/ConfigField.d.ts.map +1 -1
  23. package/dist/components/config/form/ConfigField.js +7 -3
  24. package/dist/components/config/form/ConfigForm.d.ts +1 -1
  25. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  26. package/dist/components/config/form/ConfigForm.js +5 -1
  27. package/dist/components/config/form/RecordConfigForm.d.ts +1 -1
  28. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
  29. package/dist/components/config/form/RecordConfigForm.js +2 -2
  30. package/dist/components/config/form/SettingsConfigForm.d.ts +3 -3
  31. package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -1
  32. package/dist/components/config/form/SettingsConfigForm.js +7 -2
  33. package/dist/components/config/form/helpers/fields.d.ts +2 -1
  34. package/dist/components/config/form/helpers/fields.d.ts.map +1 -1
  35. package/dist/components/config/form/helpers/fields.js +4 -0
  36. package/dist/components/config/form/types.d.ts +6 -1
  37. package/dist/components/config/form/types.d.ts.map +1 -1
  38. package/dist/components/fonts/FontFaceInjector.d.ts +9 -0
  39. package/dist/components/fonts/FontFaceInjector.d.ts.map +1 -0
  40. package/dist/components/fonts/FontFaceInjector.js +28 -0
  41. package/dist/components/fonts/FontLibraryBoard.d.ts +15 -0
  42. package/dist/components/fonts/FontLibraryBoard.d.ts.map +1 -0
  43. package/dist/components/fonts/FontLibraryBoard.js +41 -0
  44. package/dist/components/fonts/FontPickerField.d.ts +16 -0
  45. package/dist/components/fonts/FontPickerField.d.ts.map +1 -0
  46. package/dist/components/fonts/FontPickerField.js +49 -0
  47. package/dist/components/fonts/FontPreviewModal.d.ts +18 -0
  48. package/dist/components/fonts/FontPreviewModal.d.ts.map +1 -0
  49. package/dist/components/fonts/FontPreviewModal.js +57 -0
  50. package/dist/components/fonts/FontPreviewSample.d.ts +8 -0
  51. package/dist/components/fonts/FontPreviewSample.d.ts.map +1 -0
  52. package/dist/components/fonts/FontPreviewSample.js +13 -0
  53. package/dist/components/fonts/GoogleFontsBrowser.d.ts +8 -0
  54. package/dist/components/fonts/GoogleFontsBrowser.d.ts.map +1 -0
  55. package/dist/components/fonts/GoogleFontsBrowser.js +49 -0
  56. package/dist/components/resources/ResourceCell.js +3 -2
  57. package/dist/components/resources/ResourceTable.d.ts +1 -1
  58. package/dist/components/resources/ResourceTable.d.ts.map +1 -1
  59. package/dist/components/resources/ResourceTable.js +123 -5
  60. package/dist/hooks/useVertebrControls.d.ts +10 -1
  61. package/dist/hooks/useVertebrControls.d.ts.map +1 -1
  62. package/dist/hooks/useVertebrControls.js +32 -3
  63. package/dist/index.d.ts +3 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +3 -2
  66. package/dist/styles/base.css +3 -3
  67. package/dist/styles/font-library.css +401 -0
  68. package/dist/styles/form.css +89 -3
  69. package/dist/styles/index.css +3 -1
  70. package/dist/styles/main.css +1 -1
  71. package/dist/styles/page-preview.css +208 -0
  72. package/dist/styles/panel.css +7 -2
  73. package/dist/styles/states.css +6 -3
  74. package/dist/styles/table.css +18 -0
  75. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import type { CatalogResponse, ConfigResponse, EmailsBoardResponse, GalleryResponse, MetricsResponse, ResourceListResponse, VertebrClient } from "@vertebr/client";
1
+ import type { CatalogResponse, ConfigResponse, EmailsBoardResponse, FontLibraryResponse, GalleryResponse, MetricsResponse, ResourceListResponse, VertebrClient } from "@vertebr/client";
2
2
  import type { MetricsRangeId } from "../../lib/metricsRanges";
3
3
  import type { Selection } from "./types";
4
4
  export type AdminContentProps = {
@@ -16,6 +16,9 @@ export type AdminContentProps = {
16
16
  galleryData: GalleryResponse | null;
17
17
  galleryLoading: boolean;
18
18
  onRefreshGallery: () => Promise<void>;
19
+ fontLibraryData: FontLibraryResponse | null;
20
+ fontLibraryLoading: boolean;
21
+ onRefreshFonts: () => Promise<void>;
19
22
  emailsData: EmailsBoardResponse | null;
20
23
  emailsLoading: boolean;
21
24
  onRefreshEmails: () => Promise<void>;
@@ -31,6 +34,12 @@ export type AdminContentProps = {
31
34
  onListGallery?: () => Promise<GalleryResponse["items"]>;
32
35
  onUploadGallery?: (files: File[]) => Promise<GalleryResponse["items"]>;
33
36
  };
37
+ fontLibraryPicker: {
38
+ fonts?: FontLibraryResponse["items"];
39
+ fontsLoading?: boolean;
40
+ onListFonts?: () => Promise<FontLibraryResponse["items"]>;
41
+ onRefreshFonts?: () => Promise<void>;
42
+ };
34
43
  };
35
- export declare function AdminContent({ client, catalog, selection, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, emailsData, emailsLoading, onRefreshEmails, resourceData, onRefreshResource, configData, onConfigSaved, galleryPicker, }: AdminContentProps): import("react").JSX.Element;
44
+ export declare function AdminContent({ client, catalog, selection, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, fontLibraryData, fontLibraryLoading, onRefreshFonts, emailsData, emailsLoading, onRefreshEmails, resourceData, onRefreshResource, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }: AdminContentProps): import("react").JSX.Element;
36
45
  //# sourceMappingURL=AdminContent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdminContent.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminContent.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,aAAa,EACd,MAAM,iBAAiB,CAAC;AAMzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,cAAc,CAAC;IAC7B,oBAAoB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACrD,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACvC,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC1C,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,aAAa,EAAE;QACb,aAAa,CAAC,EAAE;YACd,MAAM,EAAE,MAAM,EAAE,CAAC;YACjB,QAAQ,EAAE,MAAM,CAAC;SAClB,CAAC;QACF,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;QACxD,eAAe,CAAC,EAAE,CAChB,KAAK,EAAE,IAAI,EAAE,KACV,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;KACxC,CAAC;CACH,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,OAAO,EACP,SAAS,EACT,YAAY,EACZ,OAAO,EACP,eAAe,EACf,WAAW,EACX,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,aAAa,GACd,EAAE,iBAAiB,+BAgPnB"}
1
+ {"version":3,"file":"AdminContent.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminContent.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,aAAa,EACd,MAAM,iBAAiB,CAAC;AAOzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,EAAE,cAAc,CAAC;IAC7B,oBAAoB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACrD,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,WAAW,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,cAAc,EAAE,OAAO,CAAC;IACxB,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,eAAe,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5C,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,UAAU,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACvC,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC1C,iBAAiB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,UAAU,EAAE,cAAc,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,aAAa,EAAE;QACb,aAAa,CAAC,EAAE;YACd,MAAM,EAAE,MAAM,EAAE,CAAC;YACjB,QAAQ,EAAE,MAAM,CAAC;SAClB,CAAC;QACF,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;QACxD,eAAe,CAAC,EAAE,CAChB,KAAK,EAAE,IAAI,EAAE,KACV,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;KACxC,CAAC;IACF,iBAAiB,EAAE;QACjB,KAAK,CAAC,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC;QACrC,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1D,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;KACtC,CAAC;CACH,CAAC;AA6EF,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,OAAO,EACP,SAAS,EACT,YAAY,EACZ,OAAO,EACP,eAAe,EACf,WAAW,EACX,cAAc,EACd,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,aAAa,EACb,eAAe,EACf,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,aAAa,EACb,iBAAiB,GAClB,EAAE,iBAAiB,+BAuQnB"}
@@ -1,10 +1,25 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useState } from "react";
2
3
  import { ConfigForm } from "../config/ConfigForm";
3
4
  import { EmailsBoard } from "../emails/EmailsBoard";
5
+ import { FontLibraryBoard } from "../fonts/FontLibraryBoard";
4
6
  import { GalleryBoard } from "../gallery/GalleryBoard";
5
7
  import { MetricsBoard } from "../metrics/MetricsBoard";
6
8
  import { ResourcePanel } from "../resources/ResourceTable";
7
- export function AdminContent({ client, catalog, selection, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, emailsData, emailsLoading, onRefreshEmails, resourceData, onRefreshResource, configData, onConfigSaved, galleryPicker, }) {
9
+ import { PagePreviewPane } from "./PagePreviewPane";
10
+ function ConfigEditor({ client, configData, isPageSelection, previewPath, onConfigSaved, galleryPicker, fontLibraryPicker, }) {
11
+ const [draft, setDraft] = useState(configData.value);
12
+ const showPreview = Boolean(previewPath);
13
+ const onChange = useCallback((value) => {
14
+ setDraft(value);
15
+ }, []);
16
+ const form = (_jsx(ConfigForm, { schema: configData.schema, value: configData.value, onChange: showPreview ? onChange : undefined, onSave: async (next) => {
17
+ const saved = await client.setConfig(configData.id, next);
18
+ onConfigSaved(saved);
19
+ }, ...galleryPicker, ...fontLibraryPicker }));
20
+ return (_jsxs("div", { className: "vertebr-config-editor", children: [showPreview ? (_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: isPageSelection ? "Page content" : configData.label }), _jsxs("span", { className: "vertebr-count", children: [Object.keys(configData.schema).length, " fields"] })] })) : isPageSelection ? (_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: "Page content" }), _jsxs("span", { className: "vertebr-count", children: [Object.keys(configData.schema).length, " fields"] })] })) : null, showPreview && previewPath ? (_jsxs("div", { className: "vertebr-page-editor", children: [_jsx("div", { className: "vertebr-page-editor-form", children: form }), _jsx(PagePreviewPane, { previewPath: previewPath, controlId: configData.id, draft: draft ?? configData.value })] })) : (form)] }));
21
+ }
22
+ export function AdminContent({ client, catalog, selection, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, fontLibraryData, fontLibraryLoading, onRefreshFonts, emailsData, emailsLoading, onRefreshEmails, resourceData, onRefreshResource, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }) {
8
23
  if (!catalog) {
9
24
  return (_jsx("div", { className: "vertebr-content", children: _jsx("section", { className: "vertebr-panel", children: _jsx("div", { className: "vertebr-empty", children: authRequired
10
25
  ? "Authenticated access required for this environment."
@@ -25,6 +40,21 @@ export function AdminContent({ client, catalog, selection, authRequired, loading
25
40
  await client.deleteGalleryItem(id);
26
41
  await onRefreshGallery();
27
42
  }
43
+ : undefined })) : null, selection?.kind === "fontLibrary" && fontLibraryData ? (_jsx(FontLibraryBoard, { label: fontLibraryData.label, data: fontLibraryData, loading: fontLibraryLoading, onRefresh: () => void onRefreshFonts(), onUpload: async (files, meta) => {
44
+ for (const file of files) {
45
+ await client.uploadFontFile(file, meta);
46
+ }
47
+ await onRefreshFonts();
48
+ }, onImportGoogle: fontLibraryData.canImportGoogle
49
+ ? async (payload) => {
50
+ await client.importGoogleFont(payload);
51
+ await onRefreshFonts();
52
+ }
53
+ : undefined, onDelete: fontLibraryData.canDelete
54
+ ? async (id) => {
55
+ await client.deleteFontItem(id);
56
+ await onRefreshFonts();
57
+ }
28
58
  : undefined })) : null, selection?.kind === "emails" && emailsData ? (_jsx(EmailsBoard, { data: emailsData, loading: emailsLoading, tab: selection.tab ?? "compose", onRefresh: () => void onRefreshEmails(), onLoadRecipients: async (resourceId) => {
29
59
  const listed = await client.listResource(resourceId, {
30
60
  fresh: true,
@@ -87,7 +117,7 @@ export function AdminContent({ client, catalog, selection, authRequired, loading
87
117
  }, onActionDone: onRefreshResource, onPreviewMail: async (rowId, actionId, input) => {
88
118
  return client.previewResourceMail(resourceData.id, rowId, actionId, input);
89
119
  }, onCollectionAction: async (actionId, input) => {
90
- await client.runCollectionAction(resourceData.id, actionId, input);
120
+ return client.runCollectionAction(resourceData.id, actionId, input);
91
121
  }, onUpload: resourceData.uploads
92
122
  ? async (field, files) => {
93
123
  if (files.length === 1 && files[0]) {
@@ -97,11 +127,6 @@ export function AdminContent({ client, catalog, selection, authRequired, loading
97
127
  const result = await client.uploadResourceFiles(resourceData.id, field, files);
98
128
  return result.files;
99
129
  }
100
- : 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) => {
101
- const saved = await client.setConfig(configData.id, next);
102
- onConfigSaved({
103
- ...configData,
104
- value: saved.value,
105
- });
106
- }, ...galleryPicker })] })) : null] }) }));
130
+ : undefined, ...galleryPicker })) : null, selection?.kind === "config" && configData ? (_jsx(ConfigEditor, { client: client, configData: configData, isPageSelection: isPageSelection, previewPath: catalog.configs.find((item) => item.id === configData.id)
131
+ ?.previewPath, onConfigSaved: onConfigSaved, galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker }, configData.id)) : null] }) }));
107
132
  }
@@ -7,7 +7,9 @@ export type AdminHeaderProps = {
7
7
  authRequired: boolean;
8
8
  selection: Selection;
9
9
  isPageSelection: boolean;
10
+ /** True when the active page/layout has a host previewPath. */
11
+ hasPagePreview?: boolean;
10
12
  error: string | null;
11
13
  };
12
- export declare function AdminHeader({ activeLabel, activeEnv, catalogLoaded, authRequired, selection, isPageSelection, error, }: AdminHeaderProps): import("react").JSX.Element;
14
+ export declare function AdminHeader({ activeLabel, activeEnv, catalogLoaded, authRequired, selection, isPageSelection, hasPagePreview, error, }: AdminHeaderProps): import("react").JSX.Element;
13
15
  //# sourceMappingURL=AdminHeader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdminHeader.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAkCF,wBAAgB,WAAW,CAAC,EAC1B,WAAW,EACX,SAAS,EACT,aAAa,EACb,YAAY,EACZ,SAAS,EACT,eAAe,EACf,KAAK,GACN,EAAE,gBAAgB,+BA+BlB"}
1
+ {"version":3,"file":"AdminHeader.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,+DAA+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AA+CF,wBAAgB,WAAW,CAAC,EAC1B,WAAW,EACX,SAAS,EACT,aAAa,EACb,YAAY,EACZ,SAAS,EACT,eAAe,EACf,cAAc,EACd,KAAK,GACN,EAAE,gBAAgB,+BAgClB"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- function subtitle({ catalogLoaded, authRequired, selection, isPageSelection, }) {
2
+ function subtitle({ catalogLoaded, authRequired, selection, isPageSelection, hasPagePreview, }) {
3
3
  if (!catalogLoaded && authRequired) {
4
4
  return "Provide an access token to load this environment.";
5
5
  }
@@ -9,6 +9,9 @@ function subtitle({ catalogLoaded, authRequired, selection, isPageSelection, })
9
9
  if (selection?.kind === "gallery") {
10
10
  return "Browse and upload shared media assets.";
11
11
  }
12
+ if (selection?.kind === "fontLibrary") {
13
+ return "Upload custom fonts or add Google Fonts to your library.";
14
+ }
12
15
  if (selection?.kind === "emails") {
13
16
  return "Send or schedule branded email to a list or specific people.";
14
17
  }
@@ -16,17 +19,23 @@ function subtitle({ catalogLoaded, authRequired, selection, isPageSelection, })
16
19
  return "Manage records scoped to the selected environment.";
17
20
  }
18
21
  if (selection?.kind === "config") {
22
+ if (hasPagePreview) {
23
+ return isPageSelection
24
+ ? "Edit content — live storefront preview updates as you type."
25
+ : "Edit settings — live storefront preview updates as you change typography and colors.";
26
+ }
19
27
  return isPageSelection
20
28
  ? "Edit content for this page."
21
- : "Owner-managed config for this environment.";
29
+ : "Environment settings — changes apply to this environment only.";
22
30
  }
23
31
  return "Select a panel.";
24
32
  }
25
- export function AdminHeader({ activeLabel, activeEnv, catalogLoaded, authRequired, selection, isPageSelection, error, }) {
33
+ export function AdminHeader({ activeLabel, activeEnv, catalogLoaded, authRequired, selection, isPageSelection, hasPagePreview, error, }) {
26
34
  return (_jsxs("header", { className: "vertebr-header", children: [_jsxs("div", { children: [_jsxs("div", { className: "vertebr-heading-row", children: [_jsx("h1", { children: activeLabel }), activeEnv ? (_jsx("span", { className: "vertebr-env-badge", "data-tone": activeEnv.tone ?? "neutral", children: activeEnv.label })) : null] }), _jsx("p", { children: subtitle({
27
35
  catalogLoaded,
28
36
  authRequired,
29
37
  selection,
30
38
  isPageSelection,
39
+ hasPagePreview,
31
40
  }) })] }), error ? (_jsx("span", { className: "vertebr-status", "data-tone": "danger", children: error })) : null] }));
32
41
  }
@@ -0,0 +1,14 @@
1
+ export type PagePreviewViewport = "desktop" | "mobile";
2
+ export type PagePreviewPaneProps = {
3
+ /** Host route from the page/layout definition, e.g. `/about`. */
4
+ previewPath: string;
5
+ /** Control id being edited. */
6
+ controlId: string;
7
+ /** Latest unsaved form values. */
8
+ draft: Record<string, unknown> | null;
9
+ };
10
+ /**
11
+ * Live host iframe for Pages. Vertebr posts drafts; the host renders.
12
+ */
13
+ export declare function PagePreviewPane({ previewPath, controlId, draft, }: PagePreviewPaneProps): import("react").JSX.Element;
14
+ //# sourceMappingURL=PagePreviewPane.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PagePreviewPane.d.ts","sourceRoot":"","sources":["../../../src/components/admin/PagePreviewPane.tsx"],"names":[],"mappings":"AAOA,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG;IACjC,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACvC,CAAC;AAWF;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,SAAS,EACT,KAAK,GACN,EAAE,oBAAoB,+BAmMtB"}
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { buildVertebrPreviewUrl, postVertebrPreviewDraft, subscribeVertebrPreviewReady, } from "@vertebr/client";
4
+ const VIEWPORTS = [
5
+ { id: "desktop", label: "Desktop" },
6
+ { id: "mobile", label: "Mobile" },
7
+ ];
8
+ /** Logical CSS viewport for mobile preview (host media queries use this). */
9
+ const MOBILE_WIDTH = 390;
10
+ const MOBILE_HEIGHT = 844;
11
+ /**
12
+ * Live host iframe for Pages. Vertebr posts drafts; the host renders.
13
+ */
14
+ export function PagePreviewPane({ previewPath, controlId, draft, }) {
15
+ const iframeRef = useRef(null);
16
+ const stageRef = useRef(null);
17
+ const draftRef = useRef(draft);
18
+ const [frameReady, setFrameReady] = useState(false);
19
+ const [frameError, setFrameError] = useState(false);
20
+ const [viewport, setViewport] = useState("desktop");
21
+ const [mobileScale, setMobileScale] = useState(1);
22
+ draftRef.current = draft;
23
+ const isMobile = viewport === "mobile";
24
+ const src = useMemo(() => buildVertebrPreviewUrl(previewPath, {
25
+ controlId,
26
+ }), [previewPath, controlId]);
27
+ const pushDraft = useCallback(() => {
28
+ const win = iframeRef.current?.contentWindow;
29
+ const value = draftRef.current;
30
+ if (!win || !value)
31
+ return;
32
+ postVertebrPreviewDraft(win, controlId, value);
33
+ }, [controlId]);
34
+ const notifyHostResize = useCallback(() => {
35
+ const win = iframeRef.current?.contentWindow;
36
+ if (!win)
37
+ return;
38
+ try {
39
+ win.dispatchEvent(new Event("resize"));
40
+ }
41
+ catch {
42
+ // Cross-origin should not happen on same-origin preview hosts.
43
+ }
44
+ }, []);
45
+ useEffect(() => {
46
+ setFrameReady(false);
47
+ setFrameError(false);
48
+ }, [src]);
49
+ useEffect(() => {
50
+ return subscribeVertebrPreviewReady((source) => {
51
+ const win = iframeRef.current?.contentWindow;
52
+ if (!win || source !== win)
53
+ return;
54
+ setFrameReady(true);
55
+ pushDraft();
56
+ });
57
+ }, [pushDraft, src]);
58
+ useEffect(() => {
59
+ if (!frameReady)
60
+ return;
61
+ pushDraft();
62
+ }, [draft, frameReady, pushDraft]);
63
+ // Fit the fixed mobile viewport into the stage without changing its
64
+ // layout width (transform scale keeps host media queries at 390px).
65
+ useEffect(() => {
66
+ if (!isMobile) {
67
+ setMobileScale(1);
68
+ return;
69
+ }
70
+ const stage = stageRef.current;
71
+ if (!stage || typeof ResizeObserver === "undefined")
72
+ return;
73
+ const update = () => {
74
+ const style = getComputedStyle(stage);
75
+ const padX = (Number.parseFloat(style.paddingLeft) || 0) +
76
+ (Number.parseFloat(style.paddingRight) || 0);
77
+ const padY = (Number.parseFloat(style.paddingTop) || 0) +
78
+ (Number.parseFloat(style.paddingBottom) || 0);
79
+ const availW = Math.max(0, stage.clientWidth - padX);
80
+ const availH = Math.max(0, stage.clientHeight - padY);
81
+ const next = Math.min(1, availW / MOBILE_WIDTH, availH / MOBILE_HEIGHT);
82
+ setMobileScale(Number.isFinite(next) && next > 0 ? next : 1);
83
+ };
84
+ update();
85
+ const observer = new ResizeObserver(update);
86
+ observer.observe(stage);
87
+ return () => observer.disconnect();
88
+ }, [isMobile]);
89
+ useEffect(() => {
90
+ if (!frameReady)
91
+ return;
92
+ // After the iframe box size changes, nudge hosts that key off resize.
93
+ const timer = window.setTimeout(notifyHostResize, 50);
94
+ return () => window.clearTimeout(timer);
95
+ }, [viewport, mobileScale, frameReady, notifyHostResize]);
96
+ return (_jsxs("div", { className: "vertebr-page-preview", children: [_jsxs("div", { className: "vertebr-page-preview-meta", children: [_jsxs("p", { children: [_jsx("span", { children: "Preview" }), _jsx("strong", { children: previewPath })] }), _jsxs("p", { children: [_jsx("span", { children: "Status" }), _jsx("strong", { children: frameError
97
+ ? "Unavailable"
98
+ : frameReady
99
+ ? "Live"
100
+ : "Loading…" })] }), isMobile ? (_jsxs("p", { children: [_jsx("span", { children: "Viewport" }), _jsxs("strong", { children: [MOBILE_WIDTH, "\u00D7", MOBILE_HEIGHT] })] })) : null, _jsx("div", { className: "vertebr-page-preview-viewports", role: "group", "aria-label": "Preview viewport", children: VIEWPORTS.map((item) => (_jsx("button", { type: "button", "data-active": viewport === item.id ? "true" : undefined, onClick: () => setViewport(item.id), children: item.label }, item.id))) })] }), _jsx("div", { ref: stageRef, className: "vertebr-page-preview-stage", "data-viewport": viewport, children: _jsx("div", { className: "vertebr-page-preview-shell", style: isMobile
101
+ ? {
102
+ width: MOBILE_WIDTH * mobileScale,
103
+ height: MOBILE_HEIGHT * mobileScale,
104
+ }
105
+ : undefined, children: _jsx("div", { className: "vertebr-page-preview-frame", style: isMobile
106
+ ? {
107
+ width: MOBILE_WIDTH,
108
+ height: MOBILE_HEIGHT,
109
+ transform: `scale(${mobileScale})`,
110
+ transformOrigin: "top left",
111
+ }
112
+ : undefined, children: frameError ? (_jsxs("div", { className: "vertebr-page-preview-empty", children: ["Could not load host preview at ", previewPath, "."] })) : (_jsx("iframe", { ref: iframeRef, title: `Preview ${controlId}`, src: src, width: isMobile ? MOBILE_WIDTH : undefined, height: isMobile ? MOBILE_HEIGHT : undefined, style: isMobile
113
+ ? { width: MOBILE_WIDTH, height: MOBILE_HEIGHT }
114
+ : undefined, onLoad: () => {
115
+ // Ready message is preferred; onLoad covers hosts that
116
+ // already mounted before the listener attached.
117
+ setFrameReady(true);
118
+ pushDraft();
119
+ notifyHostResize();
120
+ }, onError: () => setFrameError(true) })) }) }) })] }));
121
+ }
@@ -1,4 +1,4 @@
1
1
  import type { VertebrAdminProps } from "./types";
2
2
  export type { VertebrAdminProps } from "./types";
3
- export declare function VertebrAdmin({ apiBase, title, envStorageKey, tokenStorageKey, themeStorageKey, metricsRangeStorageKey, getAccessToken, authUserLabel, onSignOut, }: VertebrAdminProps): import("react").JSX.Element;
3
+ export declare function VertebrAdmin({ apiBase, basePath: basePathProp, title, envStorageKey, tokenStorageKey, themeStorageKey, metricsRangeStorageKey, getAccessToken, authUserLabel, onSignOut, }: VertebrAdminProps): import("react").JSX.Element;
4
4
  //# sourceMappingURL=VertebrAdmin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../../../src/components/admin/VertebrAdmin.tsx"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,wBAAgB,YAAY,CAAC,EAC3B,OAAsB,EACtB,KAAuB,EACvB,aAA6B,EAC7B,eAAiC,EACjC,eAAiC,EACjC,sBAAgD,EAChD,cAAc,EACd,aAAa,EACb,SAAS,GACV,EAAE,iBAAiB,+BA6bnB"}
1
+ {"version":3,"file":"VertebrAdmin.d.ts","sourceRoot":"","sources":["../../../src/components/admin/VertebrAdmin.tsx"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,wBAAgB,YAAY,CAAC,EAC3B,OAAsB,EACtB,QAAQ,EAAE,YAAY,EACtB,KAAuB,EACvB,aAA6B,EAC7B,eAAiC,EACjC,eAAiC,EACjC,sBAAgD,EAChD,cAAc,EACd,aAAa,EACb,SAAS,GACV,EAAE,iBAAiB,+BA+iBnB"}
@@ -1,14 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { createVertebrClient, } from "@vertebr/client";
4
4
  import { useVertebrTheme } from "../../hooks/useVertebrTheme";
5
5
  import { parseMetricsRangeId, } from "../../lib/metricsRanges";
6
6
  import { AdminContent } from "./AdminContent";
7
7
  import { AdminHeader } from "./AdminHeader";
8
8
  import { AdminSidebar } from "./AdminSidebar";
9
+ import { adminBaseFromApiBase, coerceSelection, defaultSelection, hrefForSelection, selectionFromPath, selectionsEqual, } from "./routes";
9
10
  import { sidebarGroups } from "./sidebar";
10
11
  import { clearStored, readStored, writeStored } from "./storage";
11
- export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane", envStorageKey = "vertebr:env", tokenStorageKey = "vertebr:token", themeStorageKey = "vertebr:theme", metricsRangeStorageKey = "vertebr:metrics-range", getAccessToken, authUserLabel, onSignOut, }) {
12
+ export function VertebrAdmin({ apiBase = "/admin/api", basePath: basePathProp, title = "Control plane", envStorageKey = "vertebr:env", tokenStorageKey = "vertebr:token", themeStorageKey = "vertebr:theme", metricsRangeStorageKey = "vertebr:metrics-range", getAccessToken, authUserLabel, onSignOut, }) {
13
+ const basePath = basePathProp ?? adminBaseFromApiBase(apiBase);
12
14
  const { theme, setTheme } = useVertebrTheme(themeStorageKey);
13
15
  const [environment, setEnvironment] = useState(() => readStored(envStorageKey) ?? "");
14
16
  const [token, setToken] = useState(() => readStored(tokenStorageKey) ?? "");
@@ -21,7 +23,13 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
21
23
  }), [apiBase, getEnvironment, getAccessToken, token]);
22
24
  const [bootstrap, setBootstrap] = useState(null);
23
25
  const [catalog, setCatalog] = useState(null);
24
- const [selection, setSelection] = useState({ kind: "overview" });
26
+ const [selection, setSelection] = useState(() => {
27
+ if (typeof window === "undefined")
28
+ return { kind: "overview" };
29
+ return (selectionFromPath(window.location.pathname, basePath, null) ?? {
30
+ kind: "overview",
31
+ });
32
+ });
25
33
  const [resourceData, setResourceData] = useState(null);
26
34
  const [configData, setConfigData] = useState(null);
27
35
  const [metricsData, setMetricsData] = useState(null);
@@ -29,10 +37,23 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
29
37
  const [metricsRange, setMetricsRange] = useState(() => parseMetricsRangeId(readStored(metricsRangeStorageKey)));
30
38
  const [galleryData, setGalleryData] = useState(null);
31
39
  const [galleryLoading, setGalleryLoading] = useState(false);
40
+ const [fontLibraryData, setFontLibraryData] = useState(null);
41
+ const [fontLibraryLoading, setFontLibraryLoading] = useState(false);
32
42
  const [emailsData, setEmailsData] = useState(null);
33
43
  const [emailsLoading, setEmailsLoading] = useState(false);
34
44
  const [error, setError] = useState(null);
35
45
  const [loading, setLoading] = useState(true);
46
+ /** Skip pushState when selection was applied from the URL (catalog/popstate). */
47
+ const skipNextUrlPush = useRef(true);
48
+ const syncUrl = useCallback((next, catalogSnapshot, replace = false) => {
49
+ if (typeof window === "undefined" || !next)
50
+ return;
51
+ const href = hrefForSelection(basePath, next, catalogSnapshot);
52
+ if (window.location.pathname === href)
53
+ return;
54
+ const method = replace ? "replaceState" : "pushState";
55
+ window.history[method](null, "", href);
56
+ }, [basePath]);
36
57
  useEffect(() => {
37
58
  let cancelled = false;
38
59
  (async () => {
@@ -71,35 +92,11 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
71
92
  if (cancelled)
72
93
  return;
73
94
  setCatalog(next);
74
- setSelection((current) => {
75
- if (current?.kind === "overview")
76
- return current;
77
- if (current?.kind === "gallery" && next.gallery)
78
- return current;
79
- if (current?.kind === "emails" && next.emails)
80
- return current;
81
- if (current?.kind === "resource") {
82
- if (next.resources.some((r) => r.id === current.id))
83
- return current;
84
- }
85
- if (current?.kind === "config") {
86
- if (next.configs.some((c) => c.id === current.id))
87
- return current;
88
- }
89
- if (next.metrics)
90
- return { kind: "overview" };
91
- const firstResource = next.resources[0];
92
- if (firstResource)
93
- return { kind: "resource", id: firstResource.id };
94
- if (next.gallery)
95
- return { kind: "gallery" };
96
- if (next.emails)
97
- return { kind: "emails", tab: "compose" };
98
- const firstConfig = next.configs[0];
99
- if (firstConfig)
100
- return { kind: "config", id: firstConfig.id };
101
- return { kind: "overview" };
102
- });
95
+ const fromUrl = selectionFromPath(window.location.pathname, basePath, next);
96
+ const resolved = coerceSelection(fromUrl ?? defaultSelection(next), next);
97
+ skipNextUrlPush.current = true;
98
+ setSelection(resolved);
99
+ syncUrl(resolved, next, true);
103
100
  }
104
101
  catch (err) {
105
102
  if (!cancelled) {
@@ -115,7 +112,28 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
115
112
  return () => {
116
113
  cancelled = true;
117
114
  };
118
- }, [client, bootstrap, environment]);
115
+ }, [client, bootstrap, environment, basePath, syncUrl]);
116
+ useEffect(() => {
117
+ if (!catalog || !selection)
118
+ return;
119
+ if (skipNextUrlPush.current) {
120
+ skipNextUrlPush.current = false;
121
+ return;
122
+ }
123
+ syncUrl(selection, catalog, false);
124
+ }, [selection, catalog, syncUrl]);
125
+ useEffect(() => {
126
+ const onPopState = () => {
127
+ if (!catalog)
128
+ return;
129
+ const fromUrl = selectionFromPath(window.location.pathname, basePath, catalog);
130
+ const resolved = coerceSelection(fromUrl ?? defaultSelection(catalog), catalog);
131
+ skipNextUrlPush.current = true;
132
+ setSelection((current) => selectionsEqual(current, resolved) ? current : resolved);
133
+ };
134
+ window.addEventListener("popstate", onPopState);
135
+ return () => window.removeEventListener("popstate", onPopState);
136
+ }, [catalog, basePath]);
119
137
  useEffect(() => {
120
138
  if (!selection || !catalog)
121
139
  return;
@@ -131,6 +149,7 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
131
149
  setResourceData(null);
132
150
  setConfigData(null);
133
151
  setGalleryData(null);
152
+ setFontLibraryData(null);
134
153
  setEmailsData(null);
135
154
  }
136
155
  }
@@ -142,6 +161,19 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
142
161
  setResourceData(null);
143
162
  setConfigData(null);
144
163
  setMetricsData(null);
164
+ setFontLibraryData(null);
165
+ setEmailsData(null);
166
+ }
167
+ }
168
+ else if (selection.kind === "fontLibrary") {
169
+ setFontLibraryLoading(true);
170
+ const data = await client.getFonts();
171
+ if (!cancelled) {
172
+ setFontLibraryData(data);
173
+ setResourceData(null);
174
+ setConfigData(null);
175
+ setMetricsData(null);
176
+ setGalleryData(null);
145
177
  setEmailsData(null);
146
178
  }
147
179
  }
@@ -154,6 +186,7 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
154
186
  setConfigData(null);
155
187
  setMetricsData(null);
156
188
  setGalleryData(null);
189
+ setFontLibraryData(null);
157
190
  }
158
191
  }
159
192
  else if (selection.kind === "resource") {
@@ -163,6 +196,7 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
163
196
  setConfigData(null);
164
197
  setMetricsData(null);
165
198
  setGalleryData(null);
199
+ setFontLibraryData(null);
166
200
  setEmailsData(null);
167
201
  }
168
202
  }
@@ -174,6 +208,18 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
174
208
  setMetricsData(null);
175
209
  setGalleryData(null);
176
210
  setEmailsData(null);
211
+ if (catalog.fontLibrary) {
212
+ setFontLibraryLoading(true);
213
+ try {
214
+ const fonts = await client.getFonts();
215
+ if (!cancelled)
216
+ setFontLibraryData(fonts);
217
+ }
218
+ finally {
219
+ if (!cancelled)
220
+ setFontLibraryLoading(false);
221
+ }
222
+ }
177
223
  }
178
224
  }
179
225
  }
@@ -186,6 +232,7 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
186
232
  if (!cancelled) {
187
233
  setMetricsLoading(false);
188
234
  setGalleryLoading(false);
235
+ setFontLibraryLoading(false);
189
236
  setEmailsLoading(false);
190
237
  }
191
238
  }
@@ -224,6 +271,20 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
224
271
  setGalleryLoading(false);
225
272
  }
226
273
  }, [client]);
274
+ const refreshFonts = useCallback(async () => {
275
+ setFontLibraryLoading(true);
276
+ try {
277
+ const data = await client.getFonts({ fresh: true });
278
+ setFontLibraryData(data);
279
+ }
280
+ finally {
281
+ setFontLibraryLoading(false);
282
+ }
283
+ }, [client]);
284
+ const listFonts = useCallback(async () => {
285
+ const data = await client.getFonts();
286
+ return data.items;
287
+ }, [client]);
227
288
  const refreshEmails = useCallback(async () => {
228
289
  setEmailsLoading(true);
229
290
  try {
@@ -256,6 +317,14 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
256
317
  onUploadGallery: uploadGallery,
257
318
  }
258
319
  : {};
320
+ const fontLibraryPicker = catalog?.fontLibrary
321
+ ? {
322
+ fonts: fontLibraryData?.items ?? [],
323
+ fontsLoading: fontLibraryLoading,
324
+ onListFonts: listFonts,
325
+ onRefreshFonts: refreshFonts,
326
+ }
327
+ : {};
259
328
  const envList = catalog?.environments ?? bootstrap?.environments ?? [];
260
329
  const activeEnv = envList.find((env) => env.id === environment);
261
330
  const authRequired = Boolean(activeEnv?.auth?.required);
@@ -263,6 +332,7 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
263
332
  ? catalog?.configs.find((c) => c.id === selection.id)
264
333
  : undefined;
265
334
  const isPageSelection = activeConfig?.section === "page";
335
+ const hasPagePreview = Boolean(activeConfig?.previewPath);
266
336
  const switchEnvironment = (nextEnv) => {
267
337
  if (nextEnv === environment)
268
338
  return;
@@ -277,6 +347,7 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
277
347
  setResourceData(null);
278
348
  setConfigData(null);
279
349
  setGalleryData(null);
350
+ setFontLibraryData(null);
280
351
  };
281
352
  const saveToken = () => {
282
353
  const next = tokenDraft.trim();
@@ -322,6 +393,15 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
322
393
  onSelect: () => setSelection({ kind: "gallery" }),
323
394
  });
324
395
  }
396
+ if (catalog.fontLibrary) {
397
+ entries.push({
398
+ key: "fonts",
399
+ group: catalog.fontLibrary.group ?? "Resources",
400
+ label: catalog.fontLibrary.label,
401
+ active: selection?.kind === "fontLibrary",
402
+ onSelect: () => setSelection({ kind: "fontLibrary" }),
403
+ });
404
+ }
325
405
  if (catalog.emails) {
326
406
  const emailsTab = selection?.kind === "emails" ? (selection.tab ?? "compose") : null;
327
407
  entries.push({
@@ -374,12 +454,14 @@ export function VertebrAdmin({ apiBase = "/admin/api", title = "Control plane",
374
454
  ? (catalog?.metrics?.label ?? "Overview")
375
455
  : selection?.kind === "gallery"
376
456
  ? (catalog?.gallery?.label ?? "Gallery")
377
- : selection?.kind === "emails"
378
- ? (catalog?.emails?.label ?? "Emails")
379
- : selection?.kind === "resource"
380
- ? catalog?.resources.find((r) => r.id === selection.id)?.label
381
- : selection?.kind === "config"
382
- ? catalog?.configs.find((c) => c.id === selection.id)?.label
383
- : null;
384
- return (_jsxs("div", { className: "vertebr-root", "data-env": environment, children: [_jsx(AdminSidebar, { brand: bootstrap?.brand ?? catalog?.brand ?? "Vertebr", title: title, envList: envList, environment: environment, onEnvironmentChange: switchEnvironment, navGroups: navGroups, getAccessToken: getAccessToken, authUserLabel: authUserLabel, onSignOut: onSignOut, authRequired: authRequired, activeEnv: activeEnv, token: token, tokenDraft: tokenDraft, onTokenDraftChange: setTokenDraft, onSaveToken: saveToken, onClearToken: clearToken, theme: theme, onThemeChange: setTheme }), _jsxs("main", { className: "vertebr-main", children: [_jsx(AdminHeader, { activeLabel: activeLabel ?? (catalog ? "Dashboard" : "Sign in"), activeEnv: activeEnv, catalogLoaded: Boolean(catalog), authRequired: authRequired, selection: selection, isPageSelection: isPageSelection, error: error }), _jsx(AdminContent, { client: client, catalog: catalog, selection: selection, authRequired: authRequired, loading: loading, isPageSelection: isPageSelection, metricsData: metricsData, metricsLoading: metricsLoading, metricsRange: metricsRange, onMetricsRangeChange: changeMetricsRange, onRefreshMetrics: refreshMetrics, galleryData: galleryData, galleryLoading: galleryLoading, onRefreshGallery: refreshGallery, emailsData: emailsData, emailsLoading: emailsLoading, onRefreshEmails: refreshEmails, resourceData: resourceData, onRefreshResource: refreshResource, configData: configData, onConfigSaved: setConfigData, galleryPicker: galleryPicker })] })] }));
457
+ : selection?.kind === "fontLibrary"
458
+ ? (catalog?.fontLibrary?.label ?? "Font Library")
459
+ : selection?.kind === "emails"
460
+ ? (catalog?.emails?.label ?? "Emails")
461
+ : selection?.kind === "resource"
462
+ ? catalog?.resources.find((r) => r.id === selection.id)?.label
463
+ : selection?.kind === "config"
464
+ ? catalog?.configs.find((c) => c.id === selection.id)?.label
465
+ : null;
466
+ return (_jsxs("div", { className: "vertebr-root", "data-env": environment, children: [_jsx(AdminSidebar, { brand: bootstrap?.brand ?? catalog?.brand ?? "Vertebr", title: title, envList: envList, environment: environment, onEnvironmentChange: switchEnvironment, navGroups: navGroups, getAccessToken: getAccessToken, authUserLabel: authUserLabel, onSignOut: onSignOut, authRequired: authRequired, activeEnv: activeEnv, token: token, tokenDraft: tokenDraft, onTokenDraftChange: setTokenDraft, onSaveToken: saveToken, onClearToken: clearToken, theme: theme, onThemeChange: setTheme }), _jsxs("main", { className: "vertebr-main", children: [_jsx(AdminHeader, { activeLabel: activeLabel ?? (catalog ? "Dashboard" : "Sign in"), activeEnv: activeEnv, catalogLoaded: Boolean(catalog), authRequired: authRequired, selection: selection, isPageSelection: isPageSelection, hasPagePreview: hasPagePreview, error: error }), _jsx(AdminContent, { client: client, catalog: catalog, selection: selection, authRequired: authRequired, loading: loading, isPageSelection: isPageSelection, metricsData: metricsData, metricsLoading: metricsLoading, metricsRange: metricsRange, onMetricsRangeChange: changeMetricsRange, onRefreshMetrics: refreshMetrics, galleryData: galleryData, galleryLoading: galleryLoading, onRefreshGallery: refreshGallery, fontLibraryData: fontLibraryData, fontLibraryLoading: fontLibraryLoading, onRefreshFonts: refreshFonts, emailsData: emailsData, emailsLoading: emailsLoading, onRefreshEmails: refreshEmails, resourceData: resourceData, onRefreshResource: refreshResource, configData: configData, onConfigSaved: setConfigData, galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker })] })] }));
385
467
  }