@bagelink/blox 1.5.15 → 1.5.17

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/setup.ts CHANGED
@@ -1,26 +1,428 @@
1
1
  /**
2
2
  * Blox Setup and Registration
3
3
  *
4
- * Handles automatic registration of base components and provides
5
- * configuration helpers for external projects.
4
+ * Provides a simple, fluent API for setting up the Blox library
6
5
  */
7
6
 
7
+ import type { App } from 'vue'
8
+ import type { Router } from 'vue-router'
8
9
  import { baseComponentConfigs, type ComponentConfig } from './config/baseComponents'
9
- import { registerComponent, registerComponents } from './core/registry'
10
+ import { registerComponent, registerComponents as registerComponentsInRegistry } from './core/registry'
11
+ import ExternalPreview from './views/ExternalPreview.vue'
12
+ import RenderPage from './views/RenderPage.vue'
13
+
14
+ export interface BloxInstance {
15
+ /**
16
+ * Register one or more components with the Blox library
17
+ * Accepts either a single component config or an array of component configs
18
+ */
19
+ registerComponents: (
20
+ components: ComponentConfig | ComponentConfig[],
21
+ options?: RegisterOptions
22
+ ) => BloxInstance
23
+
24
+ /**
25
+ * Register routes for preview pages with your Vue Router
26
+ */
27
+ registerRoutes: (router: Router, options?: RouteOptions) => BloxInstance
28
+
29
+ /**
30
+ * Get all registered component configurations
31
+ */
32
+ getRegisteredComponents: () => ComponentConfig[]
33
+
34
+ /**
35
+ * Get components by category
36
+ */
37
+ getComponentsByCategory: (category: string) => ComponentConfig[]
38
+
39
+ /**
40
+ * Get a list of registered component IDs
41
+ */
42
+ getComponentIds: () => string[]
43
+
44
+ /**
45
+ * Check if a component is registered
46
+ */
47
+ hasComponent: (id: string) => boolean
48
+
49
+ /**
50
+ * Install the Blox plugin into your Vue app
51
+ */
52
+ install: (app: App) => void
53
+ }
54
+
55
+ export interface RegisterOptions {
56
+ /**
57
+ * Category to assign to the components
58
+ */
59
+ category?: string
60
+
61
+ /**
62
+ * Skip validation for component configs
63
+ * @default false
64
+ */
65
+ skipValidation?: boolean
66
+ }
67
+
68
+ export interface RouteOptions {
69
+ /**
70
+ * Base path for the preview routes
71
+ * @default '/blox'
72
+ */
73
+ basePath?: string
74
+
75
+ /**
76
+ * Path for the external preview route
77
+ * @default '/preview/:pageId?'
78
+ */
79
+ previewPath?: string
80
+
81
+ /**
82
+ * Path for the render page route
83
+ * @default '/render/:pageId?'
84
+ */
85
+ renderPath?: string
86
+
87
+ /**
88
+ * Additional route meta properties
89
+ */
90
+ meta?: Record<string, any>
91
+ }
92
+
93
+ export interface BloxOptions {
94
+ /**
95
+ * Enable debug mode for additional logging
96
+ * @default false
97
+ */
98
+ debug?: boolean
99
+
100
+ /**
101
+ * Automatically register base components on first registerComponents call
102
+ * @default true
103
+ */
104
+ autoRegisterBaseComponents?: boolean
105
+ }
10
106
 
11
107
  /**
12
- * Register all base components from the library
13
- * Call this in your project's setup to make base components available
108
+ * Validation error class
14
109
  */
15
- export function registerBaseComponents(): void {
16
- const componentMap: Record<string, any> = {}
110
+ export class ComponentValidationError extends Error {
111
+ constructor(message: string, public componentId?: string) {
112
+ super(message)
113
+ this.name = 'ComponentValidationError'
114
+ }
115
+ }
17
116
 
18
- baseComponentConfigs.forEach((config) => {
19
- componentMap[config.id] = config.component
20
- })
117
+ /**
118
+ * Validate a component configuration
119
+ */
120
+ function validateComponentConfig(config: ComponentConfig): void {
121
+ const errors: string[] = []
21
122
 
22
- registerComponents(componentMap)
23
- console.log(' Registered base components:', Object.keys(componentMap))
123
+ if (!config.id || typeof config.id !== 'string') {
124
+ errors.push('Component must have a valid "id" (string)')
125
+ }
126
+
127
+ if (!config.label || typeof config.label !== 'string') {
128
+ errors.push('Component must have a valid "label" (string)')
129
+ }
130
+
131
+ if (!config.component) {
132
+ errors.push('Component must have a "component" property (Vue component or async function)')
133
+ }
134
+
135
+ if (config.component && typeof config.component !== 'function' && typeof config.component !== 'object') {
136
+ errors.push('Component "component" must be a Vue component or async function')
137
+ }
138
+
139
+ if (config.content && !Array.isArray(config.content)) {
140
+ errors.push('Component "content" must be an array')
141
+ }
142
+
143
+ if (config.settings && !Array.isArray(config.settings)) {
144
+ errors.push('Component "settings" must be an array')
145
+ }
146
+
147
+ if (errors.length > 0) {
148
+ throw new ComponentValidationError(
149
+ `Invalid component configuration for "${config.id || 'unknown'}":\n${errors.map(e => ` - ${e}`).join('\n')}`,
150
+ config.id
151
+ )
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Calculate Levenshtein distance between two strings
157
+ * Used for "did you mean" suggestions
158
+ */
159
+ function levenshteinDistance(a: string, b: string): number {
160
+ const matrix: number[][] = []
161
+
162
+ for (let i = 0; i <= b.length; i++) {
163
+ matrix[i] = [i]
164
+ }
165
+
166
+ for (let j = 0; j <= a.length; j++) {
167
+ matrix[0][j] = j
168
+ }
169
+
170
+ for (let i = 1; i <= b.length; i++) {
171
+ for (let j = 1; j <= a.length; j++) {
172
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
173
+ matrix[i][j] = matrix[i - 1][j - 1]
174
+ } else {
175
+ matrix[i][j] = Math.min(
176
+ matrix[i - 1][j - 1] + 1,
177
+ matrix[i][j - 1] + 1,
178
+ matrix[i - 1][j] + 1
179
+ )
180
+ }
181
+ }
182
+ }
183
+
184
+ return matrix[b.length][a.length]
185
+ }
186
+
187
+ /**
188
+ * Find similar component IDs (for "did you mean" suggestions)
189
+ */
190
+ function findSimilarIds(targetId: string, allIds: string[], maxSuggestions: number = 3): string[] {
191
+ return allIds
192
+ .map(id => ({ id, distance: levenshteinDistance(targetId.toLowerCase(), id.toLowerCase()) }))
193
+ .filter(item => item.distance <= 3) // Only show suggestions within 3 edits
194
+ .sort((a, b) => a.distance - b.distance)
195
+ .slice(0, maxSuggestions)
196
+ .map(item => item.id)
197
+ }
198
+
199
+ /**
200
+ * Create better error message for missing component
201
+ */
202
+ function createMissingComponentError(id: string, registeredIds: string[]): Error {
203
+ const suggestions = findSimilarIds(id, registeredIds)
204
+ let message = `Component "${id}" is not registered.`
205
+
206
+ if (suggestions.length > 0) {
207
+ message += `\n\nDid you mean: ${suggestions.map(s => `"${s}"`).join(', ')}?`
208
+ }
209
+
210
+ message += `\n\nAvailable components: ${registeredIds.join(', ')}`
211
+
212
+ return new Error(message)
213
+ }
214
+
215
+ /**
216
+ * Create a new Blox instance with a fluent API
217
+ *
218
+ * @param options - Configuration options for the Blox instance
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * import { createBlox, ButtonConfig, TextConfig } from '@bagelink/blox'
223
+ * import { MyCustomComp } from './components'
224
+ *
225
+ * const blox = createBlox({ debug: true })
226
+ * blox.registerComponents([ButtonConfig, TextConfig, MyCustomComp])
227
+ * blox.registerRoutes(router)
228
+ *
229
+ * app.use(blox)
230
+ * ```
231
+ */
232
+ export function createBlox(options: BloxOptions = {}): BloxInstance {
233
+ const {
234
+ debug = false,
235
+ autoRegisterBaseComponents = true,
236
+ } = options
237
+
238
+ const registeredConfigs: ComponentConfig[] = []
239
+ let hasRegisteredBaseComponents = false
240
+
241
+ const log = (...args: any[]) => {
242
+ if (debug) {
243
+ console.log('[Blox]', ...args)
244
+ }
245
+ }
246
+
247
+ const instance: BloxInstance = {
248
+ registerComponents(
249
+ components: ComponentConfig | ComponentConfig[],
250
+ registerOptions: RegisterOptions = {}
251
+ ) {
252
+ const { category, skipValidation = false } = registerOptions
253
+ const configs = Array.isArray(components) ? components : [components]
254
+
255
+ // Auto-register base components on first call if enabled
256
+ if (!hasRegisteredBaseComponents && autoRegisterBaseComponents) {
257
+ const componentMap: Record<string, any> = {}
258
+ baseComponentConfigs.forEach((config) => {
259
+ componentMap[config.id] = config.component
260
+ registeredConfigs.push(config)
261
+ })
262
+ registerComponentsInRegistry(componentMap)
263
+ hasRegisteredBaseComponents = true
264
+ log('Registered base components:', Object.keys(componentMap))
265
+ if (!debug) {
266
+ console.log('✅ Registered base components:', Object.keys(componentMap))
267
+ }
268
+ }
269
+
270
+ // Register the provided components
271
+ configs.forEach((config) => {
272
+ // Validate config
273
+ if (!skipValidation) {
274
+ try {
275
+ validateComponentConfig(config)
276
+ } catch (error) {
277
+ if (error instanceof ComponentValidationError) {
278
+ console.error('❌ Component validation failed:')
279
+ console.error(error.message)
280
+ if (debug) {
281
+ console.error('Component config:', config)
282
+ }
283
+ throw error
284
+ }
285
+ throw error
286
+ }
287
+ }
288
+
289
+ // Apply category if provided in options
290
+ const finalConfig = category ? { ...config, category } : config
291
+
292
+ // Check for duplicate IDs
293
+ if (registeredConfigs.some(c => c.id === finalConfig.id)) {
294
+ const msg = `Component "${finalConfig.id}" is already registered. Overwriting...`
295
+ console.warn(`⚠️ ${msg}`)
296
+ const index = registeredConfigs.findIndex(c => c.id === finalConfig.id)
297
+ registeredConfigs.splice(index, 1)
298
+ }
299
+
300
+ // For async components, store the factory function
301
+ // They will be resolved when actually needed
302
+ registerComponent(finalConfig.id, finalConfig.component)
303
+ registeredConfigs.push(finalConfig)
304
+
305
+ const categoryMsg = finalConfig.category ? ` (${finalConfig.category})` : ''
306
+ log(`Registered component: ${finalConfig.id}${categoryMsg}`)
307
+ if (!debug) {
308
+ console.log(`✅ Registered component: ${finalConfig.id}${categoryMsg}`)
309
+ }
310
+ })
311
+
312
+ return instance
313
+ },
314
+
315
+ registerRoutes(router: Router, routeOptions: RouteOptions = {}) {
316
+ const {
317
+ basePath = '/blox',
318
+ previewPath = '/preview/:pageId?',
319
+ renderPath = '/render/:pageId?',
320
+ meta = {},
321
+ } = routeOptions
322
+
323
+ // Add the external preview route
324
+ router.addRoute({
325
+ path: `${basePath}${previewPath}`,
326
+ name: 'blox-preview',
327
+ component: ExternalPreview,
328
+ meta: { blox: true, ...meta },
329
+ })
330
+
331
+ // Add the render page route
332
+ router.addRoute({
333
+ path: `${basePath}${renderPath}`,
334
+ name: 'blox-render',
335
+ component: RenderPage,
336
+ meta: { blox: true, ...meta },
337
+ })
338
+
339
+ const routes = {
340
+ preview: `${basePath}${previewPath}`,
341
+ render: `${basePath}${renderPath}`,
342
+ }
343
+ log('Registered Blox routes:', routes)
344
+ if (!debug) {
345
+ console.log('✅ Registered Blox routes:', routes)
346
+ }
347
+
348
+ return instance
349
+ },
350
+
351
+ getRegisteredComponents() {
352
+ return [...registeredConfigs]
353
+ },
354
+
355
+ getComponentsByCategory(category: string) {
356
+ return registeredConfigs.filter(config => config.category === category)
357
+ },
358
+
359
+ getComponentIds() {
360
+ return registeredConfigs.map(config => config.id)
361
+ },
362
+
363
+ hasComponent(id: string) {
364
+ return registeredConfigs.some(config => config.id === id)
365
+ },
366
+
367
+ install(app: App) {
368
+ // Make the registered configs available globally
369
+ app.config.globalProperties.$blox = {
370
+ configs: registeredConfigs,
371
+ getComponent: (id: string) => {
372
+ const config = registeredConfigs.find(c => c.id === id)
373
+ if (!config && debug) {
374
+ const error = createMissingComponentError(id, this.getComponentIds())
375
+ console.error(error.message)
376
+ }
377
+ return config
378
+ },
379
+ getComponentsByCategory: (category: string) => this.getComponentsByCategory(category),
380
+ hasComponent: (id: string) => registeredConfigs.some(c => c.id === id),
381
+ }
382
+
383
+ // Provide for composition API
384
+ app.provide('blox', {
385
+ configs: registeredConfigs,
386
+ getComponent: (id: string) => {
387
+ const config = registeredConfigs.find(c => c.id === id)
388
+ if (!config && debug) {
389
+ const error = createMissingComponentError(id, this.getComponentIds())
390
+ console.error(error.message)
391
+ }
392
+ return config
393
+ },
394
+ getComponentsByCategory: (category: string) => this.getComponentsByCategory(category),
395
+ hasComponent: (id: string) => registeredConfigs.some(c => c.id === id),
396
+ })
397
+
398
+ log('Blox plugin installed with', registeredConfigs.length, 'components')
399
+ if (!debug) {
400
+ console.log('✅ Blox plugin installed')
401
+ }
402
+
403
+ if (debug) {
404
+ console.log('[Blox] Registered components:', this.getComponentIds())
405
+ const categories = [
406
+ ...new Set(
407
+ registeredConfigs
408
+ .map(c => c.category)
409
+ .filter((cat): cat is string => Boolean(cat))
410
+ )
411
+ ]
412
+ console.log('[Blox] Categories:', categories)
413
+ console.log('[Blox] Debug mode enabled')
414
+ }
415
+ },
416
+ }
417
+
418
+ return instance
419
+ }
420
+
421
+ /**
422
+ * Helper to create a custom component configuration with better TypeScript inference
423
+ */
424
+ export function createComponentConfig<T extends ComponentConfig>(config: T): T {
425
+ return config
24
426
  }
25
427
 
26
428
  /**
@@ -32,14 +434,26 @@ export function getAllComponentConfigs(customConfigs: ComponentConfig[] = []): C
32
434
  }
33
435
 
34
436
  /**
35
- * Helper to create a custom component configuration
437
+ * Register all base components from the library
438
+ * Call this in your project's setup to make base components available
439
+ *
440
+ * @deprecated Use `createBlox().registerComponents()` instead for a simpler API
36
441
  */
37
- export function createComponentConfig(config: ComponentConfig): ComponentConfig {
38
- return config
442
+ export function registerBaseComponents(): void {
443
+ const componentMap: Record<string, any> = {}
444
+
445
+ baseComponentConfigs.forEach((config) => {
446
+ componentMap[config.id] = config.component
447
+ })
448
+
449
+ registerComponentsInRegistry(componentMap)
450
+ console.log('✅ Registered base components:', Object.keys(componentMap))
39
451
  }
40
452
 
41
453
  /**
42
454
  * Register a single custom component
455
+ *
456
+ * @deprecated Use `createBlox().registerComponents()` instead for a simpler API
43
457
  */
44
458
  export function registerCustomComponent(config: ComponentConfig): void {
45
459
  registerComponent(config.id, config.component)
@@ -48,6 +462,8 @@ export function registerCustomComponent(config: ComponentConfig): void {
48
462
 
49
463
  /**
50
464
  * Register multiple custom components
465
+ *
466
+ * @deprecated Use `createBlox().registerComponents()` instead for a simpler API
51
467
  */
52
468
  export function registerCustomComponents(configs: ComponentConfig[]): void {
53
469
  configs.forEach((config) => {
@@ -44,7 +44,8 @@ const registeredComponents = getAllComponents()
44
44
 
45
45
  // Reactive computed for block styles
46
46
  const getBlockStyles = computed(() => {
47
- const _ = updateCounter.value
47
+ // Access updateCounter to trigger reactivity
48
+ console.log('updateCounter', updateCounter.value)
48
49
  return (data: Record<string, any>, mobile: boolean = false) => generateBlockStyles(data, mobile)
49
50
  })
50
51
 
@@ -258,13 +259,10 @@ onUnmounted(() => {
258
259
 
259
260
  <template v-for="comp in components" :key="comp.id">
260
261
  <div
261
- class="blox-block-wrapper"
262
- :class="{
262
+ class="blox-block-wrapper" :class="{
263
263
  'blox-highlight': !previewMode && highlightID === comp.id,
264
264
  'blox-selected': !previewMode && selectedID === comp.id,
265
- }"
266
- :data-block-id="comp.id"
267
- @click="!previewMode ? setFocus(comp.id, true) : null"
265
+ }" :data-block-id="comp.id" @click="!previewMode ? setFocus(comp.id, true) : null"
268
266
  @mouseenter="!previewMode ? setHighlight(comp.id, true) : null"
269
267
  @focus="!previewMode ? setHighlight(comp.id, true) : null"
270
268
  >
@@ -276,14 +274,11 @@ onUnmounted(() => {
276
274
 
277
275
  <!-- Block content -->
278
276
  <div
279
- :id="comp.data.customId"
280
- :key="`${comp.id}-${updateCounter}`"
281
- :style="getBlockStyles(comp.data, isMobile)"
282
- :class="getResponsiveClasses(comp.data)"
277
+ :id="comp.data.customId" :key="`${comp.id}-${updateCounter}`"
278
+ :style="getBlockStyles(comp.data, isMobile)" :class="getResponsiveClasses(comp.data)"
283
279
  >
284
280
  <component
285
- :is="registeredComponents[comp.type]"
286
- v-if="registeredComponents[comp.type]"
281
+ :is="registeredComponents[comp.type]" v-if="registeredComponents[comp.type]"
287
282
  v-bind="{ ...normalizeComponentData(comp.data), isMobile }"
288
283
  />
289
284
  <div v-else class="blox-missing-component">
@@ -32,8 +32,8 @@ const registeredComponents = getAllComponents()
32
32
 
33
33
  // Reactive computed for block styles
34
34
  const getBlockStyles = computed(() => {
35
- // eslint-disable-next-line no-unused-vars
36
- const _ = updateCounter.value
35
+ // Access updateCounter to trigger reactivity
36
+ console.log('updateCounter', updateCounter.value)
37
37
  return (data: Record<string, any>, mobile: boolean = false) => generateBlockStyles(data, mobile)
38
38
  })
39
39