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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue-wswg-editor",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "type": "module",
5
5
  "main": "./dist/vue-wswg-editor.es.js",
6
6
  "module": "./dist/vue-wswg-editor.es.js",
@@ -88,7 +88,6 @@
88
88
  "typescript": "~5.8.3",
89
89
  "vite": "^5.4.19",
90
90
  "vitepress": "^1.6.4",
91
- "vitepress-demo-plugin": "^1.5.1",
92
91
  "vitest": "^2.1.9",
93
92
  "vue-tsc": "^2.2.12",
94
93
  "yup": "^1.7.1"
@@ -66,3 +66,18 @@
66
66
  content: none;
67
67
  }
68
68
  }
69
+
70
+ // Apply Overlay Styles (Helper)
71
+ // Applies the backdrop styles directly - useful when state classes are on parent
72
+ // Uses CSS custom properties from .wswg-page-builder element
73
+ // Usage: @include backdrop-apply($backdrop-color, $border-color);
74
+ @mixin overlay-apply(
75
+ $backdrop-color: var(--block-backdrop-color, transparent),
76
+ $border-color: var(--block-border-color, #638ef1),
77
+ $border-width: var(--block-border-width, 4px),
78
+ $border-style: var(--block-border-style, solid)
79
+ ) {
80
+ outline: $border-width $border-style $border-color;
81
+ outline-offset: calc(-1 * var(--block-border-width, 2px));
82
+ box-shadow: inset 0 0 0 9999px $backdrop-color;
83
+ }
@@ -29,7 +29,7 @@
29
29
  <script setup lang="ts">
30
30
  import { computed, defineProps, ref } from "vue";
31
31
  import type { Block } from "../../types/Block";
32
- import { getBlockThumbnailUrl } from "../../util/registry";
32
+ import { getBlockThumbnail } from "../../util/theme-registry";
33
33
  import { CubeTransparentIcon } from "@heroicons/vue/24/outline";
34
34
 
35
35
  const props = defineProps<{
@@ -38,13 +38,22 @@ const props = defineProps<{
38
38
 
39
39
  const thumbnailError = ref<boolean>(false);
40
40
  const thumbnailUrl = computed(() => {
41
- return getBlockThumbnailUrl(props.block.directory);
41
+ return getBlockThumbnail(props.block.path);
42
42
  });
43
43
 
44
44
  function handleDragStart(event: DragEvent, block: Block) {
45
45
  if (!event.dataTransfer) return;
46
- // Store the block type in dataTransfer
47
- event.dataTransfer.setData("block-type", block.__name);
46
+ // Store the block type in dataTransfer (use block.type as fallback if __name not available)
47
+ const blockType = block.__name || block.type;
48
+ event.dataTransfer.setData("block-type", blockType);
48
49
  event.dataTransfer.effectAllowed = "move";
50
+ // Allow dragging to iframes
51
+ event.dataTransfer.setData("text/plain", blockType);
52
+
53
+ // Check if the dragged element has data-prevent-drop attribute
54
+ const draggedElement = event.target as HTMLElement;
55
+ if (draggedElement?.hasAttribute("data-prevent-drop")) {
56
+ event.dataTransfer.setData("prevent-drop", "true");
57
+ }
49
58
  }
50
59
  </script>
@@ -28,15 +28,15 @@
28
28
 
29
29
  <script setup lang="ts">
30
30
  import { computed, onMounted, ref } from "vue";
31
- import { pageBuilderBlocks } from "../../util/registry";
31
+ import { themeBlocks } from "../../util/theme-registry";
32
32
  import AddBlockItem from "../AddBlockItem/AddBlockItem.vue";
33
33
  import type { Block } from "../../types/Block";
34
34
  import Sortable from "sortablejs";
35
35
 
36
36
  const blockSearch = ref("");
37
37
  const filteredBlocks = computed(() => {
38
- if (!pageBuilderBlocks.value) return [];
39
- return Object.values(pageBuilderBlocks.value).filter((block: Block) => {
38
+ if (!themeBlocks.value) return [];
39
+ return Object.values(themeBlocks.value).filter((block: Block) => {
40
40
  // against block name and label
41
41
  return (
42
42
  block.type?.toLowerCase().includes(blockSearch.value.toLowerCase()) ||
@@ -46,8 +46,8 @@ const filteredBlocks = computed(() => {
46
46
  });
47
47
 
48
48
  const blockCount = computed(() => {
49
- if (!pageBuilderBlocks.value) return 0;
50
- return Object.values(pageBuilderBlocks.value).length;
49
+ if (!themeBlocks.value) return 0;
50
+ return Object.values(themeBlocks.value).length;
51
51
  });
52
52
 
53
53
  function initSortable() {
@@ -10,19 +10,19 @@
10
10
  :data-block-id="block.id"
11
11
  >
12
12
  <div
13
- v-if="activeBlock?.id === block.id"
14
- class="absolute -top-3 right-4 z-10 rounded-full bg-blue-500 px-2 py-1 text-xs text-white"
15
- >
16
- <p>Editing</p>
17
- </div>
18
- <div
19
- v-if="pageBuilderBlocks[toCamelCase(block.type)]"
13
+ v-if="blockComponent"
20
14
  class="block-component"
21
15
  @mouseenter="emit('hoverBlock', block.id)"
22
16
  @mouseleave="emit('hoverBlock', null)"
23
17
  @click="emit('clickBlock', block)"
24
18
  >
25
- <component :is="pageBuilderBlocks[toCamelCase(block.type)]" v-bind="block" ref="blockComponentRef" />
19
+ <div
20
+ v-if="activeBlock?.id === block.id"
21
+ class="editing-badge absolute right-2 top-2 z-40 rounded-full px-2 py-1 text-xs text-white"
22
+ >
23
+ <span>Editing</span>
24
+ </div>
25
+ <component :is="blockComponent" v-bind="block" ref="blockComponentRef" />
26
26
  </div>
27
27
  <div
28
28
  v-else
@@ -41,9 +41,8 @@
41
41
  </template>
42
42
 
43
43
  <script setup lang="ts">
44
- import { useTemplateRef } from "vue";
45
- import { onClickOutside } from "@vueuse/core";
46
- import { pageBuilderBlocks } from "../../util/registry";
44
+ import { computed, useTemplateRef } from "vue";
45
+ import { getBlock } from "../../util/theme-registry";
47
46
  import type { Block } from "../../types/Block";
48
47
  import { toCamelCase } from "../../util/helpers";
49
48
  import { onKeyStroke } from "@vueuse/core";
@@ -62,6 +61,10 @@ const props = defineProps<{
62
61
 
63
62
  const blockComponentRef = useTemplateRef<HTMLElement | null>("blockComponentRef");
64
63
 
64
+ const blockComponent = computed<Block | undefined>(() => {
65
+ return getBlock(props.block.type) || undefined;
66
+ });
67
+
65
68
  // Get the margin class for the block
66
69
  // Margin is an object with top and bottom properties
67
70
  // margin classses are formatted as `margin-<direction>-<size>`
@@ -78,16 +81,6 @@ function getMarginClass(block: Block): string {
78
81
  return [getClass(top, "top"), getClass(bottom, "bottom")].join(" ");
79
82
  }
80
83
 
81
- // Click outside detection
82
- onClickOutside(
83
- blockComponentRef,
84
- () => {
85
- // Unset active block
86
- emit("clickBlock", null);
87
- },
88
- { ignore: ["#page-builder-sidebar", "#page-builder-resize-handle", "button"] }
89
- );
90
-
91
84
  // On escape key press
92
85
  onKeyStroke("Escape", () => {
93
86
  if (!props.activeBlock) return;
@@ -117,8 +110,8 @@ onKeyStroke("Escape", () => {
117
110
  font-weight: 500;
118
111
  color: #888017;
119
112
  content: "Spacing";
120
- background-color: #f7efac;
121
- border: 2px dashed #d2c564;
113
+ background-color: var(--margin-color, #faf6d5e0);
114
+ border: 2px dashed var(--margin-border-color, #cbc59c);
122
115
  border-radius: 7px;
123
116
  opacity: 0;
124
117
  transform: scaleY(0.9) scaleX(0.98);
@@ -128,32 +121,16 @@ onKeyStroke("Escape", () => {
128
121
  .block-component {
129
122
  position: relative;
130
123
  cursor: pointer;
124
+ }
131
125
 
132
- // Highlight block overlay
133
- &::before {
134
- position: absolute;
135
- top: 0;
136
- left: 0;
137
- z-index: 2;
138
- width: 100%;
139
- height: 100%;
140
- pointer-events: none;
141
- outline: 2px dashed #638ef1;
142
- outline-offset: -2px;
143
- content: "";
144
- background-color: #9fd0f643;
145
- opacity: 0;
146
- }
126
+ .editing-badge {
127
+ background-color: var(--block-badge-color, #638ef1);
147
128
  }
148
129
 
149
- // Active state
130
+ // Active state - apply overlay and show margin spacing
150
131
  &.active-block {
151
132
  .block-component {
152
- &::before {
153
- background-color: #9fd0f643;
154
- border-color: #638ef1;
155
- opacity: 1;
156
- }
133
+ @include overlay-apply;
157
134
  }
158
135
 
159
136
  // Show the margin spacing overlay
@@ -163,14 +140,10 @@ onKeyStroke("Escape", () => {
163
140
  }
164
141
  }
165
142
 
166
- // Hovered state
143
+ // Hovered state - apply overlay and show margin spacing
167
144
  &.hovered-block {
168
145
  .block-component {
169
- &::before {
170
- background-color: #9fd0f643;
171
- border-color: #638ef1;
172
- opacity: 1;
173
- }
146
+ @include overlay-apply;
174
147
  }
175
148
 
176
149
  // Show the margin spacing overlay
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <!-- Field -->
3
- <div class="editor-field-node" :class="{ 'border-red-500': validationErrorMessage }">
3
+ <div class="editor-field-node">
4
4
  <div class="mb-1 flex items-center gap-1.5">
5
5
  <!-- Label -->
6
6
  <label class="mr-auto text-sm font-medium first-letter:uppercase">
@@ -121,7 +121,7 @@
121
121
  <template v-else-if="fieldConfig.type === 'boolean'">
122
122
  <label
123
123
  :for="fieldName"
124
- class="has-checked:bg-emerald-700 w-13 relative block h-7 cursor-pointer rounded-full bg-gray-300 transition-colors [-webkit-tap-highlight-color:transparent] hover:bg-gray-400/75"
124
+ class="has-checked:bg-emerald-700 w-13 relative block h-7 cursor-pointer rounded-full bg-gray-300 transition-colors [-webkit-tap-highlight-color:transparent] hover:bg-gray-400/75 has-[:checked]:hover:bg-emerald-800"
125
125
  >
126
126
  <input :id="fieldName" v-model="fieldValue" type="checkbox" class="peer sr-only" :disabled="!editable" />
127
127
 
@@ -177,8 +177,11 @@
177
177
  </template>
178
178
 
179
179
  <!-- Validation error message -->
180
- <div v-if="validationErrorMessage" class="rounded-sm bg-red-50 p-2 px-3 text-xs text-red-600">
181
- {{ validationErrorMessage }}
180
+ <div
181
+ v-if="validationError && typeof validationError === 'string'"
182
+ class="rounded-sm bg-red-50 p-2 px-3 text-xs text-red-600"
183
+ >
184
+ {{ validationError }}
182
185
  </div>
183
186
  </div>
184
187
  </template>
@@ -191,7 +194,7 @@ import BlockMarginNode from "../BlockMarginFieldNode/BlockMarginNode.vue";
191
194
  import BlockImageNode from "../BlockImageFieldNode/BlockImageNode.vue";
192
195
  import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
193
196
  import { InformationCircleIcon } from "@heroicons/vue/24/outline";
194
- import { validateField as validateFieldUtil } from "../../util/validation";
197
+ import { validateField as validateFieldUtil, type ValidationResult } from "../../util/validation";
195
198
  import * as yup from "yup";
196
199
 
197
200
  const fieldValue = defineModel<any>();
@@ -202,7 +205,7 @@ const props = defineProps<{
202
205
  editable: boolean;
203
206
  }>();
204
207
 
205
- const validationErrorMessage = ref<string | null>(null);
208
+ const validationError = ref<string | null | ValidationResult>(null);
206
209
 
207
210
  // Computed property for checkbox values (array of selected values)
208
211
  const checkboxValues = computed({
@@ -301,21 +304,20 @@ async function validateField(): Promise<void> {
301
304
  const result = await validateFieldUtil(fieldValue.value, props.fieldConfig);
302
305
  // True = valid, false = invalid, string = error message
303
306
  if (result === true) {
304
- validationErrorMessage.value = null;
307
+ validationError.value = null;
305
308
  } else {
306
309
  // result is either false or a string error message
307
310
  // TODO: i18n
308
- validationErrorMessage.value = result === false ? "Field is invalid" : result;
311
+ validationError.value = result === false ? "Field is invalid" : result;
309
312
  }
310
313
  } catch (error) {
311
314
  const validationErrorsList = error as yup.ValidationError;
312
- validationErrorMessage.value = validationErrorsList.errors[0] || "Field is invalid";
315
+ validationError.value = validationErrorsList.errors[0] || "Field is invalid";
313
316
  }
314
317
  }
315
318
 
316
319
  function clearFieldValue(): void {
317
320
  fieldValue.value = null;
318
- validationErrorMessage.value = null;
319
321
  }
320
322
 
321
323
  // Watch the field value and validate it
@@ -3,7 +3,7 @@
3
3
  <!-- Field group tabs-->
4
4
  <div
5
5
  v-if="editorFieldGroups.length"
6
- class="field-group-tabs flex gap-2 border-b border-gray-300"
6
+ class="field-group-tabs flex gap-2 overflow-x-auto border-b border-gray-300"
7
7
  :class="nested ? 'px-0 pt-0' : 'px-5 pt-3'"
8
8
  >
9
9
  <button
@@ -51,11 +51,12 @@ import type { EditorFieldConfig } from "../../util/fieldConfig";
51
51
 
52
52
  const blockData = defineModel<any>();
53
53
  const activeFieldGroup = ref<string>("");
54
- const { fields, editable } = defineProps<{
54
+ const { fields, editable, activeTab } = defineProps<{
55
55
  fields?: Record<string, EditorFieldConfig>;
56
56
  editable: boolean;
57
57
  isLayoutBlock?: boolean;
58
58
  nested?: boolean;
59
+ activeTab?: string;
59
60
  }>();
60
61
 
61
62
  const editorFields = computed(() => {
@@ -88,13 +89,32 @@ const editorFieldGroups = computed(() => {
88
89
 
89
90
  watch(editorFieldGroups, () => {
90
91
  if (editorFieldGroups.value.length > 0) {
91
- activeFieldGroup.value = editorFieldGroups.value[0] || "";
92
+ // If activeTab prop is provided and exists in groups, use it; otherwise use first group
93
+ if (activeTab && editorFieldGroups.value.includes(activeTab)) {
94
+ activeFieldGroup.value = activeTab;
95
+ } else {
96
+ activeFieldGroup.value = editorFieldGroups.value[0] || "";
97
+ }
92
98
  }
93
99
  });
94
100
 
101
+ watch(
102
+ () => activeTab,
103
+ (newTab) => {
104
+ if (newTab && editorFieldGroups.value.includes(newTab)) {
105
+ activeFieldGroup.value = newTab;
106
+ }
107
+ }
108
+ );
109
+
95
110
  onBeforeMount(() => {
96
111
  if (editorFieldGroups.value.length > 0) {
97
- activeFieldGroup.value = editorFieldGroups.value[0] || "";
112
+ // If activeTab prop is provided and exists in groups, use it; otherwise use first group
113
+ if (activeTab && editorFieldGroups.value.includes(activeTab)) {
114
+ activeFieldGroup.value = activeTab;
115
+ } else {
116
+ activeFieldGroup.value = editorFieldGroups.value[0] || "";
117
+ }
98
118
  }
99
119
  });
100
120
  </script>
@@ -127,10 +127,15 @@ const openRepeaterItems = ref<string[]>([]);
127
127
 
128
128
  // Ensure fieldValue is always an array with id attribute
129
129
  onBeforeMount(() => {
130
- fieldValue.value = fieldValue.value.map((item) => ({
131
- id: item.id || crypto.randomUUID(),
132
- ...item,
133
- }));
130
+ // Ensure fieldValue is an array before calling map
131
+ if (!Array.isArray(fieldValue.value)) {
132
+ fieldValue.value = [];
133
+ }
134
+ fieldValue.value =
135
+ fieldValue.value?.map((item) => ({
136
+ id: item.id || crypto.randomUUID(),
137
+ ...item,
138
+ })) ?? [];
134
139
  });
135
140
 
136
141
  function addItem() {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <!-- URL bar -->
3
3
  <div class="browser-navigation-bar">
4
- <div class="flex items-center justify-between rounded-t-lg bg-zinc-600 px-5 py-4">
4
+ <div class="flex items-center justify-between bg-zinc-600 px-5 py-4">
5
5
  <div class="flex w-full items-center gap-2 rounded-md bg-zinc-700 px-4 py-1.5 text-sm text-zinc-300">
6
6
  <span class="block flex-1 truncate">{{ url }}</span>
7
7