vue-wswg-editor 0.0.9 → 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.
- package/dist/style.css +1 -1
- package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +9 -4
- package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +8 -1
- package/dist/types/components/PageRenderer/layoutModules.d.ts +1 -0
- package/dist/types/components/PageSettings/PageSettings.vue.d.ts +4 -2
- package/dist/types/components/WswgJsonEditor/WswgJsonEditor.vue.d.ts +5 -9
- package/dist/types/index.d.ts +1 -0
- package/dist/types/util/fieldConfig.d.ts +2 -1
- package/dist/vite-plugin.js +32 -9
- package/dist/vue-wswg-editor.es.js +1486 -1417
- package/package.json +1 -1
- package/src/components/AddBlockItem/AddBlockItem.vue +5 -5
- package/src/components/BlockBrowser/BlockBrowser.vue +33 -3
- package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +40 -30
- package/src/components/BlockEditorFields/BlockEditorFields.vue +4 -4
- package/src/components/BlockMarginFieldNode/BlockMarginNode.vue +6 -4
- package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +4 -2
- package/src/components/BrowserNavigation/BrowserNavigation.vue +2 -2
- package/src/components/EmptyState/EmptyState.vue +1 -1
- package/src/components/PageBlockList/PageBlockList.vue +1 -9
- package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +32 -9
- package/src/components/PageBuilderToolbar/PageBuilderToolbar.vue +4 -4
- package/src/components/PageRenderer/PageRenderer.vue +58 -5
- package/src/components/PageRenderer/layoutModules.ts +3 -0
- package/src/components/PageSettings/PageSettings.vue +19 -11
- package/src/components/ResizeHandle/ResizeHandle.vue +10 -10
- package/src/components/WswgJsonEditor/WswgJsonEditor.vue +103 -65
- package/src/index.ts +1 -0
- package/src/types/Block.d.ts +1 -1
- package/src/util/fieldConfig.ts +7 -0
- package/src/util/helpers.ts +1 -1
- package/src/vite-plugin.ts +27 -2
package/package.json
CHANGED
|
@@ -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-
|
|
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
|
-
<!--
|
|
18
|
-
<div v-else-if="block.
|
|
19
|
-
<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="
|
|
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
|
|
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="
|
|
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
|
|
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
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
class="
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
4
|
-
<div class="
|
|
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="{ '
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
16
|
-
|
|
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>`
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
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.
|
|
63
|
-
pageSettingsFields.value = getLayoutFields(pageData.value.
|
|
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.
|
|
75
|
+
if (!pageData.value[props.settingsKey]) {
|
|
68
76
|
pageData.value.settings = null;
|
|
69
77
|
}
|
|
70
78
|
});
|