@lime-bundles/widget 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,654 @@
1
+ // src/lime-bundle.ts
2
+ import {
3
+ createStorefrontClient,
4
+ BUNDLE_METAOBJECT_QUERY,
5
+ parseMetaobjectBundle,
6
+ detectCartApi,
7
+ createAjaxCartApi,
8
+ createStorefrontCartApi,
9
+ observeImpression,
10
+ reportImpression,
11
+ reportAddToCart
12
+ } from "@lime-bundles/core";
13
+
14
+ // src/renderers/fixed.ts
15
+ import { formatMoney } from "@lime-bundles/core";
16
+ function renderFixedBundle(container, bundle, addToCart) {
17
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
18
+ const title = document.createElement("h3");
19
+ title.className = "lb-bundle__title";
20
+ title.textContent = bundle.title;
21
+ title.setAttribute("part", "title");
22
+ container.appendChild(title);
23
+ if (bundle.discountLabel) {
24
+ const badge = document.createElement("span");
25
+ badge.className = "lb-bundle__discount-badge";
26
+ badge.textContent = bundle.discountLabel;
27
+ container.appendChild(badge);
28
+ }
29
+ const productsDiv = document.createElement("div");
30
+ productsDiv.className = "lb-bundle__products";
31
+ for (const product of bundle.products) {
32
+ const productEl = document.createElement("div");
33
+ productEl.className = "lb-bundle__product";
34
+ productEl.setAttribute("part", "product");
35
+ if (product.featuredImage) {
36
+ const img = document.createElement("img");
37
+ img.src = product.featuredImage.url;
38
+ img.alt = product.featuredImage.altText ?? product.title;
39
+ img.className = "lb-bundle__product-image";
40
+ img.loading = "lazy";
41
+ productEl.appendChild(img);
42
+ }
43
+ const info = document.createElement("div");
44
+ info.className = "lb-bundle__product-info";
45
+ info.innerHTML = `
46
+ <p class="lb-bundle__product-title">${escapeHtml(product.title)}</p>
47
+ <p class="lb-bundle__product-price">${escapeHtml(formatMoney(product.priceRange.minVariantPrice.amount, currency))}</p>
48
+ `;
49
+ productEl.appendChild(info);
50
+ productsDiv.appendChild(productEl);
51
+ }
52
+ container.appendChild(productsDiv);
53
+ const button = document.createElement("button");
54
+ button.className = "lb-bundle__cta";
55
+ button.textContent = bundle.widgetConfig.ctaText ?? "Add Bundle to Cart";
56
+ button.setAttribute("part", "button");
57
+ button.addEventListener("click", async () => {
58
+ button.disabled = true;
59
+ button.textContent = "Adding...";
60
+ const items = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
61
+ const variant = p.variants.nodes.find((v) => v.availableForSale);
62
+ return { variantId: variant.id, quantity: 1 };
63
+ });
64
+ const result = await addToCart(items);
65
+ button.disabled = false;
66
+ button.textContent = bundle.widgetConfig.ctaText ?? "Add Bundle to Cart";
67
+ if (!result.success) {
68
+ const error = document.createElement("p");
69
+ error.className = "lb-bundle__error";
70
+ error.textContent = result.error ?? "Failed to add to cart";
71
+ container.appendChild(error);
72
+ setTimeout(() => error.remove(), 5e3);
73
+ }
74
+ });
75
+ container.appendChild(button);
76
+ }
77
+ function escapeHtml(str) {
78
+ const div = document.createElement("div");
79
+ div.textContent = str;
80
+ return div.innerHTML;
81
+ }
82
+
83
+ // src/renderers/mix-match.ts
84
+ import { formatMoney as formatMoney2, validateQuantity } from "@lime-bundles/core";
85
+ function renderMixMatchBundle(container, bundle, addToCart) {
86
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
87
+ const selections = /* @__PURE__ */ new Map();
88
+ const title = document.createElement("h3");
89
+ title.className = "lb-bundle__title";
90
+ title.textContent = bundle.title;
91
+ title.setAttribute("part", "title");
92
+ container.appendChild(title);
93
+ const instructions = document.createElement("p");
94
+ instructions.className = "lb-bundle__instructions";
95
+ instructions.textContent = bundle.minQuantity && bundle.maxQuantity ? `Select ${bundle.minQuantity}\u2013${bundle.maxQuantity} items` : bundle.minQuantity ? `Select at least ${bundle.minQuantity} items` : "Select your items";
96
+ container.appendChild(instructions);
97
+ const productsDiv = document.createElement("div");
98
+ productsDiv.className = "lb-bundle__products lb-bundle__products--selectable";
99
+ for (const product of bundle.products) {
100
+ const variant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
101
+ if (!variant) continue;
102
+ const productEl = document.createElement("div");
103
+ productEl.className = "lb-bundle__product lb-bundle__product--selectable";
104
+ if (product.featuredImage) {
105
+ const img = document.createElement("img");
106
+ img.src = product.featuredImage.url;
107
+ img.alt = product.featuredImage.altText ?? product.title;
108
+ img.className = "lb-bundle__product-image";
109
+ img.loading = "lazy";
110
+ productEl.appendChild(img);
111
+ }
112
+ const info = document.createElement("div");
113
+ info.className = "lb-bundle__product-info";
114
+ info.innerHTML = `
115
+ <p class="lb-bundle__product-title">${escapeHtml2(product.title)}</p>
116
+ <p class="lb-bundle__product-price">${escapeHtml2(formatMoney2(variant.price.amount, currency))}</p>
117
+ `;
118
+ productEl.appendChild(info);
119
+ const selectBtn = document.createElement("button");
120
+ selectBtn.className = "lb-bundle__select-btn";
121
+ selectBtn.textContent = variant.availableForSale ? "Select" : "Sold out";
122
+ selectBtn.disabled = !variant.availableForSale;
123
+ selectBtn.addEventListener("click", () => {
124
+ const key = product.id;
125
+ if (selections.has(key)) {
126
+ selections.delete(key);
127
+ productEl.classList.remove("lb-bundle__product--selected");
128
+ selectBtn.textContent = "Select";
129
+ } else {
130
+ selections.set(key, { variantId: variant.id, quantity: 1 });
131
+ productEl.classList.add("lb-bundle__product--selected");
132
+ selectBtn.textContent = "Selected";
133
+ }
134
+ updateCta();
135
+ });
136
+ productEl.appendChild(selectBtn);
137
+ productsDiv.appendChild(productEl);
138
+ }
139
+ container.appendChild(productsDiv);
140
+ const validationEl = document.createElement("p");
141
+ validationEl.className = "lb-bundle__validation";
142
+ container.appendChild(validationEl);
143
+ const button = document.createElement("button");
144
+ button.className = "lb-bundle__cta";
145
+ button.setAttribute("part", "button");
146
+ button.disabled = true;
147
+ container.appendChild(button);
148
+ function updateCta() {
149
+ const total = Array.from(selections.values()).reduce((s, v) => s + v.quantity, 0);
150
+ const validation = validateQuantity(total, bundle.minQuantity, bundle.maxQuantity);
151
+ button.disabled = !validation.valid;
152
+ button.textContent = bundle.widgetConfig.ctaText ?? `Add ${total} Items to Cart`;
153
+ validationEl.textContent = validation.message ?? "";
154
+ }
155
+ updateCta();
156
+ button.addEventListener("click", async () => {
157
+ button.disabled = true;
158
+ button.textContent = "Adding...";
159
+ const items = Array.from(selections.entries()).map(([, s]) => ({
160
+ variantId: s.variantId,
161
+ quantity: s.quantity
162
+ }));
163
+ const result = await addToCart(items);
164
+ updateCta();
165
+ if (!result.success) {
166
+ const error = document.createElement("p");
167
+ error.className = "lb-bundle__error";
168
+ error.textContent = result.error ?? "Failed to add to cart";
169
+ container.appendChild(error);
170
+ setTimeout(() => error.remove(), 5e3);
171
+ }
172
+ });
173
+ }
174
+ function escapeHtml2(str) {
175
+ const div = document.createElement("div");
176
+ div.textContent = str;
177
+ return div.innerHTML;
178
+ }
179
+
180
+ // src/renderers/volume.ts
181
+ import { formatMoney as formatMoney3, calculateTierSavings } from "@lime-bundles/core";
182
+ function renderVolumeBundle(container, bundle, addToCart) {
183
+ const product = bundle.products[0];
184
+ if (!product) return;
185
+ const basePrice = parseFloat(product.priceRange.minVariantPrice.amount);
186
+ const currency = product.priceRange.minVariantPrice.currencyCode;
187
+ let quantity = 1;
188
+ const title = document.createElement("h3");
189
+ title.className = "lb-bundle__title";
190
+ title.textContent = bundle.title;
191
+ title.setAttribute("part", "title");
192
+ container.appendChild(title);
193
+ const productEl = document.createElement("div");
194
+ productEl.className = "lb-bundle__product lb-bundle__product--volume";
195
+ if (product.featuredImage) {
196
+ const img = document.createElement("img");
197
+ img.src = product.featuredImage.url;
198
+ img.alt = product.featuredImage.altText ?? product.title;
199
+ img.className = "lb-bundle__product-image";
200
+ img.loading = "lazy";
201
+ productEl.appendChild(img);
202
+ }
203
+ const info = document.createElement("div");
204
+ info.className = "lb-bundle__product-info";
205
+ info.innerHTML = `
206
+ <p class="lb-bundle__product-title">${escapeHtml3(product.title)}</p>
207
+ <p class="lb-bundle__product-price">${escapeHtml3(formatMoney3(basePrice, currency))} each</p>
208
+ `;
209
+ productEl.appendChild(info);
210
+ container.appendChild(productEl);
211
+ const tiersDiv = document.createElement("div");
212
+ tiersDiv.className = "lb-bundle__tiers";
213
+ tiersDiv.setAttribute("role", "table");
214
+ tiersDiv.setAttribute("aria-label", "Volume discounts");
215
+ container.appendChild(tiersDiv);
216
+ const qtyWrapper = document.createElement("div");
217
+ qtyWrapper.className = "lb-bundle__quantity-selector";
218
+ const label = document.createElement("label");
219
+ label.textContent = "Quantity";
220
+ qtyWrapper.appendChild(label);
221
+ const qtyControl = document.createElement("div");
222
+ qtyControl.className = "lb-bundle__quantity-control";
223
+ const minusBtn = document.createElement("button");
224
+ minusBtn.textContent = "\u2212";
225
+ minusBtn.setAttribute("aria-label", "Decrease quantity");
226
+ const qtyInput = document.createElement("input");
227
+ qtyInput.type = "number";
228
+ qtyInput.min = "1";
229
+ qtyInput.value = "1";
230
+ qtyInput.className = "lb-bundle__quantity-input";
231
+ const plusBtn = document.createElement("button");
232
+ plusBtn.textContent = "+";
233
+ plusBtn.setAttribute("aria-label", "Increase quantity");
234
+ qtyControl.append(minusBtn, qtyInput, plusBtn);
235
+ qtyWrapper.appendChild(qtyControl);
236
+ container.appendChild(qtyWrapper);
237
+ const button = document.createElement("button");
238
+ button.className = "lb-bundle__cta";
239
+ button.setAttribute("part", "button");
240
+ container.appendChild(button);
241
+ function updateTiers() {
242
+ const savings = calculateTierSavings(bundle.volumeTiers, basePrice, quantity);
243
+ tiersDiv.innerHTML = "";
244
+ for (const ts of savings) {
245
+ const row = document.createElement("div");
246
+ row.className = `lb-bundle__tier${ts.isActive ? " lb-bundle__tier--active" : ""}`;
247
+ row.setAttribute("role", "row");
248
+ row.innerHTML = `
249
+ <span class="lb-bundle__tier-quantity" role="cell">${ts.tier.minQuantity}+ items</span>
250
+ <span class="lb-bundle__tier-price" role="cell">${escapeHtml3(formatMoney3(ts.unitPrice, currency))} each</span>
251
+ <span class="lb-bundle__tier-savings" role="cell">Save ${ts.savingsPercent.toFixed(0)}%</span>
252
+ ${ts.tier.label ? `<span class="lb-bundle__tier-label" role="cell">${escapeHtml3(ts.tier.label)}</span>` : ""}
253
+ `;
254
+ tiersDiv.appendChild(row);
255
+ }
256
+ button.textContent = bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`;
257
+ }
258
+ updateTiers();
259
+ minusBtn.addEventListener("click", () => {
260
+ if (quantity > 1) {
261
+ quantity--;
262
+ qtyInput.value = String(quantity);
263
+ updateTiers();
264
+ }
265
+ });
266
+ plusBtn.addEventListener("click", () => {
267
+ quantity++;
268
+ qtyInput.value = String(quantity);
269
+ updateTiers();
270
+ });
271
+ qtyInput.addEventListener("change", () => {
272
+ const val = parseInt(qtyInput.value, 10);
273
+ if (!isNaN(val) && val > 0) {
274
+ quantity = val;
275
+ updateTiers();
276
+ }
277
+ });
278
+ button.addEventListener("click", async () => {
279
+ const variant = product.variants.nodes.find((v) => v.availableForSale);
280
+ if (!variant) return;
281
+ button.disabled = true;
282
+ button.textContent = "Adding...";
283
+ const items = [{ variantId: variant.id, quantity }];
284
+ const result = await addToCart(items);
285
+ button.disabled = false;
286
+ updateTiers();
287
+ if (!result.success) {
288
+ const error = document.createElement("p");
289
+ error.className = "lb-bundle__error";
290
+ error.textContent = result.error ?? "Failed to add to cart";
291
+ container.appendChild(error);
292
+ setTimeout(() => error.remove(), 5e3);
293
+ }
294
+ });
295
+ }
296
+ function escapeHtml3(str) {
297
+ const div = document.createElement("div");
298
+ div.textContent = str;
299
+ return div.innerHTML;
300
+ }
301
+
302
+ // src/styles/widget-styles.ts
303
+ var WIDGET_STYLES = `
304
+ :host {
305
+ display: block;
306
+ --lb-primary-color: #000;
307
+ --lb-secondary-color: #666;
308
+ --lb-accent-color: #2563eb;
309
+ --lb-background: #fff;
310
+ --lb-border-color: #e5e7eb;
311
+ --lb-border-radius: 8px;
312
+ --lb-font-family: inherit;
313
+ --lb-font-size: 14px;
314
+ --lb-spacing-sm: 8px;
315
+ --lb-spacing-md: 16px;
316
+ --lb-spacing-lg: 24px;
317
+ --lb-button-bg: var(--lb-accent-color);
318
+ --lb-button-text: #fff;
319
+ --lb-button-radius: var(--lb-border-radius);
320
+ --lb-savings-color: #16a34a;
321
+ --lb-error-color: #dc2626;
322
+ }
323
+
324
+ .lb-bundle {
325
+ font-family: var(--lb-font-family);
326
+ font-size: var(--lb-font-size);
327
+ color: var(--lb-primary-color);
328
+ background: var(--lb-background);
329
+ border: 1px solid var(--lb-border-color);
330
+ border-radius: var(--lb-border-radius);
331
+ padding: var(--lb-spacing-lg);
332
+ }
333
+
334
+ .lb-bundle__title { margin: 0 0 var(--lb-spacing-md); font-size: 1.25em; font-weight: 600; }
335
+ .lb-bundle__discount-badge { display: inline-block; background: var(--lb-savings-color); color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 600; margin-bottom: var(--lb-spacing-md); }
336
+ .lb-bundle__products { display: grid; gap: var(--lb-spacing-md); margin-bottom: var(--lb-spacing-lg); }
337
+ .lb-bundle__product { display: flex; gap: var(--lb-spacing-md); align-items: center; padding: var(--lb-spacing-sm); border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); }
338
+ .lb-bundle__product--selected { border-color: var(--lb-accent-color); }
339
+ .lb-bundle__product-image { width: 64px; height: 64px; object-fit: cover; border-radius: calc(var(--lb-border-radius) - 2px); flex-shrink: 0; }
340
+ .lb-bundle__product-info { flex: 1; min-width: 0; }
341
+ .lb-bundle__product-title { margin: 0; font-weight: 500; }
342
+ .lb-bundle__product-price { margin: 4px 0 0; color: var(--lb-secondary-color); }
343
+ .lb-bundle__instructions { color: var(--lb-secondary-color); margin: 0 0 var(--lb-spacing-md); }
344
+ .lb-bundle__tiers { margin-bottom: var(--lb-spacing-lg); }
345
+ .lb-bundle__tier { display: flex; align-items: center; gap: var(--lb-spacing-md); padding: var(--lb-spacing-sm) var(--lb-spacing-md); border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); margin-bottom: var(--lb-spacing-sm); }
346
+ .lb-bundle__tier--active { border-color: var(--lb-savings-color); }
347
+ .lb-bundle__tier-savings { color: var(--lb-savings-color); font-weight: 600; }
348
+ .lb-bundle__quantity-selector { margin-bottom: var(--lb-spacing-lg); }
349
+ .lb-bundle__quantity-selector label { display: block; margin-bottom: var(--lb-spacing-sm); font-weight: 500; }
350
+ .lb-bundle__quantity-control { display: inline-flex; align-items: center; border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); }
351
+ .lb-bundle__quantity-control button { width: 32px; height: 32px; border: none; background: transparent; cursor: pointer; font-size: 1.1em; display: flex; align-items: center; justify-content: center; }
352
+ .lb-bundle__quantity-input { width: 40px; text-align: center; border: none; border-left: 1px solid var(--lb-border-color); border-right: 1px solid var(--lb-border-color); height: 32px; font-size: var(--lb-font-size); -moz-appearance: textfield; }
353
+ .lb-bundle__quantity-input::-webkit-outer-spin-button, .lb-bundle__quantity-input::-webkit-inner-spin-button { -webkit-appearance: none; }
354
+ .lb-bundle__select-btn { padding: 6px 12px; border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); background: transparent; cursor: pointer; }
355
+ .lb-bundle__select-btn:disabled { opacity: 0.5; cursor: not-allowed; }
356
+ .lb-bundle__cta { width: 100%; padding: 12px 24px; border: none; border-radius: var(--lb-button-radius); background: var(--lb-button-bg); color: var(--lb-button-text); font-size: 1em; font-weight: 600; cursor: pointer; transition: opacity 0.15s; }
357
+ .lb-bundle__cta:hover:not(:disabled) { opacity: 0.9; }
358
+ .lb-bundle__cta:disabled { opacity: 0.5; cursor: not-allowed; }
359
+ .lb-bundle__error { color: var(--lb-error-color); margin: var(--lb-spacing-sm) 0; font-size: 0.9em; }
360
+ .lb-bundle__validation { color: var(--lb-secondary-color); margin: var(--lb-spacing-sm) 0; font-size: 0.9em; }
361
+
362
+ .lb-skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: lb-shimmer 1.5s infinite; border-radius: var(--lb-border-radius); }
363
+ .lb-skeleton--title { height: 24px; width: 60%; margin-bottom: var(--lb-spacing-md); }
364
+ .lb-skeleton--products { height: 200px; }
365
+ @keyframes lb-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
366
+ `;
367
+
368
+ // src/lime-bundle.ts
369
+ var LimeBundleElement = class extends HTMLElement {
370
+ static observedAttributes = [
371
+ "shop-domain",
372
+ "storefront-token",
373
+ "bundle-gid",
374
+ "cart-id",
375
+ "app-url",
376
+ "analytics",
377
+ "locale"
378
+ ];
379
+ shadow;
380
+ bundle = null;
381
+ abortController = null;
382
+ impressionCleanup = null;
383
+ constructor() {
384
+ super();
385
+ this.shadow = this.attachShadow({ mode: "open" });
386
+ }
387
+ connectedCallback() {
388
+ this.render();
389
+ this.fetchBundle();
390
+ }
391
+ disconnectedCallback() {
392
+ this.abortController?.abort();
393
+ this.teardownImpression();
394
+ }
395
+ /** Clean up stale bundle state and active impression observer. */
396
+ teardownImpression() {
397
+ this.impressionCleanup?.();
398
+ this.impressionCleanup = null;
399
+ }
400
+ attributeChangedCallback(name, oldValue, newValue) {
401
+ if (oldValue === newValue || !this.isConnected) return;
402
+ if (name === "bundle-gid" || name === "shop-domain" || name === "storefront-token") {
403
+ if (this.shopDomain && this.storefrontToken && this.bundleGid) {
404
+ this.fetchBundle();
405
+ }
406
+ }
407
+ }
408
+ get shopDomain() {
409
+ return this.getAttribute("shop-domain") ?? "";
410
+ }
411
+ get storefrontToken() {
412
+ return this.getAttribute("storefront-token") ?? "";
413
+ }
414
+ get bundleGid() {
415
+ return this.getAttribute("bundle-gid") ?? "";
416
+ }
417
+ get cartId() {
418
+ return this.getAttribute("cart-id") ?? void 0;
419
+ }
420
+ get appUrl() {
421
+ return this.getAttribute("app-url") ?? "";
422
+ }
423
+ get analyticsEnabled() {
424
+ return this.getAttribute("analytics") !== "false";
425
+ }
426
+ async fetchBundle() {
427
+ if (!this.shopDomain || !this.storefrontToken || !this.bundleGid) {
428
+ this.renderError("Missing required attributes: shop-domain, storefront-token, bundle-gid");
429
+ return;
430
+ }
431
+ this.abortController?.abort();
432
+ const controller = new AbortController();
433
+ this.abortController = controller;
434
+ this.renderLoading();
435
+ try {
436
+ const client = createStorefrontClient({
437
+ shopDomain: this.shopDomain,
438
+ accessToken: this.storefrontToken
439
+ });
440
+ const data = await client.query(
441
+ BUNDLE_METAOBJECT_QUERY,
442
+ { id: this.bundleGid }
443
+ );
444
+ if (controller.signal.aborted) return;
445
+ if (!data.metaobject) {
446
+ this.bundle = null;
447
+ this.teardownImpression();
448
+ this.renderError("Bundle not found");
449
+ return;
450
+ }
451
+ this.bundle = parseMetaobjectBundle(
452
+ data.metaobject.id,
453
+ data.metaobject.fields
454
+ );
455
+ if (!this.bundle) {
456
+ this.teardownImpression();
457
+ this.renderError("Bundle is not active or has expired");
458
+ return;
459
+ }
460
+ this.renderBundle();
461
+ this.setupImpression();
462
+ } catch (err) {
463
+ if (controller.signal.aborted) return;
464
+ this.bundle = null;
465
+ this.teardownImpression();
466
+ this.renderError(
467
+ err instanceof Error ? err.message : "Failed to load bundle"
468
+ );
469
+ }
470
+ }
471
+ renderBundle() {
472
+ if (!this.bundle) return;
473
+ const container = document.createElement("div");
474
+ container.className = "lb-bundle";
475
+ container.setAttribute("role", "region");
476
+ container.setAttribute("aria-label", this.bundle.title);
477
+ const addToCart = async (items) => {
478
+ const apiType = detectCartApi();
479
+ const cart = apiType === "ajax" ? createAjaxCartApi(this.bundleGid, this.bundle.bundleType) : createStorefrontCartApi(
480
+ createStorefrontClient({
481
+ shopDomain: this.shopDomain,
482
+ accessToken: this.storefrontToken
483
+ }),
484
+ this.bundleGid,
485
+ this.bundle.bundleType,
486
+ this.cartId
487
+ );
488
+ let result;
489
+ try {
490
+ result = await cart.addLines(items);
491
+ } catch (err) {
492
+ result = {
493
+ success: false,
494
+ error: err instanceof Error ? err.message : "Cart add failed"
495
+ };
496
+ }
497
+ if (result.success) {
498
+ this.dispatchEvent(
499
+ new CustomEvent("lime-bundle:add-to-cart", {
500
+ detail: { items, cartId: result.cartId },
501
+ bubbles: true,
502
+ composed: true
503
+ })
504
+ );
505
+ if (this.analyticsEnabled && this.appUrl) {
506
+ const quantity = items.reduce((sum, i) => sum + i.quantity, 0);
507
+ const totalPrice = items.reduce((sum, item) => {
508
+ const product = this.bundle.products.find(
509
+ (p) => p.variants.nodes.some((v) => v.id === item.variantId)
510
+ );
511
+ const variant = product?.variants.nodes.find((v) => v.id === item.variantId);
512
+ const price = variant ? parseFloat(variant.price.amount) : 0;
513
+ return sum + price * item.quantity;
514
+ }, 0);
515
+ reportAddToCart(
516
+ { shopDomain: this.shopDomain, appUrl: this.appUrl },
517
+ {
518
+ bundleGid: this.bundleGid,
519
+ bundleType: this.bundle.bundleType,
520
+ productId: this.bundle.products[0]?.id ?? "",
521
+ quantity,
522
+ totalPrice: Math.round(totalPrice * 100) / 100
523
+ }
524
+ );
525
+ }
526
+ }
527
+ return result;
528
+ };
529
+ switch (this.bundle.bundleType) {
530
+ case "fixed":
531
+ renderFixedBundle(container, this.bundle, addToCart);
532
+ break;
533
+ case "mix_match":
534
+ renderMixMatchBundle(container, this.bundle, addToCart);
535
+ break;
536
+ case "volume":
537
+ renderVolumeBundle(container, this.bundle, addToCart);
538
+ break;
539
+ }
540
+ this.shadow.innerHTML = "";
541
+ const style = document.createElement("style");
542
+ style.textContent = WIDGET_STYLES;
543
+ this.shadow.appendChild(style);
544
+ this.shadow.appendChild(container);
545
+ this.dispatchEvent(
546
+ new CustomEvent("lime-bundle:loaded", {
547
+ detail: {
548
+ bundleType: this.bundle.bundleType,
549
+ title: this.bundle.title
550
+ },
551
+ bubbles: true,
552
+ composed: true
553
+ })
554
+ );
555
+ }
556
+ setupImpression() {
557
+ if (!this.analyticsEnabled || !this.bundle || !this.appUrl) return;
558
+ this.impressionCleanup?.();
559
+ this.impressionCleanup = observeImpression(this, () => {
560
+ reportImpression(
561
+ { shopDomain: this.shopDomain, appUrl: this.appUrl },
562
+ {
563
+ bundleGid: this.bundleGid,
564
+ bundleType: this.bundle.bundleType
565
+ }
566
+ );
567
+ });
568
+ }
569
+ renderLoading() {
570
+ this.shadow.innerHTML = `
571
+ <style>${WIDGET_STYLES}</style>
572
+ <div class="lb-bundle lb-bundle--loading">
573
+ <div class="lb-skeleton lb-skeleton--title"></div>
574
+ <div class="lb-skeleton lb-skeleton--products"></div>
575
+ </div>
576
+ `;
577
+ }
578
+ renderError(message) {
579
+ this.shadow.innerHTML = "";
580
+ this.dispatchEvent(
581
+ new CustomEvent("lime-bundle:error", {
582
+ detail: { message, code: "LOAD_ERROR" },
583
+ bubbles: true,
584
+ composed: true
585
+ })
586
+ );
587
+ }
588
+ render() {
589
+ this.shadow.innerHTML = `<style>${WIDGET_STYLES}</style>`;
590
+ }
591
+ };
592
+
593
+ // src/thankyou/index.ts
594
+ var reportedPurchases = /* @__PURE__ */ new Set();
595
+ function trackPurchase(input) {
596
+ const appUrl = input.appUrl ?? `https://${input.shopDomain}`;
597
+ const bundleMap = /* @__PURE__ */ new Map();
598
+ for (const item of input.lineItems) {
599
+ if (!item.bundleGid) continue;
600
+ const existing = bundleMap.get(item.bundleGid);
601
+ if (existing) {
602
+ existing.revenue += item.price * item.quantity;
603
+ existing.lineItemCount += 1;
604
+ } else {
605
+ bundleMap.set(item.bundleGid, {
606
+ bundleType: item.bundleType,
607
+ revenue: item.price * item.quantity,
608
+ lineItemCount: 1
609
+ });
610
+ }
611
+ }
612
+ for (const [bundleGid, data] of bundleMap) {
613
+ const dedupKey = `${input.orderId}:${bundleGid}`;
614
+ if (reportedPurchases.has(dedupKey)) continue;
615
+ reportedPurchases.add(dedupKey);
616
+ const payload = {
617
+ shopDomain: input.shopDomain,
618
+ eventType: "bundle_purchased",
619
+ bundleGid,
620
+ bundleType: data.bundleType,
621
+ orderId: input.orderId,
622
+ revenue: Math.round(data.revenue * 100) / 100,
623
+ lineItemCount: data.lineItemCount,
624
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
625
+ };
626
+ const url = `${appUrl}/api/analytics`;
627
+ const body = JSON.stringify(payload);
628
+ try {
629
+ fetch(url, {
630
+ method: "POST",
631
+ headers: { "Content-Type": "application/json" },
632
+ body
633
+ }).catch(() => {
634
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
635
+ navigator.sendBeacon(url, body);
636
+ }
637
+ });
638
+ } catch {
639
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
640
+ navigator.sendBeacon(url, body);
641
+ }
642
+ }
643
+ }
644
+ }
645
+
646
+ // src/index.ts
647
+ if (typeof customElements !== "undefined" && !customElements.get("lime-bundle")) {
648
+ customElements.define("lime-bundle", LimeBundleElement);
649
+ }
650
+ export {
651
+ LimeBundleElement,
652
+ trackPurchase
653
+ };
654
+ //# sourceMappingURL=index.js.map