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,397 @@
1
+ import { shallowRef, markRaw, type Ref } from "vue";
2
+ import type { Theme } from "../types/Theme";
3
+ import type { Layout } from "../types/Layout";
4
+ import type { Block } from "../types/Block";
5
+ import { generateNameVariations, toCamelCase } from "./helpers";
6
+ import { EditorFieldConfig } from "./fieldConfig";
7
+
8
+ // Load all thumbnail images - Vite will process these as assets and provide URLs
9
+ // For images, Vite returns the URL as the default export when using eager: true
10
+ // const thumbnailModules = import.meta.glob("@page-builder/blocks/**/thumbnail.png", { eager: true });
11
+
12
+ /**
13
+ * Registry of all page builder themes, blocks, layouts, fields & thumbnails
14
+ */
15
+ export const pageBuilderThemes: Ref<Record<string, Theme>> = shallowRef({});
16
+ export const themeLayouts: Ref<Record<string, Layout>> = shallowRef({});
17
+ export const themeBlocks: Ref<Record<string, Block>> = shallowRef({});
18
+ export const themeBlockFields: Ref<Record<string, Record<string, EditorFieldConfig>>> = shallowRef({});
19
+ export const activeThemeId: Ref<string | undefined> = shallowRef(undefined);
20
+ let blockThumbnails: Record<string, any> | null = null; // non reactive cache of block thumbnails
21
+ let themeThumbnails: Record<string, any> | null = null; // non reactive cache of theme thumbnails
22
+
23
+ ////////////////////////////////////////////////////////////
24
+ // Helper Functions
25
+ ////////////////////////////////////////////////////////////
26
+
27
+ /**
28
+ * Extract the default export from a module, handling different module formats
29
+ * Works with both eager and lazy-loaded modules from import.meta.glob
30
+ */
31
+ export function getModuleDefault(module: any): any {
32
+ if (!module) return undefined;
33
+
34
+ // If module is a function (lazy-loaded), we'd need to await it, but with eager: true it should be resolved
35
+ if (typeof module === "function") {
36
+ // This shouldn't happen with eager: true, but handle it just in case
37
+ return undefined;
38
+ }
39
+
40
+ // Try .default first (standard ES module format)
41
+ if (typeof module === "object") {
42
+ // Check if it has .default property
43
+ if ("default" in module && module.default !== undefined) {
44
+ return module.default;
45
+ }
46
+ // If no .default but module has component-like properties (__name, etc.), maybe module itself is the component
47
+ if ("__name" in module || "name" in module || "setup" in module || "render" in module) {
48
+ return module;
49
+ }
50
+ }
51
+
52
+ // Fallback: return the module as-is
53
+ return module;
54
+ }
55
+
56
+ /**************************************************
57
+ * THEMES
58
+ **************************************************/
59
+ export async function initialiseThemeRegistry(): Promise<void> {
60
+ // Clear existing registry
61
+ Object.keys(pageBuilderThemes.value).forEach((key) => {
62
+ delete pageBuilderThemes.value[key];
63
+ });
64
+
65
+ // Lazy load virtual modules to prevent initialization order issues
66
+ const { modules: themeModules } = await import("vue-wswg-editor:themes");
67
+
68
+ for (const [path, module] of Object.entries(themeModules)) {
69
+ let resolvedModule = module;
70
+ // If module is a function (lazy-loaded), call it to get the actual module
71
+ if (typeof module === "function") {
72
+ resolvedModule = await module();
73
+ }
74
+ const themeConfig = getModuleDefault(resolvedModule);
75
+ if (!themeConfig) continue;
76
+
77
+ // Extract theme ID from directory name containing theme.config.js
78
+ // Path format: "@page-builder/demo-theme/theme.config.js" -> theme ID is "demo-theme"
79
+ const pathParts = path.split("/");
80
+ // Remove the filename (theme.config.js) to get the directory path
81
+ const directoryPath = path.replace(/\/[^/]+\.config\.js$/, "");
82
+ // Get the directory name (second to last part of the path)
83
+ const themeId = pathParts[pathParts.length - 2];
84
+
85
+ if (!themeId) continue;
86
+
87
+ const theme: Theme = {
88
+ id: themeId,
89
+ path: directoryPath,
90
+ title: themeConfig.title || themeId,
91
+ description: themeConfig.description || "",
92
+ version: themeConfig.version || "1.0.0",
93
+ author: themeConfig.author || "",
94
+ authorWebsite: themeConfig.authorWebsite || "",
95
+ tags: themeConfig.tags || [],
96
+ license: themeConfig.license || "",
97
+ };
98
+
99
+ pageBuilderThemes.value[themeId] = theme;
100
+ }
101
+
102
+ // If there are no themes, throw an error
103
+ if (Object.keys(pageBuilderThemes.value).length === 0) {
104
+ console.error("[vue-wswg-editor:registry] No themes found");
105
+ return;
106
+ }
107
+ }
108
+
109
+ export function getThemes(): Theme[] {
110
+ return Object.values(pageBuilderThemes.value);
111
+ }
112
+
113
+ export function getActiveTheme(): Theme {
114
+ if (!activeThemeId.value) {
115
+ throw new Error("No active theme found");
116
+ }
117
+ return pageBuilderThemes.value[activeThemeId.value];
118
+ }
119
+
120
+ export async function setActiveTheme(themeId?: string): Promise<void> {
121
+ // If the themeID is not found, set the active theme ID to the first theme in the registry
122
+ if (!themeId || !pageBuilderThemes.value[themeId]) {
123
+ const firstThemeId = Object.keys(pageBuilderThemes.value)[0];
124
+ if (!firstThemeId) {
125
+ throw new Error("[vue-wswg-editor:registry] No themes found. Cannot set active theme.");
126
+ }
127
+ activeThemeId.value = firstThemeId;
128
+ console.warn(
129
+ `[vue-wswg-editor:registry] No theme found with ID: ${themeId}, setting active theme to: ${activeThemeId.value}`
130
+ );
131
+ } else {
132
+ activeThemeId.value = themeId;
133
+ }
134
+ }
135
+
136
+ /**************************************************
137
+ * THEME THUMBNAILS
138
+ **************************************************/
139
+ async function initialiseThemeThumbnailsRegistry(): Promise<void> {
140
+ const { modules: thumbnailModules } = await import("vue-wswg-editor:thumbnails");
141
+ themeThumbnails = thumbnailModules;
142
+ }
143
+
144
+ export function getThemeThumbnail(themeDirectory: string): string | undefined {
145
+ if (!themeDirectory || !themeThumbnails) return undefined;
146
+
147
+ // Try thumbnail.jpg first, then fall back to thumbnail.png
148
+ const thumbnailPaths = [`${themeDirectory}/thumbnail.jpg`, `${themeDirectory}/thumbnail.png`];
149
+
150
+ for (const thumbnailPath of thumbnailPaths) {
151
+ const thumbnailModule = themeThumbnails[thumbnailPath];
152
+ if (thumbnailModule) {
153
+ try {
154
+ const thumbnailUrl = (thumbnailModule as any)?.default;
155
+ if (thumbnailUrl) {
156
+ return thumbnailUrl as string;
157
+ }
158
+ } catch {
159
+ // Silently continue to next format if this one fails
160
+ continue;
161
+ }
162
+ }
163
+ }
164
+
165
+ // Return undefined if no thumbnail found
166
+ return undefined;
167
+ }
168
+
169
+ /**************************************************
170
+ * LAYOUTS
171
+ **************************************************/
172
+ export async function initialiseLayoutRegistry(): Promise<void> {
173
+ // Clear existing registry
174
+ Object.keys(themeLayouts.value).forEach((key) => {
175
+ delete themeLayouts.value[key];
176
+ });
177
+
178
+ // Get the active theme
179
+ const activeTheme = getActiveTheme();
180
+
181
+ // Use virtual module to load all layouts (scans all themes at build time)
182
+ // Then filter to only process layouts from the active theme
183
+ const { modules: layoutModules } = await import("vue-wswg-editor:layouts");
184
+
185
+ // Filter to only layouts from the active theme
186
+ const themeLayoutPath = `${activeTheme.path}/layout`;
187
+ const themeLayoutModules = Object.entries(layoutModules).filter(([path]) => path.startsWith(themeLayoutPath));
188
+
189
+ // Process only the active theme's layouts
190
+ for (const [, module] of themeLayoutModules) {
191
+ let resolvedModule = module;
192
+ // If module is a function (lazy-loaded), call it to get the actual module
193
+ if (typeof module === "function") {
194
+ resolvedModule = await module();
195
+ }
196
+ const layout = getModuleDefault(resolvedModule);
197
+ // exclude modules without name or label
198
+ if (!layout || !layout.label) {
199
+ continue;
200
+ }
201
+ // Mark layout component as raw to prevent Vue from making it reactive
202
+ themeLayouts.value[layout.__name] = markRaw(layout);
203
+ }
204
+ // If there are no layouts, warn
205
+ if (Object.keys(themeLayouts.value).length === 0) {
206
+ console.warn(`[vue-wswg-editor:registry] No layouts found for theme: ${activeTheme.id}`);
207
+ return;
208
+ }
209
+ }
210
+
211
+ export function getLayout(layoutType: string): Layout | undefined {
212
+ // Generate name variations and try to find a match
213
+ const nameVariations = generateNameVariations(layoutType);
214
+
215
+ for (const variation of nameVariations) {
216
+ const layout = themeLayouts.value[variation];
217
+ if (layout) {
218
+ return layout;
219
+ }
220
+ }
221
+
222
+ return undefined;
223
+ }
224
+
225
+ /**************************************************
226
+ * BLOCK FIELDS
227
+ **************************************************/
228
+ async function initialiseBlockFieldsRegistry(): Promise<void> {
229
+ // Clear existing registry
230
+ Object.keys(themeBlockFields.value).forEach((key) => {
231
+ delete themeBlockFields.value[key];
232
+ });
233
+
234
+ // Get the active theme
235
+ const activeTheme = getActiveTheme();
236
+
237
+ // Use virtual module to load all block fields (scans all themes at build time)
238
+ // Then filter to only process block fields from the active theme
239
+ const { modules: blockFieldsModules } = await import("vue-wswg-editor:fields");
240
+
241
+ // Filter to only block fields from the active theme
242
+ const themeBlockFieldsPath = `${activeTheme.path}/blocks`;
243
+ const themeBlockFieldsModules = Object.entries(blockFieldsModules).filter(([path]) =>
244
+ path.startsWith(themeBlockFieldsPath)
245
+ );
246
+
247
+ // Process only the active theme's block fields
248
+ for (const [path, module] of themeBlockFieldsModules) {
249
+ let resolvedModule = module;
250
+ // If module is a function (lazy-loaded), call it to get the actual module
251
+ if (typeof module === "function") {
252
+ resolvedModule = await module();
253
+ }
254
+ const blockFields = getModuleDefault(resolvedModule);
255
+ // Mark block fields component as raw to prevent Vue from making it reactive
256
+ themeBlockFields.value[path] = markRaw(blockFields);
257
+ }
258
+ }
259
+
260
+ function getBlockFieldsFile(path: string): Record<string, EditorFieldConfig> {
261
+ const fieldsPath = `${path}/fields.ts`;
262
+ try {
263
+ // Generate path for fields.ts file
264
+ const blockField = themeBlockFields.value[fieldsPath];
265
+ if (blockField) {
266
+ return blockField;
267
+ }
268
+ return {};
269
+ } catch (error) {
270
+ console.error("Error getting block fields for block: ", fieldsPath, error);
271
+ return {};
272
+ }
273
+ }
274
+
275
+ /**************************************************
276
+ * BLOCKS
277
+ **************************************************/
278
+ export async function initialiseBlockRegistry(): Promise<void> {
279
+ // Clear existing registry
280
+ Object.keys(themeBlocks.value).forEach((key) => {
281
+ delete themeBlocks.value[key];
282
+ });
283
+
284
+ // Get the active theme
285
+ const activeTheme = getActiveTheme();
286
+
287
+ // Use virtual module to load all layouts (scans all themes at build time)
288
+ // Then filter to only process layouts from the active theme
289
+ const { modules: blockModules } = await import("vue-wswg-editor:blocks");
290
+
291
+ // Filter to only layouts from the active theme
292
+ const themeBlockPath = `${activeTheme.path}/blocks`;
293
+ const themeBlockModules = Object.entries(blockModules).filter(([path]) => path.startsWith(themeBlockPath));
294
+
295
+ // Process only the active theme's layouts
296
+ for (const [path, module] of themeBlockModules) {
297
+ let resolvedModule = module;
298
+ // If module is a function (lazy-loaded), call it to get the actual module
299
+ if (typeof module === "function") {
300
+ resolvedModule = await module();
301
+ }
302
+ const blockModule = getModuleDefault(resolvedModule);
303
+ // exclude modules without name or label
304
+ if (!blockModule || !blockModule.__name) {
305
+ continue;
306
+ }
307
+
308
+ // Format the block
309
+ const blockType = toCamelCase(blockModule.type || blockModule.__name);
310
+ // Extract directory path from component path (e.g., "@page-builder/my-theme/blocks/hero-section/hero-section.vue" -> "@page-builder/my-theme/blocks/hero-section")
311
+ const directory = path.replace(/\/[^/]+\.vue$/, "");
312
+ // Mark the component itself as raw before spreading
313
+ const rawComponent = markRaw(blockModule);
314
+ const block: Block = {
315
+ fields: getBlockFieldsFile(directory), // Load the block fields file
316
+ ...rawComponent, // Component can override fields if defined directly
317
+ path: directory, // Path where the block component is located (e.g., "@page-builder/my-theme/blocks/hero-section")
318
+ type: blockType,
319
+ };
320
+
321
+ // Add the block to the registry
322
+ themeBlocks.value[blockType] = markRaw(block);
323
+ }
324
+ // If there are no blocks, warn
325
+ if (Object.keys(themeBlocks.value).length === 0) {
326
+ console.warn(`[vue-wswg-editor:registry] No blocks found for theme: ${activeTheme.id}`);
327
+ return;
328
+ }
329
+ }
330
+
331
+ export function getBlock(blockType: string): Block | undefined {
332
+ // Generate name variations and try to find a match
333
+ const nameVariations = generateNameVariations(blockType);
334
+
335
+ for (const variation of nameVariations) {
336
+ const block = themeBlocks.value[variation];
337
+ if (block) {
338
+ return block;
339
+ }
340
+ }
341
+
342
+ return undefined;
343
+ }
344
+
345
+ /**************************************************
346
+ * BLOCK THUMBNAILS
347
+ **************************************************/
348
+ async function initialiseBlockThumbnailsRegistry(): Promise<void> {
349
+ const { modules: thumbnailModules } = await import("vue-wswg-editor:thumbnails");
350
+ blockThumbnails = thumbnailModules;
351
+ }
352
+
353
+ export function getBlockThumbnail(blockDirectory: string): string | undefined {
354
+ if (!blockDirectory || !blockThumbnails) return undefined;
355
+ const thumbnailPath = `${blockDirectory}/thumbnail.png`;
356
+ const thumbnailModule = blockThumbnails[thumbnailPath];
357
+ if (!thumbnailModule) return undefined;
358
+ return (thumbnailModule as any).default as string | undefined;
359
+ }
360
+
361
+ /**************************************************
362
+ * REGISTRY COMPOSERS
363
+ **************************************************/
364
+
365
+ /** THEME BLOCK & FIELD REGISTRY */
366
+ async function initialiseThemeSubRegistries(useEditingRegistry?: boolean): Promise<void> {
367
+ // Load the layouts that belong to the active theme
368
+ await initialiseLayoutRegistry();
369
+ // Load the additional theme fields and thumbnails if in edit mode
370
+ if (useEditingRegistry) {
371
+ await initialiseBlockFieldsRegistry();
372
+ await initialiseBlockThumbnailsRegistry();
373
+ await initialiseThemeThumbnailsRegistry();
374
+ }
375
+ // Load the blocks that belong to the active theme
376
+ await initialiseBlockRegistry();
377
+ }
378
+
379
+ /**
380
+ * Initialize the registry with a specific theme
381
+ * @param themeId - The theme ID to load blocks/layouts from
382
+ */
383
+ export async function initialiseRegistry(themeId?: string, useEditingRegistry: boolean = true): Promise<void> {
384
+ try {
385
+ // First, initialize theme registry to discover all available themes
386
+ await initialiseThemeRegistry();
387
+
388
+ // Set the active theme ID
389
+ await setActiveTheme(themeId);
390
+
391
+ // Load the theme sub registries (layouts, blocks, fields)
392
+ await initialiseThemeSubRegistries(useEditingRegistry);
393
+ } catch (error) {
394
+ console.error("[vue-wswg-editor:registry] Error during registry initialization:", error);
395
+ throw error;
396
+ }
397
+ }
@@ -1,25 +1,116 @@
1
1
  import type { EditorFieldConfig, ValidatorFunction } from "./fieldConfig";
2
- import { getBlockComponent, getLayoutFields } from "./registry";
2
+ import { getBlock } from "./theme-registry";
3
+ import { getLayoutFields } from "./registry";
3
4
  import { toNiceName } from "./helpers";
4
5
 
5
- export function validateField(value: any, fieldConfig: EditorFieldConfig) {
6
+ export async function validateField(
7
+ value: any,
8
+ fieldConfig: EditorFieldConfig
9
+ ): Promise<boolean | string | ValidationResult> {
6
10
  // Create generic validator from field config properties (minLength, maxLength, etc.)
7
11
  const genericValidator = createGenericValidator(fieldConfig);
8
12
 
9
13
  // Combine generic validator with custom validator if provided
10
14
  const combinedValidator = combineValidators(genericValidator, fieldConfig.validator);
11
15
 
12
- // If no validator exists, clear any error message
13
- if (!combinedValidator) return true;
16
+ // Validate the field itself first
17
+ if (combinedValidator) {
18
+ const result = await combinedValidator(value);
19
+ if (result !== true) {
20
+ return result;
21
+ }
22
+ }
23
+
24
+ // Handle nested structures
25
+ // For repeater fields, validate each item's fields
26
+ if (fieldConfig.type === "repeater" && fieldConfig.repeaterFields && Array.isArray(value)) {
27
+ const nestedValidationResult: ValidationResult = {
28
+ title: fieldConfig.label || "Items",
29
+ isValid: true,
30
+ errors: {},
31
+ };
32
+
33
+ for (let i = 0; i < value.length; i++) {
34
+ const item = value[i];
35
+ if (!item) continue;
36
+
37
+ const itemValidationResult: ValidationResult = {
38
+ title: `Item ${i + 1}`,
39
+ isValid: true,
40
+ errors: {},
41
+ };
42
+
43
+ for (const fieldName of Object.keys(fieldConfig.repeaterFields)) {
44
+ const nestedFieldConfig: EditorFieldConfig | undefined = fieldConfig.repeaterFields[fieldName];
45
+ if (!nestedFieldConfig) continue;
46
+ const nestedValue = item[fieldName];
47
+ const nestedResult = await validateField(nestedValue, nestedFieldConfig);
48
+ if (nestedResult !== true) {
49
+ const fieldLabel = nestedFieldConfig.label || fieldName;
50
+ // If nested result is a ValidationResult, nest it; otherwise use as string
51
+ if (typeof nestedResult === "object" && "isValid" in nestedResult) {
52
+ itemValidationResult.errors[fieldLabel] = nestedResult;
53
+ } else {
54
+ itemValidationResult.errors[fieldLabel] = nestedResult;
55
+ }
56
+ itemValidationResult.isValid = false;
57
+ }
58
+ }
59
+
60
+ if (!itemValidationResult.isValid) {
61
+ nestedValidationResult.errors[`Item ${i + 1}`] = itemValidationResult;
62
+ nestedValidationResult.isValid = false;
63
+ }
64
+ }
65
+
66
+ if (!nestedValidationResult.isValid) {
67
+ return nestedValidationResult;
68
+ }
69
+ }
70
+
71
+ // For object fields, validate each nested field
72
+ if (
73
+ fieldConfig.type === "object" &&
74
+ fieldConfig.objectFields &&
75
+ value &&
76
+ typeof value === "object" &&
77
+ !Array.isArray(value)
78
+ ) {
79
+ const nestedValidationResult: ValidationResult = {
80
+ title: fieldConfig.label || "Object",
81
+ isValid: true,
82
+ errors: {},
83
+ };
84
+
85
+ for (const fieldName of Object.keys(fieldConfig.objectFields)) {
86
+ const nestedFieldConfig: EditorFieldConfig | undefined = fieldConfig.objectFields[fieldName];
87
+ if (!nestedFieldConfig) continue;
88
+ const nestedValue = value[fieldName];
89
+ const nestedResult = await validateField(nestedValue, nestedFieldConfig);
90
+ if (nestedResult !== true) {
91
+ const fieldLabel = nestedFieldConfig.label || fieldName;
92
+ // If nested result is a ValidationResult, nest it; otherwise use as string
93
+ if (typeof nestedResult === "object" && "isValid" in nestedResult) {
94
+ nestedValidationResult.errors[fieldLabel] = nestedResult;
95
+ } else {
96
+ nestedValidationResult.errors[fieldLabel] = nestedResult;
97
+ }
98
+ nestedValidationResult.isValid = false;
99
+ }
100
+ }
101
+
102
+ if (!nestedValidationResult.isValid) {
103
+ return nestedValidationResult;
104
+ }
105
+ }
14
106
 
15
- // Validate the value
16
- return combinedValidator(value);
107
+ return true;
17
108
  }
18
109
 
19
110
  export interface ValidationResult {
20
111
  title: string;
21
112
  isValid: boolean;
22
- errors: Record<string, string | boolean>;
113
+ errors: Record<string, string | boolean | ValidationResult>;
23
114
  }
24
115
 
25
116
  /**
@@ -43,8 +134,8 @@ export async function validateAllFields(
43
134
  }
44
135
 
45
136
  // Validate blocks
46
- // const blockResults = await validateBlocks(value, blocksKey);
47
- // Object.assign(validationResults, blockResults);
137
+ const blockResults = await validateBlocks(value, blocksKey);
138
+ Object.assign(validationResults, blockResults);
48
139
 
49
140
  return validationResults;
50
141
  }
@@ -63,7 +154,7 @@ async function validateSettings(value: any, settingsKey: string = "settings"): P
63
154
  const fieldConfig = layoutOptions[field];
64
155
  if (!fieldConfig) continue;
65
156
  const result = await validateField(value[settingsKey][field], fieldConfig);
66
- // If validation fails (returns false or a string), add to validation results
157
+ // If validation fails (returns false, string, or ValidationResult), add to validation results
67
158
  if (result !== true) {
68
159
  validationResult.errors[fieldConfig.label || field] = result;
69
160
  validationResult.isValid = false;
@@ -84,11 +175,11 @@ async function validateBlocks(value: any, blocksKey: string = "blocks"): Promise
84
175
  // Get the block type
85
176
  const blockType = block.type;
86
177
  // Get the block editor fields
87
- const blockComponent = getBlockComponent(blockType);
178
+ const blockComponent = getBlock(blockType);
88
179
 
89
180
  // Add validation results entry for the section
90
181
  validationResults[blockType] = {
91
- title: blockComponent.label || toNiceName(blockType),
182
+ title: blockComponent?.label || toNiceName(blockType),
92
183
  isValid: true,
93
184
  errors: {},
94
185
  };
@@ -104,7 +195,7 @@ async function validateBlocks(value: any, blocksKey: string = "blocks"): Promise
104
195
  if (!fieldConfig) continue;
105
196
  // Validate
106
197
  const result = await validateField(block[field], fieldConfig);
107
- // If validation fails (returns false or a string), add to validation results
198
+ // If validation fails (returns false, string, or ValidationResult), add to validation results
108
199
  if (result !== true) {
109
200
  validationResults[blockType].errors[fieldConfig.label || field] = result;
110
201
  validationResults[blockType].isValid = false;
@@ -15,6 +15,7 @@ export function vueWswgEditorPlugin(options: VueWswgEditorPluginOptions): Plugin
15
15
  fields: "\0vue-wswg-editor:fields",
16
16
  layouts: "\0vue-wswg-editor:layouts",
17
17
  thumbnails: "\0vue-wswg-editor:thumbnails",
18
+ themes: "\0vue-wswg-editor:themes",
18
19
  };
19
20
 
20
21
  return {
@@ -41,6 +42,7 @@ export function vueWswgEditorPlugin(options: VueWswgEditorPluginOptions): Plugin
41
42
  "vue-wswg-editor:blocks",
42
43
  "vue-wswg-editor:fields",
43
44
  "vue-wswg-editor:thumbnails",
45
+ "vue-wswg-editor:themes",
44
46
  ];
45
47
  for (const item of itemsToExclude) {
46
48
  if (!exclude.includes(item)) {
@@ -90,13 +92,15 @@ export function vueWswgEditorPlugin(options: VueWswgEditorPluginOptions): Plugin
90
92
  // Using a more explicit format to ensure Vite processes it correctly
91
93
  switch (id) {
92
94
  case virtualModules.layouts:
93
- return `export const modules = import.meta.glob("${options.rootDir}/layout/**/*.vue", { eager: true });`;
95
+ return `export const modules = import.meta.glob("${options.rootDir}/*/layout/**/*.vue", { eager: true });`;
94
96
  case virtualModules.blocks:
95
- return `export const modules = import.meta.glob("${options.rootDir}/blocks/**/*.vue", { eager: true });`;
97
+ return `export const modules = import.meta.glob("${options.rootDir}/*/blocks/**/*.vue", { eager: true });`;
96
98
  case virtualModules.fields:
97
- return `export const modules = import.meta.glob("${options.rootDir}/blocks/**/fields.ts", { eager: true });`;
99
+ return `export const modules = import.meta.glob("${options.rootDir}/*/blocks/**/fields.ts", { eager: true });`;
98
100
  case virtualModules.thumbnails:
99
- return `export const modules = import.meta.glob("${options.rootDir}/blocks/**/thumbnail.png", { eager: true });`;
101
+ return `export const modules = import.meta.glob(["${options.rootDir}/*/blocks/**/thumbnail.png", "${options.rootDir}/*/thumbnail.jpg", "${options.rootDir}/*/thumbnail.png"], { eager: true });`;
102
+ case virtualModules.themes:
103
+ return `export const modules = import.meta.glob("${options.rootDir}/**/theme.config.js", { eager: true });`;
100
104
  default:
101
105
  return undefined;
102
106
  }
@@ -159,3 +159,7 @@ declare module "vue-wswg-editor:fields" {
159
159
  declare module "vue-wswg-editor:thumbnails" {
160
160
  export const modules: Record<string, () => Promise<any>>;
161
161
  }
162
+
163
+ declare module "vue-wswg-editor:themes" {
164
+ export const modules: Record<string, () => Promise<any>>;
165
+ }
@@ -1,9 +0,0 @@
1
- // Alternative 1: Use relative paths instead of aliases
2
- // This works because the consuming app processes this file
3
- // The path is relative to where the consuming app's vite.config.ts is located
4
- // For participant/admin projects, this resolves to ../../page-builder/blocks/**/*.vue
5
- export const blockModules = import.meta.glob("../../page-builder/blocks/**/*.vue", { eager: true });
6
-
7
- // However, this is fragile because it depends on the file location relative to the consuming app
8
- // Better: Use a build-time script (see Alternative 2)
9
-
@@ -1,19 +0,0 @@
1
- // Alternative 4: Manual explicit imports
2
- // Most explicit, but requires updating when adding new blocks
3
-
4
- import HeroSection from "../../../../page-builder/blocks/hero-section/hero-section.vue";
5
- import CardsSection from "../../../../page-builder/blocks/cards-section/cards-section.vue";
6
- import FaqSection from "../../../../page-builder/blocks/faq-section/faq-section.vue";
7
- import FlexibleContent from "../../../../page-builder/blocks/flexible-content/flexible-content.vue";
8
- import TabsSection from "../../../../page-builder/blocks/tabs-section/tabs-section.vue";
9
- import TextSection from "../../../../page-builder/blocks/text-section/text-section.vue";
10
-
11
- export const blockModules = {
12
- "hero-section": HeroSection,
13
- "cards-section": CardsSection,
14
- "faq-section": FaqSection,
15
- "flexible-content": FlexibleContent,
16
- "tabs-section": TabsSection,
17
- "text-section": TextSection,
18
- };
19
-
@@ -1,23 +0,0 @@
1
- // Alternative 3: Runtime registration pattern
2
- // Components register themselves when imported
3
-
4
- import { ref, type Ref } from "vue";
5
- import type { Component } from "vue";
6
-
7
- // Registry store
8
- export const blockRegistry: Ref<Record<string, Component>> = ref({});
9
-
10
- // Registration function
11
- export function registerBlock(type: string, component: Component) {
12
- blockRegistry.value[type] = component;
13
- }
14
-
15
- // In each block component, add:
16
- // import { registerBlock } from "./blockModules-runtime";
17
- // registerBlock("hero-section", defineComponent({ ... }));
18
-
19
- // Then manually import blocks:
20
- // import "./blocks/hero-section/hero-section.vue";
21
- // import "./blocks/cards-section/cards-section.vue";
22
- // etc.
23
-