vue-wswg-editor 0.0.1

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 (61) hide show
  1. package/README.md +91 -0
  2. package/dist/style.css +1 -0
  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 +15 -0
  8. package/dist/types/components/BlockMarginFieldNode/BlockMarginNode.vue.d.ts +23 -0
  9. package/dist/types/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue.d.ts +15 -0
  10. package/dist/types/components/BrowserNavigation/BrowserNavigation.vue.d.ts +5 -0
  11. package/dist/types/components/EmptyState/EmptyState.vue.d.ts +15 -0
  12. package/dist/types/components/PageBlockList/PageBlockList.vue.d.ts +19 -0
  13. package/dist/types/components/PageBuilderSidebar/PageBuilderSidebar.vue.d.ts +30 -0
  14. package/dist/types/components/PageBuilderToolbar/PageBuilderToolbar.vue.d.ts +28 -0
  15. package/dist/types/components/PageRenderer/PageRenderer.vue.d.ts +6 -0
  16. package/dist/types/components/PageRenderer/blockModules.d.ts +1 -0
  17. package/dist/types/components/PageSettings/PageSettings.vue.d.ts +15 -0
  18. package/dist/types/components/ResizeHandle/ResizeHandle.vue.d.ts +6 -0
  19. package/dist/types/components/WswgJsonEditor/WswgJsonEditor.test.d.ts +1 -0
  20. package/dist/types/components/WswgJsonEditor/WswgJsonEditor.vue.d.ts +40 -0
  21. package/dist/types/index.d.ts +7 -0
  22. package/dist/types/tsconfig.tsbuildinfo +1 -0
  23. package/dist/types/util/fieldConfig.d.ts +82 -0
  24. package/dist/types/util/helpers.d.ts +28 -0
  25. package/dist/types/util/registry.d.ts +21 -0
  26. package/dist/types/util/validation.d.ts +15 -0
  27. package/dist/vue-wswg-editor.es.js +3377 -0
  28. package/package.json +85 -0
  29. package/src/assets/images/empty-state.jpg +0 -0
  30. package/src/assets/styles/_mixins.scss +73 -0
  31. package/src/assets/styles/main.css +3 -0
  32. package/src/components/AddBlockItem/AddBlockItem.vue +50 -0
  33. package/src/components/BlockBrowser/BlockBrowser.vue +69 -0
  34. package/src/components/BlockComponent/BlockComponent.vue +186 -0
  35. package/src/components/BlockEditorFieldNode/BlockEditorFieldNode.vue +378 -0
  36. package/src/components/BlockEditorFields/BlockEditorFields.vue +91 -0
  37. package/src/components/BlockMarginFieldNode/BlockMarginNode.vue +132 -0
  38. package/src/components/BlockRepeaterFieldNode/BlockRepeaterNode.vue +217 -0
  39. package/src/components/BrowserNavigation/BrowserNavigation.vue +27 -0
  40. package/src/components/EmptyState/EmptyState.vue +94 -0
  41. package/src/components/PageBlockList/PageBlockList.vue +103 -0
  42. package/src/components/PageBuilderSidebar/PageBuilderSidebar.vue +241 -0
  43. package/src/components/PageBuilderToolbar/PageBuilderToolbar.vue +63 -0
  44. package/src/components/PageRenderer/PageRenderer.vue +65 -0
  45. package/src/components/PageRenderer/blockModules-alternative.ts.example +9 -0
  46. package/src/components/PageRenderer/blockModules-manual.ts.example +19 -0
  47. package/src/components/PageRenderer/blockModules-runtime.ts.example +23 -0
  48. package/src/components/PageRenderer/blockModules.ts +3 -0
  49. package/src/components/PageSettings/PageSettings.vue +86 -0
  50. package/src/components/ResizeHandle/ResizeHandle.vue +105 -0
  51. package/src/components/WswgJsonEditor/WswgJsonEditor.test.ts +43 -0
  52. package/src/components/WswgJsonEditor/WswgJsonEditor.vue +391 -0
  53. package/src/index.ts +15 -0
  54. package/src/shims.d.ts +72 -0
  55. package/src/style.css +3 -0
  56. package/src/types/Block.d.ts +19 -0
  57. package/src/types/Layout.d.ts +9 -0
  58. package/src/util/fieldConfig.ts +173 -0
  59. package/src/util/helpers.ts +176 -0
  60. package/src/util/registry.ts +149 -0
  61. package/src/util/validation.ts +110 -0
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "vue-wswg-editor",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "module": "src/index.ts",
7
+ "typings": "src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./src/index.ts",
11
+ "require": "./src/index.ts",
12
+ "types": "./src/index.ts"
13
+ },
14
+ "./PageRenderer": {
15
+ "import": "./src/components/PageRenderer/PageRenderer.vue",
16
+ "types": "./src/components/PageRenderer/PageRenderer.vue"
17
+ },
18
+ "./style.css": "./dist/style.css"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "scripts": {
25
+ "dev": "vite build --watch",
26
+ "build": "vite build && vue-tsc --declaration --emitDeclarationOnly",
27
+ "preview": "vite preview",
28
+ "test:unit": "vitest",
29
+ "test:unit:ui": "vitest --ui",
30
+ "test:unit:ci": "vitest run",
31
+ "--PROJECT--": "",
32
+ "tscheck": "vue-tsc --build --force",
33
+ "prettier": "prettier --check .",
34
+ "prettier-fix": "prettier . --write",
35
+ "lint": "eslint . --quiet",
36
+ "lint-fix": "npm run lint --fix --quiet",
37
+ "stylelint": "stylelint \"src/**/*.css\" \"**/*.scss\" \"**/*.vue\"",
38
+ "stylelint-fix": "stylelint \"src/**/*.css\" \"**/*.scss\" \"**/*.vue\" --fix",
39
+ "format": "npm run tscheck && npm run prettier && npm run lint && npm run stylelint",
40
+ "format-fix": "npm run tscheck && npm run prettier-fix && npm run lint-fix && npm run stylelint-fix"
41
+ },
42
+ "dependencies": {
43
+ "@headlessui/vue": "^1.7.23",
44
+ "@heroicons/vue": "^2.2.0",
45
+ "@vueuse/core": "^14.0.0",
46
+ "sortablejs": "^1.15.6",
47
+ "vue": "^3.4.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/jsdom": "^21.1.7",
51
+ "@types/node": "^24.0.8",
52
+ "@types/sortablejs": "^1.15.9",
53
+ "@vitejs/plugin-vue": "^5.2.4",
54
+ "@vitest/eslint-plugin": "^1.3.5",
55
+ "@vitest/ui": "^2.1.9",
56
+ "@vue/eslint-config-prettier": "^10.2.0",
57
+ "@vue/eslint-config-typescript": "^14.5.1",
58
+ "@vue/test-utils": "^2.4.6",
59
+ "@vue/tsconfig": "^0.7.0",
60
+ "autoprefixer": "^10.4.21",
61
+ "eslint": "^9.30.0",
62
+ "eslint-plugin-tailwindcss": "^3.18.2",
63
+ "eslint-plugin-vue": "^9.33.0",
64
+ "jsdom": "^26.1.0",
65
+ "postcss": "^8.5.6",
66
+ "postcss-import": "^16.1.0",
67
+ "postcss-scss": "^4.0.9",
68
+ "prettier": "^3.6.2",
69
+ "sass": "^1.89.2",
70
+ "stylelint": "^16.21.0",
71
+ "stylelint-config-recess-order": "^7.1.0",
72
+ "stylelint-config-standard": "^38.0.0",
73
+ "stylelint-config-standard-scss": "^15.0.1",
74
+ "stylelint-config-standard-vue": "^1.0.0",
75
+ "tailwindcss": "^3.4.10",
76
+ "typescript": "~5.8.3",
77
+ "vite": "^5.4.19",
78
+ "vitest": "^2.1.9",
79
+ "vue-tsc": "^2.2.12",
80
+ "yup": "^1.7.1"
81
+ },
82
+ "peerDependencies": {
83
+ "vue": "^3.4.0"
84
+ }
85
+ }
@@ -0,0 +1,73 @@
1
+ // Block Margin Utility Mixin
2
+ // Generates margin classes for top and bottom margins
3
+ // Usage: @include block-margin-classes;
4
+
5
+ @mixin block-margin-classes {
6
+ // Margin size variables
7
+ $sm-size: 2rem;
8
+ $md-size: 4rem;
9
+ $lg-size: 6rem;
10
+
11
+ // Top margin classes
12
+ &.margin-top-sm {
13
+ padding-top: $sm-size;
14
+
15
+ &::before {
16
+ top: 0;
17
+ height: $sm-size;
18
+ }
19
+ }
20
+
21
+ &.margin-top-md {
22
+ padding-top: $md-size;
23
+
24
+ &::before {
25
+ top: 0;
26
+ height: $md-size;
27
+ }
28
+ }
29
+
30
+ &.margin-top-lg {
31
+ padding-top: $lg-size;
32
+
33
+ &::before {
34
+ top: 0;
35
+ height: $lg-size;
36
+ }
37
+ }
38
+
39
+ // Bottom margin classes
40
+ &.margin-bottom-sm {
41
+ padding-bottom: $sm-size;
42
+
43
+ &::after {
44
+ bottom: 0;
45
+ height: $sm-size;
46
+ }
47
+ }
48
+
49
+ &.margin-bottom-md {
50
+ padding-bottom: $md-size;
51
+
52
+ &::after {
53
+ bottom: 0;
54
+ height: $md-size;
55
+ }
56
+ }
57
+
58
+ &.margin-bottom-lg {
59
+ padding-bottom: $lg-size;
60
+
61
+ &::after {
62
+ bottom: 0;
63
+ height: $lg-size;
64
+ }
65
+ }
66
+
67
+ // No margin classes
68
+ &.margin-top-none::before,
69
+ &.margin-bottom-none::after {
70
+ display: none;
71
+ content: none;
72
+ }
73
+ }
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <div
3
+ :data-block-type="block.type"
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"
6
+ @dragstart="(event) => handleDragStart(event, block)"
7
+ >
8
+ <!-- thumbnail image -->
9
+ <div v-if="thumbnailUrl" class="w-full overflow-hidden rounded-md bg-neutral-100">
10
+ <img
11
+ :src="thumbnailUrl"
12
+ :alt="block.label || block.type"
13
+ class="mx-auto mb-2 h-28 w-auto object-contain"
14
+ @error="thumbnailError = true"
15
+ />
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>
20
+ </div>
21
+ <!-- placeholder -->
22
+ <div v-else class="mb-2 flex h-28 w-full items-center justify-center rounded-md bg-zinc-200">
23
+ <CubeTransparentIcon class="size-6 text-zinc-400" />
24
+ </div>
25
+ <p class="font-bold">{{ block.label }}</p>
26
+ </div>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import { computed, defineProps, ref } from "vue";
31
+ import type { Block } from "../../types/Block";
32
+ import { getBlockThumbnailUrl } from "../../util/registry";
33
+ import { CubeTransparentIcon } from "@heroicons/vue/24/outline";
34
+
35
+ const props = defineProps<{
36
+ block: Block;
37
+ }>();
38
+
39
+ const thumbnailError = ref<boolean>(false);
40
+ const thumbnailUrl = computed(() => {
41
+ return getBlockThumbnailUrl(props.block.directory);
42
+ });
43
+
44
+ function handleDragStart(event: DragEvent, block: Block) {
45
+ if (!event.dataTransfer) return;
46
+ // Store the block type in dataTransfer
47
+ event.dataTransfer.setData("block-type", block.__name);
48
+ event.dataTransfer.effectAllowed = "move";
49
+ }
50
+ </script>
@@ -0,0 +1,69 @@
1
+ <template>
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" />
5
+ </div>
6
+ <div v-if="!blockCount" class="p-5 text-center text-sm text-zinc-500">
7
+ <p>Create your first block to get started.</p>
8
+ <p class="mt-3">
9
+ <a
10
+ href="https://github.com/sano-io/page-builder/tree/main/blocks"
11
+ target="_blank"
12
+ class="text-blue-600 underline underline-offset-2 hover:text-blue-800"
13
+ >How to create a block</a
14
+ >
15
+ </p>
16
+ </div>
17
+ <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">
19
+ <AddBlockItem v-for="block in filteredBlocks" :key="block.type" :block="block" />
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { computed, onMounted, ref } from "vue";
26
+ import { pageBuilderBlocks } from "../../util/registry";
27
+ import AddBlockItem from "../AddBlockItem/AddBlockItem.vue";
28
+ import type { Block } from "../../types/Block";
29
+ import Sortable from "sortablejs";
30
+
31
+ const blockSearch = ref("");
32
+ const filteredBlocks = computed(() => {
33
+ if (!pageBuilderBlocks.value) return [];
34
+ return Object.values(pageBuilderBlocks.value).filter((block: Block) => {
35
+ // against block name and label
36
+ return (
37
+ block.type?.toLowerCase().includes(blockSearch.value.toLowerCase()) ||
38
+ block.label?.toLowerCase().includes(blockSearch.value.toLowerCase())
39
+ );
40
+ });
41
+ });
42
+
43
+ const blockCount = computed(() => {
44
+ if (!pageBuilderBlocks.value) return 0;
45
+ return Object.values(pageBuilderBlocks.value).length;
46
+ });
47
+
48
+ function initSortable() {
49
+ const sortableBlocksWrapper = document.getElementById("available-blocks-list");
50
+ if (!sortableBlocksWrapper) return;
51
+ if (!blockCount.value) return;
52
+ new Sortable(sortableBlocksWrapper, {
53
+ animation: 150,
54
+ ghostClass: "sortable-ghost",
55
+ chosenClass: "sortable-chosen",
56
+ dragClass: "sortable-drag",
57
+ group: {
58
+ name: "page-blocks",
59
+ pull: "clone",
60
+ put: false,
61
+ },
62
+ sort: false,
63
+ });
64
+ }
65
+
66
+ onMounted(() => {
67
+ initSortable();
68
+ });
69
+ </script>
@@ -0,0 +1,186 @@
1
+ <template>
2
+ <div
3
+ class="block-wrapper"
4
+ :class="{
5
+ [getMarginClass(block)]: true,
6
+ 'active-block': activeBlock?.id === block.id,
7
+ 'hovered-block': hoveredBlockId === block.id,
8
+ }"
9
+ :data-block-index="blockIndex"
10
+ :data-block-id="block.id"
11
+ >
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)]"
20
+ class="block-component"
21
+ @mouseenter="emit('hoverBlock', block.id)"
22
+ @mouseleave="emit('hoverBlock', null)"
23
+ @click="emit('clickBlock', block)"
24
+ >
25
+ <component :is="pageBuilderBlocks[toCamelCase(block.type)]" v-bind="block" ref="blockComponentRef" />
26
+ </div>
27
+ <div
28
+ v-else
29
+ class="block-not-found px-3 py-2"
30
+ @mouseenter="emit('hoverBlock', block.id)"
31
+ @mouseleave="emit('hoverBlock', null)"
32
+ >
33
+ <div class="rounded-lg bg-zinc-200 p-5 px-3 text-center text-sm text-zinc-600">
34
+ <p class="mb-2">Block not registered</p>
35
+ <span class="rounded-full bg-zinc-300 px-2 py-1 text-zinc-600">
36
+ {{ toCamelCase(block.type) }}
37
+ </span>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ import { useTemplateRef } from "vue";
45
+ import { onClickOutside } from "@vueuse/core";
46
+ import { pageBuilderBlocks } from "../../util/registry";
47
+ import type { Block } from "../../types/Block";
48
+ import { toCamelCase } from "../../util/helpers";
49
+ import { onKeyStroke } from "@vueuse/core";
50
+
51
+ const emit = defineEmits<{
52
+ (e: "hoverBlock", id: string | null): void;
53
+ (e: "clickBlock", block: Block | null): void;
54
+ }>();
55
+
56
+ const props = defineProps<{
57
+ block: Block;
58
+ blockIndex: number;
59
+ activeBlock: Block | null;
60
+ hoveredBlockId: string | null;
61
+ }>();
62
+
63
+ const blockComponentRef = useTemplateRef<HTMLElement | null>("blockComponentRef");
64
+
65
+ // Get the margin class for the block
66
+ // Margin is an object with top and bottom properties
67
+ // margin classses are formatted as `margin-<direction>-<size>`
68
+ function getMarginClass(block: Block): string {
69
+ const top = block.margin?.top || "none";
70
+ const bottom = block.margin?.bottom || "none";
71
+
72
+ // Map margin sizes to custom class names: none, sm, md, lg, xl
73
+ const getClass = (size: string, direction: "top" | "bottom"): string => {
74
+ const normalizedSize = size === "small" ? "sm" : size === "medium" ? "md" : size === "large" ? "lg" : size;
75
+ return `margin-${direction}-${normalizedSize}`;
76
+ };
77
+
78
+ return [getClass(top, "top"), getClass(bottom, "bottom")].join(" ");
79
+ }
80
+
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
+ // On escape key press
92
+ onKeyStroke("Escape", () => {
93
+ if (!props.activeBlock) return;
94
+ // Unset active block
95
+ emit("clickBlock", null);
96
+ });
97
+ </script>
98
+
99
+ <style scoped lang="scss">
100
+ @use "../../assets/styles/mixins" as *;
101
+
102
+ .block-wrapper {
103
+ position: relative;
104
+ transition: all 0.3s ease;
105
+
106
+ // Margin spacing overlay
107
+ &::before,
108
+ &::after {
109
+ position: absolute;
110
+ left: 0;
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ width: 100%;
115
+ height: 0;
116
+ font-size: 14px;
117
+ font-weight: 500;
118
+ color: #888017;
119
+ content: "Spacing";
120
+ background-color: #f7efac;
121
+ border: 2px dashed #d2c564;
122
+ border-radius: 7px;
123
+ opacity: 0;
124
+ transform: scaleY(0.9) scaleX(0.98);
125
+ transition: all 0.3s ease;
126
+ }
127
+
128
+ .block-component {
129
+ position: relative;
130
+ cursor: pointer;
131
+
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
+ }
147
+ }
148
+
149
+ // Active state
150
+ &.active-block {
151
+ .block-component {
152
+ &::before {
153
+ background-color: #9fd0f643;
154
+ border-color: #638ef1;
155
+ opacity: 1;
156
+ }
157
+ }
158
+
159
+ // Show the margin spacing overlay
160
+ &::after,
161
+ &::before {
162
+ opacity: 1;
163
+ }
164
+ }
165
+
166
+ // Hovered state
167
+ &.hovered-block {
168
+ .block-component {
169
+ &::before {
170
+ background-color: #9fd0f643;
171
+ border-color: #638ef1;
172
+ opacity: 1;
173
+ }
174
+ }
175
+
176
+ // Show the margin spacing overlay
177
+ &::after,
178
+ &::before {
179
+ opacity: 1;
180
+ }
181
+ }
182
+
183
+ // Block margin classes - generated using mixin
184
+ @include block-margin-classes;
185
+ }
186
+ </style>