@bagelink/blox 1.5.3
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/LICENSE +21 -0
- package/README.md +844 -0
- package/components/base/Button.vue +140 -0
- package/components/base/Container.vue +64 -0
- package/components/base/Image.vue +75 -0
- package/components/base/Spacer.vue +33 -0
- package/components/base/Text.vue +37 -0
- package/components/base/Title.vue +55 -0
- package/components/index.ts +20 -0
- package/config/baseComponents.ts +342 -0
- package/core/communication.ts +140 -0
- package/core/registry.ts +108 -0
- package/core/renderer.ts +217 -0
- package/core/types.ts +148 -0
- package/dist/blox.css +296 -0
- package/dist/components/base/Button.vue.d.ts +26 -0
- package/dist/components/base/Button.vue.d.ts.map +1 -0
- package/dist/components/base/Container.vue.d.ts +37 -0
- package/dist/components/base/Container.vue.d.ts.map +1 -0
- package/dist/components/base/Image.vue.d.ts +26 -0
- package/dist/components/base/Image.vue.d.ts.map +1 -0
- package/dist/components/base/Spacer.vue.d.ts +16 -0
- package/dist/components/base/Spacer.vue.d.ts.map +1 -0
- package/dist/components/base/Text.vue.d.ts +13 -0
- package/dist/components/base/Text.vue.d.ts.map +1 -0
- package/dist/components/base/Title.vue.d.ts +14 -0
- package/dist/components/base/Title.vue.d.ts.map +1 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/config/baseComponents.d.ts +39 -0
- package/dist/config/baseComponents.d.ts.map +1 -0
- package/dist/core/communication.d.ts +44 -0
- package/dist/core/communication.d.ts.map +1 -0
- package/dist/core/registry.d.ts +56 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/renderer.d.ts +27 -0
- package/dist/core/renderer.d.ts.map +1 -0
- package/dist/core/types.d.ts +105 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.cjs +1305 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1260 -0
- package/dist/setup.d.ts +24 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/utils/normalizer.d.ts +18 -0
- package/dist/utils/normalizer.d.ts.map +1 -0
- package/dist/utils/styles.d.ts +13 -0
- package/dist/utils/styles.d.ts.map +1 -0
- package/dist/views/ExternalPreview.vue.d.ts +12 -0
- package/dist/views/ExternalPreview.vue.d.ts.map +1 -0
- package/dist/views/RenderPage.vue.d.ts +10 -0
- package/dist/views/RenderPage.vue.d.ts.map +1 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.d.ts.map +1 -0
- package/index.ts +27 -0
- package/package.json +94 -0
- package/setup.ts +56 -0
- package/utils/normalizer.ts +74 -0
- package/utils/styles.ts +228 -0
- package/views/ExternalPreview.vue +420 -0
- package/views/RenderPage.vue +127 -0
|
@@ -0,0 +1,420 @@
|
|
|
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>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Render Page View
|
|
4
|
+
*
|
|
5
|
+
* This component renders a published page without editor functionality.
|
|
6
|
+
* Use this for production rendering on external sites.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PageData } from '../core/types'
|
|
10
|
+
import { ref, onMounted, computed } from 'vue'
|
|
11
|
+
import { getAllComponents } from '../core/registry'
|
|
12
|
+
import { initializePage } from '../core/renderer'
|
|
13
|
+
import { normalizeComponentData } from '../utils/normalizer'
|
|
14
|
+
import { generateBlockStyles, getResponsiveClasses } from '../utils/styles'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
pageData: PageData
|
|
18
|
+
mobileBreakpoint?: number // Window width to consider mobile (default: 910px)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
22
|
+
mobileBreakpoint: 910,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// State
|
|
26
|
+
const isMobile = ref(false)
|
|
27
|
+
const loading = ref(true)
|
|
28
|
+
const updateCounter = ref(0)
|
|
29
|
+
|
|
30
|
+
// Get registered components
|
|
31
|
+
const registeredComponents = getAllComponents()
|
|
32
|
+
|
|
33
|
+
// Reactive computed for block styles
|
|
34
|
+
const getBlockStyles = computed(() => {
|
|
35
|
+
// eslint-disable-next-line no-unused-vars
|
|
36
|
+
const _ = updateCounter.value
|
|
37
|
+
return (data: Record<string, any>, mobile: boolean = false) => generateBlockStyles(data, mobile)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Update mobile status based on window width
|
|
42
|
+
*/
|
|
43
|
+
function updateMobileStatus() {
|
|
44
|
+
const wasMobile = isMobile.value
|
|
45
|
+
isMobile.value = window.innerWidth <= props.mobileBreakpoint
|
|
46
|
+
|
|
47
|
+
if (wasMobile !== isMobile.value) {
|
|
48
|
+
updateCounter.value++
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onMounted(async () => {
|
|
53
|
+
console.log('🚀 Render Page mounted')
|
|
54
|
+
console.log('📦 Registered components:', Object.keys(registeredComponents))
|
|
55
|
+
|
|
56
|
+
// Initialize page with settings and fonts
|
|
57
|
+
await initializePage(props.pageData)
|
|
58
|
+
|
|
59
|
+
// Set mobile status
|
|
60
|
+
updateMobileStatus()
|
|
61
|
+
|
|
62
|
+
// Listen for window resize
|
|
63
|
+
window.addEventListener('resize', updateMobileStatus)
|
|
64
|
+
|
|
65
|
+
loading.value = false
|
|
66
|
+
})
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<div class="blox-page-wrapper">
|
|
71
|
+
<div v-if="loading" class="blox-loading">
|
|
72
|
+
<p>Loading...</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<template v-else>
|
|
76
|
+
<template v-for="comp in pageData.components" :key="`${comp.id}-${updateCounter}`">
|
|
77
|
+
<div
|
|
78
|
+
:id="comp.data.customId" :style="getBlockStyles(comp.data, isMobile)"
|
|
79
|
+
:class="getResponsiveClasses(comp.data)"
|
|
80
|
+
>
|
|
81
|
+
<component
|
|
82
|
+
:is="registeredComponents[comp.type]" v-if="registeredComponents[comp.type]"
|
|
83
|
+
v-bind="{ ...normalizeComponentData(comp.data), isMobile }"
|
|
84
|
+
/>
|
|
85
|
+
<div v-else class="blox-missing-component">
|
|
86
|
+
Component not found: {{ comp.type }}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
</template>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<style>
|
|
95
|
+
/* Global styles for the Blox page */
|
|
96
|
+
.blox-page-wrapper {
|
|
97
|
+
min-height: 100vh;
|
|
98
|
+
background-color: #fff;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.blox-page-wrapper * {
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
|
|
106
|
+
<style scoped>
|
|
107
|
+
/* Loading state */
|
|
108
|
+
.blox-loading {
|
|
109
|
+
display: flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
min-height: 100vh;
|
|
113
|
+
font-size: 1.2rem;
|
|
114
|
+
color: #666;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Missing component warning */
|
|
118
|
+
.blox-missing-component {
|
|
119
|
+
padding: 2rem;
|
|
120
|
+
background: #fee;
|
|
121
|
+
border: 2px solid #faa;
|
|
122
|
+
border-radius: 8px;
|
|
123
|
+
text-align: center;
|
|
124
|
+
font-size: 1rem;
|
|
125
|
+
color: #c00;
|
|
126
|
+
}
|
|
127
|
+
</style>
|