@vertebr/react 0.10.0 → 0.11.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 (39) 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 +55 -7
  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/config/DatetimeInput.d.ts +15 -0
  16. package/dist/components/config/DatetimeInput.d.ts.map +1 -0
  17. package/dist/components/config/DatetimeInput.js +287 -0
  18. package/dist/components/config/EnumInput.d.ts.map +1 -1
  19. package/dist/components/config/EnumInput.js +4 -4
  20. package/dist/components/config/EnumListInput.d.ts.map +1 -1
  21. package/dist/components/config/EnumListInput.js +13 -3
  22. package/dist/components/config/RevisionHistory.d.ts +4 -0
  23. package/dist/components/config/RevisionHistory.d.ts.map +1 -1
  24. package/dist/components/config/RevisionHistory.js +63 -8
  25. package/dist/components/config/form/ConfigField.d.ts.map +1 -1
  26. package/dist/components/config/form/ConfigField.js +2 -1
  27. package/dist/components/config/form/ConfigForm.d.ts +1 -1
  28. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  29. package/dist/components/config/form/ConfigForm.js +54 -9
  30. package/dist/components/config/form/types.d.ts +9 -0
  31. package/dist/components/config/form/types.d.ts.map +1 -1
  32. package/dist/hooks/useSaveBar.d.ts +6 -0
  33. package/dist/hooks/useSaveBar.d.ts.map +1 -1
  34. package/dist/styles/form.css +452 -17
  35. package/dist/styles/page-preview.css +84 -0
  36. package/dist/styles/select.css +23 -0
  37. package/dist/styles/sync.css +388 -4
  38. package/dist/themes/light.css +27 -0
  39. 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) {
@@ -153,7 +200,8 @@ export function AdminContent({ client, catalog, selection, onSelectionChange, au
153
200
  return result.files;
154
201
  }
155
202
  : 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 ? ((() => {
203
+ ?.previewPath, relatedAddons: catalog.configs.find((item) => item.id === configData.id)
204
+ ?.relatedAddons, onConfigSaved: onConfigSaved, onRefreshConfig: onRefreshConfig, onSelectAddon: (id) => onSelectionChange?.({ kind: "addon", id }), galleryPicker: galleryPicker, fontLibraryPicker: fontLibraryPicker }, configData.id)) : null, selection?.kind === "addon" && configData ? ((() => {
157
205
  const addon = catalog.addons?.find((item) => item.id === configData.id);
158
206
  if (!addon)
159
207
  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"}
@@ -0,0 +1,135 @@
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 FRAME_WIDTH = 390;
5
+ const FRAME_HEIGHT = 720;
6
+ function normalizePreviewPath(path) {
7
+ const trimmed = path.trim() || "/";
8
+ try {
9
+ const url = new URL(trimmed, "http://vertebr.local");
10
+ return `${url.pathname}${url.search}${url.hash}` || "/";
11
+ }
12
+ catch {
13
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
14
+ }
15
+ }
16
+ /**
17
+ * Build related drafts for the iframe: prefer plan payloads (real enabled /
18
+ * content). Synthesize only when a listed add-on is missing from the plan.
19
+ */
20
+ function resolveRelatedDrafts(controlId, relatedDrafts, addons) {
21
+ const next = {};
22
+ for (const [id, value] of Object.entries(relatedDrafts ?? {})) {
23
+ next[id] = { ...value };
24
+ }
25
+ for (const addon of addons ?? []) {
26
+ const existing = next[addon.id] ?? {
27
+ enabled: addon.envEnabled,
28
+ };
29
+ const patched = { ...existing };
30
+ if (patched.enabled === undefined) {
31
+ patched.enabled = addon.envEnabled;
32
+ }
33
+ if (addon.id === "countdown" && controlId === "comingSoon") {
34
+ patched.target = "comingSoon";
35
+ }
36
+ if (addon.id === "countdown" && controlId === "product") {
37
+ patched.target = "product";
38
+ }
39
+ if (addon.id === "reviews" && controlId === "home") {
40
+ patched.showOnHome = true;
41
+ }
42
+ if (addon.id === "reviews" && controlId === "product") {
43
+ patched.showOnProducts = true;
44
+ }
45
+ next[addon.id] = patched;
46
+ }
47
+ return next;
48
+ }
49
+ /**
50
+ * Compact same-origin preview iframe for Sync visual compare.
51
+ * Fixed mobile-sized frame; posts the given draft after ready.
52
+ */
53
+ export function SyncPreviewFrame({ label, previewPath, controlId, draft, relatedDrafts, addons, }) {
54
+ const iframeRef = useRef(null);
55
+ const stageRef = useRef(null);
56
+ const draftRef = useRef(draft);
57
+ const [frameReady, setFrameReady] = useState(false);
58
+ const [frameError, setFrameError] = useState(false);
59
+ const [scale, setScale] = useState(1);
60
+ const resolvedRelated = useMemo(() => resolveRelatedDrafts(controlId, relatedDrafts, addons), [controlId, relatedDrafts, addons]);
61
+ const relatedRef = useRef(resolvedRelated);
62
+ draftRef.current = draft;
63
+ relatedRef.current = resolvedRelated;
64
+ const path = normalizePreviewPath(previewPath);
65
+ const src = useMemo(() => buildVertebrPreviewUrl(path, {
66
+ controlId,
67
+ }), [path, controlId]);
68
+ const pushDraft = useCallback(() => {
69
+ const win = iframeRef.current?.contentWindow;
70
+ const value = draftRef.current;
71
+ if (!win || !value)
72
+ return;
73
+ postVertebrPreviewDraft(win, controlId, value);
74
+ for (const [id, relatedValue] of Object.entries(relatedRef.current)) {
75
+ postVertebrPreviewDraft(win, id, relatedValue);
76
+ }
77
+ }, [controlId]);
78
+ useEffect(() => {
79
+ setFrameReady(false);
80
+ setFrameError(false);
81
+ }, [src]);
82
+ useEffect(() => {
83
+ return subscribeVertebrPreviewReady((source) => {
84
+ const win = iframeRef.current?.contentWindow;
85
+ if (!win || source !== win)
86
+ return;
87
+ setFrameReady(true);
88
+ pushDraft();
89
+ });
90
+ }, [pushDraft, src]);
91
+ useEffect(() => {
92
+ if (!frameReady)
93
+ return;
94
+ pushDraft();
95
+ }, [draft, resolvedRelated, frameReady, pushDraft]);
96
+ useEffect(() => {
97
+ const stage = stageRef.current;
98
+ if (!stage || typeof ResizeObserver === "undefined")
99
+ return;
100
+ const update = () => {
101
+ const style = getComputedStyle(stage);
102
+ const padX = (Number.parseFloat(style.paddingLeft) || 0) +
103
+ (Number.parseFloat(style.paddingRight) || 0);
104
+ const padY = (Number.parseFloat(style.paddingTop) || 0) +
105
+ (Number.parseFloat(style.paddingBottom) || 0);
106
+ const availW = Math.max(0, stage.clientWidth - padX);
107
+ const availH = Math.max(0, stage.clientHeight - padY);
108
+ const next = Math.min(1, availW / FRAME_WIDTH, availH / FRAME_HEIGHT);
109
+ setScale(Number.isFinite(next) && next > 0 ? next : 1);
110
+ };
111
+ update();
112
+ const observer = new ResizeObserver(update);
113
+ observer.observe(stage);
114
+ return () => observer.disconnect();
115
+ }, []);
116
+ const status = frameError
117
+ ? "Unavailable"
118
+ : frameReady
119
+ ? "Connected"
120
+ : "Loading…";
121
+ return (_jsxs("div", { className: "vertebr-sync-preview-frame", children: [_jsxs("div", { className: "vertebr-sync-preview-frame-meta", children: [_jsx("strong", { children: label }), _jsx("span", { children: path }), _jsx("span", { "data-status": frameError ? "error" : frameReady ? "ok" : "pending", children: status })] }), addons ? (_jsx("div", { className: "vertebr-sync-preview-addons", "aria-label": "Add-ons in preview", children: addons.length === 0 ? (_jsx("span", { className: "vertebr-sync-preview-addons-empty", children: "No add-ons" })) : (addons.map((addon) => (_jsxs("span", { className: "vertebr-sync-preview-addon", "data-enabled": addon.envEnabled ? "true" : "false", title: addon.envEnabled
122
+ ? `${addon.label} is enabled in this environment`
123
+ : `${addon.label} is off in this environment`, children: [addon.label, _jsx("em", { children: addon.envEnabled ? "on" : "off" })] }, addon.id)))) })) : null, _jsx("div", { ref: stageRef, className: "vertebr-sync-preview-frame-stage", children: _jsx("div", { className: "vertebr-sync-preview-frame-shell", style: {
124
+ width: FRAME_WIDTH * scale,
125
+ height: FRAME_HEIGHT * scale,
126
+ }, children: _jsx("div", { className: "vertebr-sync-preview-frame-scaler", style: {
127
+ width: FRAME_WIDTH,
128
+ height: FRAME_HEIGHT,
129
+ transform: `scale(${scale})`,
130
+ transformOrigin: "top left",
131
+ }, children: frameError ? (_jsxs("div", { className: "vertebr-sync-preview-frame-empty", children: ["Could not load preview at ", path, "."] })) : (_jsx("iframe", { ref: iframeRef, title: `${label} preview`, src: src, width: FRAME_WIDTH, height: FRAME_HEIGHT, onLoad: () => {
132
+ setFrameReady(true);
133
+ pushDraft();
134
+ }, onError: () => setFrameError(true) })) }) }) })] }));
135
+ }