vue-wswg-editor 0.0.8 → 0.0.10

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 (32) hide show
  1. package/dist/style.css +1 -1
  2. package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +9 -4
  3. package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +8 -1
  4. package/dist/types/components/PageRenderer/layoutModules.d.ts +1 -0
  5. package/dist/types/components/PageSettings/PageSettings.vue.d.ts +4 -2
  6. package/dist/types/components/WswgJsonEditor/WswgJsonEditor.vue.d.ts +5 -9
  7. package/dist/types/index.d.ts +1 -0
  8. package/dist/types/util/fieldConfig.d.ts +2 -1
  9. package/dist/vite-plugin.js +55 -22
  10. package/dist/vue-wswg-editor.es.js +1486 -1417
  11. package/package.json +1 -1
  12. package/src/components/AddBlockItem/AddBlockItem.vue +5 -5
  13. package/src/components/BlockBrowser/BlockBrowser.vue +33 -3
  14. package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +40 -30
  15. package/src/components/BlockEditorFields/BlockEditorFields.vue +4 -4
  16. package/src/components/BlockMarginFieldNode/BlockMarginNode.vue +6 -4
  17. package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +4 -2
  18. package/src/components/BrowserNavigation/BrowserNavigation.vue +2 -2
  19. package/src/components/EmptyState/EmptyState.vue +1 -1
  20. package/src/components/PageBlockList/PageBlockList.vue +1 -9
  21. package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +32 -9
  22. package/src/components/PageBuilderToolbar/PageBuilderToolbar.vue +4 -4
  23. package/src/components/PageRenderer/PageRenderer.vue +58 -5
  24. package/src/components/PageRenderer/layoutModules.ts +3 -0
  25. package/src/components/PageSettings/PageSettings.vue +19 -11
  26. package/src/components/ResizeHandle/ResizeHandle.vue +10 -10
  27. package/src/components/WswgJsonEditor/WswgJsonEditor.vue +103 -65
  28. package/src/index.ts +1 -0
  29. package/src/types/Block.d.ts +1 -1
  30. package/src/util/fieldConfig.ts +7 -0
  31. package/src/util/helpers.ts +1 -1
  32. package/src/vite-plugin.ts +49 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vue-wswg-editor",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -2,7 +2,7 @@
2
2
  <div
3
3
  :data-block-type="block.type"
4
4
  draggable="true"
5
- class="cursor-pointer rounded-md border bg-zinc-50 p-3 text-sm text-zinc-900 hover:border-zinc-400 hover:text-zinc-900"
5
+ class="cursor-pointer rounded-md border border-gray-300 bg-zinc-50 p-2 text-sm text-zinc-900 hover:border-zinc-400 hover:text-zinc-900"
6
6
  @dragstart="(event) => handleDragStart(event, block)"
7
7
  >
8
8
  <!-- thumbnail image -->
@@ -14,15 +14,15 @@
14
14
  @error="thumbnailError = true"
15
15
  />
16
16
  </div>
17
- <!-- icon -->
18
- <div v-else-if="block.icon" class="mb-2 flex h-28 w-full items-center justify-center rounded-md bg-zinc-200">
19
- <span>Icon: {{ block.icon }}</span>
17
+ <!-- emoji -->
18
+ <div v-else-if="block.emoji" class="mb-2 flex h-28 w-full items-center justify-center rounded-md bg-zinc-200">
19
+ <span class="text-2xl">{{ block.emoji }}</span>
20
20
  </div>
21
21
  <!-- placeholder -->
22
22
  <div v-else class="mb-2 flex h-28 w-full items-center justify-center rounded-md bg-zinc-200">
23
23
  <CubeTransparentIcon class="size-6 text-zinc-400" />
24
24
  </div>
25
- <p class="font-bold">{{ block.label }}</p>
25
+ <p class="text-sm">{{ block.label }}</p>
26
26
  </div>
27
27
  </template>
28
28
 
@@ -1,7 +1,12 @@
1
1
  <template>
2
2
  <div class="block-browser">
3
- <div class="block-browser-header border-b bg-white px-5 py-3">
4
- <input v-model="blockSearch" type="text" placeholder="Search blocks" class="form-control" />
3
+ <div class="block-browser-header border-b border-gray-300 bg-white px-5 py-3">
4
+ <input
5
+ v-model="blockSearch"
6
+ type="text"
7
+ placeholder="Search blocks"
8
+ class="w-full rounded-md border border-gray-300 p-2"
9
+ />
5
10
  </div>
6
11
  <div v-if="!blockCount" class="p-5 text-center text-sm text-zinc-500">
7
12
  <p>Create your first block to get started.</p>
@@ -15,7 +20,7 @@
15
20
  </p>
16
21
  </div>
17
22
  <div v-else-if="!filteredBlocks.length" class="p-5 text-center text-sm text-zinc-500">No blocks found</div>
18
- <div v-else id="available-blocks-list" class="grid grid-cols-1 gap-3 p-5">
23
+ <div v-else id="available-blocks-list" class="available-blocks-grid">
19
24
  <AddBlockItem v-for="block in filteredBlocks" :key="block.type" :block="block" />
20
25
  </div>
21
26
  </div>
@@ -67,3 +72,28 @@ onMounted(() => {
67
72
  initSortable();
68
73
  });
69
74
  </script>
75
+
76
+ <style scoped>
77
+ .block-browser {
78
+ container-type: inline-size;
79
+ }
80
+
81
+ .available-blocks-grid {
82
+ display: grid;
83
+ grid-template-columns: 1fr;
84
+ gap: 0.75rem;
85
+ padding: 1.25rem;
86
+ }
87
+
88
+ @container (min-width: 360px) {
89
+ .available-blocks-grid {
90
+ grid-template-columns: repeat(2, minmax(0, 1fr));
91
+ }
92
+ }
93
+
94
+ @container (min-width: 560px) {
95
+ .available-blocks-grid {
96
+ grid-template-columns: repeat(3, minmax(0, 1fr));
97
+ }
98
+ }
99
+ </style>
@@ -16,7 +16,11 @@
16
16
  <span>Clear</span>
17
17
  </div>
18
18
  <!-- Description -->
19
- <div v-if="fieldConfig.description" :title="fieldConfig.description" class="cursor-default">
19
+ <div
20
+ v-if="fieldConfig.description && fieldConfig.type !== 'info'"
21
+ :title="fieldConfig.description"
22
+ class="cursor-default"
23
+ >
20
24
  <InformationCircleIcon class="size-4 text-zinc-500" />
21
25
  </div>
22
26
  </div>
@@ -75,14 +79,14 @@
75
79
  <label
76
80
  v-for="option in fieldConfig.options"
77
81
  :key="`${fieldName}_${option.value}`"
78
- class="flex cursor-pointer items-center gap-2 rounded-md border p-2"
82
+ class="flex cursor-pointer items-center gap-2 rounded-md border border-gray-300 p-2"
79
83
  >
80
84
  <input
81
85
  :id="`${fieldName}_${option.value}`"
82
86
  v-model="checkboxValues"
83
87
  :value="option.value"
84
88
  type="checkbox"
85
- class="form-control"
89
+ class="form-control appearance-none"
86
90
  :disabled="!editable"
87
91
  />
88
92
  <span class="text-sm">{{ option.label }}</span>
@@ -91,39 +95,37 @@
91
95
 
92
96
  <!-- Radio -->
93
97
  <div v-else-if="fieldConfig.type === 'radio'" class="form-control flex flex-col gap-2">
94
- <label
95
- v-for="option in fieldConfig.options"
96
- :key="`${fieldName}_${option.value}`"
97
- class="flex cursor-pointer items-center gap-2 rounded-md border p-2"
98
- >
99
- <input
100
- :id="`${fieldName}_${option.value}`"
101
- v-model="fieldValue"
102
- :value="option.value"
103
- type="radio"
104
- class="form-control"
105
- :disabled="!editable"
106
- />
107
- <span class="text-sm">{{ option.label }}</span>
108
- </label>
98
+ <div v-for="option in fieldConfig.options" :key="`${fieldName}_${option.value}`">
99
+ <label
100
+ :for="`${fieldName}_${option.value}`"
101
+ class="has-checked:border-blue-600 has-checked:ring-1 has-checked:ring-blue-600 flex cursor-pointer items-center justify-between gap-4 rounded border border-gray-300 bg-white p-3 text-sm font-medium shadow-sm transition-colors hover:bg-gray-50"
102
+ >
103
+ <p class="text-gray-700">{{ option.label }}</p>
104
+
105
+ <input
106
+ :id="`${fieldName}_${option.value}`"
107
+ v-model="fieldValue"
108
+ type="radio"
109
+ :name="fieldName"
110
+ class="sr-only"
111
+ :value="option.value"
112
+ :checked="fieldValue === option.value"
113
+ />
114
+ </label>
115
+ </div>
109
116
  </div>
110
117
 
111
118
  <!-- Boolean toggle -->
112
119
  <template v-else-if="fieldConfig.type === 'boolean'">
113
120
  <label
114
- tabindex="0"
115
- class="group inline-flex h-7 w-14 cursor-pointer items-center gap-2 rounded-full p-1 transition-all duration-200"
116
- :class="!!fieldValue ? 'bg-emerald-700 hover:bg-emerald-800' : 'bg-zinc-200 hover:bg-zinc-300'"
121
+ :for="fieldName"
122
+ 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"
117
123
  >
118
- <input
119
- :id="fieldName"
120
- v-model="fieldValue"
121
- tabindex="-1"
122
- type="checkbox"
123
- class="ml-0 size-5 cursor-pointer rounded-full !border-none !bg-white !outline-none !ring-0 !ring-offset-0 transition-all duration-200 checked:ml-7"
124
- :disabled="!editable"
125
- />
126
- <span class="hidden text-sm">{{ fieldConfig.label }}</span>
124
+ <input :id="fieldName" v-model="fieldValue" type="checkbox" class="peer sr-only" :disabled="!editable" />
125
+
126
+ <span
127
+ class="absolute inset-y-0 start-0 m-1 size-5 rounded-full bg-white transition-[inset-inline-start] peer-checked:start-6"
128
+ ></span>
127
129
  </label>
128
130
  </template>
129
131
 
@@ -142,6 +144,14 @@
142
144
  <BlockMarginNode v-model="fieldValue" :fieldConfig="fieldConfig" :fieldName="fieldName" :editable="editable" />
143
145
  </div>
144
146
 
147
+ <!-- Info -->
148
+ <template v-else-if="fieldConfig.type === 'info'">
149
+ <div class="font-base mt-1 rounded-md bg-zinc-100 p-2 text-sm text-zinc-600 md:p-3">
150
+ <InformationCircleIcon class="float-left mr-1 mt-0.5 inline-block size-4" />
151
+ {{ fieldConfig.description }}
152
+ </div>
153
+ </template>
154
+
145
155
  <!-- Default fallback -->
146
156
  <template v-else>
147
157
  <input v-model="textFieldValue" type="text" class="form-control" :disabled="!editable" />
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="section-editor-fields">
3
3
  <!-- Field group tabs-->
4
- <div v-if="editorFieldGroups.length" class="field-group-tabs flex gap-2 border-b px-5 pt-3">
4
+ <div v-if="editorFieldGroups.length" class="field-group-tabs flex gap-2 border-b border-gray-300 px-5 pt-3">
5
5
  <button
6
6
  v-for="fieldGroupName in editorFieldGroups"
7
7
  :key="`fg_${fieldGroupName}`"
@@ -27,7 +27,7 @@
27
27
 
28
28
  <!-- No fields -->
29
29
  <div v-else class="p-5">
30
- <div class="rounded-lg bg-zinc-100 px-4 py-3 text-sm font-bold text-zinc-500">
30
+ <div class="rounded-lg bg-zinc-100 px-4 py-3 text-sm font-medium text-zinc-600">
31
31
  <p>
32
32
  {{ isLayoutBlock ? "No settings available for this layout." : "No options available for this block." }}
33
33
  </p>
@@ -79,13 +79,13 @@ const editorFieldGroups = computed(() => {
79
79
 
80
80
  watch(editorFieldGroups, () => {
81
81
  if (editorFieldGroups.value.length > 0) {
82
- activeFieldGroup.value = editorFieldGroups.value[0];
82
+ activeFieldGroup.value = editorFieldGroups.value[0] || "";
83
83
  }
84
84
  });
85
85
 
86
86
  onBeforeMount(() => {
87
87
  if (editorFieldGroups.value.length > 0) {
88
- activeFieldGroup.value = editorFieldGroups.value[0];
88
+ activeFieldGroup.value = editorFieldGroups.value[0] || "";
89
89
  }
90
90
  });
91
91
  </script>
@@ -1,7 +1,9 @@
1
1
  <template>
2
2
  <div class="margin-field flex gap-2" :class="linkedMargin ? '' : 'items-start'">
3
3
  <div v-if="linkedMargin" class="field-wrapper relative flex-1" title="Top & Bottom Margin">
4
- <span class="pointer-events-none absolute left-0 top-0 inline-flex h-full items-center border-r px-3 text-xs">
4
+ <span
5
+ class="pointer-events-none absolute left-0 top-0 inline-flex h-full items-center border-r border-gray-300 px-3 text-xs"
6
+ >
5
7
  <ArrowsUpDownIcon class="size-3" />
6
8
  </span>
7
9
  <select
@@ -21,7 +23,7 @@
21
23
  <div class="flex flex-1 gap-2">
22
24
  <div class="field-wrapper relative flex-1" title="Top Margin">
23
25
  <span
24
- class="pointer-events-none absolute left-0 top-0 inline-flex h-full items-center justify-center border-r px-3 text-xs"
26
+ class="pointer-events-none absolute left-0 top-0 inline-flex h-full items-center justify-center border-r border-gray-300 px-3 text-xs"
25
27
  ><ArrowUpIcon class="size-3"
26
28
  /></span>
27
29
  <select
@@ -38,7 +40,7 @@
38
40
  </div>
39
41
  <div class="field-wrapper relative flex-1" title="Bottom Margin">
40
42
  <span
41
- class="pointer-events-none absolute left-0 top-0 inline-flex h-full items-center justify-center border-r px-3 text-xs"
43
+ class="pointer-events-none absolute left-0 top-0 inline-flex h-full items-center justify-center border-r border-gray-300 px-3 text-xs"
42
44
  >
43
45
  <ArrowDownIcon class="size-3" />
44
46
  </span>
@@ -59,7 +61,7 @@
59
61
  <button
60
62
  v-if="editable"
61
63
  title="Link Margin (Top & Bottom)"
62
- class="inline-flex size-10 shrink-0 items-center justify-center rounded-md border p-2 text-center"
64
+ class="inline-flex size-10 shrink-0 items-center justify-center rounded-md border border-gray-300 p-2 text-center"
63
65
  :class="
64
66
  linkedMargin
65
67
  ? 'bg-blue-50 border-blue-200 hover:bg-blue-100'
@@ -18,7 +18,7 @@
18
18
  <div
19
19
  v-for="(item, index) in fieldValue"
20
20
  :key="`${fieldName}-item-${index}`"
21
- class="repeater-item overflow-hidden rounded-lg border bg-white hover:border-zinc-300 hover:shadow-sm"
21
+ class="repeater-item overflow-hidden rounded-lg border border-gray-300 bg-white hover:border-zinc-300 hover:shadow-sm"
22
22
  :class="{ 'is-open': openRepeaterItems.includes(item.id) }"
23
23
  >
24
24
  <div class="repeater-item-header flex items-center gap-2 bg-zinc-50 p-3">
@@ -210,7 +210,9 @@ const canAddItem = computed(() => {
210
210
 
211
211
  &.is-open {
212
212
  .repeater-item-fields {
213
- @apply p-3 h-auto border-t;
213
+ height: auto;
214
+ padding: 1rem;
215
+ border-top: 1px solid #e5e7eb;
214
216
  }
215
217
  }
216
218
  }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <!-- URL bar -->
3
- <div class="browser-navigation-bar border-b bg-zinc-600 py-3 !font-sans">
4
- <div class="mx-auto flex max-w-5xl items-center justify-between px-5">
3
+ <div class="browser-navigation-bar">
4
+ <div class="flex items-center justify-between rounded-t-lg 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
 
@@ -23,7 +23,7 @@
23
23
  <template v-if="editable">
24
24
  <button
25
25
  v-if="!showAddBlockMenu"
26
- class="mb-9 inline-flex items-center gap-1.5 rounded-md border 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"
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
27
  @click="showAddBlockMenu = true"
28
28
  >
29
29
  Add a block to get started
@@ -4,7 +4,7 @@
4
4
  <div
5
5
  v-for="block in pageBlocks"
6
6
  :key="block.id"
7
- :class="{ 'hovered-block': hoveredBlockId === block.id }"
7
+ :class="{ 'bg-blue-100 text-blue-600': hoveredBlockId === block.id }"
8
8
  class="block-item -mx-2.5 flex cursor-pointer items-center gap-1 rounded-md p-2.5 text-sm text-neutral-900"
9
9
  @mouseenter="setHoveredBlockId(block.id)"
10
10
  @mouseleave="setHoveredBlockId(null)"
@@ -93,11 +93,3 @@ onMounted(() => {
93
93
  initSortable();
94
94
  });
95
95
  </script>
96
-
97
- <style scoped lang="scss">
98
- .block-item {
99
- &.hovered-block {
100
- @apply bg-blue-100 text-blue-600;
101
- }
102
- }
103
- </style>
@@ -1,11 +1,25 @@
1
1
  <template>
2
2
  <div id="page-builder-sidebar" class="page-builder-sidebar">
3
+ <!-- Toolbar -->
4
+ <PageBuilderToolbar
5
+ v-model:editorViewport="editorViewport"
6
+ v-model:showPageSettings="showPageSettings"
7
+ v-model:activeBlock="activeBlock"
8
+ class="z-12 sticky top-0 bg-white"
9
+ :hasPageSettings="hasPageSettings"
10
+ />
3
11
  <!-- Page settings -->
4
- <PageSettings v-if="showPageSettings" v-model="pageData" :editable="editable" @close="showPageSettings = false" />
12
+ <PageSettings
13
+ v-if="showPageSettings"
14
+ v-model="pageData"
15
+ :editable="editable"
16
+ :settingsKey="settingsKey"
17
+ @close="showPageSettings = false"
18
+ />
5
19
  <!-- Active section-->
6
20
  <div v-else-if="activeBlock">
7
21
  <!-- back header -->
8
- <div class="flex items-start justify-between border-b bg-white p-5">
22
+ <div class="flex items-start justify-between border-b border-gray-300 bg-white p-5">
9
23
  <div>
10
24
  <button
11
25
  class="cursor-pointer text-sm text-zinc-500 hover:text-zinc-900 hover:underline"
@@ -18,7 +32,7 @@
18
32
  <!-- delete section button -->
19
33
  <button
20
34
  v-if="activeBlock && editable"
21
- class="inline-flex size-7 cursor-pointer items-center justify-center rounded-md border bg-zinc-100 text-zinc-500 hover:border-red-200 hover:bg-red-100 hover:text-red-600"
35
+ class="inline-flex size-7 cursor-pointer items-center justify-center rounded-md border border-gray-300 bg-zinc-100 text-zinc-500 hover:border-red-200 hover:bg-red-100 hover:text-red-600"
22
36
  title="Delete block"
23
37
  @click="handleDeleteBlock"
24
38
  >
@@ -33,7 +47,7 @@
33
47
  </div>
34
48
  <!-- Add block menu -->
35
49
  <div v-else-if="showAddBlockMenu">
36
- <div class="flex items-center justify-between border-b bg-white p-5">
50
+ <div class="flex items-center justify-between border-b border-gray-300 bg-white p-5">
37
51
  <div>
38
52
  <button
39
53
  class="cursor-pointer text-sm text-zinc-500 hover:text-zinc-900 hover:underline"
@@ -49,11 +63,11 @@
49
63
  </div>
50
64
  <!-- No active block -->
51
65
  <div v-else>
52
- <div class="flex items-center justify-between border-b bg-white p-5">
66
+ <div class="flex items-center justify-between border-b border-gray-300 bg-white p-5">
53
67
  <h4 class="text-lg font-bold">Blocks ({{ pageData?.[blocksKey]?.length }})</h4>
54
68
  <button
55
69
  v-if="editable"
56
- class="inline-flex items-center gap-1.5 rounded-md border bg-zinc-50 px-3 py-2 text-xs text-zinc-500 hover:border-zinc-400 hover:text-zinc-900 active:border-blue-600 active:bg-blue-50 active:text-blue-600"
70
+ class="inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-zinc-50 px-3 py-2 text-xs text-zinc-500 hover:border-zinc-400 hover:text-zinc-900 active:border-blue-600 active:bg-blue-50 active:text-blue-600"
57
71
  title="Add block"
58
72
  @click="handleShowAddBlockMenu"
59
73
  >
@@ -84,24 +98,28 @@ import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
84
98
  import PageBlockList from "../PageBlockList/PageBlockList.vue";
85
99
  import PageSettings from "../PageSettings/PageSettings.vue";
86
100
  import { TrashIcon, PlusIcon } from "@heroicons/vue/24/outline";
101
+ import PageBuilderToolbar from "../PageBuilderToolbar/PageBuilderToolbar.vue";
87
102
  // Models
88
103
  const pageData = defineModel<any>();
89
104
  const activeBlock = defineModel<any>("activeBlock");
90
105
  const hoveredBlockId = defineModel<string | null>("hoveredBlockId");
91
106
  const showPageSettings = defineModel<boolean>("showPageSettings");
92
107
  const showAddBlockMenu = defineModel<boolean>("showAddBlockMenu");
108
+ const editorViewport = defineModel<"desktop" | "mobile">("editorViewport");
93
109
 
94
- // Editable prop
110
+ // Props
95
111
  const props = withDefaults(
96
112
  defineProps<{
97
113
  editable?: boolean;
98
114
  blocksKey?: string;
99
115
  settingsKey?: string;
116
+ hasPageSettings?: boolean;
100
117
  }>(),
101
118
  {
102
119
  editable: true,
103
120
  blocksKey: "blocks",
104
121
  settingsKey: "settings",
122
+ hasPageSettings: false,
105
123
  }
106
124
  );
107
125
 
@@ -233,9 +251,14 @@ async function handleDeleteBlock() {
233
251
  </script>
234
252
 
235
253
  <style scoped lang="scss">
254
+ $toolbar-height: 0px;
255
+
236
256
  .page-builder-sidebar {
237
- height: 100%;
238
- overflow-y: auto;
257
+ min-width: 300px;
239
258
  background: #fff;
259
+
260
+ // position: sticky;
261
+ // top: 0;
262
+ // z-index: 12;
240
263
  }
241
264
  </style>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <!-- Control bar -->
3
- <div class="flex divide-x border-b">
3
+ <div class="flex divide-x divide-gray-300 border-b border-gray-300">
4
4
  <slot name="default">
5
5
  <!-- no default toolbar content -->
6
6
  </slot>
@@ -8,7 +8,7 @@
8
8
  <!-- Desktop / Mobile view toggle -->
9
9
  <div v-if="hasPageSettings" class="ml-auto inline-flex gap-2 px-5 py-2.5">
10
10
  <button
11
- class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
11
+ class="inline-flex items-center rounded-md border border-gray-300 px-3 py-2 text-xs"
12
12
  :class="
13
13
  showPageSettings
14
14
  ? 'bg-blue-50 text-blue-700 border-blue-800/20'
@@ -25,7 +25,7 @@
25
25
  </div>
26
26
  <div class="inline-flex gap-2 px-5 py-2.5">
27
27
  <button
28
- class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
28
+ class="inline-flex items-center rounded-md border border-gray-300 px-3 py-2 text-xs"
29
29
  :class="
30
30
  editorViewport === 'mobile'
31
31
  ? 'bg-blue-50 text-blue-700 border-blue-800/20'
@@ -37,7 +37,7 @@
37
37
  <DevicePhoneMobileIcon class="size-4" />
38
38
  </button>
39
39
  <button
40
- class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
40
+ class="inline-flex items-center rounded-md border border-gray-300 px-3 py-2 text-xs"
41
41
  :class="
42
42
  editorViewport === 'desktop'
43
43
  ? 'bg-blue-50 text-blue-700 border-blue-800/20'
@@ -1,5 +1,19 @@
1
1
  <template>
2
- <div id="page-blocks-wrapper">
2
+ <component :is="layoutComponent" v-if="withLayout && layoutComponent" v-bind="settings">
3
+ <template #default>
4
+ <div id="page-blocks-wrapper">
5
+ <div
6
+ v-for="block in blocks"
7
+ :key="block.id"
8
+ class="block-wrapper"
9
+ :class="{ [getMarginClass(block)]: true }"
10
+ >
11
+ <component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
12
+ </div>
13
+ </div>
14
+ </template>
15
+ </component>
16
+ <div v-else id="page-blocks-wrapper">
3
17
  <div v-for="block in blocks" :key="block.id" class="block-wrapper" :class="{ [getMarginClass(block)]: true }">
4
18
  <component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
5
19
  </div>
@@ -7,14 +21,25 @@
7
21
  </template>
8
22
 
9
23
  <script setup lang="ts">
10
- import { type Component } from "vue";
24
+ import { type Component, computed, withDefaults } from "vue";
11
25
  import { generateNameVariations } from "../../util/helpers";
12
26
  import { blockModules } from "./blockModules";
27
+ import { layoutModules } from "./layoutModules";
13
28
  import type { Block } from "../../types/Block";
14
29
 
15
- defineProps<{
16
- blocks: Block[];
17
- }>();
30
+ const props = withDefaults(
31
+ defineProps<{
32
+ blocks: Block[];
33
+ layout?: string;
34
+ settings?: Record<string, any>;
35
+ withLayout?: boolean;
36
+ }>(),
37
+ {
38
+ layout: "default",
39
+ settings: () => ({}),
40
+ withLayout: true,
41
+ }
42
+ );
18
43
 
19
44
  function getBlock(blockType: string): Component | undefined {
20
45
  // Generate name variations and try to find a match in blockModules keys (file paths)
@@ -39,6 +64,34 @@ function getBlock(blockType: string): Component | undefined {
39
64
  return undefined;
40
65
  }
41
66
 
67
+ function getLayout(layoutName: string): Component | undefined {
68
+ // Generate name variations and try to find a match in layoutModules keys (file paths)
69
+ const nameVariations = generateNameVariations(layoutName);
70
+
71
+ // Iterate through all layoutModules entries
72
+ for (const [filePath, module] of Object.entries(layoutModules)) {
73
+ // Check if any variation matches the file path
74
+ for (const variation of nameVariations) {
75
+ // Check if the file path contains the variation followed by .vue
76
+ // e.g., "default" matches "layout/default.vue" or "layout/default/default.vue"
77
+ if (filePath.includes(`${variation}.vue`)) {
78
+ // Extract the default export (the Vue component)
79
+ const component = (module as any).default;
80
+ if (component) {
81
+ return component;
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ return undefined;
88
+ }
89
+
90
+ // Get the layout component based on the layout prop
91
+ const layoutComponent = computed(() => {
92
+ return getLayout(props.layout);
93
+ });
94
+
42
95
  // Get the margin class for the block
43
96
  // Margin is an object with top and bottom properties
44
97
  // margin classses are formatted as `margin-<direction>-<size>`
@@ -0,0 +1,3 @@
1
+ // import.meta.glob must be at top level - it will be transformed by Vite plugin
2
+ // This file is separate from the Vue component to ensure the transform hook can intercept it
3
+ export const layoutModules = import.meta.glob("@page-builder/layout/**/*.vue", { eager: true });
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="page-settings">
3
- <div class="flex items-start justify-between border-b bg-white p-5">
3
+ <div class="flex items-start justify-between border-b border-gray-300 bg-white p-5">
4
4
  <div>
5
5
  <button
6
6
  class="cursor-pointer text-sm text-zinc-500 hover:text-zinc-900 hover:underline"
@@ -11,7 +11,7 @@
11
11
  <h4 class="mt-1 text-lg font-bold">Page settings</h4>
12
12
  </div>
13
13
  </div>
14
- <div class="border-b p-5">
14
+ <div class="border-b border-gray-300 p-5">
15
15
  <!-- Page layout -->
16
16
  <div class="editor-field-node">
17
17
  <!-- Label -->
@@ -19,7 +19,11 @@
19
19
  <label class="mr-auto font-medium first-letter:uppercase">Page layout</label>
20
20
  </div>
21
21
 
22
- <select v-model="pageData.settings.layout" class="form-control" @change="getLayoutSettings">
22
+ <select
23
+ v-model="pageData[settingsKey].layout"
24
+ class="form-control w-full rounded-md border border-gray-300 p-2"
25
+ @change="getLayoutSettings"
26
+ >
23
27
  <option v-for="layout in availableLayouts" :key="`layout-${layout.__name}`" :value="layout.__name">
24
28
  {{ layout.label }}
25
29
  </option>
@@ -29,7 +33,7 @@
29
33
  <!-- Page settings -->
30
34
  <div class="editor-field-node">
31
35
  <BlockEditorFields
32
- v-model="pageData.settings"
36
+ v-model="pageData[settingsKey]"
33
37
  :fields="pageSettingsFields"
34
38
  :editable="true"
35
39
  :isLayoutBlock="true"
@@ -50,21 +54,25 @@ const emit = defineEmits<{
50
54
  const pageData = defineModel<any>();
51
55
  const pageSettingsFields = ref<any>({});
52
56
 
53
- defineProps<{
54
- title?: string;
55
- }>();
56
-
57
+ const props = withDefaults(
58
+ defineProps<{
59
+ settingsKey?: string;
60
+ }>(),
61
+ {
62
+ settingsKey: "settings",
63
+ }
64
+ );
57
65
  const availableLayouts = computed(() => {
58
66
  return getLayouts();
59
67
  });
60
68
 
61
69
  function getLayoutSettings() {
62
- if (!pageData.value.settings.layout) return;
63
- pageSettingsFields.value = getLayoutFields(pageData.value.settings.layout) || null;
70
+ if (!pageData.value[props.settingsKey].layout) return;
71
+ pageSettingsFields.value = getLayoutFields(pageData.value[props.settingsKey].layout) || null;
64
72
  }
65
73
 
66
74
  onBeforeMount(() => {
67
- if (!pageData.value.settings) {
75
+ if (!pageData.value[props.settingsKey]) {
68
76
  pageData.value.settings = null;
69
77
  }
70
78
  });