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
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <!-- Control bar -->
3
+ <div class="flex divide-x border-b">
4
+ <slot name="default">
5
+ <!-- no default toolbar content -->
6
+ </slot>
7
+
8
+ <!-- Desktop / Mobile view toggle -->
9
+ <div v-if="hasPageSettings" class="ml-auto inline-flex gap-2 px-5 py-2.5">
10
+ <button
11
+ class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
12
+ :class="
13
+ showPageSettings
14
+ ? 'bg-blue-50 text-blue-700 border-blue-800/20'
15
+ : 'bg-zinc-50 text-zinc-500 hover:border-zinc-400 hover:text-zinc-900'
16
+ "
17
+ title="Page settings"
18
+ @click="
19
+ showPageSettings = !showPageSettings;
20
+ activeBlock = null;
21
+ "
22
+ >
23
+ <Cog6ToothIcon class="size-4" />
24
+ </button>
25
+ </div>
26
+ <div class="inline-flex gap-2 px-5 py-2.5">
27
+ <button
28
+ class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
29
+ :class="
30
+ editorViewport === 'mobile'
31
+ ? 'bg-blue-50 text-blue-700 border-blue-800/20'
32
+ : 'bg-zinc-50 text-zinc-500 hover:border-zinc-400 hover:text-zinc-900'
33
+ "
34
+ title="Mobile view"
35
+ @click="editorViewport = 'mobile'"
36
+ >
37
+ <DevicePhoneMobileIcon class="size-4" />
38
+ </button>
39
+ <button
40
+ class="inline-flex items-center rounded-md border px-3 py-2 text-xs"
41
+ :class="
42
+ editorViewport === 'desktop'
43
+ ? 'bg-blue-50 text-blue-700 border-blue-800/20'
44
+ : 'bg-zinc-50 text-zinc-500 hover:border-zinc-400 hover:text-zinc-900'
45
+ "
46
+ title="Desktop view"
47
+ @click="editorViewport = 'desktop'"
48
+ >
49
+ <ComputerDesktopIcon class="size-4" />
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </template>
54
+ <script setup lang="ts">
55
+ const editorViewport = defineModel<"desktop" | "mobile">("editorViewport");
56
+ const activeBlock = defineModel<any>("activeBlock");
57
+ const showPageSettings = defineModel<boolean>("showPageSettings");
58
+ import { ComputerDesktopIcon, DevicePhoneMobileIcon, Cog6ToothIcon } from "@heroicons/vue/24/outline";
59
+
60
+ defineProps<{
61
+ hasPageSettings: boolean;
62
+ }>();
63
+ </script>
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <div id="page-blocks-wrapper">
3
+ <div v-for="block in blocks" :key="block.id" class="block-wrapper" :class="{ [getMarginClass(block)]: true }">
4
+ <component :is="getBlock(block.type)" v-bind="block" :key="`block-${block.id}`" />
5
+ </div>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { type Component } from "vue";
11
+ import { generateNameVariations } from "../../util/helpers";
12
+ import { blockModules } from "./blockModules";
13
+ import type { Block } from "../../types/Block";
14
+
15
+ defineProps<{
16
+ blocks: Block[];
17
+ }>();
18
+
19
+ function getBlock(blockType: string): Component | undefined {
20
+ // Generate name variations and try to find a match in blockModules keys (file paths)
21
+ const nameVariations = generateNameVariations(blockType);
22
+
23
+ // Iterate through all blockModules entries
24
+ for (const [filePath, module] of Object.entries(blockModules)) {
25
+ // Check if any variation matches the file path
26
+ for (const variation of nameVariations) {
27
+ // Check if the file path contains the variation followed by .vue
28
+ // e.g., "hero-section" matches "blocks/hero-section/hero-section.vue"
29
+ if (filePath.includes(`${variation}.vue`)) {
30
+ // Extract the default export (the Vue component)
31
+ const component = (module as any).default;
32
+ if (component) {
33
+ return component;
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ return undefined;
40
+ }
41
+
42
+ // Get the margin class for the block
43
+ // Margin is an object with top and bottom properties
44
+ // margin classses are formatted as `margin-<direction>-<size>`
45
+ function getMarginClass(block: Block): string {
46
+ const top = block.margin?.top || "none";
47
+ const bottom = block.margin?.bottom || "none";
48
+
49
+ // Map margin sizes to custom class names: none, sm, md, lg, xl
50
+ const getClass = (size: string, direction: "top" | "bottom"): string => {
51
+ const normalizedSize = size === "small" ? "sm" : size === "medium" ? "md" : size === "large" ? "lg" : size;
52
+ return `margin-${direction}-${normalizedSize}`;
53
+ };
54
+
55
+ return [getClass(top, "top"), getClass(bottom, "bottom")].join(" ");
56
+ }
57
+ </script>
58
+
59
+ <style scoped lang="scss">
60
+ @use "../../assets/styles/mixins" as *;
61
+
62
+ .block-wrapper {
63
+ @include block-margin-classes;
64
+ }
65
+ </style>
@@ -0,0 +1,9 @@
1
+ // Alternative 1: Use relative paths instead of aliases
2
+ // This works because the consuming app processes this file
3
+ // The path is relative to where the consuming app's vite.config.ts is located
4
+ // For participant/admin projects, this resolves to ../../page-builder/blocks/**/*.vue
5
+ export const blockModules = import.meta.glob("../../page-builder/blocks/**/*.vue", { eager: true });
6
+
7
+ // However, this is fragile because it depends on the file location relative to the consuming app
8
+ // Better: Use a build-time script (see Alternative 2)
9
+
@@ -0,0 +1,19 @@
1
+ // Alternative 4: Manual explicit imports
2
+ // Most explicit, but requires updating when adding new blocks
3
+
4
+ import HeroSection from "../../../../page-builder/blocks/hero-section/hero-section.vue";
5
+ import CardsSection from "../../../../page-builder/blocks/cards-section/cards-section.vue";
6
+ import FaqSection from "../../../../page-builder/blocks/faq-section/faq-section.vue";
7
+ import FlexibleContent from "../../../../page-builder/blocks/flexible-content/flexible-content.vue";
8
+ import TabsSection from "../../../../page-builder/blocks/tabs-section/tabs-section.vue";
9
+ import TextSection from "../../../../page-builder/blocks/text-section/text-section.vue";
10
+
11
+ export const blockModules = {
12
+ "hero-section": HeroSection,
13
+ "cards-section": CardsSection,
14
+ "faq-section": FaqSection,
15
+ "flexible-content": FlexibleContent,
16
+ "tabs-section": TabsSection,
17
+ "text-section": TextSection,
18
+ };
19
+
@@ -0,0 +1,23 @@
1
+ // Alternative 3: Runtime registration pattern
2
+ // Components register themselves when imported
3
+
4
+ import { ref, type Ref } from "vue";
5
+ import type { Component } from "vue";
6
+
7
+ // Registry store
8
+ export const blockRegistry: Ref<Record<string, Component>> = ref({});
9
+
10
+ // Registration function
11
+ export function registerBlock(type: string, component: Component) {
12
+ blockRegistry.value[type] = component;
13
+ }
14
+
15
+ // In each block component, add:
16
+ // import { registerBlock } from "./blockModules-runtime";
17
+ // registerBlock("hero-section", defineComponent({ ... }));
18
+
19
+ // Then manually import blocks:
20
+ // import "./blocks/hero-section/hero-section.vue";
21
+ // import "./blocks/cards-section/cards-section.vue";
22
+ // etc.
23
+
@@ -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 blockModules = import.meta.glob("@page-builder/blocks/**/*.vue", { eager: true });
@@ -0,0 +1,86 @@
1
+ <template>
2
+ <div class="page-settings">
3
+ <div class="flex items-start justify-between border-b bg-white p-5">
4
+ <div>
5
+ <button
6
+ class="cursor-pointer text-sm text-zinc-500 hover:text-zinc-900 hover:underline"
7
+ @click="emit('close')"
8
+ >
9
+ ← Back
10
+ </button>
11
+ <h4 class="mt-1 text-lg font-bold">Page settings</h4>
12
+ </div>
13
+ </div>
14
+ <div class="border-b p-5">
15
+ <!-- Page layout -->
16
+ <div class="editor-field-node">
17
+ <!-- Label -->
18
+ <div class="mb-1.5 flex items-center gap-1.5">
19
+ <label class="mr-auto font-medium first-letter:uppercase">Page layout</label>
20
+ </div>
21
+
22
+ <select v-model="pageData.settings.layout" class="form-control" @change="getLayoutSettings">
23
+ <option v-for="layout in availableLayouts" :key="`layout-${layout.__name}`" :value="layout.__name">
24
+ {{ layout.label }}
25
+ </option>
26
+ </select>
27
+ </div>
28
+ </div>
29
+ <!-- Page settings -->
30
+ <div class="editor-field-node">
31
+ <BlockEditorFields
32
+ v-model="pageData.settings"
33
+ :fields="pageSettingsFields"
34
+ :editable="true"
35
+ :isLayoutBlock="true"
36
+ />
37
+ </div>
38
+ </div>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import { computed, onBeforeMount, onMounted, ref } from "vue";
43
+ import { getLayoutFields, getLayouts } from "../../util/registry";
44
+ import BlockEditorFields from "../BlockEditorFields/BlockEditorFields.vue";
45
+
46
+ const emit = defineEmits<{
47
+ (e: "close"): void;
48
+ }>();
49
+
50
+ const pageData = defineModel<any>();
51
+ const pageSettingsFields = ref<any>({});
52
+
53
+ defineProps<{
54
+ title?: string;
55
+ }>();
56
+
57
+ const availableLayouts = computed(() => {
58
+ return getLayouts();
59
+ });
60
+
61
+ function getLayoutSettings() {
62
+ if (!pageData.value.settings.layout) return;
63
+ pageSettingsFields.value = getLayoutFields(pageData.value.settings.layout) || null;
64
+ }
65
+
66
+ onBeforeMount(() => {
67
+ if (!pageData.value.settings) {
68
+ pageData.value.settings = null;
69
+ }
70
+ });
71
+
72
+ onMounted(() => {
73
+ getLayoutSettings();
74
+ });
75
+ </script>
76
+
77
+ <style scoped lang="scss">
78
+ select.form-control {
79
+ padding-right: 32px;
80
+ appearance: none;
81
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); // down chevron
82
+ background-repeat: no-repeat;
83
+ background-position: right 8px center;
84
+ background-size: 20px;
85
+ }
86
+ </style>
@@ -0,0 +1,105 @@
1
+ <template>
2
+ <div
3
+ id="page-builder-resize-handle"
4
+ ref="resizeHandle"
5
+ class="resize-handle shrink-0 cursor-col-resize transition-colors duration-200"
6
+ @mousedown="startResize"
7
+ ></div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { ref } from "vue";
12
+ import { defineEmits } from "vue";
13
+
14
+ const emit = defineEmits<{
15
+ (e: "sidebarWidth", width: number): void;
16
+ }>();
17
+
18
+ const resizeHandle = ref<HTMLElement | null>(null);
19
+ const sidebarWidth = ref(380); // Default sidebar width (380px)
20
+ const isResizing = ref(false);
21
+
22
+ // Resize functionality
23
+ function startResize(event: MouseEvent) {
24
+ isResizing.value = true;
25
+ event.preventDefault();
26
+
27
+ const startX = event.clientX;
28
+ const startSidebarWidth = sidebarWidth.value;
29
+
30
+ function handleMouseMove(event: MouseEvent) {
31
+ if (!isResizing.value) return;
32
+
33
+ const deltaX = event.clientX - startX;
34
+ const newSidebarWidth = Math.max(300, Math.min(800, startSidebarWidth - deltaX)); // Min 300px, Max 800px
35
+
36
+ sidebarWidth.value = newSidebarWidth;
37
+ emit("sidebarWidth", newSidebarWidth);
38
+ }
39
+
40
+ function handleMouseUp() {
41
+ isResizing.value = false;
42
+ document.removeEventListener("mousemove", handleMouseMove);
43
+ document.removeEventListener("mouseup", handleMouseUp);
44
+ document.body.style.cursor = "";
45
+ document.body.style.userSelect = "";
46
+ }
47
+
48
+ document.addEventListener("mousemove", handleMouseMove);
49
+ document.addEventListener("mouseup", handleMouseUp);
50
+ document.body.style.cursor = "col-resize";
51
+ document.body.style.userSelect = "none";
52
+ }
53
+ </script>
54
+
55
+ <style scoped lang="scss">
56
+ /* Resize handle styles */
57
+ .resize-handle {
58
+ position: relative;
59
+ top: 0;
60
+ right: 0;
61
+ z-index: 2;
62
+ width: 3px;
63
+ height: 100%;
64
+ cursor: col-resize;
65
+ background-color: var(--grey-20);
66
+ transition: all 0.2s ease-in-out;
67
+ }
68
+
69
+ .resize-handle:hover {
70
+ width: 3px;
71
+ background-color: var(--yellow-40);
72
+ }
73
+
74
+ .resize-handle:active {
75
+ width: 3px;
76
+ background-color: var(--yellow-50);
77
+ }
78
+
79
+ /* Add a subtle indicator when resizing */
80
+ .resize-handle::before {
81
+ position: absolute;
82
+ top: 50%;
83
+ left: -6px;
84
+ z-index: 20;
85
+ width: 14px;
86
+ height: 40px;
87
+ content: "";
88
+ background-color: var(--yellow-40);
89
+ border-radius: var(--border-radius-16);
90
+ opacity: 0;
91
+ transform: translateY(-50%);
92
+ transition:
93
+ opacity 0.2s ease-in-out,
94
+ background-color 0.2s ease-in-out;
95
+ }
96
+
97
+ .resize-handle:hover::before {
98
+ opacity: 1;
99
+ }
100
+
101
+ .resize-handle:active::before {
102
+ background-color: var(--yellow-50);
103
+ opacity: 1;
104
+ }
105
+ </style>
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import WswgJsonEditor from "./WswgJsonEditor.vue";
4
+
5
+ describe("WswgJsonEditor", () => {
6
+ it("renders the placeholder component", () => {
7
+ const wrapper = mount(WswgJsonEditor);
8
+
9
+ expect(wrapper.text()).toContain("WswgJsonEditor");
10
+ expect(wrapper.text()).toContain("This is a placeholder component");
11
+ });
12
+
13
+ it("renders header slot content when provided", () => {
14
+ const headerContent = "Custom Header Content";
15
+ const wrapper = mount(WswgJsonEditor, {
16
+ slots: {
17
+ header: headerContent,
18
+ },
19
+ });
20
+
21
+ expect(wrapper.text()).toContain(headerContent);
22
+ });
23
+
24
+ it("renders header slot with HTML content", () => {
25
+ const wrapper = mount(WswgJsonEditor, {
26
+ slots: {
27
+ header: "<div class='custom-header'>Header Title</div>",
28
+ },
29
+ });
30
+
31
+ expect(wrapper.find(".custom-header").exists()).toBe(true);
32
+ expect(wrapper.text()).toContain("Header Title");
33
+ });
34
+
35
+ it("does not render header slot when not provided", () => {
36
+ const wrapper = mount(WswgJsonEditor);
37
+
38
+ // The component should still render without the header slot
39
+ expect(wrapper.text()).toContain("WswgJsonEditor");
40
+ // Header slot should be empty/not present
41
+ expect(wrapper.html()).not.toContain("custom-header");
42
+ });
43
+ });