@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +6 -0
  3. package/dist/app.d.mts +14 -1
  4. package/dist/app.d.ts +14 -1
  5. package/dist/app.mjs +1 -1
  6. package/dist/cli.mjs +8 -6
  7. package/package.json +12 -12
  8. package/template/layouts/catalog/example-catalog/Main.vue +1 -1
  9. package/template/layouts/catalog/example-catalog/assets/account-icon.svg +11 -0
  10. package/template/layouts/catalog/example-catalog/components/Icon.vue +52 -0
  11. package/template/layouts/catalog/example-catalog/settings/content.ts +1 -0
  12. package/template/layouts/catalog/example-catalog/settings/design.ts +1 -0
  13. package/template/layouts/catalog/example-catalog/settings/translations.ts +1 -0
  14. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/CustomBottomBar.vue +69 -0
  15. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/client.ts +5 -0
  16. package/template/layouts/catalog/example-catalog/slots/custom-bottom-bar/server.ts +5 -0
  17. package/template/layouts/catalog/example-catalog/type.ts +5 -0
  18. package/template/layouts/category/example-category/settings/content.ts +1 -0
  19. package/template/layouts/category/example-category/settings/design.ts +1 -0
  20. package/template/layouts/category/example-category/type.ts +5 -0
  21. package/template/layouts/product/example-product/settings/content.ts +1 -0
  22. package/template/layouts/product/example-product/settings/design.ts +1 -0
  23. package/template/layouts/product/example-product/type.ts +5 -0
  24. package/template/page-templates/example-template/pages/catalog.ts +5 -4
  25. package/template/page-templates/example-template/pages/category.ts +5 -4
  26. package/template/page-templates/example-template/pages/product.ts +5 -4
  27. package/template/preview/sections/preview.html +89 -0
  28. package/template/preview/shared/preview.ts +300 -0
  29. package/template/preview/shared/utils.ts +3 -0
  30. 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,3 @@
1
+ export async function loadModule(path: string): Promise<any> {
2
+ return import(path);
3
+ }
@@ -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
+ });