vue-wswg-editor 0.0.12 → 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 (60) hide show
  1. package/README.md +23 -8
  2. package/dist/style.css +1 -1
  3. package/dist/types/components/BlockEditorFields/BlockEditorFields.vue.d.ts +1 -0
  4. package/dist/types/components/EditorPageRenderer/EditorPageRenderer.vue.d.ts +21 -0
  5. package/dist/types/components/EmptyState/EmptyState.vue.d.ts +2 -8
  6. package/dist/types/components/IframePreview/IframePreview.vue.d.ts +26 -0
  7. package/dist/types/components/IframePreview/iframeContent.d.ts +9 -0
  8. package/dist/types/components/IframePreview/iframePreviewApp.d.ts +36 -0
  9. package/dist/types/components/IframePreview/messageHandler.d.ts +55 -0
  10. package/dist/types/components/IframePreview/types.d.ts +77 -0
  11. package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +2 -0
  12. package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +2 -0
  13. package/dist/types/components/PageSettings/PageSettings.vue.d.ts +2 -0
  14. package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.vue.d.ts → WswgPageBuilder/WswgPageBuilder.vue.d.ts} +2 -0
  15. package/dist/types/index.d.ts +8 -2
  16. package/dist/types/util/registry.d.ts +2 -0
  17. package/dist/types/util/theme-registry.d.ts +42 -0
  18. package/dist/types/util/validation.d.ts +2 -2
  19. package/dist/vite-plugin.js +33 -29
  20. package/dist/vue-wswg-editor.es.js +2723 -1897
  21. package/package.json +1 -2
  22. package/src/assets/styles/_mixins.scss +15 -0
  23. package/src/components/AddBlockItem/AddBlockItem.vue +13 -4
  24. package/src/components/BlockBrowser/BlockBrowser.vue +5 -5
  25. package/src/components/BlockComponent/BlockComponent.vue +23 -50
  26. package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +12 -10
  27. package/src/components/BlockEditorFields/BlockEditorFields.vue +24 -4
  28. package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +9 -4
  29. package/src/components/BrowserNavigation/BrowserNavigation.vue +1 -1
  30. package/src/components/EditorPageRenderer/EditorPageRenderer.vue +641 -0
  31. package/src/components/EmptyState/EmptyState.vue +3 -12
  32. package/src/components/IframePreview/IframePreview.vue +211 -0
  33. package/src/components/IframePreview/iframeContent.ts +210 -0
  34. package/src/components/IframePreview/iframePreviewApp.ts +221 -0
  35. package/src/components/IframePreview/messageHandler.ts +219 -0
  36. package/src/components/IframePreview/types.ts +126 -0
  37. package/src/components/PageBlockList/PageBlockList.vue +8 -6
  38. package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +5 -3
  39. package/src/components/PageRenderer/PageRenderer.vue +9 -33
  40. package/src/components/PageSettings/PageSettings.vue +10 -6
  41. package/src/components/ResizeHandle/ResizeHandle.vue +68 -10
  42. package/src/components/{WswgJsonEditor/WswgJsonEditor.test.ts → WswgPageBuilder/WswgPageBuilder.test.ts} +8 -8
  43. package/src/components/WswgPageBuilder/WswgPageBuilder.vue +375 -0
  44. package/src/index.ts +10 -2
  45. package/src/shims.d.ts +4 -0
  46. package/src/types/Theme.d.ts +15 -0
  47. package/src/util/registry.ts +2 -2
  48. package/src/util/theme-registry.ts +397 -0
  49. package/src/util/validation.ts +102 -11
  50. package/src/vite-plugin.ts +8 -4
  51. package/types/vue-wswg-editor.d.ts +4 -0
  52. package/dist/types/components/PageRenderer/blockModules.d.ts +0 -3
  53. package/dist/types/components/PageRenderer/layoutModules.d.ts +0 -3
  54. package/src/components/PageRenderer/blockModules-alternative.ts.example +0 -9
  55. package/src/components/PageRenderer/blockModules-manual.ts.example +0 -19
  56. package/src/components/PageRenderer/blockModules-runtime.ts.example +0 -23
  57. package/src/components/PageRenderer/blockModules.ts +0 -32
  58. package/src/components/PageRenderer/layoutModules.ts +0 -32
  59. package/src/components/WswgJsonEditor/WswgJsonEditor.vue +0 -595
  60. /package/dist/types/components/{WswgJsonEditor/WswgJsonEditor.test.d.ts → WswgPageBuilder/WswgPageBuilder.test.d.ts} +0 -0
@@ -0,0 +1,641 @@
1
+ <template>
2
+ <div
3
+ id="page-viewport"
4
+ ref="editorRef"
5
+ class="page-renderer-wrapper relative"
6
+ :class="{ 'settings-open': settingsOpen }"
7
+ >
8
+ <template v-if="isReady">
9
+ <component :is="layoutComponent" v-if="layoutComponent" v-bind="settings" :blocks="blocks">
10
+ <template #default>
11
+ <!-- No blocks found -->
12
+ <EmptyState v-if="!blocks?.length" :editable="editable" @block-added="handleBlockAdded" />
13
+ <!-- Blocks found -->
14
+ <div
15
+ v-else
16
+ id="page-blocks-wrapper"
17
+ ref="pageBlocksWrapperRef"
18
+ class="relative"
19
+ :class="{ 'drag-over': isDraggingOver }"
20
+ @dragenter="handleDragEnter"
21
+ @dragleave="handleDragLeave"
22
+ @dragover="handleDragOver"
23
+ @drop="handleDrop"
24
+ >
25
+ <!-- Drop indicator - positioned absolutely based on calculated position -->
26
+ <div
27
+ v-if="isDraggingOver && dropInsertIndex !== null && dropIndicatorTop !== null"
28
+ class="drop-indicator"
29
+ :style="{ top: dropIndicatorTop + 'px' }"
30
+ >
31
+ <div class="drop-indicator-line"></div>
32
+ <div class="drop-indicator-label">Drop here</div>
33
+ <div class="drop-indicator-line"></div>
34
+ </div>
35
+
36
+ <template v-for="(block, blockIndex) in blocks" :key="block.id">
37
+ <div>
38
+ <BlockComponent
39
+ :block="block"
40
+ :blockIndex="blockIndex"
41
+ :activeBlock="activeBlock"
42
+ :editable="editable"
43
+ :hoveredBlockId="hoveredBlockId"
44
+ @hover-block="setHoveredBlockId"
45
+ @click-block="handleBlockClick"
46
+ />
47
+ </div>
48
+ </template>
49
+ </div>
50
+ </template>
51
+ </component>
52
+ <!-- No layout found -->
53
+ <div v-else class="rounded-b-lg bg-white px-5 py-12 md:py-20">
54
+ <div class="mx-auto max-w-md pb-7 text-center">
55
+ <svg
56
+ xmlns="http://www.w3.org/2000/svg"
57
+ fill="none"
58
+ viewBox="0 0 24 24"
59
+ stroke-width="1.5"
60
+ stroke="currentColor"
61
+ class="mx-auto size-20 text-gray-400"
62
+ >
63
+ <path
64
+ stroke-linecap="round"
65
+ stroke-linejoin="round"
66
+ d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
67
+ ></path>
68
+ </svg>
69
+
70
+ <h2 class="text-2xl font-bold text-gray-900">No layout found</h2>
71
+
72
+ <p class="mt-4 text-pretty text-gray-700">
73
+ Get started by creating your first layout. It only takes a few seconds.
74
+ </p>
75
+
76
+ <p class="mt-6 text-sm text-gray-700">
77
+ <a href="#" class="underline hover:text-gray-900">Learn how</a> or
78
+ <a href="#" class="underline hover:text-gray-900">view examples</a>
79
+ </p>
80
+ </div>
81
+ </div>
82
+ </template>
83
+ </div>
84
+ </template>
85
+
86
+ <script setup lang="ts">
87
+ import { computed, withDefaults, onBeforeMount, ref, onBeforeUnmount, onMounted, watch, nextTick } from "vue";
88
+ import { initialiseRegistry, getLayout } from "../../util/theme-registry";
89
+ import BlockComponent from "../BlockComponent/BlockComponent.vue";
90
+ import EmptyState from "../EmptyState/EmptyState.vue";
91
+ import type { Block } from "../../types/Block";
92
+ import type { PartialClickMessage } from "../IframePreview/types";
93
+ import Sortable from "sortablejs";
94
+
95
+ const props = withDefaults(
96
+ defineProps<{
97
+ blocks: Block[];
98
+ layout?: string;
99
+ settings?: Record<string, any>;
100
+ activeBlock?: Block | null;
101
+ hoveredBlockId?: string | null;
102
+ editable?: boolean;
103
+ settingsOpen?: boolean;
104
+ theme?: string;
105
+ }>(),
106
+ {
107
+ layout: "default",
108
+ settings: () => ({}),
109
+ activeBlock: null,
110
+ hoveredBlockId: null,
111
+ editable: false,
112
+ settingsOpen: false,
113
+ theme: "default",
114
+ }
115
+ );
116
+
117
+ const isReady = ref(false);
118
+ const editorRef = ref<HTMLElement | null>(null);
119
+ const isSorting = ref(false);
120
+ const sortableInstance = ref<InstanceType<typeof Sortable> | null>(null);
121
+ const isInitializingSortable = ref(false);
122
+
123
+ // Get the layout component based on the layout prop
124
+ // Only compute when isReady is true (registry is initialized)
125
+ const layoutComponent = computed(() => {
126
+ if (!isReady.value) {
127
+ return undefined;
128
+ }
129
+ return getLayout(props.layout);
130
+ });
131
+
132
+ function handleBlockClick(block: Block | null) {
133
+ sendToParent({
134
+ type: "BLOCK_CLICK",
135
+ block: block,
136
+ });
137
+ }
138
+
139
+ function setHoveredBlockId(blockId: string | null) {
140
+ sendToParent({
141
+ type: "BLOCK_HOVER",
142
+ blockId: blockId,
143
+ });
144
+ }
145
+
146
+ function handleBlockAdded(blockType: string) {
147
+ sendToParent({
148
+ type: "BLOCK_ADD",
149
+ blockType: blockType,
150
+ });
151
+ }
152
+
153
+ // Serialize data for postMessage (handles Vue reactive proxies)
154
+ function serializeForPostMessage(data: any): any {
155
+ try {
156
+ return JSON.parse(JSON.stringify(data));
157
+ } catch (error) {
158
+ console.warn("[iframe] Failed to serialize data for postMessage:", error);
159
+ return undefined;
160
+ }
161
+ }
162
+
163
+ // Message handler
164
+ function sendToParent(message: any) {
165
+ if (window.parent) {
166
+ // Serialize message to handle Vue reactive proxies
167
+ const serializedMessage = serializeForPostMessage(message);
168
+ if (!serializedMessage) {
169
+ console.error("[iframe] Failed to serialize message for postMessage");
170
+ return;
171
+ }
172
+ window.parent.postMessage(serializedMessage, "*");
173
+ } else {
174
+ console.error("[iframe] No parent window found");
175
+ }
176
+ }
177
+
178
+ /** PARTIALS HANDLING */
179
+ // Handle clicks on elements with data-partial attribute
180
+ let dataPartialClickHandler: ((event: Event) => void) | null = null;
181
+
182
+ function setupDataPartialClickHandler() {
183
+ if (!editorRef.value) {
184
+ console.error("[iframe] EditorPageRenderer: No editor reference found");
185
+ return;
186
+ }
187
+
188
+ dataPartialClickHandler = (event: Event) => {
189
+ const target = event.target as HTMLElement;
190
+ const partialElement = target.closest("[data-partial]") as HTMLElement | null;
191
+
192
+ if (partialElement) {
193
+ sendToParent({
194
+ type: "CLICK_PARTIAL",
195
+ partial: partialElement.getAttribute("data-partial"),
196
+ } as PartialClickMessage);
197
+ }
198
+ };
199
+
200
+ editorRef.value.addEventListener("click", dataPartialClickHandler);
201
+ }
202
+
203
+ /** SORTABLE HANDLING */
204
+ const pageBlocksWrapperRef = ref<HTMLElement | null>(null);
205
+
206
+ /** DRAG AND DROP HANDLING - Cross-iframe drag support */
207
+ const isDraggingOver = ref(false);
208
+ const dropInsertIndex = ref<number | null>(null);
209
+ const dropIndicatorTop = ref<number | null>(null);
210
+
211
+ function handleDragEnter(event: DragEvent) {
212
+ // Skip if SortableJS is handling the drag (reordering existing blocks)
213
+ if (isSorting.value) {
214
+ return;
215
+ }
216
+
217
+ // Check if SortableJS is dragging (has sortable drag element)
218
+ const dragSource = document.querySelector(".sortable-drag, .sortable-ghost");
219
+ if (dragSource) {
220
+ // SortableJS is handling this drag, don't interfere
221
+ return;
222
+ }
223
+
224
+ // Check if this is a new block drag from sidebar (has block-type data)
225
+ if (event.dataTransfer?.types.includes("block-type")) {
226
+ isDraggingOver.value = true;
227
+ event.preventDefault();
228
+ event.stopPropagation();
229
+ }
230
+ }
231
+
232
+ function handleDragLeave(event: DragEvent) {
233
+ // Skip if SortableJS is handling the drag
234
+ if (isSorting.value) {
235
+ return;
236
+ }
237
+
238
+ const relatedTarget = event.relatedTarget as HTMLElement | null;
239
+ const currentTarget = event.currentTarget as HTMLElement | null;
240
+
241
+ // Only hide if we're actually leaving the drop zone
242
+ if (!currentTarget?.contains(relatedTarget)) {
243
+ isDraggingOver.value = false;
244
+ dropInsertIndex.value = null;
245
+ dropIndicatorTop.value = null;
246
+ }
247
+ }
248
+
249
+ function handleDragOver(event: DragEvent) {
250
+ // Skip if SortableJS is handling the drag (reordering existing blocks)
251
+ if (isSorting.value) {
252
+ return;
253
+ }
254
+
255
+ // Check if drag source is from within blocks wrapper (existing block being reordered)
256
+ const dragSource = document.querySelector(".sortable-drag, .sortable-ghost");
257
+ if (dragSource) {
258
+ // SortableJS is handling this drag, don't interfere
259
+ return;
260
+ }
261
+
262
+ // Check if this is a new block drag from sidebar (has block-type data)
263
+ if (!event.dataTransfer?.types.includes("block-type")) {
264
+ return;
265
+ }
266
+
267
+ // Allow drop
268
+ event.preventDefault();
269
+ event.stopPropagation();
270
+ if (event.dataTransfer) {
271
+ event.dataTransfer.dropEffect = "move";
272
+ }
273
+
274
+ // Calculate insertion position
275
+ if (!props.blocks || props.blocks.length === 0) {
276
+ dropInsertIndex.value = 0;
277
+ dropIndicatorTop.value = 0;
278
+ return;
279
+ }
280
+
281
+ const dropTarget = event.target as HTMLElement;
282
+ const blockElement = dropTarget.closest("[data-block-id]");
283
+ const wrapperElement = pageBlocksWrapperRef.value;
284
+
285
+ if (!wrapperElement) return;
286
+
287
+ if (blockElement) {
288
+ const blockId = blockElement.getAttribute("data-block-id");
289
+ if (blockId) {
290
+ const currentIndex = props.blocks.findIndex((b) => b.id === blockId);
291
+ if (currentIndex !== -1) {
292
+ // Determine if we should insert before or after based on mouse position
293
+ const blockRect = blockElement.getBoundingClientRect();
294
+ const wrapperRect = wrapperElement.getBoundingClientRect();
295
+ const mouseY = event.clientY;
296
+ const blockCenterY = blockRect.top + blockRect.height / 2;
297
+
298
+ if (mouseY < blockCenterY) {
299
+ // Insert before this block
300
+ dropInsertIndex.value = currentIndex;
301
+ dropIndicatorTop.value = blockRect.top - wrapperRect.top - 2;
302
+ } else {
303
+ // Insert after this block
304
+ dropInsertIndex.value = currentIndex + 1;
305
+ dropIndicatorTop.value = blockRect.bottom - wrapperRect.top - 2;
306
+ }
307
+ }
308
+ }
309
+ } else {
310
+ // Dragging over empty area - append to end
311
+ dropInsertIndex.value = props.blocks.length;
312
+ // Calculate position at the bottom of the wrapper
313
+ const wrapperRect = wrapperElement.getBoundingClientRect();
314
+ dropIndicatorTop.value = wrapperRect.height - 2;
315
+ }
316
+ }
317
+
318
+ function handleDrop(event: DragEvent) {
319
+ // Skip if SortableJS is handling the drop (reordering existing blocks)
320
+ if (isSorting.value) {
321
+ return;
322
+ }
323
+
324
+ // Check if this is a SortableJS drop (has sortable drag element)
325
+ const dragSource = document.querySelector(".sortable-drag, .sortable-ghost");
326
+ if (dragSource) {
327
+ // SortableJS is handling this drop, don't interfere
328
+ return;
329
+ }
330
+
331
+ // Check if this is a new block drag from sidebar (has block-type data)
332
+ if (!event.dataTransfer?.types.includes("block-type")) {
333
+ return;
334
+ }
335
+
336
+ event.preventDefault();
337
+ event.stopPropagation();
338
+
339
+ if (!event.dataTransfer) {
340
+ // Reset drag state
341
+ isDraggingOver.value = false;
342
+ dropInsertIndex.value = null;
343
+ dropIndicatorTop.value = null;
344
+ return;
345
+ }
346
+
347
+ // Get block type from dataTransfer (set by AddBlockItem)
348
+ const blockType = event.dataTransfer.getData("block-type") || event.dataTransfer.getData("text/plain");
349
+
350
+ if (!blockType) {
351
+ // Reset drag state
352
+ isDraggingOver.value = false;
353
+ dropInsertIndex.value = null;
354
+ dropIndicatorTop.value = null;
355
+ return;
356
+ }
357
+
358
+ // Calculate insertion position based on drop event (don't rely on stale dropInsertIndex)
359
+ let insertIndex: number;
360
+
361
+ if (!props.blocks || props.blocks.length === 0) {
362
+ insertIndex = 0;
363
+ } else {
364
+ const dropTarget = event.target as HTMLElement;
365
+ const blockElement = dropTarget.closest("[data-block-id]");
366
+
367
+ if (blockElement) {
368
+ const blockId = blockElement.getAttribute("data-block-id");
369
+ if (blockId) {
370
+ const currentIndex = props.blocks.findIndex((b) => b.id === blockId);
371
+ if (currentIndex !== -1) {
372
+ // Determine if we should insert before or after based on mouse position
373
+ const blockRect = blockElement.getBoundingClientRect();
374
+ const mouseY = event.clientY;
375
+ const blockCenterY = blockRect.top + blockRect.height / 2;
376
+ insertIndex = mouseY < blockCenterY ? currentIndex : currentIndex + 1;
377
+ } else {
378
+ insertIndex = props.blocks.length;
379
+ }
380
+ } else {
381
+ insertIndex = props.blocks.length;
382
+ }
383
+ } else {
384
+ // Dropped in empty area - append to end
385
+ insertIndex = props.blocks.length;
386
+ }
387
+ }
388
+
389
+ // Reset drag state
390
+ isDraggingOver.value = false;
391
+ dropInsertIndex.value = null;
392
+ dropIndicatorTop.value = null;
393
+
394
+ // Send message to parent to add the block
395
+ sendToParent({
396
+ type: "BLOCK_ADD",
397
+ blockType: blockType,
398
+ index: insertIndex,
399
+ });
400
+ }
401
+
402
+ async function initSortable() {
403
+ // Don't re-initialize if already sorting or initializing (prevents conflicts)
404
+ if (isSorting.value || isInitializingSortable.value) {
405
+ return;
406
+ }
407
+
408
+ if (!layoutComponent.value) {
409
+ console.warn("Cannot initialize Sortable: layout component is not set");
410
+ return;
411
+ }
412
+
413
+ const blockCount = props.blocks?.length;
414
+ if (!blockCount || blockCount === 0) {
415
+ console.warn("Cannot initialize Sortable: no blocks exist");
416
+ return;
417
+ }
418
+
419
+ // Use the template ref first, fallback to getElementById
420
+ const sortableBlocksWrapper = pageBlocksWrapperRef.value || document.getElementById("page-blocks-wrapper");
421
+
422
+ if (!sortableBlocksWrapper) {
423
+ console.warn("page-blocks-wrapper element not found. Conditions:", {
424
+ hasLayout: !!layoutComponent.value,
425
+ blockCount: props.blocks?.length,
426
+ hasRef: !!pageBlocksWrapperRef.value,
427
+ pageViewportExists: !!document.getElementById("page-viewport"),
428
+ });
429
+ return;
430
+ }
431
+
432
+ // If Sortable is already initialized and working, don't re-initialize
433
+ if (sortableInstance.value) {
434
+ // Check if the instance is still valid by checking if the element is still attached
435
+ const { el } = sortableInstance.value;
436
+ if (el && el.isConnected && el === sortableBlocksWrapper) {
437
+ // Sortable is already initialized and valid, no need to re-initialize
438
+ return;
439
+ }
440
+ // Instance exists but element is disconnected or different, destroy it
441
+ try {
442
+ sortableInstance.value.destroy();
443
+ } catch {
444
+ // Ignore errors during destruction
445
+ }
446
+ sortableInstance.value = null;
447
+ }
448
+
449
+ isInitializingSortable.value = true;
450
+ try {
451
+ sortableInstance.value = new Sortable(sortableBlocksWrapper, {
452
+ animation: 150,
453
+ ghostClass: "sortable-ghost",
454
+ chosenClass: "sortable-chosen",
455
+ dragClass: "sortable-drag",
456
+ group: "page-blocks",
457
+ forceFallback: false, // Use native HTML5 drag if available
458
+ fallbackOnBody: true, // Append fallback element to body
459
+ swapThreshold: 0.7, // Threshold for swap
460
+ onStart: () => {
461
+ isSorting.value = true;
462
+ },
463
+ onAdd: (event: any) => {
464
+ // This fires when an item is added from another list (drag from sidebar)
465
+ const { item: draggedElement, newIndex } = event;
466
+ const blockType = draggedElement.getAttribute("data-block-type");
467
+
468
+ if (blockType) {
469
+ // Send message to parent to handle block addition
470
+ sendToParent({
471
+ type: "BLOCK_ADD",
472
+ blockType: blockType,
473
+ index: newIndex,
474
+ });
475
+
476
+ // Remove the cloned HTML element that SortableJS added
477
+ draggedElement.remove();
478
+ }
479
+ },
480
+ onEnd: async (event: any) => {
481
+ const { oldIndex, newIndex } = event;
482
+
483
+ // Only handle reordering if this wasn't an add operation (oldIndex will be null for adds)
484
+ if (
485
+ oldIndex !== null &&
486
+ oldIndex !== undefined &&
487
+ newIndex !== null &&
488
+ newIndex !== undefined &&
489
+ oldIndex !== newIndex
490
+ ) {
491
+ // Send message to parent to handle block reordering
492
+ sendToParent({
493
+ type: "BLOCK_REORDER",
494
+ oldIndex: oldIndex,
495
+ newIndex: newIndex,
496
+ });
497
+ }
498
+
499
+ // Reset sorting state after a small delay to ensure DOM is stable
500
+ await nextTick();
501
+ isSorting.value = false;
502
+ },
503
+ });
504
+ } finally {
505
+ isInitializingSortable.value = false;
506
+ }
507
+ }
508
+
509
+ onBeforeMount(async () => {
510
+ isReady.value = false;
511
+
512
+ // Initialise registries for editor preview
513
+ // Exclude the editing registry to load only the theme and blocks
514
+ await initialiseRegistry(props.theme, false);
515
+
516
+ isReady.value = true;
517
+
518
+ setupDataPartialClickHandler();
519
+ });
520
+
521
+ // Watch for blocks and layout changes to initialize Sortable
522
+ watch(
523
+ [() => props.blocks, () => layoutComponent.value, () => isReady.value],
524
+ async ([blocks, layout, ready]) => {
525
+ if (ready && layout && blocks && blocks.length > 0) {
526
+ await nextTick();
527
+ await initSortable();
528
+ }
529
+ },
530
+ { immediate: true }
531
+ );
532
+
533
+ onMounted(async () => {
534
+ // Initialize Sortable after component is mounted
535
+ if (props.blocks && props.blocks.length > 0 && layoutComponent.value && isReady.value) {
536
+ await nextTick();
537
+ await initSortable();
538
+ }
539
+ });
540
+
541
+ // Cleanup event listener and Sortable instance on unmount
542
+ onBeforeUnmount(() => {
543
+ if (dataPartialClickHandler && editorRef.value) {
544
+ editorRef.value.removeEventListener("click", dataPartialClickHandler);
545
+ dataPartialClickHandler = null;
546
+ }
547
+
548
+ // Destroy Sortable instance
549
+ if (sortableInstance.value) {
550
+ try {
551
+ sortableInstance.value.destroy();
552
+ } catch {
553
+ // Ignore errors during destruction
554
+ }
555
+ sortableInstance.value = null;
556
+ }
557
+ });
558
+ </script>
559
+
560
+ <style scoped lang="scss">
561
+ @use "../../assets/styles/mixins" as *;
562
+
563
+ .block-wrapper {
564
+ @include block-margin-classes;
565
+ }
566
+
567
+ // target elements any data-partial="header" or empty data-partial attribute
568
+ :deep([data-partial]) {
569
+ &:hover {
570
+ @include overlay-apply(
571
+ var(--partial-backdrop-color),
572
+ var(--partial-border-color),
573
+ var(--partial-border-width),
574
+ var(--partial-border-style)
575
+ );
576
+ }
577
+
578
+ cursor: pointer;
579
+ }
580
+
581
+ // When settings sidebar is open, show overlay on all partial elements using active color
582
+ .settings-open {
583
+ :deep([data-partial]) {
584
+ @include overlay-apply(
585
+ var(--partial-backdrop-color),
586
+ var(--partial-border-color),
587
+ var(--partial-border-width),
588
+ var(--partial-border-style)
589
+ );
590
+ }
591
+ }
592
+
593
+ #page-blocks-wrapper {
594
+ position: relative;
595
+
596
+ &.drag-over {
597
+ min-height: 100px;
598
+ }
599
+ }
600
+
601
+ .drop-indicator {
602
+ position: absolute;
603
+ right: 0;
604
+ left: 0;
605
+ z-index: 1000;
606
+ display: flex;
607
+ gap: 0.2rem;
608
+ align-items: center;
609
+ width: 100%;
610
+ pointer-events: none;
611
+ transform: translateY(calc(var(--block-border-width, 4px) * -2));
612
+ animation: drop-indicator-pulse 1.5s ease-in-out infinite;
613
+ }
614
+
615
+ .drop-indicator-line {
616
+ flex: 1;
617
+ height: 3px;
618
+ background: var(--block-border-color, #638ef1);
619
+ }
620
+
621
+ .drop-indicator-label {
622
+ padding: 0.25rem 0.75rem;
623
+ font-size: 0.75rem;
624
+ font-weight: 600;
625
+ color: white;
626
+ white-space: nowrap;
627
+ background: var(--block-border-color, #638ef1);
628
+ border-radius: 0.375rem;
629
+ }
630
+
631
+ @keyframes drop-indicator-pulse {
632
+ 0%,
633
+ 100% {
634
+ opacity: 1;
635
+ }
636
+
637
+ 50% {
638
+ opacity: 0.7;
639
+ }
640
+ }
641
+ </style>
@@ -21,19 +21,11 @@
21
21
 
22
22
  <h2 class="mb-3 text-xl font-bold">No blocks found</h2>
23
23
  <template v-if="editable">
24
- <button
25
- v-if="!showAddBlockMenu"
26
- class="mb-9 inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-zinc-50 px-3 py-2 text-sm text-zinc-500 hover:border-zinc-400 hover:text-zinc-900 active:border-blue-600 active:bg-blue-50 active:text-blue-600"
27
- @click="showAddBlockMenu = true"
24
+ <p
25
+ class="mb-9 inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-zinc-50 px-3 py-2 text-sm text-zinc-500"
28
26
  >
29
27
  Add a block to get started
30
- </button>
31
- <span
32
- v-else
33
- class="mb-9 inline-flex items-center gap-1.5 border border-transparent px-3 py-2 text-sm text-zinc-500"
34
- >
35
- Select a block from the sidebar
36
- </span>
28
+ </p>
37
29
  </template>
38
30
  <!-- empty state image from assets -->
39
31
  <img :src="emptyStateImage" alt="Empty state" class="mx-auto h-auto w-full max-w-xs" />
@@ -49,7 +41,6 @@
49
41
  import { ref } from "vue";
50
42
  import emptyStateImage from "../../assets/images/empty-state.jpg";
51
43
 
52
- const showAddBlockMenu = defineModel<boolean>("showAddBlockMenu");
53
44
  const isDraggingOver = ref(false);
54
45
  const emit = defineEmits<{
55
46
  (e: "blockAdded", blockType: string): void;