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