@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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +844 -0
  3. package/components/base/Button.vue +140 -0
  4. package/components/base/Container.vue +64 -0
  5. package/components/base/Image.vue +75 -0
  6. package/components/base/Spacer.vue +33 -0
  7. package/components/base/Text.vue +37 -0
  8. package/components/base/Title.vue +55 -0
  9. package/components/index.ts +20 -0
  10. package/config/baseComponents.ts +342 -0
  11. package/core/communication.ts +140 -0
  12. package/core/registry.ts +108 -0
  13. package/core/renderer.ts +217 -0
  14. package/core/types.ts +148 -0
  15. package/dist/blox.css +296 -0
  16. package/dist/components/base/Button.vue.d.ts +26 -0
  17. package/dist/components/base/Button.vue.d.ts.map +1 -0
  18. package/dist/components/base/Container.vue.d.ts +37 -0
  19. package/dist/components/base/Container.vue.d.ts.map +1 -0
  20. package/dist/components/base/Image.vue.d.ts +26 -0
  21. package/dist/components/base/Image.vue.d.ts.map +1 -0
  22. package/dist/components/base/Spacer.vue.d.ts +16 -0
  23. package/dist/components/base/Spacer.vue.d.ts.map +1 -0
  24. package/dist/components/base/Text.vue.d.ts +13 -0
  25. package/dist/components/base/Text.vue.d.ts.map +1 -0
  26. package/dist/components/base/Title.vue.d.ts +14 -0
  27. package/dist/components/base/Title.vue.d.ts.map +1 -0
  28. package/dist/components/index.d.ts +18 -0
  29. package/dist/components/index.d.ts.map +1 -0
  30. package/dist/config/baseComponents.d.ts +39 -0
  31. package/dist/config/baseComponents.d.ts.map +1 -0
  32. package/dist/core/communication.d.ts +44 -0
  33. package/dist/core/communication.d.ts.map +1 -0
  34. package/dist/core/registry.d.ts +56 -0
  35. package/dist/core/registry.d.ts.map +1 -0
  36. package/dist/core/renderer.d.ts +27 -0
  37. package/dist/core/renderer.d.ts.map +1 -0
  38. package/dist/core/types.d.ts +105 -0
  39. package/dist/core/types.d.ts.map +1 -0
  40. package/dist/index.cjs +1305 -0
  41. package/dist/index.d.ts +16 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.mjs +1260 -0
  44. package/dist/setup.d.ts +24 -0
  45. package/dist/setup.d.ts.map +1 -0
  46. package/dist/utils/normalizer.d.ts +18 -0
  47. package/dist/utils/normalizer.d.ts.map +1 -0
  48. package/dist/utils/styles.d.ts +13 -0
  49. package/dist/utils/styles.d.ts.map +1 -0
  50. package/dist/views/ExternalPreview.vue.d.ts +12 -0
  51. package/dist/views/ExternalPreview.vue.d.ts.map +1 -0
  52. package/dist/views/RenderPage.vue.d.ts +10 -0
  53. package/dist/views/RenderPage.vue.d.ts.map +1 -0
  54. package/dist/vite.config.d.ts +3 -0
  55. package/dist/vite.config.d.ts.map +1 -0
  56. package/index.ts +27 -0
  57. package/package.json +94 -0
  58. package/setup.ts +56 -0
  59. package/utils/normalizer.ts +74 -0
  60. package/utils/styles.ts +228 -0
  61. package/views/ExternalPreview.vue +420 -0
  62. 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>