@lightspeed/crane 1.4.2 → 2.0.1

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 (120) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/UPGRADE.md +19 -0
  3. package/dist/app.d.mts +1 -1028
  4. package/dist/app.d.ts +1 -1028
  5. package/dist/app.mjs +1 -1
  6. package/dist/cli.mjs +20 -7
  7. package/package.json +4 -3
  8. package/template/footers/example-footer/ExampleFooter.vue +1 -1
  9. package/template/footers/example-footer/client.ts +2 -1
  10. package/template/footers/example-footer/component/LegalLinks.vue +1 -1
  11. package/template/footers/example-footer/component/MadeWith.vue +1 -1
  12. package/template/footers/example-footer/component/ReportAbuse.vue +1 -1
  13. package/template/footers/example-footer/entity/color.ts +2 -2
  14. package/template/footers/example-footer/server.ts +2 -1
  15. package/template/headers/example-header/client.ts +2 -1
  16. package/template/headers/example-header/component/Account.vue +1 -1
  17. package/template/headers/example-header/component/Cart.vue +1 -1
  18. package/template/headers/example-header/component/CategoriesDropdown.vue +1 -1
  19. package/template/headers/example-header/component/Logo.vue +1 -1
  20. package/template/headers/example-header/component/NavigationMenu.vue +1 -1
  21. package/template/headers/example-header/component/SearchForm.vue +1 -1
  22. package/template/headers/example-header/server.ts +2 -1
  23. package/template/index.d.ts +1 -1
  24. package/template/layouts/catalog/example-catalog/Main.vue +1 -1
  25. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +2 -1
  26. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +2 -1
  27. package/template/layouts/category/example-category/Main.vue +1 -1
  28. package/template/layouts/category/example-category/settings/content.ts +1 -1
  29. package/template/layouts/category/example-category/settings/design.ts +1 -1
  30. package/template/layouts/product/example-product/Main.vue +1 -1
  31. package/template/layouts/product/example-product/settings/content.ts +1 -1
  32. package/template/layouts/product/example-product/settings/design.ts +1 -1
  33. package/template/package.json +6 -3
  34. package/template/page-templates/example-template/pages/catalog.ts +1 -1
  35. package/template/page-templates/example-template/pages/category.ts +1 -1
  36. package/template/page-templates/example-template/pages/product.ts +1 -1
  37. package/template/preview/sections/preview.html +1 -1
  38. package/template/preview/shared/api-routes.ts +515 -41
  39. package/template/preview/shared/mock.ts +43 -41
  40. package/template/preview/shared/preview.ts +220 -123
  41. package/template/preview/shared/utils.ts +209 -62
  42. package/template/preview/ssr-server.ts +430 -0
  43. package/template/preview/vite.config.js +76 -75
  44. package/template/reference/sections/about-us/AboutUs.vue +1 -1
  45. package/template/reference/sections/about-us/client.ts +1 -1
  46. package/template/reference/sections/about-us/component/Image.vue +1 -1
  47. package/template/reference/sections/about-us/component/Stats.vue +2 -2
  48. package/template/reference/sections/about-us/component/Title.vue +1 -1
  49. package/template/reference/sections/about-us/server.ts +1 -1
  50. package/template/reference/sections/about-us/util/visibility-provider.ts +1 -1
  51. package/template/reference/sections/featured-products/FeaturedProducts.vue +65 -0
  52. package/template/reference/sections/featured-products/assets/arrow.svg +3 -0
  53. package/template/reference/sections/featured-products/assets/custom_section_showcase_1_preview.png +0 -0
  54. package/template/reference/sections/featured-products/client.ts +6 -0
  55. package/template/reference/sections/featured-products/component/ProductItem.vue +71 -0
  56. package/template/reference/sections/featured-products/component/Title.vue +31 -0
  57. package/template/reference/sections/featured-products/entity/color.ts +4 -0
  58. package/template/reference/sections/featured-products/server.ts +6 -0
  59. package/template/reference/sections/featured-products/settings/content.ts +14 -0
  60. package/template/reference/sections/featured-products/settings/design.ts +33 -0
  61. package/template/reference/sections/featured-products/settings/translations.ts +24 -0
  62. package/template/reference/sections/featured-products/showcases/1.ts +28 -0
  63. package/template/reference/sections/featured-products/showcases/translations.ts +16 -0
  64. package/template/reference/sections/featured-products/type.ts +5 -0
  65. package/template/reference/sections/intro-slider/IntroSlider.vue +1 -1
  66. package/template/reference/sections/intro-slider/client.ts +2 -1
  67. package/template/reference/sections/intro-slider/component/Slider.vue +8 -2
  68. package/template/reference/sections/intro-slider/component/Title.vue +1 -1
  69. package/template/reference/sections/intro-slider/entity/color.ts +2 -2
  70. package/template/reference/sections/intro-slider/server.ts +2 -1
  71. package/template/reference/sections/tag-lines/TagLines.vue +1 -1
  72. package/template/reference/sections/tag-lines/client.ts +2 -1
  73. package/template/reference/sections/tag-lines/component/SectionImage.vue +1 -1
  74. package/template/reference/sections/tag-lines/component/Title.vue +1 -1
  75. package/template/reference/sections/tag-lines/composables/highlighted-text-image-list.ts +4 -3
  76. package/template/reference/sections/tag-lines/server.ts +2 -1
  77. package/template/reference/sections/trending-categories/TrendingCategories.vue +70 -0
  78. package/template/reference/sections/trending-categories/assets/arrow.svg +3 -0
  79. package/template/reference/sections/trending-categories/assets/custom_section_showcase_1_preview.png +0 -0
  80. package/template/reference/sections/trending-categories/client.ts +6 -0
  81. package/template/reference/sections/trending-categories/component/CategoryItem.vue +62 -0
  82. package/template/reference/sections/trending-categories/component/Title.vue +32 -0
  83. package/template/reference/sections/trending-categories/entity/color.ts +4 -0
  84. package/template/reference/sections/trending-categories/server.ts +6 -0
  85. package/template/reference/sections/trending-categories/settings/content.ts +14 -0
  86. package/template/reference/sections/trending-categories/settings/design.ts +33 -0
  87. package/template/reference/sections/trending-categories/settings/translations.ts +24 -0
  88. package/template/reference/sections/trending-categories/showcases/1.ts +36 -0
  89. package/template/reference/sections/trending-categories/showcases/translations.ts +22 -0
  90. package/template/reference/sections/trending-categories/type.ts +5 -0
  91. package/template/reference/shared/components/Button.vue +1 -1
  92. package/template/reference/shared/utils/styles.ts +1 -0
  93. package/template/reference/templates/reference-template-apparel/pages/catalog.ts +1 -1
  94. package/template/reference/templates/reference-template-apparel/pages/category.ts +1 -1
  95. package/template/reference/templates/reference-template-apparel/pages/home.ts +10 -0
  96. package/template/reference/templates/reference-template-apparel/pages/product.ts +1 -1
  97. package/template/reference/templates/reference-template-bike/pages/catalog.ts +1 -1
  98. package/template/reference/templates/reference-template-bike/pages/category.ts +1 -1
  99. package/template/reference/templates/reference-template-bike/pages/home.ts +10 -0
  100. package/template/reference/templates/reference-template-bike/pages/product.ts +1 -1
  101. package/template/sections/example-section/ExampleSection.vue +8 -1
  102. package/template/sections/example-section/client.ts +2 -1
  103. package/template/sections/example-section/component/button/Button.vue +1 -1
  104. package/template/sections/example-section/component/image/Image.vue +1 -1
  105. package/template/sections/example-section/component/image/ImagesGrid.vue +1 -1
  106. package/template/sections/example-section/component/selectbox/Selectbox.vue +1 -1
  107. package/template/sections/example-section/component/title/Title.vue +1 -1
  108. package/template/sections/example-section/component/toggle/Toggle.vue +1 -1
  109. package/template/sections/example-section/entity/color.ts +2 -2
  110. package/template/sections/example-section/server.ts +2 -1
  111. package/template/sections/example-section/settings/translations.ts +1 -1
  112. package/template/sections/example-section/showcases/1.ts +2 -22
  113. package/template/sections/example-section/showcases/2.ts +2 -22
  114. package/template/sections/example-section/showcases/3.ts +2 -22
  115. package/template/sections/example-section/showcases/translations.ts +11 -149
  116. package/template/shared/components/LanguageSelector.vue +1 -1
  117. package/template/shared/translation.ts +16 -0
  118. package/template/shared/utils.ts +3 -1
  119. package/template/tsconfig.json +1 -0
  120. package/types.d.ts +6 -457
@@ -1,7 +1,6 @@
1
- /* eslint-disable no-console */
2
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { loadModule} from "./utils.ts";
4
- import { getExternalContentMock } from "./mock";
2
+ import { getExternalContentMock } from './mock';
3
+ import { loadModule, renderServerModule } from './utils';
5
4
 
6
5
  let distFolderPath: string | null = null;
7
6
  let currentApp: any = null;
@@ -10,10 +9,29 @@ export function setDistFolderPath(path: string): void {
10
9
  distFolderPath = path;
11
10
  }
12
11
 
12
+ // ============================================================================
13
+ // Module Loading Helpers
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Loads all required modules for a showcase.
18
+ */
19
+ async function loadShowcaseModules(sectionName: string, showcaseId: string, distFolder: string): Promise<[any, any, any, any, any]> {
20
+ const basePath = `${distFolder}/sections/${sectionName}`;
21
+ return Promise.all([
22
+ loadModule(`${basePath}/js/settings/content.mjs`),
23
+ loadModule(`${basePath}/js/settings/translations.mjs`),
24
+ loadModule(`${basePath}/js/showcases/translations.mjs`),
25
+ loadModule(`${basePath}/js/showcases/${showcaseId}.mjs`),
26
+ loadModule(`${basePath}/js/settings/design.mjs`),
27
+ ]) as Promise<[any, any, any, any, any]>;
28
+ }
29
+
13
30
  interface HSLColor {
14
31
  h: number;
15
32
  s: number;
16
33
  l: number;
34
+ a: number;
17
35
  }
18
36
 
19
37
  interface RGBAColor {
@@ -27,12 +45,14 @@ interface ColorObject {
27
45
  hex: string;
28
46
  hsl: HSLColor;
29
47
  rgba: RGBAColor;
48
+ auto: boolean;
49
+ raw: string;
30
50
  }
31
51
 
32
52
  function hexToColorObject(hex: string): ColorObject {
33
53
  // Support both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) hex colors
34
54
  const match = /^#?([0-9a-fA-F]{6}([0-9a-fA-F]{2})?)$/.exec(hex);
35
- if (!match) throw new Error("Invalid hex color format");
55
+ if (!match) throw new Error('Invalid hex color format');
36
56
  const cleanHex = match[1].toLowerCase();
37
57
  const r = parseInt(cleanHex.substring(0, 2), 16);
38
58
  const g = parseInt(cleanHex.substring(2, 4), 16);
@@ -42,21 +62,38 @@ function hexToColorObject(hex: string): ColorObject {
42
62
  const rNorm = r / 255, gNorm = g / 255, bNorm = b / 255;
43
63
  const max = Math.max(rNorm, gNorm, bNorm), min = Math.min(rNorm, gNorm, bNorm);
44
64
  const delta = max - min;
45
- let h = 0, s = 0, l = (max + min) / 2;
65
+ let h = 0, s = 0;
66
+ const l = (max + min) / 2;
46
67
  if (delta !== 0) {
47
68
  s = delta / (1 - Math.abs(2 * l - 1));
48
69
  switch (max) {
49
- case rNorm: h = ((gNorm - bNorm) / delta) % 6; break;
50
- case gNorm: h = (bNorm - rNorm) / delta + 2; break;
51
- case bNorm: h = (rNorm - gNorm) / delta + 4; break;
70
+ case rNorm: {
71
+ h = ((gNorm - bNorm) / delta) % 6;
72
+ break;
73
+ }
74
+ case gNorm: {
75
+ h = (bNorm - rNorm) / delta + 2;
76
+ break;
77
+ }
78
+ case bNorm: {
79
+ h = (rNorm - gNorm) / delta + 4;
80
+ break;
81
+ }
52
82
  }
53
83
  h *= 60;
54
84
  if (h < 0) h += 360;
55
85
  }
56
86
  return {
57
87
  hex: `#${cleanHex.substring(0, 6)}${a.toString(16).padStart(2, '0')}`,
58
- hsl: { h: Math.round(h), s: +(s * 100).toFixed(1), l: +(l * 100).toFixed(1) },
88
+ hsl: {
89
+ h: +(h / 360).toFixed(4),
90
+ s: +s.toFixed(4),
91
+ l: +l.toFixed(4),
92
+ a: +(a / 255).toFixed(2),
93
+ },
59
94
  rgba: { r, g, b, a: +(a / 255).toFixed(2) },
95
+ auto: false,
96
+ raw: `#${cleanHex.substring(0, 6).toUpperCase()}`,
60
97
  };
61
98
  }
62
99
 
@@ -73,7 +110,7 @@ function updateHexColors(obj: any): any {
73
110
  // If it’s a 3-digit hex, expand it to 6 digits
74
111
  const expanded = v.replace(
75
112
  /^#?([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/,
76
- (_m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`
113
+ (_m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`,
77
114
  );
78
115
  value[key] = hexToColorObject(expanded);
79
116
  } else {
@@ -96,75 +133,68 @@ const replaceGlobalFont = (obj: any, font: string): void => {
96
133
  });
97
134
  };
98
135
 
99
- function createBackgroundStructure(type: 'solid' | 'gradient', config: any): any {
136
+ function createBackgroundObject(type: string, fromColor: any, toColor: any): any {
100
137
  return {
101
138
  background: {
102
- background: {
103
- type,
104
- ...config,
139
+ type: type,
140
+ solid: {
141
+ color: fromColor,
105
142
  },
143
+ gradient: { fromColor: fromColor, toColor: toColor },
106
144
  },
107
145
  };
108
146
  }
109
147
 
110
- // Helper function to create default gray background
111
- function createDefaultBackground(): any {
148
+ function createBackgroundDesign(showcaseBackground: any, isApiRender: boolean = false): any {
149
+ // Helper to check if color is global reference
150
+ const isGlobalColor = (color: string) => typeof color === 'string' && color.startsWith('global.');
112
151
  const defaultGrayColor = hexToColorObject('#F9F9F9');
113
- return createBackgroundStructure('solid', {
114
- solid: { color: defaultGrayColor },
115
- color: 'global.color.background',
116
- });
117
- }
118
-
119
- // Helper function to check if color is global reference
120
- function isGlobalColor(color: string): boolean {
121
- return typeof color === 'string' && color.startsWith('global.');
122
- }
123
152
 
124
- function createBackgroundDesign(showcaseBackground: any): any {
125
153
  // No background or invalid background
126
154
  if (!showcaseBackground) {
127
- return createDefaultBackground();
155
+ const defaultColor = 'global.color.background';
156
+ if (isApiRender) {
157
+ // API render: keep global as string
158
+ return createBackgroundObject('solid', defaultColor, defaultColor);
159
+ } else {
160
+ // Local preview: convert to color object
161
+ return createBackgroundObject('solid', defaultGrayColor, defaultGrayColor);
162
+ }
128
163
  }
129
164
 
130
165
  // Handle gradient backgrounds
131
166
  if (showcaseBackground.style === 'GRADIENT' && Array.isArray(showcaseBackground.color)) {
132
167
  const [fromColor, toColor] = showcaseBackground.color;
133
168
 
134
- // If either color is global, use default
135
- if (isGlobalColor(fromColor) || isGlobalColor(toColor)) {
136
- return createDefaultBackground();
169
+ // Check if colors are global references
170
+ const fromIsGlobal = isGlobalColor(fromColor);
171
+ const toIsGlobal = isGlobalColor(toColor);
172
+ if (isApiRender) {
173
+ // Convert each color: keep global as string, convert hex to object
174
+ const fromColorValue = fromIsGlobal ? fromColor : hexToColorObject(fromColor);
175
+ const toColorValue = toIsGlobal ? toColor : hexToColorObject(toColor);
176
+ return createBackgroundObject('gradient', fromColorValue, toColorValue);
177
+ } else {
178
+ const fromColorValue = fromIsGlobal ? defaultGrayColor : hexToColorObject(fromColor);
179
+ const toColorValue = toIsGlobal ? defaultGrayColor : hexToColorObject(toColor);
180
+ return createBackgroundObject('gradient', fromColorValue, toColorValue);
137
181
  }
138
-
139
- // Create gradient background
140
- return createBackgroundStructure('gradient', {
141
- solid: { color: hexToColorObject(fromColor) },
142
- gradient: {
143
- fromColor: hexToColorObject(fromColor),
144
- toColor: hexToColorObject(toColor),
145
- },
146
- color: `gradient(${fromColor}, ${toColor})`,
147
- });
148
182
  }
149
183
 
150
184
  // Handle solid color backgrounds
151
- const solidColor = showcaseBackground.color;
152
-
153
- // Global color reference
154
- if (isGlobalColor(solidColor)) {
155
- return createDefaultBackground();
156
- }
157
-
158
- // Specific hex color
159
- if (typeof solidColor === 'string') {
160
- return createBackgroundStructure('solid', {
161
- solid: { color: hexToColorObject(solidColor) },
162
- color: solidColor,
163
- });
185
+ if (showcaseBackground.style === 'COLOR') {
186
+ // According to source code array is also possible
187
+ const showcaseColor = showcaseBackground.color;
188
+ const solidColor = Array.isArray(showcaseColor) ? showcaseColor[0] : showcaseColor;
189
+ if (isApiRender) {
190
+ const finalColor = isGlobalColor(solidColor) ? solidColor : hexToColorObject(solidColor);
191
+ return createBackgroundObject('solid', finalColor, finalColor);
192
+ } else {
193
+ // Hex color - convert to color object
194
+ const finalColor = isGlobalColor(solidColor) ? defaultGrayColor : hexToColorObject(solidColor);
195
+ return createBackgroundObject('solid', finalColor, finalColor);
196
+ }
164
197
  }
165
-
166
- // Fallback to default
167
- return createDefaultBackground();
168
198
  }
169
199
 
170
200
  function overrideSettingsFromShowcase(content: any, showcases: any): any {
@@ -173,7 +203,7 @@ function overrideSettingsFromShowcase(content: any, showcases: any): any {
173
203
 
174
204
  export function mergeDesign(
175
205
  design: Record<string, any>,
176
- showcase: Record<string, any>
206
+ showcase: Record<string, any>,
177
207
  ): Record<string, any> {
178
208
  const result: Record<string, any> = { ...design };
179
209
 
@@ -183,9 +213,9 @@ export function mergeDesign(
183
213
  const override = showcase[key];
184
214
 
185
215
  if (
186
- base !== null && override !== null &&
187
- typeof base === 'object' && typeof override === 'object' &&
188
- !Array.isArray(base) && !Array.isArray(override)
216
+ base !== null && override !== null
217
+ && typeof base === 'object' && typeof override === 'object'
218
+ && !Array.isArray(base) && !Array.isArray(override)
189
219
  ) {
190
220
  // shallow merge nested objects
191
221
  result[key] = { ...base, ...override };
@@ -199,20 +229,36 @@ export function mergeDesign(
199
229
  return result;
200
230
  }
201
231
 
202
- export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any> ): Record<string, any> {
232
+ export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>): Record<string, any> {
203
233
  const parsedDesign: Record<string, any> = {};
204
234
  const parsedShowcaseDesign: Record<string, any> = {};
205
235
  Object.entries(design).forEach(([key, comp]) => {
206
- parsedDesign[key] = comp?.defaults;
236
+ // Skip DIVIDER elements - they're UI-only and don't have runtime data
237
+ if (comp?.type === 'DIVIDER') {
238
+ return;
239
+ }
240
+ // Skip BACKGROUND - it's handled separately by createBackgroundDesign
241
+ if (comp?.type === 'BACKGROUND') {
242
+ return;
243
+ }
244
+ // Use defaults if available, otherwise use empty object
245
+ parsedDesign[key] = comp?.defaults || {};
207
246
  });
208
247
 
209
248
  Object.entries(showCaseDesign).forEach(([key, comp]) => {
210
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
249
+ // Skip DIVIDER elements - they're UI-only and don't have runtime data
250
+ if (comp?.type === 'DIVIDER') {
251
+ return;
252
+ }
253
+ // Skip BACKGROUND - it's handled separately by createBackgroundDesign
254
+ if (comp?.type === 'BACKGROUND') {
255
+ return;
256
+ }
211
257
  const { type, ...withoutType } = comp;
212
- parsedShowcaseDesign[key] = type == 'TEXT' ? { ... withoutType, visible: true} : withoutType
258
+ parsedShowcaseDesign[key] = type == 'TEXT' ? { ...withoutType, visible: true } : withoutType;
213
259
  });
214
260
 
215
- let overridenDesign = mergeDesign(parsedDesign, parsedShowcaseDesign)
261
+ let overridenDesign = mergeDesign(parsedDesign, parsedShowcaseDesign);
216
262
  overridenDesign = updateHexColors(overridenDesign);
217
263
  replaceGlobalFont(overridenDesign, 'Roboto');
218
264
  return overridenDesign;
@@ -231,13 +277,13 @@ function processImage(component: any, sectionName: string, key: string): Record<
231
277
  [key]: {
232
278
  set: newSet,
233
279
  ...(component.imageData?.borderInfo && { borderInfo: component.imageData.borderInfo }),
234
- ...(component.imageData && { bucket: {} })
280
+ ...(component.imageData && { bucket: {} }),
235
281
  },
236
282
  };
237
283
  }
238
284
 
239
285
  function processComponent(key: string, component: any, translations: any, sectionName?: string): any {
240
- if (!component?.type) return '';
286
+ if (!component?.type) return {};
241
287
  switch (component.type) {
242
288
  case 'INPUTBOX':
243
289
  case 'TEXTAREA': {
@@ -281,9 +327,19 @@ function processComponent(key: string, component: any, translations: any, sectio
281
327
  return { [key]: component.defaults.value };
282
328
  case 'IMAGE':
283
329
  return processImage(component, sectionName!, key);
330
+ case 'INFO':
331
+ return {
332
+ [key]: {
333
+ label: translations[component.label],
334
+ description: translations[component.description],
335
+ button: {
336
+ label: translations[component.button.label],
337
+ link: component.button.link,
338
+ },
339
+ },
340
+ };
284
341
  default:
285
- console.warn(`Unknown type: ${component.type}`);
286
- return '';
342
+ return {};
287
343
  }
288
344
  }
289
345
 
@@ -292,7 +348,7 @@ function getContentToRender(
292
348
  showcase: any,
293
349
  contentTranslations: any,
294
350
  showcaseTranslations: any,
295
- sectionName: string
351
+ sectionName: string,
296
352
  ): any {
297
353
  const parsedContent = Object.entries(content).reduce((acc, [k, c]) => {
298
354
  return { ...acc, ...processComponent(k, c, contentTranslations, sectionName) };
@@ -306,7 +362,7 @@ function getContentToRender(
306
362
  }
307
363
 
308
364
  export function dropdownOptions(showcaseModules: Record<string, any>): Array<{ value: string; label: string }> {
309
- return Object.keys(showcaseModules).map(path => {
365
+ return Object.keys(showcaseModules).map((path) => {
310
366
  const match = path.match(/\/sections\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
311
367
  if (!match) return null;
312
368
  return {
@@ -323,89 +379,130 @@ export function loadSectionCss(sectionName: string): void {
323
379
  document.head.appendChild(link);
324
380
  }
325
381
 
382
+ /**
383
+ * Renders a showcase in the browser (client-side).
384
+ */
326
385
  export async function renderShowcase(sectionName: string, showcaseId: string): Promise<void> {
327
- const client = await loadModule(`${distFolderPath}/sections/${sectionName}/js/main/client/client.js`);
328
- const content = await loadModule(`${distFolderPath}/sections/${sectionName}/js/settings/content.mjs`);
329
- const contentTranslations = await loadModule(`${distFolderPath}/sections/${sectionName}/js/settings/translations.mjs`);
330
- const showcaseTranslations = await loadModule(`${distFolderPath}/sections/${sectionName}/js/showcases/translations.mjs`);
331
- const showcase = await loadModule(`${distFolderPath}/sections/${sectionName}/js/showcases/${showcaseId}.mjs`);
332
- const design = await loadModule(`${distFolderPath}/sections/${sectionName}/js/settings/design.mjs`);
386
+ if (!distFolderPath) throw new Error('distFolderPath not set');
333
387
 
334
- // Get showcase background and create background design
335
- const showcaseBackground = showcase.default?.design?.background;
336
- const backgroundDesign = createBackgroundDesign(showcaseBackground);
388
+ const basePath = `${distFolderPath}/sections/${sectionName}`;
389
+ const [content, contentTranslations, showcaseTranslations, showcase, design]
390
+ = await loadShowcaseModules(sectionName, showcaseId, distFolderPath);
337
391
 
338
- const ovveridenDesign = designTransformer(design.default, showcase.default.design || {});
392
+ const client = await loadModule(`${basePath}/js/main/client/client.js`) as any;
339
393
 
340
- loadSectionCss(sectionName);
394
+ // Prepare design
395
+ const showcaseBackground = (showcase as any).default?.design?.background;
396
+ const backgroundDesign = createBackgroundDesign(showcaseBackground, false); // Local preview
397
+ const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
398
+ overriddenDesign.background = backgroundDesign;
341
399
 
400
+ // Prepare content
342
401
  const overriddenContent = getContentToRender(
343
- content.default,
344
- showcase.default.content || {},
345
- contentTranslations.default.en,
346
- showcaseTranslations.default.en,
347
- sectionName
402
+ (content as any).default,
403
+ (showcase as any).default.content || {},
404
+ (contentTranslations as any).default.en,
405
+ (showcaseTranslations as any).default.en,
406
+ sectionName,
348
407
  );
349
408
 
350
- // Get external content mock (can be customized in compiled mock.js)
351
- const externalContentMock = getExternalContentMock();
352
-
353
- // Ensure background design always overrides any existing background
354
- const finalDesign = { ...ovveridenDesign };
355
- finalDesign.background = backgroundDesign.background;
409
+ // Load CSS
410
+ loadSectionCss(sectionName);
356
411
 
412
+ // Prepare state
357
413
  const state = {
358
- context: {
359
- globalDesign: { color: 'global.color.background' },
360
- },
414
+ context: { globalDesign: { color: 'global.color.background' } },
361
415
  data: {
362
416
  content: overriddenContent,
363
- design: finalDesign,
417
+ design: overriddenDesign,
364
418
  defaults: {},
365
419
  background: {},
366
- externalContent: externalContentMock,
420
+ externalContent: getExternalContentMock(),
367
421
  },
368
422
  };
369
423
 
370
- // If app is already mounted, update it; otherwise mount it
424
+ // Mount or update app
371
425
  if (currentApp) {
372
426
  currentApp.update(state);
373
427
  } else {
374
- const { mount, update, unmount } = client.default.init();
428
+ const { mount, update, unmount } = (client as any).default.init();
375
429
  currentApp = { update, unmount };
376
430
  mount('#app', state);
377
431
  }
378
432
  }
379
433
 
380
- export async function getContentAndDesign(sectionName: string, showcaseId: string, distFolder:string) {
381
- const content = await loadModule(`${distFolder}/sections/${sectionName}/js/settings/content.mjs`);
382
- const contentTranslations = await loadModule(`${distFolder}/sections/${sectionName}/js/settings/translations.mjs`);
383
- const showcaseTranslations = await loadModule(`${distFolder}/sections/${sectionName}/js/showcases/translations.mjs`);
384
- const showcase = await loadModule(`${distFolder}/sections/${sectionName}/js/showcases/${showcaseId}.mjs`);
385
- const design = await loadModule(`${distFolder}/sections/${sectionName}/js/settings/design.mjs`);
434
+ async function getLayout(showcase: any, sectionName: string, distFolder: string) {
435
+ // First, try to get layoutId from showcase
436
+ if (showcase.default?.layoutId) {
437
+ return showcase.default.layoutId;
438
+ }
386
439
 
387
- // Get showcase background and create background design
388
- const showcaseBackground = showcase.default?.design?.background;
389
- const backgroundDesign = createBackgroundDesign(showcaseBackground);
440
+ // If not present in showcase, read from section's layout.mjs
441
+ const basePath = `${distFolder}/sections/${sectionName}`;
442
+ const layoutModule = await loadModule(`${basePath}/js/settings/layout.mjs`) as any;
390
443
 
391
- const ovveridenDesign = designTransformer(design.default, showcase.default.design || {});
444
+ // layout.mjs exports an array of layout configurations
445
+ // Return the first element's layoutId
446
+ if (Array.isArray(layoutModule.default) && layoutModule.default.length > 0) {
447
+ return layoutModule.default[0].layoutId;
448
+ }
449
+ throw new Error('Layout module is not an array or is empty');
450
+ }
392
451
 
452
+ /**
453
+ * Prepares showcase data (content and design) without rendering.
454
+ * Used by API routes to return tile data.
455
+ */
456
+ export async function getShowcaseData(sectionName: string, showcaseId: string, distFolder: string) {
457
+ const [content, contentTranslations, showcaseTranslations, showcase, design]
458
+ = await loadShowcaseModules(sectionName, showcaseId, distFolder);
459
+
460
+ // Prepare design
461
+ const showcaseBackground = (showcase as any).default?.design?.background;
462
+ const backgroundDesign = createBackgroundDesign(showcaseBackground, true); // API render
463
+ const overriddenDesign = designTransformer((design as any).default, (showcase as any).default.design || {});
464
+ overriddenDesign.background = backgroundDesign;
465
+ overriddenDesign.layout = await getLayout(showcase, sectionName, distFolder);
466
+
467
+ // Prepare content
393
468
  const overriddenContent = getContentToRender(
394
- content.default,
395
- showcase.default.content || {},
396
- contentTranslations.default.en,
397
- showcaseTranslations.default.en,
398
- sectionName
469
+ (content as any).default,
470
+ (showcase as any).default.content || {},
471
+ (contentTranslations as any).default.en,
472
+ (showcaseTranslations as any).default.en,
473
+ sectionName,
399
474
  );
400
475
 
401
- // Ensure background design always overrides any existing background
402
- const finalDesign = {...ovveridenDesign};
403
- finalDesign.background = backgroundDesign.background;
476
+ return {
477
+ content: overriddenContent,
478
+ design: overriddenDesign,
479
+ };
480
+ }
404
481
 
405
- const state = {
406
- content: overriddenContent,
407
- design: finalDesign,
482
+ /**
483
+ * Gets the showcase state by rendering server-side.
484
+ * Uses renderServerModule to get HTML directly from the SSR server.
485
+ */
486
+ export async function getShowcaseState(sectionName: string, showcaseId: string, distFolder: string, context: Record<string, any>) {
487
+ const { content: overriddenContent, design: overriddenDesign }
488
+ = await getShowcaseData(sectionName, showcaseId, distFolder);
489
+
490
+ const data = {
491
+ content: overriddenContent,
492
+ design: overriddenDesign,
493
+ defaults: {},
494
+ background: {},
495
+ externalContent: getExternalContentMock(),
408
496
  };
409
497
 
410
- return state;
498
+ // Render server-side using the SSR server
499
+ const basePath = `${distFolder}/sections/${sectionName}`;
500
+ const serverModulePath = `${basePath}/js/main/server/server.js`;
501
+ const html = await renderServerModule(serverModulePath, context, data);
502
+
503
+ return {
504
+ content: overriddenContent,
505
+ design: overriddenDesign,
506
+ html,
507
+ };
411
508
  }