vue-wswg-editor 0.0.11 → 0.0.13

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 (72) hide show
  1. package/README.md +23 -8
  2. package/dist/style.css +1 -1
  3. package/dist/types/components/AddBlockItem/AddBlockItem.vue.d.ts +6 -0
  4. package/dist/types/components/BlockBrowser/BlockBrowser.vue.d.ts +2 -0
  5. package/dist/types/components/BlockComponent/BlockComponent.vue.d.ts +15 -0
  6. package/dist/types/components/BlockEditorFieldNode/BlockEditorFieldNode.vue.d.ts +15 -0
  7. package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +17 -0
  8. package/dist/types/components/BlockImageFieldNode/BlockImageNode.vue.d.ts +19 -0
  9. package/dist/types/components/BlockMarginFieldNode/BlockMarginNode.vue.d.ts +23 -0
  10. package/dist/types/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue.d.ts +15 -0
  11. package/dist/types/components/BrowserNavigation/BrowserNavigation.vue.d.ts +5 -0
  12. package/dist/types/components/EditorPageRenderer/EditorPageRenderer.vue.d.ts +21 -0
  13. package/dist/types/components/EmptyState/EmptyState.vue.d.ts +9 -0
  14. package/dist/types/components/IframePreview/IframePreview.vue.d.ts +26 -0
  15. package/dist/types/components/IframePreview/iframeContent.d.ts +9 -0
  16. package/dist/types/components/IframePreview/iframePreviewApp.d.ts +36 -0
  17. package/dist/types/components/IframePreview/messageHandler.d.ts +55 -0
  18. package/dist/types/components/IframePreview/types.d.ts +77 -0
  19. package/dist/types/components/PageBlockList/PageBlockList.vue.d.ts +19 -0
  20. package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +37 -0
  21. package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +28 -0
  22. package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +15 -0
  23. package/dist/types/components/PageSettings/PageSettings.vue.d.ts +19 -0
  24. package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +6 -0
  25. package/dist/types/components/WswgPageBuilder/WswgPageBuilder.test.d.ts +1 -0
  26. package/dist/types/components/WswgPageBuilder/WswgPageBuilder.vue.d.ts +38 -0
  27. package/dist/types/index.d.ts +13 -0
  28. package/dist/types/util/fieldConfig.d.ts +87 -0
  29. package/dist/types/util/helpers.d.ts +28 -0
  30. package/dist/types/util/registry.d.ts +27 -0
  31. package/dist/types/util/theme-registry.d.ts +42 -0
  32. package/dist/types/util/validation.d.ts +26 -0
  33. package/dist/types/vite-plugin.d.ts +9 -0
  34. package/dist/vite-plugin.js +80 -0
  35. package/dist/vue-wswg-editor.es.js +2854 -2006
  36. package/package.json +1 -2
  37. package/src/assets/styles/_mixins.scss +15 -0
  38. package/src/components/AddBlockItem/AddBlockItem.vue +13 -4
  39. package/src/components/BlockBrowser/BlockBrowser.vue +5 -5
  40. package/src/components/BlockComponent/BlockComponent.vue +23 -50
  41. package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +12 -10
  42. package/src/components/BlockEditorFields/BlockEditorFields.vue +24 -4
  43. package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +9 -4
  44. package/src/components/BrowserNavigation/BrowserNavigation.vue +1 -1
  45. package/src/components/EditorPageRenderer/EditorPageRenderer.vue +641 -0
  46. package/src/components/EmptyState/EmptyState.vue +3 -12
  47. package/src/components/IframePreview/IframePreview.vue +211 -0
  48. package/src/components/IframePreview/iframeContent.ts +210 -0
  49. package/src/components/IframePreview/iframePreviewApp.ts +221 -0
  50. package/src/components/IframePreview/messageHandler.ts +219 -0
  51. package/src/components/IframePreview/types.ts +126 -0
  52. package/src/components/PageBlockList/PageBlockList.vue +8 -6
  53. package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +5 -3
  54. package/src/components/PageRenderer/PageRenderer.vue +9 -33
  55. package/src/components/PageSettings/PageSettings.vue +10 -6
  56. package/src/components/ResizeHandle/ResizeHandle.vue +68 -10
  57. package/src/components/{WswgJsonEditor/WswgJsonEditor.test.ts → WswgPageBuilder/WswgPageBuilder.test.ts} +8 -8
  58. package/src/components/WswgPageBuilder/WswgPageBuilder.vue +375 -0
  59. package/src/index.ts +10 -2
  60. package/src/shims.d.ts +4 -0
  61. package/src/types/Theme.d.ts +15 -0
  62. package/src/util/registry.ts +2 -2
  63. package/src/util/theme-registry.ts +397 -0
  64. package/src/util/validation.ts +104 -13
  65. package/src/vite-plugin.ts +8 -4
  66. package/types/vue-wswg-editor.d.ts +4 -0
  67. package/src/components/PageRenderer/blockModules-alternative.ts.example +0 -9
  68. package/src/components/PageRenderer/blockModules-manual.ts.example +0 -19
  69. package/src/components/PageRenderer/blockModules-runtime.ts.example +0 -23
  70. package/src/components/PageRenderer/blockModules.ts +0 -32
  71. package/src/components/PageRenderer/layoutModules.ts +0 -32
  72. package/src/components/WswgJsonEditor/WswgJsonEditor.vue +0 -595
@@ -0,0 +1,211 @@
1
+ <template>
2
+ <div ref="containerRef" class="iframe-preview-container">
3
+ <iframe ref="iframeRef" title="Page preview" :src="iframeSrc" class="iframe-preview" frameborder="0"></iframe>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from "vue";
9
+ import { generateIframeHTML } from "./iframeContent";
10
+ import {
11
+ sendPageDataUpdate,
12
+ sendActiveBlock,
13
+ sendHoveredBlock,
14
+ sendSettingsOpen,
15
+ sendScrollToBlock,
16
+ handleIframeMessage,
17
+ } from "./messageHandler";
18
+ import type { Block } from "../../types/Block";
19
+ import { getIframeAppModuleUrl } from "./iframePreviewApp";
20
+
21
+ // Get the iframe app module URL
22
+ // This will be processed by the consuming app's Vite build
23
+ const iframeAppModuleUrl = getIframeAppModuleUrl();
24
+
25
+ const props = defineProps<{
26
+ pageData?: Record<string, any>;
27
+ activeBlock: Block | null;
28
+ hoveredBlockId: string | null;
29
+ viewport: "desktop" | "mobile";
30
+ editable?: boolean;
31
+ blocksKey?: string;
32
+ settingsKey?: string;
33
+ settingsOpen?: boolean;
34
+ theme?: string;
35
+ }>();
36
+
37
+ const emit = defineEmits<{
38
+ (e: "click-block", block: Block | null): void;
39
+ (e: "hover-block", blockId: string | null): void;
40
+ (e: "block-reorder", oldIndex: number, newIndex: number): void;
41
+ (e: "block-add", blockType: string, index: number): void;
42
+ (e: "click-partial", partialValue: string): void;
43
+ }>();
44
+
45
+ const iframeRef = ref<HTMLIFrameElement | null>(null);
46
+ const containerRef = ref<HTMLElement | null>(null);
47
+ const iframeReady = ref(false);
48
+ const iframeSrc = ref<string>("");
49
+
50
+ const blocksKey = computed(() => props.blocksKey || "blocks");
51
+ const settingsKey = computed(() => props.settingsKey || "settings");
52
+
53
+ // Generate blob URL for iframe
54
+ function createIframeSrc(): string {
55
+ const html = generateIframeHTML(iframeAppModuleUrl);
56
+ const blob = new Blob([html], { type: "text/html" });
57
+ return URL.createObjectURL(blob);
58
+ }
59
+
60
+ // Update iframe content - send pageData to Vue app in iframe
61
+ async function updateIframeContent() {
62
+ if (!iframeRef.value || !iframeReady.value) return;
63
+ if (!props.pageData || !props.pageData[blocksKey.value]) return;
64
+
65
+ await nextTick();
66
+ sendPageDataUpdate(iframeRef.value, props.pageData, blocksKey.value, settingsKey.value, props.theme || "default");
67
+ }
68
+
69
+ // Setup message listener
70
+ let messageListener: ((event: MessageEvent) => void) | null = null;
71
+
72
+ function setupMessageListener() {
73
+ messageListener = (event: MessageEvent) => {
74
+ // Only handle messages from our iframe
75
+ if (event.source !== iframeRef.value?.contentWindow) return;
76
+
77
+ handleIframeMessage(event, {
78
+ onBlockClick: (_blockId: string, block: any) => {
79
+ emit("click-block", block);
80
+ },
81
+ onBlockHover: (blockId: string | null) => {
82
+ emit("hover-block", blockId);
83
+ },
84
+ onBlockReorder: (oldIndex: number, newIndex: number) => {
85
+ emit("block-reorder", oldIndex, newIndex);
86
+ },
87
+ onBlockAdd: (blockType: string, index: number) => {
88
+ emit("block-add", blockType, index);
89
+ },
90
+ onPartialClick: (partialValue: string) => {
91
+ emit("click-partial", partialValue);
92
+ },
93
+ onIframeReady: () => {
94
+ iframeReady.value = true;
95
+ // Wait a bit for iframe Vue app to be fully ready
96
+ setTimeout(() => {
97
+ updateIframeContent();
98
+ // Send initial settingsOpen state
99
+ if (iframeRef.value && props.settingsOpen !== undefined) {
100
+ sendSettingsOpen(iframeRef.value, props.settingsOpen);
101
+ }
102
+ }, 100);
103
+ },
104
+ });
105
+ };
106
+
107
+ window.addEventListener("message", messageListener);
108
+ }
109
+
110
+ function cleanupMessageListener() {
111
+ if (messageListener) {
112
+ window.removeEventListener("message", messageListener);
113
+ messageListener = null;
114
+ }
115
+ }
116
+
117
+ // Watch for pageData changes
118
+ watch(
119
+ [() => props.pageData, () => props.theme],
120
+ async () => {
121
+ await nextTick();
122
+ updateIframeContent();
123
+ },
124
+ { deep: true }
125
+ );
126
+
127
+ // Watch for activeBlock changes
128
+ watch(
129
+ () => props.activeBlock,
130
+ async (block) => {
131
+ if (iframeRef.value && iframeReady.value) {
132
+ sendActiveBlock(iframeRef.value, block || null);
133
+ // Also scroll to block
134
+ if (block?.id) {
135
+ await nextTick();
136
+ sendScrollToBlock(iframeRef.value, block.id);
137
+ }
138
+ }
139
+ }
140
+ );
141
+
142
+ // Watch for hoveredBlockId changes
143
+ watch(
144
+ () => props.hoveredBlockId,
145
+ (blockId) => {
146
+ if (iframeRef.value && iframeReady.value) {
147
+ sendHoveredBlock(iframeRef.value, blockId);
148
+ }
149
+ }
150
+ );
151
+
152
+ // Watch for settingsOpen changes
153
+ watch(
154
+ () => props.settingsOpen,
155
+ (settingsOpen) => {
156
+ if (iframeRef.value && iframeReady.value) {
157
+ sendSettingsOpen(iframeRef.value, settingsOpen ?? false);
158
+ }
159
+ }
160
+ );
161
+
162
+ onMounted(() => {
163
+ iframeSrc.value = createIframeSrc();
164
+ setupMessageListener();
165
+ });
166
+
167
+ onBeforeUnmount(() => {
168
+ cleanupMessageListener();
169
+ // Cleanup blob URL
170
+ if (iframeSrc.value.startsWith("blob:")) {
171
+ URL.revokeObjectURL(iframeSrc.value);
172
+ }
173
+ });
174
+ </script>
175
+
176
+ <style scoped lang="scss">
177
+ .iframe-preview-container {
178
+ // position: relative;
179
+ // width: 100%;
180
+ // overflow: hidden;
181
+ // background-color: #ededed;
182
+ height: -webkit-fill-available;
183
+
184
+ &.mobile-viewport {
185
+ .iframe-preview {
186
+ max-width: 384px;
187
+ margin: 0 auto;
188
+ }
189
+ }
190
+ }
191
+
192
+ .iframe-preview {
193
+ display: block;
194
+ width: 100%;
195
+ height: 100%;
196
+ background: white;
197
+ border: none;
198
+ }
199
+
200
+ .hidden-renderer {
201
+ position: absolute;
202
+ top: -9999px;
203
+ left: -9999px;
204
+ visibility: hidden;
205
+ width: 1px;
206
+ height: 1px;
207
+ overflow: hidden;
208
+ pointer-events: none;
209
+ opacity: 0;
210
+ }
211
+ </style>
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Extract CSS variables from the parent document
3
+ * Extracts variables from :root and specific selectors like .wswg-page-builder
4
+ */
5
+ function extractParentCSSVariables(): string {
6
+ if (typeof document === "undefined" || typeof window === "undefined") return "";
7
+
8
+ const variables: string[] = [];
9
+
10
+ // Get all CSS variables from :root
11
+ const rootVariables = new Set<string>();
12
+ const allStyles = Array.from(document.styleSheets);
13
+
14
+ // Extract from stylesheets
15
+ allStyles.forEach((sheet) => {
16
+ try {
17
+ const rules = Array.from(sheet.cssRules || sheet.rules || []);
18
+ rules.forEach((rule) => {
19
+ if (rule instanceof CSSStyleRule) {
20
+ const { selectorText, style } = rule;
21
+ // Check if this rule targets :root or html
22
+ if (selectorText === ":root" || selectorText === "html" || selectorText === "html:root") {
23
+ for (let i = 0; i < style.length; i++) {
24
+ const property = style[i];
25
+ if (property.startsWith("--")) {
26
+ const value = style.getPropertyValue(property);
27
+ rootVariables.add(`${property}: ${value};`);
28
+ }
29
+ }
30
+ }
31
+ }
32
+ });
33
+ } catch {
34
+ // Cross-origin stylesheets will throw errors, ignore them
35
+ }
36
+ });
37
+
38
+ // Also extract from inline styles in <style> tags
39
+ const styleTags = document.querySelectorAll("head style");
40
+ styleTags.forEach((style) => {
41
+ const content = style.textContent || style.innerHTML;
42
+ if (content) {
43
+ // Match CSS variable declarations
44
+ const varRegex = /--[\w-]+\s*:\s*[^;]+;/g;
45
+ const matches = content.match(varRegex);
46
+ if (matches) {
47
+ matches.forEach((match) => {
48
+ rootVariables.add(match.trim());
49
+ });
50
+ }
51
+ }
52
+ });
53
+
54
+ if (rootVariables.size > 0) {
55
+ variables.push(`:root {`);
56
+ rootVariables.forEach((variable) => {
57
+ variables.push(` ${variable}`);
58
+ });
59
+ variables.push(`}`);
60
+ }
61
+
62
+ return variables.length > 0 ? `<style>${variables.join("\n")}</style>` : "";
63
+ }
64
+
65
+ /**
66
+ * Extract stylesheets from the parent document
67
+ * This includes both <link rel="stylesheet"> tags and <style> tags
68
+ */
69
+ function extractParentStylesheets(): string {
70
+ if (typeof document === "undefined") return "";
71
+
72
+ const stylesheets: string[] = [];
73
+
74
+ // Extract <link rel="stylesheet"> tags
75
+ const linkTags = document.querySelectorAll('head link[rel="stylesheet"]');
76
+ linkTags.forEach((link) => {
77
+ const href = link.getAttribute("href");
78
+ if (href) {
79
+ // Convert relative URLs to absolute URLs
80
+ const absoluteHref = new URL(href, window.location.href).href;
81
+ stylesheets.push(`<link rel="stylesheet" href="${absoluteHref}">`);
82
+ }
83
+ });
84
+
85
+ // Extract <style> tags
86
+ const styleTags = document.querySelectorAll("head style");
87
+ styleTags.forEach((style) => {
88
+ const content = style.textContent || style.innerHTML;
89
+ if (content) {
90
+ stylesheets.push(`<style>${content}</style>`);
91
+ }
92
+ });
93
+
94
+ return stylesheets.join("\n ");
95
+ }
96
+
97
+ /**
98
+ * Generate HTML content for the iframe
99
+ * This creates a standalone HTML page with a Vue app that will be loaded in the iframe
100
+ * The Vue app uses PageRenderer to render blocks reactively
101
+ *
102
+ * @param iframeAppModuleUrl - URL to the iframe app module bundle
103
+ * This should be obtained using import.meta.url from iframePreviewApp.ts
104
+ */
105
+ export function generateIframeHTML(iframeAppModuleUrl?: string): string {
106
+ const parentStylesheets = extractParentStylesheets();
107
+ const parentCSSVariables = extractParentCSSVariables();
108
+ const vueCdnUrl = "https://unpkg.com/vue@3/dist/vue.esm-browser.js";
109
+ // Get parent origin to use as base URL for relative asset paths
110
+ const parentOrigin = typeof window !== "undefined" ? window.location.origin : "";
111
+
112
+ // Define Vue feature flags before Vue is loaded
113
+ // These must be defined as global constants before any Vue code runs
114
+ const vueFeatureFlagsScript = `<script>
115
+ // Define Vue feature flags to prevent warnings
116
+ // These must be defined before Vue or the library code is imported
117
+ // Using var to make them available globally (not just on window)
118
+ var __VUE_OPTIONS_API__ = true;
119
+ var __VUE_PROD_DEVTOOLS__ = false;
120
+ var __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false;
121
+
122
+ // Also set on window for compatibility
123
+ window.__VUE_OPTIONS_API__ = true;
124
+ window.__VUE_PROD_DEVTOOLS__ = false;
125
+ window.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false;
126
+ </script>`;
127
+
128
+ // Generate script that loads Vue and initializes the iframe app
129
+ const appScript = iframeAppModuleUrl
130
+ ? `<script type="module">
131
+ import { createApp } from '${vueCdnUrl}';
132
+ import { createIframeApp } from '${iframeAppModuleUrl}';
133
+
134
+ const appEl = document.getElementById('app');
135
+ if (appEl) {
136
+ createIframeApp(appEl).catch(error => {
137
+ console.error('Failed to create iframe app:', error);
138
+ });
139
+ }
140
+ </script>`
141
+ : `<script type="module">
142
+ // Fallback: Wait for parent to send module URL via postMessage
143
+ import { createApp } from '${vueCdnUrl}';
144
+
145
+ let appInitialized = false;
146
+
147
+ window.addEventListener('message', async (event) => {
148
+ if (event.data.type === 'INIT_IFRAME_APP' && event.data.moduleUrl && !appInitialized) {
149
+ appInitialized = true;
150
+ try {
151
+ const { createIframeApp } = await import(event.data.moduleUrl);
152
+ const appEl = document.getElementById('app');
153
+ if (appEl) {
154
+ createIframeApp(appEl);
155
+ }
156
+ } catch (error) {
157
+ console.error('Failed to load iframe app module:', error);
158
+ // Fallback: create minimal Vue app
159
+ const appEl = document.getElementById('app');
160
+ if (appEl) {
161
+ createApp({
162
+ template: '<div class="p-4">Loading preview...</div>'
163
+ }).mount(appEl);
164
+ }
165
+ }
166
+ }
167
+ });
168
+
169
+ // Notify parent that we're ready to receive module URL
170
+ if (window.parent) {
171
+ window.parent.postMessage({ type: 'IFRAME_READY_FOR_APP' }, '*');
172
+ }
173
+ </script>`;
174
+
175
+ return `<!DOCTYPE html>
176
+ <html lang="en">
177
+ <head>
178
+ <meta charset="UTF-8">
179
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
180
+ <title>Page Preview</title>
181
+ ${parentOrigin ? `<base href="${parentOrigin}/">` : ""}
182
+ ${parentStylesheets}
183
+ ${parentCSSVariables}
184
+ <style>
185
+ /* Additional iframe-specific styles */
186
+ html, body {
187
+ margin: 0;
188
+ padding: 0;
189
+ width: 100%;
190
+ height: 100%;
191
+ overflow-x: hidden;
192
+ }
193
+
194
+ #app {
195
+ width: 100%;
196
+ min-height: 100vh;
197
+ }
198
+
199
+ .page-renderer-wrapper {
200
+ width: 100%;
201
+ }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <div id="app"></div>
206
+ ${vueFeatureFlagsScript}
207
+ ${appScript}
208
+ </body>
209
+ </html>`;
210
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Iframe Preview Vue Application
3
+ *
4
+ * This file creates a Vue application that runs inside the iframe preview.
5
+ * Since the library is consumed as source code, consuming apps' Vite builds
6
+ * will process this file, giving it access to virtual modules (blocks/layouts).
7
+ *
8
+ * Usage in consuming apps:
9
+ * ```ts
10
+ * import { createIframeApp, getIframeAppModuleUrl } from 'vue-wswg-editor/IframePreviewApp'
11
+ * ```
12
+ */
13
+
14
+ import { createApp, ref, watch, h, type App } from "vue";
15
+ import EditorPageRenderer from "../EditorPageRenderer/EditorPageRenderer.vue";
16
+ import type { Block } from "../../types/Block";
17
+
18
+ /**
19
+ * Get the URL of this module
20
+ * This can be used to import the module in the iframe HTML
21
+ */
22
+ export function getIframeAppModuleUrl(): string {
23
+ // Use import.meta.url to get the current module's URL
24
+ // In development, this will be the source file URL
25
+ // In production (after Vite build), this will be the bundled module URL
26
+ return import.meta.url;
27
+ }
28
+
29
+ export interface IframeAppState {
30
+ pageData: Record<string, any> | null;
31
+ activeBlock: Block | null;
32
+ hoveredBlockId: string | null;
33
+ blocksKey: string;
34
+ settingsKey: string;
35
+ settingsOpen: boolean;
36
+ theme: string;
37
+ }
38
+
39
+ export interface IframeAppCallbacks {
40
+ onBlockClick?: (blockId: string, block: Block | null) => void;
41
+ onBlockHover?: (blockId: string | null) => void;
42
+ }
43
+
44
+ /**
45
+ * Create and mount the Vue app for the iframe preview
46
+ */
47
+ export async function createIframeApp(container: HTMLElement): Promise<App> {
48
+ // State
49
+ const pageData = ref<Record<string, any> | null>(null);
50
+ const activeBlock = ref<Block | null>(null);
51
+ const hoveredBlockId = ref<string | null>(null);
52
+ const settingsOpen = ref<boolean>(false);
53
+ const blocksKey = ref<string>("blocks");
54
+ const settingsKey = ref<string>("settings");
55
+ const theme = ref<string>("default");
56
+ // Serialize data for postMessage (handles Vue reactive proxies)
57
+ function serializeForPostMessage(data: any): any {
58
+ try {
59
+ return JSON.parse(JSON.stringify(data));
60
+ } catch (error) {
61
+ console.warn("[iframe] Failed to serialize data for postMessage:", error);
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ // Message handler
67
+ function sendToParent(message: any) {
68
+ if (window.parent) {
69
+ // Serialize message to handle Vue reactive proxies
70
+ const serializedMessage = serializeForPostMessage(message);
71
+ if (!serializedMessage) {
72
+ console.error("[iframe] Failed to serialize message for postMessage");
73
+ return;
74
+ }
75
+ window.parent.postMessage(serializedMessage, "*");
76
+ }
77
+ }
78
+
79
+ // Handle messages from parent
80
+ window.addEventListener("message", (event: MessageEvent) => {
81
+ const msg = event.data;
82
+ if (!msg || !msg.type) return;
83
+
84
+ switch (msg.type) {
85
+ case "UPDATE_PAGE_DATA":
86
+ if (msg.pageData) {
87
+ pageData.value = msg.pageData;
88
+ }
89
+ if (msg.blocksKey) {
90
+ blocksKey.value = msg.blocksKey;
91
+ }
92
+ if (msg.settingsKey) {
93
+ settingsKey.value = msg.settingsKey;
94
+ }
95
+ if (msg.theme) {
96
+ theme.value = msg.theme;
97
+ }
98
+ break;
99
+ case "SET_ACTIVE_BLOCK":
100
+ activeBlock.value = msg.block;
101
+ break;
102
+ case "SET_HOVERED_BLOCK":
103
+ hoveredBlockId.value = msg.blockId;
104
+ break;
105
+ case "SET_SETTINGS_OPEN":
106
+ settingsOpen.value = msg.settingsOpen;
107
+ break;
108
+ case "SCROLL_TO_BLOCK": {
109
+ const block = document.querySelector(`[data-block-id="${msg.blockId}"]`);
110
+ if (block) {
111
+ block.scrollIntoView({ behavior: "smooth", block: "center" });
112
+ }
113
+ break;
114
+ }
115
+ }
116
+ });
117
+
118
+ // Create Vue app using render function (since we're using runtime-only Vue)
119
+ const app = createApp({
120
+ components: {
121
+ EditorPageRenderer,
122
+ },
123
+ setup() {
124
+ // Ensure PageRenderer has time to load blocks before rendering
125
+ const isPageReady = ref(false);
126
+
127
+ // Watch for pageData changes
128
+ watch(
129
+ () => pageData.value,
130
+ async (newPageData) => {
131
+ if (newPageData && newPageData[blocksKey.value]) {
132
+ // Give PageRenderer time to load block modules
133
+ // PageRenderer loads blocks in onBeforeMount, so we need to wait
134
+ await new Promise((resolve) => setTimeout(resolve, 200));
135
+ isPageReady.value = true;
136
+ } else {
137
+ isPageReady.value = false;
138
+ }
139
+ },
140
+ { immediate: true }
141
+ );
142
+
143
+ // Render function
144
+ return () => {
145
+ const currentPageData = pageData.value;
146
+ const hasPageData = currentPageData && currentPageData[blocksKey.value];
147
+ const blocks = hasPageData && currentPageData ? currentPageData[blocksKey.value] : [];
148
+
149
+ if (isPageReady.value && currentPageData) {
150
+ return h(EditorPageRenderer, {
151
+ blocks: blocks,
152
+ layout: currentPageData[settingsKey.value]?.layout,
153
+ settings: currentPageData[settingsKey.value],
154
+ activeBlock: activeBlock.value,
155
+ hoveredBlockId: hoveredBlockId.value,
156
+ settingsOpen: settingsOpen.value,
157
+ editable: true,
158
+ theme: theme.value,
159
+ });
160
+ } else {
161
+ // Show loading state while PageRenderer loads blocks
162
+ return h("div", { class: "bg-white px-5 py-12 md:py-20" }, [
163
+ h("div", { class: "mx-auto max-w-md pb-7 text-center" }, [
164
+ h(
165
+ "svg",
166
+ {
167
+ class: "mx-auto size-8 animate-spin text-blue-600",
168
+ xmlns: "http://www.w3.org/2000/svg",
169
+ fill: "none",
170
+ viewBox: "0 0 24 24",
171
+ },
172
+ [
173
+ h("circle", {
174
+ class: "opacity-25",
175
+ cx: "12",
176
+ cy: "12",
177
+ r: "10",
178
+ stroke: "currentColor",
179
+ "stroke-width": "4",
180
+ }),
181
+ h("path", {
182
+ class: "opacity-75",
183
+ fill: "currentColor",
184
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z",
185
+ }),
186
+ ]
187
+ ),
188
+ h("span", { class: "mt-4 text-gray-700" }, "Loading preview..."),
189
+ ]),
190
+ ]);
191
+ }
192
+ };
193
+ },
194
+ });
195
+
196
+ // Try to install Unhead plugin if available
197
+ // This is needed for layouts that use useHead from @vueuse/head
198
+ try {
199
+ // Dynamic import to check if @vueuse/head is available
200
+ // This module is externalized in vite.config.ts so it won't be bundled
201
+ // Using @vite-ignore to prevent static analysis
202
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
203
+ // @ts-ignore - @vueuse/head may not be available in all consuming apps
204
+ const headModule = await import(/* @vite-ignore */ "@vueuse/head");
205
+ if (headModule && typeof headModule.createHead === "function") {
206
+ const head = headModule.createHead();
207
+ app.use(head);
208
+ }
209
+ } catch {
210
+ // @vueuse/head not available - layouts using useHead will show warnings but won't break
211
+ // This is expected if the consuming app doesn't use @vueuse/head
212
+ }
213
+
214
+ // Mount the app
215
+ app.mount(container);
216
+
217
+ // Notify parent that iframe is ready
218
+ sendToParent({ type: "IFRAME_READY" });
219
+
220
+ return app;
221
+ }