@lightspeed/crane 1.2.4 β 1.2.6
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 +17 -0
- package/README.md +6 -0
- package/dist/app.d.mts +14 -1
- package/dist/app.d.ts +14 -1
- package/dist/app.mjs +1 -1
- package/dist/cli.mjs +8 -6
- package/package.json +12 -12
- package/template/layouts/catalog/example-catalog/Main.vue +1 -1
- package/template/layouts/catalog/example-catalog/assets/account-icon.svg +11 -0
- package/template/layouts/catalog/example-catalog/components/Icon.vue +52 -0
- package/template/layouts/catalog/example-catalog/settings/content.ts +1 -0
- package/template/layouts/catalog/example-catalog/settings/design.ts +1 -0
- package/template/layouts/catalog/example-catalog/settings/translations.ts +1 -0
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/CustomBottomBar.vue +69 -0
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +5 -0
- package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +5 -0
- package/template/layouts/catalog/example-catalog/type.ts +5 -0
- package/template/layouts/category/example-category/settings/content.ts +1 -0
- package/template/layouts/category/example-category/settings/design.ts +1 -0
- package/template/layouts/category/example-category/type.ts +5 -0
- package/template/layouts/product/example-product/settings/content.ts +1 -0
- package/template/layouts/product/example-product/settings/design.ts +1 -0
- package/template/layouts/product/example-product/type.ts +5 -0
- package/template/page-templates/example-template/pages/catalog.ts +5 -4
- package/template/page-templates/example-template/pages/category.ts +5 -4
- package/template/page-templates/example-template/pages/product.ts +5 -4
- package/template/preview/sections/preview.html +89 -0
- package/template/preview/shared/preview.ts +300 -0
- package/template/preview/shared/utils.ts +3 -0
- package/template/preview/vite.config.js +32 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
import { loadModule} from "./utils.ts";
|
|
4
|
+
|
|
5
|
+
let distFolderPath: string | null = null;
|
|
6
|
+
|
|
7
|
+
export function setDistFolderPath(path: string): void {
|
|
8
|
+
distFolderPath = path;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface HSLColor {
|
|
12
|
+
h: number;
|
|
13
|
+
s: number;
|
|
14
|
+
l: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RGBAColor {
|
|
18
|
+
r: number;
|
|
19
|
+
g: number;
|
|
20
|
+
b: number;
|
|
21
|
+
a: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ColorObject {
|
|
25
|
+
hex: string;
|
|
26
|
+
hsl: HSLColor;
|
|
27
|
+
rgba: RGBAColor;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hexToColorObject(hex: string): ColorObject {
|
|
31
|
+
const match = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
|
32
|
+
if (!match) throw new Error("Invalid hex color format");
|
|
33
|
+
const cleanHex = match[1].toLowerCase();
|
|
34
|
+
const r = parseInt(cleanHex.substring(0, 2), 16);
|
|
35
|
+
const g = parseInt(cleanHex.substring(2, 4), 16);
|
|
36
|
+
const b = parseInt(cleanHex.substring(4, 6), 16);
|
|
37
|
+
const a = 255;
|
|
38
|
+
const rNorm = r / 255, gNorm = g / 255, bNorm = b / 255;
|
|
39
|
+
const max = Math.max(rNorm, gNorm, bNorm), min = Math.min(rNorm, gNorm, bNorm);
|
|
40
|
+
const delta = max - min;
|
|
41
|
+
let h = 0, s = 0, l = (max + min) / 2;
|
|
42
|
+
if (delta !== 0) {
|
|
43
|
+
s = delta / (1 - Math.abs(2 * l - 1));
|
|
44
|
+
switch (max) {
|
|
45
|
+
case rNorm: h = ((gNorm - bNorm) / delta) % 6; break;
|
|
46
|
+
case gNorm: h = (bNorm - rNorm) / delta + 2; break;
|
|
47
|
+
case bNorm: h = (rNorm - gNorm) / delta + 4; break;
|
|
48
|
+
}
|
|
49
|
+
h *= 60;
|
|
50
|
+
if (h < 0) h += 360;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
hex: `#${cleanHex}${a.toString(16).padStart(2, '0')}`,
|
|
54
|
+
hsl: { h: Math.round(h), s: +(s * 100).toFixed(1), l: +(l * 100).toFixed(1) },
|
|
55
|
+
rgba: { r, g, b, a: 1 },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function updateHexColors(obj: any): any {
|
|
60
|
+
// Matches either 3-digit (#RGB) or 6-digit (#RRGGBB) hex
|
|
61
|
+
const hexRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
62
|
+
|
|
63
|
+
function recurse(value: any): any {
|
|
64
|
+
if (Array.isArray(value)) return value.map(recurse);
|
|
65
|
+
if (value && typeof value === 'object') {
|
|
66
|
+
for (const key in value) {
|
|
67
|
+
const v = value[key];
|
|
68
|
+
if (key === 'color' && typeof v === 'string' && hexRegex.test(v)) {
|
|
69
|
+
// If itβs a 3-digit hex, expand it to 6 digits
|
|
70
|
+
const expanded = v.replace(
|
|
71
|
+
/^#?([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/,
|
|
72
|
+
(_m, r, g, b) => `#${r}${r}${g}${g}${b}${b}`
|
|
73
|
+
);
|
|
74
|
+
value[key] = hexToColorObject(expanded);
|
|
75
|
+
} else {
|
|
76
|
+
value[key] = recurse(v);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
return recurse(obj);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const replaceGlobalFont = (obj: any, font: string): void => {
|
|
86
|
+
Object.entries(obj).forEach(([k, v]) => {
|
|
87
|
+
if (v && typeof v === 'object') {
|
|
88
|
+
replaceGlobalFont(v, font);
|
|
89
|
+
} else if (k === 'font' && v === 'global.fontFamily.body') {
|
|
90
|
+
obj[k] = font;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function overrideSettingsFromShowcase(content: any, showcases: any): any {
|
|
96
|
+
return Object.fromEntries(Object.entries(content).map(([k, v]) => [k, showcases[k] ?? v]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function mergeDesign(
|
|
100
|
+
design: Record<string, any>,
|
|
101
|
+
showcase: Record<string, any>
|
|
102
|
+
): Record<string, any> {
|
|
103
|
+
const result: Record<string, any> = { ...design };
|
|
104
|
+
|
|
105
|
+
Object.keys(design).forEach((key) => {
|
|
106
|
+
if (key in showcase) {
|
|
107
|
+
const base = design[key];
|
|
108
|
+
const override = showcase[key];
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
base !== null && override !== null &&
|
|
112
|
+
typeof base === 'object' && typeof override === 'object' &&
|
|
113
|
+
!Array.isArray(base) && !Array.isArray(override)
|
|
114
|
+
) {
|
|
115
|
+
// shallow merge nested objects
|
|
116
|
+
result[key] = { ...base, ...override };
|
|
117
|
+
} else {
|
|
118
|
+
// replace with showcase value (covers primitives & arrays)
|
|
119
|
+
result[key] = override;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function designTransformer(design: Record<string, any>, showCaseDesign: Record<string, any> ): Record<string, any> {
|
|
128
|
+
const parsedDesign: Record<string, any> = {};
|
|
129
|
+
const parsedShowcaseDesign: Record<string, any> = {};
|
|
130
|
+
Object.entries(design).forEach(([key, comp]) => {
|
|
131
|
+
parsedDesign[key] = comp?.defaults;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
Object.entries(showCaseDesign).forEach(([key, comp]) => {
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
136
|
+
const { type, ...withoutType } = comp;
|
|
137
|
+
parsedShowcaseDesign[key] = type == 'TEXT' ? { ... withoutType, visible: true} : withoutType
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
let overridenDesign = mergeDesign(parsedDesign, parsedShowcaseDesign)
|
|
141
|
+
overridenDesign = updateHexColors(overridenDesign);
|
|
142
|
+
replaceGlobalFont(overridenDesign, 'Roboto');
|
|
143
|
+
return overridenDesign;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function processImage(component: any, sectionName: string, key: string): Record<string, any> {
|
|
147
|
+
const assetLocation = `${distFolderPath}/sections/${sectionName}/assets/`;
|
|
148
|
+
const set = component.defaults?.set || component.imageData?.set;
|
|
149
|
+
const newSet = {
|
|
150
|
+
'cropped-webp-100x200': { url: assetLocation + set.MOBILE_WEBP_LOW_RES.url },
|
|
151
|
+
'cropped-webp-1000x2000': { url: assetLocation + set.MOBILE_WEBP_HI_RES.url },
|
|
152
|
+
'webp-200x200': { url: assetLocation + set.WEBP_LOW_RES.url },
|
|
153
|
+
'webp-2000x2000': { url: assetLocation + set.WEBP_HI_2X_RES.url },
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
[key]: {
|
|
157
|
+
set: newSet,
|
|
158
|
+
...(component.imageData?.borderInfo && { borderInfo: component.imageData.borderInfo }),
|
|
159
|
+
...(component.imageData && { bucket: {} })
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function processComponent(key: string, component: any, translations: any, sectionName?: string): any {
|
|
165
|
+
if (!component?.type) return '';
|
|
166
|
+
switch (component.type) {
|
|
167
|
+
case 'INPUTBOX':
|
|
168
|
+
case 'TEXTAREA': {
|
|
169
|
+
const text = component?.defaults?.text ?? component?.text ?? component?.placeholder;
|
|
170
|
+
return { [key]: translations[text] };
|
|
171
|
+
}
|
|
172
|
+
case 'DIVIDER':
|
|
173
|
+
return { [key]: translations[component.label] };
|
|
174
|
+
case 'DECK': {
|
|
175
|
+
const defaultSettings = component.cards?.defaultCardContent?.settings;
|
|
176
|
+
if (defaultSettings) {
|
|
177
|
+
const result: Record<string, any> = {};
|
|
178
|
+
Object.entries(defaultSettings).forEach(([k, c]) => {
|
|
179
|
+
Object.assign(result, processComponent(k, c, translations));
|
|
180
|
+
});
|
|
181
|
+
return { [key]: result };
|
|
182
|
+
} else {
|
|
183
|
+
const cards = component.cards.map((card: any) => {
|
|
184
|
+
const cardContent: Record<string, any> = {};
|
|
185
|
+
Object.entries(card.settings).forEach(([k, c]) => {
|
|
186
|
+
Object.assign(cardContent, processComponent(k, c, translations, sectionName));
|
|
187
|
+
});
|
|
188
|
+
return { settings: cardContent };
|
|
189
|
+
});
|
|
190
|
+
return { [key]: { cards } };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
case 'BUTTON': {
|
|
194
|
+
const button = component.defaults || component;
|
|
195
|
+
return {
|
|
196
|
+
[key]: {
|
|
197
|
+
title: translations[button.title],
|
|
198
|
+
type: button.buttonType,
|
|
199
|
+
link: button.link,
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
case 'TOGGLE':
|
|
204
|
+
return { [key]: component.defaults };
|
|
205
|
+
case 'SELECTBOX':
|
|
206
|
+
return { [key]: component.defaults.value };
|
|
207
|
+
case 'IMAGE':
|
|
208
|
+
return processImage(component, sectionName!, key);
|
|
209
|
+
default:
|
|
210
|
+
console.warn(`Unknown type: ${component.type}`);
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getContentToRender(
|
|
216
|
+
content: any,
|
|
217
|
+
showcase: any,
|
|
218
|
+
contentTranslations: any,
|
|
219
|
+
showcaseTranslations: any,
|
|
220
|
+
sectionName: string
|
|
221
|
+
): any {
|
|
222
|
+
const parsedContent = Object.entries(content).reduce((acc, [k, c]) => {
|
|
223
|
+
return { ...acc, ...processComponent(k, c, contentTranslations, sectionName) };
|
|
224
|
+
}, {});
|
|
225
|
+
|
|
226
|
+
const parsedShowcase = Object.entries(showcase).reduce((acc, [k, c]) => {
|
|
227
|
+
return { ...acc, ...processComponent(k, c, showcaseTranslations, sectionName) };
|
|
228
|
+
}, {});
|
|
229
|
+
|
|
230
|
+
return overrideSettingsFromShowcase(parsedContent, parsedShowcase);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function dropdownOptions(showcaseModules: Record<string, any>): Array<{ value: string; label: string }> {
|
|
234
|
+
return Object.keys(showcaseModules).map(path => {
|
|
235
|
+
const match = path.match(/\/sections\/([^/]+)\/js\/showcases\/(\d+)\.mjs$/);
|
|
236
|
+
if (!match) return null;
|
|
237
|
+
return {
|
|
238
|
+
value: `${match[1]}:${match[2]}`,
|
|
239
|
+
label: `${match[1]}: showcase ${match[2]}`,
|
|
240
|
+
};
|
|
241
|
+
}).filter(Boolean) as Array<{ value: string; label: string }>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function loadSectionCss(sectionName: string): void {
|
|
245
|
+
const link = document.createElement('link');
|
|
246
|
+
link.rel = 'stylesheet';
|
|
247
|
+
link.href = `${distFolderPath}/sections/${sectionName}/js/main/client/assets/client.css`;
|
|
248
|
+
document.head.appendChild(link);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function renderShowcase(sectionName: string, showcaseId: string): Promise<void> {
|
|
252
|
+
const client = await loadModule(`${distFolderPath}/sections/${sectionName}/js/main/client/client.js`);
|
|
253
|
+
const content = await loadModule(`${distFolderPath}/sections/${sectionName}/js/settings/content.mjs`);
|
|
254
|
+
const contentTranslations = await loadModule(`${distFolderPath}/sections/${sectionName}/js/settings/translations.mjs`);
|
|
255
|
+
const showcaseTranslations = await loadModule(`${distFolderPath}/sections/${sectionName}/js/showcases/translations.mjs`);
|
|
256
|
+
const showcase = await loadModule(`${distFolderPath}/sections/${sectionName}/js/showcases/${showcaseId}.mjs`);
|
|
257
|
+
const design = await loadModule(`${distFolderPath}/sections/${sectionName}/js/settings/design.mjs`);
|
|
258
|
+
|
|
259
|
+
const { mount } = client.default.init();
|
|
260
|
+
|
|
261
|
+
const ovveridenDesign = designTransformer(design.default, showcase.default.design || {});
|
|
262
|
+
|
|
263
|
+
loadSectionCss(sectionName);
|
|
264
|
+
|
|
265
|
+
const backgroundDesign = {
|
|
266
|
+
background: {
|
|
267
|
+
background: {
|
|
268
|
+
type: 'solid',
|
|
269
|
+
solid: {
|
|
270
|
+
color: {
|
|
271
|
+
raw: '#F9F9F9',
|
|
272
|
+
hex: '#F9F9F9',
|
|
273
|
+
rgba: { r: 19, g: 19, b: 19, a: 1.0 },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
color: 'global.color.background',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const overriddenContent = getContentToRender(
|
|
282
|
+
content.default,
|
|
283
|
+
showcase.default.content || {},
|
|
284
|
+
contentTranslations.default.en,
|
|
285
|
+
showcaseTranslations.default.en,
|
|
286
|
+
sectionName
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
mount('#app', {
|
|
290
|
+
context: {
|
|
291
|
+
globalDesign: { color: 'global.color.background' },
|
|
292
|
+
},
|
|
293
|
+
data: {
|
|
294
|
+
content: overriddenContent,
|
|
295
|
+
design: { ...ovveridenDesign, ...backgroundDesign },
|
|
296
|
+
defaults: {},
|
|
297
|
+
background: {},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
root: '.',
|
|
5
|
+
server: {
|
|
6
|
+
fs: { strict: false },
|
|
7
|
+
},
|
|
8
|
+
plugins: [
|
|
9
|
+
{
|
|
10
|
+
name: 'print-preview-url',
|
|
11
|
+
configureServer(server) {
|
|
12
|
+
server.httpServer?.once('listening', () => {
|
|
13
|
+
const baseUrl = `http://localhost:${server.config.server.port || 5173}`;
|
|
14
|
+
console.log("Preview URL: ", baseUrl + '/preview/sections/preview.html\n');
|
|
15
|
+
console.log('π Press Ctrl+C to stop the local server π\n');
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'no-vite-cache',
|
|
21
|
+
configureServer(server) {
|
|
22
|
+
// invalidate caches so that after build when user refreshes the page, the latest .mjs files are fetched
|
|
23
|
+
server.middlewares.use((req, res, next) => {
|
|
24
|
+
if (req.url?.includes('/sections/')) {
|
|
25
|
+
server.moduleGraph.invalidateAll();
|
|
26
|
+
}
|
|
27
|
+
next();
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|