@bagelink/blox 1.5.17 → 1.5.20

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 (109) hide show
  1. package/dist/blox.css +720 -68
  2. package/dist/components/base/Button.vue.d.ts.map +1 -1
  3. package/dist/components/base/Container.vue.d.ts.map +1 -1
  4. package/dist/components/base/Image.vue.d.ts.map +1 -1
  5. package/dist/components/base/Spacer.vue.d.ts.map +1 -1
  6. package/dist/components/base/Text.vue.d.ts.map +1 -1
  7. package/dist/components/base/Title.vue.d.ts.map +1 -1
  8. package/dist/components/blocks/BigImage.vue.d.ts +12 -0
  9. package/dist/components/blocks/BigImage.vue.d.ts.map +1 -0
  10. package/dist/components/blocks/BigQuote.vue.d.ts +14 -0
  11. package/dist/components/blocks/BigQuote.vue.d.ts.map +1 -0
  12. package/dist/components/blocks/BlockFooter.vue.d.ts +17 -0
  13. package/dist/components/blocks/BlockFooter.vue.d.ts.map +1 -0
  14. package/dist/components/blocks/BlockNav.vue.d.ts +22 -0
  15. package/dist/components/blocks/BlockNav.vue.d.ts.map +1 -0
  16. package/dist/components/blocks/Cards.vue.d.ts +18 -0
  17. package/dist/components/blocks/Cards.vue.d.ts.map +1 -0
  18. package/dist/components/blocks/Contact.vue.d.ts +22 -0
  19. package/dist/components/blocks/Contact.vue.d.ts.map +1 -0
  20. package/dist/components/blocks/CountDown.vue.d.ts +76 -0
  21. package/dist/components/blocks/CountDown.vue.d.ts.map +1 -0
  22. package/dist/components/blocks/Cta.vue.d.ts +15 -0
  23. package/dist/components/blocks/Cta.vue.d.ts.map +1 -0
  24. package/dist/components/blocks/Faq.vue.d.ts +20 -0
  25. package/dist/components/blocks/Faq.vue.d.ts.map +1 -0
  26. package/dist/components/blocks/Grid.vue.d.ts +24 -0
  27. package/dist/components/blocks/Grid.vue.d.ts.map +1 -0
  28. package/dist/components/blocks/Icons.vue.d.ts +16 -0
  29. package/dist/components/blocks/Icons.vue.d.ts.map +1 -0
  30. package/dist/components/blocks/IconsList.vue.d.ts +22 -0
  31. package/dist/components/blocks/IconsList.vue.d.ts.map +1 -0
  32. package/dist/components/blocks/ImageCarousel.vue.d.ts +16 -0
  33. package/dist/components/blocks/ImageCarousel.vue.d.ts.map +1 -0
  34. package/dist/components/blocks/ImageLinkBoxes.vue.d.ts +14 -0
  35. package/dist/components/blocks/ImageLinkBoxes.vue.d.ts.map +1 -0
  36. package/dist/components/blocks/Logos.vue.d.ts +17 -0
  37. package/dist/components/blocks/Logos.vue.d.ts.map +1 -0
  38. package/dist/components/blocks/PopUp.vue.d.ts +23 -0
  39. package/dist/components/blocks/PopUp.vue.d.ts.map +1 -0
  40. package/dist/components/blocks/PriceTable.vue.d.ts +24 -0
  41. package/dist/components/blocks/PriceTable.vue.d.ts.map +1 -0
  42. package/dist/components/blocks/Space.vue.d.ts +25 -0
  43. package/dist/components/blocks/Space.vue.d.ts.map +1 -0
  44. package/dist/components/blocks/Tabs.vue.d.ts +15 -0
  45. package/dist/components/blocks/Tabs.vue.d.ts.map +1 -0
  46. package/dist/components/blocks/Team.vue.d.ts +19 -0
  47. package/dist/components/blocks/Team.vue.d.ts.map +1 -0
  48. package/dist/components/blocks/Testimonials.vue.d.ts +18 -0
  49. package/dist/components/blocks/Testimonials.vue.d.ts.map +1 -0
  50. package/dist/components/blocks/Text.vue.d.ts +10 -0
  51. package/dist/components/blocks/Text.vue.d.ts.map +1 -0
  52. package/dist/components/blocks/TextImage.vue.d.ts +20 -0
  53. package/dist/components/blocks/TextImage.vue.d.ts.map +1 -0
  54. package/dist/components/blocks/TextSide.vue.d.ts +19 -0
  55. package/dist/components/blocks/TextSide.vue.d.ts.map +1 -0
  56. package/dist/components/blocks/Title.vue.d.ts +16 -0
  57. package/dist/components/blocks/Title.vue.d.ts.map +1 -0
  58. package/dist/components/blocks/TitleSide.vue.d.ts +21 -0
  59. package/dist/components/blocks/TitleSide.vue.d.ts.map +1 -0
  60. package/dist/components/blocks/VideoBox.vue.d.ts +15 -0
  61. package/dist/components/blocks/VideoBox.vue.d.ts.map +1 -0
  62. package/dist/components/blocks/blocks.d.ts +27 -0
  63. package/dist/components/blocks/blocks.d.ts.map +1 -0
  64. package/dist/components/index.d.ts +4 -3
  65. package/dist/components/index.d.ts.map +1 -1
  66. package/dist/config/baseComponents.d.ts.map +1 -1
  67. package/dist/config/blockComponents.d.ts +41 -0
  68. package/dist/config/blockComponents.d.ts.map +1 -0
  69. package/dist/core/communication.d.ts.map +1 -1
  70. package/dist/core/registry.d.ts.map +1 -1
  71. package/dist/core/renderer.d.ts.map +1 -1
  72. package/dist/core/types.d.ts.map +1 -1
  73. package/dist/index.cjs +9223 -472
  74. package/dist/index.d.ts +2 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.mjs +9124 -458
  77. package/dist/setup.d.ts +4 -4
  78. package/dist/setup.d.ts.map +1 -1
  79. package/dist/utils/componentPreviewGenerator.d.ts +113 -0
  80. package/dist/utils/componentPreviewGenerator.d.ts.map +1 -0
  81. package/dist/utils/normalizer.d.ts.map +1 -1
  82. package/dist/utils/styles.d.ts.map +1 -1
  83. package/dist/views/ExternalPreview.vue.d.ts.map +1 -1
  84. package/dist/views/RenderPage.vue.d.ts.map +1 -1
  85. package/package.json +3 -2
  86. package/components/base/Button.vue +0 -140
  87. package/components/base/Container.vue +0 -64
  88. package/components/base/Image.vue +0 -75
  89. package/components/base/Spacer.vue +0 -33
  90. package/components/base/Text.vue +0 -37
  91. package/components/base/Title.vue +0 -55
  92. package/components/index.ts +0 -20
  93. package/config/baseComponents.ts +0 -364
  94. package/core/communication.ts +0 -140
  95. package/core/registry.ts +0 -108
  96. package/core/renderer.ts +0 -217
  97. package/core/types.ts +0 -148
  98. package/dist/example-setup.d.ts +0 -1
  99. package/dist/example-setup.d.ts.map +0 -1
  100. package/dist/styles.d.ts +0 -8
  101. package/dist/styles.d.ts.map +0 -1
  102. package/dist/vite.config.d.ts +0 -3
  103. package/dist/vite.config.d.ts.map +0 -1
  104. package/index.ts +0 -43
  105. package/setup.ts +0 -472
  106. package/utils/normalizer.ts +0 -74
  107. package/utils/styles.ts +0 -228
  108. package/views/ExternalPreview.vue +0 -415
  109. package/views/RenderPage.vue +0 -127
package/utils/styles.ts DELETED
@@ -1,228 +0,0 @@
1
- /**
2
- * Generate CSS styles from block data
3
- */
4
- export function generateBlockStyles(
5
- data: Record<string, any>,
6
- isMobile: boolean = false
7
- ): Record<string, string> {
8
- const styles: Record<string, string> = {}
9
-
10
- // Helper to coerce numeric-like values
11
- function toNumber(v: any): number | null {
12
- if (v === undefined || v === null || v === '') return null
13
- const n = Number(v)
14
- return Number.isNaN(n) ? null : n
15
- }
16
-
17
- // Helper to get value with mobile override
18
- function getValue(desktopKey: string, mobileKey: string) {
19
- if (isMobile && data[mobileKey] !== undefined && data[mobileKey] !== null) {
20
- return data[mobileKey]
21
- }
22
- return data[desktopKey]
23
- }
24
-
25
- // Margin and padding (use rem units) with mobile overrides
26
- const mTop = toNumber(getValue('marginTop', 'marginTopMobile'))
27
- const mBottom = toNumber(getValue('marginBottom', 'marginBottomMobile'))
28
- const pad = toNumber(getValue('padding', 'paddingMobile'))
29
-
30
- // Set margin values
31
- if (mTop !== null) {
32
- styles['margin-top'] = `${mTop}rem`
33
- styles['--margin-top'] = `${mTop}rem`
34
- } else {
35
- styles['--margin-top'] = '0rem'
36
- }
37
-
38
- if (mBottom !== null) {
39
- styles['margin-bottom'] = `${mBottom}rem`
40
- styles['--margin-bottom'] = `${mBottom}rem`
41
- } else {
42
- styles['--margin-bottom'] = '0rem'
43
- }
44
-
45
- if (pad !== null) {
46
- styles.padding = `${pad}rem`
47
- styles['--block-padding'] = `${pad}rem`
48
- }
49
-
50
- // Width behavior
51
- const wDesktop = toNumber(data.width)
52
- const wMobile = toNumber(data.widthMobile)
53
- const wPercentDesktop = toNumber(data.widthPercent) || 96
54
- const wPercentMobile = toNumber(data.widthPercentMobile) || wPercentDesktop
55
- const fullWidthDesktop = data.fullWidth
56
-
57
- // Set CSS variables for responsive width
58
- styles['--max-width-desktop'] = wDesktop !== null ? `${wDesktop}px` : '800px'
59
- styles['--max-width-mobile'] = wMobile !== null ? `${wMobile}px` : wDesktop !== null ? `${wDesktop}px` : '800px'
60
- styles['--width-percent-desktop'] = `${wPercentDesktop}%`
61
- styles['--width-percent-mobile'] = `${wPercentMobile}%`
62
-
63
- // Set margin centering for non-full-width blocks
64
- if (!(fullWidthDesktop === true || fullWidthDesktop === 'true') && wDesktop !== null) {
65
- styles['margin-left'] = 'auto'
66
- styles['margin-right'] = 'auto'
67
- }
68
-
69
- // Border styles
70
- const borderW = toNumber(getValue('borderWidth', 'borderWidthMobile'))
71
- if (borderW !== null && borderW > 0) {
72
- styles['border-width'] = `${borderW}px`
73
- styles['border-style'] = getValue('borderStyle', 'borderStyleMobile') || 'solid'
74
- const borderColor = getValue('borderColor', 'borderColorMobile')
75
- if (borderColor) {
76
- styles['border-color'] = borderColor
77
- }
78
- }
79
-
80
- // Center alignment
81
- const isCenter = getValue('center', 'centerMobile')
82
- if (isCenter === true || isCenter === 'true') {
83
- styles['text-align'] = 'center'
84
- }
85
-
86
- // Border radius
87
- const borderR = toNumber(getValue('borderRadius', 'borderRadiusMobile'))
88
- if (borderR !== null) {
89
- styles['border-radius'] = `${borderR}px`
90
- }
91
-
92
- // Colors with CSS variables
93
- const bgColorDesktop = data.backgroundColor
94
- const bgColorMobile = data.backgroundColorMobile
95
- const textColorDesktop = data.textColor
96
- const { textColorMobile } = data
97
-
98
- if (bgColorDesktop) {
99
- styles['--bg-color-desktop'] = bgColorDesktop
100
- }
101
- if (bgColorMobile) {
102
- styles['--bg-color-mobile'] = bgColorMobile
103
- }
104
- if (textColorDesktop) {
105
- styles['--text-color-desktop'] = textColorDesktop
106
- }
107
- if (textColorMobile) {
108
- styles['--text-color-mobile'] = textColorMobile
109
- }
110
-
111
- // Shadow types
112
- const shadowMap = {
113
- none: 'none',
114
- sm: '0 0 10px 0 rgba(0, 0, 0, 0.05)',
115
- md: '0 0 20px 0 rgba(0, 0, 0, 0.15)',
116
- lg: '0 0 30px 0 rgba(0, 0, 0, 0.15)',
117
- xl: '0 0 40px 0 rgba(0, 0, 0, 0.5)',
118
- custom: '0 4px 6px -1px rgba(0, 0, 0, 0.15)',
119
- }
120
-
121
- const shadowType = getValue('shadowType', 'shadowTypeMobile')
122
- if (shadowType && shadowType !== 'none') {
123
- styles['box-shadow'] = shadowMap[shadowType as keyof typeof shadowMap] || shadowMap.md
124
- }
125
-
126
- // Z-index
127
- const z = toNumber(getValue('zIndex', 'zIndexMobile'))
128
- if (z !== null && z > 0) {
129
- styles.position = 'relative'
130
- styles['z-index'] = z.toString()
131
- }
132
-
133
- // Visibility
134
- if (!isMobile && data.showDesktop === false) {
135
- styles.display = 'none'
136
- } else if (isMobile && data.showMobile === false) {
137
- styles.display = 'none'
138
- }
139
-
140
- // Font family
141
- const fontFamilyDesktop = data.fontFamily
142
- const { fontFamilyMobile } = data
143
-
144
- if (isMobile && fontFamilyMobile && fontFamilyMobile.trim() !== '') {
145
- styles['font-family'] = `"${fontFamilyMobile}", sans-serif`
146
- } else if (fontFamilyDesktop && fontFamilyDesktop.trim() !== '') {
147
- styles['font-family'] = `"${fontFamilyDesktop}", sans-serif`
148
- }
149
-
150
- // Custom CSS
151
- if (data.customCSS) {
152
- try {
153
- const customRules = data.customCSS
154
- .split(';')
155
- .filter((rule: string) => rule.trim())
156
- .map((rule: string) => rule.trim().split(':'))
157
- .filter((parts: string[]) => parts.length === 2)
158
-
159
- customRules.forEach(([property, value]: [string, string]) => {
160
- const prop = property.trim()
161
- const val = value.trim()
162
- if (prop && val) {
163
- styles[prop] = val
164
- }
165
- })
166
- } catch (e) {
167
- console.warn('Invalid custom CSS:', data.customCSS)
168
- }
169
- }
170
-
171
- return styles
172
- }
173
-
174
- /**
175
- * Generate responsive CSS classes
176
- */
177
- export function getResponsiveClasses(data: Record<string, any>): string[] {
178
- const classes = ['blox-block', 'responsive-colors']
179
-
180
- const effectiveDesktopFullWidth = Boolean(data.fullWidth)
181
- const effectiveMobileFullWidth
182
- = data.fullWidthMobile !== null ? Boolean(data.fullWidthMobile) : effectiveDesktopFullWidth
183
-
184
- if (effectiveDesktopFullWidth) {
185
- classes.push('full-width-desktop')
186
- }
187
-
188
- if (effectiveMobileFullWidth) {
189
- classes.push('full-width-mobile')
190
- }
191
-
192
- return classes
193
- }
194
-
195
- /**
196
- * Generate the responsive CSS to be injected into the page
197
- */
198
- export function getResponsiveCSS(): string {
199
- return `
200
- /* Blox Responsive System */
201
- .responsive-colors {
202
- color: var(--text-color-desktop, inherit);
203
- background-color: var(--bg-color-desktop, transparent);
204
- margin-inline: auto;
205
- max-width: var(--max-width-desktop, 800px);
206
- width: var(--width-percent-desktop, 96%);
207
- }
208
-
209
- .responsive-colors.full-width-desktop {
210
- width: 100% !important;
211
- max-width: none !important;
212
- }
213
-
214
- @media (max-width: 910px) {
215
- .responsive-colors {
216
- color: var(--text-color-mobile, var(--text-color-desktop, inherit));
217
- background-color: var(--bg-color-mobile, var(--bg-color-desktop, transparent));
218
- max-width: var(--max-width-mobile, var(--max-width-desktop, 800px));
219
- width: var(--width-percent-mobile, var(--width-percent-desktop, 96%));
220
- }
221
-
222
- .responsive-colors.full-width-mobile {
223
- width: 100% !important;
224
- max-width: none !important;
225
- }
226
- }
227
- `
228
- }
@@ -1,415 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * External Preview View
4
- *
5
- * This component runs on the target site and communicates with the editor
6
- * via postMessage. It receives component updates and renders them using
7
- * the site's registered components.
8
- */
9
-
10
- import type { ComponentData, PageData } from '../core/types'
11
- import { ref, onMounted, onUnmounted, computed } from 'vue'
12
- import { createCommunicationBridge, type CommunicationBridge } from '../core/communication'
13
- import { getAllComponents } from '../core/registry'
14
- import { initializePage } from '../core/renderer'
15
- import { normalizeComponentData } from '../utils/normalizer'
16
- import { generateBlockStyles, getResponsiveClasses } from '../utils/styles'
17
-
18
- interface Props {
19
- origin?: string // Allowed editor origin (default: *)
20
- initialPageData?: PageData // Optional initial page data
21
- componentConfigs?: any[] // Component configurations with schemas
22
- }
23
-
24
- const props = withDefaults(defineProps<Props>(), {
25
- origin: '*',
26
- componentConfigs: () => [],
27
- })
28
-
29
- // State
30
- const components = ref<ComponentData[]>([])
31
- const highlightID = ref('')
32
- const selectedID = ref('')
33
- const isMobile = ref(false)
34
- const previewMode = ref(false)
35
- const linksDisabled = ref(false)
36
- const loading = ref(true)
37
- const updateCounter = ref(0)
38
-
39
- // Communication bridge
40
- let bridge: CommunicationBridge | null = null
41
-
42
- // Get registered components
43
- const registeredComponents = getAllComponents()
44
-
45
- // Reactive computed for block styles
46
- const getBlockStyles = computed(() => {
47
- // Access updateCounter to trigger reactivity
48
- console.log('updateCounter', updateCounter.value)
49
- return (data: Record<string, any>, mobile: boolean = false) => generateBlockStyles(data, mobile)
50
- })
51
-
52
- /**
53
- * Handle focus on a block
54
- */
55
- function setFocus(id: string, emit = false) {
56
- selectedID.value = id
57
- highlightID.value = id
58
-
59
- const el = document.querySelector(`[data-block-id="${id}"]`)
60
- el?.scrollIntoView({ behavior: 'smooth', block: 'center' })
61
-
62
- if (emit && bridge) {
63
- bridge.send('focus', id)
64
- }
65
- }
66
-
67
- /**
68
- * Handle highlight on a block
69
- */
70
- function setHighlight(id: string, emit = false) {
71
- highlightID.value = id
72
-
73
- if (emit && bridge) {
74
- bridge.send('highlight', id)
75
- }
76
- }
77
-
78
- /**
79
- * Handle keyboard navigation
80
- */
81
- function keyboardHandler(event: KeyboardEvent) {
82
- if (previewMode.value) return
83
-
84
- if (event.key === 'ArrowDown') {
85
- const next = components.value.findIndex(comp => comp.id === highlightID.value) + 1
86
- if (next < components.value.length) {
87
- setFocus(components.value[next].id, true)
88
- }
89
- } else if (event.key === 'ArrowUp') {
90
- const prev = components.value.findIndex(comp => comp.id === highlightID.value) - 1
91
- if (prev >= 0) {
92
- setFocus(components.value[prev].id, true)
93
- }
94
- } else if (event.key === 'Enter') {
95
- if (bridge) bridge.send('focus', highlightID.value)
96
- } else if (event.key === 'Escape') {
97
- setHighlight('')
98
- selectedID.value = ''
99
- } else if (event.key === 'Delete') {
100
- if (bridge) bridge.send('delete', highlightID.value)
101
- }
102
- }
103
-
104
- /**
105
- * Handle click on page (prevent navigation in edit mode)
106
- */
107
- function clickHandler(event: MouseEvent) {
108
- if (!linksDisabled.value) return
109
-
110
- let element: HTMLElement | null = event.target as HTMLElement
111
-
112
- while (element) {
113
- if (element.tagName === 'A' && element.getAttribute('href')) {
114
- const isButton
115
- = element.classList.contains('btn')
116
- || element.classList.contains('button')
117
- || element.getAttribute('role') === 'button'
118
-
119
- if (!isButton) {
120
- event.preventDefault()
121
- event.stopPropagation()
122
- return false
123
- } else {
124
- event.preventDefault()
125
- return false
126
- }
127
- }
128
- element = element.parentElement
129
- }
130
- }
131
-
132
- /**
133
- * Setup communication bridge
134
- */
135
- function setupBridge() {
136
- bridge = createCommunicationBridge({
137
- origin: props.origin,
138
- targetWindow: window.parent,
139
- })
140
-
141
- // Handle update messages
142
- bridge.on('update', ({ message, data, isMobile: mobile }) => {
143
- // Data can be in message.data or at top level (for backward compatibility)
144
- const componentData = message?.data || data
145
- const mobileMode = message?.isMobile !== undefined ? message.isMobile : mobile
146
-
147
- if (componentData) {
148
- components.value = [...componentData]
149
- updateCounter.value++
150
- console.log('📦 Updated components:', components.value.length)
151
- }
152
-
153
- if (mobileMode !== undefined) {
154
- isMobile.value = mobileMode
155
- console.log('📱 Mobile mode:', mobileMode)
156
- }
157
-
158
- loading.value = false
159
- })
160
-
161
- // Handle highlight messages
162
- bridge.on('highlight', ({ message }) => {
163
- setHighlight(message)
164
- })
165
-
166
- // Handle focus messages
167
- bridge.on('focus', ({ message }) => {
168
- setFocus(message)
169
- })
170
-
171
- // Handle preview mode toggle
172
- bridge.on('preview', ({ message }) => {
173
- previewMode.value = message
174
-
175
- if (previewMode.value) {
176
- document.body.classList.add('blox-preview-mode')
177
- document.body.classList.remove('blox-edit-mode')
178
- } else {
179
- document.body.classList.remove('blox-preview-mode')
180
- document.body.classList.add('blox-edit-mode')
181
- }
182
- })
183
-
184
- // Handle link disable toggle
185
- bridge.on('disableLinks', ({ message }) => {
186
- linksDisabled.value = message
187
- })
188
-
189
- // Send ready message with component configurations
190
- const registeredTypes = Object.keys(registeredComponents)
191
-
192
- // Prepare component configs to send to editor
193
- const configsToSend = props.componentConfigs.map(config => ({
194
- id: config.id,
195
- label: config.label,
196
- icon: config.icon,
197
- img: config.img,
198
- order: config.order || 999,
199
- content: config.content || [],
200
- settings: config.settings || [],
201
- }))
202
-
203
- bridge.send('ready', {
204
- registeredTypes,
205
- componentConfigs: configsToSend,
206
- })
207
-
208
- console.log('📤 Sent component configurations:', configsToSend)
209
- }
210
-
211
- onMounted(async () => {
212
- console.log('🚀 External Preview mounted')
213
- console.log('📦 Registered components:', Object.keys(registeredComponents))
214
-
215
- // Setup communication
216
- setupBridge()
217
-
218
- // Add event listeners
219
- window.addEventListener('keydown', keyboardHandler)
220
- document.addEventListener('click', clickHandler, true)
221
-
222
- // Set edit mode initially
223
- document.body.classList.add('blox-edit-mode')
224
-
225
- // Initialize with initial page data if provided
226
- if (props.initialPageData) {
227
- await initializePage(props.initialPageData)
228
- components.value = props.initialPageData.components
229
- loading.value = false
230
- }
231
- })
232
-
233
- onUnmounted(() => {
234
- // Cleanup
235
- if (bridge) {
236
- bridge.destroy()
237
- }
238
-
239
- window.removeEventListener('keydown', keyboardHandler)
240
- document.removeEventListener('click', clickHandler, true)
241
-
242
- document.body.classList.remove('blox-preview-mode', 'blox-edit-mode')
243
- })
244
- </script>
245
-
246
- <template>
247
- <div class="blox-page-wrapper">
248
- <div v-if="loading" class="blox-loading">
249
- <p>Loading preview...</p>
250
- </div>
251
-
252
- <div v-else-if="!components.length && !previewMode" class="blox-empty-state">
253
- <p>No blocks yet. Add a block from the editor to get started!</p>
254
- </div>
255
-
256
- <div v-else-if="!components.length && previewMode" class="blox-empty-preview">
257
- <!-- Empty page in preview mode -->
258
- </div>
259
-
260
- <template v-for="comp in components" :key="comp.id">
261
- <div
262
- class="blox-block-wrapper" :class="{
263
- 'blox-highlight': !previewMode && highlightID === comp.id,
264
- 'blox-selected': !previewMode && selectedID === comp.id,
265
- }" :data-block-id="comp.id" @click="!previewMode ? setFocus(comp.id, true) : null"
266
- @mouseenter="!previewMode ? setHighlight(comp.id, true) : null"
267
- @focus="!previewMode ? setHighlight(comp.id, true) : null"
268
- >
269
- <!-- Block label (edit mode only) -->
270
- <p v-if="!previewMode" class="blox-block-label">
271
- {{ comp.type }}
272
- <span v-if="comp.data?.title"> | {{ comp.data.title }}</span>
273
- </p>
274
-
275
- <!-- Block content -->
276
- <div
277
- :id="comp.data.customId" :key="`${comp.id}-${updateCounter}`"
278
- :style="getBlockStyles(comp.data, isMobile)" :class="getResponsiveClasses(comp.data)"
279
- >
280
- <component
281
- :is="registeredComponents[comp.type]" v-if="registeredComponents[comp.type]"
282
- v-bind="{ ...normalizeComponentData(comp.data), isMobile }"
283
- />
284
- <div v-else class="blox-missing-component">
285
- ⚠️ Component not registered: {{ comp.type }}
286
- </div>
287
- </div>
288
-
289
- <!-- Overlay for click handling (edit mode only) -->
290
- <div v-if="!previewMode" class="blox-block-overlay" @click.self="setFocus(comp.id, true)" />
291
- </div>
292
- </template>
293
- </div>
294
- </template>
295
-
296
- <style>
297
- /* Global styles for the Blox page */
298
- .blox-page-wrapper {
299
- min-height: 100vh;
300
- background-color: #fff;
301
- }
302
-
303
- .blox-page-wrapper * {
304
- box-sizing: border-box;
305
- }
306
- </style>
307
-
308
- <style scoped>
309
- /* Loading state */
310
- .blox-loading {
311
- display: flex;
312
- align-items: center;
313
- justify-content: center;
314
- min-height: 100vh;
315
- font-size: 1.2rem;
316
- color: #666;
317
- }
318
-
319
- /* Empty states */
320
- .blox-empty-state {
321
- display: flex;
322
- align-items: center;
323
- justify-content: center;
324
- min-height: 100vh;
325
- text-align: center;
326
- font-size: 1.2rem;
327
- color: #666;
328
- padding: 2rem;
329
- }
330
-
331
- .blox-empty-preview {
332
- min-height: 100vh;
333
- background: #fff;
334
- }
335
-
336
- /* Block wrapper */
337
- .blox-block-wrapper {
338
- position: relative;
339
- }
340
-
341
- /* Block overlay for click detection */
342
- .blox-block-overlay {
343
- position: absolute;
344
- inset: 0;
345
- pointer-events: none;
346
- z-index: 9;
347
- }
348
-
349
- /* Interactive elements should be clickable */
350
- .blox-block-wrapper button,
351
- .blox-block-wrapper a,
352
- .blox-block-wrapper input,
353
- .blox-block-wrapper textarea,
354
- .blox-block-wrapper select {
355
- pointer-events: auto;
356
- position: relative;
357
- z-index: 10;
358
- }
359
-
360
- /* Preview mode - all interactions work */
361
- .blox-preview-mode .blox-block-wrapper * {
362
- pointer-events: auto !important;
363
- }
364
-
365
- /* Block label */
366
- .blox-block-label {
367
- position: absolute;
368
- top: 0;
369
- left: 0;
370
- font-size: 0.625rem;
371
- padding: 0.25rem 0.5rem;
372
- color: white;
373
- background: #9ca3af;
374
- border-bottom-right-radius: 5px;
375
- z-index: 10001;
376
- opacity: 0;
377
- transition: opacity 0.2s;
378
- }
379
-
380
- /* Highlight state */
381
- .blox-highlight {
382
- background: rgba(156, 163, 175, 0.1);
383
- outline: 1px solid #9ca3af;
384
- outline-offset: -1px;
385
- }
386
-
387
- .blox-highlight .blox-block-label {
388
- opacity: 1;
389
- background: #9ca3af;
390
- }
391
-
392
- /* Selected state */
393
- .blox-selected {
394
- background: rgba(88, 191, 235, 0.08) !important;
395
- outline: 2px solid #3b82f6 !important;
396
- outline-offset: -2px;
397
- box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
398
- }
399
-
400
- .blox-selected .blox-block-label {
401
- opacity: 1;
402
- background: #3b82f6;
403
- }
404
-
405
- /* Missing component warning */
406
- .blox-missing-component {
407
- padding: 2rem;
408
- background: #fef3c7;
409
- border: 2px dashed #f59e0b;
410
- border-radius: 8px;
411
- text-align: center;
412
- font-size: 1rem;
413
- color: #92400e;
414
- }
415
- </style>