@lime-bundles/react 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,677 @@
1
+ // src/components/FixedBundle.tsx
2
+ import { useCallback as useCallback3 } from "react";
3
+ import { formatMoney, calculateDiscount } from "@lime-bundles/core";
4
+
5
+ // src/hooks/useBundleData.ts
6
+ import { useState, useEffect, useRef } from "react";
7
+ import {
8
+ createStorefrontClient,
9
+ BUNDLE_METAOBJECT_QUERY,
10
+ parseMetaobjectBundle
11
+ } from "@lime-bundles/core";
12
+ function useBundleData(options) {
13
+ const [bundle, setBundle] = useState(null);
14
+ const [loading, setLoading] = useState(true);
15
+ const [error, setError] = useState(null);
16
+ const abortRef = useRef(null);
17
+ useEffect(() => {
18
+ abortRef.current?.abort();
19
+ const controller = new AbortController();
20
+ abortRef.current = controller;
21
+ setLoading(true);
22
+ setError(null);
23
+ const client = createStorefrontClient({
24
+ shopDomain: options.shopDomain,
25
+ accessToken: options.storefrontAccessToken
26
+ });
27
+ client.query(BUNDLE_METAOBJECT_QUERY, {
28
+ id: options.bundleGid
29
+ }).then((data) => {
30
+ if (controller.signal.aborted) return;
31
+ if (!data.metaobject) {
32
+ setError("Bundle not found");
33
+ setBundle(null);
34
+ } else {
35
+ const parsed = parseMetaobjectBundle(
36
+ data.metaobject.id,
37
+ data.metaobject.fields
38
+ );
39
+ if (parsed) {
40
+ setBundle(parsed);
41
+ } else {
42
+ setError("Bundle is not active or has expired");
43
+ setBundle(null);
44
+ }
45
+ }
46
+ setLoading(false);
47
+ }).catch((err) => {
48
+ if (controller.signal.aborted) return;
49
+ setError(err instanceof Error ? err.message : "Failed to load bundle");
50
+ setLoading(false);
51
+ });
52
+ return () => {
53
+ controller.abort();
54
+ };
55
+ }, [options.shopDomain, options.storefrontAccessToken, options.bundleGid]);
56
+ return { bundle, loading, error };
57
+ }
58
+
59
+ // src/hooks/useCart.ts
60
+ import { useState as useState2, useCallback, useMemo } from "react";
61
+ import {
62
+ detectCartApi,
63
+ createAjaxCartApi,
64
+ createStorefrontCartApi,
65
+ createStorefrontClient as createStorefrontClient2
66
+ } from "@lime-bundles/core";
67
+ function useCart(options) {
68
+ const [loading, setLoading] = useState2(false);
69
+ const [error, setError] = useState2(null);
70
+ const [lastResult, setLastResult] = useState2(null);
71
+ const cart = useMemo(() => {
72
+ const apiType = options.cartApi ?? detectCartApi();
73
+ if (apiType === "ajax") {
74
+ return createAjaxCartApi(options.bundleGid, options.bundleType);
75
+ }
76
+ const client = createStorefrontClient2({
77
+ shopDomain: options.shopDomain,
78
+ accessToken: options.storefrontAccessToken
79
+ });
80
+ return createStorefrontCartApi(
81
+ client,
82
+ options.bundleGid,
83
+ options.bundleType,
84
+ options.cartId
85
+ );
86
+ }, [
87
+ options.shopDomain,
88
+ options.storefrontAccessToken,
89
+ options.bundleGid,
90
+ options.bundleType,
91
+ options.cartId,
92
+ options.cartApi
93
+ ]);
94
+ const addToCart = useCallback(
95
+ async (items) => {
96
+ setLoading(true);
97
+ setError(null);
98
+ try {
99
+ const result = await cart.addLines(items);
100
+ setLastResult(result);
101
+ if (!result.success) {
102
+ setError(result.error ?? "Failed to add to cart");
103
+ }
104
+ return result;
105
+ } catch (err) {
106
+ const message = err instanceof Error ? err.message : "Failed to add to cart";
107
+ setError(message);
108
+ const result = { success: false, error: message };
109
+ setLastResult(result);
110
+ return result;
111
+ } finally {
112
+ setLoading(false);
113
+ }
114
+ },
115
+ [cart]
116
+ );
117
+ return { addToCart, loading, error, lastResult };
118
+ }
119
+
120
+ // src/hooks/useAnalytics.ts
121
+ import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useCallback as useCallback2, useState as useState3 } from "react";
122
+ import {
123
+ reportImpression,
124
+ reportAddToCart,
125
+ observeImpression
126
+ } from "@lime-bundles/core";
127
+ function useAnalytics(options) {
128
+ const impressionFiredRef = useRef2(false);
129
+ const [element, setElement] = useState3(null);
130
+ const elementRef = useCallback2((el) => {
131
+ setElement(el);
132
+ }, []);
133
+ const config = useMemo2(
134
+ () => ({ shopDomain: options.shopDomain, appUrl: options.appUrl }),
135
+ [options.shopDomain, options.appUrl]
136
+ );
137
+ useEffect2(() => {
138
+ impressionFiredRef.current = false;
139
+ }, [options.bundleGid, options.abVariant]);
140
+ useEffect2(() => {
141
+ if (options.enabled === false) return;
142
+ if (impressionFiredRef.current) return;
143
+ if (!element) return;
144
+ const cleanup = observeImpression(element, () => {
145
+ if (impressionFiredRef.current) return;
146
+ impressionFiredRef.current = true;
147
+ reportImpression(config, {
148
+ bundleGid: options.bundleGid,
149
+ bundleType: options.bundleType,
150
+ abTestId: options.abTestId,
151
+ abVariant: options.abVariant
152
+ });
153
+ });
154
+ return cleanup;
155
+ }, [
156
+ element,
157
+ config,
158
+ options.enabled,
159
+ options.bundleGid,
160
+ options.bundleType,
161
+ options.abTestId,
162
+ options.abVariant
163
+ ]);
164
+ const trackAddToCart = useCallback2(
165
+ (event) => {
166
+ if (options.enabled === false) return;
167
+ reportAddToCart(config, {
168
+ bundleGid: options.bundleGid,
169
+ bundleType: options.bundleType,
170
+ productId: event.productId,
171
+ quantity: event.quantity,
172
+ totalPrice: event.totalPrice,
173
+ abTestId: options.abTestId,
174
+ abVariant: options.abVariant
175
+ });
176
+ },
177
+ [
178
+ config,
179
+ options.enabled,
180
+ options.bundleGid,
181
+ options.bundleType,
182
+ options.abTestId,
183
+ options.abVariant
184
+ ]
185
+ );
186
+ return { elementRef, trackAddToCart };
187
+ }
188
+
189
+ // src/components/FixedBundle.tsx
190
+ import { jsx, jsxs } from "react/jsx-runtime";
191
+ function FixedBundle(props) {
192
+ const {
193
+ shopDomain,
194
+ storefrontAccessToken,
195
+ bundleGid,
196
+ cartId,
197
+ cartApi,
198
+ appUrl,
199
+ analyticsEnabled,
200
+ onAddToCart,
201
+ onError,
202
+ className
203
+ } = props;
204
+ const { bundle, loading, error: loadError } = useBundleData({
205
+ shopDomain,
206
+ storefrontAccessToken,
207
+ bundleGid
208
+ });
209
+ const { addToCart, loading: cartLoading, error: cartError } = useCart({
210
+ shopDomain,
211
+ storefrontAccessToken,
212
+ bundleGid,
213
+ bundleType: "fixed",
214
+ cartId,
215
+ cartApi
216
+ });
217
+ const { elementRef, trackAddToCart } = useAnalytics({
218
+ shopDomain,
219
+ appUrl: appUrl ?? `https://${shopDomain}`,
220
+ bundleGid,
221
+ bundleType: "fixed",
222
+ enabled: analyticsEnabled !== false
223
+ });
224
+ const handleAddToCart = useCallback3(async () => {
225
+ if (!bundle) return;
226
+ const items = bundle.products.filter((p) => p.variants.nodes.some((v) => v.availableForSale)).map((p) => {
227
+ const variant = p.variants.nodes.find((v) => v.availableForSale);
228
+ return { variantId: variant.id, quantity: 1 };
229
+ });
230
+ if (items.length === 0) return;
231
+ const result = await addToCart(items);
232
+ if (result.success) {
233
+ const totalPrice = bundle.products.reduce((sum, p) => {
234
+ const price = parseFloat(p.priceRange.minVariantPrice.amount);
235
+ return sum + calculateDiscount(price, bundle.discountConfig.discountType, bundle.discountConfig.discountValue);
236
+ }, 0);
237
+ trackAddToCart({
238
+ productId: bundle.products[0]?.id ?? "",
239
+ quantity: 1,
240
+ totalPrice: Math.round(totalPrice * 100) / 100
241
+ });
242
+ onAddToCart?.(items);
243
+ }
244
+ }, [bundle, addToCart, trackAddToCart, onAddToCart]);
245
+ if (loading) {
246
+ return /* @__PURE__ */ jsxs("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
247
+ /* @__PURE__ */ jsx("div", { className: "lb-skeleton lb-skeleton--title" }),
248
+ /* @__PURE__ */ jsx("div", { className: "lb-skeleton lb-skeleton--products" })
249
+ ] });
250
+ }
251
+ if (loadError || !bundle) {
252
+ if (onError) onError(loadError ?? "Bundle not found");
253
+ return null;
254
+ }
255
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
256
+ return /* @__PURE__ */ jsxs(
257
+ "div",
258
+ {
259
+ ref: elementRef,
260
+ className: `lb-bundle lb-bundle--fixed ${className ?? ""}`,
261
+ role: "region",
262
+ "aria-label": bundle.title,
263
+ children: [
264
+ /* @__PURE__ */ jsx("h3", { className: "lb-bundle__title", children: bundle.title }),
265
+ bundle.discountLabel && /* @__PURE__ */ jsx("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
266
+ /* @__PURE__ */ jsx("div", { className: "lb-bundle__products", children: bundle.products.map((product) => /* @__PURE__ */ jsxs("div", { className: "lb-bundle__product", part: "product", children: [
267
+ product.featuredImage && /* @__PURE__ */ jsx(
268
+ "img",
269
+ {
270
+ src: product.featuredImage.url,
271
+ alt: product.featuredImage.altText ?? product.title,
272
+ className: "lb-bundle__product-image",
273
+ loading: "lazy"
274
+ }
275
+ ),
276
+ /* @__PURE__ */ jsxs("div", { className: "lb-bundle__product-info", children: [
277
+ /* @__PURE__ */ jsx("p", { className: "lb-bundle__product-title", children: product.title }),
278
+ /* @__PURE__ */ jsx("p", { className: "lb-bundle__product-price", children: formatMoney(product.priceRange.minVariantPrice.amount, currency) })
279
+ ] })
280
+ ] }, product.id)) }),
281
+ cartError && /* @__PURE__ */ jsx("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
282
+ /* @__PURE__ */ jsx(
283
+ "button",
284
+ {
285
+ className: "lb-bundle__cta",
286
+ onClick: handleAddToCart,
287
+ disabled: cartLoading,
288
+ "aria-busy": cartLoading,
289
+ children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? "Add Bundle to Cart"
290
+ }
291
+ )
292
+ ]
293
+ }
294
+ );
295
+ }
296
+
297
+ // src/components/MixMatchBundle.tsx
298
+ import { useState as useState4, useCallback as useCallback4 } from "react";
299
+ import {
300
+ formatMoney as formatMoney2,
301
+ validateQuantity
302
+ } from "@lime-bundles/core";
303
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
304
+ function MixMatchBundle(props) {
305
+ const {
306
+ shopDomain,
307
+ storefrontAccessToken,
308
+ bundleGid,
309
+ cartId,
310
+ cartApi,
311
+ appUrl,
312
+ analyticsEnabled,
313
+ onAddToCart,
314
+ onError,
315
+ className
316
+ } = props;
317
+ const { bundle, loading, error: loadError } = useBundleData({
318
+ shopDomain,
319
+ storefrontAccessToken,
320
+ bundleGid
321
+ });
322
+ const { addToCart, loading: cartLoading, error: cartError } = useCart({
323
+ shopDomain,
324
+ storefrontAccessToken,
325
+ bundleGid,
326
+ bundleType: "mix_match",
327
+ cartId,
328
+ cartApi
329
+ });
330
+ const { elementRef, trackAddToCart } = useAnalytics({
331
+ shopDomain,
332
+ appUrl: appUrl ?? `https://${shopDomain}`,
333
+ bundleGid,
334
+ bundleType: "mix_match",
335
+ enabled: analyticsEnabled !== false
336
+ });
337
+ const [selections, setSelections] = useState4(/* @__PURE__ */ new Map());
338
+ const totalQuantity = Array.from(selections.values()).reduce(
339
+ (sum, s) => sum + s.quantity,
340
+ 0
341
+ );
342
+ const validation = bundle ? validateQuantity(totalQuantity, bundle.minQuantity, bundle.maxQuantity) : { valid: false, totalQuantity: 0, message: null };
343
+ const toggleProduct = useCallback4(
344
+ (productId, variant) => {
345
+ setSelections((prev) => {
346
+ const next = new Map(prev);
347
+ const key = `${productId}:${variant.id}`;
348
+ if (next.has(key)) {
349
+ next.delete(key);
350
+ } else {
351
+ next.set(key, { productId, variantId: variant.id, quantity: 1 });
352
+ }
353
+ return next;
354
+ });
355
+ },
356
+ []
357
+ );
358
+ const updateQuantity = useCallback4(
359
+ (productId, variantId, quantity) => {
360
+ setSelections((prev) => {
361
+ const next = new Map(prev);
362
+ const key = `${productId}:${variantId}`;
363
+ if (quantity <= 0) {
364
+ next.delete(key);
365
+ } else {
366
+ next.set(key, { productId, variantId, quantity });
367
+ }
368
+ return next;
369
+ });
370
+ },
371
+ []
372
+ );
373
+ const handleAddToCart = useCallback4(async () => {
374
+ if (!bundle || !validation.valid) return;
375
+ const items = Array.from(selections.values()).map((s) => ({
376
+ variantId: s.variantId,
377
+ quantity: s.quantity
378
+ }));
379
+ const result = await addToCart(items);
380
+ if (result.success) {
381
+ const totalPrice = items.reduce((sum, item) => {
382
+ const product = bundle.products.find(
383
+ (p) => p.variants.nodes.some((v) => v.id === item.variantId)
384
+ );
385
+ const variant = product?.variants.nodes.find(
386
+ (v) => v.id === item.variantId
387
+ );
388
+ return sum + parseFloat(variant?.price.amount ?? "0") * item.quantity;
389
+ }, 0);
390
+ trackAddToCart({
391
+ productId: bundle.products[0]?.id ?? "",
392
+ quantity: totalQuantity,
393
+ totalPrice: Math.round(totalPrice * 100) / 100
394
+ });
395
+ onAddToCart?.(items);
396
+ setSelections(/* @__PURE__ */ new Map());
397
+ }
398
+ }, [bundle, selections, validation.valid, addToCart, trackAddToCart, totalQuantity, onAddToCart]);
399
+ if (loading) {
400
+ return /* @__PURE__ */ jsxs2("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
401
+ /* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--title" }),
402
+ /* @__PURE__ */ jsx2("div", { className: "lb-skeleton lb-skeleton--products" })
403
+ ] });
404
+ }
405
+ if (loadError || !bundle) {
406
+ if (onError) onError(loadError ?? "Bundle not found");
407
+ return null;
408
+ }
409
+ const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? "USD";
410
+ return /* @__PURE__ */ jsxs2(
411
+ "div",
412
+ {
413
+ ref: elementRef,
414
+ className: `lb-bundle lb-bundle--mix-match ${className ?? ""}`,
415
+ role: "region",
416
+ "aria-label": bundle.title,
417
+ children: [
418
+ /* @__PURE__ */ jsx2("h3", { className: "lb-bundle__title", children: bundle.title }),
419
+ bundle.discountLabel && /* @__PURE__ */ jsx2("span", { className: "lb-bundle__discount-badge", children: bundle.discountLabel }),
420
+ /* @__PURE__ */ jsx2("p", { className: "lb-bundle__instructions", children: bundle.minQuantity && bundle.maxQuantity ? `Select ${bundle.minQuantity}\u2013${bundle.maxQuantity} items` : bundle.minQuantity ? `Select at least ${bundle.minQuantity} items` : bundle.maxQuantity ? `Select up to ${bundle.maxQuantity} items` : "Select your items" }),
421
+ /* @__PURE__ */ jsx2("div", { className: "lb-bundle__products lb-bundle__products--selectable", children: bundle.products.map((product) => {
422
+ const defaultVariant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];
423
+ if (!defaultVariant) return null;
424
+ const key = `${product.id}:${defaultVariant.id}`;
425
+ const selected = selections.get(key);
426
+ return /* @__PURE__ */ jsxs2(
427
+ "div",
428
+ {
429
+ className: `lb-bundle__product lb-bundle__product--selectable ${selected ? "lb-bundle__product--selected" : ""}`,
430
+ children: [
431
+ product.featuredImage && /* @__PURE__ */ jsx2(
432
+ "img",
433
+ {
434
+ src: product.featuredImage.url,
435
+ alt: product.featuredImage.altText ?? product.title,
436
+ className: "lb-bundle__product-image",
437
+ loading: "lazy"
438
+ }
439
+ ),
440
+ /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__product-info", children: [
441
+ /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-title", children: product.title }),
442
+ /* @__PURE__ */ jsx2("p", { className: "lb-bundle__product-price", children: formatMoney2(defaultVariant.price.amount, currency) })
443
+ ] }),
444
+ /* @__PURE__ */ jsx2("div", { className: "lb-bundle__product-actions", children: selected ? /* @__PURE__ */ jsxs2("div", { className: "lb-bundle__quantity-control", children: [
445
+ /* @__PURE__ */ jsx2(
446
+ "button",
447
+ {
448
+ "aria-label": `Decrease ${product.title}`,
449
+ onClick: () => updateQuantity(product.id, defaultVariant.id, selected.quantity - 1),
450
+ children: "\u2212"
451
+ }
452
+ ),
453
+ /* @__PURE__ */ jsx2("span", { children: selected.quantity }),
454
+ /* @__PURE__ */ jsx2(
455
+ "button",
456
+ {
457
+ "aria-label": `Increase ${product.title}`,
458
+ onClick: () => updateQuantity(product.id, defaultVariant.id, selected.quantity + 1),
459
+ children: "+"
460
+ }
461
+ )
462
+ ] }) : /* @__PURE__ */ jsx2(
463
+ "button",
464
+ {
465
+ className: "lb-bundle__select-btn",
466
+ onClick: () => toggleProduct(product.id, defaultVariant),
467
+ disabled: !defaultVariant.availableForSale,
468
+ children: defaultVariant.availableForSale ? "Select" : "Sold out"
469
+ }
470
+ ) })
471
+ ]
472
+ },
473
+ product.id
474
+ );
475
+ }) }),
476
+ validation.message && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__validation", role: "status", children: validation.message }),
477
+ cartError && /* @__PURE__ */ jsx2("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
478
+ /* @__PURE__ */ jsx2(
479
+ "button",
480
+ {
481
+ className: "lb-bundle__cta",
482
+ onClick: handleAddToCart,
483
+ disabled: cartLoading || !validation.valid,
484
+ "aria-busy": cartLoading,
485
+ children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${totalQuantity} Items to Cart`
486
+ }
487
+ )
488
+ ]
489
+ }
490
+ );
491
+ }
492
+
493
+ // src/components/VolumeBundle.tsx
494
+ import { useState as useState5, useCallback as useCallback5 } from "react";
495
+ import {
496
+ formatMoney as formatMoney3,
497
+ calculateTierSavings,
498
+ getActiveTier
499
+ } from "@lime-bundles/core";
500
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
501
+ function VolumeBundle(props) {
502
+ const {
503
+ shopDomain,
504
+ storefrontAccessToken,
505
+ bundleGid,
506
+ cartId,
507
+ cartApi,
508
+ appUrl,
509
+ analyticsEnabled,
510
+ onAddToCart,
511
+ onError,
512
+ className
513
+ } = props;
514
+ const { bundle, loading, error: loadError } = useBundleData({
515
+ shopDomain,
516
+ storefrontAccessToken,
517
+ bundleGid
518
+ });
519
+ const { addToCart, loading: cartLoading, error: cartError } = useCart({
520
+ shopDomain,
521
+ storefrontAccessToken,
522
+ bundleGid,
523
+ bundleType: "volume",
524
+ cartId,
525
+ cartApi
526
+ });
527
+ const { elementRef, trackAddToCart } = useAnalytics({
528
+ shopDomain,
529
+ appUrl: appUrl ?? `https://${shopDomain}`,
530
+ bundleGid,
531
+ bundleType: "volume",
532
+ enabled: analyticsEnabled !== false
533
+ });
534
+ const [quantity, setQuantity] = useState5(1);
535
+ const product = bundle?.products[0];
536
+ const basePrice = product ? parseFloat(product.priceRange.minVariantPrice.amount) : 0;
537
+ const currency = product?.priceRange.minVariantPrice.currencyCode ?? "USD";
538
+ const tierSavings = bundle ? calculateTierSavings(bundle.volumeTiers, basePrice, quantity) : [];
539
+ const activeTier = bundle ? getActiveTier(bundle.volumeTiers, quantity) : null;
540
+ const handleAddToCart = useCallback5(async () => {
541
+ if (!bundle || !product) return;
542
+ const variant = product.variants.nodes.find((v) => v.availableForSale);
543
+ if (!variant) return;
544
+ const items = [
545
+ { variantId: variant.id, quantity }
546
+ ];
547
+ const result = await addToCart(items);
548
+ if (result.success) {
549
+ const unitPrice = activeTier ? basePrice * (1 - activeTier.discountValue / 100) : basePrice;
550
+ trackAddToCart({
551
+ productId: product.id,
552
+ quantity,
553
+ totalPrice: Math.round(unitPrice * quantity * 100) / 100
554
+ });
555
+ onAddToCart?.(items);
556
+ }
557
+ }, [bundle, product, quantity, activeTier, basePrice, addToCart, trackAddToCart, onAddToCart]);
558
+ if (loading) {
559
+ return /* @__PURE__ */ jsxs3("div", { className: `lb-bundle lb-bundle--loading ${className ?? ""}`, children: [
560
+ /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--title" }),
561
+ /* @__PURE__ */ jsx3("div", { className: "lb-skeleton lb-skeleton--tiers" })
562
+ ] });
563
+ }
564
+ if (loadError || !bundle || !product) {
565
+ if (onError) onError(loadError ?? "Bundle not found");
566
+ return null;
567
+ }
568
+ return /* @__PURE__ */ jsxs3(
569
+ "div",
570
+ {
571
+ ref: elementRef,
572
+ className: `lb-bundle lb-bundle--volume ${className ?? ""}`,
573
+ role: "region",
574
+ "aria-label": bundle.title,
575
+ children: [
576
+ /* @__PURE__ */ jsx3("h3", { className: "lb-bundle__title", children: bundle.title }),
577
+ /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product lb-bundle__product--volume", children: [
578
+ product.featuredImage && /* @__PURE__ */ jsx3(
579
+ "img",
580
+ {
581
+ src: product.featuredImage.url,
582
+ alt: product.featuredImage.altText ?? product.title,
583
+ className: "lb-bundle__product-image",
584
+ loading: "lazy"
585
+ }
586
+ ),
587
+ /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__product-info", children: [
588
+ /* @__PURE__ */ jsx3("p", { className: "lb-bundle__product-title", children: product.title }),
589
+ /* @__PURE__ */ jsxs3("p", { className: "lb-bundle__product-price", children: [
590
+ formatMoney3(basePrice, currency),
591
+ " each"
592
+ ] })
593
+ ] })
594
+ ] }),
595
+ /* @__PURE__ */ jsx3("div", { className: "lb-bundle__tiers", role: "table", "aria-label": "Volume discounts", children: tierSavings.map((ts) => /* @__PURE__ */ jsxs3(
596
+ "div",
597
+ {
598
+ className: `lb-bundle__tier ${ts.isActive ? "lb-bundle__tier--active" : ""}`,
599
+ role: "row",
600
+ children: [
601
+ /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-quantity", role: "cell", children: [
602
+ ts.tier.minQuantity,
603
+ "+ items"
604
+ ] }),
605
+ /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-price", role: "cell", children: [
606
+ formatMoney3(ts.unitPrice, currency),
607
+ " each"
608
+ ] }),
609
+ /* @__PURE__ */ jsxs3("span", { className: "lb-bundle__tier-savings", role: "cell", children: [
610
+ "Save ",
611
+ ts.savingsPercent.toFixed(0),
612
+ "%"
613
+ ] }),
614
+ ts.tier.label && /* @__PURE__ */ jsx3("span", { className: "lb-bundle__tier-label", role: "cell", children: ts.tier.label })
615
+ ]
616
+ },
617
+ ts.tier.minQuantity
618
+ )) }),
619
+ /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-selector", children: [
620
+ /* @__PURE__ */ jsx3("label", { htmlFor: `lb-qty-${bundle.id}`, children: "Quantity" }),
621
+ /* @__PURE__ */ jsxs3("div", { className: "lb-bundle__quantity-control", children: [
622
+ /* @__PURE__ */ jsx3(
623
+ "button",
624
+ {
625
+ "aria-label": "Decrease quantity",
626
+ onClick: () => setQuantity((q) => Math.max(1, q - 1)),
627
+ children: "\u2212"
628
+ }
629
+ ),
630
+ /* @__PURE__ */ jsx3(
631
+ "input",
632
+ {
633
+ id: `lb-qty-${bundle.id}`,
634
+ type: "number",
635
+ min: 1,
636
+ value: quantity,
637
+ onChange: (e) => {
638
+ const val = parseInt(e.target.value, 10);
639
+ if (!isNaN(val) && val > 0) setQuantity(val);
640
+ },
641
+ className: "lb-bundle__quantity-input"
642
+ }
643
+ ),
644
+ /* @__PURE__ */ jsx3(
645
+ "button",
646
+ {
647
+ "aria-label": "Increase quantity",
648
+ onClick: () => setQuantity((q) => q + 1),
649
+ children: "+"
650
+ }
651
+ )
652
+ ] })
653
+ ] }),
654
+ cartError && /* @__PURE__ */ jsx3("p", { className: "lb-bundle__error", role: "alert", children: cartError }),
655
+ /* @__PURE__ */ jsx3(
656
+ "button",
657
+ {
658
+ className: "lb-bundle__cta",
659
+ onClick: handleAddToCart,
660
+ disabled: cartLoading,
661
+ "aria-busy": cartLoading,
662
+ children: cartLoading ? "Adding..." : bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`
663
+ }
664
+ )
665
+ ]
666
+ }
667
+ );
668
+ }
669
+ export {
670
+ FixedBundle,
671
+ MixMatchBundle,
672
+ VolumeBundle,
673
+ useAnalytics,
674
+ useBundleData,
675
+ useCart
676
+ };
677
+ //# sourceMappingURL=index.js.map