@snowcone-app/sdk 0.1.10
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 +136 -0
- package/README.md +195 -0
- package/dist/chunk-7VO4EL2V.js +12 -0
- package/dist/chunk-HOYSZQET.js +476 -0
- package/dist/chunk-IIUCW2O4.js +457 -0
- package/dist/chunk-UJFJ7REN.js +485 -0
- package/dist/dev-fetcher.cjs +36 -0
- package/dist/dev-fetcher.d.cts +3 -0
- package/dist/dev-fetcher.d.ts +3 -0
- package/dist/dev-fetcher.js +6 -0
- package/dist/index.cjs +5055 -0
- package/dist/index.d.cts +2437 -0
- package/dist/index.d.ts +2437 -0
- package/dist/index.js +4424 -0
- package/dist/react.cjs +755 -0
- package/dist/react.d.cts +96 -0
- package/dist/react.d.ts +96 -0
- package/dist/react.js +245 -0
- package/dist/websocket-B8_XAwWx.d.cts +336 -0
- package/dist/websocket-B8_XAwWx.d.ts +336 -0
- package/dist/websocket-GXMYofWp.d.cts +330 -0
- package/dist/websocket-GXMYofWp.d.ts +330 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4424 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDevFetcher
|
|
3
|
+
} from "./chunk-7VO4EL2V.js";
|
|
4
|
+
import {
|
|
5
|
+
RealtimeMockupService
|
|
6
|
+
} from "./chunk-UJFJ7REN.js";
|
|
7
|
+
|
|
8
|
+
// src/validation.ts
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var CatalogProductSchema = z.object({
|
|
11
|
+
id: z.string(),
|
|
12
|
+
name: z.string(),
|
|
13
|
+
slug: z.string(),
|
|
14
|
+
tags: z.array(z.string()).optional(),
|
|
15
|
+
price: z.number().int(),
|
|
16
|
+
lowestPrice: z.number().int().optional(),
|
|
17
|
+
highestPrice: z.number().int().optional(),
|
|
18
|
+
mockups: z.array(
|
|
19
|
+
z.object({ id: z.string(), ar: z.number(), gvids: z.array(z.string()) })
|
|
20
|
+
).optional(),
|
|
21
|
+
options: z.object({
|
|
22
|
+
selected: z.record(z.string(), z.union([z.string(), z.number(), z.null()])).optional(),
|
|
23
|
+
// Accept any attribute object shape; upstream varies between choices-only and typed objects
|
|
24
|
+
attributes: z.record(z.string(), z.any()).optional(),
|
|
25
|
+
combinations: z.array(z.any()).optional(),
|
|
26
|
+
attributesList: z.array(z.string()).optional()
|
|
27
|
+
}).optional(),
|
|
28
|
+
care_guide: z.array(z.string()).optional(),
|
|
29
|
+
description: z.array(z.string()).optional(),
|
|
30
|
+
key_features: z.array(z.string()).optional(),
|
|
31
|
+
days_to_deliver: z.union([z.string(), z.number()]).optional(),
|
|
32
|
+
size_chart_data: z.any().optional(),
|
|
33
|
+
ships_from_country: z.string().optional(),
|
|
34
|
+
variants: z.array(
|
|
35
|
+
z.object({
|
|
36
|
+
gvid: z.string(),
|
|
37
|
+
gvidForMockup: z.string().nullable().optional(),
|
|
38
|
+
placements: z.array(
|
|
39
|
+
z.object({
|
|
40
|
+
label: z.string(),
|
|
41
|
+
width: z.number().nullable(),
|
|
42
|
+
height: z.number().nullable()
|
|
43
|
+
})
|
|
44
|
+
)
|
|
45
|
+
})
|
|
46
|
+
).optional(),
|
|
47
|
+
placements: z.array(
|
|
48
|
+
z.object({
|
|
49
|
+
label: z.string(),
|
|
50
|
+
type: z.enum(["image", "color"]),
|
|
51
|
+
width: z.number().int(),
|
|
52
|
+
height: z.number().int(),
|
|
53
|
+
defaultScaleMode: z.enum(["fill", "fit"]).optional(),
|
|
54
|
+
fitMarginTop: z.number().int().optional(),
|
|
55
|
+
fitMarginRight: z.number().int().optional(),
|
|
56
|
+
fitMarginBottom: z.number().int().optional(),
|
|
57
|
+
fitMarginLeft: z.number().int().optional(),
|
|
58
|
+
fitAlign: z.string().optional(),
|
|
59
|
+
align: z.string().optional(),
|
|
60
|
+
scale: z.number().optional(),
|
|
61
|
+
offsetX: z.number().optional(),
|
|
62
|
+
offsetY: z.number().optional()
|
|
63
|
+
})
|
|
64
|
+
).optional(),
|
|
65
|
+
defaultGvid: z.string().optional()
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// src/mockup/cache.ts
|
|
69
|
+
var SignatureCache = class {
|
|
70
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
71
|
+
maxMemoryEntries;
|
|
72
|
+
maxLocalStorageEntries;
|
|
73
|
+
localStorageKey = "merchify_signature_cache";
|
|
74
|
+
localStorageAvailable;
|
|
75
|
+
constructor(maxMemoryEntries = 500, maxLocalStorageEntries = 100) {
|
|
76
|
+
this.maxMemoryEntries = maxMemoryEntries;
|
|
77
|
+
this.maxLocalStorageEntries = maxLocalStorageEntries;
|
|
78
|
+
this.localStorageAvailable = this.checkLocalStorageAvailability();
|
|
79
|
+
if (this.localStorageAvailable) {
|
|
80
|
+
this.loadFromLocalStorage();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
checkLocalStorageAvailability() {
|
|
84
|
+
try {
|
|
85
|
+
const test = "__storage_test__";
|
|
86
|
+
localStorage.setItem(test, test);
|
|
87
|
+
localStorage.removeItem(test);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
loadFromLocalStorage() {
|
|
94
|
+
try {
|
|
95
|
+
const stored = localStorage.getItem(this.localStorageKey);
|
|
96
|
+
if (stored) {
|
|
97
|
+
const parsed = JSON.parse(stored);
|
|
98
|
+
if (Array.isArray(parsed)) {
|
|
99
|
+
parsed.forEach(([key, value]) => {
|
|
100
|
+
if (this.memoryCache.size < this.maxMemoryEntries) {
|
|
101
|
+
this.memoryCache.set(key, value);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.warn("Failed to load signature cache from localStorage:", error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
saveToLocalStorage() {
|
|
111
|
+
if (!this.localStorageAvailable) return;
|
|
112
|
+
try {
|
|
113
|
+
const entries = Array.from(this.memoryCache.entries()).sort((a, b) => b[1].timestamp - a[1].timestamp).slice(0, this.maxLocalStorageEntries);
|
|
114
|
+
localStorage.setItem(this.localStorageKey, JSON.stringify(entries));
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn("Failed to save signature cache to localStorage:", error);
|
|
117
|
+
this.localStorageAvailable = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
get(key) {
|
|
121
|
+
const entry = this.memoryCache.get(key);
|
|
122
|
+
if (entry) {
|
|
123
|
+
entry.timestamp = Date.now();
|
|
124
|
+
this.memoryCache.delete(key);
|
|
125
|
+
this.memoryCache.set(key, entry);
|
|
126
|
+
return entry.url;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
set(key, url) {
|
|
131
|
+
if (this.memoryCache.size >= this.maxMemoryEntries) {
|
|
132
|
+
const oldestKey = this.findOldestKey();
|
|
133
|
+
if (oldestKey) {
|
|
134
|
+
this.memoryCache.delete(oldestKey);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this.memoryCache.set(key, {
|
|
138
|
+
url,
|
|
139
|
+
timestamp: Date.now()
|
|
140
|
+
});
|
|
141
|
+
this.saveToLocalStorage();
|
|
142
|
+
}
|
|
143
|
+
findOldestKey() {
|
|
144
|
+
let oldestKey = null;
|
|
145
|
+
let oldestTime = Infinity;
|
|
146
|
+
for (const [key, entry] of this.memoryCache.entries()) {
|
|
147
|
+
if (entry.timestamp < oldestTime) {
|
|
148
|
+
oldestTime = entry.timestamp;
|
|
149
|
+
oldestKey = key;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return oldestKey;
|
|
153
|
+
}
|
|
154
|
+
clear() {
|
|
155
|
+
this.memoryCache.clear();
|
|
156
|
+
if (this.localStorageAvailable) {
|
|
157
|
+
try {
|
|
158
|
+
localStorage.removeItem(this.localStorageKey);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.warn("Failed to clear localStorage cache:", error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
size() {
|
|
165
|
+
return this.memoryCache.size;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// src/mockup/service.ts
|
|
170
|
+
var DEFAULT_IMAGE_URL = typeof process !== "undefined" && process.env?.MERCHIFY_IMAGE_URL || "https://i.snowcone.app";
|
|
171
|
+
var DEFAULT_SIGNER_URL = typeof process !== "undefined" && process.env?.MERCHIFY_SIGNER_URL || "https://s.snowcone.app/sign";
|
|
172
|
+
var RATE_LIMIT_PER_MINUTE = 450;
|
|
173
|
+
var RATE_LIMIT_PER_SECOND = 20;
|
|
174
|
+
var MockupServiceImpl = class {
|
|
175
|
+
config;
|
|
176
|
+
cache;
|
|
177
|
+
rateLimitState;
|
|
178
|
+
fetch;
|
|
179
|
+
constructor(config2, customFetch) {
|
|
180
|
+
this.config = {
|
|
181
|
+
imageUrl: config2.imageUrl || DEFAULT_IMAGE_URL,
|
|
182
|
+
signerUrl: config2.signerUrl || DEFAULT_SIGNER_URL,
|
|
183
|
+
accountId: config2.accountId || typeof process !== "undefined" && process.env?.MERCHIFY_ACCOUNT_ID || ""
|
|
184
|
+
};
|
|
185
|
+
this.cache = new SignatureCache();
|
|
186
|
+
this.fetch = customFetch || fetch.bind(globalThis);
|
|
187
|
+
this.rateLimitState = {
|
|
188
|
+
perMinute: { count: 0, resetTime: Date.now() + 6e4 },
|
|
189
|
+
perSecond: { count: 0, resetTime: Date.now() + 1e3 }
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async getMockupUrl(options) {
|
|
193
|
+
if (!options.design || !options.product) {
|
|
194
|
+
throw new Error("Missing required options: design and product are required");
|
|
195
|
+
}
|
|
196
|
+
if (!this.config.accountId) {
|
|
197
|
+
throw new Error("Account ID is required for mockup generation");
|
|
198
|
+
}
|
|
199
|
+
this.checkRateLimits();
|
|
200
|
+
const relativeUrl = this.buildMockupUrlWithParams(options);
|
|
201
|
+
const cacheKey = `${relativeUrl}:${this.config.accountId}`;
|
|
202
|
+
const cachedUrl = this.cache.get(cacheKey);
|
|
203
|
+
if (cachedUrl) {
|
|
204
|
+
return cachedUrl;
|
|
205
|
+
}
|
|
206
|
+
const signedUrl = await this.getSignedUrl(relativeUrl);
|
|
207
|
+
this.cache.set(cacheKey, signedUrl);
|
|
208
|
+
this.incrementRateLimitCounters();
|
|
209
|
+
return signedUrl;
|
|
210
|
+
}
|
|
211
|
+
buildMockupUrlWithParams(options) {
|
|
212
|
+
const { design, product, effects, ar } = options;
|
|
213
|
+
const designBase64 = typeof window !== "undefined" && window.btoa ? window.btoa(JSON.stringify(design)) : Buffer.from(JSON.stringify(design)).toString("base64");
|
|
214
|
+
const encodedDesign = encodeURIComponent(designBase64);
|
|
215
|
+
let url = `/mockup?productId=${encodeURIComponent(product.productId)}`;
|
|
216
|
+
url += `&mockupId=${encodeURIComponent(product.mockupId)}`;
|
|
217
|
+
url += `&variantId=${encodeURIComponent(product.variantId)}`;
|
|
218
|
+
url += `&design=${encodedDesign}`;
|
|
219
|
+
url += `&width=${product.width}`;
|
|
220
|
+
if (effects?.grain) {
|
|
221
|
+
url += `&grain=${effects.grain}`;
|
|
222
|
+
}
|
|
223
|
+
if (ar && ar !== "16:9") {
|
|
224
|
+
url += `&ar=${encodeURIComponent(ar)}`;
|
|
225
|
+
}
|
|
226
|
+
if (product.optionSelections && Object.keys(product.optionSelections).length > 0) {
|
|
227
|
+
const optionSelectionsBase64 = typeof window !== "undefined" && window.btoa ? window.btoa(JSON.stringify(product.optionSelections)) : Buffer.from(JSON.stringify(product.optionSelections)).toString("base64");
|
|
228
|
+
url += `&optionSelections=${encodeURIComponent(optionSelectionsBase64)}`;
|
|
229
|
+
}
|
|
230
|
+
return url;
|
|
231
|
+
}
|
|
232
|
+
buildMockupUrl(options) {
|
|
233
|
+
const { design, product, effects } = options;
|
|
234
|
+
const pathSegments = ["m"];
|
|
235
|
+
pathSegments.push(product.productId);
|
|
236
|
+
pathSegments.push(product.mockupId);
|
|
237
|
+
pathSegments.push(product.variantId);
|
|
238
|
+
pathSegments.push(String(product.width));
|
|
239
|
+
const designParts = [];
|
|
240
|
+
for (const element of design) {
|
|
241
|
+
const part = this.encodeDesignElement(element);
|
|
242
|
+
designParts.push(part);
|
|
243
|
+
}
|
|
244
|
+
if (designParts.length > 0) {
|
|
245
|
+
pathSegments.push(designParts.join("_"));
|
|
246
|
+
} else {
|
|
247
|
+
pathSegments.push("");
|
|
248
|
+
}
|
|
249
|
+
if (effects?.grain) {
|
|
250
|
+
pathSegments.push(`g${effects.grain}`);
|
|
251
|
+
}
|
|
252
|
+
return "/" + pathSegments.join("/");
|
|
253
|
+
}
|
|
254
|
+
encodeDesignElement(element) {
|
|
255
|
+
const parts = [];
|
|
256
|
+
if (element.type === "image") {
|
|
257
|
+
const encodedUrl = this.encodeImageUrl(element.imageUrl);
|
|
258
|
+
parts.push(encodedUrl);
|
|
259
|
+
parts.push(element.placement);
|
|
260
|
+
parts.push(String(element.width));
|
|
261
|
+
parts.push(String(element.height));
|
|
262
|
+
parts.push(element.alignment);
|
|
263
|
+
if (element.tiles && element.tiles > 1) {
|
|
264
|
+
parts.push(`t${element.tiles}`);
|
|
265
|
+
}
|
|
266
|
+
} else if (element.type === "color") {
|
|
267
|
+
const colorCode = element.hex.replace("#", "");
|
|
268
|
+
parts.push(colorCode);
|
|
269
|
+
parts.push(element.placement);
|
|
270
|
+
parts.push(String(element.width));
|
|
271
|
+
parts.push(String(element.height));
|
|
272
|
+
parts.push(element.alignment);
|
|
273
|
+
}
|
|
274
|
+
return parts.join(",");
|
|
275
|
+
}
|
|
276
|
+
encodeImageUrl(url) {
|
|
277
|
+
if (typeof window !== "undefined" && window.btoa) {
|
|
278
|
+
const base64 = window.btoa(url);
|
|
279
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
280
|
+
} else {
|
|
281
|
+
const base64 = Buffer.from(url).toString("base64");
|
|
282
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async getSignedUrl(relativeUrl) {
|
|
286
|
+
const urlWithAccountId = relativeUrl + (relativeUrl.includes("?") ? "&" : "?") + `accountId=${encodeURIComponent(this.config.accountId)}`;
|
|
287
|
+
const cacheKey = urlWithAccountId;
|
|
288
|
+
const cachedUrl = this.cache.get(cacheKey);
|
|
289
|
+
if (cachedUrl) {
|
|
290
|
+
return cachedUrl;
|
|
291
|
+
}
|
|
292
|
+
if (this.config.signerUrl?.includes("localhost") || this.config.signerUrl === "mock") {
|
|
293
|
+
const signedUrl = this.config.imageUrl + urlWithAccountId + "&sig=bypass-sig-for-k6-load-test";
|
|
294
|
+
this.cache.set(cacheKey, signedUrl);
|
|
295
|
+
return signedUrl;
|
|
296
|
+
}
|
|
297
|
+
const signerUrl = new URL(this.config.signerUrl);
|
|
298
|
+
signerUrl.searchParams.set("url", urlWithAccountId);
|
|
299
|
+
try {
|
|
300
|
+
const response = await this.fetch(signerUrl.toString(), {
|
|
301
|
+
method: "GET",
|
|
302
|
+
headers: {
|
|
303
|
+
"Accept": "application/json"
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
throw new Error(`Failed to sign URL: ${response.statusText}`);
|
|
308
|
+
}
|
|
309
|
+
const data = await response.json();
|
|
310
|
+
const signedUrl = this.config.imageUrl + (data.urlWithSignature || data.url);
|
|
311
|
+
this.cache.set(cacheKey, signedUrl);
|
|
312
|
+
return signedUrl;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
throw new Error(`Failed to get signed URL: ${error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
checkRateLimits() {
|
|
318
|
+
const now = Date.now();
|
|
319
|
+
if (now >= this.rateLimitState.perMinute.resetTime) {
|
|
320
|
+
this.rateLimitState.perMinute.count = 0;
|
|
321
|
+
this.rateLimitState.perMinute.resetTime = now + 6e4;
|
|
322
|
+
}
|
|
323
|
+
if (now >= this.rateLimitState.perSecond.resetTime) {
|
|
324
|
+
this.rateLimitState.perSecond.count = 0;
|
|
325
|
+
this.rateLimitState.perSecond.resetTime = now + 1e3;
|
|
326
|
+
}
|
|
327
|
+
if (this.rateLimitState.perMinute.count >= RATE_LIMIT_PER_MINUTE) {
|
|
328
|
+
throw new Error("Rate limit exceeded: Too many requests per minute");
|
|
329
|
+
}
|
|
330
|
+
if (this.rateLimitState.perSecond.count >= RATE_LIMIT_PER_SECOND) {
|
|
331
|
+
throw new Error("Rate limit exceeded: Too many requests per second");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
incrementRateLimitCounters() {
|
|
335
|
+
this.rateLimitState.perMinute.count++;
|
|
336
|
+
this.rateLimitState.perSecond.count++;
|
|
337
|
+
}
|
|
338
|
+
getConfig() {
|
|
339
|
+
return { ...this.config };
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// src/state/optionSelection.ts
|
|
344
|
+
function resolveBestCombination(selection, attributes, combinations) {
|
|
345
|
+
const relevant = Object.fromEntries(
|
|
346
|
+
Object.entries(selection).filter(
|
|
347
|
+
([k]) => attributes[k]?.affectsCombinations !== false
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
let best;
|
|
351
|
+
for (const combo of combinations) {
|
|
352
|
+
let score = 0;
|
|
353
|
+
for (const [k, v] of Object.entries(relevant)) {
|
|
354
|
+
if (combo[k] === v) score += 100;
|
|
355
|
+
else score -= 1;
|
|
356
|
+
}
|
|
357
|
+
if (!best || score > best.score) best = { combo, score };
|
|
358
|
+
}
|
|
359
|
+
return best?.combo;
|
|
360
|
+
}
|
|
361
|
+
function computeDisabledChoices(selection, attributes, combinations) {
|
|
362
|
+
const disabledByKey = {};
|
|
363
|
+
const attributeKeys = Object.keys(attributes || {});
|
|
364
|
+
for (const key of attributeKeys) {
|
|
365
|
+
const attr = attributes[key];
|
|
366
|
+
if (!attr) continue;
|
|
367
|
+
if (attr.affectsCombinations === false) {
|
|
368
|
+
disabledByKey[key] = [];
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const disabledChoices = [];
|
|
372
|
+
for (const choice of attr.choices || []) {
|
|
373
|
+
const candidateSelection = {
|
|
374
|
+
...selection,
|
|
375
|
+
[key]: choice
|
|
376
|
+
};
|
|
377
|
+
const relevantEntries = Object.entries(candidateSelection).filter(
|
|
378
|
+
([k]) => attributes[k]?.affectsCombinations !== false
|
|
379
|
+
);
|
|
380
|
+
const hasViable = combinations.some(
|
|
381
|
+
(combo) => relevantEntries.every(([k, v]) => combo[k] === v)
|
|
382
|
+
);
|
|
383
|
+
if (!hasViable) disabledChoices.push(choice);
|
|
384
|
+
}
|
|
385
|
+
disabledByKey[key] = disabledChoices;
|
|
386
|
+
}
|
|
387
|
+
return disabledByKey;
|
|
388
|
+
}
|
|
389
|
+
function deriveDefaultSelection(attributes, combinations) {
|
|
390
|
+
const best = resolveBestCombination({}, attributes, combinations);
|
|
391
|
+
const selection = {};
|
|
392
|
+
if (best) {
|
|
393
|
+
for (const [key, attr] of Object.entries(attributes)) {
|
|
394
|
+
if (attr.affectsCombinations !== false) {
|
|
395
|
+
const value = best[key];
|
|
396
|
+
if (typeof value === "string") {
|
|
397
|
+
selection[key] = value;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const [key, attr] of Object.entries(attributes)) {
|
|
403
|
+
if (!selection[key]) {
|
|
404
|
+
if (attr.type === "color-picker") {
|
|
405
|
+
selection[key] = "#000000";
|
|
406
|
+
} else if (attr.choices && attr.choices.length > 0) {
|
|
407
|
+
const firstChoice = attr.choices[0];
|
|
408
|
+
selection[key] = typeof firstChoice === "string" ? firstChoice : firstChoice.label || "";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return selection;
|
|
413
|
+
}
|
|
414
|
+
function getPricePreview(selection, attributes, combinations) {
|
|
415
|
+
const best = resolveBestCombination(selection, attributes, combinations);
|
|
416
|
+
const price = best?.price;
|
|
417
|
+
return typeof price === "number" ? price : void 0;
|
|
418
|
+
}
|
|
419
|
+
function findBestCombination(selection, combinations, attributes) {
|
|
420
|
+
const relevantSelection = {};
|
|
421
|
+
if (attributes) {
|
|
422
|
+
Object.entries(selection).forEach(([key, value]) => {
|
|
423
|
+
const attr = attributes[key];
|
|
424
|
+
if (attr && attr.affectsCombinations !== false) {
|
|
425
|
+
relevantSelection[key] = value;
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
Object.assign(relevantSelection, selection);
|
|
430
|
+
}
|
|
431
|
+
return combinations.find((combo) => {
|
|
432
|
+
return Object.entries(relevantSelection).every(([key, value]) => {
|
|
433
|
+
return !combo[key] || combo[key] === value;
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function isOptionAvailable(optionName, attributeName, selection, attributes, combinations) {
|
|
438
|
+
if (!combinations || combinations.length === 0 || !attributes[attributeName]?.affectsCombinations) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
return combinations.some((combination) => {
|
|
442
|
+
if (combination[attributeName] !== optionName) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
return Object.entries(selection).every(
|
|
446
|
+
([key, value]) => key === attributeName || // Skip the group we're checking
|
|
447
|
+
value === void 0 || value === "" || // Skip unselected options
|
|
448
|
+
!attributes[key]?.affectsCombinations || // Skip attributes that don't affect combinations
|
|
449
|
+
combination[key] === value
|
|
450
|
+
// Check if the selection matches
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/state/manager.ts
|
|
456
|
+
var StateManager = class {
|
|
457
|
+
constructor(options) {
|
|
458
|
+
this.options = options;
|
|
459
|
+
this.state = this.loadPersistedState() || options.initialState;
|
|
460
|
+
this.middleware = options.middleware || [];
|
|
461
|
+
if (options.debug) {
|
|
462
|
+
this.middleware.push({
|
|
463
|
+
name: "debug",
|
|
464
|
+
afterUpdate: (state, prevState) => {
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
state;
|
|
470
|
+
prevState;
|
|
471
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
472
|
+
selectorSubscribers = /* @__PURE__ */ new Map();
|
|
473
|
+
updateQueue = [];
|
|
474
|
+
isUpdating = false;
|
|
475
|
+
middleware;
|
|
476
|
+
/**
|
|
477
|
+
* Get current state
|
|
478
|
+
*/
|
|
479
|
+
getState() {
|
|
480
|
+
return this.state;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Update state with partial updates or updater function
|
|
484
|
+
*/
|
|
485
|
+
setState(updater) {
|
|
486
|
+
const update = typeof updater === "function" ? updater(this.state) : updater;
|
|
487
|
+
this.updateQueue.push(update);
|
|
488
|
+
if (!this.isUpdating) {
|
|
489
|
+
this.processUpdateQueue();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Subscribe to all state changes
|
|
494
|
+
*/
|
|
495
|
+
subscribe(callback) {
|
|
496
|
+
this.subscribers.add(callback);
|
|
497
|
+
callback(this.state);
|
|
498
|
+
return () => {
|
|
499
|
+
this.subscribers.delete(callback);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Subscribe to specific state selections (computed values)
|
|
504
|
+
*/
|
|
505
|
+
subscribeToSelector(selector, callback) {
|
|
506
|
+
if (!this.selectorSubscribers.has(selector)) {
|
|
507
|
+
this.selectorSubscribers.set(selector, /* @__PURE__ */ new Set());
|
|
508
|
+
}
|
|
509
|
+
const subscribers = this.selectorSubscribers.get(selector);
|
|
510
|
+
subscribers.add(callback);
|
|
511
|
+
callback(selector(this.state));
|
|
512
|
+
return () => {
|
|
513
|
+
subscribers.delete(callback);
|
|
514
|
+
if (subscribers.size === 0) {
|
|
515
|
+
this.selectorSubscribers.delete(selector);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Reset state to initial value
|
|
521
|
+
*/
|
|
522
|
+
reset() {
|
|
523
|
+
this.setState(this.options.initialState);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Get a derived value from state
|
|
527
|
+
*/
|
|
528
|
+
select(selector) {
|
|
529
|
+
return selector(this.state);
|
|
530
|
+
}
|
|
531
|
+
processUpdateQueue() {
|
|
532
|
+
if (this.updateQueue.length === 0) return;
|
|
533
|
+
this.isUpdating = true;
|
|
534
|
+
this.prevState = { ...this.state };
|
|
535
|
+
let newState = { ...this.state };
|
|
536
|
+
while (this.updateQueue.length > 0) {
|
|
537
|
+
const update = this.updateQueue.shift();
|
|
538
|
+
let processedUpdate = update;
|
|
539
|
+
for (const mw of this.middleware) {
|
|
540
|
+
if (mw.beforeUpdate) {
|
|
541
|
+
processedUpdate = mw.beforeUpdate(newState, processedUpdate);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
newState = { ...newState, ...processedUpdate };
|
|
545
|
+
}
|
|
546
|
+
if (this.hasStateChanged(this.state, newState)) {
|
|
547
|
+
this.state = newState;
|
|
548
|
+
this.persistState();
|
|
549
|
+
this.notifySubscribers();
|
|
550
|
+
for (const mw of this.middleware) {
|
|
551
|
+
if (mw.afterUpdate) {
|
|
552
|
+
mw.afterUpdate(this.state, this.prevState);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
this.isUpdating = false;
|
|
557
|
+
if (this.updateQueue.length > 0) {
|
|
558
|
+
this.processUpdateQueue();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
notifySubscribers() {
|
|
562
|
+
this.subscribers.forEach((subscriber) => {
|
|
563
|
+
try {
|
|
564
|
+
subscriber(this.state, this.prevState);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.error("[StateManager] Subscriber error:", error);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
this.selectorSubscribers.forEach((subscribers, selector) => {
|
|
570
|
+
const currentValue = selector(this.state);
|
|
571
|
+
const prevValue = this.prevState ? selector(this.prevState) : void 0;
|
|
572
|
+
if (currentValue !== prevValue) {
|
|
573
|
+
subscribers.forEach((callback) => {
|
|
574
|
+
try {
|
|
575
|
+
callback(currentValue, prevValue);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error("[StateManager] Selector subscriber error:", error);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
hasStateChanged(prev, next) {
|
|
584
|
+
return JSON.stringify(prev) !== JSON.stringify(next);
|
|
585
|
+
}
|
|
586
|
+
getStateDiff(prev, next) {
|
|
587
|
+
const diff = {};
|
|
588
|
+
for (const key in next) {
|
|
589
|
+
if (prev[key] !== next[key]) {
|
|
590
|
+
diff[key] = next[key];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return diff;
|
|
594
|
+
}
|
|
595
|
+
persistState() {
|
|
596
|
+
if (this.options.persist) {
|
|
597
|
+
const storage = this.options.persist.storage || localStorage;
|
|
598
|
+
try {
|
|
599
|
+
storage.setItem(
|
|
600
|
+
this.options.persist.key,
|
|
601
|
+
JSON.stringify(this.state)
|
|
602
|
+
);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
console.error("[StateManager] Failed to persist state:", error);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
loadPersistedState() {
|
|
609
|
+
if (!this.options.persist) return null;
|
|
610
|
+
const storage = this.options.persist.storage || localStorage;
|
|
611
|
+
try {
|
|
612
|
+
const stored = storage.getItem(this.options.persist.key);
|
|
613
|
+
return stored ? JSON.parse(stored) : null;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.error("[StateManager] Failed to load persisted state:", error);
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
function createStateStore(initialState, options) {
|
|
621
|
+
return new StateManager({
|
|
622
|
+
...options,
|
|
623
|
+
initialState
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function createStateHook(store) {
|
|
627
|
+
return {
|
|
628
|
+
getState: () => store.getState(),
|
|
629
|
+
setState: (updater) => store.setState(updater),
|
|
630
|
+
subscribe: (callback) => store.subscribe(callback),
|
|
631
|
+
select: (selector) => store.select(selector),
|
|
632
|
+
reset: () => store.reset()
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/rendering/productTitle.ts
|
|
637
|
+
function describeProductTitle(options) {
|
|
638
|
+
return {
|
|
639
|
+
type: "product-title",
|
|
640
|
+
product: options.product,
|
|
641
|
+
showPrice: options.showPrice ?? true,
|
|
642
|
+
showDescription: options.showDescription ?? true,
|
|
643
|
+
size: options.size ?? "md",
|
|
644
|
+
className: options.className
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/rendering/productPrice.ts
|
|
649
|
+
function formatPrice(cents, locale, currency, showCurrency) {
|
|
650
|
+
const amount = cents / 100;
|
|
651
|
+
if (showCurrency) {
|
|
652
|
+
return new Intl.NumberFormat(locale, {
|
|
653
|
+
style: "currency",
|
|
654
|
+
currency
|
|
655
|
+
}).format(amount);
|
|
656
|
+
}
|
|
657
|
+
return new Intl.NumberFormat(locale, {
|
|
658
|
+
minimumFractionDigits: 2,
|
|
659
|
+
maximumFractionDigits: 2
|
|
660
|
+
}).format(amount);
|
|
661
|
+
}
|
|
662
|
+
function describeProductPrice(options, contextPrice) {
|
|
663
|
+
const displayPrice = options.price ?? contextPrice;
|
|
664
|
+
if (!displayPrice) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
type: "price",
|
|
669
|
+
price: displayPrice,
|
|
670
|
+
currency: options.currency || "USD",
|
|
671
|
+
locale: options.locale || "en-US",
|
|
672
|
+
showCurrency: options.showCurrency !== false,
|
|
673
|
+
className: options.className
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/rendering/productImage.ts
|
|
678
|
+
function describeProductImage(options, contextImage) {
|
|
679
|
+
const src = options.src || contextImage?.src;
|
|
680
|
+
if (!src) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
type: "image",
|
|
685
|
+
src,
|
|
686
|
+
alt: options.alt || contextImage?.alt || "Product image",
|
|
687
|
+
width: options.width,
|
|
688
|
+
height: options.height,
|
|
689
|
+
aspectRatio: options.aspectRatio || "1",
|
|
690
|
+
className: options.className
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/rendering/productOptions.ts
|
|
695
|
+
function describeProductOptions(options) {
|
|
696
|
+
return {
|
|
697
|
+
type: "product-options",
|
|
698
|
+
options: options.options,
|
|
699
|
+
selectedOptions: options.selectedOptions || {},
|
|
700
|
+
className: options.className
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/rendering/productArtAlignment.ts
|
|
705
|
+
function getSnapPoints(containerSize, maskDimensions, effectiveAlignment) {
|
|
706
|
+
const maxDistance = effectiveAlignment === "horizontal" ? (containerSize.width - maskDimensions.width) / 2 : (containerSize.height - maskDimensions.height) / 2;
|
|
707
|
+
const containerDimension = effectiveAlignment === "horizontal" ? containerSize.width : containerSize.height;
|
|
708
|
+
const movementRatio = maxDistance / containerDimension;
|
|
709
|
+
const useCenterOnly = movementRatio < 0.05;
|
|
710
|
+
const useThreePoints = movementRatio < 0.15;
|
|
711
|
+
if (effectiveAlignment === "horizontal") {
|
|
712
|
+
if (useCenterOnly) {
|
|
713
|
+
return {
|
|
714
|
+
center: 0
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
if (useThreePoints) {
|
|
718
|
+
return {
|
|
719
|
+
"far-left": -maxDistance,
|
|
720
|
+
center: 0,
|
|
721
|
+
"far-right": maxDistance
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
"far-left": -maxDistance,
|
|
726
|
+
left: -maxDistance / 2,
|
|
727
|
+
center: 0,
|
|
728
|
+
right: maxDistance / 2,
|
|
729
|
+
"far-right": maxDistance
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
if (useCenterOnly) {
|
|
733
|
+
return {
|
|
734
|
+
center: 0
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
if (useThreePoints) {
|
|
738
|
+
return {
|
|
739
|
+
"far-top": -maxDistance,
|
|
740
|
+
center: 0,
|
|
741
|
+
"far-bottom": maxDistance
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
"far-top": -maxDistance,
|
|
746
|
+
top: -maxDistance / 2,
|
|
747
|
+
center: 0,
|
|
748
|
+
bottom: maxDistance / 2,
|
|
749
|
+
"far-bottom": maxDistance
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function getEffectiveAlignment(artworkAspectRatio, maskAspectRatio) {
|
|
753
|
+
if (artworkAspectRatio < maskAspectRatio) {
|
|
754
|
+
return "vertical";
|
|
755
|
+
}
|
|
756
|
+
return "horizontal";
|
|
757
|
+
}
|
|
758
|
+
function describeProductArtAlignment(options, context) {
|
|
759
|
+
const src = options.src || context?.product?.mockupUrl;
|
|
760
|
+
if (!src) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
let maskAspectRatio = options.maskAspectRatio;
|
|
764
|
+
if (!maskAspectRatio && context?.product?.placements) {
|
|
765
|
+
const placements = context.product.placements;
|
|
766
|
+
let placement;
|
|
767
|
+
if (options.placement) {
|
|
768
|
+
placement = placements.find((p) => p.label === options.placement);
|
|
769
|
+
} else {
|
|
770
|
+
placement = placements.find((p) => p.type === "image") || placements[0];
|
|
771
|
+
}
|
|
772
|
+
if (placement && placement.width && placement.height) {
|
|
773
|
+
maskAspectRatio = placement.width / placement.height;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (!maskAspectRatio) {
|
|
777
|
+
maskAspectRatio = 1;
|
|
778
|
+
}
|
|
779
|
+
const artworkAspectRatio = options.artworkAspectRatio || context?.product?.aspectRatio || 1;
|
|
780
|
+
const alignment = options.alignment || context?.selection?.alignment || "center";
|
|
781
|
+
const effectiveAlignment = getEffectiveAlignment(artworkAspectRatio, maskAspectRatio);
|
|
782
|
+
return {
|
|
783
|
+
type: "art-alignment",
|
|
784
|
+
src,
|
|
785
|
+
artworkAspectRatio,
|
|
786
|
+
maskAspectRatio,
|
|
787
|
+
effectiveAlignment,
|
|
788
|
+
alignment,
|
|
789
|
+
className: options.className
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function findClosestSnapPoint(position, containerSize, maskDimensions, effectiveAlignment) {
|
|
793
|
+
const snapPoints = getSnapPoints(containerSize, maskDimensions, effectiveAlignment);
|
|
794
|
+
let closestKey = "center";
|
|
795
|
+
let closestDistance = Infinity;
|
|
796
|
+
for (const [key, value] of Object.entries(snapPoints)) {
|
|
797
|
+
const distance = Math.abs(position - value);
|
|
798
|
+
if (distance < closestDistance) {
|
|
799
|
+
closestDistance = distance;
|
|
800
|
+
closestKey = key;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return closestKey;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/rendering/artSelector.ts
|
|
807
|
+
function describeArtSelector(options, context) {
|
|
808
|
+
const artworks = [];
|
|
809
|
+
const defaultArtworks = [
|
|
810
|
+
"https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
|
811
|
+
"https://images.unsplash.com/photo-1754999961467-0d6e4c2551e3?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
|
812
|
+
];
|
|
813
|
+
const sources = options.artworks || defaultArtworks;
|
|
814
|
+
sources.forEach((src) => {
|
|
815
|
+
artworks.push({
|
|
816
|
+
type: "regular",
|
|
817
|
+
src
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
return {
|
|
821
|
+
type: "art-selector",
|
|
822
|
+
artworks,
|
|
823
|
+
selectedArtwork: context?.selectedArtwork || null,
|
|
824
|
+
className: options.className
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/rendering/optionSelection.ts
|
|
829
|
+
function getOptionRenderType(attr) {
|
|
830
|
+
if (attr.type === "color-picker") {
|
|
831
|
+
return "color-picker";
|
|
832
|
+
}
|
|
833
|
+
const hasVisualChoices = attr.choices?.some(
|
|
834
|
+
(choice) => typeof choice === "object" && (choice.hex || choice.imageUrl)
|
|
835
|
+
);
|
|
836
|
+
if (hasVisualChoices) {
|
|
837
|
+
return "swatch";
|
|
838
|
+
}
|
|
839
|
+
return "select";
|
|
840
|
+
}
|
|
841
|
+
function normalizeChoice(choice) {
|
|
842
|
+
if (typeof choice === "string") {
|
|
843
|
+
return { label: choice, value: choice };
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
...choice,
|
|
847
|
+
value: choice.value || choice.label
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function prepareOptionRenderData(attributes, selection, combinations) {
|
|
851
|
+
return Object.entries(attributes).map(([key, attr]) => {
|
|
852
|
+
const currentValue = selection[key] || "";
|
|
853
|
+
const renderType = getOptionRenderType(attr);
|
|
854
|
+
const choices = (attr.choices || []).map((choice) => {
|
|
855
|
+
const normalized = normalizeChoice(choice);
|
|
856
|
+
const value = normalized.value || normalized.label;
|
|
857
|
+
const isAvailable = isOptionAvailable(
|
|
858
|
+
value,
|
|
859
|
+
key,
|
|
860
|
+
selection,
|
|
861
|
+
attributes,
|
|
862
|
+
combinations
|
|
863
|
+
);
|
|
864
|
+
return {
|
|
865
|
+
value,
|
|
866
|
+
label: normalized.label,
|
|
867
|
+
disabled: !isAvailable,
|
|
868
|
+
selected: currentValue === value,
|
|
869
|
+
hex: normalized.hex,
|
|
870
|
+
imageUrl: normalized.imageUrl
|
|
871
|
+
};
|
|
872
|
+
});
|
|
873
|
+
return {
|
|
874
|
+
key,
|
|
875
|
+
label: key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "),
|
|
876
|
+
type: renderType,
|
|
877
|
+
value: currentValue,
|
|
878
|
+
choices,
|
|
879
|
+
required: attr.affectsCombinations !== false
|
|
880
|
+
};
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function handleOptionChange(optionKey, newValue, currentSelection, attributes) {
|
|
884
|
+
const newSelection = {
|
|
885
|
+
...currentSelection,
|
|
886
|
+
[optionKey]: newValue
|
|
887
|
+
};
|
|
888
|
+
const clearedSelection = clearDependentSelections(
|
|
889
|
+
optionKey,
|
|
890
|
+
newSelection,
|
|
891
|
+
attributes
|
|
892
|
+
);
|
|
893
|
+
return clearedSelection;
|
|
894
|
+
}
|
|
895
|
+
function clearDependentSelections(changedKey, selection, attributes) {
|
|
896
|
+
return selection;
|
|
897
|
+
}
|
|
898
|
+
function validateRequiredOptions(selection, attributes) {
|
|
899
|
+
const missing = [];
|
|
900
|
+
for (const [key, attr] of Object.entries(attributes)) {
|
|
901
|
+
if (attr.affectsCombinations === false) continue;
|
|
902
|
+
const value = selection[key];
|
|
903
|
+
if (!value || value === "") {
|
|
904
|
+
missing.push(key);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
valid: missing.length === 0,
|
|
909
|
+
missing
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function getSelectionDisplayText(selection, attributes) {
|
|
913
|
+
const parts = [];
|
|
914
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
915
|
+
if (!value || value === "") continue;
|
|
916
|
+
const attr = attributes[key];
|
|
917
|
+
if (!attr) continue;
|
|
918
|
+
const choice = attr.choices?.find((c) => {
|
|
919
|
+
const normalized = normalizeChoice(c);
|
|
920
|
+
return (normalized.value || normalized.label) === value;
|
|
921
|
+
});
|
|
922
|
+
const label = choice ? normalizeChoice(choice).label : value;
|
|
923
|
+
parts.push(label);
|
|
924
|
+
}
|
|
925
|
+
return parts.join(" / ");
|
|
926
|
+
}
|
|
927
|
+
var ColorPickerUtils = {
|
|
928
|
+
/**
|
|
929
|
+
* Validates a hex color
|
|
930
|
+
*/
|
|
931
|
+
isValidHex(hex) {
|
|
932
|
+
return /^#[0-9A-F]{6}$/i.test(hex);
|
|
933
|
+
},
|
|
934
|
+
/**
|
|
935
|
+
* Converts hex to RGB
|
|
936
|
+
*/
|
|
937
|
+
hexToRgb(hex) {
|
|
938
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
939
|
+
return result ? {
|
|
940
|
+
r: parseInt(result[1], 16),
|
|
941
|
+
g: parseInt(result[2], 16),
|
|
942
|
+
b: parseInt(result[3], 16)
|
|
943
|
+
} : null;
|
|
944
|
+
},
|
|
945
|
+
/**
|
|
946
|
+
* Gets contrast color for text on background
|
|
947
|
+
*/
|
|
948
|
+
getContrastColor(hex) {
|
|
949
|
+
const rgb = this.hexToRgb(hex);
|
|
950
|
+
if (!rgb) return "black";
|
|
951
|
+
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
952
|
+
return luminance > 0.5 ? "black" : "white";
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
var SwatchUtils = {
|
|
956
|
+
/**
|
|
957
|
+
* Gets swatch style object
|
|
958
|
+
*/
|
|
959
|
+
getSwatchStyle(choice) {
|
|
960
|
+
const style = {};
|
|
961
|
+
if (choice.imageUrl) {
|
|
962
|
+
style.backgroundImage = `url(${choice.imageUrl})`;
|
|
963
|
+
style.backgroundSize = "cover";
|
|
964
|
+
style.backgroundPosition = "center";
|
|
965
|
+
style.border = "1px solid #e2e8f0";
|
|
966
|
+
} else if (choice.hex) {
|
|
967
|
+
style.backgroundColor = choice.hex;
|
|
968
|
+
style.border = "1px solid #e2e8f0";
|
|
969
|
+
} else {
|
|
970
|
+
style.border = "1px solid #e2e8f0";
|
|
971
|
+
style.display = "flex";
|
|
972
|
+
style.alignItems = "center";
|
|
973
|
+
style.justifyContent = "center";
|
|
974
|
+
}
|
|
975
|
+
if (choice.disabled) {
|
|
976
|
+
style.opacity = "0.5";
|
|
977
|
+
style.cursor = "not-allowed";
|
|
978
|
+
}
|
|
979
|
+
if (choice.selected) {
|
|
980
|
+
style.boxShadow = "0 0 0 2px #3b82f6";
|
|
981
|
+
}
|
|
982
|
+
return style;
|
|
983
|
+
},
|
|
984
|
+
/**
|
|
985
|
+
* Gets swatch class names
|
|
986
|
+
*/
|
|
987
|
+
getSwatchClasses(choice) {
|
|
988
|
+
const classes = ["option-swatch"];
|
|
989
|
+
if (choice.disabled) classes.push("option-swatch--disabled");
|
|
990
|
+
if (choice.selected) classes.push("option-swatch--selected");
|
|
991
|
+
if (choice.hex) classes.push("option-swatch--color");
|
|
992
|
+
if (choice.imageUrl) classes.push("option-swatch--image");
|
|
993
|
+
return classes;
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
// src/constants/mockup.ts
|
|
998
|
+
var DEFAULT_ARTWORK_URL = "https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D";
|
|
999
|
+
var DEFAULT_COLOR = "#000000";
|
|
1000
|
+
var DEFAULT_PLACEMENT_DIMENSIONS = {
|
|
1001
|
+
width: 1200,
|
|
1002
|
+
height: 1600
|
|
1003
|
+
};
|
|
1004
|
+
var DEFAULT_ASPECT_RATIO = 1;
|
|
1005
|
+
|
|
1006
|
+
// src/utils/url.ts
|
|
1007
|
+
function resolveUrlFromDOM(relativeUrl) {
|
|
1008
|
+
if (typeof document === "undefined") return null;
|
|
1009
|
+
const pathParts = relativeUrl.split("/");
|
|
1010
|
+
const filename = pathParts[pathParts.length - 1];
|
|
1011
|
+
if (!filename) return null;
|
|
1012
|
+
const filenameWithoutExt = filename.replace(/\.[^.]+$/, "");
|
|
1013
|
+
const images = document.querySelectorAll("img");
|
|
1014
|
+
for (const img of images) {
|
|
1015
|
+
const src = img.getAttribute("src");
|
|
1016
|
+
if (!src) continue;
|
|
1017
|
+
if (src.includes(filenameWithoutExt)) {
|
|
1018
|
+
return src;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
function normalizeImageUrl(url) {
|
|
1024
|
+
if (!url) return null;
|
|
1025
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
1026
|
+
return url;
|
|
1027
|
+
}
|
|
1028
|
+
if (url.startsWith("data:")) {
|
|
1029
|
+
return url;
|
|
1030
|
+
}
|
|
1031
|
+
if (url.startsWith("//")) {
|
|
1032
|
+
return url;
|
|
1033
|
+
}
|
|
1034
|
+
if (typeof window !== "undefined") {
|
|
1035
|
+
const domUrl = resolveUrlFromDOM(url);
|
|
1036
|
+
if (domUrl) {
|
|
1037
|
+
return domUrl;
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
const absoluteUrl = new URL(url, window.location.origin).href;
|
|
1041
|
+
if (absoluteUrl.includes("localhost") || absoluteUrl.includes("127.0.0.1")) {
|
|
1042
|
+
console.warn(
|
|
1043
|
+
`[merchify-sdk] Artwork URL "${absoluteUrl}" uses localhost. This will work in your browser but won't work for mockup generation. Deploy your app or use publicly accessible URLs for production mockups.`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
return absoluteUrl;
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
console.warn(`[merchify-sdk] Failed to normalize URL "${url}":`, e);
|
|
1049
|
+
return url;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (url.startsWith("/") || url.startsWith(".")) {
|
|
1053
|
+
console.warn(
|
|
1054
|
+
`[merchify-sdk] Relative URL "${url}" detected during SSR. URL will be resolved on the client. Consider using absolute URLs for SSR compatibility.`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
return url;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/mockup/designGeneration.ts
|
|
1061
|
+
function createDesignForPlacements(placements, providedImages, context) {
|
|
1062
|
+
const design = [];
|
|
1063
|
+
const defaultImageUrl = context?.artworkSrc || context?.defaultImageUrl || null;
|
|
1064
|
+
const imagesByPlacement = /* @__PURE__ */ new Map();
|
|
1065
|
+
if (providedImages) {
|
|
1066
|
+
providedImages.forEach((img) => {
|
|
1067
|
+
if (img.placement) {
|
|
1068
|
+
imagesByPlacement.set(img.placement, img);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
const selection = context?.selection || {};
|
|
1073
|
+
for (const placement of placements) {
|
|
1074
|
+
const providedImage = imagesByPlacement.get(placement.label);
|
|
1075
|
+
if (placement.type === "color") {
|
|
1076
|
+
let selectedColor = DEFAULT_COLOR;
|
|
1077
|
+
if (selection[placement.label]) {
|
|
1078
|
+
const selectedValue = selection[placement.label];
|
|
1079
|
+
if (typeof selectedValue === "string" && selectedValue.startsWith("#")) {
|
|
1080
|
+
selectedColor = selectedValue;
|
|
1081
|
+
} else {
|
|
1082
|
+
const optionAttr = context?.attributes?.[placement.label];
|
|
1083
|
+
if (optionAttr?.choices) {
|
|
1084
|
+
const choice = optionAttr.choices.find(
|
|
1085
|
+
(c) => typeof c === "object" && c.label === selectedValue
|
|
1086
|
+
);
|
|
1087
|
+
if (choice && typeof choice === "object" && "hex" in choice) {
|
|
1088
|
+
selectedColor = choice.hex;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
design.push({
|
|
1094
|
+
type: "color",
|
|
1095
|
+
hex: providedImage?.text || selectedColor,
|
|
1096
|
+
imageUrl: "",
|
|
1097
|
+
// Empty string for color type
|
|
1098
|
+
placement: placement.label,
|
|
1099
|
+
width: placement.width || 0,
|
|
1100
|
+
height: placement.height || 0,
|
|
1101
|
+
alignment: "center"
|
|
1102
|
+
});
|
|
1103
|
+
} else {
|
|
1104
|
+
const placementDesign = context?.placementDesigns?.[placement.label];
|
|
1105
|
+
const rawImageUrl = placementDesign?.imageUrl || providedImage?.imageUrl || defaultImageUrl;
|
|
1106
|
+
const alignment = placementDesign?.alignment || providedImage?.alignment || "center";
|
|
1107
|
+
const tiles = placementDesign?.tiles || providedImage?.tiles || context?.tiles;
|
|
1108
|
+
const imageUrl = normalizeImageUrl(rawImageUrl);
|
|
1109
|
+
if (imageUrl) {
|
|
1110
|
+
const designElement = {
|
|
1111
|
+
type: "image",
|
|
1112
|
+
imageUrl,
|
|
1113
|
+
placement: placement.label,
|
|
1114
|
+
width: placement.width || DEFAULT_PLACEMENT_DIMENSIONS.width,
|
|
1115
|
+
height: placement.height || DEFAULT_PLACEMENT_DIMENSIONS.height,
|
|
1116
|
+
alignment
|
|
1117
|
+
};
|
|
1118
|
+
if (tiles && [0.25, 0.5, 1, 2, 4].includes(tiles)) {
|
|
1119
|
+
designElement.tiles = tiles;
|
|
1120
|
+
}
|
|
1121
|
+
design.push(designElement);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (placements.length === 0 && providedImages && providedImages.length > 0) {
|
|
1126
|
+
return providedImages.map((img) => ({
|
|
1127
|
+
type: img.imageUrl ? "image" : "color",
|
|
1128
|
+
imageUrl: normalizeImageUrl(img.imageUrl) || "",
|
|
1129
|
+
hex: img.text || DEFAULT_COLOR,
|
|
1130
|
+
placement: img.placement || "Front",
|
|
1131
|
+
width: img.size?.width || DEFAULT_PLACEMENT_DIMENSIONS.width,
|
|
1132
|
+
height: img.size?.height || DEFAULT_PLACEMENT_DIMENSIONS.height,
|
|
1133
|
+
alignment: img.alignment || "center"
|
|
1134
|
+
}));
|
|
1135
|
+
}
|
|
1136
|
+
if (placements.length === 0 && (!providedImages || providedImages.length === 0)) {
|
|
1137
|
+
const normalizedDefaultUrl = normalizeImageUrl(defaultImageUrl);
|
|
1138
|
+
if (normalizedDefaultUrl) {
|
|
1139
|
+
return [{
|
|
1140
|
+
type: "image",
|
|
1141
|
+
imageUrl: normalizedDefaultUrl,
|
|
1142
|
+
placement: "Front",
|
|
1143
|
+
width: DEFAULT_PLACEMENT_DIMENSIONS.width,
|
|
1144
|
+
height: DEFAULT_PLACEMENT_DIMENSIONS.height,
|
|
1145
|
+
alignment: "center"
|
|
1146
|
+
}];
|
|
1147
|
+
}
|
|
1148
|
+
return [];
|
|
1149
|
+
}
|
|
1150
|
+
return design;
|
|
1151
|
+
}
|
|
1152
|
+
function filterImagePlacements(placements) {
|
|
1153
|
+
return placements.filter((p) => p.type === "image" || p.type === null);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/mockup/urlGenerator.ts
|
|
1157
|
+
var cachedMockupConfig = null;
|
|
1158
|
+
function resolveDefaultConfig() {
|
|
1159
|
+
const DEFAULT_MOCKUP_URL = "https://MOCKUP_URL_NOT_CONFIGURED.invalid";
|
|
1160
|
+
const DEFAULT_ACCOUNT_ID = "ACCOUNT_ID_NOT_CONFIGURED";
|
|
1161
|
+
const winConfig = typeof window !== "undefined" && window.merchify || {};
|
|
1162
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1163
|
+
const envEndpoint = env?.CATALOG_API_BASE_URL;
|
|
1164
|
+
const envImageUrl = env?.MERCHIFY_IMAGE_URL || env?.NEXT_PUBLIC_MERCH_MOCKUP_URL;
|
|
1165
|
+
const envSignerUrl = env?.MERCHIFY_SIGNER_URL || env?.NEXT_PUBLIC_MERCH_SIGNER_URL;
|
|
1166
|
+
const envAccountId = env?.MERCHIFY_ACCOUNT_ID || env?.NEXT_PUBLIC_MERCH_ACCOUNT_ID;
|
|
1167
|
+
return {
|
|
1168
|
+
endpoint: winConfig.endpoint || envEndpoint || void 0,
|
|
1169
|
+
mockupUrl: winConfig.mockupUrl || envImageUrl || DEFAULT_MOCKUP_URL,
|
|
1170
|
+
signerUrl: winConfig.signerUrl || envSignerUrl || void 0,
|
|
1171
|
+
accountId: winConfig.accountId || envAccountId || DEFAULT_ACCOUNT_ID,
|
|
1172
|
+
mode: winConfig.mode || "mock"
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
var localhostWarningShown = false;
|
|
1176
|
+
function config(overrides) {
|
|
1177
|
+
if (overrides) {
|
|
1178
|
+
const base = resolveDefaultConfig();
|
|
1179
|
+
cachedMockupConfig = { ...base, ...overrides };
|
|
1180
|
+
} else if (!cachedMockupConfig) {
|
|
1181
|
+
cachedMockupConfig = resolveDefaultConfig();
|
|
1182
|
+
}
|
|
1183
|
+
const hostname = typeof window !== "undefined" ? window.location.hostname : "";
|
|
1184
|
+
const isLocalDev = hostname === "localhost" || hostname === "127.0.0.1" || hostname.includes("192.168.") || hostname.endsWith(".local") || hostname.endsWith(".lvh.me") || hostname === "lvh.me";
|
|
1185
|
+
if (!localhostWarningShown && typeof window !== "undefined" && !isLocalDev && cachedMockupConfig?.mockupUrl?.includes("localhost")) {
|
|
1186
|
+
console.error(
|
|
1187
|
+
`[@snowcone-app/sdk] WARNING: Using localhost mockup URL in production environment.
|
|
1188
|
+
mockupUrl: ${cachedMockupConfig.mockupUrl}
|
|
1189
|
+
This was likely caused by building with .env.local present.
|
|
1190
|
+
Redeploy with "pnpm run deploy" to fix.`
|
|
1191
|
+
);
|
|
1192
|
+
localhostWarningShown = true;
|
|
1193
|
+
}
|
|
1194
|
+
return cachedMockupConfig;
|
|
1195
|
+
}
|
|
1196
|
+
var resolveMockupConfig = config;
|
|
1197
|
+
function normalizeDesignElements(design) {
|
|
1198
|
+
return design.map((element) => {
|
|
1199
|
+
const normalized = {};
|
|
1200
|
+
const keys = Object.keys(element).sort();
|
|
1201
|
+
for (const key of keys) {
|
|
1202
|
+
if (key === "width" || key === "height" || key === "type") {
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (key === "alignment" && element[key] === "center") {
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const value = element[key];
|
|
1209
|
+
if (value !== void 0 && value !== null) {
|
|
1210
|
+
normalized[key] = value;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return normalized;
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function mockupUrl(options) {
|
|
1217
|
+
const mockupConfig = config();
|
|
1218
|
+
const mockupBaseUrl = mockupConfig.mockupUrl || "https://MOCKUP_URL_NOT_CONFIGURED.invalid";
|
|
1219
|
+
const accountId = mockupConfig.accountId || "ACCOUNT_ID_NOT_CONFIGURED";
|
|
1220
|
+
const normalizedDesign = normalizeDesignElements(options.design);
|
|
1221
|
+
const designBase64 = btoa(JSON.stringify(normalizedDesign));
|
|
1222
|
+
const encodedDesign = encodeURIComponent(designBase64);
|
|
1223
|
+
let queryString = `productId=${encodeURIComponent(options.productId)}`;
|
|
1224
|
+
queryString += `&mockupId=${encodeURIComponent(options.mockupId)}`;
|
|
1225
|
+
queryString += `&variantId=${encodeURIComponent(options.variantId)}`;
|
|
1226
|
+
queryString += `&design=${encodedDesign}`;
|
|
1227
|
+
queryString += `&width=${options.width}`;
|
|
1228
|
+
queryString += `&accountId=${encodeURIComponent(accountId)}`;
|
|
1229
|
+
if (options.effects?.grain) {
|
|
1230
|
+
queryString += `&grain=${options.effects.grain}`;
|
|
1231
|
+
}
|
|
1232
|
+
if (options.ar && options.ar !== "16:9") {
|
|
1233
|
+
queryString += `&ar=${encodeURIComponent(options.ar)}`;
|
|
1234
|
+
}
|
|
1235
|
+
if (options.maskOverrides && options.maskOverrides.length > 0) {
|
|
1236
|
+
const maskOverridesBase64 = btoa(JSON.stringify(options.maskOverrides));
|
|
1237
|
+
queryString += `&maskOverrides=${encodeURIComponent(maskOverridesBase64)}`;
|
|
1238
|
+
}
|
|
1239
|
+
if (mockupConfig.mode === "mock") {
|
|
1240
|
+
const mockSignature = "bypass-sig-for-k6-load-test";
|
|
1241
|
+
queryString += `&sig=${encodeURIComponent(mockSignature)}`;
|
|
1242
|
+
} else {
|
|
1243
|
+
console.warn("Live mode signature generation not implemented in mockupUrl");
|
|
1244
|
+
}
|
|
1245
|
+
return `${mockupBaseUrl}/mockup?${queryString}`;
|
|
1246
|
+
}
|
|
1247
|
+
function getVariant(selection, product) {
|
|
1248
|
+
const combination = findBestCombination(
|
|
1249
|
+
selection,
|
|
1250
|
+
product.combinations,
|
|
1251
|
+
product.attributes
|
|
1252
|
+
);
|
|
1253
|
+
const variantId = combination?.variantId;
|
|
1254
|
+
let mockupId;
|
|
1255
|
+
if (variantId && product.mockups && product.mockups.length > 0) {
|
|
1256
|
+
const mockupForVariant = product.mockups.find(
|
|
1257
|
+
(m) => m.gvids && m.gvids.includes(variantId)
|
|
1258
|
+
);
|
|
1259
|
+
mockupId = mockupForVariant?.id || product.mockups[0].id;
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
variantId,
|
|
1263
|
+
mockupId,
|
|
1264
|
+
price: combination?.price,
|
|
1265
|
+
combination
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function resolveVariantId(context, providedGvid, providedVariantId) {
|
|
1269
|
+
if (providedGvid) return providedGvid;
|
|
1270
|
+
if (providedVariantId) return providedVariantId;
|
|
1271
|
+
if (context?.combinations && context.combinations.length > 0 && context?.selection) {
|
|
1272
|
+
const best = findBestCombination(
|
|
1273
|
+
context.selection,
|
|
1274
|
+
context.combinations,
|
|
1275
|
+
context.optionAttributes || context.attributes
|
|
1276
|
+
);
|
|
1277
|
+
if (best?.variantId) {
|
|
1278
|
+
return best.variantId;
|
|
1279
|
+
} else if (context.combinations[0]?.variantId) {
|
|
1280
|
+
return context.combinations[0].variantId;
|
|
1281
|
+
}
|
|
1282
|
+
} else if (context?.combinations && context.combinations.length > 0) {
|
|
1283
|
+
return context.combinations[0].variantId || "default";
|
|
1284
|
+
} else if (context?.product?.variants && context.product.variants.length > 0) {
|
|
1285
|
+
return context.product.variants[0].gvid || "default";
|
|
1286
|
+
}
|
|
1287
|
+
if (context?.product) {
|
|
1288
|
+
const defaultGvid = context.product.defaultGvid;
|
|
1289
|
+
const firstVariant = context.product.variants?.[0];
|
|
1290
|
+
return defaultGvid || firstVariant?.gvid || "default";
|
|
1291
|
+
}
|
|
1292
|
+
return "default";
|
|
1293
|
+
}
|
|
1294
|
+
function resolveMockupId(context, variantId, providedMockupId) {
|
|
1295
|
+
if (providedMockupId) return providedMockupId;
|
|
1296
|
+
if (context?.product?.mockups?.length) {
|
|
1297
|
+
if (variantId) {
|
|
1298
|
+
const mockupForVariant = context.product.mockups.find(
|
|
1299
|
+
(m) => m.gvids && m.gvids.includes(variantId)
|
|
1300
|
+
);
|
|
1301
|
+
if (mockupForVariant) {
|
|
1302
|
+
return mockupForVariant.id;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return context.product.mockups[0].id;
|
|
1306
|
+
}
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// src/product/dataTransformation.ts
|
|
1311
|
+
function toOptionAttributes(product) {
|
|
1312
|
+
const attrs = {};
|
|
1313
|
+
const src = product?.options?.attributes;
|
|
1314
|
+
if (src && typeof src === "object") {
|
|
1315
|
+
for (const key of Object.keys(src)) {
|
|
1316
|
+
const a = src[key] || {};
|
|
1317
|
+
const choices = Array.isArray(a.choices) ? a.choices.map((c) => {
|
|
1318
|
+
if (typeof c === "string") {
|
|
1319
|
+
return { label: c };
|
|
1320
|
+
}
|
|
1321
|
+
return c;
|
|
1322
|
+
}) : Array.isArray(a) ? a.map((x) => typeof x === "string" ? { label: x } : x) : [];
|
|
1323
|
+
attrs[key] = {
|
|
1324
|
+
key,
|
|
1325
|
+
affectsCombinations: a.affectsCombinations !== false,
|
|
1326
|
+
choices,
|
|
1327
|
+
type: a.type
|
|
1328
|
+
// Preserve the type (e.g., 'color-picker')
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return attrs;
|
|
1333
|
+
}
|
|
1334
|
+
function toCombinations(product) {
|
|
1335
|
+
const combos = product?.options?.combinations || [];
|
|
1336
|
+
if (Array.isArray(combos)) return combos;
|
|
1337
|
+
return [];
|
|
1338
|
+
}
|
|
1339
|
+
function extractProductId(product) {
|
|
1340
|
+
if (!product) return void 0;
|
|
1341
|
+
return product.id || product.productId || void 0;
|
|
1342
|
+
}
|
|
1343
|
+
function getDefaultVariantId(product) {
|
|
1344
|
+
if (!product) return "default";
|
|
1345
|
+
const defaultGvid = product.defaultGvid;
|
|
1346
|
+
const firstVariant = product.variants?.[0];
|
|
1347
|
+
return defaultGvid || firstVariant?.gvid || "default";
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/product/validation.ts
|
|
1351
|
+
function validateProductSelection(attributes, selection, combinations) {
|
|
1352
|
+
if (!attributes || Object.keys(attributes).length === 0) {
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
const allRequiredSelected = Object.keys(attributes).every((key) => {
|
|
1356
|
+
const attr = attributes[key];
|
|
1357
|
+
if (attr.optional) return true;
|
|
1358
|
+
const hasSelection = selection[key] && selection[key] !== "";
|
|
1359
|
+
return hasSelection;
|
|
1360
|
+
});
|
|
1361
|
+
if (!allRequiredSelected) {
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
if (!combinations || combinations.length === 0) {
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
1367
|
+
return combinations.some((combination) => {
|
|
1368
|
+
return Object.entries(selection).every(([key, value]) => {
|
|
1369
|
+
if (attributes[key] && attributes[key].affectsCombinations === false) {
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
return !value || value === "" || combination[key] === value;
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
function getMissingSelections(attributes, selection) {
|
|
1377
|
+
const missing = [];
|
|
1378
|
+
Object.keys(attributes).forEach((key) => {
|
|
1379
|
+
const attr = attributes[key];
|
|
1380
|
+
if (attr.optional) return;
|
|
1381
|
+
if (!selection[key] || selection[key] === "") {
|
|
1382
|
+
missing.push(key);
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
return missing;
|
|
1386
|
+
}
|
|
1387
|
+
function isSelectionComplete(attributes, selection) {
|
|
1388
|
+
return getMissingSelections(attributes, selection).length === 0;
|
|
1389
|
+
}
|
|
1390
|
+
function getIncompleteSelectionMessage(attributes, selection) {
|
|
1391
|
+
const missing = getMissingSelections(attributes, selection);
|
|
1392
|
+
if (missing.length === 0) {
|
|
1393
|
+
return "";
|
|
1394
|
+
}
|
|
1395
|
+
if (missing.length === 1) {
|
|
1396
|
+
return `Please select ${missing[0]}`;
|
|
1397
|
+
}
|
|
1398
|
+
const lastItem = missing.pop();
|
|
1399
|
+
return `Please select ${missing.join(", ")} and ${lastItem}`;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// src/validation/mockup.ts
|
|
1403
|
+
function validateImageUrl(url) {
|
|
1404
|
+
const errors = [];
|
|
1405
|
+
if (!url) {
|
|
1406
|
+
errors.push({
|
|
1407
|
+
field: "imageUrl",
|
|
1408
|
+
message: "Image URL is required",
|
|
1409
|
+
value: url
|
|
1410
|
+
});
|
|
1411
|
+
} else if (
|
|
1412
|
+
// Allow absolute URLs
|
|
1413
|
+
!url.startsWith("http://") && !url.startsWith("https://") && // Allow data URLs
|
|
1414
|
+
!url.startsWith("data:") && // Allow protocol-relative URLs
|
|
1415
|
+
!url.startsWith("//") && // Allow relative URLs (will be normalized later)
|
|
1416
|
+
!url.startsWith("/") && !url.startsWith("./") && !url.startsWith("../")
|
|
1417
|
+
) {
|
|
1418
|
+
errors.push({
|
|
1419
|
+
field: "imageUrl",
|
|
1420
|
+
message: "Image URL must be absolute (http://, https://), relative (/, ./, ../), protocol-relative (//), or a data URL",
|
|
1421
|
+
value: url
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
valid: errors.length === 0,
|
|
1426
|
+
errors
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
function validateAlignment(alignment) {
|
|
1430
|
+
const errors = [];
|
|
1431
|
+
const validAlignments = [
|
|
1432
|
+
"center",
|
|
1433
|
+
"top",
|
|
1434
|
+
"far-top",
|
|
1435
|
+
"bottom",
|
|
1436
|
+
"far-bottom",
|
|
1437
|
+
"left",
|
|
1438
|
+
"far-left",
|
|
1439
|
+
"right",
|
|
1440
|
+
"far-right"
|
|
1441
|
+
];
|
|
1442
|
+
if (alignment && !validAlignments.includes(alignment)) {
|
|
1443
|
+
errors.push({
|
|
1444
|
+
field: "alignment",
|
|
1445
|
+
message: `Alignment must be one of: ${validAlignments.join(", ")}`,
|
|
1446
|
+
value: alignment
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
return {
|
|
1450
|
+
valid: errors.length === 0,
|
|
1451
|
+
errors
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
function validateTileCount(tiles) {
|
|
1455
|
+
const errors = [];
|
|
1456
|
+
const validTiles = [0.25, 0.5, 1, 2, 4];
|
|
1457
|
+
if (tiles && !validTiles.includes(tiles)) {
|
|
1458
|
+
errors.push({
|
|
1459
|
+
field: "tiles",
|
|
1460
|
+
message: `Tile count must be one of: ${validTiles.join(", ")}`,
|
|
1461
|
+
value: tiles
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return {
|
|
1465
|
+
valid: errors.length === 0,
|
|
1466
|
+
errors
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function validateDesignElement(element) {
|
|
1470
|
+
const errors = [];
|
|
1471
|
+
if (!element.placement) {
|
|
1472
|
+
errors.push({
|
|
1473
|
+
field: "placement",
|
|
1474
|
+
message: "Placement is required for design element",
|
|
1475
|
+
value: element
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
if (element.type === "image") {
|
|
1479
|
+
if (!element.imageUrl) {
|
|
1480
|
+
errors.push({
|
|
1481
|
+
field: "imageUrl",
|
|
1482
|
+
message: "Image URL is required for image type",
|
|
1483
|
+
value: element
|
|
1484
|
+
});
|
|
1485
|
+
} else {
|
|
1486
|
+
const urlValidation = validateImageUrl(element.imageUrl);
|
|
1487
|
+
errors.push(...urlValidation.errors);
|
|
1488
|
+
}
|
|
1489
|
+
if (element.tiles) {
|
|
1490
|
+
const tilesValidation = validateTileCount(element.tiles);
|
|
1491
|
+
errors.push(...tilesValidation.errors);
|
|
1492
|
+
}
|
|
1493
|
+
} else if (element.type === "color") {
|
|
1494
|
+
if (!element.hex) {
|
|
1495
|
+
errors.push({
|
|
1496
|
+
field: "hex",
|
|
1497
|
+
message: "Hex color is required for color type",
|
|
1498
|
+
value: element
|
|
1499
|
+
});
|
|
1500
|
+
} else if (!/^#[0-9A-Fa-f]{6}$/.test(element.hex)) {
|
|
1501
|
+
errors.push({
|
|
1502
|
+
field: "hex",
|
|
1503
|
+
message: "Hex color must be in format #RRGGBB",
|
|
1504
|
+
value: element.hex
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (element.alignment) {
|
|
1509
|
+
const alignmentValidation = validateAlignment(element.alignment);
|
|
1510
|
+
errors.push(...alignmentValidation.errors);
|
|
1511
|
+
}
|
|
1512
|
+
if (element.width && element.width <= 0) {
|
|
1513
|
+
errors.push({
|
|
1514
|
+
field: "width",
|
|
1515
|
+
message: "Width must be greater than 0",
|
|
1516
|
+
value: element.width
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
if (element.height && element.height <= 0) {
|
|
1520
|
+
errors.push({
|
|
1521
|
+
field: "height",
|
|
1522
|
+
message: "Height must be greater than 0",
|
|
1523
|
+
value: element.height
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
return {
|
|
1527
|
+
valid: errors.length === 0,
|
|
1528
|
+
errors
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function validateProductSpec(spec) {
|
|
1532
|
+
const errors = [];
|
|
1533
|
+
if (!spec.productId) {
|
|
1534
|
+
errors.push({
|
|
1535
|
+
field: "productId",
|
|
1536
|
+
message: "Product ID is required",
|
|
1537
|
+
value: spec
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
if (!spec.mockupId) {
|
|
1541
|
+
errors.push({
|
|
1542
|
+
field: "mockupId",
|
|
1543
|
+
message: "Mockup ID is required",
|
|
1544
|
+
value: spec
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
if (!spec.variantId) {
|
|
1548
|
+
errors.push({
|
|
1549
|
+
field: "variantId",
|
|
1550
|
+
message: "Variant ID is required",
|
|
1551
|
+
value: spec
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
if (!spec.width || spec.width <= 0) {
|
|
1555
|
+
errors.push({
|
|
1556
|
+
field: "width",
|
|
1557
|
+
message: "Width must be greater than 0",
|
|
1558
|
+
value: spec.width
|
|
1559
|
+
});
|
|
1560
|
+
} else if (spec.width > 4e3) {
|
|
1561
|
+
errors.push({
|
|
1562
|
+
field: "width",
|
|
1563
|
+
message: "Width cannot exceed 4000 pixels",
|
|
1564
|
+
value: spec.width
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
valid: errors.length === 0,
|
|
1569
|
+
errors
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function validateEffects(effects) {
|
|
1573
|
+
const errors = [];
|
|
1574
|
+
if (effects.grain !== void 0) {
|
|
1575
|
+
if (effects.grain !== 1 && effects.grain !== 2) {
|
|
1576
|
+
errors.push({
|
|
1577
|
+
field: "grain",
|
|
1578
|
+
message: "Grain effect must be 1 (light) or 2 (heavy)",
|
|
1579
|
+
value: effects.grain
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return {
|
|
1584
|
+
valid: errors.length === 0,
|
|
1585
|
+
errors
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
function validateMockupOptions(options) {
|
|
1589
|
+
const errors = [];
|
|
1590
|
+
const productValidation = validateProductSpec(options.product);
|
|
1591
|
+
errors.push(...productValidation.errors);
|
|
1592
|
+
if (!options.design || options.design.length === 0) {
|
|
1593
|
+
errors.push({
|
|
1594
|
+
field: "design",
|
|
1595
|
+
message: "At least one design element is required",
|
|
1596
|
+
value: options.design
|
|
1597
|
+
});
|
|
1598
|
+
} else {
|
|
1599
|
+
options.design.forEach((element, index) => {
|
|
1600
|
+
const elementValidation = validateDesignElement(element);
|
|
1601
|
+
elementValidation.errors.forEach((error) => {
|
|
1602
|
+
errors.push({
|
|
1603
|
+
...error,
|
|
1604
|
+
field: `design[${index}].${error.field}`
|
|
1605
|
+
});
|
|
1606
|
+
});
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
if (options.effects) {
|
|
1610
|
+
const effectsValidation = validateEffects(options.effects);
|
|
1611
|
+
errors.push(...effectsValidation.errors);
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
valid: errors.length === 0,
|
|
1615
|
+
errors
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
function formatValidationErrors(errors) {
|
|
1619
|
+
return errors.map((e) => `${e.field}: ${e.message}`).join("\n");
|
|
1620
|
+
}
|
|
1621
|
+
function isValidAlignment(value) {
|
|
1622
|
+
const validAlignments = [
|
|
1623
|
+
"center",
|
|
1624
|
+
"top",
|
|
1625
|
+
"far-top",
|
|
1626
|
+
"bottom",
|
|
1627
|
+
"far-bottom",
|
|
1628
|
+
"left",
|
|
1629
|
+
"far-left",
|
|
1630
|
+
"right",
|
|
1631
|
+
"far-right"
|
|
1632
|
+
];
|
|
1633
|
+
return validAlignments.includes(value);
|
|
1634
|
+
}
|
|
1635
|
+
function isValidTileCount(value) {
|
|
1636
|
+
const validTiles = [0.25, 0.5, 1, 2, 4];
|
|
1637
|
+
return validTiles.includes(value);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/cart/operations.ts
|
|
1641
|
+
function createCartDetail(options) {
|
|
1642
|
+
const { productId, quantity = 1, selection, combinations, attributes } = options;
|
|
1643
|
+
if (!validateProductSelection(attributes, selection, combinations)) {
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
const bestCombination = findBestCombination(selection, combinations, attributes);
|
|
1647
|
+
return {
|
|
1648
|
+
productId,
|
|
1649
|
+
variantId: bestCombination?.variantId,
|
|
1650
|
+
quantity,
|
|
1651
|
+
selection,
|
|
1652
|
+
price: bestCombination?.price,
|
|
1653
|
+
attributes: Object.fromEntries(
|
|
1654
|
+
Object.entries(selection).map(([key, value]) => {
|
|
1655
|
+
const attr = attributes[key];
|
|
1656
|
+
return [key, {
|
|
1657
|
+
value,
|
|
1658
|
+
affectsCombinations: attr?.affectsCombinations !== false,
|
|
1659
|
+
type: attr?.type
|
|
1660
|
+
}];
|
|
1661
|
+
})
|
|
1662
|
+
)
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
function findVariantForSelection(selection, combinations, attributes) {
|
|
1666
|
+
const bestCombination = findBestCombination(selection, combinations, attributes);
|
|
1667
|
+
return bestCombination?.variantId;
|
|
1668
|
+
}
|
|
1669
|
+
function createAddToCartEvent(cartDetail) {
|
|
1670
|
+
return {
|
|
1671
|
+
detail: {
|
|
1672
|
+
productId: cartDetail.productId,
|
|
1673
|
+
variantId: cartDetail.variantId,
|
|
1674
|
+
quantity: cartDetail.quantity,
|
|
1675
|
+
selection: cartDetail.selection,
|
|
1676
|
+
price: cartDetail.price,
|
|
1677
|
+
timestamp: Date.now()
|
|
1678
|
+
},
|
|
1679
|
+
bubbles: true,
|
|
1680
|
+
composed: true,
|
|
1681
|
+
cancelable: true
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
async function simulateCartOperation(duration = 1e3) {
|
|
1685
|
+
return new Promise((resolve) => setTimeout(resolve, duration));
|
|
1686
|
+
}
|
|
1687
|
+
function createAddToCartHandler(options) {
|
|
1688
|
+
return async function handleAddToCart(productId, selection, combinations, attributes, quantity = 1) {
|
|
1689
|
+
try {
|
|
1690
|
+
if (options.onValidate && !options.onValidate()) {
|
|
1691
|
+
return null;
|
|
1692
|
+
}
|
|
1693
|
+
const cartDetail = createCartDetail({
|
|
1694
|
+
productId,
|
|
1695
|
+
quantity,
|
|
1696
|
+
selection,
|
|
1697
|
+
combinations,
|
|
1698
|
+
attributes
|
|
1699
|
+
});
|
|
1700
|
+
if (!cartDetail) {
|
|
1701
|
+
throw new Error("Invalid selection");
|
|
1702
|
+
}
|
|
1703
|
+
options.onStart?.();
|
|
1704
|
+
if (options.simulateDelay) {
|
|
1705
|
+
await simulateCartOperation(options.simulateDelay);
|
|
1706
|
+
}
|
|
1707
|
+
options.onSuccess?.(cartDetail);
|
|
1708
|
+
return cartDetail;
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
options.onError?.(error);
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/error/handling.ts
|
|
1717
|
+
var ErrorManager = class {
|
|
1718
|
+
constructor(context, onError, onClear) {
|
|
1719
|
+
this.context = context;
|
|
1720
|
+
this.onError = onError;
|
|
1721
|
+
this.onClear = onClear;
|
|
1722
|
+
}
|
|
1723
|
+
lastError = null;
|
|
1724
|
+
errorCount = 0;
|
|
1725
|
+
maxRetries = 3;
|
|
1726
|
+
handle(error) {
|
|
1727
|
+
this.lastError = error;
|
|
1728
|
+
this.errorCount++;
|
|
1729
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
|
|
1730
|
+
console.error(`[${this.context.component}] ${this.context.operation}:`, error);
|
|
1731
|
+
if (this.context.metadata) {
|
|
1732
|
+
console.error("Error metadata:", this.context.metadata);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
this.onError?.(error, this.context);
|
|
1736
|
+
}
|
|
1737
|
+
async handleAsync(error) {
|
|
1738
|
+
this.handle(error);
|
|
1739
|
+
}
|
|
1740
|
+
clear() {
|
|
1741
|
+
this.lastError = null;
|
|
1742
|
+
this.errorCount = 0;
|
|
1743
|
+
this.onClear?.();
|
|
1744
|
+
}
|
|
1745
|
+
getLastError() {
|
|
1746
|
+
return this.lastError;
|
|
1747
|
+
}
|
|
1748
|
+
canRetry() {
|
|
1749
|
+
return this.context.retryable !== false && this.errorCount < this.maxRetries;
|
|
1750
|
+
}
|
|
1751
|
+
getUserMessage() {
|
|
1752
|
+
if (this.context.userMessage) {
|
|
1753
|
+
return this.context.userMessage;
|
|
1754
|
+
}
|
|
1755
|
+
const operationMessages = {
|
|
1756
|
+
"fetch-product": "Unable to load product information",
|
|
1757
|
+
"generate-mockup": "Unable to generate product preview",
|
|
1758
|
+
"add-to-cart": "Unable to add item to cart",
|
|
1759
|
+
"update-selection": "Unable to update selection",
|
|
1760
|
+
"load-image": "Unable to load image"
|
|
1761
|
+
};
|
|
1762
|
+
return operationMessages[this.context.operation] || "An error occurred";
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
function createErrorHandler(context, callbacks) {
|
|
1766
|
+
const manager = new ErrorManager(context, callbacks?.onError, callbacks?.onClear);
|
|
1767
|
+
return {
|
|
1768
|
+
handle: (error) => manager.handle(error),
|
|
1769
|
+
handleAsync: (error) => manager.handleAsync(error),
|
|
1770
|
+
clear: () => manager.clear(),
|
|
1771
|
+
getLastError: () => manager.getLastError(),
|
|
1772
|
+
canRetry: () => manager.canRetry()
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
async function withErrorHandling(operation, errorHandler, options) {
|
|
1776
|
+
try {
|
|
1777
|
+
errorHandler.clear();
|
|
1778
|
+
return await operation();
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
errorHandler.handle(error);
|
|
1781
|
+
if (options?.retry && errorHandler.canRetry()) {
|
|
1782
|
+
if (options.retryDelay) {
|
|
1783
|
+
await new Promise((resolve) => setTimeout(resolve, options.retryDelay));
|
|
1784
|
+
}
|
|
1785
|
+
return withErrorHandling(operation, errorHandler, {
|
|
1786
|
+
...options,
|
|
1787
|
+
retry: false
|
|
1788
|
+
// Prevent infinite recursion
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
if (options?.fallback !== void 0) {
|
|
1792
|
+
return options.fallback;
|
|
1793
|
+
}
|
|
1794
|
+
return null;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function withSyncErrorHandling(operation, errorHandler, fallback) {
|
|
1798
|
+
try {
|
|
1799
|
+
errorHandler.clear();
|
|
1800
|
+
return operation();
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
errorHandler.handle(error);
|
|
1803
|
+
return fallback !== void 0 ? fallback : null;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async function retryOperation(operation, options = {}) {
|
|
1807
|
+
const maxAttempts = options.maxAttempts || 3;
|
|
1808
|
+
const baseDelay = options.delay || 1e3;
|
|
1809
|
+
let lastError;
|
|
1810
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1811
|
+
try {
|
|
1812
|
+
return await operation();
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
lastError = error;
|
|
1815
|
+
if (attempt < maxAttempts) {
|
|
1816
|
+
options.onRetry?.(attempt, lastError);
|
|
1817
|
+
const delay = options.backoff ? baseDelay * Math.pow(2, attempt - 1) : baseDelay;
|
|
1818
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
throw lastError;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/context/synchronization.ts
|
|
1826
|
+
var ContextSynchronizer = class {
|
|
1827
|
+
constructor(options = {}) {
|
|
1828
|
+
this.options = options;
|
|
1829
|
+
this.batchDelay = options.batchDelay || 0;
|
|
1830
|
+
}
|
|
1831
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
1832
|
+
currentContext;
|
|
1833
|
+
previousContext;
|
|
1834
|
+
updateTimer;
|
|
1835
|
+
pendingUpdate;
|
|
1836
|
+
batchDelay;
|
|
1837
|
+
/**
|
|
1838
|
+
* Subscribe to context changes
|
|
1839
|
+
*/
|
|
1840
|
+
subscribe(callback) {
|
|
1841
|
+
this.subscribers.add(callback);
|
|
1842
|
+
if (this.currentContext !== void 0) {
|
|
1843
|
+
callback(this.currentContext, this.previousContext);
|
|
1844
|
+
}
|
|
1845
|
+
return () => {
|
|
1846
|
+
this.subscribers.delete(callback);
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Update context with optional batching
|
|
1851
|
+
*/
|
|
1852
|
+
updateContext(context) {
|
|
1853
|
+
if (this.hasContextChanged(this.currentContext, context)) {
|
|
1854
|
+
if (this.batchDelay > 0) {
|
|
1855
|
+
this.scheduleBatchedUpdate(context);
|
|
1856
|
+
} else {
|
|
1857
|
+
this.applyContextUpdate(context);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Force immediate context update (bypasses batching)
|
|
1863
|
+
*/
|
|
1864
|
+
forceUpdate(context) {
|
|
1865
|
+
this.clearPendingUpdate();
|
|
1866
|
+
this.applyContextUpdate(context);
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Get current context
|
|
1870
|
+
*/
|
|
1871
|
+
getContext() {
|
|
1872
|
+
return this.currentContext;
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Clear all subscribers
|
|
1876
|
+
*/
|
|
1877
|
+
clear() {
|
|
1878
|
+
this.clearPendingUpdate();
|
|
1879
|
+
this.subscribers.clear();
|
|
1880
|
+
this.previousContext = this.currentContext;
|
|
1881
|
+
this.currentContext = void 0;
|
|
1882
|
+
}
|
|
1883
|
+
hasContextChanged(prev, next) {
|
|
1884
|
+
if (prev === void 0) return true;
|
|
1885
|
+
if (prev === next) return false;
|
|
1886
|
+
if (this.options.comparator) {
|
|
1887
|
+
return !this.options.comparator(prev, next);
|
|
1888
|
+
}
|
|
1889
|
+
if (typeof prev === "object" && typeof next === "object") {
|
|
1890
|
+
return !this.deepEqual(prev, next);
|
|
1891
|
+
}
|
|
1892
|
+
return prev !== next;
|
|
1893
|
+
}
|
|
1894
|
+
scheduleBatchedUpdate(context) {
|
|
1895
|
+
this.pendingUpdate = context;
|
|
1896
|
+
if (this.updateTimer) {
|
|
1897
|
+
clearTimeout(this.updateTimer);
|
|
1898
|
+
}
|
|
1899
|
+
this.updateTimer = setTimeout(() => {
|
|
1900
|
+
if (this.pendingUpdate !== void 0) {
|
|
1901
|
+
this.applyContextUpdate(this.pendingUpdate);
|
|
1902
|
+
this.pendingUpdate = void 0;
|
|
1903
|
+
}
|
|
1904
|
+
this.updateTimer = void 0;
|
|
1905
|
+
}, this.batchDelay);
|
|
1906
|
+
}
|
|
1907
|
+
clearPendingUpdate() {
|
|
1908
|
+
if (this.updateTimer) {
|
|
1909
|
+
clearTimeout(this.updateTimer);
|
|
1910
|
+
this.updateTimer = void 0;
|
|
1911
|
+
}
|
|
1912
|
+
this.pendingUpdate = void 0;
|
|
1913
|
+
}
|
|
1914
|
+
applyContextUpdate(context) {
|
|
1915
|
+
this.previousContext = this.currentContext;
|
|
1916
|
+
this.currentContext = context;
|
|
1917
|
+
if (this.options.debug) {
|
|
1918
|
+
}
|
|
1919
|
+
this.options.onUpdate?.(context);
|
|
1920
|
+
this.subscribers.forEach((subscriber) => {
|
|
1921
|
+
try {
|
|
1922
|
+
subscriber(context, this.previousContext);
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
console.error("[ContextSynchronizer] Subscriber error:", error);
|
|
1925
|
+
}
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
deepEqual(obj1, obj2) {
|
|
1929
|
+
if (obj1 === obj2) return true;
|
|
1930
|
+
if (obj1 == null || obj2 == null) return false;
|
|
1931
|
+
if (typeof obj1 !== "object" || typeof obj2 !== "object") return false;
|
|
1932
|
+
const keys1 = Object.keys(obj1);
|
|
1933
|
+
const keys2 = Object.keys(obj2);
|
|
1934
|
+
if (keys1.length !== keys2.length) return false;
|
|
1935
|
+
for (const key of keys1) {
|
|
1936
|
+
if (!keys2.includes(key)) return false;
|
|
1937
|
+
if (!this.deepEqual(obj1[key], obj2[key])) return false;
|
|
1938
|
+
}
|
|
1939
|
+
return true;
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
var ContextBridge = class {
|
|
1943
|
+
synchronizers = /* @__PURE__ */ new Map();
|
|
1944
|
+
/**
|
|
1945
|
+
* Register a synchronizer with a key
|
|
1946
|
+
*/
|
|
1947
|
+
register(key, synchronizer) {
|
|
1948
|
+
this.synchronizers.set(key, synchronizer);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Bridge context updates between synchronizers
|
|
1952
|
+
*/
|
|
1953
|
+
bridge(fromKey, toKey) {
|
|
1954
|
+
const from = this.synchronizers.get(fromKey);
|
|
1955
|
+
const to = this.synchronizers.get(toKey);
|
|
1956
|
+
if (!from || !to) {
|
|
1957
|
+
throw new Error(`Cannot bridge: synchronizer not found`);
|
|
1958
|
+
}
|
|
1959
|
+
return from.subscribe((context) => {
|
|
1960
|
+
to.updateContext(context);
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Broadcast context to all registered synchronizers
|
|
1965
|
+
*/
|
|
1966
|
+
broadcast(context, excludeKey) {
|
|
1967
|
+
this.synchronizers.forEach((sync, key) => {
|
|
1968
|
+
if (key !== excludeKey) {
|
|
1969
|
+
sync.updateContext(context);
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
function createContextProvider(initialContext, options) {
|
|
1975
|
+
const synchronizer = new ContextSynchronizer(options);
|
|
1976
|
+
if (initialContext) {
|
|
1977
|
+
synchronizer.updateContext(initialContext);
|
|
1978
|
+
}
|
|
1979
|
+
return {
|
|
1980
|
+
subscribe: (callback) => synchronizer.subscribe(callback),
|
|
1981
|
+
update: (context) => synchronizer.updateContext(context),
|
|
1982
|
+
get: () => synchronizer.getContext(),
|
|
1983
|
+
clear: () => synchronizer.clear()
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/framework/events.ts
|
|
1988
|
+
var EventEmitter = class {
|
|
1989
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1990
|
+
onceListeners = /* @__PURE__ */ new Map();
|
|
1991
|
+
/**
|
|
1992
|
+
* Add event listener
|
|
1993
|
+
*/
|
|
1994
|
+
on(event, listener) {
|
|
1995
|
+
const listeners = this.listeners.get(event) || /* @__PURE__ */ new Set();
|
|
1996
|
+
listeners.add(listener);
|
|
1997
|
+
this.listeners.set(event, listeners);
|
|
1998
|
+
return () => this.off(event, listener);
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Add one-time event listener
|
|
2002
|
+
*/
|
|
2003
|
+
once(event, listener) {
|
|
2004
|
+
const listeners = this.onceListeners.get(event) || /* @__PURE__ */ new Set();
|
|
2005
|
+
listeners.add(listener);
|
|
2006
|
+
this.onceListeners.set(event, listeners);
|
|
2007
|
+
return () => this.off(event, listener);
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Remove event listener
|
|
2011
|
+
*/
|
|
2012
|
+
off(event, listener) {
|
|
2013
|
+
this.listeners.get(event)?.delete(listener);
|
|
2014
|
+
this.onceListeners.get(event)?.delete(listener);
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Emit event
|
|
2018
|
+
*/
|
|
2019
|
+
emit(event, payload) {
|
|
2020
|
+
const eventPayload = {
|
|
2021
|
+
...payload,
|
|
2022
|
+
timestamp: Date.now()
|
|
2023
|
+
};
|
|
2024
|
+
const listeners = this.listeners.get(event);
|
|
2025
|
+
if (listeners) {
|
|
2026
|
+
listeners.forEach((listener) => listener(eventPayload));
|
|
2027
|
+
}
|
|
2028
|
+
const onceListeners = this.onceListeners.get(event);
|
|
2029
|
+
if (onceListeners) {
|
|
2030
|
+
onceListeners.forEach((listener) => listener(eventPayload));
|
|
2031
|
+
this.onceListeners.delete(event);
|
|
2032
|
+
}
|
|
2033
|
+
return !!(listeners?.size || onceListeners?.size);
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Remove all listeners for an event
|
|
2037
|
+
*/
|
|
2038
|
+
removeAllListeners(event) {
|
|
2039
|
+
if (event) {
|
|
2040
|
+
this.listeners.delete(event);
|
|
2041
|
+
this.onceListeners.delete(event);
|
|
2042
|
+
} else {
|
|
2043
|
+
this.listeners.clear();
|
|
2044
|
+
this.onceListeners.clear();
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Get listener count
|
|
2049
|
+
*/
|
|
2050
|
+
listenerCount(event) {
|
|
2051
|
+
const regular = this.listeners.get(event)?.size || 0;
|
|
2052
|
+
const once = this.onceListeners.get(event)?.size || 0;
|
|
2053
|
+
return regular + once;
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
var ComponentEventManager = class {
|
|
2057
|
+
emitter = new EventEmitter();
|
|
2058
|
+
definitions = /* @__PURE__ */ new Map();
|
|
2059
|
+
element;
|
|
2060
|
+
constructor(element) {
|
|
2061
|
+
this.element = element;
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Define an event
|
|
2065
|
+
*/
|
|
2066
|
+
define(definition) {
|
|
2067
|
+
this.definitions.set(definition.name, definition);
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Emit an event (works for both DOM and synthetic events)
|
|
2071
|
+
*/
|
|
2072
|
+
emit(name, detail) {
|
|
2073
|
+
const definition = this.definitions.get(name) || { name };
|
|
2074
|
+
const handled = this.emitter.emit(name, { detail });
|
|
2075
|
+
if (this.element) {
|
|
2076
|
+
const event = new CustomEvent(name, {
|
|
2077
|
+
detail,
|
|
2078
|
+
bubbles: definition.bubbles !== false,
|
|
2079
|
+
cancelable: definition.cancelable !== false,
|
|
2080
|
+
composed: definition.composed !== false
|
|
2081
|
+
});
|
|
2082
|
+
this.element.dispatchEvent(event);
|
|
2083
|
+
}
|
|
2084
|
+
return handled;
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Listen to an event
|
|
2088
|
+
*/
|
|
2089
|
+
on(event, listener) {
|
|
2090
|
+
return this.emitter.on(event, listener);
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Listen to an event once
|
|
2094
|
+
*/
|
|
2095
|
+
once(event, listener) {
|
|
2096
|
+
return this.emitter.once(event, listener);
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Remove listener
|
|
2100
|
+
*/
|
|
2101
|
+
off(event, listener) {
|
|
2102
|
+
this.emitter.off(event, listener);
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Set DOM element for event dispatching
|
|
2106
|
+
*/
|
|
2107
|
+
setElement(element) {
|
|
2108
|
+
this.element = element;
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Get all defined events
|
|
2112
|
+
*/
|
|
2113
|
+
getDefinitions() {
|
|
2114
|
+
return Array.from(this.definitions.values());
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
var StandardEvents = {
|
|
2118
|
+
// Lifecycle events
|
|
2119
|
+
mounted: { name: "mounted", bubbles: false },
|
|
2120
|
+
unmounted: { name: "unmounted", bubbles: false },
|
|
2121
|
+
updated: { name: "updated", bubbles: false },
|
|
2122
|
+
// User interaction events
|
|
2123
|
+
change: { name: "change", bubbles: true },
|
|
2124
|
+
input: { name: "input", bubbles: true },
|
|
2125
|
+
click: { name: "click", bubbles: true },
|
|
2126
|
+
focus: { name: "focus", bubbles: false },
|
|
2127
|
+
blur: { name: "blur", bubbles: false },
|
|
2128
|
+
// Product-specific events
|
|
2129
|
+
selectionChange: { name: "selection-change", bubbles: true },
|
|
2130
|
+
priceUpdate: { name: "price-update", bubbles: true },
|
|
2131
|
+
addToCart: { name: "add-to-cart", bubbles: true },
|
|
2132
|
+
variantChange: { name: "variant-change", bubbles: true },
|
|
2133
|
+
// Context events
|
|
2134
|
+
contextRequest: { name: "context-request", bubbles: true },
|
|
2135
|
+
contextProvide: { name: "context-provide", bubbles: false },
|
|
2136
|
+
contextUpdate: { name: "context-update", bubbles: true }
|
|
2137
|
+
};
|
|
2138
|
+
var EventDelegator = class {
|
|
2139
|
+
element;
|
|
2140
|
+
handlers = /* @__PURE__ */ new Map();
|
|
2141
|
+
constructor(element) {
|
|
2142
|
+
this.element = element;
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Add delegated event listener
|
|
2146
|
+
*/
|
|
2147
|
+
delegate(eventType, selector, handler) {
|
|
2148
|
+
const typeHandlers = this.handlers.get(eventType) || /* @__PURE__ */ new Map();
|
|
2149
|
+
const delegatedHandler = (event) => {
|
|
2150
|
+
const target = event.target;
|
|
2151
|
+
const matches = target.matches(selector) || target.closest(selector);
|
|
2152
|
+
if (matches) {
|
|
2153
|
+
handler({
|
|
2154
|
+
detail: event.detail,
|
|
2155
|
+
target,
|
|
2156
|
+
currentTarget: this.element,
|
|
2157
|
+
timestamp: Date.now()
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
typeHandlers.set(selector, delegatedHandler);
|
|
2162
|
+
this.handlers.set(eventType, typeHandlers);
|
|
2163
|
+
this.element.addEventListener(eventType, delegatedHandler);
|
|
2164
|
+
return () => {
|
|
2165
|
+
this.element.removeEventListener(eventType, delegatedHandler);
|
|
2166
|
+
typeHandlers.delete(selector);
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Remove all delegated listeners
|
|
2171
|
+
*/
|
|
2172
|
+
destroy() {
|
|
2173
|
+
for (const [eventType, typeHandlers] of this.handlers) {
|
|
2174
|
+
for (const handler of typeHandlers.values()) {
|
|
2175
|
+
this.element.removeEventListener(eventType, handler);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
this.handlers.clear();
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
function createEventManager(element) {
|
|
2182
|
+
return new ComponentEventManager(element);
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// src/context/product.ts
|
|
2186
|
+
var ProductContextManager = class extends EventEmitter {
|
|
2187
|
+
context = {
|
|
2188
|
+
optionAttributes: {},
|
|
2189
|
+
combinations: [],
|
|
2190
|
+
selection: {},
|
|
2191
|
+
loading: false,
|
|
2192
|
+
error: null
|
|
2193
|
+
};
|
|
2194
|
+
updateQueue = [];
|
|
2195
|
+
updateTimer = null;
|
|
2196
|
+
/**
|
|
2197
|
+
* Get current context
|
|
2198
|
+
*/
|
|
2199
|
+
getContext() {
|
|
2200
|
+
return { ...this.context };
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Set product data
|
|
2204
|
+
*/
|
|
2205
|
+
setProduct(product) {
|
|
2206
|
+
if (!product) {
|
|
2207
|
+
this.updateContext({
|
|
2208
|
+
product: void 0,
|
|
2209
|
+
optionAttributes: {},
|
|
2210
|
+
combinations: [],
|
|
2211
|
+
selection: {}
|
|
2212
|
+
});
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
const optionAttributes = toOptionAttributes(product);
|
|
2216
|
+
const combinations = toCombinations(product);
|
|
2217
|
+
const selection = this.deriveSelection(optionAttributes, combinations);
|
|
2218
|
+
this.updateContext({
|
|
2219
|
+
product,
|
|
2220
|
+
optionAttributes,
|
|
2221
|
+
combinations,
|
|
2222
|
+
selection,
|
|
2223
|
+
loading: false,
|
|
2224
|
+
error: null
|
|
2225
|
+
});
|
|
2226
|
+
this.emit("product-load", { detail: product });
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Update selection
|
|
2230
|
+
*/
|
|
2231
|
+
updateSelection(selection) {
|
|
2232
|
+
const completeSelection = this.deriveSelection(
|
|
2233
|
+
this.context.optionAttributes,
|
|
2234
|
+
this.context.combinations,
|
|
2235
|
+
selection
|
|
2236
|
+
);
|
|
2237
|
+
this.updateContext({ selection: completeSelection });
|
|
2238
|
+
this.emit("selection-change", { detail: completeSelection });
|
|
2239
|
+
}
|
|
2240
|
+
/**
|
|
2241
|
+
* Set loading state
|
|
2242
|
+
*/
|
|
2243
|
+
setLoading(loading) {
|
|
2244
|
+
this.updateContext({ loading });
|
|
2245
|
+
this.emit("loading-change", { detail: loading });
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Set error state
|
|
2249
|
+
*/
|
|
2250
|
+
setError(error) {
|
|
2251
|
+
this.updateContext({ error, loading: false });
|
|
2252
|
+
this.emit("product-error", { detail: error });
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Update context with batching
|
|
2256
|
+
*/
|
|
2257
|
+
updateContext(updates) {
|
|
2258
|
+
this.updateQueue.push(updates);
|
|
2259
|
+
if (this.updateTimer) {
|
|
2260
|
+
clearTimeout(this.updateTimer);
|
|
2261
|
+
}
|
|
2262
|
+
this.updateTimer = setTimeout(() => {
|
|
2263
|
+
this.processBatchUpdate();
|
|
2264
|
+
}, 0);
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Process batched updates
|
|
2268
|
+
*/
|
|
2269
|
+
processBatchUpdate() {
|
|
2270
|
+
const merged = {};
|
|
2271
|
+
for (const update of this.updateQueue) {
|
|
2272
|
+
Object.assign(merged, update);
|
|
2273
|
+
}
|
|
2274
|
+
this.context = { ...this.context, ...merged };
|
|
2275
|
+
this.updateQueue = [];
|
|
2276
|
+
this.updateTimer = null;
|
|
2277
|
+
this.emit("context-update", { detail: this.context });
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Derive complete selection
|
|
2281
|
+
*/
|
|
2282
|
+
deriveSelection(attributes, combinations, partial) {
|
|
2283
|
+
if (!attributes || Object.keys(attributes).length === 0) {
|
|
2284
|
+
return {};
|
|
2285
|
+
}
|
|
2286
|
+
const defaultSelection = deriveDefaultSelection(attributes, combinations);
|
|
2287
|
+
if (partial) {
|
|
2288
|
+
return { ...defaultSelection, ...partial };
|
|
2289
|
+
}
|
|
2290
|
+
return defaultSelection;
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Subscribe to context updates
|
|
2294
|
+
*/
|
|
2295
|
+
subscribe(callback) {
|
|
2296
|
+
return this.on("context-update", (event) => callback(event.detail));
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Reset context
|
|
2300
|
+
*/
|
|
2301
|
+
reset() {
|
|
2302
|
+
this.context = {
|
|
2303
|
+
optionAttributes: {},
|
|
2304
|
+
combinations: [],
|
|
2305
|
+
selection: {},
|
|
2306
|
+
loading: false,
|
|
2307
|
+
error: null
|
|
2308
|
+
};
|
|
2309
|
+
this.updateQueue = [];
|
|
2310
|
+
if (this.updateTimer) {
|
|
2311
|
+
clearTimeout(this.updateTimer);
|
|
2312
|
+
this.updateTimer = null;
|
|
2313
|
+
}
|
|
2314
|
+
this.removeAllListeners();
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
function createProductContext() {
|
|
2318
|
+
return new ProductContextManager();
|
|
2319
|
+
}
|
|
2320
|
+
var ProductLoader = class {
|
|
2321
|
+
context;
|
|
2322
|
+
fetcher;
|
|
2323
|
+
cache = /* @__PURE__ */ new Map();
|
|
2324
|
+
currentRequest;
|
|
2325
|
+
constructor(context, fetcher) {
|
|
2326
|
+
this.context = context;
|
|
2327
|
+
this.fetcher = fetcher;
|
|
2328
|
+
}
|
|
2329
|
+
/**
|
|
2330
|
+
* Load product by ID
|
|
2331
|
+
*/
|
|
2332
|
+
async loadProduct(productId, options) {
|
|
2333
|
+
const cached = this.cache.get(productId);
|
|
2334
|
+
if (cached) {
|
|
2335
|
+
this.context.setProduct(cached);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
this.context.setLoading(true);
|
|
2339
|
+
this.currentRequest = void 0;
|
|
2340
|
+
const fetchRequest = this.fetchProduct(productId, options);
|
|
2341
|
+
this.currentRequest = fetchRequest;
|
|
2342
|
+
try {
|
|
2343
|
+
await fetchRequest;
|
|
2344
|
+
} catch (error) {
|
|
2345
|
+
if (this.currentRequest === fetchRequest) {
|
|
2346
|
+
this.context.setError(error);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Fetch product
|
|
2352
|
+
*/
|
|
2353
|
+
async fetchProduct(productId, options) {
|
|
2354
|
+
if (!this.fetcher) {
|
|
2355
|
+
throw new Error("No fetcher configured");
|
|
2356
|
+
}
|
|
2357
|
+
const product = await this.fetcher.getProduct(productId, options);
|
|
2358
|
+
this.cache.set(productId, product);
|
|
2359
|
+
this.context.setProduct(product);
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Load first available product
|
|
2363
|
+
*/
|
|
2364
|
+
async loadFirstProduct(options) {
|
|
2365
|
+
this.context.setLoading(true);
|
|
2366
|
+
try {
|
|
2367
|
+
if (!this.fetcher) {
|
|
2368
|
+
throw new Error("No fetcher configured");
|
|
2369
|
+
}
|
|
2370
|
+
const { items } = await this.fetcher.listProducts(options);
|
|
2371
|
+
const product = items?.[0];
|
|
2372
|
+
if (product) {
|
|
2373
|
+
const productId = product.id || product.productId;
|
|
2374
|
+
if (productId) {
|
|
2375
|
+
this.cache.set(productId, product);
|
|
2376
|
+
}
|
|
2377
|
+
this.context.setProduct(product);
|
|
2378
|
+
} else {
|
|
2379
|
+
throw new Error("No products available");
|
|
2380
|
+
}
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
this.context.setError(error);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Clear cache
|
|
2387
|
+
*/
|
|
2388
|
+
clearCache() {
|
|
2389
|
+
this.cache.clear();
|
|
2390
|
+
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Set fetcher
|
|
2393
|
+
*/
|
|
2394
|
+
setFetcher(fetcher) {
|
|
2395
|
+
this.fetcher = fetcher;
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
function createProductLoader(context, fetcher) {
|
|
2399
|
+
return new ProductLoader(context, fetcher);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// src/context/provider.ts
|
|
2403
|
+
var UniversalContextProvider = class extends EventEmitter {
|
|
2404
|
+
contextManager;
|
|
2405
|
+
loader;
|
|
2406
|
+
config;
|
|
2407
|
+
consumers = /* @__PURE__ */ new Set();
|
|
2408
|
+
initialized = false;
|
|
2409
|
+
constructor(config2 = {}) {
|
|
2410
|
+
super();
|
|
2411
|
+
this.config = config2;
|
|
2412
|
+
this.contextManager = new ProductContextManager();
|
|
2413
|
+
this.loader = new ProductLoader(this.contextManager, config2.fetcher);
|
|
2414
|
+
this.setupSubscriptions();
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Initialize the provider
|
|
2418
|
+
*/
|
|
2419
|
+
async initialize() {
|
|
2420
|
+
if (this.initialized) return;
|
|
2421
|
+
this.initialized = true;
|
|
2422
|
+
if (this.config.autoLoad !== false) {
|
|
2423
|
+
await this.loadProduct();
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Load product data
|
|
2428
|
+
*/
|
|
2429
|
+
async loadProduct(productId) {
|
|
2430
|
+
const id = productId || this.config.productId;
|
|
2431
|
+
if (id) {
|
|
2432
|
+
await this.loader.loadProduct(id, {
|
|
2433
|
+
baseUrl: this.config.endpoint,
|
|
2434
|
+
mode: this.config.mode
|
|
2435
|
+
});
|
|
2436
|
+
} else {
|
|
2437
|
+
await this.loader.loadFirstProduct({
|
|
2438
|
+
baseUrl: this.config.endpoint,
|
|
2439
|
+
mode: this.config.mode
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* Get current context
|
|
2445
|
+
*/
|
|
2446
|
+
getContext() {
|
|
2447
|
+
return this.contextManager.getContext();
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Update selection
|
|
2451
|
+
*/
|
|
2452
|
+
updateSelection(selection) {
|
|
2453
|
+
this.contextManager.updateSelection(selection);
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Register a consumer
|
|
2457
|
+
*/
|
|
2458
|
+
registerConsumer(consumer) {
|
|
2459
|
+
this.consumers.add(consumer);
|
|
2460
|
+
const context = this.getContext();
|
|
2461
|
+
consumer.onContextUpdate(context);
|
|
2462
|
+
if (context.loading && consumer.onContextLoading) {
|
|
2463
|
+
consumer.onContextLoading(true);
|
|
2464
|
+
}
|
|
2465
|
+
if (context.error && consumer.onContextError) {
|
|
2466
|
+
consumer.onContextError(context.error);
|
|
2467
|
+
}
|
|
2468
|
+
return () => {
|
|
2469
|
+
this.consumers.delete(consumer);
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
/**
|
|
2473
|
+
* Setup internal subscriptions
|
|
2474
|
+
*/
|
|
2475
|
+
setupSubscriptions() {
|
|
2476
|
+
this.contextManager.on("context-update", (event) => {
|
|
2477
|
+
const context = event.detail;
|
|
2478
|
+
this.consumers.forEach((consumer) => {
|
|
2479
|
+
consumer.onContextUpdate(context);
|
|
2480
|
+
});
|
|
2481
|
+
this.emit("context-update", { detail: context });
|
|
2482
|
+
});
|
|
2483
|
+
this.contextManager.on("loading-change", (event) => {
|
|
2484
|
+
const loading = event.detail;
|
|
2485
|
+
this.consumers.forEach((consumer) => {
|
|
2486
|
+
if (consumer.onContextLoading) {
|
|
2487
|
+
consumer.onContextLoading(loading);
|
|
2488
|
+
}
|
|
2489
|
+
});
|
|
2490
|
+
this.emit("loading-change", { detail: loading });
|
|
2491
|
+
});
|
|
2492
|
+
this.contextManager.on("product-error", (event) => {
|
|
2493
|
+
const error = event.detail;
|
|
2494
|
+
this.consumers.forEach((consumer) => {
|
|
2495
|
+
if (consumer.onContextError) {
|
|
2496
|
+
consumer.onContextError(error);
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
this.emit("error", { detail: error });
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Destroy the provider
|
|
2504
|
+
*/
|
|
2505
|
+
destroy() {
|
|
2506
|
+
this.consumers.clear();
|
|
2507
|
+
this.contextManager.reset();
|
|
2508
|
+
this.removeAllListeners();
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
var ContextInjector = class {
|
|
2512
|
+
/**
|
|
2513
|
+
* React context injection
|
|
2514
|
+
*/
|
|
2515
|
+
static forReact(provider) {
|
|
2516
|
+
return {
|
|
2517
|
+
Provider: ({ children, value }) => {
|
|
2518
|
+
return { provider, children, value };
|
|
2519
|
+
},
|
|
2520
|
+
Consumer: ({ children }) => {
|
|
2521
|
+
return { provider, children };
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Vue context injection
|
|
2527
|
+
*/
|
|
2528
|
+
static forVue(provider) {
|
|
2529
|
+
return {
|
|
2530
|
+
install(app) {
|
|
2531
|
+
app.provide("merchifyContext", provider);
|
|
2532
|
+
},
|
|
2533
|
+
inject() {
|
|
2534
|
+
return provider;
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Svelte context injection
|
|
2540
|
+
*/
|
|
2541
|
+
static forSvelte(provider) {
|
|
2542
|
+
return {
|
|
2543
|
+
setContext(key) {
|
|
2544
|
+
return { key, provider };
|
|
2545
|
+
},
|
|
2546
|
+
getContext(key) {
|
|
2547
|
+
return provider;
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Web Components context injection
|
|
2553
|
+
*/
|
|
2554
|
+
static forWebComponents(provider) {
|
|
2555
|
+
return {
|
|
2556
|
+
// Attach to element
|
|
2557
|
+
attach(element) {
|
|
2558
|
+
element.__contextProvider = provider;
|
|
2559
|
+
element.addEventListener("merchify:request-context", (event) => {
|
|
2560
|
+
event.detail.context = provider.getContext();
|
|
2561
|
+
});
|
|
2562
|
+
},
|
|
2563
|
+
// Get from parent
|
|
2564
|
+
getFromParent(element) {
|
|
2565
|
+
let parent = element.parentElement;
|
|
2566
|
+
while (parent) {
|
|
2567
|
+
if (parent.__contextProvider) {
|
|
2568
|
+
return parent.__contextProvider.getContext();
|
|
2569
|
+
}
|
|
2570
|
+
parent = parent.parentElement;
|
|
2571
|
+
}
|
|
2572
|
+
return void 0;
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
function createUniversalProvider(config2) {
|
|
2578
|
+
return new UniversalContextProvider(config2);
|
|
2579
|
+
}
|
|
2580
|
+
function withContext(Base) {
|
|
2581
|
+
return class extends Base {
|
|
2582
|
+
context;
|
|
2583
|
+
contextUnsubscribe;
|
|
2584
|
+
connectedCallback() {
|
|
2585
|
+
super.connectedCallback?.();
|
|
2586
|
+
const provider = this.findProvider();
|
|
2587
|
+
if (provider) {
|
|
2588
|
+
this.contextUnsubscribe = provider.registerConsumer(this);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
disconnectedCallback() {
|
|
2592
|
+
super.disconnectedCallback?.();
|
|
2593
|
+
if (this.contextUnsubscribe) {
|
|
2594
|
+
this.contextUnsubscribe();
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
onContextUpdate(context) {
|
|
2598
|
+
this.context = context;
|
|
2599
|
+
this.requestUpdate?.();
|
|
2600
|
+
}
|
|
2601
|
+
onContextError(error) {
|
|
2602
|
+
console.error("Context error:", error);
|
|
2603
|
+
}
|
|
2604
|
+
onContextLoading(loading) {
|
|
2605
|
+
}
|
|
2606
|
+
findProvider() {
|
|
2607
|
+
return void 0;
|
|
2608
|
+
}
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// src/dom/utils.ts
|
|
2613
|
+
var ClassNames = {
|
|
2614
|
+
/**
|
|
2615
|
+
* Combine class names conditionally
|
|
2616
|
+
*/
|
|
2617
|
+
combine(...args) {
|
|
2618
|
+
const classes = [];
|
|
2619
|
+
for (const arg of args) {
|
|
2620
|
+
if (!arg) continue;
|
|
2621
|
+
if (typeof arg === "string") {
|
|
2622
|
+
classes.push(arg);
|
|
2623
|
+
} else if (typeof arg === "object") {
|
|
2624
|
+
for (const [className, condition] of Object.entries(arg)) {
|
|
2625
|
+
if (condition) {
|
|
2626
|
+
classes.push(className);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return classes.join(" ");
|
|
2632
|
+
},
|
|
2633
|
+
/**
|
|
2634
|
+
* Toggle a class on an element
|
|
2635
|
+
*/
|
|
2636
|
+
toggle(element, className, force) {
|
|
2637
|
+
if (force !== void 0) {
|
|
2638
|
+
if (force) {
|
|
2639
|
+
element.classList.add(className);
|
|
2640
|
+
} else {
|
|
2641
|
+
element.classList.remove(className);
|
|
2642
|
+
}
|
|
2643
|
+
} else {
|
|
2644
|
+
element.classList.toggle(className);
|
|
2645
|
+
}
|
|
2646
|
+
},
|
|
2647
|
+
/**
|
|
2648
|
+
* Add classes with prefix
|
|
2649
|
+
*/
|
|
2650
|
+
withPrefix(prefix, ...suffixes) {
|
|
2651
|
+
return suffixes.map((suffix) => `${prefix}${suffix}`).join(" ");
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
var Styles = {
|
|
2655
|
+
/**
|
|
2656
|
+
* Merge style objects
|
|
2657
|
+
*/
|
|
2658
|
+
merge(...styles) {
|
|
2659
|
+
return Object.assign({}, ...styles.filter(Boolean));
|
|
2660
|
+
},
|
|
2661
|
+
/**
|
|
2662
|
+
* Convert style object to CSS string
|
|
2663
|
+
*/
|
|
2664
|
+
toCss(style) {
|
|
2665
|
+
return Object.entries(style).map(([key, value]) => {
|
|
2666
|
+
const cssKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
2667
|
+
return `${cssKey}: ${value}`;
|
|
2668
|
+
}).join("; ");
|
|
2669
|
+
},
|
|
2670
|
+
/**
|
|
2671
|
+
* Parse CSS string to style object
|
|
2672
|
+
*/
|
|
2673
|
+
fromCss(css) {
|
|
2674
|
+
const style = {};
|
|
2675
|
+
css.split(";").forEach((rule) => {
|
|
2676
|
+
const [key, value] = rule.split(":").map((s) => s.trim());
|
|
2677
|
+
if (key && value) {
|
|
2678
|
+
const jsKey = key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2679
|
+
style[jsKey] = value;
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
return style;
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
var Attributes = {
|
|
2686
|
+
/**
|
|
2687
|
+
* Set attributes on an element
|
|
2688
|
+
*/
|
|
2689
|
+
set(element, attributes) {
|
|
2690
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
2691
|
+
if (value === false || value === null || value === void 0) {
|
|
2692
|
+
element.removeAttribute(key);
|
|
2693
|
+
} else if (value === true) {
|
|
2694
|
+
element.setAttribute(key, "");
|
|
2695
|
+
} else {
|
|
2696
|
+
element.setAttribute(key, String(value));
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2700
|
+
/**
|
|
2701
|
+
* Get all attributes as an object
|
|
2702
|
+
*/
|
|
2703
|
+
getAll(element) {
|
|
2704
|
+
const attrs = {};
|
|
2705
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
2706
|
+
const attr = element.attributes[i];
|
|
2707
|
+
attrs[attr.name] = attr.value;
|
|
2708
|
+
}
|
|
2709
|
+
return attrs;
|
|
2710
|
+
},
|
|
2711
|
+
/**
|
|
2712
|
+
* Parse JSON attribute safely
|
|
2713
|
+
*/
|
|
2714
|
+
parseJson(element, attributeName, defaultValue) {
|
|
2715
|
+
const value = element.getAttribute(attributeName);
|
|
2716
|
+
if (!value) return defaultValue;
|
|
2717
|
+
try {
|
|
2718
|
+
return JSON.parse(value);
|
|
2719
|
+
} catch {
|
|
2720
|
+
console.warn(`Failed to parse JSON attribute: ${attributeName}`);
|
|
2721
|
+
return defaultValue;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
var Elements = {
|
|
2726
|
+
/**
|
|
2727
|
+
* Create element with properties
|
|
2728
|
+
*/
|
|
2729
|
+
create(tag, props) {
|
|
2730
|
+
const element = document.createElement(tag);
|
|
2731
|
+
if (props?.className) {
|
|
2732
|
+
element.className = props.className;
|
|
2733
|
+
}
|
|
2734
|
+
if (props?.style) {
|
|
2735
|
+
Object.assign(element.style, props.style);
|
|
2736
|
+
}
|
|
2737
|
+
if (props?.attributes) {
|
|
2738
|
+
Attributes.set(element, props.attributes);
|
|
2739
|
+
}
|
|
2740
|
+
if (props?.text) {
|
|
2741
|
+
element.textContent = props.text;
|
|
2742
|
+
}
|
|
2743
|
+
if (props?.html) {
|
|
2744
|
+
element.innerHTML = props.html;
|
|
2745
|
+
}
|
|
2746
|
+
if (props?.children) {
|
|
2747
|
+
props.children.forEach((child) => {
|
|
2748
|
+
if (typeof child === "string") {
|
|
2749
|
+
element.appendChild(document.createTextNode(child));
|
|
2750
|
+
} else {
|
|
2751
|
+
element.appendChild(child);
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
return element;
|
|
2756
|
+
},
|
|
2757
|
+
/**
|
|
2758
|
+
* Create fragment with children
|
|
2759
|
+
*/
|
|
2760
|
+
fragment(...children) {
|
|
2761
|
+
const fragment = document.createDocumentFragment();
|
|
2762
|
+
children.forEach((child) => {
|
|
2763
|
+
if (typeof child === "string") {
|
|
2764
|
+
fragment.appendChild(document.createTextNode(child));
|
|
2765
|
+
} else {
|
|
2766
|
+
fragment.appendChild(child);
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
return fragment;
|
|
2770
|
+
},
|
|
2771
|
+
/**
|
|
2772
|
+
* Replace element's children
|
|
2773
|
+
*/
|
|
2774
|
+
replaceChildren(element, ...children) {
|
|
2775
|
+
element.innerHTML = "";
|
|
2776
|
+
children.forEach((child) => {
|
|
2777
|
+
if (typeof child === "string") {
|
|
2778
|
+
element.appendChild(document.createTextNode(child));
|
|
2779
|
+
} else {
|
|
2780
|
+
element.appendChild(child);
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
var Focus = {
|
|
2786
|
+
/**
|
|
2787
|
+
* Trap focus within an element
|
|
2788
|
+
*/
|
|
2789
|
+
trap(container) {
|
|
2790
|
+
const focusableSelectors = [
|
|
2791
|
+
"a[href]",
|
|
2792
|
+
"button:not([disabled])",
|
|
2793
|
+
"input:not([disabled])",
|
|
2794
|
+
"select:not([disabled])",
|
|
2795
|
+
"textarea:not([disabled])",
|
|
2796
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
2797
|
+
];
|
|
2798
|
+
const getFocusableElements = () => {
|
|
2799
|
+
return Array.from(
|
|
2800
|
+
container.querySelectorAll(focusableSelectors.join(","))
|
|
2801
|
+
);
|
|
2802
|
+
};
|
|
2803
|
+
const handleKeyDown = (event) => {
|
|
2804
|
+
if (event.key !== "Tab") return;
|
|
2805
|
+
const focusable = getFocusableElements();
|
|
2806
|
+
if (focusable.length === 0) return;
|
|
2807
|
+
const first = focusable[0];
|
|
2808
|
+
const last = focusable[focusable.length - 1];
|
|
2809
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
2810
|
+
event.preventDefault();
|
|
2811
|
+
last.focus();
|
|
2812
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
2813
|
+
event.preventDefault();
|
|
2814
|
+
first.focus();
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
container.addEventListener("keydown", handleKeyDown);
|
|
2818
|
+
return () => {
|
|
2819
|
+
container.removeEventListener("keydown", handleKeyDown);
|
|
2820
|
+
};
|
|
2821
|
+
},
|
|
2822
|
+
/**
|
|
2823
|
+
* Restore focus to previous element
|
|
2824
|
+
*/
|
|
2825
|
+
restore(previousElement) {
|
|
2826
|
+
if (previousElement && previousElement.focus) {
|
|
2827
|
+
previousElement.focus();
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
var Animations = {
|
|
2832
|
+
/**
|
|
2833
|
+
* Fade in element
|
|
2834
|
+
*/
|
|
2835
|
+
fadeIn(element, duration = 300) {
|
|
2836
|
+
return new Promise((resolve) => {
|
|
2837
|
+
element.style.opacity = "0";
|
|
2838
|
+
element.style.transition = `opacity ${duration}ms`;
|
|
2839
|
+
element.offsetHeight;
|
|
2840
|
+
element.style.opacity = "1";
|
|
2841
|
+
setTimeout(resolve, duration);
|
|
2842
|
+
});
|
|
2843
|
+
},
|
|
2844
|
+
/**
|
|
2845
|
+
* Fade out element
|
|
2846
|
+
*/
|
|
2847
|
+
fadeOut(element, duration = 300) {
|
|
2848
|
+
return new Promise((resolve) => {
|
|
2849
|
+
element.style.transition = `opacity ${duration}ms`;
|
|
2850
|
+
element.style.opacity = "0";
|
|
2851
|
+
setTimeout(resolve, duration);
|
|
2852
|
+
});
|
|
2853
|
+
},
|
|
2854
|
+
/**
|
|
2855
|
+
* Slide toggle
|
|
2856
|
+
*/
|
|
2857
|
+
slideToggle(element, duration = 300) {
|
|
2858
|
+
return new Promise((resolve) => {
|
|
2859
|
+
const isHidden = element.style.maxHeight === "0px" || !element.style.maxHeight;
|
|
2860
|
+
element.style.transition = `max-height ${duration}ms`;
|
|
2861
|
+
element.style.overflow = "hidden";
|
|
2862
|
+
if (isHidden) {
|
|
2863
|
+
element.style.maxHeight = element.scrollHeight + "px";
|
|
2864
|
+
} else {
|
|
2865
|
+
element.style.maxHeight = "0px";
|
|
2866
|
+
}
|
|
2867
|
+
setTimeout(resolve, duration);
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
|
|
2872
|
+
// src/framework/adapter.ts
|
|
2873
|
+
function createComponent(descriptor, adapter) {
|
|
2874
|
+
return adapter.render(descriptor, {});
|
|
2875
|
+
}
|
|
2876
|
+
function defineComponent(definition) {
|
|
2877
|
+
return {
|
|
2878
|
+
name: definition.name,
|
|
2879
|
+
props: definition.props || {},
|
|
2880
|
+
state: definition.state,
|
|
2881
|
+
methods: definition.methods,
|
|
2882
|
+
lifecycle: definition.lifecycle,
|
|
2883
|
+
render: (adapter) => definition.render({}, adapter)
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
var AdapterRegistry = class {
|
|
2887
|
+
adapters = /* @__PURE__ */ new Map();
|
|
2888
|
+
register(adapter) {
|
|
2889
|
+
this.adapters.set(adapter.name, adapter);
|
|
2890
|
+
}
|
|
2891
|
+
get(name) {
|
|
2892
|
+
return this.adapters.get(name);
|
|
2893
|
+
}
|
|
2894
|
+
getAll() {
|
|
2895
|
+
return Array.from(this.adapters.values());
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
var adapterRegistry = new AdapterRegistry();
|
|
2899
|
+
|
|
2900
|
+
// src/framework/lifecycle.ts
|
|
2901
|
+
var LifecycleManager = class {
|
|
2902
|
+
currentPhase = "created";
|
|
2903
|
+
hooks = /* @__PURE__ */ new Map();
|
|
2904
|
+
transitions = [];
|
|
2905
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2906
|
+
/**
|
|
2907
|
+
* Register a lifecycle hook
|
|
2908
|
+
*/
|
|
2909
|
+
addHook(hook) {
|
|
2910
|
+
const phaseHooks = this.hooks.get(hook.phase) || /* @__PURE__ */ new Set();
|
|
2911
|
+
phaseHooks.add(hook);
|
|
2912
|
+
this.hooks.set(hook.phase, phaseHooks);
|
|
2913
|
+
return () => {
|
|
2914
|
+
phaseHooks.delete(hook);
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
/**
|
|
2918
|
+
* Transition to a new lifecycle phase
|
|
2919
|
+
*/
|
|
2920
|
+
async transitionTo(phase, context) {
|
|
2921
|
+
const startTime = performance.now();
|
|
2922
|
+
const transition = {
|
|
2923
|
+
from: this.currentPhase,
|
|
2924
|
+
to: phase,
|
|
2925
|
+
timestamp: Date.now()
|
|
2926
|
+
};
|
|
2927
|
+
const phaseHooks = Array.from(this.hooks.get(phase) || []).sort((a, b) => (a.priority || 0) - (b.priority || 0));
|
|
2928
|
+
for (const hook of phaseHooks) {
|
|
2929
|
+
try {
|
|
2930
|
+
await hook.handler(context);
|
|
2931
|
+
} catch (error) {
|
|
2932
|
+
console.error(`Lifecycle hook error in ${phase}:`, error);
|
|
2933
|
+
if (phase !== "error") {
|
|
2934
|
+
await this.transitionTo("error", { error, originalPhase: phase });
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
transition.duration = performance.now() - startTime;
|
|
2939
|
+
this.transitions.push(transition);
|
|
2940
|
+
this.currentPhase = phase;
|
|
2941
|
+
this.listeners.forEach((listener) => listener(phase));
|
|
2942
|
+
}
|
|
2943
|
+
/**
|
|
2944
|
+
* Get current lifecycle phase
|
|
2945
|
+
*/
|
|
2946
|
+
getPhase() {
|
|
2947
|
+
return this.currentPhase;
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* Subscribe to lifecycle changes
|
|
2951
|
+
*/
|
|
2952
|
+
subscribe(listener) {
|
|
2953
|
+
this.listeners.add(listener);
|
|
2954
|
+
return () => this.listeners.delete(listener);
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Get lifecycle history
|
|
2958
|
+
*/
|
|
2959
|
+
getHistory() {
|
|
2960
|
+
return [...this.transitions];
|
|
2961
|
+
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Reset lifecycle state
|
|
2964
|
+
*/
|
|
2965
|
+
reset() {
|
|
2966
|
+
this.currentPhase = "created";
|
|
2967
|
+
this.transitions = [];
|
|
2968
|
+
this.hooks.clear();
|
|
2969
|
+
this.listeners.clear();
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
var ComponentLifecycle = class {
|
|
2973
|
+
manager = new LifecycleManager();
|
|
2974
|
+
mounted = false;
|
|
2975
|
+
/**
|
|
2976
|
+
* Component creation
|
|
2977
|
+
*/
|
|
2978
|
+
async create(context) {
|
|
2979
|
+
await this.manager.transitionTo("created", context);
|
|
2980
|
+
}
|
|
2981
|
+
/**
|
|
2982
|
+
* Component mounting
|
|
2983
|
+
*/
|
|
2984
|
+
async mount(context) {
|
|
2985
|
+
if (this.mounted) return;
|
|
2986
|
+
await this.manager.transitionTo("mounting", context);
|
|
2987
|
+
this.mounted = true;
|
|
2988
|
+
await this.manager.transitionTo("mounted", context);
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Component updating
|
|
2992
|
+
*/
|
|
2993
|
+
async update(context) {
|
|
2994
|
+
if (!this.mounted) return;
|
|
2995
|
+
await this.manager.transitionTo("updating", context);
|
|
2996
|
+
await this.manager.transitionTo("updated", context);
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Component unmounting
|
|
3000
|
+
*/
|
|
3001
|
+
async unmount(context) {
|
|
3002
|
+
if (!this.mounted) return;
|
|
3003
|
+
await this.manager.transitionTo("unmounting", context);
|
|
3004
|
+
this.mounted = false;
|
|
3005
|
+
await this.manager.transitionTo("unmounted", context);
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Handle error
|
|
3009
|
+
*/
|
|
3010
|
+
async error(error, context) {
|
|
3011
|
+
await this.manager.transitionTo("error", { error, ...context });
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Add lifecycle hook
|
|
3015
|
+
*/
|
|
3016
|
+
onPhase(phase, handler, priority = 0) {
|
|
3017
|
+
return this.manager.addHook({ phase, handler, priority });
|
|
3018
|
+
}
|
|
3019
|
+
/**
|
|
3020
|
+
* Subscribe to lifecycle changes
|
|
3021
|
+
*/
|
|
3022
|
+
subscribe(listener) {
|
|
3023
|
+
return this.manager.subscribe(listener);
|
|
3024
|
+
}
|
|
3025
|
+
/**
|
|
3026
|
+
* Get current phase
|
|
3027
|
+
*/
|
|
3028
|
+
getPhase() {
|
|
3029
|
+
return this.manager.getPhase();
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* Check if mounted
|
|
3033
|
+
*/
|
|
3034
|
+
isMounted() {
|
|
3035
|
+
return this.mounted;
|
|
3036
|
+
}
|
|
3037
|
+
};
|
|
3038
|
+
function createLifecycle() {
|
|
3039
|
+
return new ComponentLifecycle();
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// src/framework/properties.ts
|
|
3043
|
+
function validateType(value, type) {
|
|
3044
|
+
switch (type) {
|
|
3045
|
+
case "string":
|
|
3046
|
+
return typeof value === "string";
|
|
3047
|
+
case "number":
|
|
3048
|
+
return typeof value === "number" && !isNaN(value);
|
|
3049
|
+
case "boolean":
|
|
3050
|
+
return typeof value === "boolean";
|
|
3051
|
+
case "object":
|
|
3052
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3053
|
+
case "array":
|
|
3054
|
+
return Array.isArray(value);
|
|
3055
|
+
case "function":
|
|
3056
|
+
return typeof value === "function";
|
|
3057
|
+
case "any":
|
|
3058
|
+
return true;
|
|
3059
|
+
default:
|
|
3060
|
+
return false;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
var PropertyManager = class {
|
|
3064
|
+
schema;
|
|
3065
|
+
values = {};
|
|
3066
|
+
listeners = /* @__PURE__ */ new Map();
|
|
3067
|
+
transforms = /* @__PURE__ */ new Map();
|
|
3068
|
+
constructor(schema) {
|
|
3069
|
+
this.schema = schema;
|
|
3070
|
+
this.initializeDefaults();
|
|
3071
|
+
}
|
|
3072
|
+
/**
|
|
3073
|
+
* Initialize default values
|
|
3074
|
+
*/
|
|
3075
|
+
initializeDefaults() {
|
|
3076
|
+
for (const [key, definition] of Object.entries(this.schema)) {
|
|
3077
|
+
if (definition.default !== void 0) {
|
|
3078
|
+
this.values[key] = typeof definition.default === "function" ? definition.default() : definition.default;
|
|
3079
|
+
}
|
|
3080
|
+
if (definition.transform) {
|
|
3081
|
+
this.transforms.set(key, definition.transform);
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Set a property value
|
|
3087
|
+
*/
|
|
3088
|
+
set(key, value) {
|
|
3089
|
+
const definition = this.schema[key];
|
|
3090
|
+
if (!definition) {
|
|
3091
|
+
console.warn(`Property "${String(key)}" is not defined in schema`);
|
|
3092
|
+
return false;
|
|
3093
|
+
}
|
|
3094
|
+
if (!this.validate(key, value)) {
|
|
3095
|
+
return false;
|
|
3096
|
+
}
|
|
3097
|
+
const transformedValue = this.transform(key, value);
|
|
3098
|
+
const oldValue = this.values[key];
|
|
3099
|
+
if (oldValue === transformedValue) {
|
|
3100
|
+
return true;
|
|
3101
|
+
}
|
|
3102
|
+
this.values[key] = transformedValue;
|
|
3103
|
+
this.notify(key, transformedValue);
|
|
3104
|
+
return true;
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Get a property value
|
|
3108
|
+
*/
|
|
3109
|
+
get(key) {
|
|
3110
|
+
return this.values[key];
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Get all property values
|
|
3114
|
+
*/
|
|
3115
|
+
getAll() {
|
|
3116
|
+
return { ...this.values };
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Validate a property value
|
|
3120
|
+
*/
|
|
3121
|
+
validate(key, value) {
|
|
3122
|
+
const definition = this.schema[key];
|
|
3123
|
+
if (!definition) return false;
|
|
3124
|
+
if (definition.required && value == null) {
|
|
3125
|
+
console.error(`Property "${String(key)}" is required`);
|
|
3126
|
+
return false;
|
|
3127
|
+
}
|
|
3128
|
+
if (value == null && !definition.required) {
|
|
3129
|
+
return true;
|
|
3130
|
+
}
|
|
3131
|
+
const types = Array.isArray(definition.type) ? definition.type : [definition.type];
|
|
3132
|
+
const isValidType = types.some((type) => validateType(value, type));
|
|
3133
|
+
if (!isValidType) {
|
|
3134
|
+
console.error(`Property "${String(key)}" has invalid type`);
|
|
3135
|
+
return false;
|
|
3136
|
+
}
|
|
3137
|
+
if (definition.validator && !definition.validator(value)) {
|
|
3138
|
+
console.error(`Property "${String(key)}" failed custom validation`);
|
|
3139
|
+
return false;
|
|
3140
|
+
}
|
|
3141
|
+
return true;
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Transform a property value
|
|
3145
|
+
*/
|
|
3146
|
+
transform(key, value) {
|
|
3147
|
+
const transform = this.transforms.get(key);
|
|
3148
|
+
return transform ? transform(value) : value;
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* Subscribe to property changes
|
|
3152
|
+
*/
|
|
3153
|
+
subscribe(key, listener) {
|
|
3154
|
+
const listeners = this.listeners.get(key) || /* @__PURE__ */ new Set();
|
|
3155
|
+
listeners.add(listener);
|
|
3156
|
+
this.listeners.set(key, listeners);
|
|
3157
|
+
return () => {
|
|
3158
|
+
listeners.delete(listener);
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Subscribe to all property changes
|
|
3163
|
+
*/
|
|
3164
|
+
subscribeAll(listener) {
|
|
3165
|
+
const wrappedListeners = /* @__PURE__ */ new Map();
|
|
3166
|
+
for (const key of Object.keys(this.schema)) {
|
|
3167
|
+
const wrapped = (value) => listener(key, value);
|
|
3168
|
+
wrappedListeners.set(key, wrapped);
|
|
3169
|
+
this.subscribe(key, wrapped);
|
|
3170
|
+
}
|
|
3171
|
+
return () => {
|
|
3172
|
+
for (const [key, wrapped] of wrappedListeners) {
|
|
3173
|
+
const listeners = this.listeners.get(key);
|
|
3174
|
+
listeners?.delete(wrapped);
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* Notify listeners of property change
|
|
3180
|
+
*/
|
|
3181
|
+
notify(key, value) {
|
|
3182
|
+
const listeners = this.listeners.get(key);
|
|
3183
|
+
if (listeners) {
|
|
3184
|
+
listeners.forEach((listener) => listener(value));
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
/**
|
|
3188
|
+
* Batch update multiple properties
|
|
3189
|
+
*/
|
|
3190
|
+
update(props) {
|
|
3191
|
+
for (const [key, value] of Object.entries(props)) {
|
|
3192
|
+
this.set(key, value);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Reset to default values
|
|
3197
|
+
*/
|
|
3198
|
+
reset() {
|
|
3199
|
+
this.values = {};
|
|
3200
|
+
this.initializeDefaults();
|
|
3201
|
+
for (const [key, value] of Object.entries(this.values)) {
|
|
3202
|
+
this.notify(key, value);
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
/**
|
|
3206
|
+
* Get schema
|
|
3207
|
+
*/
|
|
3208
|
+
getSchema() {
|
|
3209
|
+
return this.schema;
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
function createPropertyManager(schema) {
|
|
3213
|
+
return new PropertyManager(schema);
|
|
3214
|
+
}
|
|
3215
|
+
var CommonProps = {
|
|
3216
|
+
className: {
|
|
3217
|
+
type: "string",
|
|
3218
|
+
attribute: "class"
|
|
3219
|
+
},
|
|
3220
|
+
style: {
|
|
3221
|
+
type: "object"
|
|
3222
|
+
},
|
|
3223
|
+
id: {
|
|
3224
|
+
type: "string",
|
|
3225
|
+
reflect: true
|
|
3226
|
+
},
|
|
3227
|
+
disabled: {
|
|
3228
|
+
type: "boolean",
|
|
3229
|
+
default: false,
|
|
3230
|
+
reflect: true
|
|
3231
|
+
},
|
|
3232
|
+
hidden: {
|
|
3233
|
+
type: "boolean",
|
|
3234
|
+
default: false,
|
|
3235
|
+
reflect: true
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
var ProductProps = {
|
|
3239
|
+
productId: {
|
|
3240
|
+
type: "string",
|
|
3241
|
+
attribute: "product-id",
|
|
3242
|
+
reflect: true
|
|
3243
|
+
},
|
|
3244
|
+
mode: {
|
|
3245
|
+
type: "string",
|
|
3246
|
+
default: "mock",
|
|
3247
|
+
validator: (value) => ["mock", "live"].includes(value)
|
|
3248
|
+
},
|
|
3249
|
+
endpoint: {
|
|
3250
|
+
type: "string"
|
|
3251
|
+
},
|
|
3252
|
+
...CommonProps
|
|
3253
|
+
};
|
|
3254
|
+
|
|
3255
|
+
// src/framework/templates.ts
|
|
3256
|
+
var TemplateBuilder = class {
|
|
3257
|
+
/**
|
|
3258
|
+
* Create an element node
|
|
3259
|
+
*/
|
|
3260
|
+
static element(tag, props, children) {
|
|
3261
|
+
return {
|
|
3262
|
+
type: "element",
|
|
3263
|
+
tag,
|
|
3264
|
+
props,
|
|
3265
|
+
children
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Create a text node
|
|
3270
|
+
*/
|
|
3271
|
+
static text(text) {
|
|
3272
|
+
return {
|
|
3273
|
+
type: "text",
|
|
3274
|
+
text
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Create a component node
|
|
3279
|
+
*/
|
|
3280
|
+
static component(component, props, children) {
|
|
3281
|
+
return {
|
|
3282
|
+
type: "component",
|
|
3283
|
+
component,
|
|
3284
|
+
props,
|
|
3285
|
+
children
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
/**
|
|
3289
|
+
* Create a fragment node
|
|
3290
|
+
*/
|
|
3291
|
+
static fragment(children) {
|
|
3292
|
+
return {
|
|
3293
|
+
type: "fragment",
|
|
3294
|
+
children
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
/**
|
|
3298
|
+
* Create a conditional node
|
|
3299
|
+
*/
|
|
3300
|
+
static if(condition, thenNode, elseNode) {
|
|
3301
|
+
const thenNodes = Array.isArray(thenNode) ? thenNode : [thenNode];
|
|
3302
|
+
const elseNodes = elseNode ? Array.isArray(elseNode) ? elseNode : [elseNode] : void 0;
|
|
3303
|
+
return {
|
|
3304
|
+
type: "conditional",
|
|
3305
|
+
condition,
|
|
3306
|
+
children: thenNodes,
|
|
3307
|
+
fallback: elseNodes
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
/**
|
|
3311
|
+
* Create a loop node
|
|
3312
|
+
*/
|
|
3313
|
+
static forEach(items, renderItem) {
|
|
3314
|
+
return {
|
|
3315
|
+
type: "loop",
|
|
3316
|
+
items,
|
|
3317
|
+
renderItem
|
|
3318
|
+
};
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* Create a slot node
|
|
3322
|
+
*/
|
|
3323
|
+
static slot(name, fallback) {
|
|
3324
|
+
return {
|
|
3325
|
+
type: "slot",
|
|
3326
|
+
name,
|
|
3327
|
+
fallback
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
var ComponentTemplates = class {
|
|
3332
|
+
/**
|
|
3333
|
+
* Product options template
|
|
3334
|
+
*/
|
|
3335
|
+
static productOptions() {
|
|
3336
|
+
return {
|
|
3337
|
+
name: "ProductOptions",
|
|
3338
|
+
props: {
|
|
3339
|
+
attributes: { type: "object", required: true },
|
|
3340
|
+
selection: { type: "object", required: true },
|
|
3341
|
+
disabled: { type: "object" },
|
|
3342
|
+
onChange: { type: "function" }
|
|
3343
|
+
},
|
|
3344
|
+
events: [
|
|
3345
|
+
{ name: "change", bubbles: true }
|
|
3346
|
+
],
|
|
3347
|
+
render: (props) => {
|
|
3348
|
+
const { attributes = {}, selection = {}, disabled = {} } = props;
|
|
3349
|
+
return TemplateBuilder.element("div", { class: "product-options" }, [
|
|
3350
|
+
TemplateBuilder.forEach(
|
|
3351
|
+
() => Object.entries(attributes),
|
|
3352
|
+
([key, attr]) => {
|
|
3353
|
+
return TemplateBuilder.element("div", { class: "option-group", key }, [
|
|
3354
|
+
TemplateBuilder.element("label", {}, [
|
|
3355
|
+
TemplateBuilder.text(attr.label || key)
|
|
3356
|
+
]),
|
|
3357
|
+
TemplateBuilder.element("select", {
|
|
3358
|
+
value: selection[key],
|
|
3359
|
+
onChange: props.onChange,
|
|
3360
|
+
"data-attribute": key
|
|
3361
|
+
}, [
|
|
3362
|
+
TemplateBuilder.forEach(
|
|
3363
|
+
() => attr.choices || [],
|
|
3364
|
+
(choice) => {
|
|
3365
|
+
const value = typeof choice === "string" ? choice : choice.label;
|
|
3366
|
+
const isDisabled = disabled[key]?.includes(value);
|
|
3367
|
+
return TemplateBuilder.element("option", {
|
|
3368
|
+
value,
|
|
3369
|
+
disabled: isDisabled
|
|
3370
|
+
}, [
|
|
3371
|
+
TemplateBuilder.text(value)
|
|
3372
|
+
]);
|
|
3373
|
+
}
|
|
3374
|
+
)
|
|
3375
|
+
])
|
|
3376
|
+
]);
|
|
3377
|
+
}
|
|
3378
|
+
)
|
|
3379
|
+
]);
|
|
3380
|
+
}
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Product price template
|
|
3385
|
+
*/
|
|
3386
|
+
static productPrice() {
|
|
3387
|
+
return {
|
|
3388
|
+
name: "ProductPrice",
|
|
3389
|
+
props: {
|
|
3390
|
+
price: { type: "number" },
|
|
3391
|
+
currency: { type: "string", default: "USD" },
|
|
3392
|
+
locale: { type: "string", default: "en-US" },
|
|
3393
|
+
showCurrency: { type: "boolean", default: true }
|
|
3394
|
+
},
|
|
3395
|
+
render: (props) => {
|
|
3396
|
+
const { price, currency, locale, showCurrency } = props;
|
|
3397
|
+
return TemplateBuilder.if(
|
|
3398
|
+
() => price != null,
|
|
3399
|
+
TemplateBuilder.element("span", { class: "product-price" }, [
|
|
3400
|
+
TemplateBuilder.text(formatPrice2(price, locale, currency, showCurrency))
|
|
3401
|
+
]),
|
|
3402
|
+
TemplateBuilder.element("span", { class: "product-price-empty" }, [
|
|
3403
|
+
TemplateBuilder.text("\u2014")
|
|
3404
|
+
])
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
/**
|
|
3410
|
+
* Product image template
|
|
3411
|
+
*/
|
|
3412
|
+
static productImage() {
|
|
3413
|
+
return {
|
|
3414
|
+
name: "ProductImage",
|
|
3415
|
+
props: {
|
|
3416
|
+
src: { type: "string" },
|
|
3417
|
+
alt: { type: "string", default: "Product image" },
|
|
3418
|
+
width: { type: "number" },
|
|
3419
|
+
height: { type: "number" },
|
|
3420
|
+
loading: { type: "string", default: "lazy" }
|
|
3421
|
+
},
|
|
3422
|
+
slots: ["loading", "error"],
|
|
3423
|
+
render: (props) => {
|
|
3424
|
+
const { src, alt, width, height, loading } = props;
|
|
3425
|
+
return TemplateBuilder.if(
|
|
3426
|
+
() => !!src,
|
|
3427
|
+
TemplateBuilder.element("img", {
|
|
3428
|
+
src,
|
|
3429
|
+
alt,
|
|
3430
|
+
width,
|
|
3431
|
+
height,
|
|
3432
|
+
loading,
|
|
3433
|
+
class: "product-image"
|
|
3434
|
+
}),
|
|
3435
|
+
TemplateBuilder.slot("loading", [
|
|
3436
|
+
TemplateBuilder.element("div", { class: "product-image-placeholder" }, [
|
|
3437
|
+
TemplateBuilder.text("Loading...")
|
|
3438
|
+
])
|
|
3439
|
+
])
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Add to cart button template
|
|
3446
|
+
*/
|
|
3447
|
+
static addToCartButton() {
|
|
3448
|
+
return {
|
|
3449
|
+
name: "AddToCartButton",
|
|
3450
|
+
props: {
|
|
3451
|
+
disabled: { type: "boolean", default: false },
|
|
3452
|
+
loading: { type: "boolean", default: false },
|
|
3453
|
+
text: { type: "string", default: "Add to Cart" },
|
|
3454
|
+
onClick: { type: "function" }
|
|
3455
|
+
},
|
|
3456
|
+
events: [
|
|
3457
|
+
{ name: "click", bubbles: true }
|
|
3458
|
+
],
|
|
3459
|
+
render: (props) => {
|
|
3460
|
+
const { disabled, loading, text, onClick } = props;
|
|
3461
|
+
return TemplateBuilder.element("button", {
|
|
3462
|
+
class: "add-to-cart-button",
|
|
3463
|
+
disabled: disabled || loading,
|
|
3464
|
+
onClick
|
|
3465
|
+
}, [
|
|
3466
|
+
TemplateBuilder.if(
|
|
3467
|
+
() => loading,
|
|
3468
|
+
TemplateBuilder.text("Adding..."),
|
|
3469
|
+
TemplateBuilder.text(text)
|
|
3470
|
+
)
|
|
3471
|
+
]);
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
var AbstractTemplateRenderer = class {
|
|
3477
|
+
adapter;
|
|
3478
|
+
constructor(adapter) {
|
|
3479
|
+
this.adapter = adapter;
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Render a template node
|
|
3483
|
+
*/
|
|
3484
|
+
render(node, context) {
|
|
3485
|
+
switch (node.type) {
|
|
3486
|
+
case "element":
|
|
3487
|
+
return this.renderElement(node, context);
|
|
3488
|
+
case "text":
|
|
3489
|
+
return this.renderText(node, context);
|
|
3490
|
+
case "component":
|
|
3491
|
+
return this.renderComponent(node, context);
|
|
3492
|
+
case "fragment":
|
|
3493
|
+
return this.renderFragment(node, context);
|
|
3494
|
+
case "conditional":
|
|
3495
|
+
return this.renderConditional(node, context);
|
|
3496
|
+
case "loop":
|
|
3497
|
+
return this.renderLoop(node, context);
|
|
3498
|
+
case "slot":
|
|
3499
|
+
return this.renderSlot(node, context);
|
|
3500
|
+
default:
|
|
3501
|
+
throw new Error(`Unknown node type: ${node.type}`);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Render child nodes
|
|
3506
|
+
*/
|
|
3507
|
+
renderChildren(children, context) {
|
|
3508
|
+
if (!children) return [];
|
|
3509
|
+
return children.map((child) => this.render(child, context));
|
|
3510
|
+
}
|
|
3511
|
+
};
|
|
3512
|
+
function formatPrice2(cents, locale, currency, showCurrency) {
|
|
3513
|
+
const amount = cents / 100;
|
|
3514
|
+
if (showCurrency) {
|
|
3515
|
+
return new Intl.NumberFormat(locale, {
|
|
3516
|
+
style: "currency",
|
|
3517
|
+
currency
|
|
3518
|
+
}).format(amount);
|
|
3519
|
+
}
|
|
3520
|
+
return new Intl.NumberFormat(locale, {
|
|
3521
|
+
minimumFractionDigits: 2,
|
|
3522
|
+
maximumFractionDigits: 2
|
|
3523
|
+
}).format(amount);
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// src/framework/registry.ts
|
|
3527
|
+
var ComponentRegistry = class {
|
|
3528
|
+
components = /* @__PURE__ */ new Map();
|
|
3529
|
+
frameworks = /* @__PURE__ */ new Map();
|
|
3530
|
+
/**
|
|
3531
|
+
* Register a component
|
|
3532
|
+
*/
|
|
3533
|
+
register(definition) {
|
|
3534
|
+
const { name, framework } = definition.metadata;
|
|
3535
|
+
this.components.set(name, definition);
|
|
3536
|
+
if (framework) {
|
|
3537
|
+
const frameworkComponents = this.frameworks.get(framework) || /* @__PURE__ */ new Set();
|
|
3538
|
+
frameworkComponents.add(name);
|
|
3539
|
+
this.frameworks.set(framework, frameworkComponents);
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Get a component definition
|
|
3544
|
+
*/
|
|
3545
|
+
get(name) {
|
|
3546
|
+
return this.components.get(name);
|
|
3547
|
+
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Get all components for a framework
|
|
3550
|
+
*/
|
|
3551
|
+
getByFramework(framework) {
|
|
3552
|
+
const names = this.frameworks.get(framework) || /* @__PURE__ */ new Set();
|
|
3553
|
+
return Array.from(names).map((name) => this.components.get(name)).filter(Boolean);
|
|
3554
|
+
}
|
|
3555
|
+
/**
|
|
3556
|
+
* Get all registered components
|
|
3557
|
+
*/
|
|
3558
|
+
getAll() {
|
|
3559
|
+
return Array.from(this.components.values());
|
|
3560
|
+
}
|
|
3561
|
+
/**
|
|
3562
|
+
* Check if a component is registered
|
|
3563
|
+
*/
|
|
3564
|
+
has(name) {
|
|
3565
|
+
return this.components.has(name);
|
|
3566
|
+
}
|
|
3567
|
+
/**
|
|
3568
|
+
* Clear registry
|
|
3569
|
+
*/
|
|
3570
|
+
clear() {
|
|
3571
|
+
this.components.clear();
|
|
3572
|
+
this.frameworks.clear();
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
var componentRegistry = new ComponentRegistry();
|
|
3576
|
+
var StandardComponents = {
|
|
3577
|
+
/**
|
|
3578
|
+
* Product component
|
|
3579
|
+
*/
|
|
3580
|
+
Product: {
|
|
3581
|
+
metadata: {
|
|
3582
|
+
name: "Product",
|
|
3583
|
+
tagName: "merchify-product",
|
|
3584
|
+
displayName: "MerchifyProduct",
|
|
3585
|
+
description: "Product context provider component",
|
|
3586
|
+
props: {
|
|
3587
|
+
productId: { type: "string" },
|
|
3588
|
+
mode: {
|
|
3589
|
+
type: "string",
|
|
3590
|
+
default: "mock",
|
|
3591
|
+
validator: (v) => ["mock", "live"].includes(v)
|
|
3592
|
+
},
|
|
3593
|
+
endpoint: { type: "string" },
|
|
3594
|
+
autoLoad: { type: "boolean", default: true }
|
|
3595
|
+
},
|
|
3596
|
+
events: [
|
|
3597
|
+
{ name: "ready", bubbles: true },
|
|
3598
|
+
{ name: "error", bubbles: true },
|
|
3599
|
+
{ name: "context-update", bubbles: true }
|
|
3600
|
+
],
|
|
3601
|
+
slots: ["default", "loading", "error"]
|
|
3602
|
+
}
|
|
3603
|
+
},
|
|
3604
|
+
/**
|
|
3605
|
+
* Product Options component
|
|
3606
|
+
*/
|
|
3607
|
+
ProductOptions: {
|
|
3608
|
+
metadata: {
|
|
3609
|
+
name: "ProductOptions",
|
|
3610
|
+
tagName: "merchify-product-options",
|
|
3611
|
+
displayName: "MerchifyProductOptions",
|
|
3612
|
+
description: "Product options selection component",
|
|
3613
|
+
props: {
|
|
3614
|
+
attributes: { type: "object" },
|
|
3615
|
+
selection: { type: "object" },
|
|
3616
|
+
combinations: { type: "array" },
|
|
3617
|
+
disabled: { type: "boolean", default: false }
|
|
3618
|
+
},
|
|
3619
|
+
events: [
|
|
3620
|
+
{ name: "change", bubbles: true },
|
|
3621
|
+
{ name: "selection-change", bubbles: true }
|
|
3622
|
+
]
|
|
3623
|
+
}
|
|
3624
|
+
},
|
|
3625
|
+
/**
|
|
3626
|
+
* Product Price component
|
|
3627
|
+
*/
|
|
3628
|
+
ProductPrice: {
|
|
3629
|
+
metadata: {
|
|
3630
|
+
name: "ProductPrice",
|
|
3631
|
+
tagName: "merchify-product-price",
|
|
3632
|
+
displayName: "MerchifyProductPrice",
|
|
3633
|
+
description: "Product price display component",
|
|
3634
|
+
props: {
|
|
3635
|
+
price: { type: "number" },
|
|
3636
|
+
currency: { type: "string", default: "USD" },
|
|
3637
|
+
locale: { type: "string", default: "en-US" },
|
|
3638
|
+
showCurrency: { type: "boolean", default: true }
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
},
|
|
3642
|
+
/**
|
|
3643
|
+
* Product Image component
|
|
3644
|
+
*/
|
|
3645
|
+
ProductImage: {
|
|
3646
|
+
metadata: {
|
|
3647
|
+
name: "ProductImage",
|
|
3648
|
+
tagName: "merchify-product-image",
|
|
3649
|
+
displayName: "MerchifyProductImage",
|
|
3650
|
+
description: "Product mockup image component",
|
|
3651
|
+
props: {
|
|
3652
|
+
productId: { type: "string" },
|
|
3653
|
+
mockupId: { type: "string" },
|
|
3654
|
+
variantId: { type: "string" },
|
|
3655
|
+
width: { type: "number", default: 400 },
|
|
3656
|
+
aspectRatio: { type: "string" },
|
|
3657
|
+
design: { type: "object" }
|
|
3658
|
+
},
|
|
3659
|
+
slots: ["loading", "error"]
|
|
3660
|
+
}
|
|
3661
|
+
},
|
|
3662
|
+
/**
|
|
3663
|
+
* Add to Cart component
|
|
3664
|
+
*/
|
|
3665
|
+
AddToCart: {
|
|
3666
|
+
metadata: {
|
|
3667
|
+
name: "AddToCart",
|
|
3668
|
+
tagName: "merchify-add-to-cart",
|
|
3669
|
+
displayName: "MerchifyAddToCart",
|
|
3670
|
+
description: "Add to cart button component",
|
|
3671
|
+
props: {
|
|
3672
|
+
text: { type: "string", default: "Add to Cart" },
|
|
3673
|
+
loadingText: { type: "string", default: "Adding..." },
|
|
3674
|
+
successText: { type: "string", default: "Added!" },
|
|
3675
|
+
disabled: { type: "boolean", default: false },
|
|
3676
|
+
quantity: { type: "number", default: 1 }
|
|
3677
|
+
},
|
|
3678
|
+
events: [
|
|
3679
|
+
{ name: "click", bubbles: true },
|
|
3680
|
+
{ name: "add-to-cart", bubbles: true },
|
|
3681
|
+
{ name: "success", bubbles: true },
|
|
3682
|
+
{ name: "error", bubbles: true }
|
|
3683
|
+
]
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
};
|
|
3687
|
+
function registerStandardComponents() {
|
|
3688
|
+
Object.values(StandardComponents).forEach((component) => {
|
|
3689
|
+
componentRegistry.register({
|
|
3690
|
+
metadata: component.metadata,
|
|
3691
|
+
descriptor: {
|
|
3692
|
+
name: component.metadata.name,
|
|
3693
|
+
props: component.metadata.props,
|
|
3694
|
+
render: () => null
|
|
3695
|
+
// Placeholder - implementations will override
|
|
3696
|
+
}
|
|
3697
|
+
});
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
var ComponentFactory = class {
|
|
3701
|
+
/**
|
|
3702
|
+
* Create a React component
|
|
3703
|
+
*/
|
|
3704
|
+
static forReact(definition, React) {
|
|
3705
|
+
const { metadata, descriptor } = definition;
|
|
3706
|
+
const Component = (props) => {
|
|
3707
|
+
return React.createElement("div", props);
|
|
3708
|
+
};
|
|
3709
|
+
Component.displayName = metadata.displayName || metadata.name;
|
|
3710
|
+
return Component;
|
|
3711
|
+
}
|
|
3712
|
+
/**
|
|
3713
|
+
* Create a Vue component
|
|
3714
|
+
*/
|
|
3715
|
+
static forVue(definition) {
|
|
3716
|
+
const { metadata, descriptor } = definition;
|
|
3717
|
+
return {
|
|
3718
|
+
name: metadata.name,
|
|
3719
|
+
props: metadata.props,
|
|
3720
|
+
emits: metadata.events?.map((e) => e.name),
|
|
3721
|
+
setup(props, context) {
|
|
3722
|
+
return () => null;
|
|
3723
|
+
}
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
/**
|
|
3727
|
+
* Create a Svelte component
|
|
3728
|
+
*/
|
|
3729
|
+
static forSvelte(definition) {
|
|
3730
|
+
return {
|
|
3731
|
+
metadata: definition.metadata,
|
|
3732
|
+
descriptor: definition.descriptor
|
|
3733
|
+
};
|
|
3734
|
+
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Create a Web Component
|
|
3737
|
+
*/
|
|
3738
|
+
static forWebComponent(definition, LitElement) {
|
|
3739
|
+
const { metadata, descriptor } = definition;
|
|
3740
|
+
class Component extends LitElement {
|
|
3741
|
+
static properties = metadata.props;
|
|
3742
|
+
render() {
|
|
3743
|
+
return null;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
if (metadata.tagName && !customElements.get(metadata.tagName)) {
|
|
3747
|
+
customElements.define(metadata.tagName, Component);
|
|
3748
|
+
}
|
|
3749
|
+
return Component;
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
function autoRegister(framework, registrar) {
|
|
3753
|
+
const components = componentRegistry.getByFramework(framework);
|
|
3754
|
+
components.forEach((definition) => {
|
|
3755
|
+
registrar(definition);
|
|
3756
|
+
});
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// src/framework/adapters/lit.ts
|
|
3760
|
+
var LitAdapter = class {
|
|
3761
|
+
name = "lit";
|
|
3762
|
+
element;
|
|
3763
|
+
// LitElement instance
|
|
3764
|
+
properties = /* @__PURE__ */ new Map();
|
|
3765
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
3766
|
+
constructor(element) {
|
|
3767
|
+
this.element = element;
|
|
3768
|
+
}
|
|
3769
|
+
/**
|
|
3770
|
+
* Create a stateful value using reactive properties
|
|
3771
|
+
*/
|
|
3772
|
+
createState(initialValue) {
|
|
3773
|
+
const key = `state_${Date.now()}_${Math.random()}`;
|
|
3774
|
+
this.properties.set(key, initialValue);
|
|
3775
|
+
return {
|
|
3776
|
+
get: () => {
|
|
3777
|
+
return this.properties.get(key);
|
|
3778
|
+
},
|
|
3779
|
+
set: (newValue) => {
|
|
3780
|
+
const current = this.properties.get(key);
|
|
3781
|
+
const nextValue = typeof newValue === "function" ? newValue(current) : newValue;
|
|
3782
|
+
this.properties.set(key, nextValue);
|
|
3783
|
+
if (this.element) {
|
|
3784
|
+
this.element.requestUpdate();
|
|
3785
|
+
}
|
|
3786
|
+
const subs = this.subscriptions.get(key);
|
|
3787
|
+
if (subs) {
|
|
3788
|
+
subs.forEach((callback) => callback(nextValue));
|
|
3789
|
+
}
|
|
3790
|
+
},
|
|
3791
|
+
subscribe: (callback) => {
|
|
3792
|
+
const subs = this.subscriptions.get(key) || /* @__PURE__ */ new Set();
|
|
3793
|
+
subs.add(callback);
|
|
3794
|
+
this.subscriptions.set(key, subs);
|
|
3795
|
+
return () => {
|
|
3796
|
+
subs.delete(callback);
|
|
3797
|
+
};
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
/**
|
|
3802
|
+
* Create a context provider/consumer
|
|
3803
|
+
*/
|
|
3804
|
+
createContext(name) {
|
|
3805
|
+
return {
|
|
3806
|
+
provide: (value) => {
|
|
3807
|
+
if (this.element) {
|
|
3808
|
+
this.element[`__context_${name}`] = value;
|
|
3809
|
+
this.element.dispatchEvent(new CustomEvent("context-provide", {
|
|
3810
|
+
detail: { name, value },
|
|
3811
|
+
bubbles: true,
|
|
3812
|
+
composed: true
|
|
3813
|
+
}));
|
|
3814
|
+
}
|
|
3815
|
+
},
|
|
3816
|
+
consume: () => {
|
|
3817
|
+
if (!this.element) return void 0;
|
|
3818
|
+
const stored = this.element[`__context_${name}`];
|
|
3819
|
+
if (stored !== void 0) return stored;
|
|
3820
|
+
const detail = { context: void 0 };
|
|
3821
|
+
this.element.dispatchEvent(new CustomEvent("context-request", {
|
|
3822
|
+
detail: { name, ...detail },
|
|
3823
|
+
bubbles: true,
|
|
3824
|
+
composed: true
|
|
3825
|
+
}));
|
|
3826
|
+
return detail.context;
|
|
3827
|
+
},
|
|
3828
|
+
subscribe: (callback) => {
|
|
3829
|
+
if (!this.element) return () => {
|
|
3830
|
+
};
|
|
3831
|
+
const handler = (event) => {
|
|
3832
|
+
if (event.detail?.name === name) {
|
|
3833
|
+
callback(event.detail.value);
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
this.element.addEventListener("context-update", handler);
|
|
3837
|
+
return () => {
|
|
3838
|
+
this.element.removeEventListener("context-update", handler);
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
/**
|
|
3844
|
+
* Register lifecycle hooks
|
|
3845
|
+
*/
|
|
3846
|
+
useLifecycle(lifecycle) {
|
|
3847
|
+
if (!this.element) return;
|
|
3848
|
+
const originalConnected = this.element.connectedCallback;
|
|
3849
|
+
const originalDisconnected = this.element.disconnectedCallback;
|
|
3850
|
+
const originalUpdated = this.element.updated;
|
|
3851
|
+
this.element.connectedCallback = function() {
|
|
3852
|
+
originalConnected?.call(this);
|
|
3853
|
+
lifecycle.onMount?.();
|
|
3854
|
+
};
|
|
3855
|
+
this.element.disconnectedCallback = function() {
|
|
3856
|
+
originalDisconnected?.call(this);
|
|
3857
|
+
lifecycle.onUnmount?.();
|
|
3858
|
+
};
|
|
3859
|
+
this.element.updated = function(changedProperties) {
|
|
3860
|
+
originalUpdated?.call(this, changedProperties);
|
|
3861
|
+
lifecycle.onUpdate?.({});
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Register event handlers
|
|
3866
|
+
*/
|
|
3867
|
+
useEvents(handlers) {
|
|
3868
|
+
if (!this.element) return;
|
|
3869
|
+
handlers.forEach(({ name, handler }) => {
|
|
3870
|
+
this.element.addEventListener(name, handler);
|
|
3871
|
+
});
|
|
3872
|
+
}
|
|
3873
|
+
/**
|
|
3874
|
+
* Create a ref to a DOM element
|
|
3875
|
+
*/
|
|
3876
|
+
createRef() {
|
|
3877
|
+
const ref = { current: null };
|
|
3878
|
+
if (this.element) {
|
|
3879
|
+
this.element.updateComplete.then(() => {
|
|
3880
|
+
ref.current = this.element.renderRoot.querySelector("[ref]");
|
|
3881
|
+
});
|
|
3882
|
+
}
|
|
3883
|
+
return ref;
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Render a component
|
|
3887
|
+
*/
|
|
3888
|
+
render(component, props) {
|
|
3889
|
+
return component.render(this);
|
|
3890
|
+
}
|
|
3891
|
+
/**
|
|
3892
|
+
* Get Lit-specific utilities
|
|
3893
|
+
*/
|
|
3894
|
+
getUtilities() {
|
|
3895
|
+
return {
|
|
3896
|
+
batchUpdates: (callback) => {
|
|
3897
|
+
callback();
|
|
3898
|
+
if (this.element) {
|
|
3899
|
+
this.element.requestUpdate();
|
|
3900
|
+
}
|
|
3901
|
+
},
|
|
3902
|
+
nextTick: (callback) => {
|
|
3903
|
+
if (this.element) {
|
|
3904
|
+
this.element.updateComplete.then(callback);
|
|
3905
|
+
} else {
|
|
3906
|
+
Promise.resolve().then(callback);
|
|
3907
|
+
}
|
|
3908
|
+
},
|
|
3909
|
+
computed: (deps, compute) => {
|
|
3910
|
+
return compute();
|
|
3911
|
+
},
|
|
3912
|
+
memo: (value, deps) => {
|
|
3913
|
+
return value;
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Set the element instance
|
|
3919
|
+
*/
|
|
3920
|
+
setElement(element) {
|
|
3921
|
+
this.element = element;
|
|
3922
|
+
}
|
|
3923
|
+
};
|
|
3924
|
+
function createLitComponent(descriptor, LitElement, html) {
|
|
3925
|
+
return class extends LitElement {
|
|
3926
|
+
adapter = new LitAdapter(this);
|
|
3927
|
+
connectedCallback() {
|
|
3928
|
+
super.connectedCallback();
|
|
3929
|
+
if (descriptor.lifecycle) {
|
|
3930
|
+
this.adapter.useLifecycle(descriptor.lifecycle);
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
render() {
|
|
3934
|
+
return this.adapter.render(descriptor, this);
|
|
3935
|
+
}
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// src/framework/adapters/vue.ts
|
|
3940
|
+
var VueAdapter = class {
|
|
3941
|
+
name = "vue";
|
|
3942
|
+
vue;
|
|
3943
|
+
// Vue instance
|
|
3944
|
+
instance;
|
|
3945
|
+
// Component instance
|
|
3946
|
+
constructor(vue, instance) {
|
|
3947
|
+
this.vue = vue;
|
|
3948
|
+
this.instance = instance;
|
|
3949
|
+
}
|
|
3950
|
+
/**
|
|
3951
|
+
* Create a stateful value using ref/reactive
|
|
3952
|
+
*/
|
|
3953
|
+
createState(initialValue) {
|
|
3954
|
+
const { ref, watch } = this.vue;
|
|
3955
|
+
const state = ref(initialValue);
|
|
3956
|
+
const subscribers = /* @__PURE__ */ new Set();
|
|
3957
|
+
watch(state, (newValue) => {
|
|
3958
|
+
subscribers.forEach((callback) => callback(newValue));
|
|
3959
|
+
});
|
|
3960
|
+
return {
|
|
3961
|
+
get: () => state.value,
|
|
3962
|
+
set: (newValue) => {
|
|
3963
|
+
if (typeof newValue === "function") {
|
|
3964
|
+
state.value = newValue(state.value);
|
|
3965
|
+
} else {
|
|
3966
|
+
state.value = newValue;
|
|
3967
|
+
}
|
|
3968
|
+
},
|
|
3969
|
+
subscribe: (callback) => {
|
|
3970
|
+
subscribers.add(callback);
|
|
3971
|
+
return () => {
|
|
3972
|
+
subscribers.delete(callback);
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* Create a context provider/consumer
|
|
3979
|
+
*/
|
|
3980
|
+
createContext(name) {
|
|
3981
|
+
const { provide, inject } = this.vue;
|
|
3982
|
+
const key = Symbol(name);
|
|
3983
|
+
return {
|
|
3984
|
+
provide: (value) => {
|
|
3985
|
+
provide(key, value);
|
|
3986
|
+
},
|
|
3987
|
+
consume: () => {
|
|
3988
|
+
return inject(key);
|
|
3989
|
+
},
|
|
3990
|
+
subscribe: (callback) => {
|
|
3991
|
+
const value = inject(key);
|
|
3992
|
+
if (value !== void 0) {
|
|
3993
|
+
const { watchEffect } = this.vue;
|
|
3994
|
+
const unwatch = watchEffect(() => {
|
|
3995
|
+
callback(value);
|
|
3996
|
+
});
|
|
3997
|
+
return unwatch;
|
|
3998
|
+
}
|
|
3999
|
+
return () => {
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
/**
|
|
4005
|
+
* Register lifecycle hooks
|
|
4006
|
+
*/
|
|
4007
|
+
useLifecycle(lifecycle) {
|
|
4008
|
+
const { onMounted, onUnmounted, onUpdated, onErrorCaptured } = this.vue;
|
|
4009
|
+
if (lifecycle.onMount) {
|
|
4010
|
+
onMounted(lifecycle.onMount);
|
|
4011
|
+
}
|
|
4012
|
+
if (lifecycle.onUnmount) {
|
|
4013
|
+
onUnmounted(lifecycle.onUnmount);
|
|
4014
|
+
}
|
|
4015
|
+
if (lifecycle.onUpdate) {
|
|
4016
|
+
onUpdated(() => lifecycle.onUpdate({}));
|
|
4017
|
+
}
|
|
4018
|
+
if (lifecycle.onError) {
|
|
4019
|
+
onErrorCaptured((error) => {
|
|
4020
|
+
lifecycle.onError(error);
|
|
4021
|
+
return false;
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* Register event handlers
|
|
4027
|
+
*/
|
|
4028
|
+
useEvents(handlers) {
|
|
4029
|
+
if (!this.instance) return;
|
|
4030
|
+
handlers.forEach(({ name, handler }) => {
|
|
4031
|
+
this.instance.$on?.(name, handler);
|
|
4032
|
+
});
|
|
4033
|
+
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Create a ref to a DOM element
|
|
4036
|
+
*/
|
|
4037
|
+
createRef() {
|
|
4038
|
+
const { ref } = this.vue;
|
|
4039
|
+
const elementRef = ref(null);
|
|
4040
|
+
return {
|
|
4041
|
+
get current() {
|
|
4042
|
+
return elementRef.value;
|
|
4043
|
+
},
|
|
4044
|
+
set current(value) {
|
|
4045
|
+
elementRef.value = value;
|
|
4046
|
+
}
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
/**
|
|
4050
|
+
* Render a component
|
|
4051
|
+
*/
|
|
4052
|
+
render(component, props) {
|
|
4053
|
+
const { h } = this.vue;
|
|
4054
|
+
return h("div", props, component.render(this));
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Get Vue-specific utilities
|
|
4058
|
+
*/
|
|
4059
|
+
getUtilities() {
|
|
4060
|
+
const { nextTick, computed, watchEffect } = this.vue;
|
|
4061
|
+
return {
|
|
4062
|
+
batchUpdates: (callback) => {
|
|
4063
|
+
callback();
|
|
4064
|
+
},
|
|
4065
|
+
nextTick: (callback) => {
|
|
4066
|
+
nextTick(callback);
|
|
4067
|
+
},
|
|
4068
|
+
computed: (deps, compute) => {
|
|
4069
|
+
return computed(compute).value;
|
|
4070
|
+
},
|
|
4071
|
+
memo: (value, deps) => {
|
|
4072
|
+
return value;
|
|
4073
|
+
}
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
};
|
|
4077
|
+
function createVueComponent(descriptor, vue) {
|
|
4078
|
+
const { defineComponent: defineComponent2, h } = vue;
|
|
4079
|
+
return defineComponent2({
|
|
4080
|
+
name: descriptor.name,
|
|
4081
|
+
props: descriptor.props || {},
|
|
4082
|
+
setup(props, context) {
|
|
4083
|
+
const adapter = new VueAdapter(vue, context);
|
|
4084
|
+
if (descriptor.lifecycle) {
|
|
4085
|
+
adapter.useLifecycle(descriptor.lifecycle);
|
|
4086
|
+
}
|
|
4087
|
+
return () => adapter.render(descriptor, props);
|
|
4088
|
+
}
|
|
4089
|
+
});
|
|
4090
|
+
}
|
|
4091
|
+
function useFrameworkAdapter(vue) {
|
|
4092
|
+
return new VueAdapter(vue);
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
// src/framework/adapters/svelte.ts
|
|
4096
|
+
var SvelteAdapter = class {
|
|
4097
|
+
name = "svelte";
|
|
4098
|
+
svelte;
|
|
4099
|
+
// Svelte imports
|
|
4100
|
+
component;
|
|
4101
|
+
// Component instance
|
|
4102
|
+
constructor(svelte, component) {
|
|
4103
|
+
this.svelte = svelte;
|
|
4104
|
+
this.component = component;
|
|
4105
|
+
}
|
|
4106
|
+
/**
|
|
4107
|
+
* Create a stateful value using Svelte stores
|
|
4108
|
+
*/
|
|
4109
|
+
createState(initialValue) {
|
|
4110
|
+
const { writable, get } = this.svelte;
|
|
4111
|
+
const store = writable(initialValue);
|
|
4112
|
+
return {
|
|
4113
|
+
get: () => get(store),
|
|
4114
|
+
set: (newValue) => {
|
|
4115
|
+
if (typeof newValue === "function") {
|
|
4116
|
+
store.update((prev) => newValue(prev));
|
|
4117
|
+
} else {
|
|
4118
|
+
store.set(newValue);
|
|
4119
|
+
}
|
|
4120
|
+
},
|
|
4121
|
+
subscribe: (callback) => {
|
|
4122
|
+
const unsubscribe = store.subscribe(callback);
|
|
4123
|
+
return unsubscribe;
|
|
4124
|
+
}
|
|
4125
|
+
};
|
|
4126
|
+
}
|
|
4127
|
+
/**
|
|
4128
|
+
* Create a context provider/consumer
|
|
4129
|
+
*/
|
|
4130
|
+
createContext(name) {
|
|
4131
|
+
const { setContext, getContext, hasContext } = this.svelte;
|
|
4132
|
+
const key = Symbol(name);
|
|
4133
|
+
return {
|
|
4134
|
+
provide: (value) => {
|
|
4135
|
+
setContext(key, value);
|
|
4136
|
+
},
|
|
4137
|
+
consume: () => {
|
|
4138
|
+
if (hasContext(key)) {
|
|
4139
|
+
return getContext(key);
|
|
4140
|
+
}
|
|
4141
|
+
return void 0;
|
|
4142
|
+
},
|
|
4143
|
+
subscribe: (callback) => {
|
|
4144
|
+
const value = getContext(key);
|
|
4145
|
+
if (value && typeof value === "object" && "subscribe" in value) {
|
|
4146
|
+
const unsubscribe = value.subscribe(callback);
|
|
4147
|
+
return unsubscribe;
|
|
4148
|
+
}
|
|
4149
|
+
if (value !== void 0) {
|
|
4150
|
+
callback(value);
|
|
4151
|
+
}
|
|
4152
|
+
return () => {
|
|
4153
|
+
};
|
|
4154
|
+
}
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
/**
|
|
4158
|
+
* Register lifecycle hooks
|
|
4159
|
+
*/
|
|
4160
|
+
useLifecycle(lifecycle) {
|
|
4161
|
+
const { onMount, onDestroy, afterUpdate } = this.svelte;
|
|
4162
|
+
if (lifecycle.onMount) {
|
|
4163
|
+
onMount(lifecycle.onMount);
|
|
4164
|
+
}
|
|
4165
|
+
if (lifecycle.onUnmount) {
|
|
4166
|
+
onDestroy(lifecycle.onUnmount);
|
|
4167
|
+
}
|
|
4168
|
+
if (lifecycle.onUpdate) {
|
|
4169
|
+
afterUpdate(() => lifecycle.onUpdate({}));
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* Register event handlers
|
|
4174
|
+
*/
|
|
4175
|
+
useEvents(handlers) {
|
|
4176
|
+
const { createEventDispatcher } = this.svelte;
|
|
4177
|
+
const dispatch = createEventDispatcher();
|
|
4178
|
+
handlers.forEach(({ name, handler }) => {
|
|
4179
|
+
this[`handle${name}`] = (event) => {
|
|
4180
|
+
handler(event);
|
|
4181
|
+
dispatch(name, event);
|
|
4182
|
+
};
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
/**
|
|
4186
|
+
* Create a ref to a DOM element
|
|
4187
|
+
*/
|
|
4188
|
+
createRef() {
|
|
4189
|
+
const ref = { current: null };
|
|
4190
|
+
return ref;
|
|
4191
|
+
}
|
|
4192
|
+
/**
|
|
4193
|
+
* Render a component
|
|
4194
|
+
*/
|
|
4195
|
+
render(component, props) {
|
|
4196
|
+
return component.render(this);
|
|
4197
|
+
}
|
|
4198
|
+
/**
|
|
4199
|
+
* Get Svelte-specific utilities
|
|
4200
|
+
*/
|
|
4201
|
+
getUtilities() {
|
|
4202
|
+
const { tick, derived } = this.svelte;
|
|
4203
|
+
return {
|
|
4204
|
+
batchUpdates: (callback) => {
|
|
4205
|
+
callback();
|
|
4206
|
+
},
|
|
4207
|
+
nextTick: (callback) => {
|
|
4208
|
+
tick().then(callback);
|
|
4209
|
+
},
|
|
4210
|
+
computed: (deps, compute) => {
|
|
4211
|
+
const { get } = this.svelte;
|
|
4212
|
+
const derivedStore = derived(deps, compute);
|
|
4213
|
+
return get(derivedStore);
|
|
4214
|
+
},
|
|
4215
|
+
memo: (value, deps) => {
|
|
4216
|
+
return value;
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
function createSvelteComponent(descriptor, svelte) {
|
|
4222
|
+
return {
|
|
4223
|
+
name: descriptor.name,
|
|
4224
|
+
props: descriptor.props,
|
|
4225
|
+
setup: (props) => {
|
|
4226
|
+
const adapter = new SvelteAdapter(svelte);
|
|
4227
|
+
if (descriptor.lifecycle) {
|
|
4228
|
+
adapter.useLifecycle(descriptor.lifecycle);
|
|
4229
|
+
}
|
|
4230
|
+
return adapter.render(descriptor, props);
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
// src/index.ts
|
|
4236
|
+
function getFetcher(config2) {
|
|
4237
|
+
return config2?.fetcher || globalThis.fetch.bind(globalThis);
|
|
4238
|
+
}
|
|
4239
|
+
function validateProductLoose(product) {
|
|
4240
|
+
try {
|
|
4241
|
+
return CatalogProductSchema.parse(product);
|
|
4242
|
+
} catch (_err) {
|
|
4243
|
+
return product;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
async function listProducts(config2) {
|
|
4247
|
+
const f = getFetcher(config2);
|
|
4248
|
+
const meilisearchHost = config2?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://ms-e5d999b2eaca-15654.sfo.meilisearch.io";
|
|
4249
|
+
const meilisearchApiKey = config2?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
|
|
4250
|
+
const meilisearchIndex = config2?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "merchify";
|
|
4251
|
+
const headers = {
|
|
4252
|
+
"Content-Type": "application/json"
|
|
4253
|
+
};
|
|
4254
|
+
if (meilisearchApiKey) {
|
|
4255
|
+
headers["Authorization"] = `Bearer ${meilisearchApiKey}`;
|
|
4256
|
+
}
|
|
4257
|
+
const url = `${meilisearchHost}/indexes/${meilisearchIndex}/search`;
|
|
4258
|
+
const res = await f(url, {
|
|
4259
|
+
method: "POST",
|
|
4260
|
+
headers,
|
|
4261
|
+
body: JSON.stringify({
|
|
4262
|
+
q: "",
|
|
4263
|
+
limit: config2?.limit || 1e3,
|
|
4264
|
+
filter: "mockups IS NOT EMPTY"
|
|
4265
|
+
})
|
|
4266
|
+
});
|
|
4267
|
+
if (!res.ok) throw new Error(`listProducts failed: ${res.status}`);
|
|
4268
|
+
const data = await res.json();
|
|
4269
|
+
const items = (data.hits || []).map((p) => validateProductLoose(p));
|
|
4270
|
+
return { items, total: data.estimatedTotalHits || items.length };
|
|
4271
|
+
}
|
|
4272
|
+
async function getProduct(idOrSlug, config2) {
|
|
4273
|
+
const f = getFetcher(config2);
|
|
4274
|
+
const meilisearchHost = config2?.meilisearch?.host || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_HOST || "https://ms-e5d999b2eaca-15654.sfo.meilisearch.io";
|
|
4275
|
+
const meilisearchApiKey = config2?.meilisearch?.apiKey || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_API_KEY || "eee819b849798ad9091228c486ec05d0931e5292";
|
|
4276
|
+
const meilisearchIndex = config2?.meilisearch?.index || typeof process !== "undefined" && process.env?.NEXT_PUBLIC_MEILISEARCH_INDEX || "merchify";
|
|
4277
|
+
const headers = {
|
|
4278
|
+
"Content-Type": "application/json"
|
|
4279
|
+
};
|
|
4280
|
+
if (meilisearchApiKey) {
|
|
4281
|
+
headers["Authorization"] = `Bearer ${meilisearchApiKey}`;
|
|
4282
|
+
}
|
|
4283
|
+
const url = `${meilisearchHost}/indexes/${meilisearchIndex}/documents/${idOrSlug}`;
|
|
4284
|
+
const res = await f(url, { method: "GET", headers });
|
|
4285
|
+
if (res.status === 404)
|
|
4286
|
+
throw Object.assign(new Error("Not found"), { code: "NOT_FOUND" });
|
|
4287
|
+
if (!res.ok) throw new Error(`getProduct failed: ${res.status}`);
|
|
4288
|
+
const raw = await res.json();
|
|
4289
|
+
return validateProductLoose(raw);
|
|
4290
|
+
}
|
|
4291
|
+
function createClient(config2) {
|
|
4292
|
+
const fetcher = getFetcher(config2);
|
|
4293
|
+
const mockupService = new MockupServiceImpl(config2.mockup || {}, fetcher);
|
|
4294
|
+
return {
|
|
4295
|
+
catalog: {
|
|
4296
|
+
listProducts: (overrides) => listProducts({ ...config2, ...overrides }),
|
|
4297
|
+
getProduct: (idOrSlug, overrides) => getProduct(idOrSlug, { ...config2, ...overrides })
|
|
4298
|
+
},
|
|
4299
|
+
mockup: mockupService
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
export {
|
|
4303
|
+
AbstractTemplateRenderer,
|
|
4304
|
+
AdapterRegistry,
|
|
4305
|
+
Animations,
|
|
4306
|
+
Attributes,
|
|
4307
|
+
ClassNames,
|
|
4308
|
+
ColorPickerUtils,
|
|
4309
|
+
CommonProps,
|
|
4310
|
+
ComponentEventManager,
|
|
4311
|
+
ComponentFactory,
|
|
4312
|
+
ComponentLifecycle,
|
|
4313
|
+
ComponentRegistry,
|
|
4314
|
+
ComponentTemplates,
|
|
4315
|
+
ContextBridge,
|
|
4316
|
+
ContextInjector,
|
|
4317
|
+
ContextSynchronizer,
|
|
4318
|
+
DEFAULT_ARTWORK_URL,
|
|
4319
|
+
DEFAULT_ASPECT_RATIO,
|
|
4320
|
+
DEFAULT_COLOR,
|
|
4321
|
+
DEFAULT_PLACEMENT_DIMENSIONS,
|
|
4322
|
+
Elements,
|
|
4323
|
+
ErrorManager,
|
|
4324
|
+
EventDelegator,
|
|
4325
|
+
EventEmitter,
|
|
4326
|
+
Focus,
|
|
4327
|
+
LifecycleManager,
|
|
4328
|
+
LitAdapter,
|
|
4329
|
+
ProductContextManager,
|
|
4330
|
+
ProductLoader,
|
|
4331
|
+
ProductProps,
|
|
4332
|
+
PropertyManager,
|
|
4333
|
+
RealtimeMockupService,
|
|
4334
|
+
StandardComponents,
|
|
4335
|
+
StandardEvents,
|
|
4336
|
+
StateManager,
|
|
4337
|
+
Styles,
|
|
4338
|
+
SvelteAdapter,
|
|
4339
|
+
SwatchUtils,
|
|
4340
|
+
TemplateBuilder,
|
|
4341
|
+
UniversalContextProvider,
|
|
4342
|
+
VueAdapter,
|
|
4343
|
+
adapterRegistry,
|
|
4344
|
+
autoRegister,
|
|
4345
|
+
componentRegistry,
|
|
4346
|
+
computeDisabledChoices,
|
|
4347
|
+
config,
|
|
4348
|
+
createAddToCartEvent,
|
|
4349
|
+
createAddToCartHandler,
|
|
4350
|
+
createCartDetail,
|
|
4351
|
+
createClient,
|
|
4352
|
+
createComponent,
|
|
4353
|
+
createContextProvider,
|
|
4354
|
+
createDesignForPlacements,
|
|
4355
|
+
createDevFetcher,
|
|
4356
|
+
createErrorHandler,
|
|
4357
|
+
createEventManager,
|
|
4358
|
+
createLifecycle,
|
|
4359
|
+
createLitComponent,
|
|
4360
|
+
createProductContext,
|
|
4361
|
+
createProductLoader,
|
|
4362
|
+
createPropertyManager,
|
|
4363
|
+
createStateHook,
|
|
4364
|
+
createStateStore,
|
|
4365
|
+
createSvelteComponent,
|
|
4366
|
+
createUniversalProvider,
|
|
4367
|
+
createVueComponent,
|
|
4368
|
+
defineComponent,
|
|
4369
|
+
deriveDefaultSelection,
|
|
4370
|
+
describeArtSelector,
|
|
4371
|
+
describeProductArtAlignment,
|
|
4372
|
+
describeProductImage,
|
|
4373
|
+
describeProductOptions,
|
|
4374
|
+
describeProductPrice,
|
|
4375
|
+
describeProductTitle,
|
|
4376
|
+
extractProductId,
|
|
4377
|
+
filterImagePlacements,
|
|
4378
|
+
findBestCombination,
|
|
4379
|
+
findClosestSnapPoint,
|
|
4380
|
+
findVariantForSelection,
|
|
4381
|
+
formatPrice,
|
|
4382
|
+
formatValidationErrors,
|
|
4383
|
+
getDefaultVariantId,
|
|
4384
|
+
getEffectiveAlignment,
|
|
4385
|
+
getIncompleteSelectionMessage,
|
|
4386
|
+
getMissingSelections,
|
|
4387
|
+
getOptionRenderType,
|
|
4388
|
+
getPricePreview,
|
|
4389
|
+
getProduct,
|
|
4390
|
+
getSelectionDisplayText,
|
|
4391
|
+
getSnapPoints,
|
|
4392
|
+
getVariant,
|
|
4393
|
+
handleOptionChange,
|
|
4394
|
+
isOptionAvailable,
|
|
4395
|
+
isSelectionComplete,
|
|
4396
|
+
isValidAlignment,
|
|
4397
|
+
isValidTileCount,
|
|
4398
|
+
listProducts,
|
|
4399
|
+
mockupUrl,
|
|
4400
|
+
normalizeChoice,
|
|
4401
|
+
prepareOptionRenderData,
|
|
4402
|
+
registerStandardComponents,
|
|
4403
|
+
resolveBestCombination,
|
|
4404
|
+
resolveMockupConfig,
|
|
4405
|
+
resolveMockupId,
|
|
4406
|
+
resolveVariantId,
|
|
4407
|
+
retryOperation,
|
|
4408
|
+
simulateCartOperation,
|
|
4409
|
+
toCombinations,
|
|
4410
|
+
toOptionAttributes,
|
|
4411
|
+
useFrameworkAdapter as useVueAdapter,
|
|
4412
|
+
validateAlignment,
|
|
4413
|
+
validateDesignElement,
|
|
4414
|
+
validateEffects,
|
|
4415
|
+
validateImageUrl,
|
|
4416
|
+
validateMockupOptions,
|
|
4417
|
+
validateProductSelection,
|
|
4418
|
+
validateProductSpec,
|
|
4419
|
+
validateRequiredOptions,
|
|
4420
|
+
validateTileCount,
|
|
4421
|
+
withContext,
|
|
4422
|
+
withErrorHandling,
|
|
4423
|
+
withSyncErrorHandling
|
|
4424
|
+
};
|