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