@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.
- package/CHANGELOG.md +34 -0
- package/dist/cli.mjs +37 -21
- package/package.json +3 -2
- package/template/headers/example-header/showcases/2.ts +0 -3
- package/template/page-templates/example-template/pages/custom.ts +6 -0
- package/template/preview/shared/api-routes.ts +337 -11
- package/template/preview/shared/preview.ts +86 -30
- package/template/preview/shared/utils.ts +0 -1
- package/template/preview/vite.config.js +5 -0
- package/template/reference/sections/about-us/component/Stats.vue +2 -2
- package/template/reference/sections/about-us/settings/content.ts +28 -21
- package/template/reference/sections/about-us/settings/design.ts +1 -1
- package/template/reference/sections/about-us/showcases/1.ts +1 -0
- package/template/reference/sections/about-us/showcases/2.ts +1 -0
- package/template/reference/sections/intro-slider/component/Slider.vue +1 -1
- package/template/reference/sections/intro-slider/component/Title.vue +2 -2
- package/template/reference/sections/intro-slider/settings/content.ts +22 -1
- package/template/reference/sections/intro-slider/settings/design.ts +1 -1
- package/template/reference/sections/intro-slider/showcases/1.ts +1 -0
- package/template/reference/sections/intro-slider/showcases/2.ts +1 -0
- package/template/reference/sections/tag-lines/component/HighlightedText.vue +1 -1
- package/template/reference/sections/tag-lines/component/Title.vue +1 -1
- package/template/reference/sections/tag-lines/settings/content.ts +34 -0
- package/template/reference/sections/trending-categories/component/Title.vue +1 -1
- package/template/sections/example-section/settings/content.ts +15 -13
- package/template/sections/example-section/showcases/1.ts +0 -1
- package/template/sections/example-section/showcases/2.ts +0 -1
- 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
|
+
"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.
|
|
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",
|
|
@@ -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
|
-
|
|
71
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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 {
|
|
@@ -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)
|
|
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)
|
|
82
|
+
color: (captionDesign.color as Color)?.hex,
|
|
83
83
|
fontStyle: captionDesign.italic ? 'italic' : 'normal',
|
|
84
84
|
fontWeight: captionDesign.bold ? 'bold' : 'normal',
|
|
85
85
|
}));
|