@vertebr/react 0.10.0 → 0.12.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 (52) hide show
  1. package/dist/components/admin/AddonEditor.d.ts.map +1 -1
  2. package/dist/components/admin/AddonEditor.js +10 -8
  3. package/dist/components/admin/AdminContent.d.ts.map +1 -1
  4. package/dist/components/admin/AdminContent.js +62 -17
  5. package/dist/components/admin/AdminSaveBar.d.ts.map +1 -1
  6. package/dist/components/admin/AdminSaveBar.js +6 -1
  7. package/dist/components/admin/AdminSyncPage.d.ts.map +1 -1
  8. package/dist/components/admin/AdminSyncPage.js +50 -6
  9. package/dist/components/admin/PagePreviewPane.d.ts +3 -1
  10. package/dist/components/admin/PagePreviewPane.d.ts.map +1 -1
  11. package/dist/components/admin/PagePreviewPane.js +41 -9
  12. package/dist/components/admin/SyncPreviewFrame.d.ts +22 -0
  13. package/dist/components/admin/SyncPreviewFrame.d.ts.map +1 -0
  14. package/dist/components/admin/SyncPreviewFrame.js +135 -0
  15. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  16. package/dist/components/admin/VertebrAdmin.js +6 -4
  17. package/dist/components/config/DatetimeInput.d.ts +15 -0
  18. package/dist/components/config/DatetimeInput.d.ts.map +1 -0
  19. package/dist/components/config/DatetimeInput.js +287 -0
  20. package/dist/components/config/EnumInput.d.ts.map +1 -1
  21. package/dist/components/config/EnumInput.js +4 -4
  22. package/dist/components/config/EnumListInput.d.ts.map +1 -1
  23. package/dist/components/config/EnumListInput.js +13 -3
  24. package/dist/components/config/ImagePickerModal.d.ts +9 -1
  25. package/dist/components/config/ImagePickerModal.d.ts.map +1 -1
  26. package/dist/components/config/ImagePickerModal.js +14 -7
  27. package/dist/components/config/RevisionHistory.d.ts +4 -0
  28. package/dist/components/config/RevisionHistory.d.ts.map +1 -1
  29. package/dist/components/config/RevisionHistory.js +63 -8
  30. package/dist/components/config/form/ConfigField.d.ts.map +1 -1
  31. package/dist/components/config/form/ConfigField.js +41 -16
  32. package/dist/components/config/form/ConfigForm.d.ts +1 -1
  33. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  34. package/dist/components/config/form/ConfigForm.js +54 -9
  35. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
  36. package/dist/components/config/form/RecordConfigForm.js +24 -10
  37. package/dist/components/config/form/types.d.ts +9 -0
  38. package/dist/components/config/form/types.d.ts.map +1 -1
  39. package/dist/components/fonts/FontLibraryBoard.d.ts.map +1 -1
  40. package/dist/components/fonts/FontLibraryBoard.js +1 -1
  41. package/dist/components/resources/ImageUploadControl.d.ts +3 -1
  42. package/dist/components/resources/ImageUploadControl.d.ts.map +1 -1
  43. package/dist/components/resources/ImageUploadControl.js +5 -3
  44. package/dist/hooks/useSaveBar.d.ts +6 -0
  45. package/dist/hooks/useSaveBar.d.ts.map +1 -1
  46. package/dist/styles/form.css +465 -17
  47. package/dist/styles/modal.css +2 -1
  48. package/dist/styles/page-preview.css +84 -0
  49. package/dist/styles/select.css +23 -0
  50. package/dist/styles/sync.css +388 -4
  51. package/dist/themes/light.css +27 -0
  52. package/package.json +3 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AddonEditor.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AddonEditor.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EACf,cAAc,EACd,aAAa,EACd,MAAM,iBAAiB,CAAC;AAKzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,aAAa,EAAE,iBAAiB,CAAC,eAAe,CAAC,CAAC;IAClD,iBAAiB,EAAE,iBAAiB,CAAC,mBAAmB,CAAC,CAAC;CAC3D,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,KAAK,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,iBAAiB,GAClB,EAAE,gBAAgB,+BAyFlB"}
1
+ {"version":3,"file":"AddonEditor.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AddonEditor.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EACf,cAAc,EACd,aAAa,EACd,MAAM,iBAAiB,CAAC;AAMzB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;IACzB,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,aAAa,EAAE,iBAAiB,CAAC,eAAe,CAAC,CAAC;IAClD,iBAAiB,EAAE,iBAAiB,CAAC,mBAAmB,CAAC,CAAC;CAC3D,CAAC;AAEF,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,OAAO,EACP,KAAK,EACL,UAAU,EACV,aAAa,EACb,aAAa,EACb,iBAAiB,GAClB,EAAE,gBAAgB,+BAiGlB"}
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useMemo, useState } from "react";
3
+ import { resolvePreviewPath } from "@vertebr/client";
3
4
  import { ConfigForm } from "../config/ConfigForm";
4
5
  import { hasCap } from "../../lib/capabilities";
5
6
  import { addonCategoryLabel } from "./addonLabels";
@@ -7,18 +8,19 @@ import { PagePreviewPane } from "./PagePreviewPane";
7
8
  export function AddonEditor({ client, catalog, addon, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }) {
8
9
  const [draft, setDraft] = useState(configData.value);
9
10
  const onChange = useCallback((value) => {
10
- setDraft(value);
11
- }, []);
12
- const enabled = Boolean(draft?.enabled ?? configData.value.enabled);
13
- const previewPath = addon.previewPath;
11
+ // Merge — react-hook-form watch can omit unchanged keys.
12
+ setDraft((prev) => ({ ...(prev ?? configData.value), ...value }));
13
+ }, [configData.value]);
14
+ const draftValue = draft ?? configData.value;
15
+ const enabled = Boolean(draftValue.enabled);
16
+ const previewPath = resolvePreviewPath(addon.previewPath, addon.previewPathRules, draftValue);
14
17
  const showPreview = Boolean(previewPath);
15
18
  const canWrite = hasCap(catalog, "addon.write");
16
19
  const previewDraft = useMemo(() => {
17
- const base = draft ?? configData.value;
18
20
  if (!showPreview)
19
- return base;
20
- return { ...base, enabled: true };
21
- }, [configData.value, draft, showPreview]);
21
+ return draftValue;
22
+ return { ...draftValue, enabled: true };
23
+ }, [draftValue, showPreview]);
22
24
  const form = (_jsx(ConfigForm, { schema: configData.schema, value: configData.value, client: client, canWrite: canWrite, onChange: showPreview ? onChange : undefined, onSave: async (next) => {
23
25
  const saved = await client.setConfig(configData.id, next);
24
26
  onConfigSaved(saved);
@@ -1 +1 @@
1
- {"version":3,"file":"AdminContent.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminContent.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AdminContent.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminContent.tsx"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useState } from "react";
2
+ import { useCallback, useEffect, useState } from "react";
3
3
  import { ConfigForm } from "../config/ConfigForm";
4
4
  import { RevisionHistory } from "../config/RevisionHistory";
5
5
  import { EmailsBoard } from "../emails/EmailsBoard";
@@ -13,21 +13,68 @@ import { AddonOverview } from "./AddonOverview";
13
13
  import { AuditBoard } from "./AuditBoard";
14
14
  import { PagePreviewPane } from "./PagePreviewPane";
15
15
  import { AdminSyncPage } from "./AdminSyncPage";
16
- function ConfigEditor({ client, catalog, configData, isPageSelection, previewPath, onConfigSaved, onRefreshConfig, galleryPicker, fontLibraryPicker, }) {
16
+ function ConfigEditor({ client, catalog, configData, isPageSelection, previewPath, relatedAddons = [], onConfigSaved, onRefreshConfig, onSelectAddon, galleryPicker, fontLibraryPicker, }) {
17
17
  const [draft, setDraft] = useState(configData.value);
18
+ const [relatedDrafts, setRelatedDrafts] = useState({});
18
19
  const showPreview = Boolean(previewPath);
19
20
  const canWrite = hasCap(catalog, "config.write");
21
+ const canPublish = hasCap(catalog, "config.publish");
20
22
  const showRevisions = Boolean(catalog.revisionsEnabled) || Boolean(configData.revisionsEnabled);
23
+ const relatedKey = relatedAddons.map((item) => item.id).join(",");
24
+ useEffect(() => {
25
+ setDraft(configData.value);
26
+ }, [configData.id, configData.activeRevisionId, configData.value]);
27
+ useEffect(() => {
28
+ if (!relatedAddons.length) {
29
+ setRelatedDrafts({});
30
+ return;
31
+ }
32
+ let cancelled = false;
33
+ void (async () => {
34
+ const entries = await Promise.all(relatedAddons.map(async (addon) => {
35
+ try {
36
+ const config = await client.getConfig(addon.id);
37
+ const value = { ...config.value };
38
+ // Coming soon page owns site-launch countdown placement.
39
+ if (configData.id === "comingSoon" && addon.id === "countdown") {
40
+ value.target = "comingSoon";
41
+ }
42
+ if (configData.id === "product" && addon.id === "countdown") {
43
+ value.target = "product";
44
+ }
45
+ return [addon.id, value];
46
+ }
47
+ catch {
48
+ return [addon.id, { enabled: false }];
49
+ }
50
+ }));
51
+ if (cancelled)
52
+ return;
53
+ setRelatedDrafts(Object.fromEntries(entries));
54
+ })();
55
+ return () => {
56
+ cancelled = true;
57
+ };
58
+ }, [client, configData.id, relatedKey, relatedAddons]);
21
59
  const onChange = useCallback((value) => {
22
60
  setDraft(value);
23
61
  }, []);
24
- const form = (_jsx(ConfigForm, { schema: configData.schema, value: configData.value, client: client, canWrite: canWrite, onChange: showPreview ? onChange : undefined, onSave: async (next) => {
62
+ const form = (_jsx(ConfigForm, { schema: configData.schema, value: configData.value, client: client, canWrite: canWrite, revisionsEnabled: showRevisions, canPublish: canPublish && Boolean(configData.draftRevisionId), onPublish: canPublish && configData.draftRevisionId
63
+ ? async () => {
64
+ const published = await client.publishRevision(configData.id, configData.draftRevisionId);
65
+ onConfigSaved(published);
66
+ await onRefreshConfig?.();
67
+ }
68
+ : undefined, onChange: showPreview ? onChange : undefined, onSave: async (next) => {
25
69
  const saved = await client.setConfig(configData.id, next);
26
70
  onConfigSaved(saved);
27
71
  }, ...galleryPicker, ...fontLibraryPicker }));
28
- 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), showRevisions ? (_jsx(RevisionHistory, { client: client, configId: configData.id, onPublished: async () => {
29
- await onRefreshConfig?.();
30
- } })) : null] }));
72
+ return (_jsxs("div", { className: "vertebr-config-editor", children: [showPreview || isPageSelection || showRevisions ? (_jsxs("div", { className: "vertebr-panel-head", children: [_jsx("h2", { children: isPageSelection ? "Page content" : configData.label }), _jsxs("div", { className: "vertebr-panel-head-actions", children: [showPreview || isPageSelection ? (_jsxs("span", { className: "vertebr-count", children: [Object.keys(configData.schema).length, " fields"] })) : null, showRevisions ? (_jsx(RevisionHistory, { client: client, configId: configData.id, onPublished: async () => {
73
+ await onRefreshConfig?.();
74
+ } })) : null] })] })) : null, relatedAddons.length ? (_jsxs("div", { className: "vertebr-page-related-addons", "aria-label": "Linked add-ons", children: [_jsx("span", { className: "vertebr-page-related-addons-label", children: "Linked add-ons" }), _jsx("div", { className: "vertebr-page-related-addons-list", children: relatedAddons.map((addon) => {
75
+ const enabled = relatedDrafts[addon.id]?.enabled === true;
76
+ return (_jsxs("button", { type: "button", className: "vertebr-page-related-addon", "data-enabled": enabled ? "true" : "false", onClick: () => onSelectAddon?.(addon.id), children: [_jsx("span", { children: addon.label }), _jsx("em", { children: enabled ? "on" : "off" })] }, addon.id));
77
+ }) })] })) : 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, relatedDrafts: Object.keys(relatedDrafts).length ? relatedDrafts : undefined })] })) : (form)] }));
31
78
  }
32
79
  export function AdminContent({ client, catalog, selection, onSelectionChange, authRequired, loading, isPageSelection, metricsData, metricsLoading, metricsRange, onMetricsRangeChange, onRefreshMetrics, galleryData, galleryLoading, onRefreshGallery, fontLibraryData, fontLibraryLoading, onRefreshFonts, emailsData, emailsLoading, onRefreshEmails, resourceData, resourceSearch, onResourceSearchChange, onRefreshResource, onRefreshConfig, configData, onConfigSaved, galleryPicker, fontLibraryPicker, }) {
33
80
  if (!catalog) {
@@ -38,11 +85,8 @@ export function AdminContent({ client, catalog, selection, onSelectionChange, au
38
85
  : "Unable to load catalog." }) }) }));
39
86
  }
40
87
  return (_jsx("div", { className: "vertebr-content", children: _jsxs("section", { className: "vertebr-panel", children: [selection?.kind === "overview" ? (_jsx(MetricsBoard, { label: metricsData?.label ?? catalog.metrics?.label, windowLabel: metricsData?.windowLabel, compareLabel: metricsData?.compareLabel, metrics: metricsData?.metrics ?? [], charts: metricsData?.charts ?? [], range: metricsRange, onRangeChange: onMetricsRangeChange, loading: metricsLoading, onRefresh: () => void onRefreshMetrics() })) : null, selection?.kind === "audit" ? (_jsx(AuditBoard, { client: client, label: catalog.audit?.label })) : null, selection?.kind === "sync" ? (_jsx(AdminSyncPage, { client: client, catalog: catalog })) : 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 onRefreshGallery(), onUpload: async (files) => {
41
- if (files.length === 1 && files[0]) {
42
- await client.uploadGalleryFile(files[0]);
43
- }
44
- else {
45
- await client.uploadGalleryFiles(files);
88
+ for (const file of files) {
89
+ await client.uploadGalleryFile(file);
46
90
  }
47
91
  await onRefreshGallery();
48
92
  }, onDelete: galleryData.canDelete
@@ -145,15 +189,16 @@ export function AdminContent({ client, catalog, selection, onSelectionChange, au
145
189
  }
146
190
  : undefined, onUpload: resourceData.uploads
147
191
  ? async (field, files) => {
148
- if (files.length === 1 && files[0]) {
149
- const result = await client.uploadResourceFile(resourceData.id, field, files[0]);
150
- return [{ url: result.url, name: result.name }];
192
+ const uploaded = [];
193
+ for (const file of files) {
194
+ const result = await client.uploadResourceFile(resourceData.id, field, file);
195
+ uploaded.push({ url: result.url, name: result.name });
151
196
  }
152
- const result = await client.uploadResourceFiles(resourceData.id, field, files);
153
- return result.files;
197
+ return uploaded;
154
198
  }
155
199
  : undefined, ...galleryPicker })) : null, selection?.kind === "addons" ? (_jsx(AddonOverview, { client: client, catalog: catalog, onSelectAddon: (id) => onSelectionChange?.({ kind: "addon", id }) })) : null, selection?.kind === "config" && configData ? (_jsx(ConfigEditor, { client: client, catalog: catalog, configData: configData, isPageSelection: isPageSelection, previewPath: catalog.configs.find((item) => item.id === configData.id)
156
- ?.previewPath, onConfigSaved: onConfigSaved, onRefreshConfig: onRefreshConfig, galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker }, configData.id)) : null, selection?.kind === "addon" && configData ? ((() => {
200
+ ?.previewPath, relatedAddons: catalog.configs.find((item) => item.id === configData.id)
201
+ ?.relatedAddons, onConfigSaved: onConfigSaved, onRefreshConfig: onRefreshConfig, onSelectAddon: (id) => onSelectionChange?.({ kind: "addon", id }), galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker }, configData.id)) : null, selection?.kind === "addon" && configData ? ((() => {
157
202
  const addon = catalog.addons?.find((item) => item.id === configData.id);
158
203
  if (!addon)
159
204
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"AdminSaveBar.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminSaveBar.tsx"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,uCAmC3B"}
1
+ {"version":3,"file":"AdminSaveBar.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminSaveBar.tsx"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,uCAqD3B"}
@@ -6,5 +6,10 @@ export function AdminSaveBar() {
6
6
  return null;
7
7
  return (_jsxs("div", { className: "vertebr-savebar", "data-tone": state.tone, children: [_jsx("div", { className: "vertebr-savebar-message", children: state.saving
8
8
  ? "Saving…"
9
- : state.message ?? (state.isDirty ? "Unsaved changes" : "Saved") }), _jsxs("div", { className: "vertebr-savebar-actions", children: [state.onDiscard && state.isDirty ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: state.saving, onClick: state.onDiscard, children: "Discard" })) : null, state.onSave ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: state.saving || !state.isDirty, onClick: () => void state.onSave?.(), children: "Save" })) : null] })] }));
9
+ : state.publishing
10
+ ? "Publishing…"
11
+ : (state.message ??
12
+ (state.isDirty ? "Unsaved changes" : "Saved")) }), _jsxs("div", { className: "vertebr-savebar-actions", children: [state.onDiscard && state.isDirty ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: state.saving || state.publishing, onClick: state.onDiscard, children: "Discard" })) : null, state.onSave ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: state.saving || state.publishing || !state.isDirty, onClick: () => void state.onSave?.(), children: state.saveLabel ?? "Save" })) : null, state.onPublish ? (_jsx("button", { type: "button", className: "vertebr-btn", disabled: state.saving || state.publishing || state.isDirty, title: state.isDirty
13
+ ? "Save draft before publishing"
14
+ : "Publish the current draft to the live storefront", onClick: () => void state.onPublish?.(), children: state.publishLabel ?? "Publish" })) : null] })] }));
10
15
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AdminSyncPage.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminSyncPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,eAAe,EAGf,aAAa,EACd,MAAM,iBAAiB,CAAC;AAOzB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;CAC1B,CAAC;AAiDF,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,+BA2VpE"}
1
+ {"version":3,"file":"AdminSyncPage.d.ts","sourceRoot":"","sources":["../../../src/components/admin/AdminSyncPage.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,eAAe,EAIf,aAAa,EACd,MAAM,iBAAiB,CAAC;AAQzB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;CAC1B,CAAC;AAuPF,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,kBAAkB,+BA0YpE"}
@@ -5,6 +5,7 @@ import { EnvSelect } from "../env/EnvSelect";
5
5
  import { useToast } from "../../hooks/useToast";
6
6
  import { Badge, Card } from "../ui/primitives";
7
7
  import { IconSync } from "../ui/icons";
8
+ import { SyncPreviewFrame } from "./SyncPreviewFrame";
8
9
  function envById(environments, id) {
9
10
  return environments.find((env) => env.id === id);
10
11
  }
@@ -46,6 +47,36 @@ function statusLabel(status) {
46
47
  return status;
47
48
  }
48
49
  }
50
+ function formatDiffValue(value) {
51
+ if (value === undefined || value === null)
52
+ return "—";
53
+ if (typeof value === "string")
54
+ return value.length ? value : '""';
55
+ if (typeof value === "number" || typeof value === "boolean") {
56
+ return String(value);
57
+ }
58
+ try {
59
+ return JSON.stringify(value, null, 2);
60
+ }
61
+ catch {
62
+ return String(value);
63
+ }
64
+ }
65
+ function SyncDiffModal({ inspect, sourceLabel, onClose, }) {
66
+ const changes = inspect.item.changes ?? [];
67
+ const preview = inspect.item.preview;
68
+ const hasVisual = Boolean(preview);
69
+ const [tab, setTab] = useState(hasVisual ? "visual" : "fields");
70
+ return (_jsx("div", { className: "vertebr-modal-backdrop", role: "presentation", onClick: onClose, children: _jsxs("div", { className: "vertebr-modal vertebr-sync-diff-modal", "data-has-visual": hasVisual ? "true" : undefined, role: "dialog", "aria-modal": "true", "aria-labelledby": "vertebr-sync-diff-title", onClick: (event) => event.stopPropagation(), children: [_jsxs("header", { className: "vertebr-modal-head vertebr-sync-diff-head", children: [_jsxs("div", { className: "vertebr-modal-heading", children: [_jsx("h2", { id: "vertebr-sync-diff-title", children: inspect.targetLabel }), _jsxs("p", { className: "vertebr-sync-diff-subtitle", children: [_jsxs("span", { children: [sourceLabel, " \u2192 ", inspect.destinationLabel] }), inspect.item.summary ? (_jsx("span", { className: "vertebr-sync-diff-meta", children: inspect.item.summary })) : null] })] }), _jsx("div", { className: "vertebr-modal-actions", children: _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", onClick: onClose, children: "Close" }) })] }), hasVisual ? (_jsxs("div", { className: "vertebr-sync-diff-tabs", role: "tablist", "aria-label": "Diff view", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": tab === "visual", "data-active": tab === "visual" ? "true" : undefined, onClick: () => setTab("visual"), children: "Visual" }), _jsx("button", { type: "button", role: "tab", "aria-selected": tab === "fields", "data-active": tab === "fields" ? "true" : undefined, onClick: () => setTab("fields"), children: "Fields" })] })) : null, _jsxs("div", { className: "vertebr-modal-body", children: [inspect.item.warnings?.length ? (_jsx("ul", { className: "vertebr-sync-diff-warnings", children: inspect.item.warnings.map((warning) => (_jsx("li", { children: warning }, warning))) })) : null, _jsxs("div", { className: "vertebr-sync-diff-panels", "data-tab": tab, children: [preview ? (_jsx("div", { className: "vertebr-sync-diff-panel", "data-panel": "visual", "data-active": tab === "visual" ? "true" : undefined, role: "tabpanel", "aria-hidden": tab !== "visual", children: _jsxs("div", { className: "vertebr-sync-diff-visual", children: [_jsx(SyncPreviewFrame, { label: `${inspect.destinationLabel} (now)`, previewPath: preview.previewPath, controlId: preview.controlId, draft: preview.destDraft, relatedDrafts: preview.destRelatedDrafts, addons: preview.addons?.map((addon) => ({
71
+ id: addon.id,
72
+ label: addon.label,
73
+ envEnabled: addon.destEnabled,
74
+ })) }), _jsx(SyncPreviewFrame, { label: `${sourceLabel} (after sync)`, previewPath: preview.previewPath, controlId: preview.controlId, draft: preview.sourceDraft, relatedDrafts: preview.sourceRelatedDrafts, addons: preview.addons?.map((addon) => ({
75
+ id: addon.id,
76
+ label: addon.label,
77
+ envEnabled: addon.sourceEnabled,
78
+ })) })] }) })) : null, _jsx("div", { className: "vertebr-sync-diff-panel", "data-panel": "fields", "data-active": tab === "fields" ? "true" : undefined, role: "tabpanel", "aria-hidden": tab !== "fields", children: changes.length === 0 ? (_jsx("p", { className: "vertebr-sync-diff-empty", children: "No field-level preview for this target. The summary still applies when you sync." })) : (_jsx("div", { className: "vertebr-sync-diff-table-wrap", children: _jsxs("table", { className: "vertebr-sync-diff-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Field" }), _jsxs("th", { scope: "col", children: [inspect.destinationLabel, " (now)"] }), _jsxs("th", { scope: "col", children: [sourceLabel, " (after sync)"] })] }) }), _jsx("tbody", { children: changes.map((change) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: _jsx("code", { children: change.path }) }), _jsx("td", { children: _jsx("pre", { children: formatDiffValue(change.destination) }) }), _jsx("td", { children: _jsx("pre", { children: formatDiffValue(change.source) }) })] }, change.path))) })] }) })) })] })] })] }) }));
79
+ }
49
80
  export function AdminSyncPage({ client, catalog }) {
50
81
  const sync = catalog.sync;
51
82
  const { push } = useToast();
@@ -63,24 +94,27 @@ export function AdminSyncPage({ client, catalog }) {
63
94
  const [planLoading, setPlanLoading] = useState(false);
64
95
  const [running, setRunning] = useState(false);
65
96
  const [error, setError] = useState(null);
97
+ const [inspect, setInspect] = useState(null);
66
98
  const fromEnvironment = envById(environments, fromEnv);
67
99
  const toEnvironments = useMemo(() => toEnv
68
100
  .map((id) => envById(environments, id))
69
101
  .filter((env) => Boolean(env)), [environments, toEnv]);
70
102
  const targetGroups = useMemo(() => groupTargets(catalog), [catalog]);
71
103
  const hasTargets = (sync?.targets.length ?? 0) > 0;
104
+ const allTargetIds = useMemo(() => (sync?.targets ?? []).map((target) => target.id), [sync?.targets]);
72
105
  const refreshPlan = useCallback(async () => {
73
- if (!hasTargets || toEnv.length === 0) {
106
+ if (!hasTargets || toEnv.length === 0 || allTargetIds.length === 0) {
74
107
  setPlan(null);
75
108
  return;
76
109
  }
77
110
  setPlanLoading(true);
78
111
  setError(null);
79
112
  try {
113
+ // Plan every target so Match/Changes + View diff work without selecting.
80
114
  const next = await client.getSyncPlan({
81
115
  from: fromEnv,
82
116
  to: toEnv,
83
- targets: selectedTargets,
117
+ targets: allTargetIds,
84
118
  });
85
119
  setPlan(next);
86
120
  }
@@ -91,7 +125,7 @@ export function AdminSyncPage({ client, catalog }) {
91
125
  finally {
92
126
  setPlanLoading(false);
93
127
  }
94
- }, [client, fromEnv, toEnv, selectedTargets, hasTargets]);
128
+ }, [client, fromEnv, toEnv, allTargetIds, hasTargets]);
95
129
  useEffect(() => {
96
130
  void refreshPlan();
97
131
  }, [refreshPlan]);
@@ -155,15 +189,25 @@ export function AdminSyncPage({ client, catalog }) {
155
189
  setToEnv((current) => current.filter((id) => id !== next));
156
190
  } })] }), _jsx("div", { className: "vertebr-sync-connector", "aria-hidden": "true", children: toEnvironments.length > 0 ? (toEnvironments.map((destination) => (_jsx("span", { className: "vertebr-sync-connector-arrow", children: "\u2192" }, destination.id)))) : (_jsx("span", { className: "vertebr-sync-connector-arrow", children: "\u2192" })) }), _jsxs("label", { className: "vertebr-sync-field", children: [_jsx("span", { className: "vertebr-sync-field-label", children: "To" }), _jsx(EnvMultiSelect, { environments: environments, value: toEnv, excludeId: fromEnv, onChange: setToEnv, placeholder: "Select one or more destinations" })] })] }), fromEnvironment && toEnvironments.length > 0 ? (_jsx("div", { className: "vertebr-sync-preview", "aria-label": "Sync direction", children: _jsxs("div", { className: "vertebr-sync-preview-flow", "data-dest-count": toEnvironments.length, children: [_jsxs("div", { className: "vertebr-sync-preview-col vertebr-sync-preview-col-from", children: [_jsx("span", { className: "vertebr-sync-preview-caption", children: "From" }), _jsx(EnvFlowBadge, { environment: fromEnvironment })] }), _jsx("div", { className: "vertebr-sync-preview-col vertebr-sync-preview-col-routes", children: toEnvironments.map((destination, index) => (_jsxs("div", { className: "vertebr-sync-preview-route", children: [_jsx("span", { className: "vertebr-sync-preview-arrow", "aria-hidden": "true", children: "\u2192" }), _jsxs("div", { className: "vertebr-sync-preview-col vertebr-sync-preview-col-to", children: [index === 0 ? (_jsx("span", { className: "vertebr-sync-preview-caption", children: "To" })) : (_jsx("span", { className: "vertebr-sync-preview-caption", "aria-hidden": "true", children: "\u00A0" })), _jsx(EnvFlowBadge, { environment: destination })] })] }, destination.id))) })] }) })) : (_jsx("p", { className: "vertebr-sync-direction-hint", children: "Pick a source environment and one or more destinations to preview changes." }))] }) }), _jsxs(Card, { title: "What to sync", children: [_jsxs("div", { className: "vertebr-sync-targets-head", children: [_jsx("p", { className: "vertebr-sync-targets-copy", children: "Review what will change in each destination before syncing." }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet vertebr-sync-refresh", disabled: planLoading || toEnv.length === 0, onClick: () => void refreshPlan(), children: planLoading ? "Refreshing…" : "Refresh preview" })] }), [...targetGroups.entries()].map(([group, targets]) => (_jsxs("div", { className: "vertebr-sync-target-group", children: [_jsx("div", { className: "vertebr-sync-target-group-label", children: group }), _jsx("ul", { className: "vertebr-sync-target-list", children: targets.map((target) => {
157
191
  const selected = selectedTargets.includes(target.id);
158
- return (_jsx("li", { children: _jsxs("button", { type: "button", className: "vertebr-sync-target-row", "data-selected": selected || undefined, onClick: () => toggleTarget(target.id), children: [_jsx("span", { className: "vertebr-sync-target-check", "aria-hidden": "true", children: selected ? "✓" : "" }), _jsxs("span", { className: "vertebr-sync-target-main", children: [_jsx("span", { className: "vertebr-sync-target-title", children: target.label }), target.description ? (_jsx("span", { className: "vertebr-sync-target-desc", children: target.description })) : null] }), _jsx("span", { className: "vertebr-sync-target-badges", children: toEnvironments.length === 0 ? (_jsx(Badge, { children: "Choose destination" })) : planLoading ? (_jsx(Badge, { children: "Previewing\u2026" })) : (toEnvironments.map((destination) => {
192
+ return (_jsx("li", { children: _jsxs("div", { className: "vertebr-sync-target-row", "data-selected": selected || undefined, children: [_jsxs("button", { type: "button", className: "vertebr-sync-target-toggle", onClick: () => toggleTarget(target.id), "aria-pressed": selected, children: [_jsx("span", { className: "vertebr-sync-target-check", "aria-hidden": "true", children: selected ? "✓" : "" }), _jsxs("span", { className: "vertebr-sync-target-main", children: [_jsx("span", { className: "vertebr-sync-target-title", children: target.label }), target.description ? (_jsx("span", { className: "vertebr-sync-target-desc", children: target.description })) : null] })] }), _jsx("span", { className: "vertebr-sync-target-badges", children: toEnvironments.length === 0 ? (_jsx(Badge, { children: "Choose destination" })) : planLoading ? (_jsx(Badge, { children: "Previewing\u2026" })) : (toEnvironments.map((destination) => {
159
193
  const item = planForTarget(destination.id, target.id);
160
194
  if (!item) {
161
195
  return (_jsx(Badge, { children: destination.label }, destination.id));
162
196
  }
163
- return (_jsxs("span", { className: "vertebr-sync-target-dest", title: item.summary, children: [_jsxs(Badge, { tone: statusTone(item.status), children: [destination.label, ": ", statusLabel(item.status)] }), _jsx("span", { className: "vertebr-sync-target-summary", children: item.summary })] }, destination.id));
197
+ const canInspect = (item.status === "changed" ||
198
+ item.status === "new") &&
199
+ (Boolean(item.changes?.length) ||
200
+ Boolean(item.preview));
201
+ return (_jsxs("span", { className: "vertebr-sync-target-dest", title: item.summary, children: [_jsxs(Badge, { tone: statusTone(item.status), children: [destination.label, ": ", statusLabel(item.status)] }), _jsx("span", { className: "vertebr-sync-target-summary", children: item.summary }), canInspect ? (_jsx("button", { type: "button", className: "vertebr-sync-diff-btn", onClick: () => setInspect({
202
+ targetId: target.id,
203
+ targetLabel: target.label,
204
+ destinationId: destination.id,
205
+ destinationLabel: destination.label,
206
+ item,
207
+ }), children: item.preview ? "View preview" : "View diff" })) : null] }, destination.id));
164
208
  })) })] }) }, target.id));
165
209
  }) })] }, group)))] }), sync?.excluded.length ? (_jsx(Card, { title: "Never synced", subdued: true, children: _jsx("ul", { className: "vertebr-sync-excluded-list", children: sync.excluded.map((item) => (_jsxs("li", { children: [_jsx("span", { className: "vertebr-sync-excluded-name", children: item.label }), _jsx("span", { className: "vertebr-sync-excluded-reason", children: item.reason })] }, item.label))) }) })) : null, error ? _jsx("div", { className: "vertebr-banner", "data-tone": "danger", children: error }) : null] }), _jsxs("div", { className: "vertebr-savebar vertebr-sync-footer", children: [_jsx("div", { className: "vertebr-savebar-message", children: syncSummary }), _jsx("div", { className: "vertebr-savebar-actions", children: _jsxs("button", { type: "button", className: "vertebr-btn vertebr-btn-primary vertebr-sync-run-btn", disabled: running ||
166
210
  planLoading ||
167
211
  toEnv.length === 0 ||
168
- selectedTargets.length === 0, onClick: () => void runSync(), children: [_jsx(IconSync, {}), _jsx("span", { children: running ? "Syncing…" : "Sync selected" })] }) })] })] }));
212
+ selectedTargets.length === 0, onClick: () => void runSync(), children: [_jsx(IconSync, {}), _jsx("span", { children: running ? "Syncing…" : "Sync selected" })] }) })] }), inspect ? (_jsx(SyncDiffModal, { inspect: inspect, sourceLabel: fromEnvironment?.label ?? fromEnv, onClose: () => setInspect(null) })) : null] }));
169
213
  }
@@ -6,11 +6,13 @@ export type PagePreviewPaneProps = {
6
6
  controlId: string;
7
7
  /** Latest unsaved form values. */
8
8
  draft: Record<string, unknown> | null;
9
+ /** Extra add-on drafts owned by this page (e.g. countdown on coming soon). */
10
+ relatedDrafts?: Record<string, Record<string, unknown>>;
9
11
  /** Label for the preview chrome. */
10
12
  previewKind?: "page" | "addon";
11
13
  };
12
14
  /**
13
15
  * Live host iframe for Pages. Vertebr posts drafts; the host renders.
14
16
  */
15
- export declare function PagePreviewPane({ previewPath, controlId, draft, previewKind, }: PagePreviewPaneProps): import("react").JSX.Element;
17
+ export declare function PagePreviewPane({ previewPath, controlId, draft, relatedDrafts, previewKind, }: PagePreviewPaneProps): import("react").JSX.Element;
16
18
  //# sourceMappingURL=PagePreviewPane.d.ts.map
@@ -1 +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;IACtC,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAChC,CAAC;AAWF;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,SAAS,EACT,KAAK,EACL,WAAoB,GACrB,EAAE,oBAAoB,+BAmMtB"}
1
+ {"version":3,"file":"PagePreviewPane.d.ts","sourceRoot":"","sources":["../../../src/components/admin/PagePreviewPane.tsx"],"names":[],"mappings":"AAQA,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;IACtC,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAChC,CAAC;AAqBF;;GAEG;AACH,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,SAAS,EACT,KAAK,EACL,aAAa,EACb,WAAoB,GACrB,EAAE,oBAAoB,+BAoOtB"}
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { buildVertebrPreviewUrl, postVertebrPreviewDraft, subscribeVertebrPreviewReady, } from "@vertebr/client";
3
+ import { buildVertebrPreviewUrl, postVertebrPreviewDraft, subscribeVertebrPreviewNavigate, subscribeVertebrPreviewReady, } from "@vertebr/client";
4
4
  const VIEWPORTS = [
5
5
  { id: "desktop", label: "Desktop" },
6
6
  { id: "mobile", label: "Mobile" },
@@ -8,28 +8,52 @@ const VIEWPORTS = [
8
8
  /** Logical CSS viewport for mobile preview (host media queries use this). */
9
9
  const MOBILE_WIDTH = 390;
10
10
  const MOBILE_HEIGHT = 844;
11
+ function normalizePreviewPath(path) {
12
+ const trimmed = path.trim() || "/";
13
+ try {
14
+ const url = new URL(trimmed, "http://vertebr.local");
15
+ return `${url.pathname}${url.search}${url.hash}` || "/";
16
+ }
17
+ catch {
18
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
19
+ }
20
+ }
11
21
  /**
12
22
  * Live host iframe for Pages. Vertebr posts drafts; the host renders.
13
23
  */
14
- export function PagePreviewPane({ previewPath, controlId, draft, previewKind = "page", }) {
24
+ export function PagePreviewPane({ previewPath, controlId, draft, relatedDrafts, previewKind = "page", }) {
15
25
  const iframeRef = useRef(null);
16
26
  const stageRef = useRef(null);
17
27
  const draftRef = useRef(draft);
28
+ const relatedRef = useRef(relatedDrafts);
18
29
  const [frameReady, setFrameReady] = useState(false);
19
30
  const [frameError, setFrameError] = useState(false);
20
31
  const [viewport, setViewport] = useState("desktop");
21
32
  const [mobileScale, setMobileScale] = useState(1);
33
+ const [activePath, setActivePath] = useState(() => normalizePreviewPath(previewPath));
22
34
  draftRef.current = draft;
35
+ relatedRef.current = relatedDrafts;
23
36
  const isMobile = viewport === "mobile";
24
- const src = useMemo(() => buildVertebrPreviewUrl(previewPath, {
37
+ const basePath = normalizePreviewPath(previewPath);
38
+ const navigatedAway = activePath !== basePath;
39
+ useEffect(() => {
40
+ setActivePath(normalizePreviewPath(previewPath));
41
+ }, [previewPath]);
42
+ const src = useMemo(() => buildVertebrPreviewUrl(activePath, {
25
43
  controlId,
26
- }), [previewPath, controlId]);
44
+ }), [activePath, controlId]);
27
45
  const pushDraft = useCallback(() => {
28
46
  const win = iframeRef.current?.contentWindow;
29
47
  const value = draftRef.current;
30
48
  if (!win || !value)
31
49
  return;
32
50
  postVertebrPreviewDraft(win, controlId, value);
51
+ const related = relatedRef.current;
52
+ if (!related)
53
+ return;
54
+ for (const [id, relatedValue] of Object.entries(related)) {
55
+ postVertebrPreviewDraft(win, id, relatedValue);
56
+ }
33
57
  }, [controlId]);
34
58
  const notifyHostResize = useCallback(() => {
35
59
  const win = iframeRef.current?.contentWindow;
@@ -55,11 +79,19 @@ export function PagePreviewPane({ previewPath, controlId, draft, previewKind = "
55
79
  pushDraft();
56
80
  });
57
81
  }, [pushDraft, src]);
82
+ useEffect(() => {
83
+ return subscribeVertebrPreviewNavigate((path, source) => {
84
+ const win = iframeRef.current?.contentWindow;
85
+ if (!win || source !== win)
86
+ return;
87
+ setActivePath(normalizePreviewPath(path));
88
+ });
89
+ }, []);
58
90
  useEffect(() => {
59
91
  if (!frameReady)
60
92
  return;
61
93
  pushDraft();
62
- }, [draft, frameReady, pushDraft]);
94
+ }, [draft, relatedDrafts, frameReady, pushDraft]);
63
95
  // Fit the fixed mobile viewport into the stage without changing its
64
96
  // layout width (transform scale keeps host media queries at 390px).
65
97
  useEffect(() => {
@@ -93,11 +125,11 @@ export function PagePreviewPane({ previewPath, controlId, draft, previewKind = "
93
125
  const timer = window.setTimeout(notifyHostResize, 50);
94
126
  return () => window.clearTimeout(timer);
95
127
  }, [viewport, mobileScale, frameReady, notifyHostResize]);
96
- return (_jsxs("div", { className: "vertebr-page-preview", "data-preview-kind": previewKind, children: [_jsxs("div", { className: "vertebr-page-preview-meta", children: [_jsxs("p", { children: [_jsx("span", { children: previewKind === "addon" ? "Storefront" : "Preview" }), _jsx("strong", { children: previewPath })] }), _jsxs("p", { children: [_jsx("span", { children: "Status" }), _jsx("strong", { children: frameError
128
+ return (_jsxs("div", { className: "vertebr-page-preview", "data-preview-kind": previewKind, children: [_jsxs("div", { className: "vertebr-page-preview-meta", children: [_jsxs("p", { children: [_jsx("span", { children: previewKind === "addon" ? "Storefront" : "Preview" }), _jsx("strong", { children: activePath })] }), _jsxs("p", { children: [_jsx("span", { children: "Status" }), _jsx("strong", { children: frameError
97
129
  ? "Unavailable"
98
130
  : 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
131
+ ? "Connected"
132
+ : "Loading…" })] }), isMobile ? (_jsxs("p", { children: [_jsx("span", { children: "Viewport" }), _jsxs("strong", { children: [MOBILE_WIDTH, "\u00D7", MOBILE_HEIGHT] })] })) : null, navigatedAway ? (_jsx("button", { type: "button", className: "vertebr-page-preview-back", onClick: () => setActivePath(basePath), children: "Back to page" })) : 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
133
  ? {
102
134
  width: MOBILE_WIDTH * mobileScale,
103
135
  height: MOBILE_HEIGHT * mobileScale,
@@ -109,7 +141,7 @@ export function PagePreviewPane({ previewPath, controlId, draft, previewKind = "
109
141
  transform: `scale(${mobileScale})`,
110
142
  transformOrigin: "top left",
111
143
  }
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
144
+ : undefined, children: frameError ? (_jsxs("div", { className: "vertebr-page-preview-empty", children: ["Could not load host preview at ", activePath, "."] })) : (_jsx("iframe", { ref: iframeRef, title: `Preview ${controlId}`, src: src, width: isMobile ? MOBILE_WIDTH : undefined, height: isMobile ? MOBILE_HEIGHT : undefined, style: isMobile
113
145
  ? { width: MOBILE_WIDTH, height: MOBILE_HEIGHT }
114
146
  : undefined, onLoad: () => {
115
147
  // Ready message is preferred; onLoad covers hosts that
@@ -0,0 +1,22 @@
1
+ export type SyncPreviewAddonChip = {
2
+ id: string;
3
+ label: string;
4
+ /** True when the add-on is enabled in that environment. */
5
+ envEnabled: boolean;
6
+ };
7
+ export type SyncPreviewFrameProps = {
8
+ label: string;
9
+ previewPath: string;
10
+ controlId: string;
11
+ draft: Record<string, unknown>;
12
+ /** Extra controls to inject (e.g. countdown alongside coming soon). */
13
+ relatedDrafts?: Record<string, Record<string, unknown>>;
14
+ /** Add-ons that belong on this page preview (always force-shown in the iframe). */
15
+ addons?: SyncPreviewAddonChip[];
16
+ };
17
+ /**
18
+ * Compact same-origin preview iframe for Sync visual compare.
19
+ * Fixed mobile-sized frame; posts the given draft after ready.
20
+ */
21
+ export declare function SyncPreviewFrame({ label, previewPath, controlId, draft, relatedDrafts, addons, }: SyncPreviewFrameProps): import("react").JSX.Element;
22
+ //# sourceMappingURL=SyncPreviewFrame.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SyncPreviewFrame.d.ts","sourceRoot":"","sources":["../../../src/components/admin/SyncPreviewFrame.tsx"],"names":[],"mappings":"AAOA,MAAM,MAAM,oBAAoB,GAAG;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxD,mFAAmF;IACnF,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACjC,CAAC;AAqDF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,SAAS,EACT,KAAK,EACL,aAAa,EACb,MAAM,GACP,EAAE,qBAAqB,+BA6JvB"}