@lightspeed/crane 1.3.3 → 1.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightspeed/crane",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "bin": "bin/crane.js",
6
6
  "main": "./dist/app.mjs",
@@ -0,0 +1,175 @@
1
+
2
+ export type ExternalContentMock = ExternalContentData
3
+
4
+ /**
5
+ * Default mock configuration
6
+ * Users can modify this in the compiled mock.js file
7
+ */
8
+ export const externalContentMock: ExternalContentMock = {
9
+ site: {
10
+ isPreviewMode: true,
11
+ account: {
12
+ title: 'My Account',
13
+ url: '/account',
14
+ target: '_self'
15
+ },
16
+ cart: {
17
+ url: '/cart',
18
+ count: 3
19
+ },
20
+ languages: [
21
+ {
22
+ code: 'en',
23
+ description: 'English',
24
+ main: true,
25
+ selected: true,
26
+ url: '/en'
27
+ },
28
+ {
29
+ code: 'es',
30
+ description: 'Español',
31
+ main: false,
32
+ selected: false,
33
+ url: '/es'
34
+ }
35
+ ],
36
+ legalPages: [
37
+ {
38
+ title: 'Privacy Policy',
39
+ url: '/privacy'
40
+ },
41
+ {
42
+ title: 'Terms of Service',
43
+ url: '/terms'
44
+ },
45
+ {
46
+ title: 'Refund Policy',
47
+ url: '/refunds'
48
+ }
49
+ ],
50
+ reportAbuse: {
51
+ title: 'Report Abuse',
52
+ url: '/report-abuse',
53
+ target: '_blank'
54
+ },
55
+ madeWith: {
56
+ url: 'https://www.lightspeedhq.com',
57
+ target: '_blank',
58
+ icon: '/assets/lightspeed-icon.png',
59
+ poweredBy: 'Powered by',
60
+ company: 'Lightspeed'
61
+ }
62
+ },
63
+
64
+ category: {
65
+ categories: [
66
+ {
67
+ id: 1,
68
+ name: 'Electronics',
69
+ url: '/categories/electronics',
70
+ imageUrl: '/assets/electronics-category.jpg',
71
+ thumbnailImageUrl: '/assets/electronics-thumb.jpg',
72
+ alt: 'Electronics category',
73
+ imageBorderInfo: {
74
+ homogeneity: true,
75
+ color: {
76
+ r: 255,
77
+ g: 255,
78
+ b: 255,
79
+ a: 1
80
+ }
81
+ }
82
+ },
83
+ {
84
+ id: 2,
85
+ name: 'Clothing',
86
+ url: '/categories/clothing',
87
+ imageUrl: '/assets/clothing-category.jpg',
88
+ thumbnailImageUrl: '/assets/clothing-thumb.jpg',
89
+ alt: 'Clothing category'
90
+ },
91
+ {
92
+ id: 3,
93
+ name: 'Home & Garden',
94
+ url: '/categories/home-garden',
95
+ imageUrl: '/assets/home-garden-category.jpg',
96
+ thumbnailImageUrl: '/assets/home-garden-thumb.jpg',
97
+ alt: 'Home & Garden category'
98
+ }
99
+ ],
100
+ categoryTree: [
101
+ {
102
+ id: 1,
103
+ name: 'Electronics',
104
+ nameTranslated: {
105
+ en: 'Electronics',
106
+ es: 'Electrónicos'
107
+ },
108
+ urlPath: '/electronics',
109
+ enabled: true,
110
+ children: [
111
+ {
112
+ id: 11,
113
+ name: 'Smartphones',
114
+ nameTranslated: {
115
+ en: 'Smartphones',
116
+ es: 'Teléfonos inteligentes'
117
+ },
118
+ urlPath: '/electronics/smartphones',
119
+ enabled: true,
120
+ children: []
121
+ },
122
+ {
123
+ id: 12,
124
+ name: 'Laptops',
125
+ nameTranslated: {
126
+ en: 'Laptops',
127
+ es: 'Portátiles'
128
+ },
129
+ urlPath: '/electronics/laptops',
130
+ enabled: true,
131
+ children: []
132
+ }
133
+ ]
134
+ },
135
+ {
136
+ id: 2,
137
+ name: 'Clothing',
138
+ nameTranslated: {
139
+ en: 'Clothing',
140
+ es: 'Ropa'
141
+ },
142
+ urlPath: '/clothing',
143
+ enabled: true,
144
+ children: [
145
+ {
146
+ id: 21,
147
+ name: 'Men\'s Clothing',
148
+ nameTranslated: {
149
+ en: 'Men\'s Clothing',
150
+ es: 'Ropa para hombres'
151
+ },
152
+ urlPath: '/clothing/mens',
153
+ enabled: true,
154
+ children: []
155
+ },
156
+ {
157
+ id: 22,
158
+ name: 'Women\'s Clothing',
159
+ nameTranslated: {
160
+ en: 'Women\'s Clothing',
161
+ es: 'Ropa para mujeres'
162
+ },
163
+ urlPath: '/clothing/womens',
164
+ enabled: true,
165
+ children: []
166
+ }
167
+ ]
168
+ }
169
+ ]
170
+ }
171
+ };
172
+
173
+ export function getExternalContentMock(): ExternalContentMock {
174
+ return externalContentMock;
175
+ }
@@ -1,8 +1,10 @@
1
1
  /* eslint-disable no-console */
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  import { loadModule} from "./utils.ts";
4
+ import { getExternalContentMock } from "./mock";
4
5
 
5
6
  let distFolderPath: string | null = null;
7
+ let currentApp: any = null;
6
8
 
7
9
  export function setDistFolderPath(path: string): void {
8
10
  distFolderPath = path;
@@ -28,13 +30,15 @@ interface ColorObject {
28
30
  }
29
31
 
30
32
  function hexToColorObject(hex: string): ColorObject {
31
- const match = /^#?([0-9a-fA-F]{6})$/.exec(hex);
33
+ // Support both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) hex colors
34
+ const match = /^#?([0-9a-fA-F]{6}([0-9a-fA-F]{2})?)$/.exec(hex);
32
35
  if (!match) throw new Error("Invalid hex color format");
33
36
  const cleanHex = match[1].toLowerCase();
34
37
  const r = parseInt(cleanHex.substring(0, 2), 16);
35
38
  const g = parseInt(cleanHex.substring(2, 4), 16);
36
39
  const b = parseInt(cleanHex.substring(4, 6), 16);
37
- const a = 255;
40
+ // Extract alpha if present (8-digit hex), otherwise default to 255 (fully opaque)
41
+ const a = cleanHex.length === 8 ? parseInt(cleanHex.substring(6, 8), 16) : 255;
38
42
  const rNorm = r / 255, gNorm = g / 255, bNorm = b / 255;
39
43
  const max = Math.max(rNorm, gNorm, bNorm), min = Math.min(rNorm, gNorm, bNorm);
40
44
  const delta = max - min;
@@ -50,15 +54,15 @@ function hexToColorObject(hex: string): ColorObject {
50
54
  if (h < 0) h += 360;
51
55
  }
52
56
  return {
53
- hex: `#${cleanHex}${a.toString(16).padStart(2, '0')}`,
57
+ hex: `#${cleanHex.substring(0, 6)}${a.toString(16).padStart(2, '0')}`,
54
58
  hsl: { h: Math.round(h), s: +(s * 100).toFixed(1), l: +(l * 100).toFixed(1) },
55
- rgba: { r, g, b, a: 1 },
59
+ rgba: { r, g, b, a: +(a / 255).toFixed(2) },
56
60
  };
57
61
  }
58
62
 
59
63
  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})$/;
64
+ // Matches 3-digit (#RGB), 6-digit (#RRGGBB), or 8-digit (#RRGGBBAA) hex
65
+ const hexRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
62
66
 
63
67
  function recurse(value: any): any {
64
68
  if (Array.isArray(value)) return value.map(recurse);
@@ -92,6 +96,77 @@ const replaceGlobalFont = (obj: any, font: string): void => {
92
96
  });
93
97
  };
94
98
 
99
+ function createBackgroundStructure(type: 'solid' | 'gradient', config: any): any {
100
+ return {
101
+ background: {
102
+ background: {
103
+ type,
104
+ ...config,
105
+ },
106
+ },
107
+ };
108
+ }
109
+
110
+ // Helper function to create default gray background
111
+ function createDefaultBackground(): any {
112
+ 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
+
124
+ function createBackgroundDesign(showcaseBackground: any): any {
125
+ // No background or invalid background
126
+ if (!showcaseBackground) {
127
+ return createDefaultBackground();
128
+ }
129
+
130
+ // Handle gradient backgrounds
131
+ if (showcaseBackground.style === 'GRADIENT' && Array.isArray(showcaseBackground.color)) {
132
+ const [fromColor, toColor] = showcaseBackground.color;
133
+
134
+ // If either color is global, use default
135
+ if (isGlobalColor(fromColor) || isGlobalColor(toColor)) {
136
+ return createDefaultBackground();
137
+ }
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
+ }
149
+
150
+ // 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
+ });
164
+ }
165
+
166
+ // Fallback to default
167
+ return createDefaultBackground();
168
+ }
169
+
95
170
  function overrideSettingsFromShowcase(content: any, showcases: any): any {
96
171
  return Object.fromEntries(Object.entries(content).map(([k, v]) => [k, showcases[k] ?? v]));
97
172
  }
@@ -256,28 +331,14 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
256
331
  const showcase = await loadModule(`${distFolderPath}/sections/${sectionName}/js/showcases/${showcaseId}.mjs`);
257
332
  const design = await loadModule(`${distFolderPath}/sections/${sectionName}/js/settings/design.mjs`);
258
333
 
259
- const { mount } = client.default.init();
334
+ // Get showcase background and create background design
335
+ const showcaseBackground = showcase.default?.design?.background;
336
+ const backgroundDesign = createBackgroundDesign(showcaseBackground);
260
337
 
261
338
  const ovveridenDesign = designTransformer(design.default, showcase.default.design || {});
262
339
 
263
340
  loadSectionCss(sectionName);
264
341
 
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
342
  const overriddenContent = getContentToRender(
282
343
  content.default,
283
344
  showcase.default.content || {},
@@ -286,15 +347,32 @@ export async function renderShowcase(sectionName: string, showcaseId: string): P
286
347
  sectionName
287
348
  );
288
349
 
289
- mount('#app', {
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;
356
+
357
+ const state = {
290
358
  context: {
291
359
  globalDesign: { color: 'global.color.background' },
292
360
  },
293
361
  data: {
294
362
  content: overriddenContent,
295
- design: { ...ovveridenDesign, ...backgroundDesign },
363
+ design: finalDesign,
296
364
  defaults: {},
297
365
  background: {},
366
+ externalContent: externalContentMock,
298
367
  },
299
- });
368
+ };
369
+
370
+ // If app is already mounted, update it; otherwise mount it
371
+ if (currentApp) {
372
+ currentApp.update(state);
373
+ } else {
374
+ const { mount, update, unmount } = client.default.init();
375
+ currentApp = { update, unmount };
376
+ mount('#app', state);
377
+ }
300
378
  }