@lightspeed/crane 2.0.4 → 3.0.0

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 (131) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/UPGRADE.md +96 -0
  3. package/dist/cli.mjs +44 -25
  4. package/package.json +6 -3
  5. package/template/blank/sections/blank-section/BlankSection.vue +9 -0
  6. package/template/blank/sections/blank-section/assets/blank_section_showcase_1_preview.jpg +0 -0
  7. package/template/blank/sections/blank-section/client.ts +6 -0
  8. package/template/blank/sections/blank-section/server.ts +6 -0
  9. package/template/blank/sections/blank-section/settings/content.ts +2 -0
  10. package/template/blank/sections/blank-section/settings/design.ts +2 -0
  11. package/template/blank/sections/blank-section/settings/layout.ts +2 -0
  12. package/template/blank/sections/blank-section/settings/translations.ts +8 -0
  13. package/template/blank/sections/blank-section/showcases/1.ts +15 -0
  14. package/template/blank/sections/blank-section/showcases/translations.ts +10 -0
  15. package/template/blank/sections/blank-section/type.ts +5 -0
  16. package/template/collections/assets/collection_cover_image.png +0 -0
  17. package/template/collections/example-collection/configuration.ts +21 -0
  18. package/template/crane.config.json +1 -1
  19. package/template/footers/example-footer/ExampleFooter.vue +4 -4
  20. package/template/footers/example-footer/component/LegalLinks.vue +1 -1
  21. package/template/footers/example-footer/component/MadeWith.vue +1 -1
  22. package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
  23. package/template/footers/example-footer/settings/design.ts +5 -4
  24. package/template/footers/example-footer/settings/translations.ts +4 -3
  25. package/template/footers/example-footer/showcases/1.ts +4 -2
  26. package/template/footers/example-footer/showcases/translations.ts +4 -2
  27. package/template/headers/example-header/ExampleHeader.vue +1 -1
  28. package/template/headers/example-header/component/Account.vue +1 -1
  29. package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
  30. package/template/headers/example-header/component/Logo.vue +7 -7
  31. package/template/headers/example-header/component/NavigationMenu.vue +1 -1
  32. package/template/headers/example-header/settings/content.ts +8 -1
  33. package/template/headers/example-header/settings/design.ts +7 -1
  34. package/template/headers/example-header/settings/layout.ts +1 -1
  35. package/template/headers/example-header/settings/translations.ts +4 -2
  36. package/template/headers/example-header/showcases/1.ts +15 -11
  37. package/template/headers/example-header/showcases/2.ts +12 -8
  38. package/template/headers/example-header/showcases/translations.ts +4 -2
  39. package/template/layouts/catalog/example-catalog/components/Icon.vue +14 -14
  40. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/CustomBottomBar.vue +1 -1
  41. package/template/package.json +1 -0
  42. package/template/page-templates/example-template/configuration.ts +8 -10
  43. package/template/page-templates/example-template/pages/catalog.ts +3 -6
  44. package/template/page-templates/example-template/pages/category.ts +3 -6
  45. package/template/page-templates/example-template/pages/home.ts +42 -57
  46. package/template/page-templates/example-template/pages/product.ts +3 -6
  47. package/template/preview/sections/preview.html +10 -6
  48. package/template/preview/shared/api-routes.ts +235 -102
  49. package/template/preview/shared/logger.ts +9 -0
  50. package/template/preview/shared/preview.ts +150 -69
  51. package/template/preview/shared/utils.ts +63 -43
  52. package/template/preview/ssr-server.ts +1 -1
  53. package/template/reference/sections/about-us/AboutUs.vue +20 -22
  54. package/template/reference/sections/about-us/component/Image.vue +18 -18
  55. package/template/reference/sections/about-us/component/Stats.vue +42 -42
  56. package/template/reference/sections/about-us/component/Title.vue +1 -1
  57. package/template/reference/sections/about-us/settings/content.ts +15 -19
  58. package/template/reference/sections/about-us/settings/design.ts +14 -18
  59. package/template/reference/sections/about-us/settings/layout.ts +7 -5
  60. package/template/reference/sections/about-us/settings/translations.ts +4 -2
  61. package/template/reference/sections/about-us/showcases/1.ts +48 -62
  62. package/template/reference/sections/about-us/showcases/2.ts +44 -56
  63. package/template/reference/sections/about-us/showcases/translations.ts +4 -2
  64. package/template/reference/sections/featured-products/FeaturedProducts.vue +12 -6
  65. package/template/reference/sections/featured-products/component/ProductItem.vue +18 -1
  66. package/template/reference/sections/featured-products/component/ProductPlaceholder.vue +42 -0
  67. package/template/reference/sections/featured-products/component/Title.vue +1 -1
  68. package/template/reference/sections/featured-products/settings/content.ts +8 -10
  69. package/template/reference/sections/featured-products/settings/design.ts +7 -7
  70. package/template/reference/sections/featured-products/settings/translations.ts +4 -2
  71. package/template/reference/sections/featured-products/showcases/1.ts +8 -12
  72. package/template/reference/sections/featured-products/showcases/translations.ts +4 -2
  73. package/template/reference/sections/intro-slider/IntroSlider.vue +6 -6
  74. package/template/reference/sections/intro-slider/component/Slider.vue +43 -44
  75. package/template/reference/sections/intro-slider/component/Title.vue +9 -9
  76. package/template/reference/sections/intro-slider/settings/content.ts +36 -22
  77. package/template/reference/sections/intro-slider/settings/design.ts +17 -22
  78. package/template/reference/sections/intro-slider/settings/layout.ts +6 -4
  79. package/template/reference/sections/intro-slider/settings/translations.ts +4 -2
  80. package/template/reference/sections/intro-slider/showcases/1.ts +52 -75
  81. package/template/reference/sections/intro-slider/showcases/2.ts +50 -72
  82. package/template/reference/sections/intro-slider/showcases/translations.ts +4 -2
  83. package/template/reference/sections/tag-lines/TagLines.vue +41 -47
  84. package/template/reference/sections/tag-lines/component/HighlightedText.vue +2 -2
  85. package/template/reference/sections/tag-lines/component/SectionImage.vue +18 -18
  86. package/template/reference/sections/tag-lines/component/Title.vue +2 -2
  87. package/template/reference/sections/tag-lines/settings/content.ts +53 -19
  88. package/template/reference/sections/tag-lines/settings/design.ts +15 -19
  89. package/template/reference/sections/tag-lines/settings/layout.ts +6 -4
  90. package/template/reference/sections/tag-lines/settings/translations.ts +4 -2
  91. package/template/reference/sections/tag-lines/showcases/1.ts +40 -50
  92. package/template/reference/sections/tag-lines/showcases/2.ts +40 -50
  93. package/template/reference/sections/tag-lines/showcases/translations.ts +4 -2
  94. package/template/reference/sections/trending-categories/TrendingCategories.vue +1 -1
  95. package/template/reference/sections/trending-categories/component/CategoryItem.vue +18 -1
  96. package/template/reference/sections/trending-categories/component/Title.vue +2 -2
  97. package/template/reference/sections/trending-categories/settings/content.ts +8 -10
  98. package/template/reference/sections/trending-categories/settings/design.ts +7 -7
  99. package/template/reference/sections/trending-categories/settings/translations.ts +4 -2
  100. package/template/reference/sections/trending-categories/showcases/1.ts +14 -15
  101. package/template/reference/sections/trending-categories/showcases/translations.ts +4 -2
  102. package/template/reference/shared/components/Button.vue +6 -6
  103. package/template/reference/shared/components/SectionWrapper.vue +5 -5
  104. package/template/reference/shared/components/Tagline.vue +12 -11
  105. package/template/reference/templates/reference-template-apparel/configuration.ts +8 -8
  106. package/template/reference/templates/reference-template-apparel/pages/catalog.ts +3 -6
  107. package/template/reference/templates/reference-template-apparel/pages/category.ts +3 -6
  108. package/template/reference/templates/reference-template-apparel/pages/home.ts +14 -18
  109. package/template/reference/templates/reference-template-apparel/pages/product.ts +3 -6
  110. package/template/reference/templates/reference-template-bike/configuration.ts +9 -9
  111. package/template/reference/templates/reference-template-bike/pages/catalog.ts +3 -6
  112. package/template/reference/templates/reference-template-bike/pages/category.ts +3 -6
  113. package/template/reference/templates/reference-template-bike/pages/home.ts +14 -18
  114. package/template/reference/templates/reference-template-bike/pages/product.ts +3 -6
  115. package/template/sections/example-section/ExampleSection.vue +3 -5
  116. package/template/sections/example-section/component/button/Button.vue +1 -1
  117. package/template/sections/example-section/component/image/Image.vue +43 -43
  118. package/template/sections/example-section/component/image/ImagesGrid.vue +21 -32
  119. package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
  120. package/template/sections/example-section/component/title/Title.vue +1 -1
  121. package/template/sections/example-section/component/toggle/Toggle.vue +4 -4
  122. package/template/sections/example-section/settings/content.ts +25 -34
  123. package/template/sections/example-section/settings/design.ts +15 -19
  124. package/template/sections/example-section/settings/layout.ts +15 -14
  125. package/template/sections/example-section/settings/translations.ts +4 -2
  126. package/template/sections/example-section/showcases/1.ts +52 -79
  127. package/template/sections/example-section/showcases/2.ts +46 -62
  128. package/template/sections/example-section/showcases/3.ts +50 -76
  129. package/template/sections/example-section/showcases/translations.ts +4 -2
  130. package/template/shared/components/LanguageSelector.vue +1 -1
  131. package/template/shared/components/SectionWrapper.vue +5 -5
@@ -2,8 +2,18 @@
2
2
  import { getExternalContentMock } from './mock';
3
3
  import { loadModule, renderServerModule } from './utils';
4
4
 
5
+ export type BlockType = 'sections' | 'headers' | 'footers';
6
+ export const BLOCK_TYPES: BlockType[] = ['sections', 'headers', 'footers'];
7
+
8
+ const BLOCK_TYPE_LABELS: Record<BlockType, string> = {
9
+ sections: 'Section',
10
+ headers: 'Header',
11
+ footers: 'Footer',
12
+ };
13
+
5
14
  let distFolderPath: string | null = null;
6
15
  let currentApp: any = null;
16
+ let currentBlockKey: string | null = null;
7
17
 
8
18
  export function setDistFolderPath(path: string): void {
9
19
  distFolderPath = path;
@@ -16,8 +26,8 @@ export function setDistFolderPath(path: string): void {
16
26
  /**
17
27
  * Loads all required modules for a showcase.
18
28
  */
19
- async function loadShowcaseModules(sectionName: string, showcaseId: string, distFolder: string): Promise<[any, any, any, any, any]> {
20
- const basePath = `${distFolder}/sections/${sectionName}`;
29
+ async function loadShowcaseModules(blockType: BlockType, blockName: string, showcaseId: string, distFolder: string): Promise<[any, any, any, any, any]> {
30
+ const basePath = `${distFolder}/${blockType}/${blockName}`;
21
31
  return Promise.all([
22
32
  loadModule(`${basePath}/js/settings/content.mjs`),
23
33
  loadModule(`${basePath}/js/settings/translations.mjs`),
@@ -229,10 +239,10 @@ export function mergeDesign(
229
239
  return result;
230
240
  }
231
241
 
232
- export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>): Record<string, any> {
242
+ export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>, isApiRender: boolean = false): Record<string, any> {
233
243
  const parsedDesign: Record<string, any> = {};
234
244
  const parsedShowcaseDesign: Record<string, any> = {};
235
- Object.entries(design).forEach(([key, comp]) => {
245
+ Object.entries(design || {}).forEach(([key, comp]) => {
236
246
  // Skip DIVIDER elements - they're UI-only and don't have runtime data
237
247
  if (comp?.type === 'DIVIDER') {
238
248
  return;
@@ -245,7 +255,7 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
245
255
  parsedDesign[key] = comp?.defaults || {};
246
256
  });
247
257
 
248
- Object.entries(showCaseDesign).forEach(([key, comp]) => {
258
+ Object.entries(showCaseDesign || {}).forEach(([key, comp]) => {
249
259
  // Skip DIVIDER elements - they're UI-only and don't have runtime data
250
260
  if (comp?.type === 'DIVIDER') {
251
261
  return;
@@ -259,33 +269,46 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
259
269
  });
260
270
 
261
271
  let overridenDesign = mergeDesign(parsedDesign, parsedShowcaseDesign);
262
- overridenDesign = updateHexColors(overridenDesign);
263
- replaceGlobalFont(overridenDesign, 'Roboto');
272
+
273
+ try {
274
+ overridenDesign = updateHexColors(overridenDesign);
275
+ } catch (error) {
276
+ console.error('[Preview] Failed to update hex colors:', error);
277
+ }
278
+
279
+ if ( !isApiRender ) {
280
+ replaceGlobalFont(overridenDesign, 'Roboto');
281
+ }
264
282
  return overridenDesign;
265
283
  }
266
284
 
267
- function processImage(component: any, sectionName: string, key: string, isApiRender: boolean = false): Record<string, any> {
268
- const assetLocation = isApiRender ?
269
- `http://${distFolderPath}/sections/${sectionName}/assets/` :
270
- `${distFolderPath}/sections/${sectionName}/assets/`;
271
-
272
- const set = component.defaults?.imageData?.set || component.imageData?.set;
273
- const newSet = {
274
- 'cropped-webp-100x200': { url: assetLocation + set.MOBILE_WEBP_LOW_RES.url },
275
- 'cropped-webp-1000x2000': { url: assetLocation + set.MOBILE_WEBP_HI_RES.url },
276
- 'webp-200x200': { url: assetLocation + set.WEBP_LOW_RES.url },
277
- 'webp-2000x2000': { url: assetLocation + set.WEBP_HI_2X_RES.url },
278
- };
279
- return {
280
- [key]: {
281
- set: newSet,
282
- ...(component.imageData?.borderInfo && { borderInfo: component.imageData.borderInfo }),
283
- ...(component.imageData && { bucket: {} }),
284
- },
285
- };
285
+ function processImage(component: any, blockType: BlockType, blockName: string, key: string, isApiRender: boolean = false): Record<string, any> {
286
+ try {
287
+ const assetLocation = isApiRender ?
288
+ `http://${distFolderPath}/${blockType}/${blockName}/assets/` :
289
+ `${distFolderPath}/${blockType}/${blockName}/assets/`;
290
+
291
+ const set = component.defaults?.imageData?.set || component.imageData?.set;
292
+ const newSet = {
293
+ 'cropped-webp-100x200': { url: assetLocation + set?.MOBILE_WEBP_LOW_RES.url },
294
+ 'cropped-webp-1000x2000': { url: assetLocation + set?.MOBILE_WEBP_HI_RES.url },
295
+ 'webp-200x200': { url: assetLocation + set?.WEBP_LOW_RES.url },
296
+ 'webp-2000x2000': { url: assetLocation + set?.WEBP_HI_2X_RES.url },
297
+ };
298
+ return {
299
+ [key]: {
300
+ set: newSet,
301
+ ...(component.imageData?.borderInfo && { borderInfo: component.imageData.borderInfo }),
302
+ ...(component.imageData && { bucket: {} }),
303
+ },
304
+ };
305
+ } catch (error) {
306
+ console.error(`[Preview] Failed to process image "${key}":`, error);
307
+ return {};
308
+ }
286
309
  }
287
310
 
288
- function processComponent(key: string, component: any, translations: any, sectionName?: string,
311
+ export function processComponent(key: string, component: any, translations: any, blockType?: BlockType, blockName?: string,
289
312
  isApiRender: boolean = false, extraParams?: Record<string, any>): any {
290
313
  if (!component?.type) return {};
291
314
  switch (component.type) {
@@ -301,16 +324,16 @@ function processComponent(key: string, component: any, translations: any, sectio
301
324
  if (defaultSettings) {
302
325
  const result: Record<string, any> = {};
303
326
  Object.entries(defaultSettings).forEach(([k, c]) => {
304
- Object.assign(result, processComponent(k, c, translations, sectionName, isApiRender));
327
+ Object.assign(result, processComponent(k, c, translations, blockType, blockName, isApiRender));
305
328
  });
306
329
  return { [key]: result };
307
330
  } else {
308
331
  const cards = component.cards.map((card: any, index: number) => {
309
332
  const cardContent: Record<string, any> = {};
310
333
  Object.entries(card.settings).forEach(([k, c]) => {
311
- Object.assign(cardContent, processComponent(k, c, translations, sectionName, isApiRender));
334
+ Object.assign(cardContent, processComponent(k, c, translations, blockType, blockName, isApiRender));
312
335
  });
313
- return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel};
336
+ return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel };
314
337
  });
315
338
  return { [key]: { cards } };
316
339
  }
@@ -328,14 +351,14 @@ function processComponent(key: string, component: any, translations: any, sectio
328
351
  case 'TOGGLE': {
329
352
  return {
330
353
  [key]: {
331
- enabled: component.defaults?.enabled ?? component.enabled
332
- }
354
+ enabled: component.defaults?.enabled ?? component.enabled,
355
+ },
333
356
  };
334
357
  }
335
358
  case 'SELECTBOX':
336
359
  return { [key]: component.defaults?.value ?? component.value };
337
360
  case 'IMAGE':
338
- return processImage(component, sectionName!, key, isApiRender);
361
+ return processImage(component, blockType!, blockName!, key, isApiRender);
339
362
  case 'INFO':
340
363
  return {
341
364
  [key]: {
@@ -347,6 +370,38 @@ function processComponent(key: string, component: any, translations: any, sectio
347
370
  },
348
371
  },
349
372
  };
373
+ case 'PRODUCT_SELECTOR':
374
+ return {
375
+ [key]: {
376
+ maxProducts: component?.maxProducts ?? component?.defaults?.maxProducts,
377
+ products: {
378
+ selectionType: 'MANUAL',
379
+ },
380
+ },
381
+ };
382
+ case 'CATEGORY_SELECTOR':
383
+ return {
384
+ [key]: {
385
+ maxCategories: component?.maxCategories ?? component?.defaults?.maxCategories,
386
+ categories: {
387
+ selectionType: 'ROOT',
388
+ },
389
+ },
390
+ };
391
+ case 'NAVIGATION_MENU':
392
+ return {
393
+ [key]: {
394
+ items: component.items ?? component?.defaults?.items ?? [],
395
+ },
396
+ };
397
+ case 'LOGO':
398
+ return {
399
+ [key]: {
400
+ text: translations[component?.text ?? component?.defaults?.text ?? ''],
401
+ type: component.logoType,
402
+ image: component.image,
403
+ },
404
+ };
350
405
  default:
351
406
  return {};
352
407
  }
@@ -357,20 +412,20 @@ function getContentToRender(
357
412
  showcase: any,
358
413
  contentTranslations: any,
359
414
  showcaseTranslations: any,
360
- sectionName: string,
415
+ blockType: BlockType,
416
+ blockName: string,
361
417
  isApiRender: boolean = false,
362
418
  ): any {
363
419
  let deckCardLabel: string | undefined = undefined;
364
- const parsedContent = Object.entries(content).reduce((acc, [k, c]) => {
420
+ const parsedContent = Object.entries(content || {}).reduce((acc, [k, c]) => {
365
421
  if ((c as any).type === 'DECK') {
366
422
  deckCardLabel = contentTranslations[(c as any).cards?.defaultCardContent?.label];
367
423
  }
368
- return { ...acc, ...processComponent(k, c, contentTranslations, sectionName, isApiRender) };
424
+ return { ...acc, ...processComponent(k, c, contentTranslations, blockType, blockName, isApiRender) };
369
425
  }, {});
370
426
 
371
-
372
- const parsedShowcase = Object.entries(showcase).reduce((acc, [k, c]) => {
373
- return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName, isApiRender, {deckCardLabel}) };
427
+ const parsedShowcase = Object.entries(showcase || {}).reduce((acc, [k, c]) => {
428
+ return { ...acc, ...processComponent(k, c, showcaseTranslations, blockType, blockName, isApiRender, { deckCardLabel }) };
374
429
  }, {});
375
430
 
376
431
  return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
@@ -378,31 +433,34 @@ function getContentToRender(
378
433
 
379
434
  export function dropdownOptions(showcaseModules: Record<string, any>): Array<{ value: string; label: string }> {
380
435
  return Object.keys(showcaseModules).map((path) => {
381
- const match = path.match(/\/sections\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
436
+ const match = path.match(/\/(sections|headers|footers)\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
382
437
  if (!match) return null;
438
+ const blockType = match[1] as BlockType;
439
+ const blockName = match[2];
440
+ const showcaseId = match[3];
383
441
  return {
384
- value: `${match[1]}:${match[2]}`,
385
- label: `${match[1]}: showcase ${match[2]}`,
442
+ value: `${blockType}:${blockName}:${showcaseId}`,
443
+ label: `${BLOCK_TYPE_LABELS[blockType]}: ${blockName} - Showcase ${showcaseId}`,
386
444
  };
387
445
  }).filter(Boolean) as Array<{ value: string; label: string }>;
388
446
  }
389
447
 
390
- export function loadSectionCss(sectionName: string): void {
448
+ export function loadBlockCss(blockType: BlockType, blockName: string): void {
391
449
  const link = document.createElement('link');
392
450
  link.rel = 'stylesheet';
393
- link.href = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
451
+ link.href = `${distFolderPath}/${blockType}/${blockName}/js/main/client/assets/client.css`;
394
452
  document.head.appendChild(link);
395
453
  }
396
454
 
397
455
  /**
398
456
  * Renders a showcase in the browser (client-side).
399
457
  */
400
- export async function renderShowcase(sectionName: string, showcaseId: string): Promise<void> {
458
+ export async function renderShowcase(blockType: BlockType, blockName: string, showcaseId: string): Promise<void> {
401
459
  if (!distFolderPath) throw new Error('distFolderPath not set');
402
460
 
403
- const basePath = `${distFolderPath}/sections/${sectionName}`;
461
+ const basePath = `${distFolderPath}/${blockType}/${blockName}`;
404
462
  const [content, contentTranslations, showcaseTranslations, showcase, design]
405
- = await loadShowcaseModules(sectionName, showcaseId, distFolderPath);
463
+ = await loadShowcaseModules(blockType, blockName, showcaseId, distFolderPath);
406
464
 
407
465
  const client = await loadModule(`${basePath}/js/main/client/client.js`) as any;
408
466
 
@@ -418,11 +476,12 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
418
476
  (showcase as any).default.content || {},
419
477
  (contentTranslations as any).default.en,
420
478
  (showcaseTranslations as any).default.en,
421
- sectionName,
479
+ blockType,
480
+ blockName,
422
481
  );
423
482
 
424
483
  // Load CSS
425
- loadSectionCss(sectionName);
484
+ loadBlockCss(blockType, blockName);
426
485
 
427
486
  // Prepare state
428
487
  const state = {
@@ -436,48 +495,63 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
436
495
  },
437
496
  };
438
497
 
498
+ // Check if we're switching to a different block
499
+ const blockKey = `${blockType}:${blockName}`;
500
+ const isSameBlock = currentBlockKey === blockKey;
501
+
439
502
  // Mount or update app
440
- if (currentApp) {
503
+ if (currentApp && isSameBlock) {
441
504
  currentApp.update(state);
442
505
  } else {
506
+ if (currentApp) {
507
+ currentApp.unmount();
508
+ const appContainer = document.getElementById('app');
509
+ if (appContainer) {
510
+ appContainer.innerHTML = '';
511
+ }
512
+ }
443
513
  const { mount, update, unmount } = (client as any).default.init();
444
514
  currentApp = { update, unmount };
515
+ currentBlockKey = blockKey;
445
516
  mount('#app', state);
446
517
  }
447
518
  }
448
519
 
449
- async function getLayout(showcase: any, sectionName: string, distFolder: string) {
520
+ async function getLayout(showcase: any, blockType: BlockType, blockName: string, distFolder: string) {
450
521
  // First, try to get layoutId from showcase
451
522
  if (showcase.default?.layoutId) {
452
523
  return showcase.default.layoutId;
453
524
  }
454
525
 
455
- // If not present in showcase, read from section's layout.mjs
456
- const basePath = `${distFolder}/sections/${sectionName}`;
457
- const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;
526
+ // If not in showcase, try reading from section's layout.mjs (optional)
527
+ try {
528
+ const basePath = `${distFolder}/${blockType}/${blockName}`;
529
+ const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;
458
530
 
459
- // layout.mjs exports an array of layout configurations
460
- // Return the first element's layoutId
461
- if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
462
- return layoutModule.default[0].layoutId;
531
+ // layout.mjs exports an array of layout configurations
532
+ // Return the first element's layoutId
533
+ if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
534
+ return layoutModule.default[0].layoutId;
535
+ }
536
+ } catch {
537
+ // Layout file is optional
463
538
  }
464
- throw new Error('Layout module is not an array or is empty');
465
539
  }
466
540
 
467
541
  /**
468
542
  * Prepares showcase data (content and design) without rendering.
469
543
  * Used by API routes to return tile data.
470
544
  */
471
- export async function getShowcaseData(sectionName: string, showcaseId: string, distFolder: string) {
545
+ export async function getShowcaseData(blockType: BlockType, blockName: string, showcaseId: string, distFolder: string) {
472
546
  const [content, contentTranslations, showcaseTranslations, showcase, design]
473
- = await loadShowcaseModules(sectionName, showcaseId, distFolder);
547
+ = await loadShowcaseModules(blockType, blockName, showcaseId, distFolder);
474
548
 
475
549
  // Prepare design
476
550
  const showcaseBackground = (showcase as any).default?.design?.background;
477
551
  const backgroundDesign = createBackgroundDesign(showcaseBackground, true); // API render
478
- const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
552
+ const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {}, true);
479
553
  overriddenDesign.background = backgroundDesign;
480
- overriddenDesign.layout = await getLayout(showcase, sectionName, distFolder);
554
+ overriddenDesign.layout = await getLayout(showcase, blockType, blockName, distFolder);
481
555
 
482
556
  // Prepare content
483
557
  const overriddenContent = getContentToRender(
@@ -485,8 +559,9 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
485
559
  (showcase as any).default.content || {},
486
560
  (contentTranslations as any).default.en,
487
561
  (showcaseTranslations as any).default.en,
488
- sectionName,
489
- true
562
+ blockType,
563
+ blockName,
564
+ true,
490
565
  );
491
566
 
492
567
  return {
@@ -499,20 +574,26 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
499
574
  * Gets the showcase state by rendering server-side.
500
575
  * Uses renderServerModule to get HTML directly from the SSR server.
501
576
  */
502
- export async function getShowcaseState(sectionName: string, showcaseId: string, distFolder: string, context: Record<string, any>) {
577
+ export async function getShowcaseState(
578
+ blockType: BlockType,
579
+ blockName: string,
580
+ showcaseId: string,
581
+ distFolder: string,
582
+ context: Record<string, any>,
583
+ externalContent: Record<string, any>) {
503
584
  const { content: overriddenContent, design: overriddenDesign }
504
- = await getShowcaseData(sectionName, showcaseId, distFolder);
585
+ = await getShowcaseData(blockType, blockName, showcaseId, distFolder);
505
586
 
506
587
  const data = {
507
588
  content: overriddenContent,
508
589
  design: overriddenDesign,
509
590
  defaults: {},
510
591
  background: {},
511
- externalContent: getExternalContentMock(),
592
+ externalContent: externalContent,
512
593
  };
513
594
 
514
595
  // Render server-side using the SSR server
515
- const basePath = `${distFolder}/sections/${sectionName}`;
596
+ const basePath = `${distFolder}/${blockType}/${blockName}`;
516
597
  const serverModulePath = `${basePath}/js/main/server/server.js`;
517
598
  const html = await renderServerModule(serverModulePath, context, data);
518
599
 
@@ -2,7 +2,7 @@
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
 
5
- import { getShowcaseState } from './preview';
5
+ import { getShowcaseState, BlockType, BLOCK_TYPES } from './preview';
6
6
 
7
7
  /**
8
8
  * Renders a server.js module by directly calling the SSR server.
@@ -107,21 +107,19 @@ export function updateTilesSection(
107
107
  ): any {
108
108
  const targetPattern = `_${sectionName}_${showcaseId}`;
109
109
 
110
- const targetItem = tilesResponse.tiles.find((item: any) => {
110
+ const updatedTiles = tilesResponse.tiles.map((item: any) => {
111
111
  const itemId = item.sourceId;
112
- if (itemId && typeof itemId === 'string') {
113
- return itemId.includes(targetPattern);
112
+ if (itemId && typeof itemId === 'string' && itemId.includes(targetPattern)) {
113
+ item.content = content;
114
+ // If new design has no layout, use CUSTOM_LAYOUT
115
+ const layout = design.layout || 'CUSTOM_LAYOUT';
116
+ item.design = design;
117
+ item.design.layout = layout;
114
118
  }
115
- return false;
119
+ return item;
116
120
  });
117
121
 
118
- if (targetItem) {
119
- // Replace only content and design, preserve all other properties including tils
120
- targetItem.content = content;
121
- targetItem.design = design;
122
- }
123
-
124
- return tilesResponse;
122
+ return { ...tilesResponse, tiles: updatedTiles };
125
123
  }
126
124
 
127
125
  /**
@@ -154,17 +152,18 @@ export async function getSectionIds(sectionName: string, showcaseId: string, aut
154
152
  }
155
153
 
156
154
  /**
157
- * Gets all showcase IDs for a given section
158
- * @param sectionName - The section name (e.g., 'example-section')
155
+ * Gets all showcase IDs for a given block
156
+ * @param blockType - The block type ('sections', 'headers', 'footers')
157
+ * @param blockName - The block name (e.g., 'example-section')
159
158
  * @param distPath - The dist path
160
159
  * @returns Array of showcase IDs
161
160
  */
162
- export function getShowcaseIds(sectionName: string, distPath: string): string[] {
161
+ export function getShowcaseIds(blockType: BlockType, blockName: string, distPath: string): string[] {
163
162
  try {
164
- const showcasesPath = path.join(distPath, 'sections', sectionName, 'js', 'showcases');
163
+ const showcasesPath = path.join(distPath, blockType, blockName, 'js', 'showcases');
165
164
 
166
165
  if (!fs.existsSync(showcasesPath)) {
167
- console.error(`Showcases directory not found for section: ${sectionName}`);
166
+ console.error(`Showcases directory not found for ${blockType}: ${blockName}`);
168
167
  return [];
169
168
  }
170
169
 
@@ -174,23 +173,38 @@ export function getShowcaseIds(sectionName: string, distPath: string): string[]
174
173
 
175
174
  return showcaseIds;
176
175
  } catch (error) {
177
- console.error(`Error getting showcase IDs for section ${sectionName}:`, error);
176
+ console.error(`Error getting showcase IDs for ${blockType} ${blockName}:`, error);
178
177
  return [];
179
178
  }
180
179
  }
181
180
 
182
181
  /**
183
- * Updates all matching section showcases in custom content
184
- * @param sectionName - The section name
182
+ * Determine the block type by checking which folder contains the block
183
+ */
184
+ export function getBlockType(blockName: string, distPath: string): BlockType | null {
185
+ for (const blockType of BLOCK_TYPES) {
186
+ const blockPath = path.join(distPath, blockType, blockName);
187
+ if (fs.existsSync(blockPath)) {
188
+ return blockType;
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+
194
+ /**
195
+ * Updates all matching block showcases in custom content
196
+ * @param blockType - The block type ('sections', 'headers', 'footers')
197
+ * @param blockName - The block name
185
198
  * @param showcaseId - The showcase ID
186
199
  * @param distPath - The dist path
187
200
  * @param customContentSections - The sections array from custom content
188
201
  * @param authToken - Bearer token
189
202
  * @param tilesUrl - The tiles URL
190
- * @returns True if at least one section was updated successfully, false otherwise
203
+ * @returns True if at least one block was updated successfully, false otherwise
191
204
  */
192
- async function updateSectionShowcase(
193
- sectionName: string,
205
+ async function updateBlockShowcase(
206
+ blockType: BlockType,
207
+ blockName: string,
194
208
  showcaseId: string,
195
209
  distPath: string,
196
210
  customContentSections: Array<Record<string, unknown>>,
@@ -198,61 +212,62 @@ async function updateSectionShowcase(
198
212
  tilesUrl?: string,
199
213
  ): Promise<boolean> {
200
214
  try {
201
- // Get all section IDs from tiles
202
- const sectionIds = await getSectionIds(sectionName, showcaseId, authToken, tilesUrl);
215
+ // Get all section IDs from tiles matching this block/showcase
216
+ const sectionIds = await getSectionIds(blockName, showcaseId, authToken, tilesUrl);
203
217
 
204
218
  if (!sectionIds || sectionIds.length === 0) {
205
219
  return false;
206
- }
207
- // Process each section ID
220
+ }// Process each section ID
208
221
  for (const sectionId of sectionIds) {
209
- // Find the section by ID in custom content
210
- const targetSection = customContentSections.find((section) => {
222
+ // Find the block by block ID in custom content
223
+ const targetBlock = customContentSections.find((section) => {
211
224
  const state = section.state as Record<string, unknown>;
212
225
  const data = state.data as Record<string, unknown>;
213
226
  return data.tileId === sectionId;
214
227
  });
215
228
 
216
- if (!targetSection) {
217
- console.error(`Section not found in custom content with ID: ${sectionId}`);
229
+ if (!targetBlock) {
230
+ console.error(`Block not found in custom content with ID: ${sectionId}`);
218
231
  continue;
219
232
  }
220
233
 
221
234
  // Fetch content, design, and html using getShowcaseState
222
- const state = targetSection.state as Record<string, unknown>;
235
+ const state = targetBlock.state as Record<string, unknown>;
223
236
  // eslint-disable-next-line no-await-in-loop
224
237
  const { content, design, html } = await getShowcaseState(
225
- sectionName,
238
+ blockType,
239
+ blockName,
226
240
  showcaseId,
227
241
  distPath,
228
242
  state.context as Record<string, unknown>,
243
+ (state.data as Record<string, unknown>).externalContent as Record<string, unknown>,
229
244
  );
230
245
 
231
246
  // Update content, design, and html
232
247
  const data = state.data as Record<string, unknown>;
233
248
  data.content = JSON.stringify(content);
234
249
  data.design = JSON.stringify(design);
235
- targetSection.html = html;
250
+ targetBlock.html = html;
236
251
  }
237
252
 
238
253
  return true;
239
254
  } catch (error) {
240
- console.error(`Error updating ${sectionName} showcase ${showcaseId}:`, error);
255
+ console.error(`Error updating ${blockType} ${blockName} showcase ${showcaseId}:`, error);
241
256
  return false;
242
257
  }
243
258
  }
244
259
 
245
260
  /**
246
261
  * Fetches custom content and updates all specified sections with their showcases
247
- * @param sectionNames - Array of section names to update (e.g., ['example-section', 'test-section'])
262
+ * @param blockNames - Array of block names to update (e.g., ['about-us', 'example-header', 'example-footer'])
248
263
  * @param distPath - The dist path for getContentAndDesign
249
264
  * @param authToken - Bearer token from request header (e.g., "Bearer xxx")
250
265
  * @param customContentUrl - The custom content URL
251
- * @param tilesUrl - The tiles URL to fetch section IDs
266
+ * @param tilesUrl - The tiles URL to fetch block IDs
252
267
  * @returns The updated custom content response or null if failed
253
268
  */
254
269
  export async function updateCustomContent(
255
- sectionNames: string[],
270
+ blockNames: string[],
256
271
  distPath: string,
257
272
  authToken?: string,
258
273
  customContentUrl?: string,
@@ -290,13 +305,18 @@ export async function updateCustomContent(
290
305
  }
291
306
 
292
307
  const customContentSections = customContent.sections as Array<Record<string, unknown>>;
308
+ // Process all blocks and showcases in parallel
309
+ const updatePromises = blockNames.flatMap((sectionName) => {
310
+ const blockType = getBlockType(sectionName, distPath);
311
+ if (!blockType) {
312
+ console.warn(`Block not found: ${sectionName}`);
313
+ return [];
314
+ }
293
315
 
294
- // Process all sections and showcases in parallel
295
- const updatePromises = sectionNames.flatMap((sectionName) => {
296
- const showcaseIds = getShowcaseIds(sectionName, distPath);
297
-
316
+ const showcaseIds = getShowcaseIds(blockType, sectionName, distPath);
298
317
  return showcaseIds.map(showcaseId =>
299
- updateSectionShowcase(
318
+ updateBlockShowcase(
319
+ blockType,
300
320
  sectionName,
301
321
  showcaseId,
302
322
  distPath,
@@ -391,7 +391,7 @@ const server = http.createServer(async (req: http.IncomingMessage, res: http.Ser
391
391
  const { render } = serverModule.init();
392
392
 
393
393
  // Call render with context and data
394
- const result = await render(context, data || { defaults: {}, background: {}, externalContent: {} });
394
+ const result = await render(context, data || { defaults: {}, background: {} });
395
395
 
396
396
  res.statusCode = 200;
397
397
  res.setHeader('Content-Type', 'application/json');
@@ -43,31 +43,29 @@ const backgroundStyle = computed<CSSProperties>(() => {
43
43
  });
44
44
  </script>
45
45
 
46
- <style lang="scss" scoped>
47
- .about-us-section {
48
- &__wrapper {
49
- display: grid;
50
- grid-template-columns: 1fr;
51
- gap: 75px;
46
+ <style scoped>
47
+ .about-us-section__wrapper {
48
+ display: grid;
49
+ grid-template-columns: 1fr;
50
+ gap: 75px;
52
51
 
53
- @media screen and (min-width: 900px) {
54
- grid-template-columns: 1fr 1fr;
55
- align-items: center;
56
- gap: 100px;
57
- }
52
+ @media screen and (min-width: 900px) {
53
+ grid-template-columns: 1fr 1fr;
54
+ align-items: center;
55
+ gap: 100px;
58
56
  }
57
+ }
59
58
 
60
- &__image {
61
- max-width: 479px;
62
- max-height: 610px;
63
- margin: 0 auto;
64
- }
59
+ .about-us-section__image {
60
+ max-width: 479px;
61
+ max-height: 610px;
62
+ margin: 0 auto;
63
+ }
65
64
 
66
- &__content {
67
- flex: 1;
68
- display: flex;
69
- flex-direction: column;
70
- align-self: center;
71
- }
65
+ .about-us-section__content {
66
+ flex: 1;
67
+ display: flex;
68
+ flex-direction: column;
69
+ align-self: center;
72
70
  }
73
71
  </style>