@lightspeed/crane 2.0.3 → 2.0.5

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 (28) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/cli.mjs +37 -21
  3. package/package.json +3 -2
  4. package/template/headers/example-header/showcases/2.ts +0 -3
  5. package/template/page-templates/example-template/pages/custom.ts +6 -0
  6. package/template/preview/shared/api-routes.ts +337 -11
  7. package/template/preview/shared/preview.ts +86 -30
  8. package/template/preview/shared/utils.ts +0 -1
  9. package/template/preview/vite.config.js +5 -0
  10. package/template/reference/sections/about-us/component/Stats.vue +2 -2
  11. package/template/reference/sections/about-us/settings/content.ts +28 -21
  12. package/template/reference/sections/about-us/settings/design.ts +1 -1
  13. package/template/reference/sections/about-us/showcases/1.ts +1 -0
  14. package/template/reference/sections/about-us/showcases/2.ts +1 -0
  15. package/template/reference/sections/intro-slider/component/Slider.vue +1 -1
  16. package/template/reference/sections/intro-slider/component/Title.vue +2 -2
  17. package/template/reference/sections/intro-slider/settings/content.ts +22 -1
  18. package/template/reference/sections/intro-slider/settings/design.ts +1 -1
  19. package/template/reference/sections/intro-slider/showcases/1.ts +1 -0
  20. package/template/reference/sections/intro-slider/showcases/2.ts +1 -0
  21. package/template/reference/sections/tag-lines/component/HighlightedText.vue +1 -1
  22. package/template/reference/sections/tag-lines/component/Title.vue +1 -1
  23. package/template/reference/sections/tag-lines/settings/content.ts +34 -0
  24. package/template/reference/sections/trending-categories/component/Title.vue +1 -1
  25. package/template/sections/example-section/settings/content.ts +15 -13
  26. package/template/sections/example-section/showcases/1.ts +0 -1
  27. package/template/sections/example-section/showcases/2.ts +0 -1
  28. package/template/sections/example-section/showcases/3.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightspeed/crane",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "type": "module",
5
5
  "bin": "bin/crane.js",
6
6
  "main": "./dist/app.mjs",
@@ -64,8 +64,9 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@jridgewell/sourcemap-codec": "^1.5.4",
67
- "@lightspeed/crane-api": "1.0.2",
67
+ "@lightspeed/crane-api": "1.1.1",
68
68
  "@lightspeed/eslint-config-crane": "1.1.3",
69
+ "@types/micromatch": "^4.0.8",
69
70
  "@types/prompts": "^2.4.2",
70
71
  "@vitejs/plugin-vue": "^6.0.1",
71
72
  "adm-zip": "^0.5.16",
@@ -17,9 +17,6 @@ export default {
17
17
  logoType: 'IMAGE',
18
18
  imageData: {
19
19
  set: {
20
- LOW_RES: {
21
- url: 'lightspeed_logo.png',
22
- },
23
20
  MOBILE_WEBP_LOW_RES: {
24
21
  url: 'lightspeed_logo.png',
25
22
  },
@@ -0,0 +1,6 @@
1
+ export default {
2
+ metadata: {
3
+ title: 'Custom Page',
4
+ },
5
+ sections: [],
6
+ };
@@ -3,9 +3,54 @@ import type { IncomingMessage, ServerResponse } from 'http';
3
3
  import * as path from 'path';
4
4
  import { URL } from 'url';
5
5
 
6
+ import type {
7
+ ContentEditor,
8
+ ContentSettings,
9
+ DeckContentEditor,
10
+ DesignSettings,
11
+ LayoutSettings,
12
+ } from '@lightspeed/crane-api';
13
+
6
14
  import { getShowcaseData } from './preview';
7
15
  import { fetchTiles, updateTilesSection, updateCustomContent } from './utils';
8
16
 
17
+ /**
18
+ * AppBlock type definition for block-config response
19
+ */
20
+ interface AppBlock {
21
+ id: string;
22
+ type: string;
23
+ name: string;
24
+ contentEditors: ContentSettings;
25
+ designEditors: DesignSettings;
26
+ layouts: LayoutSettings[];
27
+ }
28
+
29
+ /**
30
+ * Layout config item for the config.layoutConfigList
31
+ */
32
+ interface LayoutConfigItem {
33
+ name: string;
34
+ contentEditorConfig: {
35
+ // mainEditors contains all fields from each editor definition
36
+ // with type transformed and translations extracted to English
37
+ mainEditors: Record<string, unknown>[];
38
+ buttons: unknown[];
39
+ };
40
+ designEditorConfig: {
41
+ // mainEditors contains all fields from each editor definition
42
+ // with type transformed and translations extracted to English
43
+ mainEditors: Record<string, unknown>[];
44
+ customEditors: unknown[];
45
+ buttons: unknown[];
46
+ designEditorGroups: unknown[];
47
+ };
48
+ defaults: Record<string, unknown>;
49
+ deprecated: boolean;
50
+ svgIcon: string;
51
+ svgIconText: string;
52
+ }
53
+
9
54
 
10
55
  /**
11
56
  * Extract query parameter from URL
@@ -52,6 +97,282 @@ function setNoCacheHeaders(res: ServerResponse): void {
52
97
  res.setHeader('Surrogate-Control', 'no-store');
53
98
  }
54
99
 
100
+ /**
101
+ * Check if an object is a translation object (has 'en' key with string value)
102
+ */
103
+ function isTranslationObject(obj: unknown): obj is Record<string, string> {
104
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false;
105
+ const record = obj as Record<string, unknown>;
106
+ return 'en' in record && typeof record.en === 'string';
107
+ }
108
+
109
+ /**
110
+ * Recursively process a value and extract English translations where applicable
111
+ * - If the value is a translation object (has 'en' key), return the English value
112
+ * - If the value is an array, process each element
113
+ * - If the value is an object, process each property
114
+ * - Otherwise, return the value as-is
115
+ */
116
+ function extractEnglishTranslations(value: unknown): unknown {
117
+ if (value === null || value === undefined) return value;
118
+
119
+ // Check if it's a translation object
120
+ if (isTranslationObject(value)) {
121
+ return value.en;
122
+ }
123
+
124
+ // Handle arrays
125
+ if (Array.isArray(value)) {
126
+ return value.map(item => extractEnglishTranslations(item));
127
+ }
128
+
129
+ // Handle objects (but not translation objects, which we already handled)
130
+ if (typeof value === 'object') {
131
+ const result: Record<string, unknown> = {};
132
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
133
+ result[key] = extractEnglishTranslations(val);
134
+ }
135
+ return result;
136
+ }
137
+
138
+ // Return primitives as-is
139
+ return value;
140
+ }
141
+
142
+ /**
143
+ * Transform editor type to the expected format
144
+ * INPUTBOX -> TEXT_EDITOR
145
+ * TEXTAREA -> MULTILINE_TEXT_EDITOR
146
+ * All other types -> {TYPE}_EDITOR (e.g., DECK -> DECK_EDITOR)
147
+ */
148
+ function transformEditorType(type: string): string {
149
+ const upperType = type.toUpperCase();
150
+ switch (upperType) {
151
+ case 'INPUTBOX':
152
+ return 'TEXT_EDITOR';
153
+ case 'TEXTAREA':
154
+ return 'MULTILINE_TEXT_EDITOR';
155
+ case 'BUTTON':
156
+ return 'ACTION_LINK_EDITOR';
157
+ default:
158
+ return `${upperType}_EDITOR`;
159
+ }
160
+ }
161
+
162
+ function processEditorExceptDeck(setting: ContentEditor, fieldName: string) {
163
+ const result: Record<string, unknown> = {
164
+ field: fieldName,
165
+ };
166
+
167
+ for (const [key, value] of Object.entries(setting)) {
168
+ if (key === 'type') {
169
+ result.type = transformEditorType(value as string);
170
+ } else {
171
+ result[key] = extractEnglishTranslations(value);
172
+ }
173
+ }
174
+ return result;
175
+ }
176
+
177
+ /**
178
+ * Process a DECK editor: transform cards.defaultCardContent.settings into editors array
179
+ */
180
+ function processDeckEditor(editor: DeckContentEditor, fieldName: string): Record<string, unknown> {
181
+ const result: Record<string, unknown> = {
182
+ field: fieldName,
183
+ type: 'DECK_EDITOR',
184
+ };
185
+
186
+ // Process cards to extract editors and defaultCard
187
+ const cards = editor.cards as Record<string, unknown> | undefined;
188
+ if (cards && cards.defaultCardContent) {
189
+ const defaultCardContent = cards.defaultCardContent as Record<string, unknown>;
190
+ const settings = defaultCardContent.settings as ContentSettings;
191
+
192
+ // Create defaultCard with label and editors
193
+ const defaultCard: Record<string, unknown> = {
194
+ label: extractEnglishTranslations(defaultCardContent.label),
195
+ };
196
+
197
+ // Create editors array from settings and add to defaultCard
198
+ if (settings) {
199
+ const editors: Record<string, unknown>[] = [];
200
+ for (const [settingFieldName, settingEditor] of Object.entries(settings)) {
201
+ const processedEditor: Record<string, unknown> = processEditorExceptDeck(settingEditor, settingFieldName);
202
+ editors.push(processedEditor);
203
+ }
204
+ defaultCard.editors = editors;
205
+ }
206
+
207
+ result.defaultCard = defaultCard;
208
+ }
209
+
210
+ // Copy other fields (maxCards, addButtonLabel, label, etc.) but skip 'cards' and 'type'
211
+ for (const [key, value] of Object.entries(editor)) {
212
+ if (key === 'type' || key === 'cards') {
213
+ continue;
214
+ }
215
+ result[key] = extractEnglishTranslations(value);
216
+ }
217
+
218
+ return result;
219
+ }
220
+
221
+ /**
222
+ * Process an editor definition: keep all fields, transform type, extract English translations
223
+ * For DECK type, special processing is applied to create editors array from cards.defaultCardContent.settings
224
+ */
225
+ function processEditor(editor: ContentEditor, fieldName: string): Record<string, unknown> {
226
+ const editorType = (editor.type as string || '').toUpperCase();
227
+
228
+ // Special handling for DECK type
229
+ if (editorType === 'DECK') {
230
+ return processDeckEditor(editor as DeckContentEditor, fieldName);
231
+ }
232
+
233
+ return processEditorExceptDeck(editor, fieldName);
234
+ }
235
+
236
+
237
+ /**
238
+ * Build layout config from appBlock data
239
+ * Creates the config.layoutConfigList structure from appBlock's layouts, contentEditors, and designEditors
240
+ */
241
+ function buildLayoutConfig(appBlock: AppBlock): { type: string; layoutConfigList: LayoutConfigItem[] } {
242
+ const { layouts, contentEditors, designEditors } = appBlock;
243
+
244
+ // contentEditors and designEditors are objects with field names as keys
245
+ const contentEditorKeys = Object.keys(contentEditors);
246
+ const designEditorKeys = Object.keys(designEditors);
247
+
248
+ const layoutConfigList = layouts.map((layout, index) => {
249
+ const { layoutId, selectedContentSettings, selectedDesignSettings } = layout;
250
+
251
+ const contentFieldsToInclude = selectedContentSettings.length === 0
252
+ ? contentEditorKeys
253
+ : selectedContentSettings;
254
+
255
+ const contentMainEditors = contentFieldsToInclude
256
+ .filter((fieldName) => {
257
+ const editor = contentEditors[fieldName];
258
+ return editor && editor.type;
259
+ })
260
+ .map((fieldName) => {
261
+ const editor = contentEditors[fieldName];
262
+ return processEditor(editor as ContentEditor, fieldName);
263
+ });
264
+
265
+ // Determine which design fields to include
266
+ const designFieldsToInclude = selectedDesignSettings.length === 0
267
+ ? designEditorKeys
268
+ : selectedDesignSettings.map(s => s.fieldName);
269
+
270
+ // Build design mainEditors - include all design editor types with all their fields
271
+ const designMainEditors = designFieldsToInclude
272
+ .filter((fieldName) => {
273
+ const editor = designEditors[fieldName];
274
+ return editor && editor.type;
275
+ })
276
+ .map((fieldName) => {
277
+ const editor = designEditors[fieldName];
278
+ return processEditor(editor as ContentEditor, fieldName);
279
+ });
280
+
281
+ // Build defaults from design editors (extract English translations)
282
+ const defaults: Record<string, unknown> = {};
283
+ designFieldsToInclude.forEach((fieldName) => {
284
+ const editor = designEditors[fieldName];
285
+ if (editor && editor.defaults) {
286
+ defaults[fieldName] = extractEnglishTranslations(editor.defaults);
287
+ }
288
+ });
289
+
290
+ // Apply layout-specific design overrides from selectedDesignSettings
291
+ selectedDesignSettings.forEach((setting) => {
292
+ if (typeof setting === 'object' && setting.fieldName && setting.defaults) {
293
+ defaults[setting.fieldName] = {
294
+ ...(defaults[setting.fieldName] as Record<string, unknown> || {}),
295
+ ...(extractEnglishTranslations(setting.defaults) as Record<string, unknown>),
296
+ };
297
+ }
298
+ });
299
+
300
+ return {
301
+ name: layoutId,
302
+ contentEditorConfig: {
303
+ mainEditors: contentMainEditors,
304
+ buttons: [],
305
+ },
306
+ designEditorConfig: {
307
+ mainEditors: designMainEditors,
308
+ customEditors: [],
309
+ buttons: [],
310
+ designEditorGroups: [],
311
+ },
312
+ defaults,
313
+ deprecated: false,
314
+ svgIcon: 'CustomAutomaticLayoutIcon',
315
+ svgIconText: String(index + 1),
316
+ };
317
+ });
318
+
319
+ return {
320
+ type: 'CUSTOM',
321
+ layoutConfigList,
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Handle GET /api/v1/block-config-full
327
+ * Fetches appBlock from /api/v1/block-config and adds config.layoutConfigList
328
+ */
329
+ async function handleGetBlockConfigFull(
330
+ req: IncomingMessage,
331
+ res: ServerResponse,
332
+ sectionName: string,
333
+ typeParam: string,
334
+ ): Promise<void> {
335
+ try {
336
+ // Fetch appBlock from the CLI's /api/v1/block-config endpoint
337
+ const host = req.headers.host || 'localhost:5173';
338
+ const protocol = 'http';
339
+ const blockConfigUrl = `${protocol}://${host}/api/v1/block-config?section=${encodeURIComponent(sectionName)}&type=${encodeURIComponent(typeParam)}`;
340
+
341
+ const response = await fetch(blockConfigUrl);
342
+
343
+ if (!response.ok) {
344
+ const errorText = await response.text();
345
+ res.statusCode = response.status;
346
+ res.setHeader('Content-Type', 'application/json');
347
+ setNoCacheHeaders(res);
348
+ res.end(errorText);
349
+ return;
350
+ }
351
+
352
+ const appBlock = await response.json() as AppBlock;
353
+
354
+ // Build the config with layoutConfigList
355
+ const config = buildLayoutConfig(appBlock);
356
+
357
+ // Return appBlock with config added
358
+ res.statusCode = 200;
359
+ res.setHeader('Content-Type', 'application/json');
360
+ setNoCacheHeaders(res);
361
+ res.end(JSON.stringify({
362
+ config,
363
+ }, null, 2));
364
+ } catch (error) {
365
+ console.error('[API Routes] ❌ Error fetching block config:', error);
366
+ res.statusCode = 500;
367
+ res.setHeader('Content-Type', 'application/json');
368
+ setNoCacheHeaders(res);
369
+ res.end(JSON.stringify({
370
+ error: 'Failed to fetch block config',
371
+ message: error instanceof Error ? error.message : String(error),
372
+ }));
373
+ }
374
+ }
375
+
55
376
  /**
56
377
  * Get all available showcases for a section from the dist folder
57
378
  */
@@ -67,15 +388,8 @@ function getAvailableShowcases(sectionName: string, distPath: string): string[]
67
388
  const showcases = fs.readdirSync(showcasesPath, { withFileTypes: true })
68
389
  .filter(dirent => dirent.isFile() && dirent.name.endsWith('.mjs'))
69
390
  .map(dirent => dirent.name.replace('.mjs', ''))
70
- .sort((a, b) => {
71
- // Sort numerically if both are numbers, otherwise alphabetically
72
- const aNum = parseInt(a, 10);
73
- const bNum = parseInt(b, 10);
74
- if (!isNaN(aNum) && !isNaN(bNum)) {
75
- return aNum - bNum;
76
- }
77
- return a.localeCompare(b);
78
- });
391
+ // Filter out non-showcase files like 'translations'
392
+ .filter(name => name !== 'translations');
79
393
 
80
394
  return showcases;
81
395
  } catch (error) {
@@ -124,7 +438,6 @@ async function handleGetTile(
124
438
  });
125
439
 
126
440
  const allShowcaseData = await Promise.all(showcaseDataPromises);
127
-
128
441
  for (const { sectionName, showcaseId, content, design } of allShowcaseData) {
129
442
  responseData = updateTilesSection(responseData, sectionName, showcaseId, content, design);
130
443
  }
@@ -159,7 +472,14 @@ async function handleUpdateTile(
159
472
  ): Promise<void> {
160
473
  const requestBody = await readRequestBody(req);
161
474
  try {
162
- // Read request body containing tiles array
475
+ // If URL matches tile/custom-* pattern, return request body as-is
476
+ if (originalUrl && /\/tile\/custom-[a-zA-Z0-9]+/.test(originalUrl)) {
477
+ res.statusCode = 200;
478
+ res.setHeader('Content-Type', 'application/json');
479
+ setNoCacheHeaders(res);
480
+ res.end(JSON.stringify(requestBody, null, 2));
481
+ return;
482
+ }
163
483
 
164
484
  if (!requestBody || !Array.isArray(requestBody.tiles)) {
165
485
  res.statusCode = 400;
@@ -583,6 +903,12 @@ export function handleApiRequest(
583
903
  return;
584
904
  }
585
905
 
906
+ if (url.startsWith('/api/v1/block-config-full')) {
907
+ const typeParam = getQueryParam(url, 'type') || 'SECTION';
908
+ handleGetBlockConfigFull(req, res, sectionName, typeParam);
909
+ return;
910
+ }
911
+
586
912
  if (url.startsWith('/api/v1/client-js')) {
587
913
  handleGetClientJs(req, res, sectionName);
588
914
  return;
@@ -4,6 +4,7 @@ import { loadModule, renderServerModule } from './utils';
4
4
 
5
5
  let distFolderPath: string | null = null;
6
6
  let currentApp: any = null;
7
+ let currentSectionName: string | null = null;
7
8
 
8
9
  export function setDistFolderPath(path: string): void {
9
10
  distFolderPath = path;
@@ -232,7 +233,7 @@ export function mergeDesign(
232
233
  export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any>): Record<string, any> {
233
234
  const parsedDesign: Record<string, any> = {};
234
235
  const parsedShowcaseDesign: Record<string, any> = {};
235
- Object.entries(design).forEach(([key, comp]) => {
236
+ Object.entries(design || {}).forEach(([key, comp]) => {
236
237
  // Skip DIVIDER elements - they're UI-only and don't have runtime data
237
238
  if (comp?.type === 'DIVIDER') {
238
239
  return;
@@ -245,7 +246,7 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
245
246
  parsedDesign[key] = comp?.defaults || {};
246
247
  });
247
248
 
248
- Object.entries(showCaseDesign).forEach(([key, comp]) => {
249
+ Object.entries(showCaseDesign || {}).forEach(([key, comp]) => {
249
250
  // Skip DIVIDER elements - they're UI-only and don't have runtime data
250
251
  if (comp?.type === 'DIVIDER') {
251
252
  return;
@@ -259,30 +260,45 @@ export function designTransformer(design: Record<string, any>, showCaseDesign: R
259
260
  });
260
261
 
261
262
  let overridenDesign = mergeDesign(parsedDesign, parsedShowcaseDesign);
262
- overridenDesign = updateHexColors(overridenDesign);
263
+
264
+ try {
265
+ overridenDesign = updateHexColors(overridenDesign);
266
+ } catch (error) {
267
+ console.error('[Preview] Failed to update hex colors:', error);
268
+ }
269
+
263
270
  replaceGlobalFont(overridenDesign, 'Roboto');
264
271
  return overridenDesign;
265
272
  }
266
273
 
267
- function processImage(component: any, sectionName: string, key: string): Record<string, any> {
268
- const assetLocation = `${distFolderPath}/sections/${sectionName}/assets/`;
269
- const set = component.defaults?.set || component.imageData?.set;
270
- const newSet = {
271
- 'cropped-webp-100x200': { url: assetLocation + set.MOBILE_WEBP_LOW_RES.url },
272
- 'cropped-webp-1000x2000': { url: assetLocation + set.MOBILE_WEBP_HI_RES.url },
273
- 'webp-200x200': { url: assetLocation + set.WEBP_LOW_RES.url },
274
- 'webp-2000x2000': { url: assetLocation + set.WEBP_HI_2X_RES.url },
275
- };
276
- return {
277
- [key]: {
278
- set: newSet,
279
- ...(component.imageData?.borderInfo && { borderInfo: component.imageData.borderInfo }),
280
- ...(component.imageData && { bucket: {} }),
281
- },
282
- };
274
+ function processImage(component: any, sectionName: string, key: string, isApiRender: boolean = false): Record<string, any> {
275
+ try {
276
+ const assetLocation = isApiRender ?
277
+ `http://${distFolderPath}/sections/${sectionName}/assets/` :
278
+ `${distFolderPath}/sections/${sectionName}/assets/`;
279
+
280
+ const set = component.defaults?.imageData?.set || component.imageData?.set;
281
+ const newSet = {
282
+ 'cropped-webp-100x200': { url: assetLocation + set?.MOBILE_WEBP_LOW_RES.url },
283
+ 'cropped-webp-1000x2000': { url: assetLocation + set?.MOBILE_WEBP_HI_RES.url },
284
+ 'webp-200x200': { url: assetLocation + set?.WEBP_LOW_RES.url },
285
+ 'webp-2000x2000': { url: assetLocation + set?.WEBP_HI_2X_RES.url },
286
+ };
287
+ return {
288
+ [key]: {
289
+ set: newSet,
290
+ ...(component.imageData?.borderInfo && { borderInfo: component.imageData.borderInfo }),
291
+ ...(component.imageData && { bucket: {} }),
292
+ },
293
+ };
294
+ } catch (error) {
295
+ console.error(`[Preview] Failed to process image "${key}":`, error);
296
+ return {};
297
+ }
283
298
  }
284
299
 
285
- function processComponent(key: string, component: any, translations: any, sectionName?: string): any {
300
+ export function processComponent(key: string, component: any, translations: any, sectionName?: string,
301
+ isApiRender: boolean = false, extraParams?: Record<string, any>): any {
286
302
  if (!component?.type) return {};
287
303
  switch (component.type) {
288
304
  case 'INPUTBOX':
@@ -297,16 +313,16 @@ function processComponent(key: string, component: any, translations: any, sectio
297
313
  if (defaultSettings) {
298
314
  const result: Record<string, any> = {};
299
315
  Object.entries(defaultSettings).forEach(([k, c]) => {
300
- Object.assign(result, processComponent(k, c, translations));
316
+ Object.assign(result, processComponent(k, c, translations, sectionName, isApiRender));
301
317
  });
302
318
  return { [key]: result };
303
319
  } else {
304
- const cards = component.cards.map((card: any) => {
320
+ const cards = component.cards.map((card: any, index: number) => {
305
321
  const cardContent: Record<string, any> = {};
306
322
  Object.entries(card.settings).forEach(([k, c]) => {
307
- Object.assign(cardContent, processComponent(k, c, translations, sectionName));
323
+ Object.assign(cardContent, processComponent(k, c, translations, sectionName, isApiRender));
308
324
  });
309
- return { settings: cardContent };
325
+ return { settings: cardContent, id: index.toString(), title: extraParams?.deckCardLabel};
310
326
  });
311
327
  return { [key]: { cards } };
312
328
  }
@@ -331,7 +347,7 @@ function processComponent(key: string, component: any, translations: any, sectio
331
347
  case 'SELECTBOX':
332
348
  return { [key]: component.defaults?.value ?? component.value };
333
349
  case 'IMAGE':
334
- return processImage(component, sectionName!, key);
350
+ return processImage(component, sectionName!, key, isApiRender);
335
351
  case 'INFO':
336
352
  return {
337
353
  [key]: {
@@ -343,6 +359,11 @@ function processComponent(key: string, component: any, translations: any, sectio
343
359
  },
344
360
  },
345
361
  };
362
+ // Leaving PRODUCT_SELECTOR and CATEGORY_SELECTOR references for future implementation
363
+ case 'PRODUCT_SELECTOR':
364
+ return {};
365
+ case 'CATEGORY_SELECTOR':
366
+ return {};
346
367
  default:
347
368
  return {};
348
369
  }
@@ -354,13 +375,19 @@ function getContentToRender(
354
375
  contentTranslations: any,
355
376
  showcaseTranslations: any,
356
377
  sectionName: string,
378
+ isApiRender: boolean = false,
357
379
  ): any {
358
- const parsedContent = Object.entries(content).reduce((acc, [k, c]) => {
359
- return { ...acc, ...processComponent(k, c, contentTranslations, sectionName) };
380
+ let deckCardLabel: string | undefined = undefined;
381
+ const parsedContent = Object.entries(content || {}).reduce((acc, [k, c]) => {
382
+ if ((c as any).type === 'DECK') {
383
+ deckCardLabel = contentTranslations[(c as any).cards?.defaultCardContent?.label];
384
+ }
385
+ return { ...acc, ...processComponent(k, c, contentTranslations, sectionName, isApiRender) };
360
386
  }, {});
361
387
 
362
- const parsedShowcase = Object.entries(showcase).reduce((acc, [k, c]) => {
363
- return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName) };
388
+
389
+ const parsedShowcase = Object.entries(showcase || {}).reduce((acc, [k, c]) => {
390
+ return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName, isApiRender, {deckCardLabel}) };
364
391
  }, {});
365
392
 
366
393
  return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
@@ -378,9 +405,22 @@ export function dropdownOptions(showcaseModules: Record<string, any>): Array<{ v
378
405
  }
379
406
 
380
407
  export function loadSectionCss(sectionName: string): void {
408
+ const cssHref = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
409
+
410
+ // Check if CSS for this section is already loaded
411
+ const existingLink = document.querySelector(`link[href="${cssHref}"]`);
412
+ if (existingLink) {
413
+ return; // CSS already loaded
414
+ }
415
+
416
+ // Remove CSS from other sections (cleanup)
417
+ const oldLinks = document.querySelectorAll('link[data-section-css]');
418
+ oldLinks.forEach(link => link.remove());
419
+
381
420
  const link = document.createElement('link');
382
421
  link.rel = 'stylesheet';
383
422
  link.href = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
423
+ link.setAttribute('data-section-css', sectionName);
384
424
  document.head.appendChild(link);
385
425
  }
386
426
 
@@ -426,12 +466,27 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
426
466
  },
427
467
  };
428
468
 
469
+ // Check if we're switching to a different section
470
+ const isSameSection = currentSectionName === sectionName;
471
+
429
472
  // Mount or update app
430
- if (currentApp) {
473
+ if (currentApp && isSameSection) {
474
+ // Same section, just update with new showcase data
431
475
  currentApp.update(state);
432
476
  } else {
477
+ // Different section or first load - unmount old app and mount new one
478
+ if (currentApp) {
479
+ currentApp.unmount();
480
+ // Clear the app container
481
+ const appContainer = document.getElementById('app');
482
+ if (appContainer) {
483
+ appContainer.innerHTML = '';
484
+ }
485
+ }
486
+
433
487
  const { mount, update, unmount } = (client as any).default.init();
434
488
  currentApp = { update, unmount };
489
+ currentSectionName = sectionName;
435
490
  mount('#app', state);
436
491
  }
437
492
  }
@@ -476,6 +531,7 @@ export async function getShowcaseData(sectionName: string, showcaseId: string, d
476
531
  (contentTranslations as any).default.en,
477
532
  (showcaseTranslations as any).default.en,
478
533
  sectionName,
534
+ true
479
535
  );
480
536
 
481
537
  return {
@@ -204,7 +204,6 @@ async function updateSectionShowcase(
204
204
  if (!sectionIds || sectionIds.length === 0) {
205
205
  return false;
206
206
  }
207
-
208
207
  // Process each section ID
209
208
  for (const sectionId of sectionIds) {
210
209
  // Find the section by ID in custom content
@@ -98,6 +98,11 @@ export default defineConfig({
98
98
 
99
99
  // Handle API routes
100
100
  if (url.startsWith('/api/v1/')) {
101
+ // Skip block-config route (but not block-config-full) - handled by CLI middleware
102
+ if (url.startsWith('/api/v1/block-config') && !url.startsWith('/api/v1/block-config-full')) {
103
+ return next();
104
+ }
105
+
101
106
  // Extract auth header from request
102
107
  const authHeader = req.headers['authorization'] || '';
103
108
 
@@ -71,7 +71,7 @@ const isCaptionVisible = computed<boolean>(() => captionDesign.visible ?? false)
71
71
  const valueStyle = computed<CSSProperties>(() => ({
72
72
  fontSize: `${valueDesign.size}px`,
73
73
  fontFamily: valueDesign.font,
74
- color: (valueDesign.color as Color).hex,
74
+ color: (valueDesign.color as Color)?.hex,
75
75
  fontStyle: valueDesign.italic ? 'italic' : 'normal',
76
76
  fontWeight: valueDesign.bold ? 'bold' : 'normal',
77
77
  }));
@@ -79,7 +79,7 @@ const valueStyle = computed<CSSProperties>(() => ({
79
79
  const captionStyle = computed<CSSProperties>(() => ({
80
80
  fontSize: `${captionDesign.size}px`,
81
81
  fontFamily: captionDesign.font,
82
- color: (captionDesign.color as Color).hex,
82
+ color: (captionDesign.color as Color)?.hex,
83
83
  fontStyle: captionDesign.italic ? 'italic' : 'normal',
84
84
  fontWeight: captionDesign.bold ? 'bold' : 'normal',
85
85
  }));