@lime-bundles/widget 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../core/src/storefront-api/client.ts","../../core/src/storefront-api/queries.ts","../../core/src/storefront-api/cache-key.ts","../../core/src/bundle/types.ts","../../core/src/bundle/widget-config.ts","../../core/src/bundle/parser.ts","../../core/src/bundle/fetch-by-product.ts","../../core/src/bundle/tier-calculator.ts","../../core/src/bundle/validator.ts","../../core/src/bundle/qty.ts","../../core/src/bundle/ab-merge.ts","../../core/src/analytics/reporter.ts","../../core/src/ab-test/consent.ts","../../core/src/ab-test/assigner.ts","../../core/src/styles/sanitize.ts","../../core/src/styles/custom-css-injector.ts","../../core/src/utils/money.ts","../../core/src/utils/pricing.ts","../../core/src/utils/image.ts","../../core/src/utils/countdown.ts","../../core/src/dropdown/index.ts","../../core/src/dropdown/compute-position.ts","../../core/src/dropdown/keyboard.ts","../../core/src/dropdown/type-ahead.ts","../../core/src/variants/picker.ts","../../core/src/inventory/predicate.ts","../src/dropdown/bind-dropdown.ts","../src/renderers/countdown.ts","../src/renderers/cta-button.ts","../src/renderers/dom.ts","../src/renderers/fixed.ts","../src/renderers/mix-match.ts","../src/renderers/volume.ts","../src/utils/input-mode.ts","../src/styles/bundle-css.ts","../src/lime-bundle.ts"],"sourcesContent":["/**\n * @lime-bundles/widget — Vanilla JS Web Component for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-bundle.js\"></script>\n * <lime-bundle shop-domain=\"...\" storefront-token=\"...\" bundle-gid=\"...\"></lime-bundle>\n */\nimport { LimeBundleElement } from \"./lime-bundle\";\n\n// Register custom element\nif (\n typeof customElements !== \"undefined\" &&\n !customElements.get(\"lime-bundle\")\n) {\n customElements.define(\"lime-bundle\", LimeBundleElement);\n}\n\nexport { LimeBundleElement };\nexport { trackInputMode } from \"./utils/input-mode\";\n","/**\n * Lightweight Storefront API client.\n * No framework dependencies — just fetch.\n *\n * SSR and Trusted Publishing environments (Hydrogen, Next.js server\n * components) MUST forward the end-buyer's IP via the `buyerIp` config\n * field. Without it Shopify may return `430 Shopify Security Rejection`\n * for server-originated traffic. Pass the IP in IPv4 or IPv6 string form.\n */\n\nexport class StorefrontApiError extends Error {\n constructor(\n public readonly errors: Array<{ message: string; locations?: unknown[] }>,\n ) {\n super(errors.map((e) => e.message).join(\"; \"));\n this.name = \"StorefrontApiError\";\n }\n}\n\nexport interface StorefrontClient {\n query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T>;\n}\n\nexport interface QueryOptions {\n /** Abort the request mid-flight. */\n signal?: AbortSignal;\n}\n\n/**\n * B2B buyer identity for Storefront API `@inContext(buyer: ...)`.\n * `customerAccessToken` is required when `buyer` is set (per Shopify's\n * BuyerInput spec). `companyLocationId` selects a specific B2B location\n * for catalog pricing when the customer has access to multiple.\n *\n * Security: this token MUST come from a same-origin authenticated endpoint\n * (Customer Account API PKCE flow) and MUST NOT be passed via URL params,\n * localStorage shared with third-party scripts, or static prop trees that\n * land in DOM snapshots. See each package's README (\"Markets & B2B\").\n */\nexport interface BuyerInput {\n customerAccessToken: string;\n companyLocationId?: string;\n}\n\n/** Lazy buyer resolver — called per request so tokens stay off static props. */\nexport type BuyerResolver = BuyerInput | (() => BuyerInput | Promise<BuyerInput>);\n\nexport interface StorefrontClientConfig {\n shopDomain: string;\n accessToken: string;\n apiVersion?: string;\n /**\n * End-buyer's IP, forwarded as `Shopify-Storefront-Buyer-IP`. Required\n * for server-side calls with private tokens and recommended for all SSR.\n * See the class doc-comment.\n */\n buyerIp?: string;\n /**\n * ISO-3166 alpha-2 country code passed as `@inContext(country: ...)`.\n * Drives Shopify Markets pricing, currency, and availability. Omit for\n * the shop's default market.\n */\n country?: string;\n /**\n * ISO-639-1 language code passed as `@inContext(language: ...)`.\n * Translations come from the shop's locale settings. Omit for default.\n */\n language?: string;\n /**\n * B2B buyer identity. Either a plain object (token captured at config\n * time) or a callback that resolves per request (recommended for\n * security). When set, the SDK adds `@inContext(buyer: ...)` to queries\n * and the response carries B2B catalog prices for that customer.\n */\n buyer?: BuyerResolver;\n}\n\n// Keep in sync with CLAUDE.md → Shopify API Version Alignment.\nconst DEFAULT_API_VERSION = \"2025-10\";\n\nasync function resolveBuyer(buyer?: BuyerResolver): Promise<BuyerInput | undefined> {\n if (!buyer) return undefined;\n if (typeof buyer === \"function\") {\n return await buyer();\n }\n return buyer;\n}\n\n/**\n * Returns true if any `@inContext` field is set. Used by the query layer\n * to decide whether to emit the directive-wrapped or directive-free\n * variant — keeps cache fragmentation off for the no-context case.\n */\nexport function hasInContext(config: StorefrontClientConfig): boolean {\n return Boolean(config.country || config.language || config.buyer);\n}\n\nexport function createStorefrontClient(\n config: StorefrontClientConfig,\n): StorefrontClient {\n const version = config.apiVersion ?? DEFAULT_API_VERSION;\n const endpoint = `https://${config.shopDomain}/api/${version}/graphql.json`;\n\n return {\n async query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Shopify-Storefront-Access-Token\": config.accessToken,\n };\n if (config.buyerIp) {\n headers[\"Shopify-Storefront-Buyer-IP\"] = config.buyerIp;\n }\n\n // Inject @inContext variables when set. The query string template\n // already declares the optional `$country`, `$language`, and\n // `$buyer` variables; the directive is emitted via the caller\n // selecting the *_INCONTEXT query variant.\n const buyer = await resolveBuyer(config.buyer);\n const mergedVariables: Record<string, unknown> = { ...(variables ?? {}) };\n if (config.country) mergedVariables.country = config.country;\n if (config.language) mergedVariables.language = config.language;\n if (buyer) mergedVariables.buyer = buyer;\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables: mergedVariables }),\n signal: options?.signal,\n });\n\n if (!response.ok) {\n throw new StorefrontApiError([\n {\n message: `Storefront API error: ${response.status} ${response.statusText}`,\n },\n ]);\n }\n\n const json = (await response.json()) as {\n data?: T;\n errors?: Array<{ message: string }>;\n };\n\n if (json.errors?.length) {\n throw new StorefrontApiError(json.errors);\n }\n\n return json.data as T;\n },\n };\n}\n","/**\n * Storefront API GraphQL queries.\n *\n * Pricing-sensitive queries (BUNDLE_METAOBJECT_QUERY, BUNDLES_FOR_PRODUCT_QUERY)\n * are wrapped with `@inContext(country:, language:, buyer:)` by\n * `withInContext()` below when the SDK config has any context field set.\n * The unwrapped variant stays publicly cacheable; the wrapped variant\n * must be marked `Cache-Control: private` per Hydrogen guidance for\n * buyer-contextual responses.\n *\n * The SDK fetcher selects the variant at request time via `hasInContext()`.\n */\nimport type { MetaobjectField } from \"./types\";\n\n/**\n * Returns the `@inContext`-wrapped variant of `query`. Adds the three\n * optional variables to the parameter list and the directive to the\n * operation root. Variable substitution itself is handled by the\n * Storefront client which injects `country`, `language`, `buyer` into\n * the request variables.\n *\n * Handles both arg-list (`query Name($x: Int) { ... }`) and zero-arg\n * (`query Name { ... }`) operations. Mutations are not supported —\n * adjust if one ever needs `@inContext`.\n */\nexport function withInContext(query: string): string {\n return query.replace(\n /query\\s+(\\w+)\\s*(?:\\(([^)]*)\\))?\\s*\\{/,\n (_match, name, args) => {\n const trimmed = (args || \"\").trim();\n const extra = \"$country: CountryCode, $language: LanguageCode, $buyer: BuyerInput\";\n const combined = trimmed ? `${trimmed}, ${extra}` : extra;\n return `query ${name}(${combined}) @inContext(country: $country, language: $language, buyer: $buyer) {`;\n },\n );\n}\n\nexport const BUNDLE_METAOBJECT_QUERY = `#graphql\n query BundleMetaobject($id: ID!) {\n metaobject(id: $id) {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Shop-level custom CSS metafield query. The SDK fetches this alongside\n * bundle data and auto-injects the result as a scoped <style> tag so\n * headless storefronts get CSS parity with classic-theme merchants.\n */\nexport const SHOP_CUSTOM_CSS_QUERY = `#graphql\n query ShopCustomCss {\n shop {\n metafield(namespace: \"$app\", key: \"custom_css\") {\n value\n }\n }\n }\n`;\n\nexport interface ShopCustomCssResponse {\n shop: {\n metafield: { value: string | null } | null;\n } | null;\n}\n\n/**\n * Product-aware bundle lookup. Mirrors the pattern the classic Liquid theme\n * block uses (`product.metafields[\"$app\"].active_bundle_ids`): one query\n * returns every bundle that applies to the current product, fully resolved\n * (nested product/collection references included) so the caller gets the\n * same shape as `parseMetaobjectBundleStrict` expects.\n *\n * The product handle is the natural input on a headless storefront — every\n * canonical Shopify product URL is `/products/<handle>`, so the widget can\n * often derive it from `window.location` without merchant input.\n *\n * Complexity: the nested references (bundle → products → variants) are deep\n * but stay within the Storefront API budget for token-based queries at\n * realistic bundle sizes (≤10 bundles per product, ≤50 products per bundle).\n */\nexport const BUNDLES_FOR_PRODUCT_QUERY = `#graphql\n query BundlesForProduct($handle: String!) {\n product(handle: $handle) {\n id\n handle\n title\n metafield(namespace: \"$app\", key: \"active_bundle_ids\") {\n references(first: 10) {\n nodes {\n ... on Metaobject {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Return shape for BUNDLES_FOR_PRODUCT_QUERY. The inner `fields` array\n * reuses `MetaobjectField` verbatim so callers can pass it straight to\n * `parseMetaobjectBundleStrict` without a type cast — the same shape the\n * BUNDLE_METAOBJECT_QUERY response produces for a single bundle.\n */\nexport interface BundlesForProductResponse {\n product: {\n id: string;\n handle: string;\n title: string;\n metafield: {\n references: {\n nodes: Array<{\n id: string;\n type: string;\n fields: MetaobjectField[];\n }>;\n };\n } | null;\n } | null;\n}\n\n/**\n * Tokenless cart mutations. Storefront API docs: \"Cart (read/write)\" is\n * available on the Tokenless Access tier. The widget's default-cart path\n * uses these so merchants don't need a cart implementation.\n */\nexport const CART_CREATE_MUTATION = `#graphql\n mutation CartCreate($input: CartInput!) {\n cartCreate(input: $input) {\n cart { id checkoutUrl }\n userErrors { field message }\n }\n }\n`;\n\nexport const CART_LINES_ADD_MUTATION = `#graphql\n mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $lines) {\n cart { id checkoutUrl }\n userErrors { field message }\n }\n }\n`;\n\n/**\n * Inner payload shape shared by both cart mutations. Not a GraphQL\n * response on its own — see `CartCreateResponse` and `CartLinesAddResponse`\n * for the correctly-wrapped top-level response shapes.\n */\nexport interface CartMutationPayload {\n cart: { id: string; checkoutUrl: string } | null;\n userErrors: Array<{ field: string[] | null; message: string }>;\n}\n\n/** Top-level response shape for `CART_CREATE_MUTATION`. */\nexport interface CartCreateResponse {\n cartCreate: CartMutationPayload;\n}\n\n/** Top-level response shape for `CART_LINES_ADD_MUTATION`. */\nexport interface CartLinesAddResponse {\n cartLinesAdd: CartMutationPayload;\n}\n","/**\n * Stable cache-key helper for SWR / TanStack Query / Hydrogen consumers.\n *\n * Buyer-contextual queries (Storefront `@inContext(buyer:)`) must NEVER\n * be shared across users — per Hydrogen's caching guidance, set\n * Cache-Control: private on those responses and ensure your cache key\n * includes a buyer fingerprint, never the raw token.\n *\n * Example usage with TanStack Query:\n * queryKey: getCacheKey(\"BundleMetaobject\", { id }, {\n * shopDomain, country, language, buyer,\n * })\n */\nimport type { BuyerInput } from \"./client\";\n\nexport interface CacheKeyContext {\n shopDomain: string;\n country?: string;\n language?: string;\n /**\n * Pass the resolved BuyerInput or undefined. Never pass a function —\n * the consumer is responsible for resolving lazy buyers before keying.\n * The raw `customerAccessToken` is hashed before being included in the\n * key so it doesn't appear in devtools / log output.\n */\n buyer?: BuyerInput;\n}\n\nexport function getCacheKey(\n queryName: string,\n variables: Record<string, unknown>,\n ctx: CacheKeyContext,\n): string[] {\n return [\n ctx.shopDomain,\n queryName,\n stableStringify(variables),\n ctx.country ?? \"_\",\n ctx.language ?? \"_\",\n ctx.buyer ? buyerFingerprint(ctx.buyer) : \"_\",\n ];\n}\n\n/**\n * Deterministic JSON.stringify with sorted keys. Avoids cache misses caused\n * by object property order varying across consumers.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) {\n return \"[\" + value.map(stableStringify).join(\",\") + \"]\";\n }\n const entries = Object.entries(value as Record<string, unknown>).sort(\n ([a], [b]) => a.localeCompare(b),\n );\n return (\n \"{\" +\n entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(\",\") +\n \"}\"\n );\n}\n\n/**\n * Lightweight non-cryptographic hash of the buyer token + optional company\n * location. Stable across runs, fast, and good enough to discriminate\n * cache entries per buyer without leaking the raw token. Don't use this\n * for security purposes — it's purely for cache keying.\n */\nfunction buyerFingerprint(buyer: BuyerInput): string {\n const input = `${buyer.customerAccessToken}:${buyer.companyLocationId ?? \"\"}`;\n // FNV-1a 32-bit\n let hash = 2166136261;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 16777619);\n }\n // Unsigned hex, 8 chars\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n","/**\n * Bundle types for headless rendering.\n *\n * `ParsedBundle` is a discriminated union keyed on `bundleType` so consumers\n * can narrow the type via a switch statement or conditional check:\n *\n * function render(bundle: ParsedBundle) {\n * switch (bundle.bundleType) {\n * case \"fixed\": // bundle.products is load-bearing\n * case \"volume\": // bundle.volumeTiers is load-bearing\n * case \"mix_match\": // bundle.minQuantity / maxQuantity are load-bearing\n * }\n * }\n *\n * Each variant carries ONLY the fields it actually uses. Fields that are\n * always present (id, title, status, schedule, widget config, A/B test\n * metadata) live on the shared `BundleBase` type.\n *\n * Locked for v2.0.0 — widening or narrowing these discriminants is a\n * breaking change. `WidgetConfig` widened in 2.0.0 to expose the full\n * admin-editor shape; see WidgetConfig for the eight sub-section structure.\n */\nimport type { Product } from \"../storefront-api/types\";\n\nexport type BundleType = \"fixed\" | \"mix_match\" | \"volume\";\nexport type BundleStatus = \"active\" | \"draft\" | \"inactive\";\n\nexport interface DiscountConfig {\n discountType: \"percentage\" | \"fixed_amount\";\n discountValue: number;\n allowStacking: boolean;\n}\n\n/**\n * Widget configuration as stored in the bundle metaobject's `widget_config`\n * field. Structured by visual area to mirror the in-app editor's accordion.\n *\n * The web component (`<lime-bundle>`) applies every value as a `--lb-*` CSS\n * custom property so the rendered widget matches what the merchant saw in\n * the editor. The React SDK exposes this object unchanged on\n * `ParsedBundle.widgetConfig`; whether and how to apply it is up to the\n * developer.\n */\nexport interface HeaderConfig {\n textColor: string;\n headerStyle: \"solid\" | \"gradient\";\n gradientStart: string;\n gradientEnd: string;\n saveBadgeBgColor: string;\n saveBadgeTextColor: string;\n saveBadgeBorderColor: string;\n saveBadgeBorderWidth: number;\n saveBadgeBorderRadius: number;\n countdownBgColor: string;\n countdownTextColor: string;\n}\n\nexport interface LayoutConfig {\n backgroundColor: string;\n borderColor: string;\n borderWidth: number;\n borderRadius: number;\n}\n\n/**\n * Thumbnail aspect-ratio choice. Drives a derived block in\n * applyWidgetConfigVars that sets three CSS vars\n * (--lb-thumbnail-aspect-ratio, --lb-thumbnail-img-fit,\n * --lb-thumbnail-img-height). \"original\" disables the aspect crop and\n * lets each image render at its intrinsic ratio.\n */\nexport type ThumbnailRatio = \"square\" | \"tall\" | \"wide\" | \"original\";\n\nexport interface ProductListConfig {\n textColor: string;\n imageBorderWidth: number;\n imageBorderColor: string;\n imageBorderRadius: number;\n thumbnailRatio: ThumbnailRatio;\n variantBorderWidth: number;\n variantBorderColor: string;\n variantBorderRadius: number;\n showPrice: boolean;\n showCompareAtPrice: boolean;\n showCountBubble: boolean;\n countBubbleBgColor: string;\n countBubbleTextColor: string;\n}\n\nexport interface PricingConfig {\n showSaveBadge: boolean;\n showComparePrice: boolean;\n showPerUnitPrice: boolean;\n showItemCount: boolean;\n showCompareAtPrice: boolean;\n}\n\nexport interface SavingsBarConfig {\n visible: boolean;\n bgColor: string;\n textColor: string;\n borderWidth: number;\n borderColor: string;\n borderRadius: number;\n}\n\nexport interface CtaConfig {\n ctaText: string;\n primaryColor: string;\n buttonTextColor: string;\n borderWidth: number;\n borderColor: string;\n borderRadius: number;\n}\n\nexport interface CountdownConfig {\n showCountdown: boolean;\n}\n\nexport interface PopularBadgeConfig {\n visible: boolean;\n text: string;\n /** Optional — merchant may not pick a tier. */\n tierIndex?: number;\n bgColor: string;\n textColor: string;\n borderWidth: number;\n borderColor: string;\n borderRadius: number;\n}\n\nexport interface WidgetConfig {\n header: HeaderConfig;\n layout: LayoutConfig;\n productList: ProductListConfig;\n pricing: PricingConfig;\n cta: CtaConfig;\n savingsBar: SavingsBarConfig;\n countdown: CountdownConfig;\n popularBadge: PopularBadgeConfig;\n outOfStockBehavior: \"show_greyed_out\" | \"hide\";\n /** Show \"Only X left\" badge when remaining stock is at or below\n * `lowStockThreshold`. Hidden silently when the Storefront token\n * lacks `unauthenticated_read_product_inventory`. */\n showLowStockBadge: boolean;\n /** Threshold (1-99) below which the low-stock badge appears. */\n lowStockThreshold: number;\n lowStockBgColor: string;\n lowStockTextColor: string;\n\n // --- Mix-match picker section ---\n showSearch: boolean;\n /**\n * When true (default), the picker modal renders a per-pick qty stepper so\n * the shopper can choose how many of each product to add. When false, the\n * stepper is hidden and Add adds qty = `productRules[productId]?.min ?? 1`.\n */\n mixMatchShowQuantitySelector: boolean;\n pickerBgColor: string;\n pickerTextColor: string;\n pickerBorderWidth: number;\n pickerBorderColor: string;\n pickerBorderRadius: number;\n pickerSearchBorderWidth: number;\n pickerSearchBorderColor: string;\n pickerSearchBorderRadius: number;\n pickerProductBorderWidth: number;\n pickerProductBorderColor: string;\n pickerProductBorderRadius: number;\n /** Aspect ratio for product thumbnails inside the picker modal —\n * independent of the main widget's `productList.thumbnailRatio`. */\n pickerThumbnailRatio: ThumbnailRatio;\n pickerShowCountBubble: boolean;\n pickerCountBubbleBgColor: string;\n pickerCountBubbleTextColor: string;\n pickerAddBgColor: string;\n pickerAddLabelColor: string;\n pickerAddBorderWidth: number;\n pickerAddBorderColor: string;\n pickerAddBorderRadius: number;\n pickerVariantBorderWidth: number;\n pickerVariantBorderColor: string;\n pickerVariantBorderRadius: number;\n pickerQtyStepperBorderWidth: number;\n pickerQtyStepperBorderColor: string;\n pickerQtyStepperBorderRadius: number;\n\n // --- Volume tier section ---\n tierBorderColor: string;\n tierBorderWidth: number;\n tierBorderRadius: number;\n tierSelectedBorderColor: string;\n tierSelectedBorderWidth: number;\n defaultTier: \"first\" | \"best_value\" | number;\n}\n\n/**\n * A single volume-bundle tier row. `discountType` lives at the bundle\n * level (`bundle.discountConfig.discountType`) — each tier carries only\n * the magnitude in whichever field matches that type:\n * - `percentage` is a whole-number (e.g. 10 for 10% off)\n * - `amount` is the per-unit fixed amount off, in the shop's currency\n *\n * Exactly one of `percentage` or `amount` is present per tier, determined\n * by the parent bundle's `discountType`. This mirrors the on-metaobject\n * shape — see `app/lib/bundle-types.ts` VolumeTierSchema.\n */\nexport interface VolumeTier {\n minQuantity: number;\n percentage?: number;\n amount?: number;\n}\n\n/**\n * A/B variant override. Present when an A/B test is active on the bundle\n * and this visitor is bucketed into Variant B. Fields are individually\n * optional — merchants can A/B-test any subset (e.g. just the discount\n * value, or just the title). Fields absent from the override fall back\n * to the base bundle's values.\n */\nexport interface ABVariantOverrides {\n title?: string;\n description?: string;\n discountConfig?: DiscountConfig;\n volumeTiers?: VolumeTier[];\n}\n\n/** Fields present on every bundle regardless of type. */\ninterface BundleBase {\n id: string;\n title: string;\n /** Customer-facing subtitle rendered under the bundle title. */\n description: string | null;\n status: BundleStatus;\n products: Product[];\n discountConfig: DiscountConfig;\n widgetConfig: WidgetConfig;\n startsAt: string | null;\n endsAt: string | null;\n discountLabel: string | null;\n\n /**\n * Per-product allowed-variant lists, positionally aligned with\n * `products`. When the merchant restricted a product to specific\n * variants, only those GIDs should appear in the picker and dropdown.\n * Inner array empty or whole entry null means \"all available variants\".\n */\n selectedVariantIds: string[][] | null;\n\n /** Per-product bundle quantity, keyed by Shopify Product GID. */\n productQuantities: Record<string, number>;\n /** Per-variant quantity override, keyed by Shopify ProductVariant GID. */\n variantQuantities: Record<string, number>;\n\n abTestId: string | null;\n /** Variant B overrides, present when an A/B test is active. */\n abVariantB: ABVariantOverrides | null;\n\n /**\n * Market visibility scoping.\n * - \"all\" (default): bundle is visible in every Shopify market.\n * - \"specific\": bundle is visible only in the markets listed in marketIds.\n *\n * Consumers querying with a marketId can use isVisibleInMarket() to filter.\n */\n marketVisibility: \"all\" | \"specific\";\n /**\n * Allowed Shopify Market GIDs when marketVisibility = \"specific\".\n * Empty when marketVisibility = \"all\".\n * Example: [\"gid://shopify/Market/1\", \"gid://shopify/Market/2\"]\n */\n marketIds: string[];\n}\n\nexport interface FixedBundleData extends BundleBase {\n bundleType: \"fixed\";\n}\n\nexport interface VolumeBundleData extends BundleBase {\n bundleType: \"volume\";\n volumeTiers: VolumeTier[];\n}\n\n/** Per-product unit-count rule for mix & match bundles. */\nexport interface ProductRule {\n /** Smallest qty the shopper must add when picking this product. */\n min: number;\n /** Largest qty the shopper may add for this pick. >= min. */\n max: number;\n}\n\n/** Default rule applied to products without an explicit entry (e.g. collection-sourced). */\nexport const DEFAULT_PRODUCT_RULE: ProductRule = { min: 1, max: 99 };\n\nexport interface MixMatchBundleData extends BundleBase {\n bundleType: \"mix_match\";\n /** Number of distinct products the shopper must pick to qualify for the discount. */\n minQuantity: number | null;\n /**\n * @deprecated removed from the data model — always null on parsed bundles.\n * The bundle no longer has a top-level total cap; per-product max lives in productRules.\n */\n maxQuantity: number | null;\n /** Per-product unit-count rules keyed by product GID. Missing entries default to DEFAULT_PRODUCT_RULE. */\n productRules: Record<string, ProductRule>;\n}\n\nexport type ParsedBundle =\n | FixedBundleData\n | VolumeBundleData\n | MixMatchBundleData;\n\n/**\n * Thrown when metaobject fields cannot be parsed into a valid ParsedBundle.\n * Distinct from StorefrontApiError so callers can handle \"bundle not\n * renderable\" (inactive, expired, schema mismatch) differently from\n * \"network failure\".\n */\nexport class BundleParseError extends Error {\n constructor(message: string, public readonly reason: BundleParseFailReason) {\n super(message);\n this.name = \"BundleParseError\";\n }\n}\n\nexport type BundleParseFailReason =\n | \"not_found\"\n | \"invalid_type\"\n | \"inactive\"\n | \"not_started\"\n | \"expired\";\n","/**\n * Default widget config + CSS-variable flattener.\n *\n * Mirrors the Zod defaults and `flattenWidgetConfig` in the admin package's\n * `app/lib/bundle-types.ts`. Kept in plain TypeScript so the SDK packages\n * stay Zod-free; merchant-authored config is validated at the admin\n * boundary before it reaches the metaobject, so consumers of this module\n * can trust the JSON shape and just merge against these defaults.\n *\n * If a future merchant-editor field is added:\n * 1. Update `WidgetConfig` in types.ts\n * 2. Add the default here\n * 3. Add the flattened CSS-var key below\n * 4. Consume the var in packages/widget/src/styles/*.css\n */\nimport type { WidgetConfig } from \"./types\";\n\nexport const WIDGET_CONFIG_DEFAULTS: WidgetConfig = {\n header: {\n textColor: \"#555555\",\n headerStyle: \"solid\",\n gradientStart: \"#C1E67A\",\n gradientEnd: \"#B2D572\",\n saveBadgeBgColor: \"#C1E67A\",\n saveBadgeTextColor: \"#555555\",\n saveBadgeBorderColor: \"#555555\",\n saveBadgeBorderWidth: 2,\n saveBadgeBorderRadius: 4,\n countdownBgColor: \"#B2D57247\",\n countdownTextColor: \"#555555\",\n },\n layout: {\n backgroundColor: \"#FCFCFC\",\n borderColor: \"#E5E5E5\",\n borderWidth: 1,\n borderRadius: 8,\n },\n productList: {\n textColor: \"#555555\",\n imageBorderWidth: 0,\n imageBorderColor: \"#E5E5E5\",\n imageBorderRadius: 4,\n thumbnailRatio: \"square\",\n variantBorderWidth: 1,\n variantBorderColor: \"#E5E5E5\",\n variantBorderRadius: 4,\n showPrice: true,\n showCompareAtPrice: true,\n showCountBubble: true,\n countBubbleBgColor: \"#E9F1D9\",\n countBubbleTextColor: \"#555555\",\n },\n pricing: {\n showSaveBadge: true,\n showComparePrice: true,\n showPerUnitPrice: true,\n showItemCount: true,\n showCompareAtPrice: true,\n },\n cta: {\n ctaText: \"Add to cart\",\n primaryColor: \"#C1E67A\",\n buttonTextColor: \"#555555\",\n borderWidth: 0,\n borderColor: \"#C1E67A\",\n borderRadius: 4,\n },\n savingsBar: {\n visible: true,\n bgColor: \"#B2D57247\",\n textColor: \"#555555\",\n borderWidth: 0,\n borderColor: \"#B2D57247\",\n borderRadius: 4,\n },\n countdown: {\n showCountdown: true,\n },\n popularBadge: {\n visible: true,\n text: \"Most Popular\",\n bgColor: \"#1A1A1A\",\n textColor: \"#FFFFFF\",\n borderWidth: 0,\n borderColor: \"#1A1A1A\",\n borderRadius: 12,\n },\n outOfStockBehavior: \"show_greyed_out\",\n showLowStockBadge: true,\n lowStockThreshold: 10,\n lowStockBgColor: \"#FEF3C7\",\n lowStockTextColor: \"#92400E\",\n\n showSearch: true,\n mixMatchShowQuantitySelector: true,\n pickerBgColor: \"#FCFCFC\",\n pickerTextColor: \"#555555\",\n pickerBorderWidth: 0,\n pickerBorderColor: \"#E5E5E5\",\n pickerBorderRadius: 8,\n pickerSearchBorderWidth: 1,\n pickerSearchBorderColor: \"#E5E5E5\",\n pickerSearchBorderRadius: 8,\n pickerProductBorderWidth: 0,\n pickerProductBorderColor: \"#E5E5E5\",\n pickerProductBorderRadius: 8,\n pickerThumbnailRatio: \"square\",\n pickerShowCountBubble: true,\n pickerCountBubbleBgColor: \"#E9F1D9\",\n pickerCountBubbleTextColor: \"#555555\",\n pickerAddBgColor: \"#C1E67A\",\n pickerAddLabelColor: \"#555555\",\n pickerAddBorderWidth: 0,\n pickerAddBorderColor: \"#C1E67A\",\n pickerAddBorderRadius: 4,\n pickerVariantBorderWidth: 1,\n pickerVariantBorderColor: \"#555555\",\n pickerVariantBorderRadius: 4,\n pickerQtyStepperBorderWidth: 1,\n pickerQtyStepperBorderColor: \"#555555\",\n pickerQtyStepperBorderRadius: 4,\n\n tierBorderColor: \"#E5E5E5\",\n tierBorderWidth: 1,\n tierBorderRadius: 12,\n tierSelectedBorderColor: \"#1A1A1A\",\n tierSelectedBorderWidth: 2,\n defaultTier: \"first\",\n};\n\n/**\n * Merge a partial (possibly stale-schema) widget config against the\n * defaults, so downstream renderers can trust every field. Top-level and\n * nested-section fields both merge shallow; unknown keys on nested sections\n * are preserved (but ignored by the renderer). This is what the parser\n * calls after pulling `widget_config` JSON from the metaobject.\n */\nexport function mergeWidgetConfig(raw: unknown): WidgetConfig {\n if (!raw || typeof raw !== \"object\") {\n return WIDGET_CONFIG_DEFAULTS;\n }\n const input = raw as Partial<WidgetConfig> & Record<string, unknown>;\n return {\n ...WIDGET_CONFIG_DEFAULTS,\n ...input,\n defaultTier: sanitizeDefaultTier(input.defaultTier),\n header: { ...WIDGET_CONFIG_DEFAULTS.header, ...(input.header ?? {}) },\n layout: { ...WIDGET_CONFIG_DEFAULTS.layout, ...(input.layout ?? {}) },\n productList: {\n ...WIDGET_CONFIG_DEFAULTS.productList,\n ...(input.productList ?? {}),\n },\n pricing: {\n ...WIDGET_CONFIG_DEFAULTS.pricing,\n ...(input.pricing ?? {}),\n },\n cta: { ...WIDGET_CONFIG_DEFAULTS.cta, ...(input.cta ?? {}) },\n savingsBar: {\n ...WIDGET_CONFIG_DEFAULTS.savingsBar,\n ...(input.savingsBar ?? {}),\n },\n countdown: {\n ...WIDGET_CONFIG_DEFAULTS.countdown,\n ...(input.countdown ?? {}),\n },\n popularBadge: {\n ...WIDGET_CONFIG_DEFAULTS.popularBadge,\n ...(input.popularBadge ?? {}),\n },\n };\n}\n\nfunction sanitizeDefaultTier(\n raw: unknown,\n): \"first\" | \"best_value\" | number {\n if (raw === \"first\" || raw === \"best_value\") return raw;\n if (typeof raw === \"number\" && Number.isInteger(raw) && raw >= 0) return raw;\n return WIDGET_CONFIG_DEFAULTS.defaultTier;\n}\n\n/**\n * Flatten the nested WidgetConfig to the CSS-custom-property keys consumed\n * by `packages/widget/src/styles/*.css`. Keys match the `--lb-*` variable\n * names (without the `--lb-` prefix). Keep in lockstep with the matching\n * function in `app/lib/bundle-types.ts` — the admin's in-app preview and\n * the shipped web component both read the same variable names, so drift\n * between the two maps is a merchant-visible rendering bug.\n */\nexport function flattenWidgetConfig(\n config: WidgetConfig,\n): Record<string, string | number | boolean> {\n return {\n primaryColor: config.cta.primaryColor,\n backgroundColor: config.layout.backgroundColor,\n textColor: config.productList.textColor,\n borderColor: config.layout.borderColor,\n borderWidth: config.layout.borderWidth,\n buttonTextColor: config.cta.buttonTextColor,\n ctaBorderWidth: config.cta.borderWidth,\n ctaBorderColor: config.cta.borderColor,\n ctaBorderRadius: config.cta.borderRadius,\n savingsBarBgColor: config.savingsBar.bgColor,\n savingsBarTextColor: config.savingsBar.textColor,\n savingsBarBorderWidth: config.savingsBar.borderWidth,\n savingsBarBorderColor: config.savingsBar.borderColor,\n savingsBarBorderRadius: config.savingsBar.borderRadius,\n borderRadius: config.layout.borderRadius,\n headerTextColor: config.header.textColor,\n saveBadgeBgColor: config.header.saveBadgeBgColor,\n saveBadgeTextColor: config.header.saveBadgeTextColor,\n saveBadgeBorderColor: config.header.saveBadgeBorderColor,\n saveBadgeBorderWidth: config.header.saveBadgeBorderWidth,\n saveBadgeBorderRadius: config.header.saveBadgeBorderRadius,\n countdownBgColor: config.header.countdownBgColor,\n countdownTextColor: config.header.countdownTextColor,\n headerStyle: config.header.headerStyle,\n headerGradientStart: config.header.gradientStart,\n headerGradientEnd: config.header.gradientEnd,\n showSaveBadge: config.pricing.showSaveBadge,\n imageBorderWidth: config.productList.imageBorderWidth,\n imageBorderColor: config.productList.imageBorderColor,\n imageBorderRadius: config.productList.imageBorderRadius,\n thumbnailRatio: config.productList.thumbnailRatio,\n variantBorderWidth: config.productList.variantBorderWidth,\n variantBorderColor: config.productList.variantBorderColor,\n variantBorderRadius: config.productList.variantBorderRadius,\n countBubbleBgColor: config.productList.countBubbleBgColor,\n countBubbleTextColor: config.productList.countBubbleTextColor,\n showProductPrice: config.productList.showPrice,\n showProductCompareAtPrice: config.productList.showCompareAtPrice,\n showCountBubble: config.productList.showCountBubble,\n showCountdown: config.countdown.showCountdown,\n ctaText: config.cta.ctaText,\n outOfStockBehavior: config.outOfStockBehavior,\n showLowStockBadge: config.showLowStockBadge,\n lowStockThreshold: config.lowStockThreshold,\n lowStockBgColor: config.lowStockBgColor,\n lowStockTextColor: config.lowStockTextColor,\n showComparePrice: config.pricing.showComparePrice,\n showPerUnitPrice: config.pricing.showPerUnitPrice,\n showItemCount: config.pricing.showItemCount,\n showCompareAtPrice: config.pricing.showCompareAtPrice,\n showMostPopular: config.popularBadge.visible,\n popularBadgeText: config.popularBadge.text,\n popularBadgeBgColor: config.popularBadge.bgColor,\n popularBadgeTextColor: config.popularBadge.textColor,\n popularBadgeBorderWidth: config.popularBadge.borderWidth,\n popularBadgeBorderColor: config.popularBadge.borderColor,\n popularBadgeBorderRadius: config.popularBadge.borderRadius,\n showSearch: config.showSearch,\n pickerBgColor: config.pickerBgColor,\n pickerTextColor: config.pickerTextColor,\n pickerBorderWidth: config.pickerBorderWidth,\n pickerBorderColor: config.pickerBorderColor,\n pickerBorderRadius: config.pickerBorderRadius,\n pickerSearchBorderWidth: config.pickerSearchBorderWidth,\n pickerSearchBorderColor: config.pickerSearchBorderColor,\n pickerSearchBorderRadius: config.pickerSearchBorderRadius,\n pickerProductBorderWidth: config.pickerProductBorderWidth,\n pickerProductBorderColor: config.pickerProductBorderColor,\n pickerProductBorderRadius: config.pickerProductBorderRadius,\n pickerThumbnailRatio: config.pickerThumbnailRatio,\n pickerShowCountBubble: config.pickerShowCountBubble,\n pickerCountBubbleBgColor: config.pickerCountBubbleBgColor,\n pickerCountBubbleTextColor: config.pickerCountBubbleTextColor,\n pickerAddBgColor: config.pickerAddBgColor,\n pickerAddLabelColor: config.pickerAddLabelColor,\n pickerAddBorderWidth: config.pickerAddBorderWidth,\n pickerAddBorderColor: config.pickerAddBorderColor,\n pickerAddBorderRadius: config.pickerAddBorderRadius,\n pickerVariantBorderWidth: config.pickerVariantBorderWidth,\n pickerVariantBorderColor: config.pickerVariantBorderColor,\n pickerVariantBorderRadius: config.pickerVariantBorderRadius,\n pickerQtyStepperBorderWidth: config.pickerQtyStepperBorderWidth,\n pickerQtyStepperBorderColor: config.pickerQtyStepperBorderColor,\n pickerQtyStepperBorderRadius: config.pickerQtyStepperBorderRadius,\n tierBorderColor: config.tierBorderColor,\n tierBorderWidth: config.tierBorderWidth,\n tierBorderRadius: config.tierBorderRadius,\n tierSelectedBorderColor: config.tierSelectedBorderColor,\n tierSelectedBorderWidth: config.tierSelectedBorderWidth,\n defaultTier: config.defaultTier,\n ...(config.popularBadge.tierIndex !== undefined && {\n mostPopularTierIndex: config.popularBadge.tierIndex,\n }),\n };\n}\n\n/**\n * Map from `flattenWidgetConfig` keys to the actual CSS custom property\n * names consumed by the `bundle-*.css` stylesheets. The names are bespoke,\n * not mechanical, so we can't derive them with a naive camel-to-kebab\n * transform. Mirror of `CSS_VAR_MAP` in\n * `app/components/WidgetEditorPreview.tsx`.\n */\nexport const CSS_VAR_MAP: Record<string, string> = {\n primaryColor: \"--lb-primary-color\",\n backgroundColor: \"--lb-bg\",\n textColor: \"--lb-text\",\n imageBorderWidth: \"--lb-image-border-width\",\n imageBorderColor: \"--lb-image-border-color\",\n imageBorderRadius: \"--lb-image-border-radius\",\n variantBorderWidth: \"--lb-variant-border-width\",\n variantBorderColor: \"--lb-variant-border-color\",\n variantBorderRadius: \"--lb-variant-radius\",\n countBubbleBgColor: \"--lb-qty-badge-bg\",\n countBubbleTextColor: \"--lb-qty-badge-color\",\n borderColor: \"--lb-border\",\n borderWidth: \"--lb-border-width\",\n buttonTextColor: \"--lb-btn-text\",\n borderRadius: \"--lb-radius\",\n headerTextColor: \"--lb-header-text\",\n saveBadgeBgColor: \"--lb-save-badge-bg\",\n saveBadgeTextColor: \"--lb-save-badge-text\",\n saveBadgeBorderColor: \"--lb-save-badge-border-color\",\n saveBadgeBorderWidth: \"--lb-save-badge-border-width\",\n saveBadgeBorderRadius: \"--lb-save-badge-radius\",\n countdownBgColor: \"--lb-countdown-bg\",\n countdownTextColor: \"--lb-countdown-text\",\n ctaBorderWidth: \"--lb-cta-border-width\",\n ctaBorderColor: \"--lb-cta-border-color\",\n ctaBorderRadius: \"--lb-cta-radius\",\n savingsBarBgColor: \"--lb-savings-bar-bg\",\n savingsBarTextColor: \"--lb-savings-bar-text\",\n savingsBarBorderWidth: \"--lb-savings-bar-border-width\",\n savingsBarBorderColor: \"--lb-savings-bar-border-color\",\n savingsBarBorderRadius: \"--lb-savings-bar-radius\",\n tierBorderColor: \"--lb-tier-border-color\",\n tierBorderWidth: \"--lb-tier-border-width\",\n tierBorderRadius: \"--lb-tier-radius\",\n tierSelectedBorderColor: \"--lb-tier-selected-border-color\",\n tierSelectedBorderWidth: \"--lb-tier-selected-border-width\",\n popularBadgeBgColor: \"--lb-popular-badge-bg\",\n popularBadgeTextColor: \"--lb-popular-badge-text\",\n popularBadgeBorderWidth: \"--lb-popular-badge-border-width\",\n popularBadgeBorderColor: \"--lb-popular-badge-border-color\",\n popularBadgeBorderRadius: \"--lb-popular-badge-radius\",\n headerGradientStart: \"--lb-header-start\",\n headerGradientEnd: \"--lb-header-end\",\n pickerBgColor: \"--lb-picker-bg\",\n pickerTextColor: \"--lb-picker-text\",\n pickerBorderWidth: \"--lb-picker-border-width\",\n pickerBorderColor: \"--lb-picker-border-color\",\n pickerBorderRadius: \"--lb-picker-radius\",\n pickerSearchBorderWidth: \"--lb-picker-search-border-width\",\n pickerSearchBorderColor: \"--lb-picker-search-border-color\",\n pickerSearchBorderRadius: \"--lb-picker-search-radius\",\n pickerProductBorderWidth: \"--lb-picker-product-border-width\",\n pickerProductBorderColor: \"--lb-picker-product-border-color\",\n pickerProductBorderRadius: \"--lb-picker-product-radius\",\n pickerCountBubbleBgColor: \"--lb-picker-qty-badge-bg\",\n pickerCountBubbleTextColor: \"--lb-picker-qty-badge-color\",\n pickerAddBgColor: \"--lb-picker-add-bg\",\n pickerAddLabelColor: \"--lb-picker-add-label\",\n pickerAddBorderWidth: \"--lb-picker-add-border-width\",\n pickerAddBorderColor: \"--lb-picker-add-border-color\",\n pickerAddBorderRadius: \"--lb-picker-add-radius\",\n pickerVariantBorderWidth: \"--lb-picker-variant-border-width\",\n pickerVariantBorderColor: \"--lb-picker-variant-border-color\",\n pickerVariantBorderRadius: \"--lb-picker-variant-radius\",\n pickerQtyStepperBorderWidth: \"--lb-picker-qty-stepper-border-width\",\n pickerQtyStepperBorderColor: \"--lb-picker-qty-stepper-border-color\",\n pickerQtyStepperBorderRadius: \"--lb-picker-qty-stepper-radius\",\n};\n\n/** Flat keys whose numeric value should be serialized with a `px` unit. */\nexport const PX_KEYS: ReadonlySet<string> = new Set([\n \"borderRadius\",\n \"borderWidth\",\n \"saveBadgeBorderWidth\",\n \"saveBadgeBorderRadius\",\n \"tierBorderWidth\",\n \"tierBorderRadius\",\n \"tierSelectedBorderWidth\",\n \"savingsBarBorderWidth\",\n \"savingsBarBorderRadius\",\n \"ctaBorderWidth\",\n \"ctaBorderRadius\",\n \"popularBadgeBorderWidth\",\n \"popularBadgeBorderRadius\",\n \"imageBorderWidth\",\n \"imageBorderRadius\",\n \"variantBorderWidth\",\n \"variantBorderRadius\",\n \"pickerBorderWidth\",\n \"pickerBorderRadius\",\n \"pickerSearchBorderWidth\",\n \"pickerSearchBorderRadius\",\n \"pickerProductBorderWidth\",\n \"pickerProductBorderRadius\",\n \"pickerAddBorderWidth\",\n \"pickerAddBorderRadius\",\n \"pickerVariantBorderWidth\",\n \"pickerVariantBorderRadius\",\n \"pickerQtyStepperBorderWidth\",\n \"pickerQtyStepperBorderRadius\",\n]);\n\n/**\n * Apply a WidgetConfig to a DOM element as CSS custom properties.\n *\n * Sets one `--lb-*` property per entry in CSS_VAR_MAP, px-suffixing numeric\n * dimensions, plus the four boolean toggles that the Liquid template emits\n * as display values (`block`, `inline`, `flex`, or `none`):\n *\n * - `--lb-product-price-display` from productList.showPrice\n * - `--lb-product-compare-display` from productList.showCompareAtPrice\n * - `--lb-qty-badge-display` from productList.showCountBubble\n * - `--lb-picker-qty-badge-display` from pickerShowCountBubble\n *\n * Anything the merchant didn't explicitly configure falls through to the\n * CSS file's own fallback value, so this call is safe on any element.\n */\nexport function applyWidgetConfigVars(\n el: { style: CSSStyleDeclaration },\n config: WidgetConfig,\n): void {\n const flat = flattenWidgetConfig(config);\n\n for (const [flatKey, cssVar] of Object.entries(CSS_VAR_MAP)) {\n const value = flat[flatKey];\n if (value === undefined || value === null) continue;\n const serialized = PX_KEYS.has(flatKey) ? `${value}px` : String(value);\n el.style.setProperty(cssVar, serialized);\n }\n\n // Conditional display toggles — merchant-configurable booleans that the\n // Liquid template emits as explicit display-mode CSS vars. We mirror that\n // exactly so the shipped bundle-*.css rules (which read these vars) hide\n // or show the right elements.\n el.style.setProperty(\n \"--lb-product-price-display\",\n config.productList.showPrice ? \"block\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-compare-display\",\n config.productList.showCompareAtPrice ? \"inline\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-qty-badge-display\",\n config.productList.showCountBubble ? \"flex\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-picker-qty-badge-display\",\n config.pickerShowCountBubble ? \"flex\" : \"none\",\n );\n\n // Variant dropdown chevrons — embed the border color into the SVG stroke\n // so the arrow auto-matches. Mirrors the Liquid template in\n // `extensions/bundle-theme/blocks/bundle-widget.liquid` and the admin\n // preview helper in `app/components/WidgetEditorPreview.tsx`.\n el.style.setProperty(\n \"--lb-variant-chevron\",\n variantChevronUrl(config.productList.variantBorderColor),\n );\n el.style.setProperty(\n \"--lb-picker-variant-chevron\",\n variantChevronUrl(config.pickerVariantBorderColor),\n );\n\n // Header background — derived from headerStyle choice.\n if (config.header.headerStyle === \"solid\") {\n el.style.setProperty(\"--lb-header-bg\", config.header.gradientStart);\n } else {\n el.style.setProperty(\n \"--lb-header-bg\",\n `linear-gradient(135deg, ${config.header.gradientStart}, ${config.header.gradientEnd})`,\n );\n }\n\n // Thumbnail ratio — one merchant enum drives three CSS vars. \"original\"\n // disables aspect-ratio cropping (img renders at intrinsic ratio); the\n // other three values force a fixed aspect with object-fit: cover.\n const ratioVars = thumbnailRatioVars(config.productList.thumbnailRatio);\n el.style.setProperty(\"--lb-thumbnail-aspect-ratio\", ratioVars.aspectRatio);\n el.style.setProperty(\"--lb-thumbnail-img-fit\", ratioVars.imgFit);\n el.style.setProperty(\"--lb-thumbnail-img-height\", ratioVars.imgHeight);\n\n // Picker-modal thumbnail ratio — independent from the main widget so\n // merchants can e.g. show tall picker thumbs while keeping square main\n // thumbs. Reuses `thumbnailRatioVars` with a different CSS-var prefix.\n const pickerRatioVars = thumbnailRatioVars(config.pickerThumbnailRatio);\n el.style.setProperty(\n \"--lb-picker-thumbnail-aspect-ratio\",\n pickerRatioVars.aspectRatio,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-fit\",\n pickerRatioVars.imgFit,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-height\",\n pickerRatioVars.imgHeight,\n );\n}\n\n/**\n * Build an inline-SVG `url(...)` value for the variant dropdown chevron,\n * with the given hex color embedded as the stroke. `#` must be URL-encoded\n * inside a data URI. Kept in lockstep with the Liquid template and admin\n * preview helper — any change to the path/size/stroke-width must update\n * all three.\n */\nfunction variantChevronUrl(strokeHex: string): string {\n const encoded = strokeHex.replace(/#/g, \"%23\");\n return `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' fill='none' stroke='${encoded}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")`;\n}\n\n/**\n * Map the merchant-facing thumbnail-ratio choice to the three CSS vars\n * the bundle stylesheets read. Exported so the Liquid template's mirror\n * of this logic stays in lockstep — any change to the value strings here\n * must be reflected in `bundle-widget.liquid` and the admin preview.\n */\nexport function thumbnailRatioVars(ratio: WidgetConfig[\"productList\"][\"thumbnailRatio\"]): {\n aspectRatio: string;\n imgFit: string;\n imgHeight: string;\n} {\n switch (ratio) {\n case \"tall\":\n return { aspectRatio: \"3 / 4\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"wide\":\n return { aspectRatio: \"4 / 3\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"original\":\n return { aspectRatio: \"auto\", imgFit: \"contain\", imgHeight: \"auto\" };\n case \"square\":\n default:\n return { aspectRatio: \"1 / 1\", imgFit: \"cover\", imgHeight: \"100%\" };\n }\n}\n","/**\n * Parse metaobject fields into a typed ParsedBundle (discriminated union).\n * Returns null if the bundle is not renderable (invalid type, inactive,\n * outside schedule) — callers who want structured reasons for \"not\n * renderable\" should use parseMetaobjectBundleStrict, which throws a\n * BundleParseError with a reason code.\n */\nimport type { MetaobjectField, Product } from \"../storefront-api/types\";\nimport {\n BundleParseError,\n type ABVariantOverrides,\n type ParsedBundle,\n type BundleType,\n type BundleStatus,\n type DiscountConfig,\n type WidgetConfig,\n type VolumeTier,\n type FixedBundleData,\n type VolumeBundleData,\n type MixMatchBundleData,\n} from \"./types\";\nimport { mergeWidgetConfig } from \"./widget-config\";\n\nconst VALID_BUNDLE_TYPES = new Set<BundleType>([\"fixed\", \"mix_match\", \"volume\"]);\nconst ACTIVE_STATUSES = new Set<BundleStatus>([\"active\"]);\n\n/**\n * Parse a metaobject's fields array into a structured ParsedBundle.\n * Returns null if the bundle is not renderable.\n */\nexport function parseMetaobjectBundle(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle | null {\n try {\n return parseMetaobjectBundleStrict(metaobjectId, fields);\n } catch (err) {\n if (err instanceof BundleParseError) return null;\n throw err;\n }\n}\n\n/**\n * Same as parseMetaobjectBundle but throws BundleParseError with a\n * structured reason instead of returning null.\n */\nexport function parseMetaobjectBundleStrict(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle {\n const fieldMap = new Map(fields.map((f) => [f.key, f]));\n\n const title = fieldMap.get(\"title\")?.value ?? \"Bundle\";\n const rawBundleType = fieldMap.get(\"bundle_type\")?.value;\n const rawStatus = fieldMap.get(\"status\")?.value;\n\n if (!rawBundleType || !VALID_BUNDLE_TYPES.has(rawBundleType as BundleType)) {\n throw new BundleParseError(\n `Invalid or missing bundle_type: ${rawBundleType ?? \"null\"}`,\n \"invalid_type\",\n );\n }\n const bundleType = rawBundleType as BundleType;\n\n if (!rawStatus || !ACTIVE_STATUSES.has(rawStatus as BundleStatus)) {\n throw new BundleParseError(\n `Bundle is not active: status=${rawStatus ?? \"null\"}`,\n \"inactive\",\n );\n }\n const status = rawStatus as BundleStatus;\n\n const startsAt = fieldMap.get(\"starts_at\")?.value ?? null;\n const endsAt = fieldMap.get(\"ends_at\")?.value ?? null;\n const now = new Date();\n\n if (startsAt) {\n const start = new Date(startsAt);\n if (Number.isNaN(start.getTime())) {\n throw new BundleParseError(\n `Invalid starts_at: ${startsAt}`,\n \"invalid_type\",\n );\n }\n if (start > now) {\n throw new BundleParseError(\n `Bundle not yet started: starts_at=${startsAt}`,\n \"not_started\",\n );\n }\n }\n if (endsAt) {\n const end = new Date(endsAt);\n if (Number.isNaN(end.getTime())) {\n throw new BundleParseError(\n `Invalid ends_at: ${endsAt}`,\n \"invalid_type\",\n );\n }\n if (end < now) {\n throw new BundleParseError(\n `Bundle has expired: ends_at=${endsAt}`,\n \"expired\",\n );\n }\n }\n\n const products = resolveProducts(fieldMap);\n const discountConfig = parseDiscountConfig(fieldMap);\n const widgetConfig = parseWidgetConfig(fieldMap);\n\n const base = {\n id: metaobjectId,\n title,\n description: fieldMap.get(\"description\")?.value || null,\n status,\n products,\n discountConfig,\n widgetConfig,\n startsAt,\n endsAt,\n discountLabel: fieldMap.get(\"discount_label\")?.value ?? null,\n selectedVariantIds: parseSelectedVariantIds(fieldMap),\n productQuantities: parseNumericRecord(fieldMap, \"product_quantities\"),\n variantQuantities: parseNumericRecord(fieldMap, \"variant_quantities\"),\n abTestId: fieldMap.get(\"ab_test_id\")?.value ?? null,\n abVariantB: parseABVariantB(fieldMap),\n marketVisibility: parseMarketVisibility(fieldMap),\n marketIds: parseMarketIds(fieldMap),\n };\n\n switch (bundleType) {\n case \"fixed\": {\n const result: FixedBundleData = { ...base, bundleType: \"fixed\" };\n return result;\n }\n case \"volume\": {\n const result: VolumeBundleData = {\n ...base,\n bundleType: \"volume\",\n volumeTiers: parseVolumeTiers(fieldMap),\n };\n return result;\n }\n case \"mix_match\": {\n const result: MixMatchBundleData = {\n ...base,\n bundleType: \"mix_match\",\n minQuantity: parseIntField(fieldMap, \"min_quantity\", { min: 1 }),\n maxQuantity: null,\n productRules: parseProductRules(fieldMap),\n };\n return result;\n }\n }\n}\n\nfunction resolveProducts(fieldMap: Map<string, MetaobjectField>): Product[] {\n const products: Product[] = [];\n\n const productsField = fieldMap.get(\"products\");\n if (productsField?.reference && \"variants\" in productsField.reference) {\n products.push(productsField.reference as Product);\n }\n\n if (productsField?.references?.nodes) {\n for (const node of productsField.references.nodes) {\n if (\"variants\" in node) {\n products.push(node as Product);\n } else if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n const collectionField = fieldMap.get(\"collection\");\n if (collectionField?.references?.nodes) {\n for (const node of collectionField.references.nodes) {\n if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n return products;\n}\n\nfunction parseDiscountConfig(\n fieldMap: Map<string, MetaobjectField>,\n): DiscountConfig {\n return {\n discountType:\n (fieldMap.get(\"discount_type\")?.value as \"percentage\" | \"fixed_amount\") ??\n \"percentage\",\n discountValue: parseFloat(fieldMap.get(\"discount_value\")?.value ?? \"0\"),\n allowStacking: fieldMap.get(\"allow_stacking\")?.value === \"true\",\n };\n}\n\n/**\n * Parse the merchant's widget_config metaobject field. The JSON is authored\n * by the Zod schema in app/lib/bundle-types.ts, so consumers can trust the\n * shape — but old bundles may be missing fields added after they were\n * created. mergeWidgetConfig fills those gaps from WIDGET_CONFIG_DEFAULTS,\n * so every renderer downstream gets a fully populated config.\n */\nfunction parseWidgetConfig(\n fieldMap: Map<string, MetaobjectField>,\n): WidgetConfig {\n return mergeWidgetConfig(parseJsonField(fieldMap, \"widget_config\"));\n}\n\n/**\n * Parse `selected_variant_ids`: a list of per-product variant-GID arrays,\n * positionally aligned with `products`. Returns null when the merchant\n * didn't restrict variants (any entry means full list allowed).\n */\nfunction parseSelectedVariantIds(\n fieldMap: Map<string, MetaobjectField>,\n): string[][] | null {\n const raw = parseJsonField(fieldMap, \"selected_variant_ids\");\n if (!Array.isArray(raw)) return null;\n const result: string[][] = [];\n for (const entry of raw) {\n if (Array.isArray(entry)) {\n result.push(\n entry.filter((x): x is string => typeof x === \"string\"),\n );\n } else {\n result.push([]);\n }\n }\n return result;\n}\n\n/**\n * Parse a JSON record of `{ [GID]: number }`. Used for\n * `product_quantities` and `variant_quantities`. Non-numeric values are\n * dropped; returns an empty object when the field is absent or malformed.\n */\nfunction parseNumericRecord(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): Record<string, number> {\n const raw = parseJsonField(fieldMap, key);\n // Arrays pass `typeof === \"object\"`. Reject them explicitly so a\n // malformed metaobject field (e.g. `[1, 2, 3]` where an object was\n // expected) doesn't produce a `{ \"0\": 1, \"1\": 2, ... }` record.\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const record: Record<string, number> = {};\n for (const [k, v] of Object.entries(raw)) {\n const num = typeof v === \"number\" ? v : Number(v);\n // Defense-in-depth: Zod enforces 0/1–99 at the write path. The parser\n // also clamps at read time so a metafield that bypasses Zod (seed script,\n // direct Admin API edit, manual Shopify CLI) can't feed `Number.MAX_SAFE_INTEGER`\n // downstream into `CartLineInput.quantity` — which would crash Hydrogen\n // or Storefront Cart API with an opaque error. Zero is preserved\n // (merchant-set opt-out); renderers filter zero-qty rows. Negative and\n // out-of-range values are dropped as malformed.\n if (Number.isFinite(num) && num >= 0 && num <= 99) {\n record[k] = Math.floor(num);\n }\n }\n return record;\n}\n\n/**\n * Parse the `product_rules` JSON metaobject field into a record of\n * `{ [productGid]: { min, max } }`. Drops malformed entries. Clamps min/max\n * to 1..99 and ensures `min <= max`. Returns an empty object when the field\n * is absent or unparseable.\n */\nfunction parseProductRules(\n fieldMap: Map<string, MetaobjectField>,\n): Record<string, { min: number; max: number }> {\n const raw = parseJsonField(fieldMap, \"product_rules\");\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const out: Record<string, { min: number; max: number }> = {};\n for (const [pid, rule] of Object.entries(raw)) {\n if (!rule || typeof rule !== \"object\" || Array.isArray(rule)) continue;\n const { min, max } = rule as { min?: unknown; max?: unknown };\n const minN = typeof min === \"number\" ? min : Number(min);\n const maxN = typeof max === \"number\" ? max : Number(max);\n if (!Number.isFinite(minN) || !Number.isFinite(maxN)) continue;\n const minClamped = Math.max(1, Math.min(99, Math.floor(minN)));\n const maxClamped = Math.max(minClamped, Math.min(99, Math.floor(maxN)));\n out[pid] = { min: minClamped, max: maxClamped };\n }\n return out;\n}\n\n/**\n * Parse the merchant's A/B variant B overrides from the `ab_*` metaobject\n * fields. Returns null when no A/B test is active (ab_test_id absent) or\n * when every override is blank.\n */\nfunction parseMarketVisibility(\n fieldMap: Map<string, MetaobjectField>,\n): \"all\" | \"specific\" {\n const raw = fieldMap.get(\"market_visibility\")?.value;\n return raw === \"specific\" ? \"specific\" : \"all\";\n}\n\nfunction parseMarketIds(fieldMap: Map<string, MetaobjectField>): string[] {\n const raw = fieldMap.get(\"markets\")?.value;\n if (!raw) return [];\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter((v): v is string => typeof v === \"string\");\n } catch {\n return [];\n }\n}\n\nfunction parseABVariantB(\n fieldMap: Map<string, MetaobjectField>,\n): ABVariantOverrides | null {\n const abTestId = fieldMap.get(\"ab_test_id\")?.value;\n if (!abTestId) return null;\n\n const overrides: ABVariantOverrides = {};\n const title = fieldMap.get(\"ab_title\")?.value;\n if (title) overrides.title = title;\n const description = fieldMap.get(\"ab_description\")?.value;\n if (description) overrides.description = description;\n\n const discountType = fieldMap.get(\"ab_discount_type\")?.value;\n const discountValueRaw = fieldMap.get(\"ab_discount_value\")?.value;\n if (\n discountType &&\n (discountType === \"percentage\" || discountType === \"fixed_amount\") &&\n discountValueRaw\n ) {\n const value = parseFloat(discountValueRaw);\n if (Number.isFinite(value)) {\n overrides.discountConfig = {\n discountType,\n discountValue: value,\n allowStacking: fieldMap.get(\"allow_stacking\")?.value === \"true\",\n };\n }\n }\n\n const abTiers = parseJsonField(fieldMap, \"ab_volume_tiers\");\n if (Array.isArray(abTiers)) {\n overrides.volumeTiers = abTiers\n .filter(\n (t): t is Record<string, unknown> =>\n typeof t === \"object\" && t !== null,\n )\n .map(parseOneVolumeTier);\n }\n\n return Object.keys(overrides).length === 0 ? null : overrides;\n}\n\nfunction parseVolumeTiers(\n fieldMap: Map<string, MetaobjectField>,\n): VolumeTier[] {\n const raw = parseJsonField(fieldMap, \"volume_tiers\");\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (t): t is Record<string, unknown> => typeof t === \"object\" && t !== null,\n )\n .map(parseOneVolumeTier);\n}\n\n// Volume tiers are stored as `{ minQuantity, percentage?, amount? }` on the\n// metaobject (see admin VolumeTierSchema in app/lib/bundle-types.ts). The\n// parent bundle's `discountConfig.discountType` tells renderers which of the\n// two magnitude fields to use for each tier.\nfunction parseOneVolumeTier(t: Record<string, unknown>): VolumeTier {\n const minQty = Number(t.minQuantity ?? 0);\n const tier: VolumeTier = {\n // Reject NaN / Infinity / negatives — same discipline as\n // percentage/amount below. Malformed tiers collapse to 0 so the\n // renderer's bounds checks can drop them deterministically.\n minQuantity: Number.isFinite(minQty) && minQty >= 0 ? minQty : 0,\n };\n if (typeof t.percentage === \"number\" && Number.isFinite(t.percentage)) {\n tier.percentage = t.percentage;\n }\n if (typeof t.amount === \"number\" && Number.isFinite(t.amount)) {\n tier.amount = t.amount;\n }\n return tier;\n}\n\nfunction parseJsonField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): unknown {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\nfunction parseIntField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n options: { min?: number } = {},\n): number | null {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n const num = parseInt(value, 10);\n if (isNaN(num)) return null;\n if (options.min !== undefined && num < options.min) return null;\n return num;\n}\n","/**\n * Product-aware bundle fetch — the headless analogue of the classic Liquid\n * theme block.\n *\n * Classic-theme widget reads `product.metafields[\"$app\"].active_bundle_ids`\n * inside Liquid and renders every active bundle that applies. Headless\n * storefronts get the same behaviour via this helper: pass a product\n * handle, receive `ParsedBundle[]` for every active bundle configured\n * against that product.\n *\n * The caller is responsible for knowing the handle — either because their\n * storefront template already knows it (Hydrogen loader, Next.js param),\n * or because the widget auto-detects from `/products/<handle>` URLs.\n *\n * Non-active bundles (draft / inactive / scheduled / expired) are silently\n * skipped rather than throwing — the calling surface is a list, not a\n * single lookup. Surface errors only for genuine failures (network /\n * GraphQL / invalid schema).\n */\nimport { createStorefrontClient } from \"../storefront-api/client\";\nimport {\n BUNDLES_FOR_PRODUCT_QUERY,\n type BundlesForProductResponse,\n} from \"../storefront-api/queries\";\nimport { parseMetaobjectBundleStrict } from \"./parser\";\nimport { BundleParseError, type ParsedBundle } from \"./types\";\nimport type { MetaobjectField } from \"../storefront-api/types\";\n\nexport interface FetchBundlesForProductOptions {\n shopDomain: string;\n storefrontAccessToken: string;\n productHandle: string;\n /** Forwarded as `Shopify-Storefront-Buyer-IP` — required for SSR. */\n buyerIp?: string;\n signal?: AbortSignal;\n apiVersion?: string;\n}\n\n/**\n * Reasons a bundle is expected-not-renderable (skipped silently, not\n * surfaced as an error). Matches `BundleParseFailReason` minus\n * `not_found` / `invalid_type`, which indicate genuine data problems\n * worth propagating.\n */\nconst SKIPPABLE_REASONS = new Set([\"inactive\", \"not_started\", \"expired\"]);\n\nexport async function fetchBundlesForProduct(\n options: FetchBundlesForProductOptions,\n): Promise<ParsedBundle[]> {\n const client = createStorefrontClient({\n shopDomain: options.shopDomain,\n accessToken: options.storefrontAccessToken,\n buyerIp: options.buyerIp,\n apiVersion: options.apiVersion,\n });\n\n const data = await client.query<BundlesForProductResponse>(\n BUNDLES_FOR_PRODUCT_QUERY,\n { handle: options.productHandle },\n { signal: options.signal },\n );\n\n if (!data.product) {\n // Product not found for this handle. The widget's caller (merchant's\n // storefront) is presumably on a valid product page, so this likely\n // means the handle is wrong or the product is unpublished. Return\n // empty rather than throwing — the widget renders nothing in that\n // case, which is the right UX (bundle section invisible).\n return [];\n }\n\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n\n for (const ref of refs) {\n try {\n // The nested references query resolves products + collections to the\n // same shape BUNDLE_METAOBJECT_QUERY returns, so parser.ts doesn't\n // need a new code path.\n const bundle = parseMetaobjectBundleStrict(\n ref.id,\n ref.fields as MetaobjectField[],\n );\n bundles.push(bundle);\n } catch (err) {\n if (err instanceof BundleParseError && SKIPPABLE_REASONS.has(err.reason)) {\n continue;\n }\n throw err;\n }\n }\n\n return bundles;\n}\n","// Volume tier savings calculation. `discountType` lives on the parent\n// bundle (bundle.discountConfig.discountType); each tier carries the\n// magnitude in either `percentage` (whole-number, e.g. 10 = 10%) or\n// `amount` (currency units, e.g. 5.00 = $5 off per unit).\nimport type { VolumeTier } from \"./types\";\nimport type { DiscountConfig } from \"./types\";\n\nexport interface TierSavings {\n tier: VolumeTier;\n unitPrice: number;\n savings: number;\n savingsPercent: number;\n isActive: boolean;\n}\n\n/**\n * Calculate savings for each tier at a given base price and quantity.\n */\nexport function calculateTierSavings(\n tiers: VolumeTier[],\n basePrice: number,\n currentQuantity: number,\n discountType: DiscountConfig[\"discountType\"],\n): TierSavings[] {\n // Sort tiers by minQuantity ascending\n const sorted = [...tiers].sort((a, b) => a.minQuantity - b.minQuantity);\n\n return sorted.map((tier) => {\n const discount =\n discountType === \"percentage\"\n ? basePrice * ((tier.percentage ?? 0) / 100)\n : (tier.amount ?? 0);\n\n const unitPrice = Math.max(0, basePrice - discount);\n const savings = basePrice - unitPrice;\n const savingsPercent = basePrice > 0 ? (savings / basePrice) * 100 : 0;\n const isActive = currentQuantity >= tier.minQuantity;\n\n return { tier, unitPrice, savings, savingsPercent, isActive };\n });\n}\n\n/**\n * Get the active tier for a given quantity.\n * Returns the highest tier where quantity >= minQuantity.\n */\nexport function getActiveTier(\n tiers: VolumeTier[],\n quantity: number,\n): VolumeTier | null {\n const sorted = [...tiers].sort((a, b) => b.minQuantity - a.minQuantity);\n return sorted.find((t) => quantity >= t.minQuantity) ?? null;\n}\n","/**\n * Quantity constraint validation for mix-and-match bundles.\n */\n\nexport interface QuantityValidation {\n valid: boolean;\n totalQuantity: number;\n message: string | null;\n}\n\nexport function validateQuantity(\n totalQuantity: number,\n minQuantity: number | null,\n maxQuantity: number | null,\n): QuantityValidation {\n if (minQuantity !== null && totalQuantity < minQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at least ${minQuantity} item${minQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n if (maxQuantity !== null && totalQuantity > maxQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at most ${maxQuantity} item${maxQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n return { valid: true, totalQuantity, message: null };\n}\n","/**\n * Canonical resolver for a bundle's per-line quantity.\n *\n * Bundle configs carry two qty fields: `productQuantities` (keyed by product\n * GID) and `variantQuantities` (keyed by variant GID, overrides product-level\n * when the customer picks that variant). The admin form strips product-level\n * entries that equal the default 1, so downstream consumers must consult\n * variantQuantities first and fall back to productQuantities then to 1.\n *\n * Before this helper existed, the fallback chain was reimplemented in five\n * places (React FixedBundle, React MixMatchBundle, headless widget fixed\n * renderer, Liquid mix-match IIFE, and the Rust discount function). They\n * drifted — PR #147 fixed the resulting cart-line split. Use this helper\n * everywhere the config is available to keep the TS/React/widget runtimes\n * in lockstep. The Liquid IIFE and Rust function have their own\n * implementations pinned by parity tests.\n */\n\nexport interface BundleQtySource {\n variantQuantities: Record<string, number>;\n productQuantities: Record<string, number>;\n}\n\n/**\n * Returns the configured qty for a cart line: `variantQuantities[variantId]`\n * if present, else `productQuantities[productId]`, else 1.\n *\n * Using `!== undefined` (not `??`) for the variant check so that a merchant\n * explicitly setting `0` is honoured — caller is expected to filter zero-qty\n * lines out, matching the headless widget's opt-out semantics.\n */\nexport function resolveBundleQty(\n bundle: BundleQtySource,\n productId: string,\n variantId: string,\n): number {\n const vq = bundle.variantQuantities[variantId];\n if (vq !== undefined) return vq;\n return bundle.productQuantities[productId] ?? 1;\n}\n","// Pure merge — no cookies, no fetches. Call after getABTestAssignment()\n// has resolved which variant to render; fields absent from abVariantB\n// fall back to the base bundle.\nimport type { ParsedBundle } from \"./types\";\n\nexport function applyABVariantB(bundle: ParsedBundle): ParsedBundle {\n const overrides = bundle.abVariantB;\n if (!overrides) return bundle;\n\n // Shared field overrides applied to every variant.\n const title = overrides.title ?? bundle.title;\n const description =\n overrides.description !== undefined\n ? overrides.description\n : bundle.description;\n const discountConfig = overrides.discountConfig ?? bundle.discountConfig;\n\n // Narrow by bundleType so the spread keeps the discriminant. A cast here\n // would silence the error if ABVariantOverrides ever grows a mix-match\n // or fixed-specific field; spreading the already-narrowed `bundle`\n // forces a compile error in that case.\n switch (bundle.bundleType) {\n case \"volume\":\n return {\n ...bundle,\n title,\n description,\n discountConfig,\n volumeTiers: overrides.volumeTiers ?? bundle.volumeTiers,\n };\n case \"mix_match\":\n return { ...bundle, title, description, discountConfig };\n case \"fixed\":\n return { ...bundle, title, description, discountConfig };\n }\n}\n","/**\n * Analytics event reporter for headless widgets.\n * Fires events to /api/analytics with single retry on failure.\n */\n\nexport interface AnalyticsConfig {\n shopDomain: string;\n appUrl: string; // Base URL of the Lime Bundles app\n}\n\nexport async function reportImpression(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n productId?: string;\n abTestId?: string;\n abVariant?: string;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_impression\",\n bundleGid: event.bundleGid,\n bundleType: event.bundleType,\n productId: event.productId,\n abTestId: event.abTestId,\n abVariant: event.abVariant,\n occurredAt: new Date().toISOString(),\n });\n}\n\nexport async function reportAddToCart(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n /**\n * Product GID the widget is rendered on. Optional because multi-product\n * bundles (fixed, mix-match) have no single \"page product\" — the event\n * is already attributed to the bundle via `bundleGid`. Volume bundles\n * typically pass the anchor product's GID. Previously this was required\n * and consumers defaulted to `products[0]?.id ?? \"\"` for multi-product\n * bundles, which was misleading.\n */\n productId?: string;\n quantity: number;\n totalPrice: number;\n abTestId?: string;\n abVariant?: string;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_add_to_cart\",\n ...event,\n occurredAt: new Date().toISOString(),\n });\n}\n\n/**\n * IntersectionObserver-based impression tracking.\n * Fires once when element is 50% visible, then unobserves.\n */\nexport function observeImpression(\n element: Element,\n callback: () => void,\n): () => void {\n if (typeof IntersectionObserver === \"undefined\") {\n // Fallback: fire immediately if IntersectionObserver not available\n callback();\n return () => {};\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n callback();\n observer.unobserve(entry.target);\n }\n }\n },\n { threshold: 0.5 },\n );\n\n observer.observe(element);\n return () => observer.disconnect();\n}\n\n// --- Internal ---\n\nasync function sendEvent(\n config: AnalyticsConfig,\n payload: Record<string, unknown>,\n): Promise<void> {\n const url = `${config.appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n\n if (!response.ok) {\n // Single retry after 2 seconds\n await new Promise((r) => setTimeout(r, 2000));\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n }\n } catch {\n // Fire-and-forget — never block UX\n try {\n // Use sendBeacon as last resort\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n } catch {\n // Silently drop\n }\n }\n}\n","/**\n * Merchant-facing consent gate for A/B bucket persistence.\n *\n * Why this exists:\n *\n * On classic-theme Shopify storefronts, `window.Shopify.customerPrivacy`\n * exposes GDPR consent state. We can gate A/B cookie writes on that.\n *\n * On headless storefronts (Hydrogen, custom Next.js, etc.) there is no\n * universal consent primitive — the merchant is the privacy controller,\n * not Shopify. GDPR Art. 6(1)(a) requires opt-in consent for analytics-\n * related cookies, so the SDK must default to deny and accept explicit\n * opt-in from the merchant.\n *\n * Usage:\n *\n * import { setConsent } from \"@lime-bundles/core\";\n *\n * // In the merchant's consent-banner accept handler:\n * setConsent(true);\n *\n * // To revoke (e.g. on consent-banner decline):\n * setConsent(false);\n *\n * The A/B assigner calls `hasConsent()` before writing the bucket cookie.\n * Without consent, the assigner returns a stable \"control\" bucket and\n * writes no cookie — no tracking.\n *\n * SSR / edge runtimes:\n *\n * Both functions are no-ops on the server. The module-scoped flag would\n * otherwise leak across requests in SSR (Hydrogen, Next.js Server\n * Components, edge workers), so `setConsent` won't mutate state when\n * `document` is unavailable, and `hasConsent` returns false. Server\n * renders default to the deny path (Variant A, no cookies); the real\n * decision happens on the client when the consent banner accepts.\n */\n\nlet consentGranted = false;\n\n/** Browser-only — module-scope state means consent can't be safely\n * read or written on the server. */\nfunction isBrowser(): boolean {\n return typeof document !== \"undefined\";\n}\n\n/**\n * Merchant signals explicit opt-in (true) or revocation (false) for\n * A/B-related cookie and analytics writes. No-op on the server.\n */\nexport function setConsent(allowed: boolean): void {\n if (!isBrowser()) return;\n consentGranted = allowed;\n}\n\n/**\n * Check if writes are permitted. Returns true when:\n * (a) the merchant has called `setConsent(true)`, OR\n * (b) Shopify's `customerPrivacy.analyticsProcessingAllowed()` returns true\n * (classic-theme / hosted checkout context).\n *\n * Returns false by default (GDPR-safe), and always returns false on\n * the server.\n */\nexport function hasConsent(): boolean {\n if (!isBrowser()) return false;\n if (consentGranted) return true;\n if (typeof window === \"undefined\") return false;\n\n // Type cast — Shopify global isn't typed in headless TS environments.\n const shopifyGlobal = (\n window as unknown as {\n Shopify?: {\n customerPrivacy?: {\n analyticsProcessingAllowed?: () => boolean;\n };\n };\n }\n ).Shopify;\n\n try {\n return shopifyGlobal?.customerPrivacy?.analyticsProcessingAllowed?.() === true;\n } catch {\n return false;\n }\n}\n\n/** @internal Test-only: reset the consent flag between tests. */\nexport function __resetConsent(): void {\n consentGranted = false;\n}\n","/**\n * A/B test variant assignment for headless widgets.\n *\n * Writes a first-party `__Host-_lb_ab_${bundleId}` cookie carrying the\n * deterministic bucket. The `__Host-` prefix is a browser-enforced\n * hardening: requires `Secure`, `Path=/`, no `Domain=` — the cookie is\n * locked to the exact origin that set it.\n *\n * GDPR default: DENY. The assigner returns a stable \"control\" bucket and\n * writes no cookie when consent is absent. Merchants on classic-theme\n * Shopify get consent automatically via `window.Shopify.customerPrivacy`;\n * headless merchants must call `setConsent(true)` from\n * `@lime-bundles/core` in their consent-banner accept handler.\n *\n * Bucket assignment is deterministic (fnv1a hash) so repeated calls with\n * the same `sessionId + testId` always produce the same bucket — a client\n * that revokes consent and re-grants it later still lands on the same\n * variant. The same hash powers the analytics path: impression and\n * add-to-cart events emitted by the widget include the resolved variant\n * in their payload, which `getABTestResults` aggregates server-side.\n */\nimport { hasConsent } from \"./consent\";\n\nconst SESSION_COOKIE_NAME = \"lb_session\";\nconst SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\nconst AB_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\n\nexport interface ABTestAssignment {\n variant: \"A\" | \"B\";\n testId: string;\n /**\n * Whether this assignment was persisted (cookie + server write) or\n * returned in-memory as a consent-denied control bucket.\n */\n persisted: boolean;\n}\n\nfunction abCookieName(bundleId: string): string {\n // __Host- prefix requires Secure + Path=/ + no Domain attribute.\n // Browser rejects the cookie on write if any of those aren't set, so it\n // doubles as a defense-in-depth check against misconfigured deployments.\n return `__Host-_lb_ab_${bundleId}`;\n}\n\n/**\n * Get or assign an A/B test variant.\n *\n * Reads/creates the `lb_session` cookie, computes the deterministic\n * bucket, and (if consent is granted) writes the\n * `__Host-_lb_ab_${bundleId}` cookie. Variant attribution is recorded\n * server-side from the analytics events the widget emits, so this\n * function performs no network calls.\n */\nexport async function getABTestAssignment(\n testId: string,\n bundleId: string,\n): Promise<ABTestAssignment | null> {\n if (typeof document === \"undefined\") return null; // SSR guard\n\n // If we already persisted a bucket for this bundle, trust the cookie —\n // switching buckets mid-session skews test results. (Reading the cookie\n // doesn't require consent; a cookie's existence implies prior consent.)\n const existing = readABCookie(bundleId);\n if (existing && existing.testId === testId) {\n return { variant: existing.variant, testId, persisted: true };\n }\n\n // Consent gate FIRST — must not write lb_session or any tracking cookie\n // before consent is granted. All non-consenting users see control\n // (\"A\") without any cookie or network write.\n if (!hasConsent()) {\n return { variant: \"A\", testId, persisted: false };\n }\n\n // Consent granted — safe to create/read the session cookie and compute\n // the deterministic bucket.\n const sessionId = getOrCreateSessionId();\n const variant = fnv1aVariant(sessionId, testId);\n\n writeABCookie(bundleId, testId, variant);\n\n return { variant, testId, persisted: true };\n}\n\nfunction readABCookie(\n bundleId: string,\n): { testId: string; variant: \"A\" | \"B\" } | null {\n if (typeof document === \"undefined\") return null;\n const name = abCookieName(bundleId);\n const prefix = `${name}=`;\n const cookies = document.cookie.split(\";\").map((c) => c.trim());\n // Use substring(prefix.length) rather than split(\"=\")[1] — robust against\n // values that contain \"=\" (e.g. base64 padding if the cookie shape ever\n // changes). CUIDs don't contain \"=\" today but this is defensive.\n const raw = cookies.find((c) => c.startsWith(prefix));\n if (!raw) return null;\n const value = raw.substring(prefix.length);\n\n // Format: `${bucket}|${testId}|${ts}` — see writeABCookie.\n const [variant, testId] = value.split(\"|\");\n if (variant !== \"A\" && variant !== \"B\") return null;\n if (!testId) return null;\n return { testId, variant };\n}\n\nfunction writeABCookie(\n bundleId: string,\n testId: string,\n variant: \"A\" | \"B\",\n): void {\n if (typeof document === \"undefined\") return;\n const name = abCookieName(bundleId);\n const value = `${variant}|${testId}|${Date.now()}`;\n // __Host- prefix requires Secure + Path=/ + no Domain.\n document.cookie =\n `${name}=${value}; Path=/; Max-Age=${AB_COOKIE_MAX_AGE}; ` +\n `SameSite=Lax; Secure`;\n}\n\nfunction getOrCreateSessionId(): string {\n if (typeof document === \"undefined\") return generateUUID();\n\n const cookies = document.cookie.split(\";\").map((c) => c.trim());\n const existing = cookies\n .find((c) => c.startsWith(`${SESSION_COOKIE_NAME}=`))\n ?.split(\"=\")[1];\n\n if (existing) return existing;\n\n const id = generateUUID();\n // `Secure` gates the cookie to HTTPS origins. All Shopify storefronts\n // use HTTPS in production; local dev hits http://localhost, where\n // browsers skip the Secure attribute rather than rejecting the\n // Set-Cookie header. No behavior change on either.\n document.cookie = `${SESSION_COOKIE_NAME}=${id}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;\n return id;\n}\n\nfunction generateUUID(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n // Fallback\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * FNV-1a hash-based deterministic variant assignment. Same hash is\n * mirrored in the storefront widget's bundle-widget.js so the cart\n * attribute and the analytics event both record the same bucket.\n */\nfunction fnv1aVariant(sessionId: string, testId: string): \"A\" | \"B\" {\n const input = sessionId + \":\" + testId;\n let hash = 0x811c9dc5; // FNV offset basis\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193); // FNV prime\n }\n return (hash >>> 0) % 2 === 0 ? \"A\" : \"B\";\n}\n","/**\n * CSS sanitization for merchant-authored custom CSS.\n *\n * Kept in lockstep with `app/lib/sanitize-css.ts`. When updating one,\n * update the other — the app-side sanitizer runs on merchant INPUT when\n * CSS is saved to the shop metafield, and this SDK-side sanitizer runs\n * on OUTPUT before the CSS is injected into a <style> tag on the\n * storefront. Both must enforce the same rules; a drift means either\n * the merchant's saved CSS fails to render on headless, or malicious\n * CSS sneaks through one side but not the other.\n *\n * 5-step pipeline:\n * 1. Strip comments (prevents keyword obfuscation like @im/**\\/port)\n * 2. Decode unicode escapes (prevents \\0040import → @import bypass)\n * 3. Strip HTML angle brackets (prevents </style><script> breakout)\n * 4. Reject blocked CSS features (@import, expression(), behavior, ...)\n * 5. Allowlist url() values (https://, relative, fragment only)\n */\n\nexport const MAX_CSS_LENGTH = 10_000;\n\nconst BLOCKED_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n [/@import/i, \"@import rules\"],\n [/@charset/i, \"@charset declarations\"],\n [/expression\\s*\\(/i, \"CSS expressions\"],\n [/-moz-binding/i, \"-moz-binding\"],\n [/-webkit-binding/i, \"-webkit-binding\"],\n [/behavior\\s*:/i, \"behavior property\"],\n];\n\nconst SAFE_URL_VALUE = /^(https:|\\/[^/]|\\.\\/|\\.\\.\\/|#)/;\n\nexport type SanitizeResult =\n | { ok: true; css: string }\n | { ok: false; error: string };\n\nexport function sanitizeCustomCss(raw: string): SanitizeResult {\n if (raw.length > MAX_CSS_LENGTH) {\n return {\n ok: false,\n error: `CSS exceeds ${MAX_CSS_LENGTH.toLocaleString(\"en-US\")} character limit`,\n };\n }\n\n let css = raw.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n try {\n // 1. Decode hex unicode escapes: \\XXXXXX → codepoint. CSS lets authors\n // obfuscate keywords this way (e.g. `\\0040import` → `@import`).\n css = css.replace(/\\\\([0-9a-fA-F]{1,6})[ \\t\\n\\f]?/g, (_, hex) => {\n const codePoint = parseInt(hex, 16);\n if (codePoint < 0 || codePoint > 0x10ffff) return \"\\uFFFD\";\n return String.fromCodePoint(codePoint);\n });\n // 2. Fold backslash + newline (line continuation per CSS spec).\n css = css.replace(/\\\\\\r?\\n/g, \"\");\n // 3. Decode character escapes: \\c → c (backslash + non-hex non-newline).\n // Without this, `@\\i\\mport` would slip past our keyword blocklist.\n css = css.replace(/\\\\([^\\n\\r\\f0-9a-fA-F])/g, \"$1\");\n } catch {\n return {\n ok: false,\n error: \"CSS contains invalid unicode escape sequences\",\n };\n }\n\n css = css.replace(/</g, \"\").replace(/>/g, \"\");\n\n for (const [pattern, label] of BLOCKED_PATTERNS) {\n if (pattern.test(css)) {\n return { ok: false, error: `CSS contains blocked pattern: ${label}` };\n }\n }\n\n // url() extraction: match ANY opening paren → closing paren content,\n // regardless of whether quotes match. Previous regex used `\\1` backref\n // which allowed malformed inputs like `url('javascript:alert(1))` to\n // slip past (mismatched quote → no match → no validation). Now every\n // url(...) is extracted and validated.\n const urlPattern = /url\\s*\\(\\s*([\\s\\S]*?)\\s*\\)/gi;\n let urlMatch: RegExpExecArray | null;\n while ((urlMatch = urlPattern.exec(css)) !== null) {\n let urlValue = urlMatch[1].trim();\n // Strip a single matching or unmatched quote from either end.\n if (urlValue.startsWith(\"'\") || urlValue.startsWith('\"')) {\n urlValue = urlValue.slice(1);\n }\n if (urlValue.endsWith(\"'\") || urlValue.endsWith('\"')) {\n urlValue = urlValue.slice(0, -1);\n }\n urlValue = urlValue.trim();\n if (urlValue && !SAFE_URL_VALUE.test(urlValue)) {\n return {\n ok: false,\n error: \"CSS url() values must use https:// or relative paths\",\n };\n }\n }\n\n return { ok: true, css };\n}\n","/**\n * Merchant custom-CSS injection into the storefront DOM.\n *\n * The headless SDK fetches `$app:custom_css` from the shop metafield\n * alongside bundle data, runs the sanitizer, and injects the output as\n * a <style> tag scoped to a deterministic id. Dedup is by id — if a\n * merchant has two <lime-bundle> elements on the same page, only one\n * <style> tag appears.\n *\n * SSR-safe: no-op when `document` is undefined.\n */\nimport { sanitizeCustomCss } from \"./sanitize\";\n\nconst STYLE_ID_PREFIX = \"lb-custom-css-\";\n\n/**\n * Inject merchant CSS into the document head. Idempotent — calling\n * repeatedly with the same shop updates the existing <style> tag in\n * place rather than appending new ones.\n *\n * Returns true if CSS was injected (or updated), false if skipped\n * (SSR, empty CSS, or sanitization rejected the input).\n */\nexport function injectCustomCss(\n shopDomain: string,\n rawCss: string | null | undefined,\n): boolean {\n if (typeof document === \"undefined\") return false;\n if (!rawCss) return false;\n\n const sanitized = sanitizeCustomCss(rawCss);\n if (!sanitized.ok) return false;\n if (!sanitized.css.trim()) return false;\n\n const id = STYLE_ID_PREFIX + simpleHash(shopDomain);\n\n let style = document.getElementById(id) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement(\"style\");\n style.id = id;\n style.setAttribute(\"data-lime-bundles\", \"custom-css\");\n document.head.appendChild(style);\n }\n\n // Only touch textContent when it actually changed — avoids triggering\n // layout or style-recalc cycles on repeat invocations.\n if (style.textContent !== sanitized.css) {\n style.textContent = sanitized.css;\n }\n return true;\n}\n\n/** FNV-1a — short, fast, DOM-id-safe hash. Not cryptographic. */\nfunction simpleHash(input: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16);\n}\n","/**\n * Price formatting utilities.\n */\nimport type {\n Money,\n UnitPriceMeasurement,\n UnitPriceMeasurementUnit,\n} from \"../storefront-api/types\";\n\nexport function formatMoney(amount: string | number, currencyCode: string): string {\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(num);\n } catch {\n return `${currencyCode} ${num.toFixed(2)}`;\n }\n}\n\nexport function calculateDiscount(\n price: number,\n discountType: \"percentage\" | \"fixed_amount\",\n discountValue: number,\n): number {\n if (discountType === \"percentage\") {\n return Math.max(0, price * (1 - discountValue / 100));\n }\n return Math.max(0, price - discountValue);\n}\n\nconst UNIT_LABEL: Record<UnitPriceMeasurementUnit, string> = {\n CL: \"cl\",\n CM: \"cm\",\n FLOZ: \"fl oz\",\n FT: \"ft\",\n FT2: \"ft²\",\n G: \"g\",\n GAL: \"gal\",\n IN: \"in\",\n ITEM: \"each\",\n KG: \"kg\",\n L: \"l\",\n LB: \"lb\",\n M: \"m\",\n M2: \"m²\",\n M3: \"m³\",\n MG: \"mg\",\n ML: \"ml\",\n MM: \"mm\",\n OZ: \"oz\",\n PT: \"pt\",\n QT: \"qt\",\n UNKNOWN: \"\",\n YD: \"yd\",\n};\n\n/**\n * Format a Storefront `unitPrice` + `unitPriceMeasurement` pair as a\n * compact display string (\"$0.50/100ml\", \"$12.99/kg\", \"$2.00/each\").\n *\n * Returns `null` when either input is absent — the merchant hasn't\n * configured unit pricing in the Shopify admin and no UI should render.\n *\n * The reference value is omitted when it equals 1 (\"$X.XX/kg\" rather\n * than \"$X.XX/1kg\") to match the format Shopify's `money` filter +\n * `unit_price_with_measurement` produce in Liquid themes.\n */\nexport function formatUnitPrice(\n unitPrice: Money | null | undefined,\n measurement: UnitPriceMeasurement | null | undefined,\n fallbackCurrency?: string,\n): string | null {\n if (!unitPrice || !measurement || measurement.referenceUnit === \"UNKNOWN\") {\n return null;\n }\n\n // Guard against malformed Money.amount strings — Storefront responses\n // are always valid decimals, but a non-finite number would silently\n // render \"$NaN\" via Intl.NumberFormat.format(NaN).\n const amount = parseFloat(unitPrice.amount);\n if (!Number.isFinite(amount)) return null;\n\n // `?? \"\"` + `if (!label)` is defence against Shopify adding enum values\n // we don't yet know about — the type-level lookup would still succeed\n // but the runtime value would be undefined.\n const label = UNIT_LABEL[measurement.referenceUnit] ?? \"\";\n if (!label) return null;\n\n const currency = unitPrice.currencyCode || fallbackCurrency || \"USD\";\n const formatted = formatMoney(amount, currency);\n const value = measurement.referenceValue;\n const measurementText = value === 1 ? label : `${value}${label}`;\n return `${formatted}/${measurementText}`;\n}\n","// Integer-cent arithmetic that matches Shopify's Discount Function\n// per-unit floor rounding — naive `total * (1 - discount)` drifts a\n// cent or two from what the customer actually pays.\nimport type { FixedBundleData, DiscountConfig } from \"../bundle/types\";\n\nexport interface PricingRow {\n /** Pre-discount unit price in cents. */\n unitCents: number;\n /** Quantity of this product in the bundle. */\n qty: number;\n /** Pre-discount line subtotal in cents (`unitCents * qty`). */\n lineCents: number;\n /** Variant's compare-at price in cents, if different from unit. */\n compareCents: number | null;\n}\n\nexport interface FixedBundlePricing {\n rows: PricingRow[];\n /** Pre-discount bundle total in cents. */\n totalCents: number;\n /** Post-discount bundle total in cents. */\n saleCents: number;\n /** Savings in cents — always `max(0, totalCents - saleCents)`. */\n savingsCents: number;\n /** Pre-formatted header badge text (e.g. `-75%`, `-$12.00`). Empty when `pricing.showSaveBadge` is false or no savings. */\n headerBadge: string;\n currency: string;\n}\n\n/**\n * Parse a money amount (Storefront API returns major-unit strings like\n * \"749.95\") into integer cents. Safe on numbers, null, undefined.\n * Rounds to the nearest cent to absorb float-representation drift.\n */\nexport function parseCents(\n amount: string | number | null | undefined,\n): number {\n if (amount === null || amount === undefined) return 0;\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n if (!Number.isFinite(num)) return 0;\n return Math.round(num * 100);\n}\n\n/**\n * Format integer cents as localized currency. Falls back to\n * `CUR 12.34` when the runtime's Intl.NumberFormat rejects the code.\n */\nexport function formatCents(cents: number, currencyCode: string): string {\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(cents / 100);\n } catch {\n return `${currencyCode} ${(cents / 100).toFixed(2)}`;\n }\n}\n\n/**\n * Apply a percentage discount to a single unit with floor rounding —\n * matches the rounding Shopify's Discount Function performs at checkout.\n * For a bundle total, apply this per-unit and sum, rather than computing\n * a single discount on the totalled line — those paths can differ by a\n * cent or two when the per-unit price has odd cents.\n */\nexport function percentageDiscountUnit(\n unitCents: number,\n percent: number,\n): number {\n return Math.max(0, unitCents - Math.floor((unitCents * percent) / 100));\n}\n\n/**\n * Compute the full pricing snapshot for a fixed bundle. Handles\n * percentage and fixed-amount discount types, per-product quantities,\n * and the header badge format that matches the Liquid theme block.\n *\n * `productQuantities` defaults to `bundle.productQuantities` (the\n * merchant-configured map); product IDs missing from that map fall back\n * to qty 1 via the `?? 1` in the row loop.\n * The bundle's `widgetConfig.pricing.showSaveBadge` is read unless\n * overridden.\n */\nexport function computeFixedPricing(\n bundle: FixedBundleData,\n productQuantities: Record<string, number> = bundle.productQuantities,\n showSaveBadge: boolean = bundle.widgetConfig.pricing.showSaveBadge,\n): FixedBundlePricing {\n const { discountType, discountValue } = bundle.discountConfig;\n const rows: PricingRow[] = [];\n let totalCents = 0;\n let saleCents = 0;\n\n for (const product of bundle.products) {\n const variant =\n product.variants.nodes.find((v) => v.availableForSale) ??\n product.variants.nodes[0];\n if (!variant) continue;\n\n const unitCents = parseCents(variant.price.amount);\n const qty = productQuantities[product.id] ?? 1;\n const lineCents = unitCents * qty;\n const compareCents = variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null;\n\n rows.push({ unitCents, qty, lineCents, compareCents });\n totalCents += lineCents;\n\n if (discountType === \"percentage\") {\n const perUnit = percentageDiscountUnit(unitCents, discountValue);\n saleCents += perUnit * qty;\n } else {\n saleCents += lineCents;\n }\n }\n\n if (discountType === \"fixed_amount\") {\n saleCents = Math.max(0, totalCents - Math.round(discountValue * 100));\n }\n\n const savingsCents = Math.max(0, totalCents - saleCents);\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n let headerBadge = \"\";\n if (showSaveBadge && savingsCents > 0) {\n if (discountType === \"percentage\" && discountValue > 0) {\n headerBadge = `-${Math.round(discountValue)}%`;\n } else if (discountType === \"fixed_amount\" && discountValue > 0) {\n headerBadge = `-${formatCents(\n Math.round(discountValue * 100),\n currency,\n )}`;\n }\n }\n\n return { rows, totalCents, saleCents, savingsCents, headerBadge, currency };\n}\n\n/**\n * Pure helper: compute the sale-price total for an arbitrary set of\n * cart lines under a discount config. Useful for mix-match bundles\n * where the lines are user-selected rather than bundle-configured.\n * Applies percentage discount on the aggregated total (mix-match uses\n * `applies_to_each_item: false`), matching the Liquid block's behaviour.\n */\nexport function computeBundleSaleCents(\n totalCents: number,\n discount: DiscountConfig,\n): number {\n if (discount.discountType === \"percentage\") {\n const off = Math.floor((totalCents * discount.discountValue) / 100);\n return Math.max(0, totalCents - off);\n }\n return Math.max(0, totalCents - Math.round(discount.discountValue * 100));\n}\n","/**\n * Append Shopify CDN image-transform params to a product-image URL so the\n * browser downloads a correctly-sized asset. Mirrors what Liquid's\n * `image_url: width:..., height:..., crop:...` filter chain produces.\n * Non-Shopify CDNs silently ignore the extra params — graceful fallback.\n */\nexport interface ImageTransform {\n width?: number;\n height?: number;\n crop?: \"center\" | \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\n/**\n * Standard thumbnail size for bundle product rows and picker tiles.\n * Matches the Liquid theme's `image_url: width: 152, height: 152,\n * crop: 'center'`. CSS caps display size around 76px; 152 gives\n * retina-quality rendering on 2x displays.\n */\nexport const THUMB_PX = 152;\n\nexport function transformImageUrl(\n url: string | null | undefined,\n t: ImageTransform = {},\n): string {\n if (!url) return \"\";\n try {\n const u = new URL(url);\n if (t.width !== undefined) u.searchParams.set(\"width\", String(t.width));\n if (t.height !== undefined) u.searchParams.set(\"height\", String(t.height));\n if (t.crop) u.searchParams.set(\"crop\", t.crop);\n return u.toString();\n } catch {\n return url;\n }\n}\n","/**\n * Pure countdown formatter — returns the `Nd HHh MMm SSs` / `HHh MMm SSs`\n * string given a millisecond remaining. SSR-safe, no DOM, no timers.\n *\n * A developer building their own widget wires their own `setInterval`\n * + React state and calls this to format the label. The web component's\n * `renderCountdown` uses this internally.\n */\nexport function formatCountdown(msRemaining: number): string {\n if (!Number.isFinite(msRemaining) || msRemaining <= 0) return \"\";\n const totalSeconds = Math.floor(msRemaining / 1000);\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n const pad = (n: number) => String(n).padStart(2, \"0\");\n if (days > 0) {\n return `${days}d ${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n }\n return `${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n}\n","// Public surface — only the algorithm and its types. Layout-tuning\n// constants (item height, list padding, max visible items) live inline\n// in each consumer so the SDK contract isn't locked into specific\n// numeric values that might change.\nexport {\n computePosition,\n type DropdownPlacement,\n type ComputePositionArgs,\n type ComputePositionResult,\n} from \"./compute-position\";\n\nexport {\n handleKey,\n type DropdownKeyEvent,\n type DropdownKeyState,\n type DropdownAction,\n} from \"./keyboard\";\n\nexport {\n pushTypeAheadChar,\n emptyTypeAheadState,\n type TypeAheadState,\n type TypeAheadOption,\n type PushCharResult,\n} from \"./type-ahead\";\n","/**\n * Pure positioning helper for an accessible dropdown popover.\n *\n * Given a trigger rect and the viewport height, decide whether the panel\n * should open above or below the trigger and how tall it can be. Used by\n * the Liquid theme dropdown, the `<lime-bundle>` web component, and the\n * React `<VariantDropdown>` so all three surfaces agree.\n *\n * The function is intentionally framework-agnostic — caller passes plain\n * numbers (typically from `getBoundingClientRect()` and `window.innerHeight`)\n * and applies the returned `position: fixed` coordinates itself.\n */\n\nexport const DEFAULT_TRIGGER_MARGIN = 4;\n\nexport type DropdownPlacement = \"up\" | \"down\";\n\nexport interface ComputePositionArgs {\n trigger: { top: number; bottom: number; left: number; width: number };\n viewportHeight: number;\n desiredHeight: number;\n margin?: number;\n /**\n * Optional viewport-coordinate bounds of the nearest clipping ancestor\n * (e.g. a `overflow:auto` product list). When provided, placement is\n * decided against these bounds instead of the full viewport so a panel\n * inside a short scroll container flips upward before it would clip.\n *\n * Bounds are intersected with the viewport (`[0, viewportHeight]`) so a\n * caller that passes the raw `getBoundingClientRect()` of an ancestor\n * still gets a sane result when the ancestor itself extends off-screen.\n */\n clip?: { top: number; bottom: number };\n}\n\nexport interface ComputePositionResult {\n placement: DropdownPlacement;\n maxHeight: number;\n offsetTop: number;\n offsetLeft: number;\n width: number;\n}\n\nexport function computePosition({\n trigger,\n viewportHeight,\n desiredHeight,\n margin = DEFAULT_TRIGGER_MARGIN,\n clip,\n}: ComputePositionArgs): ComputePositionResult {\n const upperBound = clip\n ? Math.min(viewportHeight, clip.bottom)\n : viewportHeight;\n const lowerBound = clip ? Math.max(0, clip.top) : 0;\n const spaceBelow = Math.max(0, upperBound - trigger.bottom - margin);\n const spaceAbove = Math.max(0, trigger.top - lowerBound - margin);\n\n // Prefer downward unless the panel would clip AND upward has more room.\n // Matches native <select> behaviour on most platforms.\n const placement: DropdownPlacement =\n desiredHeight <= spaceBelow\n ? \"down\"\n : spaceAbove > spaceBelow\n ? \"up\"\n : \"down\";\n\n const available = placement === \"down\" ? spaceBelow : spaceAbove;\n // Cap maxHeight at the actual available space so the panel never spills\n // outside the viewport. When `available` is less than the desired height\n // the listbox shrinks and its `overflow-y: auto` scrolls — better than\n // overflowing the viewport with a fixed-minimum tall panel.\n const maxHeight = Math.min(desiredHeight, available);\n\n const offsetTop =\n placement === \"down\"\n ? trigger.bottom + margin\n : trigger.top - margin - maxHeight;\n\n return {\n placement,\n maxHeight,\n offsetTop,\n offsetLeft: trigger.left,\n width: trigger.width,\n };\n}\n","/**\n * Keyboard event → action discriminator for an accessible dropdown.\n *\n * The DOM glue layer (Liquid asset, web component, React component) calls\n * `handleKey(event, state)` with the event details and current state, then\n * applies the returned action. This keeps WAI-ARIA combobox semantics in a\n * single, unit-testable place.\n *\n * Pattern follows the WAI-ARIA Authoring Practices \"Combobox with Listbox\n * Popup, manual selection\" pattern.\n */\n\nexport interface DropdownKeyEvent {\n key: string;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n shiftKey?: boolean;\n}\n\nexport interface DropdownKeyState {\n isOpen: boolean;\n activeIndex: number;\n selectedIndex: number;\n options: ReadonlyArray<{ disabled: boolean }>;\n}\n\nexport type DropdownAction =\n | {\n type: \"open\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"close\";\n commit: false;\n restoreFocus: boolean;\n preventDefault: boolean;\n }\n | {\n type: \"move-active\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"commit\";\n index: number;\n preventDefault: true;\n }\n | {\n type: \"type-ahead\";\n char: string;\n preventDefault: false;\n }\n | {\n type: \"passthrough\";\n preventDefault: boolean;\n };\n\nconst PASSTHROUGH: DropdownAction = {\n type: \"passthrough\",\n preventDefault: false,\n};\n\nfunction firstEnabled(options: ReadonlyArray<{ disabled: boolean }>): number {\n for (let i = 0; i < options.length; i++) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction lastEnabled(options: ReadonlyArray<{ disabled: boolean }>): number {\n for (let i = options.length - 1; i >= 0; i--) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction nextEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from + step) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from; // all disabled — stay put\n}\n\nfunction prevEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from - step + options.length) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from;\n}\n\nfunction isPrintable(key: string): boolean {\n return key.length === 1 && key !== \" \" && /\\S/.test(key);\n}\n\nexport function handleKey(\n event: DropdownKeyEvent,\n state: DropdownKeyState,\n): DropdownAction {\n // Ignore modifier-key combos (browser shortcuts, etc).\n if (event.ctrlKey || event.metaKey || event.altKey) return PASSTHROUGH;\n\n const { key } = event;\n const { isOpen, activeIndex, selectedIndex, options } = state;\n\n // Closed → key opens (or noop)\n if (!isOpen) {\n switch (key) {\n case \"Enter\":\n case \" \":\n case \"ArrowDown\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : lastEnabled(options),\n preventDefault: true,\n };\n default:\n if (isPrintable(key)) {\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n }\n return PASSTHROUGH;\n }\n }\n\n // Open → navigation, commit, close\n switch (key) {\n case \"ArrowDown\":\n return {\n type: \"move-active\",\n activeIndex: nextEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"move-active\",\n activeIndex: prevEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"Home\":\n return {\n type: \"move-active\",\n activeIndex: firstEnabled(options),\n preventDefault: true,\n };\n case \"End\":\n return {\n type: \"move-active\",\n activeIndex: lastEnabled(options),\n preventDefault: true,\n };\n case \"Enter\":\n case \" \":\n if (activeIndex >= 0 && !options[activeIndex]?.disabled) {\n return {\n type: \"commit\",\n index: activeIndex,\n preventDefault: true,\n };\n }\n // Enter while open with no committable target — swallow it. The\n // dropdown lives inside a Shopify product-page <form>, so a\n // bubbling Enter would submit the page form. WAI-ARIA combobox\n // pattern says the popup should absorb Enter regardless.\n return { type: \"passthrough\", preventDefault: true };\n case \"Escape\":\n return {\n type: \"close\",\n commit: false,\n restoreFocus: true,\n preventDefault: true,\n };\n case \"Tab\":\n // Don't preventDefault — let the browser advance focus naturally.\n // Don't restoreFocus either: stealing focus back to the trigger\n // would interfere with the browser's tab-advance to the next\n // focusable element.\n return {\n type: \"close\",\n commit: false,\n restoreFocus: false,\n preventDefault: false,\n };\n default:\n if (isPrintable(key)) {\n return { type: \"type-ahead\", char: key, preventDefault: false };\n }\n return PASSTHROUGH;\n }\n}\n","/**\n * Type-ahead matching for dropdown listboxes.\n *\n * The caller drives `now` (typically `Date.now()`) so the function stays\n * pure and deterministic in tests. After `RESET_MS` of inactivity the\n * buffer clears; otherwise additional chars accumulate so the user can\n * type \"me\" to jump from \"Mango\" to \"Medium\" without overshoot.\n */\n\nexport const TYPE_AHEAD_RESET_MS = 500;\n\nexport interface TypeAheadState {\n buffer: string;\n lastTime: number;\n}\n\nexport interface TypeAheadOption {\n disabled: boolean;\n label: string;\n}\n\nexport function emptyTypeAheadState(): TypeAheadState {\n return { buffer: \"\", lastTime: 0 };\n}\n\nexport interface PushCharResult {\n newState: TypeAheadState;\n matchedIndex: number | null;\n}\n\nexport function pushTypeAheadChar(\n state: TypeAheadState,\n char: string,\n now: number,\n options: ReadonlyArray<TypeAheadOption>,\n resetMs: number = TYPE_AHEAD_RESET_MS,\n): PushCharResult {\n if (char.length !== 1) {\n return { newState: state, matchedIndex: null };\n }\n\n const expired = now - state.lastTime > resetMs;\n const buffer = (expired ? \"\" : state.buffer) + char.toLowerCase();\n const newState: TypeAheadState = { buffer, lastTime: now };\n\n for (let i = 0; i < options.length; i++) {\n const opt = options[i];\n if (opt.disabled) continue;\n if (opt.label.toLowerCase().startsWith(buffer)) {\n return { newState, matchedIndex: i };\n }\n }\n\n return { newState, matchedIndex: null };\n}\n","/**\n * Variant resolution + availability cascade for per-option pickers.\n *\n * Mirrors the algorithms in `extensions/bundle-theme/assets/bundle-fixed.js`\n * (`findVariantByOptions`, `isOptionValueAvailable`) so the React component\n * and web component renderer share the same source of truth as the Liquid\n * widget. Liquid keeps its inlined copy because theme assets cannot import\n * npm — a CI parity guard prevents drift.\n *\n * Inputs are normalized to positional option-value arrays. Storefront API\n * consumers can derive this with `v.selectedOptions.map(o => o.value)`.\n */\n\nexport interface PickerVariant {\n id: string;\n /** Positional option values: index i matches the product's option-position i+1. */\n optionValues: string[];\n /** True when the variant has stock and is purchasable. */\n available: boolean;\n}\n\n/**\n * Returns the variant whose positional options exactly match the selected\n * values, or `null` if none. Caller decides what to do on miss (typically\n * fall back to the first available variant).\n */\nexport function findVariantByOptions<V extends PickerVariant>(\n variants: ReadonlyArray<V>,\n optionValues: ReadonlyArray<string>,\n): V | null {\n if (variants.length === 0) return null;\n for (const v of variants) {\n if (v.optionValues.length !== optionValues.length) continue;\n let match = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (v.optionValues[j] !== optionValues[j]) {\n match = false;\n break;\n }\n }\n if (match) return v;\n }\n return null;\n}\n\n/**\n * True iff some in-stock variant pairs `value` at `optionIndex` with the\n * currently selected values at every other option index. Drives the\n * disabled state of options in cascading pickers (e.g. selecting Color=Red\n * may disable Size=XL if no Red XL variant exists or it's sold out).\n */\nexport function isOptionValueAvailable(\n variants: ReadonlyArray<PickerVariant>,\n optionIndex: number,\n value: string,\n selected: ReadonlyArray<string>,\n): boolean {\n for (const v of variants) {\n if (!v.available) continue;\n if (v.optionValues[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (j === optionIndex) continue;\n if (v.optionValues[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n}\n\n/**\n * Convenience helper for Storefront API consumers. Converts a Storefront\n * `ProductVariant` (with `selectedOptions: [{name, value}]`) into the\n * normalized `PickerVariant` shape.\n */\nexport function toPickerVariant<\n T extends {\n id: string;\n selectedOptions: ReadonlyArray<{ value: string }>;\n availableForSale: boolean;\n },\n>(variant: T): PickerVariant {\n return {\n id: variant.id,\n optionValues: variant.selectedOptions.map((o) => o.value),\n available: variant.availableForSale,\n };\n}\n","/**\n * Storefront-side fulfillability predicate.\n *\n * Mirrors `app/lib/inventory.server.ts:isFulfillable` but reads from a\n * Storefront-shaped variant (no Admin types, no Node imports). Same\n * semantics: a tracked + DENY variant with insufficient stock fails;\n * everything else succeeds.\n *\n * Used by the widget renderers and React components to gate add-to-cart\n * eligibility, badge rendering, and stepper caps. Lives in `core` so the\n * Liquid widget (via `@lime-bundles/widget`) and React consumers (via\n * `@lime-bundles/react`) draw from one source of truth.\n *\n * Storefront API limitations:\n * - `quantityAvailable` is `null` when the merchant has not granted\n * `unauthenticated_read_product_inventory` to the Storefront token.\n * In that case, treat the variant as fulfillable (boolean fallback)\n * rather than blocking the cart on a permission gap.\n * - `currentlyNotInStock` is true for backordered variants (zero\n * stock + `inventoryPolicy = CONTINUE`). Fulfillable for cart\n * purposes — Shopify's checkout still accepts them.\n */\n\n/**\n * Minimal variant shape the predicate needs. Wider Storefront variants\n * (with title, price, etc.) are accepted via structural typing.\n */\nexport interface StorefrontVariantStock {\n availableForSale: boolean;\n /** Optional — Storefront API field, may be undefined on hand-built mocks. */\n currentlyNotInStock?: boolean;\n /** Null when the Storefront token lacks the inventory scope, or when\n * the variant is untracked. Treat as \"unknown stock — defer to\n * availableForSale\". */\n quantityAvailable?: number | null;\n}\n\n/**\n * True when a customer can add `requiredQty` units of this variant to\n * their cart and pass Shopify's checkout-time stock check.\n *\n * - `availableForSale = false` → never fulfillable (Shopify will reject).\n * - `currentlyNotInStock = true` → backordered (zero stock + CONTINUE\n * inventory policy). Shopify will accept the cart line for any qty,\n * so treat as fulfillable. Mirrors the Admin-side predicate's\n * CONTINUE branch in [app/lib/inventory.server.ts].\n * - `quantityAvailable` is `null` / `undefined` → defer to the boolean\n * (the merchant's Storefront token may be missing\n * `unauthenticated_read_product_inventory`, or the variant is\n * untracked).\n * - Otherwise → require `quantityAvailable >= requiredQty`.\n */\nexport function isFulfillable(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): boolean {\n if (!variant.availableForSale) return false;\n if (variant.currentlyNotInStock) return true;\n if (variant.quantityAvailable == null) return true;\n return variant.quantityAvailable >= requiredQty;\n}\n\n/**\n * Whether the \"Only X left\" low-stock badge should render for this\n * variant. False when:\n * - The merchant disabled the badge (`enabled = false`).\n * - We don't know the quantity (`quantityAvailable` is null/undefined).\n * - The variant is below `requiredQty` (the renderer treats this as\n * out-of-stock instead — no double signaling).\n * - The variant has more stock than the threshold (no badge needed).\n */\nexport function shouldShowLowStockBadge(\n variant: StorefrontVariantStock,\n requiredQty: number,\n threshold: number,\n enabled: boolean,\n): boolean {\n if (!enabled) return false;\n if (variant.quantityAvailable == null) return false;\n if (variant.quantityAvailable < requiredQty) return false;\n return variant.quantityAvailable <= threshold;\n}\n\n/**\n * Maximum number of bundles the customer can request given a variant's\n * stock and the per-bundle required quantity. Used to cap quantity\n * steppers — without a cap, a customer can repeatedly press \"+\" until\n * the cart-add request fails at Shopify's checkout-time stock check.\n *\n * Returns `Infinity` when stock is unknown or unbounded (the boolean\n * fallback path) — callers should guard with the existing UI ceiling\n * (e.g. `Math.min(maxBundlesPerOrder, maxBundlesByStock)`).\n */\nexport function maxBundlesByStock(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): number {\n if (variant.quantityAvailable == null) return Number.POSITIVE_INFINITY;\n if (requiredQty <= 0) return Number.POSITIVE_INFINITY;\n return Math.floor(variant.quantityAvailable / requiredQty);\n}\n\n/**\n * Largest qty the shopper may add to a mix-and-match bundle pick for a given\n * variant, after subtracting any units they've already allocated to this\n * bundle. Returns 0 when the variant isn't fulfillable.\n *\n * Bounded by:\n * - the merchant's per-product `productMax` (rule.max)\n * - the variant's `quantityAvailable` (when known)\n * Backordered variants (`currentlyNotInStock` with policy CONTINUE) bypass\n * the stock cap and rely on `productMax`.\n */\nexport function maxAddableQuantity(\n variant: StorefrontVariantStock,\n productMax: number,\n alreadyInBundle: number,\n): number {\n if (!variant.availableForSale) return 0;\n // Backorder: stock cap doesn't apply.\n if (variant.currentlyNotInStock) {\n return Math.max(0, productMax - alreadyInBundle);\n }\n const stockCap =\n variant.quantityAvailable == null\n ? Number.POSITIVE_INFINITY\n : variant.quantityAvailable;\n return Math.max(0, Math.min(productMax, stockCap) - alreadyInBundle);\n}\n","/**\n * TypeScript bind helper for the custom variant-picker dropdown inside the\n * `<lime-bundle>` web component. Uses pure algorithms from\n * `@lime-bundles/core/dropdown` and adds the DOM glue for shadow-DOM use.\n *\n * Mirrors the contract of the vanilla theme asset\n * `extensions/bundle-theme/assets/bundle-dropdown.js`. The native `<select>`\n * stays in DOM as the canonical state holder; the custom UI dispatches\n * synthetic `change` events on commit so existing renderer change-handlers\n * work unchanged.\n */\nimport { dropdown } from \"@lime-bundles/core\";\n\nconst { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } =\n dropdown;\n\n// Layout-tuning constants for panel-height calculation. Inlined here\n// rather than imported from core so they stay tweakable without locking\n// numeric values into the public SDK contract.\nconst ITEM_HEIGHT_PX = 32;\nconst LIST_PAD_Y = 8;\nconst MAX_VISIBLE_ITEMS = 8;\ntype DropdownAction = dropdown.DropdownAction;\ntype TypeAheadState = dropdown.TypeAheadState;\n\nexport interface DropdownInstance {\n readonly shell: HTMLElement;\n readonly listbox: HTMLElement;\n readonly select: HTMLSelectElement;\n close(): void;\n destroy(): void;\n}\n\n// Single module-level set of currently open dropdowns. Document-level\n// listeners are attached on the 0→1 transition and detached on 1→0.\n// `composedPath()` lets one listener correctly identify hits across any\n// number of shadow roots — events bubble out of shadow with retargeted\n// `event.target`, but composedPath still surfaces the original element.\n// `bind-dropdown` keeps a module-level list of currently-open instances so\n// document-level listeners are reference-counted (one set of listeners\n// across N dropdowns). Note for test authors: a test that opens a\n// dropdown without calling `inst.destroy()` in cleanup will leak document\n// listeners across cases — call __resetDropdownsForTest() in beforeEach\n// or always tear down via the returned instance.\nconst openInstances: DropdownInstance[] = [];\n\n// Outside-click and ancestor-scroll both close any open dropdown whose\n// shell + listbox aren't in the event path. Same handler for both.\nfunction closeOutsideEvent(event: Event) {\n const path = event.composedPath();\n for (let i = openInstances.length - 1; i >= 0; i--) {\n const inst = openInstances[i];\n if (!path.includes(inst.shell) && !path.includes(inst.listbox)) {\n inst.close();\n }\n }\n}\n\nfunction onDocResize() {\n for (let i = openInstances.length - 1; i >= 0; i--) openInstances[i].close();\n}\n\nlet docListenersAttached = false;\nfunction attachDocumentListeners() {\n if (docListenersAttached) return;\n document.addEventListener(\"pointerdown\", closeOutsideEvent, true);\n window.addEventListener(\"scroll\", closeOutsideEvent, true);\n window.addEventListener(\"resize\", onDocResize);\n docListenersAttached = true;\n}\n\nfunction detachDocumentListeners() {\n if (!docListenersAttached || openInstances.length > 0) return;\n document.removeEventListener(\"pointerdown\", closeOutsideEvent, true);\n window.removeEventListener(\"scroll\", closeOutsideEvent, true);\n window.removeEventListener(\"resize\", onDocResize);\n docListenersAttached = false;\n}\n\n/** Test-only reset hook. Vitest caches module imports across cases; a\n * test that opens a dropdown without destroying it would leak document\n * listeners and stale entries in openInstances into subsequent tests.\n * Call this in beforeEach when a test exercises bindDropdown directly. */\nexport function __resetDropdownsForTest(): void {\n while (openInstances.length > 0) {\n openInstances[openInstances.length - 1].destroy();\n }\n detachDocumentListeners();\n}\n\ntype OptionState = dropdown.TypeAheadOption;\n\nfunction readOptions(select: HTMLSelectElement): OptionState[] {\n const out: OptionState[] = [];\n for (let i = 0; i < select.options.length; i++) {\n const o = select.options[i];\n out.push({ disabled: o.disabled, label: o.textContent || o.value });\n }\n return out;\n}\n\nfunction firstEnabled(opts: OptionState[]): number {\n for (let i = 0; i < opts.length; i++) if (!opts[i].disabled) return i;\n return -1;\n}\n\ntype SelectWithInstance = HTMLSelectElement & {\n __lbDropdownInstance?: DropdownInstance;\n};\n\n// Walk up to find the nearest ancestor that clips overflow on the Y axis.\n// Used so placement flips upward before the listbox would be hidden by a\n// scrollable container (`.lb-fixed__products` / `.lb-bundle__products`).\n// Stops at body/html — past that, the viewport bound is correct.\nfunction findScrollableAncestor(el: Element): HTMLElement | null {\n const win = el.ownerDocument?.defaultView;\n if (!win) return null;\n let cur: Element | null = el.parentElement;\n while (cur && cur !== el.ownerDocument.body) {\n const style = win.getComputedStyle(cur);\n const overflowY = style.overflowY;\n if (\n overflowY === \"auto\" ||\n overflowY === \"scroll\" ||\n overflowY === \"hidden\"\n ) {\n return cur as HTMLElement;\n }\n cur = cur.parentElement;\n }\n return null;\n}\n\nconst VARIANT_SELECT_CLASSES = [\n \"lb-bundle-variant-select\",\n \"lb-mix-match__variant-select\",\n] as const;\n\nconst BIND_SELECTOR = VARIANT_SELECT_CLASSES.map(\n (c) => `select.${c}:not(.lb-dropdown-state)`,\n).join(\", \");\n\nexport function bindDropdown(\n select: HTMLSelectElement,\n): DropdownInstance | null {\n const slot = select as SelectWithInstance;\n if (select.classList.contains(\"lb-dropdown-state\")) {\n return slot.__lbDropdownInstance ?? null;\n }\n\n const doc = select.ownerDocument;\n const rootNode = select.getRootNode() as ShadowRoot | Document;\n const labelText = select.getAttribute(\"aria-label\") ?? \"\";\n const idBase = `lb-dd-${Math.random().toString(36).slice(2, 9)}`;\n\n select.classList.add(\"lb-dropdown-state\");\n select.setAttribute(\"aria-hidden\", \"true\");\n select.setAttribute(\"tabindex\", \"-1\");\n\n const shell = doc.createElement(\"div\");\n shell.className = \"lb-dropdown\";\n shell.setAttribute(\"data-lb-dropdown\", \"\");\n\n const trigger = doc.createElement(\"button\");\n trigger.type = \"button\";\n trigger.className = \"lb-dropdown-trigger\";\n trigger.setAttribute(\"role\", \"combobox\");\n trigger.setAttribute(\"aria-haspopup\", \"listbox\");\n trigger.setAttribute(\"aria-expanded\", \"false\");\n const listboxId = `${idBase}-listbox`;\n trigger.setAttribute(\"aria-controls\", listboxId);\n if (labelText) trigger.setAttribute(\"aria-label\", labelText);\n\n const triggerLabel = doc.createElement(\"span\");\n triggerLabel.className = \"lb-dropdown-trigger-value\";\n\n const chevron = doc.createElement(\"span\");\n chevron.className = \"lb-dropdown-chevron\";\n chevron.setAttribute(\"aria-hidden\", \"true\");\n\n trigger.appendChild(triggerLabel);\n trigger.appendChild(chevron);\n\n const listbox = doc.createElement(\"ul\");\n listbox.id = listboxId;\n listbox.className = \"lb-dropdown-listbox\";\n listbox.setAttribute(\"role\", \"listbox\");\n if (labelText) listbox.setAttribute(\"aria-label\", labelText);\n listbox.hidden = true;\n\n shell.appendChild(trigger);\n select.parentNode?.insertBefore(shell, select.nextSibling);\n // The mix-match modal applies translateY for its slide-in animation,\n // which turns position:fixed into relative-to-modal. Portal the\n // listbox up to the overlay (carries per-bundle --lb-* variables AND\n // has no transform of its own) only when the trigger is inside a\n // modal. For the main widget, leave the listbox inside the shell —\n // there's no transformed ancestor to escape.\n const modalOverlay = select.closest(\"[data-modal-overlay]\");\n if (modalOverlay) {\n modalOverlay.appendChild(listbox);\n listbox.setAttribute(\"data-lb-dropdown-portal\", \"\");\n } else {\n shell.appendChild(listbox);\n }\n\n let isOpen = false;\n let activeIndex = -1;\n let typeAhead: TypeAheadState = emptyTypeAheadState();\n let optionEls: HTMLLIElement[] = [];\n let instance: DropdownInstance; // eslint-disable-line prefer-const\n\n function syncFromSelect() {\n // Mirror the underlying select's disabled state to the trigger\n // button. Without this, a row that disables its <select> (e.g.\n // mix-match products already in the bundle) leaves the custom\n // dropdown trigger focusable and clickable — the visible chrome\n // disagrees with the form control's actual state.\n trigger.disabled = select.disabled;\n const opts = readOptions(select);\n const idx = select.selectedIndex;\n triggerLabel.textContent = idx >= 0 && opts[idx] ? opts[idx].label : \"\";\n\n while (listbox.firstChild) listbox.removeChild(listbox.firstChild);\n optionEls = [];\n\n for (let i = 0; i < opts.length; i++) {\n const li = doc.createElement(\"li\");\n li.id = `${idBase}-opt-${i}`;\n li.className = \"lb-dropdown-option\";\n li.setAttribute(\"role\", \"option\");\n li.setAttribute(\"aria-selected\", i === idx ? \"true\" : \"false\");\n if (opts[i].disabled) li.setAttribute(\"aria-disabled\", \"true\");\n li.setAttribute(\"data-value\", select.options[i].value);\n li.setAttribute(\"data-index\", String(i));\n li.textContent = opts[i].label;\n listbox.appendChild(li);\n optionEls.push(li);\n }\n }\n\n function setActive(newIndex: number) {\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = newIndex;\n if (newIndex >= 0 && optionEls[newIndex]) {\n const li = optionEls[newIndex];\n li.classList.add(\"is-active\");\n trigger.setAttribute(\"aria-activedescendant\", li.id);\n // Scroll within the listbox only — Element.scrollIntoView falls\n // through to the document scroll when the listbox itself isn't\n // overflowing, which can yank the page when the active option's\n // viewport position differs from its offsetTop-relative position.\n const liTop = li.offsetTop;\n const liBottom = liTop + li.offsetHeight;\n const visTop = listbox.scrollTop;\n const visBottom = visTop + listbox.clientHeight;\n if (liTop < visTop) {\n listbox.scrollTop = liTop;\n } else if (liBottom > visBottom) {\n listbox.scrollTop = liBottom - listbox.clientHeight;\n }\n } else {\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n }\n }\n\n function position(): boolean {\n const rect = trigger.getBoundingClientRect();\n if (rect.width === 0) return false;\n const visibleCount = Math.min(optionEls.length || 1, MAX_VISIBLE_ITEMS);\n const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;\n // Always clip placement to the trigger's nearest scrollable ancestor.\n // In-shell (main widget): clips to the bundle's product list so the\n // panel flips upward before it would hide behind the widget footer.\n // Portaled (modal context): clips to the modal's scrollable list\n // (e.g. `.lb-mix-match__modal-list`) so the panel flips upward when\n // the trigger sits near the modal's bottom edge — even if there's\n // viewport room below. Without this clip, position:fixed listboxes\n // attached to the overlay would open downward and overflow the modal.\n const scrollable = findScrollableAncestor(trigger);\n const clip = scrollable\n ? (() => {\n const r = scrollable.getBoundingClientRect();\n return { top: r.top, bottom: r.bottom };\n })()\n : undefined;\n const result = computePosition({\n trigger: {\n top: rect.top,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n },\n viewportHeight: window.innerHeight,\n desiredHeight,\n clip,\n });\n listbox.setAttribute(\"data-placement\", result.placement);\n listbox.style.maxHeight = `${result.maxHeight}px`;\n // In-shell (main widget) case: CSS [data-placement] selectors\n // anchor the listbox against the position:relative shell. Only the\n // portaled (modal) case needs inline viewport coords.\n if (listbox.hasAttribute(\"data-lb-dropdown-portal\")) {\n listbox.style.top = `${result.offsetTop}px`;\n listbox.style.left = `${result.offsetLeft}px`;\n listbox.style.width = `${result.width}px`;\n }\n return true;\n }\n\n function open() {\n if (isOpen) return;\n // Close any other open dropdown first — single-open semantics.\n for (let i = openInstances.length - 1; i >= 0; i--) {\n if (openInstances[i] !== instance) openInstances[i].close();\n }\n isOpen = true;\n listbox.hidden = false;\n trigger.setAttribute(\"aria-expanded\", \"true\");\n if (!position()) {\n requestAnimationFrame(() => position());\n }\n const opts = readOptions(select);\n const selIdx = select.selectedIndex;\n if (selIdx >= 0 && opts[selIdx] && !opts[selIdx].disabled) {\n setActive(selIdx);\n } else {\n setActive(firstEnabled(opts));\n }\n openInstances.push(instance);\n if (openInstances.length === 1) attachDocumentListeners();\n }\n\n function close(restoreFocus: boolean) {\n if (!isOpen) return;\n isOpen = false;\n listbox.hidden = true;\n trigger.setAttribute(\"aria-expanded\", \"false\");\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = -1;\n const idx = openInstances.indexOf(instance);\n if (idx >= 0) openInstances.splice(idx, 1);\n if (openInstances.length === 0) detachDocumentListeners();\n if (restoreFocus) trigger.focus();\n }\n\n function commit(index: number) {\n const opt = select.options[index];\n if (!opt || opt.disabled) return;\n if (select.value !== opt.value) {\n select.value = opt.value;\n const event = new Event(\"change\", { bubbles: true });\n select.dispatchEvent(event);\n }\n syncFromSelect();\n close(true);\n }\n\n function applyAction(action: DropdownAction) {\n switch (action.type) {\n case \"open\":\n open();\n if (action.activeIndex >= 0) setActive(action.activeIndex);\n return;\n case \"close\":\n close(action.restoreFocus);\n return;\n case \"move-active\":\n setActive(action.activeIndex);\n return;\n case \"commit\":\n commit(action.index);\n return;\n case \"type-ahead\": {\n const opts = readOptions(select);\n const result = pushTypeAheadChar(\n typeAhead,\n action.char,\n Date.now(),\n opts,\n );\n typeAhead = result.newState;\n if (result.matchedIndex !== null) {\n if (!isOpen) open();\n setActive(result.matchedIndex);\n }\n return;\n }\n case \"passthrough\":\n return;\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n }\n }\n }\n\n function onKeydown(event: KeyboardEvent) {\n const opts = readOptions(select);\n const action = handleKey(\n {\n key: event.key,\n ctrlKey: event.ctrlKey,\n metaKey: event.metaKey,\n altKey: event.altKey,\n shiftKey: event.shiftKey,\n },\n {\n isOpen,\n activeIndex,\n selectedIndex: select.selectedIndex,\n options: opts,\n },\n );\n if (action.preventDefault) event.preventDefault();\n applyAction(action);\n }\n\n function onTriggerClick(event: MouseEvent) {\n event.preventDefault();\n if (isOpen) close(false);\n else open();\n }\n\n function onListboxClick(event: MouseEvent) {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList?.contains(\"lb-dropdown-option\")) {\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n if (!Number.isNaN(idx)) {\n commit(idx);\n return;\n }\n }\n target = target.parentElement;\n }\n }\n\n function onListboxMousemove(event: MouseEvent) {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList?.contains(\"lb-dropdown-option\")) {\n if (target.getAttribute(\"aria-disabled\") === \"true\") return;\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n if (!Number.isNaN(idx) && idx !== activeIndex) setActive(idx);\n return;\n }\n target = target.parentElement;\n }\n }\n\n function onShellFocusout() {\n // In shadow DOM, document.activeElement returns the shadow host;\n // rootNode.activeElement returns the actual focused element inside\n // the shadow tree. Falls back to document.activeElement in the\n // light-DOM (non-shadow) case.\n setTimeout(() => {\n if (!isOpen) return;\n const active = rootNode.activeElement ?? doc.activeElement;\n if (!shell.contains(active)) close(false);\n }, 0);\n }\n\n function onSelectChange() {\n syncFromSelect();\n }\n\n const observer = new MutationObserver(() => {\n syncFromSelect();\n });\n observer.observe(select, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"disabled\", \"value\", \"selected\"],\n });\n\n // Prevent mousedown on a non-focusable <li> from blurring the trigger.\n // Without this, focusout fires on the shell and queues a setTimeout(0)\n // that closes the dropdown — and on desktop the close runs before the\n // synthesized click event, so onListboxClick never sees the option and\n // commit never runs. Mobile is unaffected because touchstart doesn't\n // shift focus.\n const onListboxMousedown = (event: MouseEvent) => event.preventDefault();\n\n trigger.addEventListener(\"click\", onTriggerClick);\n trigger.addEventListener(\"keydown\", onKeydown);\n shell.addEventListener(\"focusout\", onShellFocusout);\n listbox.addEventListener(\"mousedown\", onListboxMousedown);\n listbox.addEventListener(\"click\", onListboxClick);\n listbox.addEventListener(\"mousemove\", onListboxMousemove);\n select.addEventListener(\"change\", onSelectChange);\n\n function destroy() {\n if (isOpen) close(false);\n observer.disconnect();\n trigger.removeEventListener(\"click\", onTriggerClick);\n trigger.removeEventListener(\"keydown\", onKeydown);\n shell.removeEventListener(\"focusout\", onShellFocusout);\n listbox.removeEventListener(\"mousedown\", onListboxMousedown);\n listbox.removeEventListener(\"click\", onListboxClick);\n listbox.removeEventListener(\"mousemove\", onListboxMousemove);\n select.removeEventListener(\"change\", onSelectChange);\n if (shell.parentNode) shell.parentNode.removeChild(shell);\n if (listbox.parentNode) listbox.parentNode.removeChild(listbox);\n select.classList.remove(\"lb-dropdown-state\");\n select.removeAttribute(\"aria-hidden\");\n select.removeAttribute(\"tabindex\");\n delete slot.__lbDropdownInstance;\n }\n\n instance = {\n shell,\n listbox,\n select,\n close: () => close(false),\n destroy,\n };\n slot.__lbDropdownInstance = instance;\n\n syncFromSelect();\n return instance;\n}\n\nexport function bindAllDropdowns(root: ParentNode): DropdownInstance[] {\n const selects = root.querySelectorAll(BIND_SELECTOR);\n const instances: DropdownInstance[] = [];\n selects.forEach((sel) => {\n const inst = bindDropdown(sel as HTMLSelectElement);\n if (inst) instances.push(inst);\n });\n return instances;\n}\n\nexport function unbindAllDropdowns(root: ParentNode): void {\n const bound = root.querySelectorAll(\"select.lb-dropdown-state\");\n bound.forEach((sel) => {\n const inst = (sel as SelectWithInstance).__lbDropdownInstance;\n if (inst) inst.destroy();\n });\n}\n","// Matches the DOM/class structure of the Liquid `bundle-widget.liquid`'s\n// `.lb-bundle-countdown` bar so the shared CSS themes both renderers.\nimport { formatCountdown } from \"@lime-bundles/core\";\n\nexport interface CountdownHandle {\n /** DOM element to append to the widget. */\n el: HTMLElement;\n /** Call on widget disconnect to clear the tick interval. */\n stop: () => void;\n}\n\nexport function renderCountdown(endsAtIso: string): CountdownHandle | null {\n const parsed = parseIso(endsAtIso);\n if (parsed === null) return null;\n // Already expired — caller doesn't append anything.\n if (parsed <= Date.now()) return null;\n const target: number = parsed;\n\n const wrap = document.createElement(\"div\");\n wrap.className = \"lb-bundle-countdown\";\n wrap.setAttribute(\"data-countdown\", \"\");\n\n const labelWrap = document.createElement(\"div\");\n labelWrap.className = \"lb-bundle-countdown__label\";\n const labelText = document.createElement(\"span\");\n labelText.textContent = \"Ends in\";\n labelWrap.appendChild(labelText);\n wrap.appendChild(labelWrap);\n\n const timer = document.createElement(\"span\");\n timer.className = \"lb-bundle-countdown__timer\";\n timer.setAttribute(\"data-countdown-timer\", \"\");\n wrap.appendChild(timer);\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n\n function tick() {\n const msLeft = target - Date.now();\n if (msLeft <= 0) {\n wrap.style.display = \"none\";\n stop();\n return;\n }\n timer.textContent = formatCountdown(msLeft);\n }\n\n function stop() {\n if (intervalId !== null) {\n clearInterval(intervalId);\n intervalId = null;\n }\n }\n\n tick();\n intervalId = setInterval(tick, 1000);\n\n return { el: wrap, stop };\n}\n\nfunction parseIso(iso: string): number | null {\n const t = Date.parse(iso);\n return Number.isFinite(t) ? t : null;\n}\n","/**\n * Builds the shared `.lb-bundle-cta` button structure used by all three\n * widget renderers (fixed, volume, mix_match).\n *\n * The button has two children in a 1×1 CSS grid (see\n * packages/widget/src/styles/bundle-css.ts):\n *\n * <button class=\"lb-bundle-cta\" data-add-bundle>\n * <span class=\"lb-cta-label\" data-cta-label>{label}</span>\n * <span class=\"lb-cta-spinner\" data-cta-spinner aria-hidden=\"true\">…</span>\n * </button>\n *\n * The spinner is visible only when the button has `data-loading=\"true\"`.\n * The SDK itself doesn't toggle that attribute — merchants opt into the\n * loading-state affordance by setting it during their async cart mutation:\n *\n * el.querySelector('[data-add-bundle]').setAttribute('data-loading', 'true');\n * try { await cart.linesAdd(…); }\n * finally { el.querySelector('[data-add-bundle]').removeAttribute('data-loading'); }\n *\n * Keeping the SDK neutral on timing (BYO-cart contract) means no additive\n * Promise API surface needs to change.\n */\nexport function buildCtaButton(label: string): HTMLButtonElement {\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.className = \"lb-bundle-cta\";\n button.setAttribute(\"data-add-bundle\", \"\");\n\n const labelSpan = document.createElement(\"span\");\n labelSpan.className = \"lb-cta-label\";\n labelSpan.setAttribute(\"data-cta-label\", \"\");\n labelSpan.textContent = label;\n button.appendChild(labelSpan);\n\n const spinnerSpan = document.createElement(\"span\");\n spinnerSpan.className = \"lb-cta-spinner\";\n spinnerSpan.setAttribute(\"data-cta-spinner\", \"\");\n spinnerSpan.setAttribute(\"aria-hidden\", \"true\");\n // Same markup as snippets/lb-cta-spinner.liquid — keep in sync.\n spinnerSpan.innerHTML =\n '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" ' +\n 'stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\">' +\n '<path d=\"M12 2a10 10 0 0 1 10 10\" /></svg>';\n button.appendChild(spinnerSpan);\n\n return button;\n}\n\n/**\n * Updates the visible label text on a button built by `buildCtaButton`.\n * Targets the `[data-cta-label]` span so the sibling spinner isn't\n * clobbered — a plain `button.textContent = \"...\"` would destroy every\n * child, including the spinner span.\n *\n * If `[data-cta-label]` isn't found, this function is a no-op rather than\n * falling back to `button.textContent`. Buttons that omit the label span\n * either (a) also omit the spinner (nothing to destroy — but also nothing\n * the caller needs to worry about; returning early is fine) or (b) were\n * mutated in-flight by an adapter that should have kept the label. Either\n * way, overwriting `button.textContent` is strictly harmful: it either\n * silently nukes a spinner we're trying to preserve, or replaces whatever\n * structure the adapter built. Callers needing a plain-button text update\n * should write `button.textContent = ...` themselves.\n */\nexport function setCtaLabel(button: HTMLButtonElement, text: string): void {\n const label = button.querySelector<HTMLElement>(\"[data-cta-label]\");\n if (label) {\n label.textContent = text;\n }\n}\n","/**\n * Shared DOM helper for the widget renderers. Keep this internal to\n * `packages/widget/src/renderers/`; it intentionally isn't re-exported\n * from the package's public entrypoint.\n */\nexport function el(\n tag: string,\n className: string,\n attrs: Record<string, string> = {},\n): HTMLElement {\n const node = document.createElement(tag);\n if (className) node.className = className;\n for (const [k, v] of Object.entries(attrs)) {\n node.setAttribute(k, v);\n }\n return node;\n}\n","/**\n * DOM renderer for fixed bundles.\n *\n * Emits the class names and structure of `lb-fixed.liquid` so the ported\n * `bundle-fixed.css` themes it. Features:\n * - Variant dropdown per product (when the product has >1 available\n * variants, filtered by merchant's `selected_variant_ids`). Changing\n * a variant live-updates its row price, the bundle total, the\n * header save badge, and the savings bar.\n * - Merchant `productQuantities` + `variantQuantities` honoured.\n * - Out-of-stock behaviour (`hide` / `show_greyed_out`) applied per\n * product.\n * - Bundle-level guard: fixed bundles are all-or-nothing, so the\n * entire widget hides if any product has no available variants.\n *\n * Pricing matches Shopify's per-unit floor rounding via the `pricing`\n * helper, so what the widget shows equals what the customer pays at\n * checkout.\n */\nimport {\n resolveBundleQty,\n isVariantFulfillable,\n shouldShowLowStockBadge,\n type CartLineInput,\n type FixedBundleData,\n type Product,\n type ProductVariant,\n} from \"@lime-bundles/core\";\nimport { bindAllDropdowns, unbindAllDropdowns } from \"../dropdown/bind-dropdown\";\nimport {\n computeFixedPricing,\n formatCents,\n formatUnitPrice,\n parseCents,\n} from \"./pricing\";\nimport { renderCountdown } from \"./countdown\";\nimport { buildCtaButton } from \"./cta-button\";\nimport { el } from \"./dom\";\nimport { THUMB_PX, transformImageUrl } from \"./image\";\n\nconst PLACEHOLDER_THUMB_SVG = `\n<svg class=\"lb-bundle-placeholder-icon\" viewBox=\"0 0 28 28\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" focusable=\"false\">\n <rect x=\"4\" y=\"4\" width=\"20\" height=\"20\" rx=\"3\"></rect>\n <line x1=\"4\" y1=\"20\" x2=\"24\" y2=\"20\"></line>\n <circle cx=\"10\" cy=\"12\" r=\"2\"></circle>\n</svg>`;\n\ninterface ProductRowState {\n product: Product;\n /** Variants eligible for this row (intersection of available + merchant selection). */\n eligibleVariants: ProductVariant[];\n /** Currently selected variant; null when none are available (OOS). */\n selected: ProductVariant | null;\n /** Quantity applied to this row (merchant product/variant qty or 1). */\n qty: number;\n /** Whether the product has zero available variants. */\n isOos: boolean;\n}\n\nexport function renderFixedBundle(\n container: HTMLElement,\n bundle: FixedBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n) {\n const wc = bundle.widgetConfig;\n\n // Per-variant quantity lookup — delegated to the canonical resolver in\n // `@lime-bundles/core` so the fallback chain stays in lockstep with the\n // React SDK and the server-side discount metafield producer.\n const qtyFor = (productId: string, variantId: string): number =>\n resolveBundleQty(bundle, productId, variantId);\n\n // Build row state honouring merchant selections + OOS behaviour.\n const rows: ProductRowState[] = [];\n let oosCount = 0;\n bundle.products.forEach((product, idx) => {\n const row = buildRowState(bundle, product, idx);\n // Merchant explicitly set productQuantities/variantQuantities to 0 —\n // skip the row entirely. Treated as opt-out, not as a zero-quantity\n // line in pricing.\n if (row.qty === 0) return;\n if (row.isOos) {\n oosCount++;\n if (wc.outOfStockBehavior === \"hide\") return; // skip the row entirely\n }\n rows.push(row);\n });\n\n // Bundle-level guard: fixed bundles are all-or-nothing. Even in\n // \"show_greyed_out\" mode, if any product has no stock, disable the CTA\n // and render a warning. In \"hide\" mode, if we lost any rows, bail out\n // entirely — matches Liquid behaviour.\n if (rows.length === 0) return;\n\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n const root = el(\"div\", \"lb-fixed\", {\n \"data-discount-type\": bundle.discountConfig.discountType,\n \"data-discount-value\": String(bundle.discountConfig.discountValue),\n });\n\n // --- Header (title + subtitle + save badge) ---\n const headerHandle = renderHeader(bundle, currency);\n root.appendChild(headerHandle.el);\n\n // --- Countdown ---\n if (wc.countdown.showCountdown && bundle.endsAt) {\n const countdown = renderCountdown(bundle.endsAt);\n if (countdown) {\n root.appendChild(countdown.el);\n onCleanup?.(countdown.stop);\n }\n }\n\n // --- Product list ---\n const list = el(\"div\", \"lb-fixed__products\");\n const rowHandles: Array<ReturnType<typeof renderProductRow>> = [];\n rows.forEach((rowState) => {\n const handle = renderProductRow(\n rowState,\n currency,\n qtyFor,\n {\n enabled: wc.showLowStockBadge,\n threshold: wc.lowStockThreshold,\n },\n () => {\n // Variant change → recompute pricing.\n updatePricing();\n },\n );\n rowHandles.push(handle);\n list.appendChild(handle.el);\n });\n root.appendChild(list);\n\n root.appendChild(el(\"div\", \"lb-bundle-divider\"));\n\n // --- Pricing row + savings bar ---\n const pricingHandle = renderPricingRow(bundle);\n root.appendChild(pricingHandle.el);\n const savingsBarHandle = wc.savingsBar.visible ? renderSavingsBar() : null;\n if (savingsBarHandle) root.appendChild(savingsBarHandle.el);\n\n // --- CTA ---\n const cta = renderCta(bundle, oosCount, () => {\n const lines: CartLineInput[] = rows\n .filter((r) => r.selected)\n .map((r) => ({\n merchandiseId: r.selected!.id,\n quantity: r.qty,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n }));\n if (lines.length === 0) return;\n onAddToCart(lines);\n });\n root.appendChild(cta);\n\n root.appendChild(\n el(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n container.appendChild(root);\n\n // Replace native <select> popup chrome with our accessible custom dropdown.\n // Native <select> stays in DOM as state holder; the change handler attached\n // above continues to fire on commit.\n bindAllDropdowns(root);\n onCleanup?.(() => unbindAllDropdowns(root));\n\n updatePricing();\n\n function updatePricing() {\n const totalCents = rows.reduce((sum, r) => {\n if (!r.selected) return sum;\n const unit = parseCents(r.selected.price.amount);\n return sum + unit * r.qty;\n }, 0);\n const saleCents = computeSale(totalCents, bundle.discountConfig, rows);\n const savingsCents = Math.max(0, totalCents - saleCents);\n\n pricingHandle.update({ totalCents, saleCents, savingsCents, currency });\n if (savingsBarHandle) {\n savingsBarHandle.update({ savingsCents, currency });\n }\n headerHandle.refresh(\n deriveHeaderBadge(bundle, totalCents, saleCents, currency),\n );\n }\n}\n\n// --- State ---\n\nfunction buildRowState(\n bundle: FixedBundleData,\n product: Product,\n productIndex: number,\n): ProductRowState {\n const selectedVariantIds =\n bundle.selectedVariantIds?.[productIndex] ?? null;\n\n // All variants the merchant scoped into the bundle. Sold-out variants are\n // INCLUDED here so the per-option dropdowns render them as disabled\n // options (same UX as unavailable combinations) rather than hiding them\n // from the picker. The initial `selected` still prefers an in-stock\n // variant so the default shown is purchasable.\n const merchantScoped =\n selectedVariantIds && selectedVariantIds.length > 0\n ? product.variants.nodes.filter((v) => selectedVariantIds.includes(v.id))\n : product.variants.nodes;\n\n // \"In stock\" here means fulfillable for THIS variant's required quantity —\n // a variant with 3 units and required qty 5 is unfulfillable, even though\n // `availableForSale` is true. See packages/core/src/inventory/predicate.ts.\n const firstInStock =\n merchantScoped.find((v) =>\n isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id)),\n ) ?? null;\n const eligibleVariants = merchantScoped;\n const isOos = !firstInStock;\n const selected = firstInStock ?? merchantScoped[0] ?? null;\n const qty = selected ? resolveBundleQty(bundle, product.id, selected.id) : 1;\n\n return { product, eligibleVariants, selected, qty, isOos };\n}\n\n// --- Section renderers ---\n\ninterface HeaderHandle {\n el: HTMLElement;\n /** Update the save-badge text when pricing changes. */\n refresh: (badgeText: string) => void;\n}\n\nfunction renderHeader(\n bundle: FixedBundleData,\n currency: string,\n): HeaderHandle {\n const wc = bundle.widgetConfig;\n const header = el(\"div\", \"lb-bundle-header\");\n const content = el(\"div\", \"lb-bundle-header__content\");\n\n const title = el(\"h3\", \"lb-bundle-title\");\n title.textContent = bundle.title;\n content.appendChild(title);\n\n if (bundle.description) {\n const subtitle = el(\"p\", \"lb-bundle-subtitle\");\n subtitle.textContent = bundle.description;\n content.appendChild(subtitle);\n }\n header.appendChild(content);\n\n const badgeEl = el(\"span\", \"lb-bundle-header__badge\", {\n \"data-header-badge\": \"\",\n });\n header.appendChild(badgeEl);\n\n // Placeholder initial badge — updated by refresh() from updatePricing().\n const initialPricing = computeFixedPricing(\n bundle,\n bundle.productQuantities,\n wc.pricing.showSaveBadge,\n );\n if (initialPricing.headerBadge) {\n badgeEl.textContent = initialPricing.headerBadge;\n } else {\n badgeEl.style.display = \"none\";\n }\n void currency;\n\n return {\n el: header,\n refresh(badgeText) {\n if (!wc.pricing.showSaveBadge) {\n badgeEl.style.display = \"none\";\n return;\n }\n if (badgeText) {\n badgeEl.textContent = badgeText;\n badgeEl.style.display = \"\";\n } else {\n badgeEl.style.display = \"none\";\n }\n },\n };\n}\n\nfunction deriveHeaderBadge(\n bundle: FixedBundleData,\n totalCents: number,\n saleCents: number,\n currency: string,\n): string {\n if (!bundle.widgetConfig.pricing.showSaveBadge) return \"\";\n const savings = totalCents - saleCents;\n if (savings <= 0) return \"\";\n const dc = bundle.discountConfig;\n if (dc.discountType === \"percentage\" && dc.discountValue > 0) {\n return `-${Math.round(dc.discountValue)}%`;\n }\n if (dc.discountType === \"fixed_amount\" && dc.discountValue > 0) {\n return `-${formatCents(Math.round(dc.discountValue * 100), currency)}`;\n }\n return `-${formatCents(savings, currency)}`;\n}\n\ninterface ProductRowHandle {\n el: HTMLElement;\n state: ProductRowState;\n}\n\nfunction renderProductRow(\n state: ProductRowState,\n currency: string,\n qtyFor: (productId: string, variantId: string) => number,\n lowStock: { enabled: boolean; threshold: number },\n onVariantChange: () => void,\n): ProductRowHandle {\n const rowEl = el(\n \"div\",\n state.isOos\n ? \"lb-bundle-product-row lb-bundle-product-row--oos\"\n : \"lb-bundle-product-row\",\n {\n \"data-product-id\": state.product.id.replace(/^.*\\//, \"\"),\n ...(state.isOos ? { \"aria-disabled\": \"true\" } : {}),\n },\n );\n\n // Prefer the selected variant's image so the thumb tracks colour-swatch\n // selections; falls back to product hero, then placeholder.\n const thumb = el(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n const initialThumbImage =\n state.selected?.image ?? state.product.featuredImage ?? null;\n let thumbImg: HTMLImageElement | null = null;\n if (initialThumbImage) {\n thumbImg = document.createElement(\"img\");\n thumbImg.src = transformImageUrl(initialThumbImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = initialThumbImage.altText ?? state.product.title;\n thumbImg.width = THUMB_PX;\n thumbImg.height = THUMB_PX;\n thumbImg.loading = \"lazy\";\n thumb.appendChild(thumbImg);\n } else {\n thumb.insertAdjacentHTML(\"beforeend\", PLACEHOLDER_THUMB_SVG);\n }\n let qtyBadgeRef: HTMLElement | null = null;\n if (!state.isOos) {\n qtyBadgeRef = el(\"span\", \"lb-bundle-qty-badge\", {\n \"data-qty-badge\": \"\",\n });\n qtyBadgeRef.textContent = String(state.qty);\n thumb.appendChild(qtyBadgeRef);\n }\n rowEl.appendChild(thumb);\n\n // Info column\n const info = el(\"div\", \"lb-bundle-product-info\");\n const name = document.createElement(\"a\");\n name.className = \"lb-bundle-product-name\";\n name.href = `/products/${state.product.handle}`;\n name.textContent = state.product.title;\n info.appendChild(name);\n\n if (state.isOos) {\n const oosLabel = el(\"span\", \"lb-bundle-oos-label\");\n oosLabel.textContent = \"Out of stock\";\n info.appendChild(oosLabel);\n } else if (state.selected) {\n // Read-only variant text right under the title — same treatment as\n // the mix-match widget's filled-slot variant. Only renders when\n // the merchant pinned a single variant of a multi-variant product\n // (no picker would render in that case). Suppressed for products\n // with the default single variant.\n const isSinglePinnedVariant =\n state.eligibleVariants.length === 1 &&\n state.product.variants.nodes.length > 1;\n if (isSinglePinnedVariant) {\n const badge = el(\"span\", \"lb-bundle-variant-badge\");\n badge.textContent = state.eligibleVariants[0].title;\n info.appendChild(badge);\n }\n\n // Price row — updated on variant change.\n const prices = el(\"span\", \"lb-bundle-product-prices\");\n const compare = el(\"span\", \"lb-bundle-product-compare-price\", {\n \"data-product-compare-price\": \"\",\n });\n const priceEl = el(\"span\", \"lb-bundle-product-price\", {\n \"data-product-price\": \"\",\n });\n prices.appendChild(compare);\n prices.appendChild(priceEl);\n info.appendChild(prices);\n\n // Unit price (e.g. \"$0.50/100ml\") — sibling of the price row so it sits\n // on its own line beneath the price. Hidden when the merchant hasn't\n // set unit pricing in the Shopify admin.\n const unitPriceEl = el(\"span\", \"lb-bundle-product-unit-price\", {\n \"data-product-unit-price\": \"\",\n });\n unitPriceEl.setAttribute(\"hidden\", \"\");\n info.appendChild(unitPriceEl);\n\n // Low-stock badge — updated alongside price on variant change. Hidden\n // when the threshold isn't met or quantityAvailable is unknown.\n const lowStockEl = el(\"span\", \"lb-bundle-low-stock-badge\", {\n \"data-low-stock-badge\": \"\",\n });\n lowStockEl.setAttribute(\"hidden\", \"\");\n info.appendChild(lowStockEl);\n\n const applyVariantToRow = (variant: ProductVariant) => {\n const unit = parseCents(variant.price.amount);\n priceEl.textContent = formatCents(unit, currency);\n if (variant.compareAtPrice) {\n const cmp = parseCents(variant.compareAtPrice.amount);\n if (cmp > unit) {\n compare.textContent = formatCents(cmp, currency);\n compare.removeAttribute(\"hidden\");\n } else {\n compare.setAttribute(\"hidden\", \"\");\n }\n } else {\n compare.setAttribute(\"hidden\", \"\");\n }\n const unitText = formatUnitPrice(\n variant.unitPrice,\n variant.unitPriceMeasurement,\n currency,\n );\n if (unitText) {\n unitPriceEl.textContent = unitText;\n unitPriceEl.removeAttribute(\"hidden\");\n } else {\n unitPriceEl.setAttribute(\"hidden\", \"\");\n }\n const nextImage = variant.image ?? state.product.featuredImage;\n if (thumbImg && nextImage) {\n thumbImg.src = transformImageUrl(nextImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = nextImage.altText ?? state.product.title;\n }\n\n const required = qtyFor(state.product.id, variant.id);\n if (\n shouldShowLowStockBadge(\n variant,\n required,\n lowStock.threshold,\n lowStock.enabled,\n )\n ) {\n lowStockEl.textContent = `Only ${variant.quantityAvailable} left`;\n lowStockEl.removeAttribute(\"hidden\");\n } else {\n lowStockEl.setAttribute(\"hidden\", \"\");\n }\n };\n\n applyVariantToRow(state.selected);\n\n // Per-option dropdowns when more than one eligible variant exists —\n // Shopify's recommended approach via product.options_with_values (here\n // derived from variants[].selectedOptions since the Storefront API gives\n // us that). Values that don't combine with the current selection of other\n // options are disabled (Dawn-style availability) so the customer sees\n // what's possible instead of the variant silently jumping combos.\n if (state.eligibleVariants.length > 1) {\n const optionNames: string[] = state.eligibleVariants[0].selectedOptions.map(\n (o) => o.name,\n );\n const productIdTail = state.product.id.replace(/^.*\\//, \"\");\n const optionSelects: HTMLSelectElement[] = [];\n\n const resolveVariant = (values: string[]) =>\n state.eligibleVariants.find(\n (v) =>\n v.selectedOptions.every((o, i) => o.value === values[i]) &&\n v.selectedOptions.length === values.length,\n ) ?? null;\n\n const syncSelectsToVariant = (variant: ProductVariant) => {\n variant.selectedOptions.forEach((o, i) => {\n const sel = optionSelects[i];\n if (sel && sel.value !== o.value) sel.value = o.value;\n });\n };\n\n const isValueAvailable = (\n optionIndex: number,\n value: string,\n selected: string[],\n ): boolean =>\n state.eligibleVariants.some((v) => {\n if (!isVariantFulfillable(v, qtyFor(state.product.id, v.id))) return false;\n if (v.selectedOptions[optionIndex]?.value !== value) return false;\n return v.selectedOptions.every(\n (o, i) => i === optionIndex || o.value === selected[i],\n );\n });\n\n const recomputeDisabled = (selected: string[]) => {\n optionSelects.forEach((sel, i) => {\n Array.from(sel.options).forEach((opt) => {\n opt.disabled = !isValueAvailable(i, opt.value, selected);\n });\n });\n };\n\n const handleChange = () => {\n const values = optionSelects.map((s) => s.value);\n const variant = resolveVariant(values);\n if (!variant) {\n // Disabled combo reached (keyboard nav edge case) — revert selects\n // to the previously selected variant rather than jumping.\n if (state.selected) {\n syncSelectsToVariant(state.selected);\n recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));\n }\n return;\n }\n state.selected = variant;\n state.qty = qtyFor(state.product.id, variant.id);\n if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);\n applyVariantToRow(variant);\n recomputeDisabled(variant.selectedOptions.map((o) => o.value));\n onVariantChange();\n };\n\n const groupsContainer = el(\"div\", \"lb-bundle-variant-option-groups\");\n optionNames.forEach((name, position) => {\n const group = el(\"div\", \"lb-bundle-variant-option-group\");\n\n const label = el(\"span\", \"lb-bundle-variant-option-label\");\n label.textContent = name;\n group.appendChild(label);\n\n const select = document.createElement(\"select\");\n select.className = \"lb-bundle-variant-select\";\n select.setAttribute(\"data-variant-option\", \"\");\n select.setAttribute(\"data-option-position\", String(position + 1));\n select.name = `lb-variant-${productIdTail}-${position + 1}`;\n select.setAttribute(\"aria-label\", name);\n\n const seen = new Set<string>();\n state.eligibleVariants.forEach((v) => {\n const value = v.selectedOptions[position]?.value;\n if (!value || seen.has(value)) return;\n seen.add(value);\n const opt = document.createElement(\"option\");\n opt.value = value;\n opt.textContent = value;\n if (state.selected?.selectedOptions[position]?.value === value) {\n opt.selected = true;\n }\n select.appendChild(opt);\n });\n\n select.addEventListener(\"change\", handleChange);\n optionSelects.push(select);\n group.appendChild(select);\n groupsContainer.appendChild(group);\n });\n info.appendChild(groupsContainer);\n\n if (state.selected) {\n recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));\n }\n }\n // Single-variant case is handled above the price row, immediately\n // under the product title (same pattern as the mix-match widget).\n }\n\n rowEl.appendChild(info);\n return { el: rowEl, state };\n}\n\ninterface PricingHandle {\n el: HTMLElement;\n update: (p: {\n totalCents: number;\n saleCents: number;\n savingsCents: number;\n currency: string;\n }) => void;\n}\n\nfunction renderPricingRow(bundle: FixedBundleData): PricingHandle {\n const row = el(\"div\", \"lb-bundle-pricing\");\n const label = el(\"span\", \"lb-bundle-pricing__label\");\n label.textContent = \"Bundle price\";\n row.appendChild(label);\n\n const prices = el(\"span\", \"lb-bundle-pricing__prices\");\n const compare = el(\"span\", \"lb-bundle-compare-price\", {\n \"data-compare-price\": \"\",\n });\n compare.style.display = \"none\";\n prices.appendChild(compare);\n const sale = el(\"span\", \"lb-bundle-sale-price\", {\n \"data-sale-price\": \"\",\n });\n prices.appendChild(sale);\n row.appendChild(prices);\n\n return {\n el: row,\n update({ totalCents, saleCents, savingsCents, currency }) {\n sale.textContent = formatCents(saleCents, currency);\n if (bundle.widgetConfig.pricing.showCompareAtPrice && savingsCents > 0) {\n compare.textContent = formatCents(totalCents, currency);\n compare.style.display = \"\";\n } else {\n compare.style.display = \"none\";\n }\n },\n };\n}\n\ninterface SavingsBarHandle {\n el: HTMLElement;\n update: (p: { savingsCents: number; currency: string }) => void;\n}\n\nfunction renderSavingsBar(): SavingsBarHandle {\n const bar = el(\"div\", \"lb-bundle-savings-bar\", {\n \"data-savings-bar\": \"\",\n });\n const label = document.createElement(\"span\");\n label.textContent = \"You save\";\n bar.appendChild(label);\n const amount = el(\"span\", \"\", { \"data-savings-amount\": \"\" });\n bar.appendChild(amount);\n return {\n el: bar,\n update({ savingsCents, currency }) {\n if (savingsCents <= 0) {\n bar.style.display = \"none\";\n return;\n }\n bar.style.display = \"\";\n amount.textContent = formatCents(savingsCents, currency);\n },\n };\n}\n\nfunction renderCta(\n bundle: FixedBundleData,\n oosCount: number,\n onClick: () => void,\n): HTMLElement {\n const label =\n oosCount > 0\n ? `${oosCount} item${oosCount === 1 ? \"\" : \"s\"} out of stock`\n : bundle.widgetConfig.cta.ctaText || \"Add to cart\";\n const button = buildCtaButton(label);\n if (oosCount > 0) {\n button.disabled = true;\n } else {\n button.addEventListener(\"click\", () => {\n if (button.disabled) return;\n onClick();\n });\n }\n return button;\n}\n\n// --- Pricing helpers ---\n\nfunction computeSale(\n totalCents: number,\n discount: FixedBundleData[\"discountConfig\"],\n rows: ProductRowState[],\n): number {\n if (discount.discountType === \"percentage\") {\n // Per-unit floor rounding — matches Shopify Discount Function.\n let saleCents = 0;\n for (const r of rows) {\n if (!r.selected) continue;\n const unit = parseCents(r.selected.price.amount);\n const off = Math.floor((unit * discount.discountValue) / 100);\n const perUnit = Math.max(0, unit - off);\n saleCents += perUnit * r.qty;\n }\n return saleCents;\n }\n // fixed_amount: total minus absolute discount (clamped >= 0).\n return Math.max(0, totalCents - Math.round(discount.discountValue * 100));\n}\n\n","/**\n * DOM renderer for mix-and-match bundles.\n *\n * Mirrors `extensions/bundle-theme/snippets/lb-mix-match.liquid` and its\n * associated picker-modal JS. Merchant flow:\n *\n * 1. Widget renders `bundle.minQuantity` empty slots (one per distinct\n * product the shopper must pick).\n * 2. Clicking a slot opens the picker modal with the eligible products.\n * 3. Inside the modal, each product card shows a variant select, a qty\n * stepper (when `widgetConfig.mixMatchShowQuantitySelector !== false`),\n * and an Add button. The stepper is bounded by the merchant's\n * `productRules[productId]` (`{ min, max }`) and the variant's stock\n * via `maxAddableQuantity`.\n * 4. Adding a pick fills a slot with the chosen variant and quantity.\n * Remove-x on a filled slot empties it again.\n * 5. CTA unlocks when distinct picks ≥ `bundle.minQuantity`. On click,\n * selections are aggregated by `(productId, variantId)` into one cart\n * line per variant with summed quantity.\n *\n * Class names match the Liquid template one-for-one so the ported\n * bundle-mix-match.css styles this DOM without changes.\n */\nimport type {\n CartLineInput,\n MixMatchBundleData,\n Product,\n ProductRule,\n ProductVariant,\n} from \"@lime-bundles/core\";\nimport {\n DEFAULT_PRODUCT_RULE,\n isVariantFulfillable,\n maxAddableQuantity,\n shouldShowLowStockBadge,\n} from \"@lime-bundles/core\";\nimport {\n computeBundleSaleCents,\n formatCents,\n formatUnitPrice,\n parseCents,\n} from \"./pricing\";\nimport { bindAllDropdowns, unbindAllDropdowns } from \"../dropdown/bind-dropdown\";\nimport { renderCountdown } from \"./countdown\";\nimport { buildCtaButton, setCtaLabel } from \"./cta-button\";\nimport { el } from \"./dom\";\nimport { THUMB_PX, transformImageUrl } from \"./image\";\n\ninterface EligibleProduct {\n product: Product;\n variants: ProductVariant[];\n firstAvailableVariant: ProductVariant | null;\n isOos: boolean;\n}\n\n/**\n * One pick by the shopper. A pick is a unique (productId, variantId) row\n * in the slot list with its own merchant-bounded quantity. Two picks of\n * the same variant are not allowed — the shopper bumps the stepper instead\n * (the slot's row is replaced when re-added). The CartLineInput aggregator\n * still defends against duplicate (productId, variantId) entries by summing.\n */\ninterface Selection {\n productId: string;\n productTitle: string;\n variantId: string;\n variantTitle: string;\n imageUrl: string | null;\n priceCents: number;\n compareCents: number | null;\n /** Pre-formatted unit price (\"$0.50/100ml\") for the filled-slot view. */\n unitPriceLabel: string | null;\n /** Shopper-chosen qty, clamped to `[rule.min, maxAddableQuantity(...)]`. */\n quantity: number;\n}\n\nconst PLACEHOLDER_THUMB_SVG = `\n<svg class=\"lb-bundle-placeholder-icon\" viewBox=\"0 0 28 28\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" focusable=\"false\">\n <rect x=\"4\" y=\"4\" width=\"20\" height=\"20\" rx=\"3\"></rect>\n <line x1=\"4\" y1=\"20\" x2=\"24\" y2=\"20\"></line>\n <circle cx=\"10\" cy=\"12\" r=\"2\"></circle>\n</svg>`;\n\nconst PLUS_ICON_SVG = `\n<svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"9\" y1=\"3\" x2=\"9\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <line x1=\"3\" y1=\"9\" x2=\"15\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\nconst CLOSE_ICON_SVG = `\n<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"5\" y1=\"5\" x2=\"15\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <line x1=\"15\" y1=\"5\" x2=\"5\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\nconst SEARCH_CLEAR_ICON_SVG = `\n<svg width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z\"/>\n</svg>`;\n\nconst STEPPER_MINUS_SVG = `\n<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"3\" y1=\"7\" x2=\"11\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\nconst STEPPER_PLUS_SVG = `\n<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"7\" y1=\"3\" x2=\"7\" y2=\"11\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <line x1=\"3\" y1=\"7\" x2=\"11\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\n/** Resolve the merchant rule for a product, applying the runtime default. */\nfunction ruleFor(bundle: MixMatchBundleData, productId: string): ProductRule {\n return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;\n}\n\n/** Sum of quantities the shopper has already allocated to this variant. */\nfunction alreadyInBundleFor(\n selections: Selection[],\n productId: string,\n variantId: string,\n): number {\n let sum = 0;\n for (const s of selections) {\n if (s.productId === productId && s.variantId === variantId) sum += s.quantity;\n }\n return sum;\n}\n\nexport function renderMixMatchBundle(\n container: HTMLElement,\n bundle: MixMatchBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n) {\n const wc = bundle.widgetConfig;\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n const requiredQty = bundle.minQuantity ?? 1;\n const showQtySelector = wc.mixMatchShowQuantitySelector !== false;\n\n // Build eligible-products list, honoring outOfStockBehavior. A product is\n // eligible when at least one of its variants can satisfy `rule.min` units.\n const eligible = buildEligibleProducts(bundle, wc.outOfStockBehavior);\n const inStockCount = eligible.filter((e) => !e.isOos).length;\n\n // Bundle visibility guard: if we can't possibly satisfy requiredQty from\n // distinct in-stock products, don't render the widget at all. Matches\n // Liquid.\n if (inStockCount < requiredQty) return;\n\n const selections: Selection[] = [];\n const root = el(\"div\", \"lb-mix-match\", {\n \"data-required-quantity\": String(requiredQty),\n });\n\n // --- Header ---\n const header = renderHeader(bundle);\n root.appendChild(header);\n\n // --- Countdown ---\n if (wc.countdown.showCountdown && bundle.endsAt) {\n const countdown = renderCountdown(bundle.endsAt);\n if (countdown) {\n root.appendChild(countdown.el);\n onCleanup?.(countdown.stop);\n }\n }\n\n // --- Progress bar ---\n const progress = renderProgress(requiredQty);\n root.appendChild(progress.el);\n\n // --- Slots ---\n const slotsContainer = el(\"div\", \"lb-mix-match__slots\", {\n \"data-selection-slots\": \"\",\n });\n root.appendChild(slotsContainer);\n\n root.appendChild(el(\"div\", \"lb-bundle-divider\"));\n\n // --- Pricing (hidden until first selection) ---\n const pricingSection = renderPricingSection(wc.pricing.showCompareAtPrice);\n root.appendChild(pricingSection.el);\n\n const savingsBar = wc.savingsBar.visible ? renderSavingsBar() : null;\n if (savingsBar) root.appendChild(savingsBar.el);\n\n // --- CTA ---\n // Built before the modal so the picker's getFocusAfterAdd closure can\n // refer to it as a fallback when every slot is filled.\n const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);\n cta.disabled = true;\n cta.addEventListener(\"click\", () => {\n if (cta.disabled) return;\n onAddToCart(buildCartLines(selections, bundle));\n });\n\n // --- Modal overlay ---\n const modal = renderModal(bundle, eligible, currency, {\n showSearch: wc.showSearch,\n showQtySelector,\n selections,\n onAdd: (product, variant, quantity) =>\n addSelection(product, variant, quantity),\n isComplete: () => selections.length >= requiredQty,\n getFocusAfterAdd: () => {\n // afterMutation has already re-rendered slots, so the first\n // .lb-mix-match__slot--empty is wherever focus should go next. When\n // every slot is filled, fall back to the now-enabled CTA so Enter\n // adds the whole bundle to cart.\n const empty = slotsContainer.querySelector<HTMLElement>(\n \".lb-mix-match__slot--empty\",\n );\n return empty ?? cta;\n },\n });\n root.appendChild(modal.el);\n\n root.appendChild(cta);\n\n root.appendChild(\n el(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n container.appendChild(root);\n\n // Custom dropdown teardown — bind happens lazily inside buildRows().\n onCleanup?.(() => unbindAllDropdowns(root));\n\n // Initial render. afterMutation() handles slots, progress, pricing,\n // savings bar, placeholder, modal refreshCounts, and CTA in one place.\n // Safe to call here: modal.refreshCounts is a no-op while the picker\n // is closed (productRows is built lazily on first open).\n afterMutation();\n\n // --- Mutation helpers (closures over local state) ---\n\n function addSelection(\n product: Product,\n variant: ProductVariant,\n quantity: number,\n ) {\n if (selections.length >= requiredQty) return;\n const rule = ruleFor(bundle, product.id);\n const cap = maxAddableQuantity(\n variant,\n rule.max,\n alreadyInBundleFor(selections, product.id, variant.id),\n );\n // Refuse the pick when stock can't satisfy the per-product minimum —\n // otherwise the slot would carry more units than the variant has, and\n // Shopify's checkout-time inventory check would fail with an opaque\n // \"out of stock\" error after the customer hit Add to cart.\n if (cap < rule.min) return;\n // Clamp defensively. The picker stepper enforces these bounds, but\n // a malformed Add (e.g. keyboard event before stepper init) shouldn't\n // bypass them.\n const clamped = Math.max(rule.min, Math.min(quantity, cap));\n selections.push({\n productId: product.id,\n productTitle: product.title,\n variantId: variant.id,\n variantTitle: variant.title,\n imageUrl: variant.image?.url ?? product.featuredImage?.url ?? null,\n priceCents: parseCents(variant.price.amount),\n compareCents: variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null,\n unitPriceLabel: formatUnitPrice(\n variant.unitPrice,\n variant.unitPriceMeasurement,\n currency,\n ),\n quantity: clamped,\n });\n afterMutation();\n }\n\n function removeSlotAt(index: number) {\n if (index < 0 || index >= selections.length) return;\n selections.splice(index, 1);\n afterMutation();\n }\n\n function afterMutation() {\n renderSlots();\n progress.update(selections.length);\n pricingSection.update(selections, bundle, currency);\n if (savingsBar) savingsBar.update(selections, bundle, currency);\n modal.refreshCounts();\n updateCta();\n }\n\n function renderSlots() {\n slotsContainer.innerHTML = \"\";\n const totalSlots = Math.max(requiredQty, selections.length);\n for (let i = 0; i < totalSlots; i++) {\n const selection = selections[i];\n if (selection) {\n slotsContainer.appendChild(\n renderFilledSlot(selection, i, currency, () => removeSlotAt(i)),\n );\n } else {\n slotsContainer.appendChild(\n renderEmptySlot(i, () => modal.open()),\n );\n }\n }\n }\n\n function updateCta() {\n // Target the label span, not the button itself — replacing the button's\n // textContent would destroy the sibling spinner span built by\n // buildCtaButton. See packages/widget/src/renderers/cta-button.ts.\n const count = selections.length;\n if (count < requiredQty) {\n cta.disabled = true;\n setCtaLabel(cta, `Select ${requiredQty - count} more to unlock`);\n } else {\n cta.disabled = false;\n setCtaLabel(cta, wc.cta.ctaText || \"Add to cart\");\n }\n }\n}\n\n/**\n * Aggregate selections into one CartLineInput per (productId, variantId).\n * Matches the discount-function attribution contract: every line carries\n * `_lime_bundle_gid` and `_lime_bundle_type` so the orders/create webhook\n * can map purchases back to this bundle.\n */\nfunction buildCartLines(\n selections: Selection[],\n bundle: MixMatchBundleData,\n): CartLineInput[] {\n const grouped = new Map<string, { variantId: string; quantity: number }>();\n for (const s of selections) {\n const key = `${s.productId}::${s.variantId}`;\n const existing = grouped.get(key);\n if (existing) {\n existing.quantity += s.quantity;\n } else {\n grouped.set(key, { variantId: s.variantId, quantity: s.quantity });\n }\n }\n return Array.from(grouped.values()).map((line) => ({\n merchandiseId: line.variantId,\n quantity: line.quantity,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n }));\n}\n\n// --- Section renderers ---\n\nfunction renderHeader(bundle: MixMatchBundleData): HTMLElement {\n const wc = bundle.widgetConfig;\n const header = el(\"div\", \"lb-bundle-header\");\n const content = el(\"div\", \"lb-bundle-header__content\");\n const title = el(\"h3\", \"lb-bundle-title\");\n title.textContent = bundle.title;\n content.appendChild(title);\n header.appendChild(content);\n\n if (wc.pricing.showSaveBadge) {\n const { discountType, discountValue } = bundle.discountConfig;\n let label: string | null = null;\n if (discountType === \"percentage\" && discountValue > 0) {\n label = `-${Math.round(discountValue)}%`;\n } else if (discountType === \"fixed_amount\" && discountValue > 0) {\n label = `-${formatCents(\n Math.round(discountValue * 100),\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\",\n )}`;\n }\n if (label) {\n const badge = el(\"span\", \"lb-bundle-header__badge\");\n badge.textContent = label;\n header.appendChild(badge);\n }\n }\n return header;\n}\n\nfunction renderProgress(requiredQty: number) {\n const wrap = el(\"div\", \"lb-mix-match__progress\");\n const labels = el(\"div\", \"lb-mix-match__progress-labels\");\n const count = el(\"span\", \"lb-mix-match__progress-count\", {\n \"data-progress-count\": \"\",\n \"aria-live\": \"polite\",\n });\n count.textContent = `0 of ${requiredQty} selected`;\n labels.appendChild(count);\n const remaining = el(\"span\", \"lb-mix-match__progress-remaining\", {\n \"data-progress-remaining\": \"\",\n \"aria-live\": \"polite\",\n });\n remaining.textContent = `${requiredQty} more to go`;\n labels.appendChild(remaining);\n wrap.appendChild(labels);\n\n const track = el(\"div\", \"lb-mix-match__progress-track\", {\n role: \"progressbar\",\n \"aria-valuenow\": \"0\",\n \"aria-valuemin\": \"0\",\n \"aria-valuemax\": String(requiredQty),\n });\n const fill = el(\"div\", \"lb-mix-match__progress-fill\", {\n \"data-progress-fill\": \"\",\n });\n fill.style.width = \"0%\";\n track.appendChild(fill);\n wrap.appendChild(track);\n\n function update(selected: number) {\n const pct = Math.min(100, (selected / requiredQty) * 100);\n count.textContent = `${selected} of ${requiredQty} selected`;\n if (selected >= requiredQty) {\n remaining.textContent = \"Complete\";\n } else {\n remaining.textContent = `${requiredQty - selected} more to go`;\n }\n fill.style.width = `${pct}%`;\n track.setAttribute(\"aria-valuenow\", String(Math.min(selected, requiredQty)));\n }\n\n return { el: wrap, update };\n}\n\nfunction renderEmptySlot(index: number, onClick: () => void): HTMLElement {\n const slot = el(\n \"div\",\n \"lb-bundle-product-row lb-mix-match__slot lb-mix-match__slot--empty\",\n {\n \"data-slot\": String(index + 1),\n tabindex: \"0\",\n role: \"button\",\n \"aria-label\": \"Add a product to the bundle\",\n },\n );\n const thumb = el(\"div\", \"lb-mix-match__empty-thumb\");\n thumb.innerHTML = PLUS_ICON_SVG;\n slot.appendChild(thumb);\n const text = el(\"span\", \"lb-mix-match__empty-text\");\n text.textContent = \"Choose an item\";\n slot.appendChild(text);\n slot.addEventListener(\"click\", onClick);\n slot.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick();\n }\n });\n return slot;\n}\n\nfunction renderFilledSlot(\n selection: Selection,\n index: number,\n currency: string,\n onRemove: () => void,\n): HTMLElement {\n const slot = el(\n \"div\",\n \"lb-bundle-product-row lb-mix-match__slot lb-mix-match__slot--filled\",\n { \"data-slot\": String(index + 1) },\n );\n const thumb = el(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n if (selection.imageUrl) {\n const img = document.createElement(\"img\");\n img.src = transformImageUrl(selection.imageUrl, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n img.alt = selection.productTitle;\n img.width = THUMB_PX;\n img.height = THUMB_PX;\n img.loading = \"lazy\";\n thumb.appendChild(img);\n } else {\n thumb.insertAdjacentHTML(\"beforeend\", PLACEHOLDER_THUMB_SVG);\n }\n const qtyBadge = el(\"span\", \"lb-bundle-qty-badge\");\n // Filled-slot badge shows the shopper's chosen qty as \"× N\", which makes\n // it visually distinct from the picker's per-product qty stepper. Bare\n // \"1\" on a square thumbnail looks like a placeholder digit.\n qtyBadge.textContent = `×${selection.quantity}`;\n thumb.appendChild(qtyBadge);\n slot.appendChild(thumb);\n\n const info = el(\"div\", \"lb-mix-match__filled-info\");\n const title = el(\"span\", \"lb-mix-match__filled-title\");\n title.textContent = selection.productTitle;\n info.appendChild(title);\n if (selection.variantTitle && selection.variantTitle !== \"Default Title\") {\n const variant = el(\"span\", \"lb-mix-match__filled-variant\");\n variant.textContent = selection.variantTitle;\n info.appendChild(variant);\n }\n const linePrice = selection.priceCents * selection.quantity;\n const lineCompare =\n selection.compareCents !== null\n ? selection.compareCents * selection.quantity\n : null;\n const priceWrap = el(\"span\", \"lb-mix-match__filled-price\");\n if (lineCompare !== null && lineCompare > linePrice) {\n const compare = el(\"span\", \"lb-mix-match__filled-compare\");\n compare.textContent = formatCents(lineCompare, currency);\n priceWrap.appendChild(compare);\n }\n const priceEl = document.createElement(\"span\");\n priceEl.textContent = formatCents(linePrice, currency);\n priceWrap.appendChild(priceEl);\n info.appendChild(priceWrap);\n if (selection.unitPriceLabel) {\n const unitPrice = el(\"span\", \"lb-bundle-product-unit-price\");\n unitPrice.textContent = selection.unitPriceLabel;\n info.appendChild(unitPrice);\n }\n slot.appendChild(info);\n\n const remove = document.createElement(\"button\");\n remove.type = \"button\";\n remove.className = \"lb-mix-match__slot-remove\";\n remove.setAttribute(\"aria-label\", `Remove ${selection.productTitle}`);\n remove.innerHTML = CLOSE_ICON_SVG;\n remove.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n onRemove();\n });\n slot.appendChild(remove);\n return slot;\n}\n\nfunction renderPricingSection(showCompareAtPrice: boolean) {\n const wrap = el(\"div\", \"lb-bundle-pricing\", { \"data-pricing-section\": \"\" });\n wrap.style.display = \"none\";\n const label = el(\"span\", \"lb-bundle-pricing__label\");\n label.textContent = \"Bundle price\";\n wrap.appendChild(label);\n const prices = el(\"span\", \"lb-bundle-pricing__prices\");\n const compare = el(\"span\", \"lb-bundle-compare-price\", {\n \"data-compare-price\": \"\",\n });\n if (showCompareAtPrice) prices.appendChild(compare);\n const sale = el(\"span\", \"lb-bundle-sale-price\", { \"data-sale-price\": \"\" });\n prices.appendChild(sale);\n wrap.appendChild(prices);\n\n function update(\n selections: Selection[],\n bundle: MixMatchBundleData,\n currency: string,\n ) {\n if (selections.length === 0) {\n wrap.style.display = \"none\";\n return;\n }\n wrap.style.display = \"\";\n const totalCents = selections.reduce(\n (s, sel) => s + sel.priceCents * sel.quantity,\n 0,\n );\n const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);\n if (showCompareAtPrice && totalCents > saleCents) {\n compare.textContent = formatCents(totalCents, currency);\n compare.style.display = \"\";\n } else {\n compare.style.display = \"none\";\n }\n sale.textContent = formatCents(saleCents, currency);\n }\n\n return { el: wrap, update };\n}\n\nfunction renderSavingsBar() {\n const wrap = el(\"div\", \"lb-bundle-savings-bar\", {\n \"data-savings-bar\": \"\",\n });\n wrap.style.display = \"none\";\n const labelEl = document.createElement(\"span\");\n labelEl.textContent = \"You save\";\n wrap.appendChild(labelEl);\n const amount = el(\"span\", \"\", { \"data-savings-amount\": \"\" });\n wrap.appendChild(amount);\n\n function update(\n selections: Selection[],\n bundle: MixMatchBundleData,\n currency: string,\n ) {\n if (selections.length === 0) {\n wrap.style.display = \"none\";\n return;\n }\n const totalCents = selections.reduce(\n (s, sel) => s + sel.priceCents * sel.quantity,\n 0,\n );\n const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);\n const savings = Math.max(0, totalCents - saleCents);\n if (savings <= 0) {\n wrap.style.display = \"none\";\n return;\n }\n wrap.style.display = \"\";\n amount.textContent = formatCents(savings, currency);\n }\n\n return { el: wrap, update };\n}\n\n// --- Picker modal ---\n\ninterface ModalHandlers {\n showSearch: boolean;\n showQtySelector: boolean;\n /** Live reference to the parent's selections. Read inside the modal to\n * compute remaining stock per variant (`maxAddableQuantity`). */\n selections: Selection[];\n onAdd: (product: Product, variant: ProductVariant, quantity: number) => void;\n isComplete: () => boolean;\n /** Where to send focus after Add closes the modal. The slot the modal was\n * opened from has been rebuilt as a filled slot (no tabindex/role) by the\n * parent's afterMutation, so close()'s default restore would .focus() a\n * non-focusable element and the browser would silently fall back to\n * <body> — dumping keyboard users at the address bar. The parent picks\n * the next empty slot, or the CTA when the bundle is complete. */\n getFocusAfterAdd: () => HTMLElement | null;\n}\n\nfunction renderModal(\n bundle: MixMatchBundleData,\n eligible: EligibleProduct[],\n currency: string,\n handlers: ModalHandlers,\n) {\n const overlay = el(\"div\", \"lb-mix-match__modal-overlay\", {\n \"data-modal-overlay\": \"\",\n \"data-bundle-gid\": bundle.id,\n });\n overlay.style.display = \"none\";\n\n const modal = el(\"div\", \"lb-mix-match__modal\", {\n role: \"dialog\",\n \"aria-modal\": \"true\",\n \"aria-labelledby\": `lb-modal-title-${sanitizeId(bundle.id)}`,\n tabindex: \"-1\",\n });\n\n // Header\n const modalHeader = el(\"div\", \"lb-mix-match__modal-header\");\n const modalTitle = el(\"h4\", \"lb-mix-match__modal-title\", {\n id: `lb-modal-title-${sanitizeId(bundle.id)}`,\n });\n modalTitle.textContent = \"Pick an item\";\n modalHeader.appendChild(modalTitle);\n const closeBtn = document.createElement(\"button\");\n closeBtn.type = \"button\";\n closeBtn.className = \"lb-mix-match__modal-close\";\n closeBtn.setAttribute(\"data-modal-close\", \"\");\n closeBtn.setAttribute(\"aria-label\", \"Close\");\n closeBtn.innerHTML = CLOSE_ICON_SVG;\n closeBtn.addEventListener(\"click\", close);\n modalHeader.appendChild(closeBtn);\n modal.appendChild(modalHeader);\n\n // Search\n let searchInput: HTMLInputElement | null = null;\n let searchClearBtn: HTMLButtonElement | null = null;\n if (handlers.showSearch) {\n const searchWrap = el(\"div\", \"lb-mix-match__modal-search\");\n searchInput = document.createElement(\"input\");\n searchInput.type = \"text\";\n searchInput.className = \"lb-mix-match__modal-search-input\";\n searchInput.setAttribute(\"data-modal-search\", \"\");\n searchInput.setAttribute(\"role\", \"searchbox\");\n searchInput.setAttribute(\"aria-label\", \"Search products\");\n searchInput.setAttribute(\"placeholder\", \"Search products\");\n searchInput.autocomplete = \"off\";\n searchInput.addEventListener(\"input\", () => applySearch());\n searchWrap.appendChild(searchInput);\n\n searchClearBtn = document.createElement(\"button\");\n searchClearBtn.type = \"button\";\n searchClearBtn.className = \"lb-mix-match__modal-search-clear\";\n searchClearBtn.setAttribute(\"data-modal-search-clear\", \"\");\n searchClearBtn.setAttribute(\"aria-label\", \"Clear search\");\n searchClearBtn.style.display = \"none\";\n searchClearBtn.innerHTML = SEARCH_CLEAR_ICON_SVG;\n searchClearBtn.addEventListener(\"click\", () => {\n if (!searchInput) return;\n searchInput.value = \"\";\n applySearch();\n searchInput.focus();\n });\n searchWrap.appendChild(searchClearBtn);\n modal.appendChild(searchWrap);\n }\n\n // Product list\n const list = el(\"div\", \"lb-mix-match__modal-list\", {\n \"data-modal-list\": \"\",\n });\n modal.appendChild(list);\n\n const empty = el(\"div\", \"lb-mix-match__modal-empty\", {\n \"data-modal-empty\": \"\",\n });\n empty.style.display = \"none\";\n const emptyText = document.createElement(\"p\");\n emptyText.textContent = \"No products match your search.\";\n empty.appendChild(emptyText);\n modal.appendChild(empty);\n\n const live = el(\"span\", \"lb-visually-hidden\", {\n \"data-modal-live\": \"\",\n \"aria-live\": \"polite\",\n });\n modal.appendChild(live);\n\n overlay.appendChild(modal);\n\n // Build product rows lazily on first open.\n let rowsBuilt = false;\n const productRows: Array<{\n el: HTMLElement;\n product: Product;\n variant: ProductVariant;\n /** Recompute stepper bounds + count badge from current selections. */\n refreshFromSelections: () => void;\n }> = [];\n\n function buildRows() {\n if (rowsBuilt) return;\n rowsBuilt = true;\n list.innerHTML = \"\";\n\n eligible.forEach((ep) => {\n const rule = ruleFor(bundle, ep.product.id);\n\n // Mirrors the Liquid picker-modal pattern in lb-mix-match.liquid:\n // - title/price/unit-price are <p> elements so they stack as blocks.\n // - <select> appears for products with >1 variant; the add button\n // dispatches the currently-selected variant.\n // - Variants that can't satisfy `rule.min` units are sold-out for\n // this bundle's purposes.\n const availableVariants = ep.variants;\n const firstAvailVariant =\n ep.variants.find((v) => isVariantFulfillable(v, rule.min)) ??\n ep.firstAvailableVariant ??\n ep.variants[0];\n if (!firstAvailVariant) return;\n\n let currentVariant = firstAvailVariant;\n\n const productEl = el(\n \"div\",\n ep.isOos\n ? \"lb-mix-match__modal-product lb-mix-match__modal-product--sold-out\"\n : \"lb-mix-match__modal-product\",\n { \"data-product-id\": ep.product.id.replace(/^.*\\//, \"\") },\n );\n\n const thumb = el(\"div\", \"lb-mix-match__modal-product-thumb\");\n const initialThumbVariant =\n ep.variants.find((v) => isVariantFulfillable(v, rule.min)) ??\n ep.variants[0] ??\n null;\n const initialThumbImage =\n initialThumbVariant?.image ?? ep.product.featuredImage ?? null;\n let thumbImg: HTMLImageElement | null = null;\n if (initialThumbImage) {\n thumbImg = document.createElement(\"img\");\n thumbImg.src = transformImageUrl(initialThumbImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = initialThumbImage.altText ?? ep.product.title;\n thumbImg.width = THUMB_PX;\n thumbImg.height = THUMB_PX;\n thumbImg.loading = \"lazy\";\n thumb.appendChild(thumbImg);\n } else {\n thumb.insertAdjacentHTML(\"beforeend\", PLACEHOLDER_THUMB_SVG);\n }\n // Count badge shows how many units of the *current* variant the\n // shopper has already pinned to this bundle. Hidden at zero.\n const countBadge = el(\"span\", \"lb-bundle-qty-badge\");\n countBadge.hidden = true;\n thumb.appendChild(countBadge);\n productEl.appendChild(thumb);\n\n const info = el(\"div\", \"lb-mix-match__modal-product-info\");\n const title = el(\"p\", \"lb-mix-match__modal-product-title\");\n title.textContent = ep.product.title;\n info.appendChild(title);\n\n const price = el(\"p\", \"lb-mix-match__modal-product-price\");\n // Picker price reflects single-unit price — the stepper shows\n // multiplier separately. Keeps the price label stable as the\n // shopper bumps the stepper.\n price.textContent = formatCents(parseCents(currentVariant.price.amount), currency);\n info.appendChild(price);\n\n const unitPrice = el(\n \"p\",\n \"lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price\",\n );\n const initialUnitText = formatUnitPrice(\n currentVariant.unitPrice,\n currentVariant.unitPriceMeasurement,\n currency,\n );\n if (initialUnitText) {\n unitPrice.textContent = initialUnitText;\n } else {\n unitPrice.hidden = true;\n }\n info.appendChild(unitPrice);\n\n // Low-stock badge — re-rendered on variant change.\n const lowStockBadge = el(\"span\", \"lb-bundle-low-stock-badge\", {\n \"data-low-stock-badge\": \"\",\n });\n lowStockBadge.hidden = true;\n info.appendChild(lowStockBadge);\n const refreshLowStockBadge = (variant: ProductVariant) => {\n if (\n shouldShowLowStockBadge(\n variant,\n rule.min,\n bundle.widgetConfig.lowStockThreshold,\n bundle.widgetConfig.showLowStockBadge,\n )\n ) {\n lowStockBadge.textContent = `Only ${variant.quantityAvailable} left`;\n lowStockBadge.hidden = false;\n } else {\n lowStockBadge.hidden = true;\n }\n };\n refreshLowStockBadge(currentVariant);\n\n // Per-pick quantity stepper sits inside info between the low-stock\n // badge and the variant picker so the modal reads price → unit\n // price → qty → variant. Built before the variant logic so its\n // bounds closure can pick up later currentVariant updates from\n // the variant select handlers via the shared `currentVariant`\n // binding. Mirrors the Liquid template structure.\n const computeStepperBounds = () => {\n const already = alreadyInBundleFor(\n handlers.selections,\n ep.product.id,\n currentVariant.id,\n );\n const cap = maxAddableQuantity(currentVariant, rule.max, already);\n // Stepper max can never drop below `rule.min` while the variant is\n // fulfillable — but if `cap` is below `rule.min` (e.g. only 1 unit\n // left and rule.min=2) we surface that by disabling Add and\n // pinning the stepper at rule.min.\n const max = Math.max(rule.min, cap);\n return { min: rule.min, max, cap };\n };\n\n let stepper: ReturnType<typeof renderQtyStepper> | null = null;\n let qtyGroup: HTMLElement | null = null;\n if (!ep.isOos && handlers.showQtySelector) {\n // Wrap stepper in a .lb-bundle-variant-option-group so it\n // inherits the same flex-column layout the variant pickers use.\n // The compound .lb-mix-match__qty-stepper-group class is the\n // qty-stepper hook; the group is appended to the action-row\n // wrapper below (next to the Add button) — not to info — so the\n // stepper sits left of the Add button.\n qtyGroup = el(\n \"div\",\n \"lb-bundle-variant-option-group lb-mix-match__qty-stepper-group\",\n );\n stepper = renderQtyStepper({\n initial: rule.min,\n getBounds: () => {\n const { min, max } = computeStepperBounds();\n return { min, max };\n },\n });\n qtyGroup.appendChild(stepper.el);\n }\n\n const row = {\n el: productEl,\n product: ep.product,\n variant: firstAvailVariant,\n refreshFromSelections: () => {},\n };\n\n // Lifted from the multi-variant branch below so refreshAddState can\n // propagate the row-disabled state into each select. The custom\n // dropdown trigger reads `select.disabled` via its MutationObserver\n // (see bind-dropdown.ts) so it follows automatically.\n const optionSelects: HTMLSelectElement[] = [];\n\n // Per-option dropdowns (Shopify's recommended pattern — one <select>\n // per product option). Values that don't combine with the currently\n // selected values are disabled, so the customer gets clear feedback.\n if (availableVariants.length > 1) {\n const optionNames: string[] = availableVariants[0].selectedOptions.map(\n (o) => o.name,\n );\n const productIdTail = ep.product.id.replace(/^.*\\//, \"\");\n\n const resolveVariant = (values: string[]): ProductVariant | null =>\n availableVariants.find(\n (v) =>\n v.selectedOptions.length === values.length &&\n v.selectedOptions.every((o, i) => o.value === values[i]),\n ) ?? null;\n\n const syncSelectsToVariant = (v: ProductVariant) => {\n v.selectedOptions.forEach((o, i) => {\n const sel = optionSelects[i];\n if (sel && sel.value !== o.value) sel.value = o.value;\n });\n };\n\n const isValueAvailable = (\n optionIndex: number,\n value: string,\n selected: string[],\n ): boolean =>\n availableVariants.some((v) => {\n if (!isVariantFulfillable(v, rule.min)) return false;\n if (v.selectedOptions[optionIndex]?.value !== value) return false;\n return v.selectedOptions.every(\n (o, i) => i === optionIndex || o.value === selected[i],\n );\n });\n\n const recomputeDisabled = (selected: string[]) => {\n optionSelects.forEach((sel, i) => {\n Array.from(sel.options).forEach((opt) => {\n opt.disabled = !isValueAvailable(i, opt.value, selected);\n });\n });\n };\n\n const handleChange = () => {\n const values = optionSelects.map((s) => s.value);\n const next = resolveVariant(values);\n if (!next) {\n // Disabled combo reached via keyboard — revert selects.\n syncSelectsToVariant(currentVariant);\n recomputeDisabled(\n currentVariant.selectedOptions.map((o) => o.value),\n );\n return;\n }\n currentVariant = next;\n row.variant = next;\n price.textContent = formatCents(parseCents(currentVariant.price.amount), currency);\n const nextUnitText = formatUnitPrice(\n currentVariant.unitPrice,\n currentVariant.unitPriceMeasurement,\n currency,\n );\n if (nextUnitText) {\n unitPrice.textContent = nextUnitText;\n unitPrice.hidden = false;\n } else {\n unitPrice.textContent = \"\";\n unitPrice.hidden = true;\n }\n // Swap the row thumbnail to the picked variant's image when it\n // has one.\n const nextImage = currentVariant.image ?? ep.product.featuredImage;\n if (thumbImg && nextImage) {\n thumbImg.src = transformImageUrl(nextImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = nextImage.altText ?? ep.product.title;\n }\n recomputeDisabled(next.selectedOptions.map((o) => o.value));\n refreshLowStockBadge(currentVariant);\n row.refreshFromSelections();\n };\n\n const groupsContainer = el(\"div\", \"lb-bundle-variant-option-groups\");\n optionNames.forEach((name, position) => {\n const group = el(\"div\", \"lb-bundle-variant-option-group\");\n\n const label = el(\"span\", \"lb-bundle-variant-option-label\");\n label.textContent = name;\n group.appendChild(label);\n\n const select = document.createElement(\"select\");\n select.className = \"lb-mix-match__variant-select\";\n select.setAttribute(\"data-variant-option\", \"\");\n select.setAttribute(\"data-option-position\", String(position + 1));\n select.name = `lb-variant-${productIdTail}-${position + 1}`;\n select.setAttribute(\"aria-label\", name);\n\n const seen = new Set<string>();\n availableVariants.forEach((v) => {\n const value = v.selectedOptions[position]?.value;\n if (!value || seen.has(value)) return;\n seen.add(value);\n const opt = document.createElement(\"option\");\n opt.value = value;\n opt.textContent = value;\n if (firstAvailVariant.selectedOptions[position]?.value === value) {\n opt.selected = true;\n }\n select.appendChild(opt);\n });\n\n select.addEventListener(\"change\", handleChange);\n optionSelects.push(select);\n group.appendChild(select);\n groupsContainer.appendChild(group);\n });\n info.appendChild(groupsContainer);\n\n recomputeDisabled(\n firstAvailVariant.selectedOptions.map((o) => o.value),\n );\n } else if (\n availableVariants.length === 1 &&\n firstAvailVariant.title !== \"Default Title\"\n ) {\n const variantLabel = el(\"span\", \"lb-mix-match__filled-variant\");\n variantLabel.textContent = firstAvailVariant.title;\n info.appendChild(variantLabel);\n }\n\n if (ep.isOos) {\n const soldOut = el(\"span\", \"lb-mix-match__modal-sold-out-label\");\n soldOut.textContent = \"Sold out\";\n info.appendChild(soldOut);\n }\n productEl.appendChild(info);\n\n // --- Quantity stepper + Add button ---\n // Stepper rendering happened earlier (right after the low-stock\n // badge, before the variant picker). The action row sits at the\n // bottom of the info column with a CSS margin-top so the modal\n // reads thumb | (title → price → variants → [stepper] [Add]).\n // Mirrors the Liquid template ordering in extensions/bundle-theme/\n // snippets/lb-mix-match.liquid + bundle-mix-match.js.\n let addBtn: HTMLButtonElement | null = null;\n\n const refreshAddState = () => {\n if (!addBtn) {\n // OOS row — addBtn is never built (see `if (!ep.isOos)` below).\n // Lock the picker controls so keyboard users can't operate the\n // variant dropdowns on a sold-out row. Stepper is also never\n // built for OOS (gated by !ep.isOos in qty-stepper branch);\n // the guard is defensive in case that gating changes.\n optionSelects.forEach((sel) => {\n sel.disabled = true;\n });\n if (stepper) stepper.setExternallyDisabled(true);\n return;\n }\n const { cap } = computeStepperBounds();\n const already = alreadyInBundleFor(\n handlers.selections,\n ep.product.id,\n currentVariant.id,\n );\n // Update the in-thumb count badge to show the live aggregate qty.\n if (already > 0) {\n countBadge.textContent = String(already);\n countBadge.hidden = false;\n } else {\n countBadge.hidden = true;\n }\n // The discount counts distinct products, so re-adding the same\n // product across slots wastes them without advancing the bundle's\n // pick count. When this product (any variant) is already in a\n // slot, mark the row \"Added\" and disable Add — product-in-bundle\n // takes priority over the stock-based disable below.\n const productInBundle = handlers.selections.some(\n (s) => s.productId === ep.product.id,\n );\n if (productInBundle) {\n addBtn.disabled = true;\n addBtn.textContent = \"Added\";\n // Per-row accessible name — without this every Add button in\n // the modal announces as just \"Add, button\" and a screen reader\n // user can't tell which product they're about to add.\n addBtn.setAttribute(\"aria-label\", `Added ${ep.product.title}`);\n productEl.classList.add(\"lb-mix-match__modal-product--in-bundle\");\n } else {\n addBtn.textContent = \"Add\";\n addBtn.setAttribute(\"aria-label\", `Add ${ep.product.title}`);\n productEl.classList.remove(\"lb-mix-match__modal-product--in-bundle\");\n // Add disabled when: bundle is already full, or variant can't\n // accept another rule.min units.\n addBtn.disabled = handlers.isComplete() || cap < rule.min;\n }\n // Propagate row-disabled state to the picker controls so keyboard\n // users can't operate dropdowns / stepper buttons on a row whose\n // pick can't land. The dropdown trigger picks up `select.disabled`\n // via its MutationObserver in bind-dropdown.ts.\n const rowDisabled = addBtn.disabled;\n optionSelects.forEach((sel) => {\n sel.disabled = rowDisabled;\n });\n if (stepper) stepper.setExternallyDisabled(rowDisabled);\n };\n\n if (!ep.isOos) {\n const actions = el(\"div\", \"lb-mix-match__modal-product-actions\");\n\n // Stepper sits to the left of the Add button (when the merchant\n // enabled the qty selector). When disabled, the Add button takes\n // the full action-row width via `flex: 1` in CSS.\n if (qtyGroup) actions.appendChild(qtyGroup);\n\n addBtn = document.createElement(\"button\");\n addBtn.type = \"button\";\n addBtn.className = \"lb-mix-match__modal-add\";\n addBtn.textContent = \"Add\";\n addBtn.setAttribute(\"aria-label\", `Add ${ep.product.title}`);\n addBtn.addEventListener(\"click\", () => {\n if (!addBtn || addBtn.disabled) return;\n if (handlers.isComplete()) return;\n // When the stepper is hidden, Add adds qty = rule.min (per spec).\n const qty = stepper ? stepper.value() : rule.min;\n handlers.onAdd(ep.product, currentVariant, qty);\n // Reset stepper to rule.min so the next pick of this product\n // doesn't carry over the previous shopper-chosen qty.\n if (stepper) stepper.reset(rule.min);\n // Override the close-time focus restore so keyboard users land on\n // the next empty slot (or the CTA when the bundle is complete)\n // instead of <body>. See ModalHandlers.getFocusAfterAdd.\n const nextFocus = handlers.getFocusAfterAdd();\n if (nextFocus) lastFocused = nextFocus;\n close();\n });\n actions.appendChild(addBtn);\n info.appendChild(actions);\n }\n\n row.refreshFromSelections = () => {\n if (stepper) stepper.refresh();\n refreshAddState();\n };\n // Initialise the badge + Add disabled state.\n row.refreshFromSelections();\n\n productRows.push(row);\n\n list.appendChild(productEl);\n });\n\n // Replace native <select> popup chrome with our accessible custom dropdown.\n bindAllDropdowns(list);\n\n refreshCounts();\n }\n\n function applySearch() {\n if (!searchInput) return;\n const query = searchInput.value.trim().toLowerCase();\n if (searchClearBtn) {\n searchClearBtn.style.display = query ? \"\" : \"none\";\n }\n let visibleCount = 0;\n productRows.forEach((row) => {\n const match = !query || row.product.title.toLowerCase().includes(query);\n row.el.style.display = match ? \"\" : \"none\";\n if (match) visibleCount++;\n });\n empty.style.display = visibleCount === 0 && query ? \"\" : \"none\";\n }\n\n // Focus trap + keyboard handling\n let lastFocused: Element | null = null;\n function onKeydown(e: KeyboardEvent) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n close();\n return;\n }\n if (e.key === \"Tab\") {\n trapFocus(e, modal);\n }\n }\n\n let isOpen = false;\n\n function open() {\n if (isOpen) return;\n if (handlers.isComplete()) return;\n isOpen = true;\n buildRows();\n lastFocused = (overlay.getRootNode() as Document | ShadowRoot)\n .activeElement;\n overlay.style.display = \"\";\n overlay.classList.add(\"lb-mix-match__modal-overlay--open\");\n modal.focus();\n document.addEventListener(\"keydown\", onKeydown);\n overlay.addEventListener(\"click\", onOverlayClick);\n }\n\n function close() {\n if (!isOpen) return;\n isOpen = false;\n overlay.classList.remove(\"lb-mix-match__modal-overlay--open\");\n overlay.style.display = \"none\";\n document.removeEventListener(\"keydown\", onKeydown);\n overlay.removeEventListener(\"click\", onOverlayClick);\n if (lastFocused instanceof HTMLElement) {\n lastFocused.focus();\n }\n }\n\n function onOverlayClick(e: MouseEvent) {\n if (e.target === overlay) close();\n }\n\n function refreshCounts() {\n productRows.forEach((r) => r.refreshFromSelections());\n }\n\n return { el: overlay, open, close, refreshCounts };\n}\n\n// --- Quantity stepper ---\n\ninterface QtyStepper {\n el: HTMLElement;\n /** Current value (clamped to bounds). */\n value: () => number;\n /** Reset to a known qty (clamped to current bounds). */\n reset: (qty: number) => void;\n /** Re-evaluate bounds — disable / clamp when the cap drops. */\n refresh: () => void;\n /** Override — disable both buttons regardless of bounds. Used when the\n * row's Add is disabled (product already in bundle / out-of-stock) so\n * keyboard users can't operate a stepper whose pick can't land. */\n setExternallyDisabled: (disabled: boolean) => void;\n}\n\nfunction renderQtyStepper(opts: {\n initial: number;\n getBounds: () => { min: number; max: number };\n}): QtyStepper {\n const wrap = el(\"div\", \"lb-mix-match__qty-stepper\", {\n role: \"group\",\n \"aria-label\": \"Quantity\",\n });\n const minus = document.createElement(\"button\");\n minus.type = \"button\";\n minus.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus\";\n minus.setAttribute(\"aria-label\", \"Decrease quantity\");\n minus.innerHTML = STEPPER_MINUS_SVG;\n wrap.appendChild(minus);\n\n const valueEl = el(\"span\", \"lb-mix-match__qty-stepper-value\", {\n \"aria-live\": \"polite\",\n });\n wrap.appendChild(valueEl);\n\n const plus = document.createElement(\"button\");\n plus.type = \"button\";\n plus.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus\";\n plus.setAttribute(\"aria-label\", \"Increase quantity\");\n plus.innerHTML = STEPPER_PLUS_SVG;\n wrap.appendChild(plus);\n\n let current = clamp(opts.initial, opts.getBounds());\n let externallyDisabled = false;\n\n function clamp(n: number, b: { min: number; max: number }): number {\n return Math.max(b.min, Math.min(b.max, n));\n }\n\n function paint() {\n const b = opts.getBounds();\n current = clamp(current, b);\n valueEl.textContent = String(current);\n minus.disabled = externallyDisabled || current <= b.min;\n plus.disabled = externallyDisabled || current >= b.max;\n }\n\n minus.addEventListener(\"click\", () => {\n const b = opts.getBounds();\n current = clamp(current - 1, b);\n paint();\n });\n plus.addEventListener(\"click\", () => {\n const b = opts.getBounds();\n current = clamp(current + 1, b);\n paint();\n });\n\n paint();\n\n return {\n el: wrap,\n value: () => current,\n reset(qty) {\n current = clamp(qty, opts.getBounds());\n paint();\n },\n refresh: paint,\n setExternallyDisabled(disabled) {\n externallyDisabled = disabled;\n paint();\n },\n };\n}\n\n// --- Helpers ---\n\nfunction buildEligibleProducts(\n bundle: MixMatchBundleData,\n oosBehavior: \"show_greyed_out\" | \"hide\",\n): EligibleProduct[] {\n const result: EligibleProduct[] = [];\n const seen = new Set<string>();\n for (const product of bundle.products) {\n if (seen.has(product.id)) continue;\n seen.add(product.id);\n const rule = ruleFor(bundle, product.id);\n // A variant is \"available for this bundle\" when it can fulfil at least\n // `rule.min` units. Stricter rules raise the bar (e.g. min=3 + only\n // 2 in stock = sold out for this bundle's purposes).\n const available = product.variants.nodes.filter((v) =>\n isVariantFulfillable(v, rule.min),\n );\n const isOos = available.length === 0;\n if (isOos && oosBehavior === \"hide\") continue;\n result.push({\n product,\n variants: product.variants.nodes,\n firstAvailableVariant: available[0] ?? null,\n isOos,\n });\n }\n return result;\n}\n\nfunction trapFocus(e: KeyboardEvent, container: HTMLElement) {\n const focusables = container.querySelectorAll<HTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n );\n if (focusables.length === 0) return;\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = (container.getRootNode() as Document | ShadowRoot)\n .activeElement;\n if (e.shiftKey && active === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && active === last) {\n e.preventDefault();\n first.focus();\n }\n}\n\nfunction sanitizeId(gid: string): string {\n return gid.replace(/[^a-zA-Z0-9_-]/g, \"-\");\n}\n","/**\n * DOM renderer for volume bundles.\n *\n * Mirrors `extensions/bundle-theme/snippets/lb-volume.liquid`. Each tier is a\n * radio-styled card; clicking one updates the pricing row and recalculates\n * the total. Add bundle dispatches the active tier's quantity for the first\n * available variant.\n */\nimport type {\n CartLineInput,\n DiscountConfig,\n VolumeBundleData,\n VolumeTier,\n} from \"@lime-bundles/core\";\nimport { isVariantFulfillable, shouldShowLowStockBadge } from \"@lime-bundles/core\";\nimport { formatCents, parseCents } from \"./pricing\";\nimport { renderCountdown } from \"./countdown\";\nimport { buildCtaButton } from \"./cta-button\";\nimport { el } from \"./dom\";\n\ninterface ResolvedTier {\n tier: VolumeTier;\n index: number;\n qty: number;\n /** Per-unit price after applying this tier's discount, in cents. */\n pricePerUnitCents: number;\n /** Pre-discount per-unit baseline in cents. */\n basePricePerUnitCents: number;\n}\n\nexport function renderVolumeBundle(\n container: HTMLElement,\n bundle: VolumeBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n) {\n const wc = bundle.widgetConfig;\n const product = bundle.products[0];\n // For volume bundles, the natural per-bundle floor is the smallest\n // tier's minQuantity — anything below that can't even buy the cheapest\n // tier. Variants that satisfy that floor are \"fulfillable\" for visibility\n // and CTA-enable purposes.\n const minTierQty = bundle.volumeTiers[0]?.minQuantity ?? 1;\n const variant = product?.variants.nodes.find((v) =>\n isVariantFulfillable(v, minTierQty),\n );\n\n // Bundle visibility guard: in \"hide\" mode, don't render the widget at\n // all if the product has no available variants. Matches Liquid behaviour.\n if (!variant && wc.outOfStockBehavior === \"hide\") return;\n\n const basePriceCents = variant ? parseCents(variant.price.amount) : 0;\n const currency = variant?.price.currencyCode ?? \"USD\";\n\n // Discount shape: bundle.discountConfig.discountType selects which field\n // on each tier carries the magnitude. \"percentage\" → tier.percentage (a\n // whole-number e.g. 10); \"fixed_amount\" → tier.amount (currency units\n // e.g. 5.00). Missing fields fall through to zero (tier renders at base\n // price — merchant config error, not a crash path).\n const discountType = bundle.discountConfig.discountType;\n\n const resolved = bundle.volumeTiers.map<ResolvedTier>((tier, index) => {\n let perUnit: number;\n if (discountType === \"fixed_amount\") {\n const amt = Math.round((tier.amount ?? 0) * 100);\n perUnit = Math.max(0, basePriceCents - amt);\n } else {\n const pct = tier.percentage ?? 0;\n const discount = Math.floor((basePriceCents * pct) / 100);\n perUnit = Math.max(0, basePriceCents - discount);\n }\n return {\n tier,\n index,\n qty: tier.minQuantity,\n pricePerUnitCents: perUnit,\n basePricePerUnitCents: basePriceCents,\n };\n });\n\n const bestTierIndex = pickBestTierIndex(resolved);\n let selectedIndex = wc.defaultTier === \"best_value\" ? bestTierIndex : 0;\n if (typeof wc.defaultTier === \"number\") {\n selectedIndex = clamp(wc.defaultTier, 0, resolved.length - 1);\n }\n\n const popularIndex =\n wc.popularBadge.tierIndex !== undefined\n ? clamp(wc.popularBadge.tierIndex, 0, resolved.length - 1)\n : bestTierIndex;\n\n const root = el(\"div\", \"lb-volume\");\n root.appendChild(\n renderHeader(bundle, resolved, selectedIndex, currency, discountType),\n );\n\n if (wc.countdown.showCountdown && bundle.endsAt) {\n const countdown = renderCountdown(bundle.endsAt);\n if (countdown) {\n root.appendChild(countdown.el);\n onCleanup?.(countdown.stop);\n }\n }\n\n const tierGroup = el(\"div\", \"lb-volume__tiers\", {\n role: \"radiogroup\",\n \"aria-label\": \"Quantity tiers\",\n \"data-tier-group\": \"\",\n });\n\n resolved.forEach((r) => {\n const tierEl = renderTierCard(\n r,\n r.index === selectedIndex,\n currency,\n wc.popularBadge.visible && r.index === popularIndex\n ? wc.popularBadge.text\n : null,\n wc.pricing.showComparePrice,\n wc.pricing.showPerUnitPrice,\n );\n tierEl.addEventListener(\"click\", () => selectTier(r.index));\n // Radiogroup keyboard contract: arrow keys move focus + selection\n // between siblings; Space/Enter activates the focused tier.\n tierEl.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n selectTier(r.index);\n return;\n }\n if (\n e.key === \"ArrowDown\" ||\n e.key === \"ArrowRight\" ||\n e.key === \"ArrowUp\" ||\n e.key === \"ArrowLeft\"\n ) {\n e.preventDefault();\n const delta =\n e.key === \"ArrowDown\" || e.key === \"ArrowRight\" ? 1 : -1;\n const next = (r.index + delta + resolved.length) % resolved.length;\n selectTier(next);\n const target = tierGroup.children[next] as HTMLElement | undefined;\n target?.focus();\n }\n });\n tierGroup.appendChild(tierEl);\n });\n\n root.appendChild(tierGroup);\n root.appendChild(el(\"div\", \"lb-bundle-divider\"));\n\n let pricingEl = renderPricingRow(\n resolved,\n selectedIndex,\n currency,\n wc.pricing.showItemCount,\n wc.pricing.showCompareAtPrice,\n );\n root.appendChild(pricingEl);\n\n let savingsBarEl: HTMLElement | null = wc.savingsBar.visible\n ? renderSavingsBar(resolved, selectedIndex, currency)\n : null;\n if (savingsBarEl) root.appendChild(savingsBarEl);\n\n const cta = renderCta(bundle, () => {\n if (!variant) return;\n const r = resolved[selectedIndex];\n if (!r) return;\n onAddToCart([\n {\n merchandiseId: variant.id,\n quantity: r.qty,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n },\n ]);\n });\n // Low-stock badge — shown when the variant has stock at or below the\n // merchant-configured threshold. Hidden when stock is unknown\n // (Storefront token without read_product_inventory) or above threshold.\n if (\n variant &&\n shouldShowLowStockBadge(\n variant,\n minTierQty,\n wc.lowStockThreshold,\n wc.showLowStockBadge,\n )\n ) {\n const lowStock = el(\"span\", \"lb-bundle-low-stock-badge\", {\n \"data-low-stock-badge\": \"\",\n });\n lowStock.textContent = `Only ${variant.quantityAvailable} left`;\n root.appendChild(lowStock);\n }\n root.appendChild(cta);\n\n root.appendChild(\n el(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n container.appendChild(root);\n\n function selectTier(idx: number) {\n if (idx === selectedIndex || idx < 0 || idx >= resolved.length) return;\n selectedIndex = idx;\n Array.from(tierGroup.children).forEach((card, i) => {\n card.setAttribute(\"aria-checked\", String(i === idx));\n (card as HTMLElement).tabIndex = i === idx ? 0 : -1;\n });\n const newPricing = renderPricingRow(\n resolved,\n idx,\n currency,\n wc.pricing.showItemCount,\n wc.pricing.showCompareAtPrice,\n );\n pricingEl.replaceWith(newPricing);\n pricingEl = newPricing;\n if (savingsBarEl) {\n const newBar = renderSavingsBar(resolved, idx, currency);\n savingsBarEl.replaceWith(newBar);\n savingsBarEl = newBar;\n }\n // Keep the header save-badge text in sync with the selected tier.\n // Hidden when showSaveBadge is off (the element doesn't exist), or\n // when the tier has no discount (badgeFor returns null).\n const badgeEl = root.querySelector<HTMLElement>(\"[data-header-badge]\");\n if (badgeEl) {\n const label = badgeFor(resolved[idx], currency, discountType);\n if (label) {\n badgeEl.textContent = label;\n badgeEl.style.display = \"\";\n } else {\n badgeEl.style.display = \"none\";\n }\n }\n }\n}\n\n// --- Section renderers ---\n\nfunction renderHeader(\n bundle: VolumeBundleData,\n resolved: ResolvedTier[],\n selectedIndex: number,\n currency: string,\n discountType: DiscountConfig[\"discountType\"],\n): HTMLElement {\n const wc = bundle.widgetConfig;\n const header = el(\"div\", \"lb-bundle-header\");\n const content = el(\"div\", \"lb-bundle-header__content\");\n const title = el(\"h3\", \"lb-bundle-title\");\n title.textContent = bundle.title;\n content.appendChild(title);\n header.appendChild(content);\n\n if (wc.pricing.showSaveBadge) {\n const badge = badgeFor(resolved[selectedIndex], currency, discountType);\n if (badge) {\n const badgeEl = el(\"span\", \"lb-bundle-header__badge\", {\n \"data-header-badge\": \"\",\n });\n badgeEl.textContent = badge;\n header.appendChild(badgeEl);\n }\n }\n return header;\n}\n\nfunction renderTierCard(\n r: ResolvedTier,\n isSelected: boolean,\n currency: string,\n popularLabel: string | null,\n showComparePrice: boolean,\n showPerUnitPrice: boolean,\n): HTMLElement {\n const tier = el(\"div\", \"lb-volume__tier\", {\n role: \"radio\",\n \"aria-checked\": String(isSelected),\n tabindex: isSelected ? \"0\" : \"-1\",\n \"data-tier-index\": String(r.index),\n \"data-tier-qty\": String(r.qty),\n });\n\n const radio = el(\"span\", \"lb-volume__radio\");\n radio.appendChild(el(\"span\", \"lb-volume__radio-dot\"));\n tier.appendChild(radio);\n\n const grid = el(\"span\", \"lb-volume__tier-grid\");\n const label = el(\"span\", \"lb-volume__tier-label\");\n label.textContent = `Buy ${r.qty}`;\n grid.appendChild(label);\n\n const price = el(\"span\", \"lb-volume__tier-price\");\n if (showComparePrice && r.pricePerUnitCents < r.basePricePerUnitCents) {\n const compare = el(\"span\", \"lb-volume__tier-compare\");\n compare.textContent = formatCents(r.basePricePerUnitCents, currency);\n price.appendChild(compare);\n }\n if (showPerUnitPrice) {\n const each = document.createElement(\"span\");\n each.setAttribute(\"data-tier-price-each\", \"\");\n each.textContent = formatCents(r.pricePerUnitCents, currency);\n price.appendChild(each);\n const unit = el(\"span\", \"lb-volume__tier-unit\");\n unit.textContent = \" each\";\n price.appendChild(unit);\n }\n grid.appendChild(price);\n tier.appendChild(grid);\n\n const badge = el(\"span\", \"lb-volume__tier-badge\");\n if (popularLabel) {\n badge.textContent = popularLabel;\n } else {\n badge.style.display = \"none\";\n }\n tier.appendChild(badge);\n\n return tier;\n}\n\nfunction renderPricingRow(\n resolved: ResolvedTier[],\n selectedIndex: number,\n currency: string,\n showItemCount: boolean,\n showCompareAtPrice: boolean,\n): HTMLElement {\n const r = resolved[selectedIndex];\n const totalCents = r ? r.pricePerUnitCents * r.qty : 0;\n const undiscountedCents = r ? r.basePricePerUnitCents * r.qty : 0;\n const savings = Math.max(0, undiscountedCents - totalCents);\n\n const row = el(\"div\", \"lb-bundle-pricing\");\n const label = el(\"span\", \"lb-bundle-pricing__label\", {\n \"data-total-label\": \"\",\n });\n label.textContent = \"Total\";\n if (showItemCount && r) {\n const count = document.createElement(\"span\");\n count.setAttribute(\"data-item-count\", \"\");\n count.textContent = ` (${r.qty} item${r.qty === 1 ? \"\" : \"s\"})`;\n label.appendChild(count);\n }\n row.appendChild(label);\n\n const prices = el(\"span\", \"lb-bundle-pricing__prices\");\n if (showCompareAtPrice && savings > 0) {\n const compare = el(\"span\", \"lb-bundle-compare-price\", {\n \"data-compare-price\": \"\",\n });\n compare.textContent = formatCents(undiscountedCents, currency);\n prices.appendChild(compare);\n }\n const sale = el(\"span\", \"lb-bundle-sale-price\", { \"data-total-price\": \"\" });\n sale.textContent = formatCents(totalCents, currency);\n prices.appendChild(sale);\n row.appendChild(prices);\n return row;\n}\n\nfunction renderSavingsBar(\n resolved: ResolvedTier[],\n selectedIndex: number,\n currency: string,\n): HTMLElement {\n const r = resolved[selectedIndex];\n const totalCents = r ? r.pricePerUnitCents * r.qty : 0;\n const undiscountedCents = r ? r.basePricePerUnitCents * r.qty : 0;\n const savings = Math.max(0, undiscountedCents - totalCents);\n\n const bar = el(\"div\", \"lb-bundle-savings-bar\", { \"data-savings-bar\": \"\" });\n if (savings <= 0) bar.style.display = \"none\";\n const labelEl = document.createElement(\"span\");\n labelEl.textContent = \"You save\";\n bar.appendChild(labelEl);\n const amount = el(\"span\", \"\", { \"data-savings-amount\": \"\" });\n amount.textContent = formatCents(savings, currency);\n bar.appendChild(amount);\n return bar;\n}\n\nfunction renderCta(\n bundle: VolumeBundleData,\n onClick: () => void,\n): HTMLElement {\n const product = bundle.products[0];\n const minTierQty = bundle.volumeTiers[0]?.minQuantity ?? 1;\n const isAvailable = product?.variants.nodes.some((v) =>\n isVariantFulfillable(v, minTierQty),\n );\n const label = isAvailable\n ? bundle.widgetConfig.cta.ctaText || \"Add to cart\"\n : \"Sold out\";\n const button = buildCtaButton(label);\n if (!isAvailable) button.disabled = true;\n button.addEventListener(\"click\", () => {\n if (button.disabled) return;\n onClick();\n });\n return button;\n}\n\n// --- Helpers ---\n\nfunction badgeFor(\n resolved: ResolvedTier | undefined,\n currency: string,\n discountType: DiscountConfig[\"discountType\"],\n): string | null {\n if (!resolved) return null;\n const { tier } = resolved;\n if (discountType === \"fixed_amount\") {\n const amount = tier.amount ?? 0;\n if (amount > 0) return `-${formatCents(Math.round(amount * 100), currency)}`;\n return null;\n }\n if (discountType === \"percentage\") {\n const pct = tier.percentage ?? 0;\n if (pct > 0) return `-${Math.round(pct)}%`;\n return null;\n }\n return null;\n}\n\nfunction pickBestTierIndex(resolved: ResolvedTier[]): number {\n let bestSavings = 0;\n let bestIndex = 0;\n resolved.forEach((r, i) => {\n const savings = r.basePricePerUnitCents - r.pricePerUnitCents;\n if (savings > bestSavings) {\n bestSavings = savings;\n bestIndex = i;\n }\n });\n return bestIndex;\n}\n\nfunction clamp(n: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, n));\n}\n\n","/**\n * Input-mode tracker — toggles `using-mouse` / `using-keyboard` classes on a\n * target element so CSS can scope focus styles by the customer's current\n * input device. Default is mouse; the first keyboard-navigation keypress\n * (Tab, arrow keys, Enter, Space, Escape, Home/End/PageUp/PageDown) flips\n * to keyboard mode, and the next pointer click flips back.\n *\n * Multiple targets share one pair of document-level listeners — installed\n * on the first `trackInputMode` call, removed when the last target is\n * released. Safe to call across every widget instance on a page without\n * stacking listeners.\n *\n * CSS shape (see bundle-base.css):\n * .using-mouse .lb-bundle-widget :focus { outline: none; }\n *\n * The Liquid theme mirrors this behaviour from `bundle-widget.js` against\n * `document.documentElement` so classic and headless storefronts render the\n * same focus rings.\n */\n\nconst NAV_KEYS = new Set([\n \"Tab\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n \"PageUp\",\n \"PageDown\",\n \"Enter\",\n \" \",\n \"Escape\",\n]);\n\nconst targets = new Set<HTMLElement>();\nlet listenersAttached = false;\n\nfunction setAll(on: \"using-mouse\" | \"using-keyboard\"): void {\n const off = on === \"using-mouse\" ? \"using-keyboard\" : \"using-mouse\";\n for (const el of targets) {\n el.classList.add(on);\n el.classList.remove(off);\n }\n}\n\nfunction onKeyDown(e: KeyboardEvent): void {\n if (NAV_KEYS.has(e.key)) setAll(\"using-keyboard\");\n}\n\nfunction onPointerDown(): void {\n setAll(\"using-mouse\");\n}\n\nfunction attachListeners(): void {\n if (listenersAttached) return;\n listenersAttached = true;\n document.addEventListener(\"keydown\", onKeyDown, true);\n document.addEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nfunction detachListeners(): void {\n if (!listenersAttached) return;\n listenersAttached = false;\n document.removeEventListener(\"keydown\", onKeyDown, true);\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nexport function trackInputMode(target: HTMLElement): () => void {\n target.classList.add(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n targets.add(target);\n attachListeners();\n\n return () => {\n targets.delete(target);\n target.classList.remove(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n if (targets.size === 0) detachListeners();\n };\n}\n","/**\n * Base + type-specific widget CSS, inlined as template literals so the web\n * component can dump them into its shadow root. Source of truth for these\n * rules is `extensions/bundle-theme/assets/*.css` — the classic Shopify\n * theme app block reads the same files. Keep the two in lockstep; the\n * bundle-css-parity.test.ts golden test enforces byte equality.\n *\n * `BUNDLE_SKELETON_CSS` (at the bottom of this file) is intentionally\n * web-component-only. The theme app block never renders a loading\n * state — its Liquid render is synchronous on the server — so the\n * skeleton styles would be dead rules there. Excluding from parity.\n */\nexport const BUNDLE_BASE_CSS = `/* Lime Bundles — shared base styles for all bundle widget types */\n\n.lb-bundle-widget.lb-bundle-widget,\n.lb-bundle-widget.lb-bundle-widget * {\n line-height: normal;\n}\n\n.lb-bundle-widget {\n /* Internal CSS-only vars (not merchant-configurable). */\n --lb-thumbnail-bg: #F0F0F0;\n --lb-widget-pad: 20px;\n --lb-progress-color: var(--lb-primary-color);\n /* Cap on the per-bundle product/slot/tier list height — keeps long\n bundles from pushing the CTA off-screen. The list scrolls\n internally with the same custom 4px scrollbar as the variant\n dropdown when content exceeds this. */\n --lb-list-max-height: 360px;\n\n font-family: inherit;\n font-size: 16px;\n background: var(--lb-bg);\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: var(--lb-widget-pad) var(--lb-widget-pad) 20px;\n box-sizing: border-box;\n /* Cap the widget at a comfortable reading width on desktop. Below\n 440px viewports the container is already narrower than the cap,\n so the rule is inert on mobile. */\n max-width: 440px;\n}\n\n/* Countdown timer bar — sits below the gradient header */\n.lb-bundle-countdown {\n margin: 0 calc(-1 * var(--lb-widget-pad)) 20px;\n padding: 12px 20px;\n background: var(--lb-countdown-bg);\n border-top: 1px solid color-mix(in srgb, var(--lb-text) 6%, transparent);\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.lb-bundle-countdown__label {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.lb-bundle-countdown__label svg {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__label span {\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__timer {\n font-family: 'SF Mono', 'Roboto Mono', ui-monospace, monospace;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n color: var(--lb-countdown-text);\n letter-spacing: 0.02em;\n}\n\n/* Hide wrapper when the inner snippet rendered nothing (product OOS / unfulfillable) */\n.lb-bundle-widget:not(:has(.lb-fixed, .lb-mix-match, .lb-volume)) {\n display: none;\n}\n\n/* Sibling widget spacing — separates multiple bundles on the same product page.\n Uses \\`~\\` (general sibling) rather than \\`+\\` (adjacent) because each Liquid\n loop iteration emits a {% style %} block before its widget div, so the\n rendered DOM alternates <style><widget><style><widget>. The \\`+\\` combinator\n requires immediate adjacency and would match nothing; \\`~\\` matches every\n widget after the first regardless of elements between. Single-widget pages\n stay unaffected (no prior \\`.lb-bundle-widget\\` sibling to match against).\n Only \\`margin-top\\` — do NOT override \\`padding-top\\` here. The gradient header\n uses \\`margin-top: calc(-1 * var(--lb-widget-pad))\\` to reach the widget's\n inner border edge, assuming padding-top == --lb-widget-pad. Changing\n padding-top on the subsequent widget breaks that math and leaves a visible\n gap above the header. */\n.lb-bundle-widget ~ .lb-bundle-widget {\n margin-top: 24px;\n}\n\n/* Header — gradient banner with title + savings badge */\n.lb-bundle-header {\n margin: calc(-1 * var(--lb-widget-pad)) calc(-1 * var(--lb-widget-pad)) 20px;\n padding: 20px 20px;\n background: var(--lb-header-bg);\n /* Match the widget's inner border curve so there's no background gap at the top corners. */\n border-radius: max(0px, calc(var(--lb-radius) - var(--lb-border-width))) max(0px, calc(var(--lb-radius) - var(--lb-border-width))) 0 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n}\n\n/* When countdown follows header, remove header bottom margin */\n.lb-bundle-header:has(+ .lb-bundle-countdown) {\n margin-bottom: 0;\n}\n\n.lb-bundle-header__content {\n flex: 1;\n min-width: 0;\n}\n\n.lb-bundle-title {\n font-size: 20px;\n font-weight: 700;\n line-height: 28px;\n letter-spacing: -0.02em;\n color: var(--lb-text);\n margin: 0;\n}\n\n.lb-bundle-header .lb-bundle-title {\n color: var(--lb-header-text);\n}\n\n.lb-bundle-subtitle {\n font-size: 16px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 4px 0 0;\n}\n\n.lb-bundle-header .lb-bundle-subtitle {\n color: var(--lb-header-text);\n opacity: 0.85;\n margin-top: 8px;\n}\n\n.lb-bundle-header:has(.lb-bundle-subtitle) {\n align-items: flex-start;\n}\n\n.lb-bundle-header__badge {\n background: var(--lb-save-badge-bg);\n color: var(--lb-save-badge-text);\n border: var(--lb-save-badge-border-width) solid var(--lb-save-badge-border-color);\n font-size: 16px;\n font-weight: 700;\n line-height: 1;\n padding: 4px 12px;\n border-radius: var(--lb-save-badge-radius);\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n/* Override Dawn's \\`div:empty { display: none }\\` reset for decorative elements */\n.lb-bundle-divider:empty,\n.lb-mix-match__progress-fill:empty {\n display: block;\n}\n\n/* Divider */\n.lb-bundle-divider {\n height: 1px;\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n margin: 16px 0;\n}\n\n/* Product rows */\n.lb-bundle-product-row {\n display: flex;\n gap: 20px;\n padding: 12px 0;\n}\n\n.lb-bundle-thumbnail {\n position: relative;\n width: 48px;\n min-width: 48px;\n /* Aspect-ratio comes from the merchant \\`thumbnailRatio\\` enum via Liquid;\n \"original\" sets it to \\`auto\\` so the box sizes to the image's intrinsic\n ratio. Default keeps the historical 1:1 behaviour. */\n aspect-ratio: var(--lb-thumbnail-aspect-ratio, 1 / 1);\n background: var(--lb-thumbnail-bg);\n border-radius: 8px;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-bundle-thumbnail img {\n width: 100%;\n /* Height + fit come from the same merchant enum; \"original\" sets them to\n \\`auto\\` / \\`contain\\` so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-thumbnail-img-height, 100%);\n object-fit: var(--lb-thumbnail-img-fit, cover);\n}\n\n.lb-bundle-thumbnail svg {\n width: 28px;\n height: 28px;\n color: color-mix(in srgb, var(--lb-text) 35%, transparent);\n}\n\n.lb-bundle-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-bundle-product-name {\n font-size: 16px;\n font-weight: 700;\n line-height: 20px;\n color: var(--lb-text);\n margin: 0;\n text-decoration: none;\n display: block;\n}\n\n.lb-bundle-product-name:hover {\n text-decoration: underline;\n}\n\na.lb-bundle-product-name:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n border-radius: 2px;\n}\n\n.lb-bundle-product-price {\n /* --lb-product-price-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-price-display, block);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: var(--lb-text);\n}\n\n.lb-bundle-product-prices {\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 2px;\n}\n\n.lb-bundle-product-compare-price {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n}\n\n/* Setting \\`display\\` above outranks the UA [hidden] rule — restore it so rows\n without a compare-at price don't leave a phantom flex item + gap. */\n.lb-bundle-product-compare-price[hidden] {\n display: none;\n}\n\n/* Unit price (e.g. \"$0.50/100ml\") — only rendered when the merchant has\n configured unit pricing on the variant in the Shopify admin. No merchant\n toggle: present in admin → shown; absent → hidden. Styled as muted\n secondary text beneath the price row so it doesn't compete visually. */\n.lb-bundle-product-unit-price {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-top: 2px;\n}\n\n.lb-bundle-product-unit-price[hidden] {\n display: none;\n}\n\n/* Read-only variant text shown below the product title when only one\n variant is in scope (single-variant product, or merchant pinned a\n single variant). Same visual treatment as the mix-match filled\n slot's variant text — see .lb-mix-match__filled-variant in\n bundle-mix-match.css. Both share this rule via the comma selector\n so the storefront UX stays consistent across bundle types. */\n.lb-bundle-variant-badge,\n.lb-mix-match__slot--filled .lb-mix-match__filled-variant {\n display: block;\n font-size: 12px;\n line-height: 20px;\n margin-top: 2px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n/* Per-option variant pickers. Each option's label + select sit inside a\n .lb-bundle-variant-option-group (flex column, 2px gap between label and\n select); the groups stack inside a .lb-bundle-variant-option-groups\n parent (flex column, 12px gap between groups). The parent owns the top\n offset from the preceding unit-price line, so individual labels and\n selects don't carry their own vertical margins. */\n.lb-bundle-variant-option-groups {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.lb-bundle-variant-option-group {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.lb-bundle-variant-option-label {\n display: block;\n margin: 0;\n font-size: 12px;\n line-height: 16px;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n margin-top: 8px;\n}\n\n/* Desktop: lay variant option groups in a wrapping row inside the modal\n (Size + Color side-by-side). Mobile keeps the column stack from the\n base rule above. The 768px breakpoint mirrors bundle-mix-match.css. */\n@media (min-width: 768px) {\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n flex-direction: row;\n flex-wrap: wrap;\n }\n /* Direct children only — the qty-stepper-group is also a\n .lb-bundle-variant-option-group but lives in the actions wrapper\n and must keep its natural width. */\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups > .lb-bundle-variant-option-group {\n flex: 1;\n }\n}\n\n/* Focus-ring suppression for mouse users. A small JS helper — see\n packages/widget/src/utils/input-mode.ts and bundle-widget.js — toggles\n .using-mouse / .using-keyboard on the widget root (or html in the Liquid\n path) based on the customer's current input device. Default is mouse, so\n click-to-focus doesn't leave a keyboard-style ring. The modal overlay\n gets its own selector because the Liquid path reparents it to body,\n outside the widget root. */\n.using-mouse .lb-bundle-widget :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-bundle-widget :focus-visible:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus-visible:not([aria-checked=\"true\"]) {\n outline: none;\n outline-offset: 0;\n box-shadow: none;\n}\n\n.lb-bundle-quantity {\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-left: 8px;\n white-space: nowrap;\n}\n\n/* Pricing row — label left, prices right */\n.lb-bundle-pricing {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n padding: 4px 0 8px;\n gap: 12px;\n}\n\n.lb-bundle-pricing__label {\n font-size: 16px;\n font-weight: 400;\n line-height: 20px;\n color: var(--lb-text);\n white-space: nowrap;\n}\n\n.lb-bundle-pricing__prices {\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.lb-bundle-sale-price {\n font-size: 20px;\n font-weight: 700;\n line-height: 1;\n letter-spacing: -0.02em;\n color: var(--lb-text);\n}\n\n.lb-bundle-compare-price {\n font-size: 16px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n}\n\n/* Savings bar — green banner below pricing */\n.lb-bundle-savings-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--lb-savings-bar-bg);\n color: var(--lb-savings-bar-text);\n border: var(--lb-savings-bar-border-width) solid var(--lb-savings-bar-border-color);\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n padding: 8px 12px;\n border-radius: var(--lb-savings-bar-radius);\n margin-bottom: 12px;\n}\n\n/* Quantity badge — overlay on thumbnail top-right */\n.lb-bundle-qty-badge.lb-bundle-qty-badge {\n position: absolute;\n top: -8px;\n right: -8px;\n /* --lb-qty-badge-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-qty-badge-display, flex);\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--lb-qty-badge-bg);\n border: var(--lb-image-border-width) solid var(--lb-image-border-color);\n color: var(--lb-qty-badge-color);\n font-size: 12px;\n font-weight: 700;\n line-height: 0;\n text-align: center;\n z-index: 1;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n\n/* Override Dawn's \\`div:empty\\` for savings bar when hidden */\n.lb-bundle-savings-bar:empty {\n display: none;\n}\n\n/* CTA button.\n * Label and spinner share a single 1×1 grid cell so the button's intrinsic\n * width/height stays fixed when swapping between them — no layout shift when\n * entering the loading state. Visibility (not display) is used so the hidden\n * child still contributes to the cell's min-content sizing. See the\n * [data-loading=\"true\"] rules below. */\n.lb-bundle-cta {\n display: grid;\n grid-template-rows: 1fr;\n grid-template-columns: 1fr;\n width: 100%;\n padding: 12px 16px;\n border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);\n border-radius: var(--lb-cta-radius);\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n cursor: pointer;\n text-align: center;\n transition: opacity 0.15s ease;\n font-family: inherit;\n}\n\n.lb-bundle-cta:not(:disabled) {\n background: var(--lb-primary-color);\n color: var(--lb-btn-text);\n}\n\n.lb-bundle-cta:not(:disabled):hover {\n opacity: 0.9;\n}\n\n.lb-bundle-cta:disabled {\n background: color-mix(in srgb, var(--lb-primary-color) 35%, var(--lb-bg));\n color: color-mix(in srgb, var(--lb-btn-text) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Both the label and the spinner occupy grid cell (1, 1). Only one is\n * visible at a time; the other keeps its box for sizing but is invisible. */\n.lb-cta-label,\n.lb-cta-spinner {\n grid-row: 1;\n grid-column: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n}\n\n.lb-cta-spinner {\n visibility: hidden;\n}\n\n.lb-cta-spinner svg {\n width: 20px;\n height: 20px;\n animation: lb-cta-spin 0.8s linear infinite;\n}\n\n@keyframes lb-cta-spin {\n to { transform: rotate(360deg); }\n}\n\n.lb-bundle-cta[data-loading=\"true\"] {\n cursor: wait;\n pointer-events: none;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-label {\n visibility: hidden;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-spinner {\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-cta-spinner svg {\n animation-duration: 2.5s;\n }\n}\n\n/* Error message */\n.lb-bundle-error {\n font-size: 16px;\n color: #D72C0D;\n margin-top: 8px;\n display: none;\n}\n\n.lb-bundle-error[data-visible=\"true\"] {\n display: block;\n}\n\n/* Visually hidden — accessible to screen readers only */\n.lb-visually-hidden {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip-path: inset(50%) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n/* Placeholder SVG icon for missing images */\n.lb-bundle-placeholder-icon {\n width: 28px;\n height: 28px;\n stroke: color-mix(in srgb, var(--lb-text) 35%, transparent);\n stroke-width: 1.5;\n fill: none;\n}\n\n/* Out-of-stock product row */\n.lb-bundle-product-row--oos {\n opacity: 0.5;\n}\n\n.lb-bundle-oos-label {\n font-size: 12px;\n font-weight: 500;\n color: #D72C0D;\n white-space: nowrap;\n margin-left: auto;\n}\n\n/* \"Only X left\" low-stock badge — appears alongside product/variant\n info when ProductVariant.quantityAvailable falls at or below\n wc.lowStockThreshold. Hidden when quantityAvailable is unknown\n (Storefront token without read_product_inventory). */\n.lb-bundle-low-stock-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n font-size: 11px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--lb-low-stock-text);\n background-color: var(--lb-low-stock-bg);\n border-radius: 4px;\n white-space: nowrap;\n}\n\n/* A/B test: hide save badge until JS swaps the label (prevents flash of default) */\n.lb-ab-pending {\n visibility: hidden;\n}\n`;\nexport const BUNDLE_FIXED_CSS = `/* Lime Bundles — Fixed bundle styles */\n\n.lb-fixed__products {\n display: flex;\n flex-direction: column;\n gap: 0;\n margin: 0;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Custom scrollbar — text colour at 15% opacity (thumb) and 5% (track). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-fixed__products::-webkit-scrollbar { width: 4px; }\n.lb-fixed__products::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-fixed__products::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n/* Fixed bundles: product rows */\n.lb-fixed .lb-bundle-product-row {\n gap: 20px;\n align-items: center;\n}\n\n/* Fixed bundles: larger thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */\n.lb-fixed .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-image-border-radius);\n border: var(--lb-image-border-width) solid var(--lb-image-border-color);\n box-sizing: border-box;\n overflow: visible;\n}\n\n.lb-fixed .lb-bundle-thumbnail img {\n border-radius: max(0px, calc(var(--lb-image-border-radius) - var(--lb-image-border-width)));\n border: none;\n}\n\n/* Variant picker select — styled to match the variant badge aesthetic.\n Sits inside .lb-bundle-variant-option-group so vertical spacing is owned\n by the group/groups flex gap, not the select itself. */\n.lb-bundle-variant-select {\n display: inline-block;\n border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);\n border-radius: var(--lb-variant-radius);\n padding: 8px 32px 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n background-image: var(--lb-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n width: 50%;\n max-width: 50%;\n}\n\n.lb-bundle-variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n`;\nexport const BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles — Mix & Match styles */\n\n/* === Slot list ============================================================\n Caps the height of the slot stack so long bundles don't push the CTA off\n the page. Internal scroll with the same custom 4px scrollbar as the\n variant dropdown panel. */\n.lb-mix-match__slots {\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-mix-match__slots::-webkit-scrollbar { width: 4px; }\n.lb-mix-match__slots::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-mix-match__slots::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n/* === Progress Bar === */\n.lb-mix-match__progress {\n margin-bottom: 16px;\n}\n\n.lb-mix-match__progress-labels {\n display: flex;\n justify-content: space-between;\n margin-bottom: 8px;\n}\n\n.lb-mix-match__progress-count {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__progress-remaining {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: var(--lb-text);\n}\n\n.lb-mix-match__progress-track {\n width: 100%;\n height: 4px;\n background: color-mix(in srgb, var(--lb-text) 10%, transparent);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.lb-mix-match__progress-fill {\n height: 100%;\n background: var(--lb-text);\n border-radius: 4px;\n transition: width 0.3s ease;\n}\n\n/* === Slots === */\n.lb-mix-match__slot {\n cursor: pointer;\n}\n\n.lb-mix-match .lb-bundle-product-row {\n align-items: center;\n}\n\n.lb-mix-match__slot--empty:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n border-radius: 8px;\n}\n\n.lb-mix-match__slot--empty .lb-mix-match__empty-thumb {\n width: 60px;\n height: 60px;\n min-width: 60px;\n /* 2px border is intentionally independent of --lb-image-border-width —\n empty slots always need a visible dashed outline as an affordance,\n regardless of how the merchant has styled populated thumbnails. */\n border: 2px dashed color-mix(in srgb, var(--lb-text) 35%, transparent);\n border-radius: var(--lb-image-border-radius);\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n color: color-mix(in srgb, var(--lb-text) 35%, transparent);\n}\n\n.lb-mix-match__empty-text {\n font-size: 16px;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n}\n\n/* Filled slot */\n.lb-mix-match__slot--filled {\n cursor: default;\n}\n\n/* Mix-match thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */\n.lb-mix-match .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-image-border-radius);\n border: var(--lb-image-border-width) solid var(--lb-image-border-color);\n box-sizing: border-box;\n overflow: visible;\n}\n\n.lb-mix-match .lb-bundle-thumbnail img {\n border-radius: max(0px, calc(var(--lb-image-border-radius) - var(--lb-image-border-width)));\n border: none;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-title {\n font-size: 16px;\n font-weight: 700;\n line-height: 20px;\n color: var(--lb-text);\n text-decoration: none;\n overflow-wrap: break-word;\n}\n\n.lb-mix-match__slot--filled a.lb-mix-match__filled-title:hover {\n text-decoration: underline;\n}\n\n.lb-mix-match__slot--filled a.lb-mix-match__filled-title:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n border-radius: 2px;\n}\n\n/* .lb-mix-match__filled-variant — styled jointly with\n .lb-bundle-variant-badge above to keep the variant text consistent\n across mix-match and fixed bundle widgets. */\n\n.lb-mix-match__filled-price {\n /* --lb-product-price-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-price-display, block);\n font-size: 12px;\n line-height: 20px;\n margin-top: 4px;\n color: var(--lb-text);\n font-weight: 500;\n}\n\n.lb-mix-match__filled-compare {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 12px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n margin-right: 4px;\n}\n\n.lb-mix-match__slot-remove {\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-text);\n padding: 0;\n margin-left: auto;\n border-radius: 8px;\n}\n\n.lb-mix-match__slot-remove:hover {\n color: var(--lb-text);\n}\n\n.lb-mix-match__slot-remove:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n/* === Modal Overlay === */\n.lb-mix-match__modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 9999;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s ease-out;\n}\n\n.lb-mix-match__modal-overlay--open {\n opacity: 1;\n}\n\n/* === Modal Panel === */\n.lb-mix-match__modal {\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n border-radius: var(--lb-picker-radius);\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n box-sizing: border-box;\n width: 100%;\n max-width: 520px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16);\n transform: translateY(24px);\n transition: transform 0.25s ease-out;\n will-change: transform;\n}\n\n.lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n}\n\n/* === Modal Header === */\n.lb-mix-match__modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 20px 20px 12px;\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-title {\n font-size: 20px;\n font-weight: 600;\n line-height: 24px;\n margin: 0;\n color: inherit;\n}\n\n.lb-mix-match__modal-close {\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-picker-text);\n border-radius: 8px;\n padding: 0;\n margin: -12px -12px -12px 0;\n}\n\n.lb-mix-match__modal-close:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n}\n\n/* === Modal Search === */\n.lb-mix-match__modal-search {\n padding: 0 20px 12px;\n position: relative;\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-search-input {\n width: 100%;\n padding: 12px 40px 12px 16px;\n border: var(--lb-picker-search-border-width) solid var(--lb-picker-search-border-color);\n border-radius: var(--lb-picker-search-radius);\n font-size: 16px;\n line-height: 20px;\n color: var(--lb-picker-text);\n background: var(--lb-picker-bg);\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.lb-mix-match__modal-search-input::placeholder {\n color: color-mix(in srgb, var(--lb-picker-text) 50%, transparent);\n}\n\n.lb-mix-match__modal-search-input:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-search-clear {\n position: absolute;\n right: 32px;\n /* Anchor to the input area only — parent has padding-bottom: 12px which would\n otherwise push a top:50% center down by 6px. */\n top: 0;\n bottom: 12px;\n min-width: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-picker-text);\n padding: 0;\n}\n\n/* === Modal Product List === */\n.lb-mix-match__modal-list {\n overflow-y: auto;\n flex: 1;\n padding: 0 20px;\n -webkit-overflow-scrolling: touch;\n}\n\n.lb-mix-match__modal-product {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: start;\n column-gap: 20px;\n padding-top: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid color-mix(in srgb, var(--lb-picker-text) 7%, transparent);\n}\n\n.lb-mix-match__modal-product:last-child {\n border-bottom: none;\n}\n\n.lb-mix-match__modal-product-thumb {\n position: relative;\n width: 60px;\n min-width: 60px;\n /* Modal picker thumbs follow the merchant's pickerThumbnailRatio —\n independent from the main widget's thumbnailRatio so a merchant can\n e.g. show tall picker thumbs with square main thumbs. */\n aspect-ratio: var(--lb-picker-thumbnail-aspect-ratio, 1 / 1);\n border-radius: var(--lb-picker-product-radius);\n border: var(--lb-picker-product-border-width) solid var(--lb-picker-product-border-color);\n box-sizing: border-box;\n overflow: visible;\n background: var(--lb-thumbnail-bg);\n}\n\n/* Qty badge inside the picker modal inherits picker-product border (width + color) plus\n inverted picker bg/text for clear contrast against the modal — always stays round\n (the badge shape is independent of the thumbnail shape). */\n.lb-mix-match__modal-product-thumb .lb-bundle-qty-badge.lb-bundle-qty-badge {\n /* --lb-picker-qty-badge-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-picker-qty-badge-display, flex);\n background: var(--lb-picker-qty-badge-bg);\n color: var(--lb-picker-qty-badge-color);\n border: var(--lb-picker-product-border-width) solid var(--lb-picker-product-border-color);\n}\n\n.lb-mix-match__modal-product-thumb img {\n width: 100%;\n /* Height + fit come from pickerThumbnailRatio — \"original\" sets both\n to auto/contain so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-picker-thumbnail-img-height, 100%);\n object-fit: var(--lb-picker-thumbnail-img-fit, cover);\n border-radius: max(0px, calc(var(--lb-picker-product-radius) - var(--lb-picker-product-border-width)));\n}\n\n.lb-mix-match__modal-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-mix-match__modal-product-title {\n font-size: 16px;\n font-weight: 700;\n line-height: 20px;\n color: inherit;\n margin: 0;\n}\n\n.lb-mix-match__modal-product-price {\n font-size: 12px;\n line-height: 20px;\n color: inherit;\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-unit-price {\n font-size: 11px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-unit-price[hidden] {\n display: none;\n}\n\n.lb-mix-match__variant-select {\n font-size: 12px;\n padding: 4px 24px 4px 8px;\n border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);\n border-radius: var(--lb-picker-variant-radius);\n background-color: var(--lb-picker-bg);\n background-image: var(--lb-picker-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 12px;\n color: var(--lb-picker-text);\n font-family: inherit;\n min-height: 32px;\n cursor: pointer;\n width: 50%;\n max-width: 50%;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.lb-mix-match__variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Per-pick quantity stepper inside the picker modal product row.\n Three flex cells (− / number / +) sharing a single outer border —\n driven by its own --lb-picker-qty-stepper-* variables so merchants\n can style the stepper independently of the variant dropdown. The\n cell dividers come from a 1px border on the centre value rather\n than per-button borders, so the rounded outer corners stay clean.\n Mirrors extensions/bundle-theme/assets/bundle-mix-match.css so\n the headless web component matches the Liquid theme widget. */\n/* Quantity label + stepper sit together in a .lb-bundle-variant-option-group\n wrapper so the label-to-stepper gap inherits the same 2px the variant\n pickers use. The wrapper carries the margin-top that spaces the qty\n group from the unit-price / low-stock-badge above; the stepper itself\n has no top margin so the group can be repositioned without coupling. */\n.lb-mix-match__qty-stepper-group {\n /* Override the base .lb-bundle-variant-option-group column flex so the\n stepper child stretches on the cross-axis (vertical) — this is what\n lets the group track the Add button's height in the action row\n without pinning a fixed pixel value. */\n flex-direction: row;\n width: fit-content;\n}\n\n.lb-mix-match__qty-stepper {\n display: inline-flex;\n align-items: stretch;\n flex-shrink: 0;\n border: var(--lb-picker-qty-stepper-border-width) solid var(--lb-picker-qty-stepper-border-color);\n border-radius: var(--lb-picker-qty-stepper-radius);\n background-color: var(--lb-picker-bg);\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.lb-mix-match__qty-stepper-button {\n appearance: none;\n -webkit-appearance: none;\n background: transparent;\n border: none;\n margin: 0;\n padding: 0;\n width: 28px;\n font-family: inherit;\n font-size: 16px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-picker-text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-mix-match__qty-stepper-button:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n.lb-mix-match__qty-stepper-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.lb-mix-match__qty-stepper-value {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 36px;\n padding: 0 8px;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n color: var(--lb-picker-text);\n box-sizing: border-box;\n}\n\n/* Action row — qty stepper (left) + Add button (right). Sits at the\n bottom of the info column with a 12px top margin so it visually\n separates from the variant pickers / price block above. When the\n stepper is hidden, Add fills the row via flex: 1 below.\n align-items: stretch so the stepper-group tracks the Add button's\n height (driven by font size + button padding) without a pinned px. */\n.lb-mix-match__modal-product-actions {\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n row-gap: 12px;\n column-gap: 8px;\n margin-top: 12px;\n}\n\n.lb-mix-match__modal-add {\n flex: 1;\n padding: 10px 20px;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);\n border-radius: var(--lb-picker-add-radius);\n box-sizing: border-box;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.lb-mix-match__modal-add:hover {\n opacity: 0.9;\n}\n\n.lb-mix-match__modal-add:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-add:disabled {\n background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));\n color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Sold out product row */\n.lb-mix-match__modal-product--sold-out {\n opacity: 0.5;\n}\n\n/* Already in a slot — prevents shoppers from filling multiple slots\n with the same product (which wouldn't qualify the discount: it\n counts distinct products, not slot occupancy). The thumb keeps\n its qty badge so the shopper sees what's already in the bundle. */\n.lb-mix-match__modal-product--in-bundle {\n opacity: 0.5;\n}\n\n.lb-mix-match__modal-product--sold-out .lb-mix-match__modal-sold-out-label {\n font-size: 12px;\n color: inherit;\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* === Modal Empty State === */\n.lb-mix-match__modal-empty {\n padding: 32px 20px;\n text-align: center;\n}\n\n.lb-mix-match__modal-empty p {\n margin: 0;\n font-size: 16px;\n color: var(--lb-picker-text);\n}\n\n/* Hidden utility for search filtering */\n.lb-hidden {\n display: none !important;\n}\n\n/* === Mobile Full-Screen Modal === */\n@media (max-width: 767px) {\n .lb-mix-match__modal-overlay {\n align-items: flex-end;\n }\n\n .lb-mix-match__modal {\n max-width: 100%;\n max-height: 90vh;\n border-radius: 16px 16px 0 0;\n transform: translateY(100%);\n }\n\n .lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n }\n\n .lb-mix-match__variant-select {\n width: 80%;\n max-width: 80%;\n }\n}\n\n/* === Reduced Motion === */\n@media (prefers-reduced-motion: reduce) {\n .lb-mix-match__modal-overlay,\n .lb-mix-match__modal,\n .lb-mix-match__progress-fill {\n transition: none;\n }\n}\n`;\nexport const BUNDLE_VOLUME_CSS = `/* Lime Bundles — Volume / Quantity Breaks styles */\n\n.lb-volume__tiers {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Custom scrollbar — text colour at 15% opacity (thumb) and 5% (track). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-volume__tiers::-webkit-scrollbar { width: 4px; }\n.lb-volume__tiers::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-volume__tiers::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n.lb-volume__tier {\n display: flex;\n align-items: center;\n gap: 12px;\n border: var(--lb-tier-border-width) solid var(--lb-tier-border-color);\n border-radius: var(--lb-tier-radius);\n padding: 12px 16px;\n cursor: pointer;\n position: relative;\n transition: border-color 0.15s ease;\n}\n\n.lb-volume__tier:hover {\n border-color: var(--lb-tier-selected-border-color);\n}\n\n/* Suppress the UA default focus outline + Dawn's focus shadow so a\n freshly-clicked selected tier doesn't briefly show the 1px focus ring\n or shadow on top of (or in place of) the custom selected-state\n outline below. Keyboard focus is still indicated by the\n :focus-visible rule. */\n.lb-volume__tier:focus {\n outline: none;\n box-shadow: none;\n}\n\n/* Keyboard focus indicator — explicitly excludes the selected tier so\n the selected-state outline rule below has full ownership of the\n outline property when both states apply at once. */\n.lb-volume__tier:focus-visible:not([aria-checked=\"true\"]) {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] {\n border-color: var(--lb-tier-selected-border-color);\n outline: var(--lb-tier-selected-border-width) solid var(--lb-tier-selected-border-color);\n outline-offset: calc(-1 * var(--lb-tier-selected-border-width));\n}\n\n.lb-volume__radio {\n width: 20px;\n height: 20px;\n min-width: 20px;\n border: 2px solid var(--lb-text);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: border-color 0.15s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio {\n border-color: var(--lb-text);\n}\n\n.lb-volume__radio-dot {\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: transparent;\n transition: background 0.15s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio-dot {\n background: var(--lb-text);\n}\n\n/* Tier content: 2x2 grid layout */\n.lb-volume__tier-grid {\n flex: 1;\n display: grid;\n row-gap: 4px;\n align-items: center;\n}\n\n.lb-volume__tier-label {\n font-size: 16px;\n font-weight: 700;\n color: var(--lb-text);\n}\n\n.lb-volume__tier-badge {\n flex-shrink: 0;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-popular-badge-text);\n background: var(--lb-popular-badge-bg);\n border: var(--lb-popular-badge-border-width) solid var(--lb-popular-badge-border-color);\n border-radius: var(--lb-popular-badge-radius);\n padding: 4px 8px;\n}\n\n.lb-volume__tier-price {\n grid-column: 1 / -1;\n font-size: 14px;\n font-weight: 500;\n color: var(--lb-text);\n}\n\n.lb-volume__tier-unit {\n font-size: 12px;\n color: var(--lb-text);\n}\n\n/* Compare-at (strikethrough) price */\n.lb-volume__tier-compare {\n font-size: 14px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n margin-right: 4px;\n}\n\n\n`;\n\nexport const BUNDLE_DROPDOWN_CSS = `/**\n * Lime Bundles — Custom variant-picker dropdown styling.\n *\n * Reuses existing CSS variables: no new merchant-configurable surface.\n * --lb-variant-border-{width,color}, --lb-variant-radius, --lb-variant-chevron\n * --lb-bg, --lb-text, --lb-primary-color\n *\n * Mix-match modal context overrides via .lb-mix-match__modal scope to use\n * --lb-picker-variant-* and --lb-picker-bg.\n */\n\n/* Hide the native <select> while keeping it form-serializable and focusable\n programmatically. The .lb-dropdown-state marker is added by JS at bind\n time, so this rule matches every variant-select class (main widget,\n mix-match modal, future bundle types). aria-hidden + tabindex=-1\n (also set in JS) remove it from the accessibility tree. */\n.lb-dropdown-state {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0 0 0 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n pointer-events: none !important;\n}\n\n/* Shell fills its parent column. */\n.lb-dropdown {\n position: relative;\n display: inline-block;\n width: 100%;\n max-width: 100%;\n font-family: inherit;\n}\n\n/* Trigger styled identically to the closed-state native select */\n.lb-dropdown-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);\n border-radius: var(--lb-variant-radius);\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n text-align: start;\n transition: border-color 120ms ease;\n}\n\n.lb-dropdown-trigger:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] {\n border-color: var(--lb-text);\n}\n\n.lb-dropdown-trigger-value {\n flex: 1 1 auto;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n text-align: start;\n}\n\n.lb-dropdown-chevron {\n flex: 0 0 auto;\n width: 12px;\n height: 12px;\n background: var(--lb-variant-chevron) center / contain no-repeat;\n transition: transform 120ms ease;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] .lb-dropdown-chevron {\n transform: rotate(180deg);\n}\n\n/* Popover panel — position: absolute against the .lb-dropdown shell\n (already position: relative). Top/left/width come from CSS so we\n never depend on JS having set inline coords by the time the panel\n becomes visible. JS only sets max-height. */\n.lb-dropdown-listbox {\n position: absolute;\n left: 0;\n /* Default to below-trigger placement so the panel doesn't overlap the\n trigger if data-placement is missing for any reason. The explicit\n [data-placement=\"down\"|\"up\"] rules below override this. */\n top: calc(100% + 4px);\n width: 100%;\n z-index: 9999;\n margin: 0;\n padding: 4px 0;\n list-style: none;\n background: var(--lb-bg);\n color: var(--lb-text);\n border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);\n border-radius: var(--lb-variant-radius);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);\n overflow-y: auto;\n overflow-x: hidden;\n /* Custom scrollbar — text colour at 15% opacity (thumb) and 5% (track). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n animation: lb-dropdown-in-down 120ms ease-out;\n transform-origin: top center;\n}\n\n.lb-dropdown-listbox[data-placement=\"down\"] {\n top: calc(100% + 4px);\n}\n\n.lb-dropdown-listbox[data-placement=\"up\"] {\n top: auto;\n bottom: calc(100% + 4px);\n animation-name: lb-dropdown-in-up;\n transform-origin: bottom center;\n}\n\n/* When portaled out of the .lb-dropdown shell (mix-match modal context:\n .lb-mix-match__modal applies translateY which would otherwise trap\n position:fixed), switch to fixed and let JS set viewport coords. */\n.lb-dropdown-listbox[data-lb-dropdown-portal] {\n position: fixed;\n top: auto;\n left: auto;\n bottom: auto;\n width: auto;\n}\n\n/* Custom scrollbar — Webkit/Blink: exact 4px width */\n.lb-dropdown-listbox::-webkit-scrollbar {\n width: 4px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n/* Options */\n.lb-dropdown-option {\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--lb-text);\n}\n\n.lb-dropdown-option[aria-selected=\"true\"] {\n font-weight: 600;\n}\n\n.lb-dropdown-option.is-active,\n.lb-dropdown-option:hover:not([aria-disabled=\"true\"]) {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-dropdown-option[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n/* Animations */\n@keyframes lb-dropdown-in-down {\n from { opacity: 0; transform: translateY(-4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@keyframes lb-dropdown-in-up {\n from { opacity: 0; transform: translateY(4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-dropdown-listbox { animation: none; }\n .lb-dropdown-chevron { transition: none; }\n .lb-dropdown-trigger { transition: none; }\n}\n\n/* Forced-colors mode (Windows high-contrast) */\n@media (forced-colors: active) {\n .lb-dropdown-trigger {\n border-color: ButtonBorder;\n color: ButtonText;\n background: ButtonFace;\n }\n .lb-dropdown-listbox {\n border-color: ButtonBorder;\n background: Canvas;\n color: CanvasText;\n }\n .lb-dropdown-option.is-active {\n background: Highlight;\n color: HighlightText;\n }\n}\n\n/* Mix-match modal context — use picker-scoped variables.\n No CSS fallbacks: --lb-picker-* are always emitted by bundle-widget.liquid\n because WidgetConfig.parse() fully hydrates the merchant config.\n See docs/solutions/ui-bugs/widget-css-single-source-defaults.md. */\n.lb-mix-match__modal .lb-dropdown-trigger {\n border-color: var(--lb-picker-variant-border-color);\n border-width: var(--lb-picker-variant-border-width);\n border-radius: var(--lb-picker-variant-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n}\n\n.lb-mix-match__modal .lb-dropdown-chevron {\n background-image: var(--lb-picker-variant-chevron);\n}\n\n/* Listbox is portaled out of the transformed .lb-mix-match__modal up to\n its [data-modal-overlay] parent, so picker-scoped rules anchor on the\n overlay attribute, not the modal class. */\n[data-modal-overlay] > .lb-dropdown-listbox {\n border-color: var(--lb-picker-variant-border-color);\n border-width: var(--lb-picker-variant-border-width);\n border-radius: var(--lb-picker-variant-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n scrollbar-color: color-mix(in srgb, var(--lb-picker-text) 15%, transparent)\n color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n}\n\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n border-radius: 2px;\n}\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n}\n`;\n\n/**\n * Skeleton styles for the web component's loading state. Rendered\n * synchronously in connectedCallback → renderLoading() so the host\n * element has intrinsic size from render-0 and doesn't shift layout\n * when the real bundle paints. Web-component only (see file header).\n */\nexport const BUNDLE_SKELETON_CSS = `/* Lime Bundles — web-component loading skeleton (not mirrored to theme assets) */\n\n.lb-bundle-widget--loading {\n display: block;\n padding: var(--lb-widget-pad, 20px);\n border: 1px solid var(--lb-border, #E5E5E5);\n border-radius: var(--lb-radius, 12px);\n background: var(--lb-bg, #FFFFFF);\n /* Contain layout/paint so the skeleton doesn't influence ancestor\n layout once the real content swaps in. */\n contain: layout paint;\n}\n\n.lb-bundle-widget--loading .lb-skeleton {\n background: linear-gradient(\n 90deg,\n rgba(0, 0, 0, 0.06) 0%,\n rgba(0, 0, 0, 0.10) 50%,\n rgba(0, 0, 0, 0.06) 100%\n );\n background-size: 200% 100%;\n border-radius: 6px;\n animation: lb-skeleton-pulse 1.4s ease-in-out infinite;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--title {\n height: 28px;\n width: 60%;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--products {\n display: grid;\n gap: 12px;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--row {\n display: grid;\n grid-template-columns: 56px 1fr 60px;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--thumb {\n height: 56px;\n width: 56px;\n border-radius: 8px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line {\n height: 14px;\n border-radius: 4px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line + .lb-skeleton--line {\n margin-top: 8px;\n width: 70%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--price {\n height: 20px;\n width: 60px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--footer {\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid var(--lb-border, #E5E5E5);\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--total {\n height: 24px;\n width: 40%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--cta {\n height: 44px;\n width: 140px;\n border-radius: var(--lb-cta-radius, 8px);\n}\n\n@keyframes lb-skeleton-pulse {\n 0% { background-position: 0% 50%; }\n 100% { background-position: -200% 50%; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-bundle-widget--loading .lb-skeleton {\n animation: none;\n }\n}\n`;\n\n","/**\n * <lime-bundle> Web Component — renders Lime Bundles on any storefront.\n *\n * ## The two modes\n *\n * Single-bundle (pinned):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * bundle-gid=\"gid://shopify/Metaobject/42\"\n * ></lime-bundle>\n *\n * Product-aware (matches classic Liquid theme block behaviour — one snippet\n * on the product page renders every active bundle configured against that\n * product):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * ></lime-bundle>\n *\n * Product resolution cascade (when no `bundle-gid` is set):\n * 1. explicit `product-handle` attribute\n * 2. <meta name=\"shopify:product-handle\" content=\"...\"> on the page\n * 3. /products/<handle> segment of window.location.pathname\n * 4. fallthrough: renders nothing, fires `lime-bundle:error`\n *\n * ## Add-to-cart behaviour\n *\n * Merchants who do nothing get a default: the widget calls Shopify's\n * Storefront Cart API (tokenless — no extra scopes required) and redirects\n * the browser to the returned checkoutUrl. One-click-to-checkout is the\n * right UX for most merchants pasting the widget into Webflow / Wix /\n * Squarespace / static HTML.\n *\n * Merchants with their own cart state (Hydrogen's useCart, a custom cart\n * drawer, etc.) opt out by attaching a listener that calls\n * `event.preventDefault()`:\n *\n * document.querySelector(\"lime-bundle\").addEventListener(\n * \"lime-bundle:add-to-cart\",\n * (ev) => {\n * ev.preventDefault(); // suppress the default redirect\n * myCart.linesAdd(ev.detail.lines);\n * },\n * );\n *\n * The event is always dispatched; only the default action is conditional.\n */\nimport {\n createStorefrontClient,\n BUNDLE_METAOBJECT_QUERY,\n BUNDLES_FOR_PRODUCT_QUERY,\n CART_CREATE_MUTATION,\n CART_LINES_ADD_MUTATION,\n SHOP_CUSTOM_CSS_QUERY,\n parseMetaobjectBundle,\n observeImpression,\n reportImpression,\n reportAddToCart,\n injectCustomCss,\n sanitizeCustomCss,\n getABTestAssignment,\n applyABVariantB,\n type ParsedBundle,\n type BundleMetaobjectResponse,\n type BundlesForProductResponse,\n type CartCreateResponse,\n type CartLinesAddResponse,\n type ShopCustomCssResponse,\n type CartLineInput,\n type StorefrontClient,\n type BuyerResolver,\n hasInContext,\n withInContext,\n} from \"@lime-bundles/core\";\nimport { renderFixedBundle } from \"./renderers/fixed\";\nimport { renderMixMatchBundle } from \"./renderers/mix-match\";\nimport { renderVolumeBundle } from \"./renderers/volume\";\nimport { applyWidgetConfigVars } from \"@lime-bundles/core\";\nimport { trackInputMode } from \"./utils/input-mode\";\nimport {\n BUNDLE_BASE_CSS,\n BUNDLE_DROPDOWN_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_SKELETON_CSS,\n BUNDLE_VOLUME_CSS,\n} from \"./styles/bundle-css\";\n\n/**\n * Per-shop localStorage key for the active cart id. Scoping by shop domain\n * keeps the cart isolated when a single browser visits multiple Lime-\n * Bundles-powered storefronts (rare, but correct).\n */\nconst cartStorageKey = (shopDomain: string) => `lb_cart_id:${shopDomain}`;\n\n/**\n * Resolve the current product handle from the page. Runs the cascade\n * documented on LimeBundleElement and returns null if no source matches.\n */\nfunction resolveProductHandle(explicit: string | null): string | null {\n if (explicit) return explicit.trim() || null;\n\n if (typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>(\n 'meta[name=\"shopify:product-handle\"]',\n );\n if (meta?.content) return meta.content.trim() || null;\n }\n\n if (typeof window !== \"undefined\") {\n const match = window.location.pathname.match(/\\/products\\/([^/?#]+)/);\n if (match?.[1]) return decodeURIComponent(match[1]);\n }\n\n return null;\n}\n\nexport class LimeBundleElement extends HTMLElement {\n static observedAttributes = [\n \"shop-domain\",\n \"storefront-token\",\n \"bundle-gid\",\n \"product-handle\",\n \"app-url\",\n \"analytics\",\n \"locale\",\n \"country\",\n \"language\",\n \"market-id\",\n ];\n\n /**\n * B2B buyer identity. Set programmatically — `element.buyer = {...}` or\n * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute\n * because the customer access token would land in DOM snapshots (Sentry,\n * analytics, browser extensions) and Referer headers. See the package\n * README (\"Markets & B2B\").\n */\n buyer: BuyerResolver | undefined = undefined;\n\n private shadow: ShadowRoot;\n private bundles: ParsedBundle[] = [];\n private abortController: AbortController | null = null;\n private impressionCleanups: Array<() => void> = [];\n /**\n * Tick / observer / listener cleanups registered by individual renderers\n * (e.g. countdown setInterval handles). Flushed in disconnectedCallback\n * so we never leak timers or event listeners when the widget is removed.\n */\n private renderCleanups: Array<() => void> = [];\n /**\n * Merchant custom CSS fetched from the shop metafield. Injected inside\n * the shadow root alongside the bundle stylesheets so selectors like\n * `.lb-bundle-widget { ... }` reach the widget's DOM.\n */\n private shopCustomCss: string | null = null;\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n // `fetchBundle()` calls `renderLoading()` synchronously before any\n // await — the skeleton paints on the first frame, reserving\n // layout space before the Storefront API call resolves.\n this.fetchBundle();\n }\n\n disconnectedCallback() {\n this.abortController?.abort();\n this.teardownImpressions();\n this.teardownRenderers();\n }\n\n private teardownImpressions() {\n for (const cleanup of this.impressionCleanups) cleanup();\n this.impressionCleanups = [];\n }\n\n private teardownRenderers() {\n for (const cleanup of this.renderCleanups) cleanup();\n this.renderCleanups = [];\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue || !this.isConnected) return;\n if (\n name === \"bundle-gid\" ||\n name === \"product-handle\" ||\n name === \"shop-domain\" ||\n name === \"storefront-token\"\n ) {\n if (this.shopDomain && this.storefrontToken) {\n this.fetchBundle();\n }\n }\n }\n\n private get shopDomain(): string {\n return this.getAttribute(\"shop-domain\") ?? \"\";\n }\n\n private get storefrontToken(): string {\n return this.getAttribute(\"storefront-token\") ?? \"\";\n }\n\n private get bundleGid(): string {\n return this.getAttribute(\"bundle-gid\") ?? \"\";\n }\n\n private get productHandleAttr(): string {\n return this.getAttribute(\"product-handle\") ?? \"\";\n }\n\n private get appUrl(): string {\n return this.getAttribute(\"app-url\") ?? \"\";\n }\n\n private get analyticsEnabled(): boolean {\n return this.getAttribute(\"analytics\") !== \"false\";\n }\n\n private get country(): string | undefined {\n return this.getAttribute(\"country\") ?? undefined;\n }\n\n private get language(): string | undefined {\n return this.getAttribute(\"language\") ?? undefined;\n }\n\n private get marketId(): string | undefined {\n return this.getAttribute(\"market-id\") ?? undefined;\n }\n\n /**\n * Returns the @inContext-wrapped query when any context field is set;\n * otherwise the plain query. Keeps responses publicly cacheable when\n * no buyer/country/language is set.\n */\n private wrapQueryForContext(query: string): string {\n return hasInContext({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.buyer,\n })\n ? withInContext(query)\n : query;\n }\n\n /**\n * Returns true if this bundle should be hidden for the current market.\n * For \"all\" bundles, always returns false (visible). For \"specific\"\n * bundles, returns true when no marketId was set or when the bundle's\n * marketIds doesn't include the configured market.\n */\n private isMarketHidden(bundle: ParsedBundle): boolean {\n if (bundle.marketVisibility !== \"specific\") return false;\n if (!this.marketId) return true;\n return !bundle.marketIds.includes(this.marketId);\n }\n\n private async fetchBundle() {\n if (!this.shopDomain || !this.storefrontToken) {\n this.renderError(\n \"Missing required attributes: shop-domain, storefront-token\",\n );\n return;\n }\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n this.renderLoading();\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.buyer,\n });\n\n try {\n // Fire bundle query BEFORE CSS so order-sensitive call logs (and any\n // test harness that mocks fetch with sequential mockResolvedValueOnce)\n // see the bundle as call #1, CSS as call #2. Parallelism preserved\n // via Promise.all at the await site below.\n let bundlePromise: Promise<void>;\n let singleBundleMode = false;\n if (this.bundleGid) {\n singleBundleMode = true;\n bundlePromise = this.fetchSingleBundle(client, controller.signal);\n } else {\n const handle = resolveProductHandle(this.productHandleAttr);\n if (!handle) {\n this.teardownImpressions();\n this.renderError(\n \"No bundle-gid or product-handle provided, and the current URL doesn't match /products/<handle>.\",\n );\n return;\n }\n bundlePromise = this.fetchProductBundles(\n client,\n controller.signal,\n handle,\n );\n }\n\n // CSS fetch kicks off AFTER bundle fetch for deterministic call order.\n // Best-effort — widget renders without merchant styling if it fails.\n const cssPromise = client\n .query<ShopCustomCssResponse>(SHOP_CUSTOM_CSS_QUERY, undefined, {\n signal: controller.signal,\n })\n .catch(() => null);\n\n await bundlePromise;\n if (controller.signal.aborted) return;\n\n // Single-bundle mode with an explicit GID that didn't resolve is a\n // genuine error — the merchant pinned a specific bundle and it's\n // missing. Product-handle mode with zero bundles is NOT an error:\n // the product legitimately has no bundles configured; widget stays\n // invisible (mirrors classic-theme Liquid block behaviour).\n if (singleBundleMode && this.bundles.length === 0) {\n this.teardownImpressions();\n this.renderError(\"Bundle not found\");\n return;\n }\n\n const css = await cssPromise;\n if (css?.shop?.metafield?.value) {\n // Classic storefronts get the CSS in document.head; the web\n // component also stores the sanitized CSS for its own shadow\n // root injection below, since shadow DOM blocks inherited styles.\n injectCustomCss(this.shopDomain, css.shop.metafield.value);\n const sanitized = sanitizeCustomCss(css.shop.metafield.value);\n if (sanitized.ok) this.shopCustomCss = sanitized.css;\n }\n\n // Resolve A/B variants for any bundle that's in an active test.\n // Awaited so bundles render with the assigned variant on first\n // paint (no flash of Variant A). Individual failures inside\n // applyABVariants fall back to Variant A silently.\n await this.applyABVariants();\n\n this.renderBundles();\n } catch (err) {\n if (controller.signal.aborted) return;\n this.bundles = [];\n this.teardownImpressions();\n this.renderError(\n err instanceof Error ? err.message : \"Failed to load bundle\",\n );\n }\n }\n\n /**\n * Resolve the visitor's A/B bucket for every bundle with an active test\n * and merge Variant B overrides where applicable. Runs in parallel; any\n * assignment failure logs internally but still renders Variant A (safe\n * default). The `getABTestAssignment` helper persists the bucket via a\n * first-party cookie; variant attribution is recorded server-side from\n * the analytics events the widget already emits.\n */\n private async applyABVariants(): Promise<void> {\n if (this.bundles.length === 0) return;\n const results = await Promise.all(\n this.bundles.map(async (bundle) => {\n if (!bundle.abTestId || !bundle.abVariantB) return bundle;\n try {\n const assignment = await getABTestAssignment(\n bundle.abTestId,\n bundle.id,\n );\n if (assignment?.variant === \"B\") {\n return applyABVariantB(bundle);\n }\n } catch (err) {\n // Surface A/B failures so misconfigured tests aren't invisible.\n // Variant A still renders — the customer is never blocked.\n // eslint-disable-next-line no-console\n console.warn(\n `[lime-bundle] A/B assignment failed for bundle ${bundle.id}; falling back to Variant A.`,\n err,\n );\n }\n return bundle;\n }),\n );\n this.bundles = results;\n }\n\n private async fetchSingleBundle(\n client: StorefrontClient,\n signal: AbortSignal,\n ): Promise<void> {\n const data = await client.query<BundleMetaobjectResponse>(\n this.wrapQueryForContext(BUNDLE_METAOBJECT_QUERY),\n { id: this.bundleGid },\n { signal },\n );\n if (!data.metaobject) {\n this.bundles = [];\n return;\n }\n const parsed = parseMetaobjectBundle(\n data.metaobject.id,\n data.metaobject.fields,\n );\n this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];\n }\n\n private async fetchProductBundles(\n client: StorefrontClient,\n signal: AbortSignal,\n productHandle: string,\n ): Promise<void> {\n // fetchBundlesForProduct re-creates its own client; skip that indirection\n // and reuse the one we already built so the request shares the same\n // AbortSignal and header config.\n const data = await client.query<BundlesForProductResponse>(\n this.wrapQueryForContext(BUNDLES_FOR_PRODUCT_QUERY),\n { handle: productHandle },\n { signal },\n );\n if (!data.product) {\n this.bundles = [];\n return;\n }\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n for (const ref of refs) {\n const parsed = parseMetaobjectBundle(ref.id, ref.fields);\n if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);\n }\n this.bundles = bundles;\n }\n\n /**\n * Dispatch add-to-cart with a cancelable event, then — unless a listener\n * called preventDefault — execute the default cart-and-checkout flow.\n *\n * `fire-and-forget` against `reportAddToCart` runs regardless so merchants\n * with BYO cart still get analytics.\n */\n private handleAddToCart = async (\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): Promise<void> => {\n const ev = new CustomEvent(\"lime-bundle:add-to-cart\", {\n detail: { lines },\n bubbles: true,\n composed: true,\n cancelable: true,\n });\n // dispatchEvent returns false if preventDefault() was called on a\n // cancelable event. That's how merchants opt out of the default flow.\n const allowDefault = this.dispatchEvent(ev);\n\n // Analytics fire regardless of which cart path runs.\n this.reportAddToCartEvent(bundle, lines);\n\n if (allowDefault) {\n await this.defaultAddToCart(lines);\n }\n };\n\n /**\n * Default cart flow: Shopify's Storefront Cart API is tokenless, so we\n * don't need any additional scopes. Persist the cart ID in localStorage\n * so subsequent adds on the same browser session join the existing cart\n * instead of creating a new one every click.\n */\n private async defaultAddToCart(lines: CartLineInput[]): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n const storage = window.localStorage;\n const key = cartStorageKey(this.shopDomain);\n const existingCartId = storage?.getItem(key) ?? null;\n\n try {\n let checkoutUrl: string | null = null;\n\n if (existingCartId) {\n const res = await client.query<CartLinesAddResponse>(\n CART_LINES_ADD_MUTATION,\n { cartId: existingCartId, lines },\n );\n const payload = res.cartLinesAdd;\n if (payload?.userErrors?.length) {\n // Cart GID expired or was merged on Shopify's side — fall back to\n // cartCreate below. This happens after ~10 days of inactivity.\n storage?.removeItem(key);\n } else if (payload?.cart) {\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (!checkoutUrl) {\n const res = await client.query<CartCreateResponse>(\n CART_CREATE_MUTATION,\n { input: { lines } },\n );\n const payload = res.cartCreate;\n if (payload?.cart) {\n storage?.setItem(key, payload.cart.id);\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (checkoutUrl) {\n window.location.assign(checkoutUrl);\n } else {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message: \"Cart creation failed\", code: \"CART_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n }\n } catch (err) {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: {\n message:\n err instanceof Error ? err.message : \"Cart mutation failed\",\n code: \"CART_ERROR\",\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n }\n\n private reportAddToCartEvent(\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): void {\n if (!this.analyticsEnabled || !this.appUrl) return;\n\n const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);\n const totalPrice = lines.reduce((sum, line) => {\n const product = bundle.products.find((p) =>\n p.variants.nodes.some((v) => v.id === line.merchandiseId),\n );\n const variant = product?.variants.nodes.find(\n (v) => v.id === line.merchandiseId,\n );\n const price = variant ? parseFloat(variant.price.amount) : 0;\n return sum + price * line.quantity;\n }, 0);\n\n reportAddToCart(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n productId: bundle.products[0]?.id ?? \"\",\n quantity,\n totalPrice: Math.round(totalPrice * 100) / 100,\n },\n );\n }\n\n private renderBundles() {\n this.teardownImpressions();\n this.teardownRenderers();\n this.shadow.innerHTML = \"\";\n\n // Bundle the base stylesheet and every type-specific sheet in one <style>.\n // This matches the Liquid theme app block's output (base.css +\n // bundle-{fixed,mix-match,volume}.css) so the shipped widget honours the\n // same selectors a merchant already styled against. Shadow DOM scope\n // keeps these rules from leaking out; the ported styles all target\n // `.lb-bundle-widget` descendants so there's no global bleed.\n const style = document.createElement(\"style\");\n style.textContent = [\n BUNDLE_BASE_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_VOLUME_CSS,\n BUNDLE_DROPDOWN_CSS,\n ].join(\"\\n\");\n this.shadow.appendChild(style);\n\n // Merchant custom CSS — injected AFTER the built-in stylesheets so\n // the merchant's rules override defaults. Sanitized upstream via\n // sanitizeCustomCss (strips < > and script-safety patterns).\n if (this.shopCustomCss) {\n const customStyle = document.createElement(\"style\");\n customStyle.setAttribute(\"data-lime-bundles\", \"shop-custom-css\");\n customStyle.textContent = this.shopCustomCss;\n this.shadow.appendChild(customStyle);\n }\n\n // Empty state: the shadow root holds only the <style> tag — nothing\n // visible. Matches Liquid theme UX where a product with no bundles\n // simply shows no block. We still fire `lime-bundle:loaded` below so\n // consumers know the async work completed (important for test\n // synchronisation and for merchants who want to hide a parent\n // placeholder once the widget has decided whether to render).\n\n for (const bundle of this.bundles) {\n const container = document.createElement(\"div\");\n container.className = \"lb-bundle-widget\";\n container.setAttribute(\"role\", \"region\");\n container.setAttribute(\"aria-label\", bundle.title);\n container.setAttribute(\"data-bundle-type\", bundle.bundleType);\n container.setAttribute(\"data-bundle-gid\", bundle.id);\n\n // Emit every merchant-configurable --lb-* property so the ported CSS\n // renders the widget with the look the merchant set in the admin\n // editor.\n applyWidgetConfigVars(container, bundle.widgetConfig);\n\n // Toggle using-mouse / using-keyboard on this container so the CSS\n // focus-ring rules match the customer's current input device.\n this.renderCleanups.push(trackInputMode(container));\n\n const dispatch = (lines: CartLineInput[]) =>\n this.handleAddToCart(bundle, lines);\n const registerCleanup = (fn: () => void) =>\n this.renderCleanups.push(fn);\n\n switch (bundle.bundleType) {\n case \"fixed\":\n renderFixedBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"mix_match\":\n renderMixMatchBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"volume\":\n renderVolumeBundle(container, bundle, dispatch, registerCleanup);\n break;\n }\n\n this.shadow.appendChild(container);\n this.setupImpressionFor(bundle, container);\n }\n\n const first = this.bundles[0];\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:loaded\", {\n detail: {\n bundleCount: this.bundles.length,\n bundleTypes: this.bundles.map((b) => b.bundleType),\n // Legacy fields — meaningful only in single-bundle mode. Preserved\n // for merchants who attached listeners against the pre-1.0 shape.\n bundleType: first?.bundleType,\n title: first?.title,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private setupImpressionFor(bundle: ParsedBundle, element: Element) {\n if (!this.analyticsEnabled || !this.appUrl) return;\n const cleanup = observeImpression(element, () => {\n reportImpression(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n },\n );\n });\n this.impressionCleanups.push(cleanup);\n }\n\n private renderLoading() {\n // Synchronous paint on every mount (before the Storefront fetch\n // resolves) so the host element has intrinsic size from render-0\n // and doesn't shift layout when the real bundle swaps in.\n this.shadow.innerHTML = `\n <style>${BUNDLE_BASE_CSS}</style>\n <style>${BUNDLE_SKELETON_CSS}</style>\n <div class=\"lb-bundle-widget lb-bundle-widget--loading\" aria-busy=\"true\" aria-label=\"Loading bundle\">\n <div class=\"lb-skeleton lb-skeleton--title\"></div>\n <div class=\"lb-skeleton--products\">\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n </div>\n <div class=\"lb-skeleton--footer\">\n <div class=\"lb-skeleton lb-skeleton--total\"></div>\n <div class=\"lb-skeleton lb-skeleton--cta\"></div>\n </div>\n </div>\n `;\n }\n\n private renderError(message: string) {\n this.shadow.innerHTML = \"\";\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message, code: \"LOAD_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n}\n\n// Re-export the helper so consumers of the widget package can import it\n// when they want to share the URL-detection logic with their own code.\n"],"mappings":"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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAimBlBC,GAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6EnBC,GAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAynBvBC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8IpBC,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqQtBC,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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","names":["src_exports","__export","LimeBundleElement","trackInputMode","StorefrontApiError","errors","e","DEFAULT_API_VERSION","resolveBuyer","buyer","hasInContext","config","createStorefrontClient","version","endpoint","query","variables","options","headers","mergedVariables","response","json","withInContext","_match","name","args","trimmed","extra","combined","BUNDLE_METAOBJECT_QUERY","SHOP_CUSTOM_CSS_QUERY","BUNDLES_FOR_PRODUCT_QUERY","CART_CREATE_MUTATION","CART_LINES_ADD_MUTATION","DEFAULT_PRODUCT_RULE","BundleParseError","message","reason","WIDGET_CONFIG_DEFAULTS","mergeWidgetConfig","raw","input","sanitizeDefaultTier","flattenWidgetConfig","config","CSS_VAR_MAP","PX_KEYS","applyWidgetConfigVars","el","flat","flatKey","cssVar","value","serialized","variantChevronUrl","ratioVars","thumbnailRatioVars","pickerRatioVars","strokeHex","ratio","VALID_BUNDLE_TYPES","ACTIVE_STATUSES","parseMetaobjectBundle","metaobjectId","fields","parseMetaobjectBundleStrict","err","fieldMap","f","title","rawBundleType","rawStatus","bundleType","status","startsAt","endsAt","now","start","end","products","resolveProducts","discountConfig","parseDiscountConfig","widgetConfig","parseWidgetConfig","base","parseSelectedVariantIds","parseNumericRecord","parseABVariantB","parseMarketVisibility","parseMarketIds","parseVolumeTiers","parseIntField","parseProductRules","productsField","node","collectionField","parseJsonField","result","entry","x","key","record","k","v","num","out","pid","rule","min","max","minN","maxN","minClamped","maxClamped","parsed","overrides","description","discountType","discountValueRaw","abTiers","t","parseOneVolumeTier","minQty","tier","options","resolveBundleQty","bundle","productId","variantId","vq","applyABVariantB","overrides","title","description","discountConfig","reportImpression","config","event","sendEvent","reportAddToCart","observeImpression","element","callback","observer","entries","entry","payload","url","body","r","consentGranted","isBrowser","hasConsent","isBrowser","consentGranted","shopifyGlobal","SESSION_COOKIE_NAME","SESSION_COOKIE_MAX_AGE","AB_COOKIE_MAX_AGE","abCookieName","bundleId","getABTestAssignment","testId","existing","readABCookie","sessionId","getOrCreateSessionId","variant","fnv1aVariant","writeABCookie","prefix","raw","c","value","name","generateUUID","id","r","input","hash","i","MAX_CSS_LENGTH","BLOCKED_PATTERNS","SAFE_URL_VALUE","sanitizeCustomCss","css","_","hex","codePoint","pattern","label","urlPattern","urlMatch","urlValue","STYLE_ID_PREFIX","injectCustomCss","shopDomain","rawCss","sanitized","simpleHash","style","formatMoney","amount","currencyCode","num","UNIT_LABEL","formatUnitPrice","unitPrice","measurement","fallbackCurrency","amount","label","currency","formatted","formatMoney","value","measurementText","parseCents","num","formatCents","cents","currencyCode","percentageDiscountUnit","unitCents","percent","computeFixedPricing","bundle","productQuantities","showSaveBadge","discountType","discountValue","rows","totalCents","saleCents","product","variant","v","qty","lineCents","compareCents","perUnit","savingsCents","headerBadge","computeBundleSaleCents","discount","off","THUMB_PX","transformImageUrl","url","u","formatCountdown","msRemaining","totalSeconds","days","hours","minutes","seconds","pad","n","dropdown_exports","__export","computePosition","emptyTypeAheadState","handleKey","pushTypeAheadChar","DEFAULT_TRIGGER_MARGIN","trigger","viewportHeight","desiredHeight","margin","clip","upperBound","lowerBound","spaceBelow","spaceAbove","placement","maxHeight","offsetTop","PASSTHROUGH","firstEnabled","options","i","lastEnabled","nextEnabled","from","step","idx","prevEnabled","isPrintable","key","event","state","isOpen","activeIndex","selectedIndex","TYPE_AHEAD_RESET_MS","char","now","resetMs","buffer","newState","opt","picker_exports","findVariantByOptions","isOptionValueAvailable","toPickerVariant","variants","optionValues","match","j","optionIndex","selected","ok","o","isFulfillable","requiredQty","shouldShowLowStockBadge","threshold","enabled","maxAddableQuantity","variant","productMax","alreadyInBundle","stockCap","computePosition","emptyTypeAheadState","handleKey","pushTypeAheadChar","dropdown_exports","ITEM_HEIGHT_PX","LIST_PAD_Y","MAX_VISIBLE_ITEMS","openInstances","closeOutsideEvent","event","path","i","inst","onDocResize","docListenersAttached","attachDocumentListeners","detachDocumentListeners","readOptions","select","out","i","firstEnabled","opts","findScrollableAncestor","el","win","cur","overflowY","VARIANT_SELECT_CLASSES","BIND_SELECTOR","c","bindDropdown","slot","doc","rootNode","labelText","idBase","shell","trigger","listboxId","triggerLabel","chevron","listbox","modalOverlay","isOpen","activeIndex","typeAhead","emptyTypeAheadState","optionEls","instance","syncFromSelect","idx","li","setActive","newIndex","liTop","liBottom","visTop","visBottom","position","rect","desiredHeight","MAX_VISIBLE_ITEMS","ITEM_HEIGHT_PX","LIST_PAD_Y","scrollable","clip","r","result","computePosition","open","openInstances","selIdx","attachDocumentListeners","close","restoreFocus","detachDocumentListeners","commit","index","opt","event","applyAction","action","pushTypeAheadChar","_exhaustive","onKeydown","handleKey","onTriggerClick","onListboxClick","target","onListboxMousemove","onShellFocusout","active","onSelectChange","observer","onListboxMousedown","destroy","bindAllDropdowns","root","selects","instances","sel","inst","unbindAllDropdowns","renderCountdown","endsAtIso","parsed","parseIso","target","wrap","labelWrap","labelText","timer","intervalId","tick","msLeft","stop","formatCountdown","iso","buildCtaButton","label","button","labelSpan","spinnerSpan","setCtaLabel","text","el","tag","className","attrs","node","k","v","PLACEHOLDER_THUMB_SVG","renderFixedBundle","container","bundle","onAddToCart","onCleanup","wc","qtyFor","productId","variantId","resolveBundleQty","rows","oosCount","product","idx","row","buildRowState","currency","root","el","headerHandle","renderHeader","countdown","renderCountdown","list","rowHandles","rowState","handle","renderProductRow","updatePricing","pricingHandle","renderPricingRow","savingsBarHandle","renderSavingsBar","cta","renderCta","lines","r","bindAllDropdowns","unbindAllDropdowns","totalCents","sum","unit","parseCents","saleCents","computeSale","savingsCents","deriveHeaderBadge","productIndex","selectedVariantIds","merchantScoped","v","firstInStock","isFulfillable","eligibleVariants","isOos","selected","qty","header","content","title","subtitle","badgeEl","initialPricing","computeFixedPricing","badgeText","savings","dc","formatCents","state","lowStock","onVariantChange","rowEl","thumb","initialThumbImage","thumbImg","transformImageUrl","THUMB_PX","qtyBadgeRef","info","name","oosLabel","badge","prices","compare","priceEl","unitPriceEl","lowStockEl","applyVariantToRow","variant","cmp","unitText","formatUnitPrice","nextImage","required","shouldShowLowStockBadge","optionNames","o","productIdTail","optionSelects","resolveVariant","values","i","syncSelectsToVariant","sel","isValueAvailable","optionIndex","value","recomputeDisabled","opt","handleChange","s","groupsContainer","position","group","label","select","seen","sale","bar","amount","onClick","button","buildCtaButton","discount","off","perUnit","PLACEHOLDER_THUMB_SVG","PLUS_ICON_SVG","CLOSE_ICON_SVG","SEARCH_CLEAR_ICON_SVG","STEPPER_MINUS_SVG","STEPPER_PLUS_SVG","ruleFor","bundle","productId","DEFAULT_PRODUCT_RULE","alreadyInBundleFor","selections","variantId","sum","s","renderMixMatchBundle","container","onAddToCart","onCleanup","wc","currency","requiredQty","showQtySelector","eligible","buildEligibleProducts","e","root","el","header","renderHeader","countdown","renderCountdown","progress","renderProgress","slotsContainer","pricingSection","renderPricingSection","savingsBar","renderSavingsBar","cta","buildCtaButton","buildCartLines","modal","renderModal","product","variant","quantity","addSelection","unbindAllDropdowns","afterMutation","rule","cap","maxAddableQuantity","clamped","parseCents","formatUnitPrice","removeSlotAt","index","renderSlots","updateCta","totalSlots","i","selection","renderFilledSlot","renderEmptySlot","count","setCtaLabel","grouped","key","existing","line","content","title","discountType","discountValue","label","formatCents","badge","wrap","labels","remaining","track","fill","update","selected","pct","onClick","slot","thumb","text","onRemove","img","transformImageUrl","THUMB_PX","qtyBadge","info","linePrice","lineCompare","priceWrap","compare","priceEl","unitPrice","remove","showCompareAtPrice","prices","sale","totalCents","sel","saleCents","computeBundleSaleCents","labelEl","amount","savings","handlers","overlay","sanitizeId","modalHeader","modalTitle","closeBtn","close","searchInput","searchClearBtn","searchWrap","applySearch","list","empty","emptyText","live","rowsBuilt","productRows","buildRows","ep","availableVariants","firstAvailVariant","v","isFulfillable","currentVariant","productEl","initialThumbImage","thumbImg","countBadge","price","initialUnitText","lowStockBadge","refreshLowStockBadge","shouldShowLowStockBadge","computeStepperBounds","already","max","stepper","qtyGroup","renderQtyStepper","min","row","optionSelects","optionNames","o","productIdTail","resolveVariant","values","syncSelectsToVariant","isValueAvailable","optionIndex","value","recomputeDisabled","opt","handleChange","next","nextUnitText","nextImage","groupsContainer","name","position","group","select","seen","variantLabel","soldOut","addBtn","refreshAddState","rowDisabled","actions","qty","nextFocus","lastFocused","bindAllDropdowns","refreshCounts","query","visibleCount","match","onKeydown","trapFocus","isOpen","open","onOverlayClick","r","opts","minus","valueEl","plus","current","clamp","externallyDisabled","n","b","paint","disabled","oosBehavior","result","available","isOos","focusables","first","last","active","gid","renderVolumeBundle","container","bundle","onAddToCart","onCleanup","wc","product","minTierQty","variant","v","isFulfillable","basePriceCents","parseCents","currency","discountType","resolved","tier","index","perUnit","amt","pct","discount","bestTierIndex","pickBestTierIndex","selectedIndex","clamp","popularIndex","root","el","renderHeader","countdown","renderCountdown","tierGroup","r","tierEl","renderTierCard","selectTier","e","delta","next","pricingEl","renderPricingRow","savingsBarEl","renderSavingsBar","cta","renderCta","shouldShowLowStockBadge","lowStock","idx","card","i","newPricing","newBar","badgeEl","label","badgeFor","header","content","title","badge","isSelected","popularLabel","showComparePrice","showPerUnitPrice","radio","grid","price","compare","formatCents","each","unit","showItemCount","showCompareAtPrice","totalCents","undiscountedCents","savings","row","count","prices","sale","bar","labelEl","amount","onClick","isAvailable","button","buildCtaButton","bestSavings","bestIndex","n","min","max","NAV_KEYS","targets","listenersAttached","setAll","on","off","el","onKeyDown","onPointerDown","attachListeners","detachListeners","trackInputMode","target","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_DROPDOWN_CSS","BUNDLE_SKELETON_CSS","cartStorageKey","shopDomain","resolveProductHandle","explicit","meta","match","LimeBundleElement","cleanup","name","oldValue","newValue","query","hasInContext","withInContext","bundle","controller","client","createStorefrontClient","bundlePromise","singleBundleMode","handle","cssPromise","SHOP_CUSTOM_CSS_QUERY","css","injectCustomCss","sanitized","sanitizeCustomCss","err","results","getABTestAssignment","applyABVariantB","signal","data","BUNDLE_METAOBJECT_QUERY","parsed","parseMetaobjectBundle","productHandle","BUNDLES_FOR_PRODUCT_QUERY","refs","bundles","ref","lines","ev","allowDefault","storage","key","existingCartId","checkoutUrl","payload","CART_LINES_ADD_MUTATION","CART_CREATE_MUTATION","quantity","sum","l","totalPrice","line","variant","v","price","reportAddToCart","style","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_DROPDOWN_CSS","customStyle","container","applyWidgetConfigVars","trackInputMode","dispatch","registerCleanup","fn","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","first","b","element","observeImpression","reportImpression","BUNDLE_SKELETON_CSS","message","LimeBundleElement"]}
1
+ {"version":3,"sources":["../src/index.ts","../../core/src/storefront-api/client.ts","../../core/src/storefront-api/queries.ts","../../core/src/storefront-api/cache-key.ts","../../core/src/bundle/types.ts","../../core/src/bundle/widget-config.ts","../../core/src/bundle/parser.ts","../../core/src/bundle/fetch-by-product.ts","../../core/src/bundle/tier-calculator.ts","../../core/src/bundle/validator.ts","../../core/src/bundle/qty.ts","../../core/src/analytics/reporter.ts","../../core/src/ab-test/consent.ts","../../core/src/ab-test/assigner.ts","../../core/src/styles/sanitize.ts","../../core/src/styles/custom-css-injector.ts","../../core/src/utils/money.ts","../../core/src/utils/pricing.ts","../../core/src/utils/image.ts","../../core/src/utils/countdown.ts","../../core/src/dropdown/index.ts","../../core/src/dropdown/compute-position.ts","../../core/src/dropdown/keyboard.ts","../../core/src/dropdown/type-ahead.ts","../../core/src/variants/picker.ts","../../core/src/inventory/predicate.ts","../src/dropdown/bind-dropdown.ts","../src/renderers/countdown.ts","../src/renderers/cta-button.ts","../src/renderers/dom.ts","../src/renderers/fixed.ts","../src/renderers/mix-match.ts","../src/renderers/volume.ts","../src/utils/input-mode.ts","../src/styles/bundle-css.ts","../src/lime-bundle.ts"],"sourcesContent":["/**\n * @lime-bundles/widget — Vanilla JS Web Component for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-bundle.js\"></script>\n * <lime-bundle shop-domain=\"...\" storefront-token=\"...\" bundle-gid=\"...\"></lime-bundle>\n */\nimport { LimeBundleElement } from \"./lime-bundle\";\n\n// Register custom element\nif (\n typeof customElements !== \"undefined\" &&\n !customElements.get(\"lime-bundle\")\n) {\n customElements.define(\"lime-bundle\", LimeBundleElement);\n}\n\nexport { LimeBundleElement };\nexport { trackInputMode } from \"./utils/input-mode\";\n","/**\n * Lightweight Storefront API client.\n * No framework dependencies — just fetch.\n *\n * SSR and Trusted Publishing environments (Hydrogen, Next.js server\n * components) MUST forward the end-buyer's IP via the `buyerIp` config\n * field. Without it Shopify may return `430 Shopify Security Rejection`\n * for server-originated traffic. Pass the IP in IPv4 or IPv6 string form.\n */\n\nexport class StorefrontApiError extends Error {\n constructor(\n public readonly errors: Array<{ message: string; locations?: unknown[] }>,\n ) {\n super(errors.map((e) => e.message).join(\"; \"));\n this.name = \"StorefrontApiError\";\n }\n}\n\nexport interface StorefrontClient {\n query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T>;\n}\n\nexport interface QueryOptions {\n /** Abort the request mid-flight. */\n signal?: AbortSignal;\n}\n\n/**\n * B2B buyer identity for Storefront API `@inContext(buyer: ...)`.\n * `customerAccessToken` is required when `buyer` is set (per Shopify's\n * BuyerInput spec). `companyLocationId` selects a specific B2B location\n * for catalog pricing when the customer has access to multiple.\n *\n * Security: this token MUST come from a same-origin authenticated endpoint\n * (Customer Account API PKCE flow) and MUST NOT be passed via URL params,\n * localStorage shared with third-party scripts, or static prop trees that\n * land in DOM snapshots. See each package's README (\"Markets & B2B\").\n */\nexport interface BuyerInput {\n customerAccessToken: string;\n companyLocationId?: string;\n}\n\n/** Lazy buyer resolver — called per request so tokens stay off static props. */\nexport type BuyerResolver = BuyerInput | (() => BuyerInput | Promise<BuyerInput>);\n\nexport interface StorefrontClientConfig {\n shopDomain: string;\n accessToken: string;\n apiVersion?: string;\n /**\n * End-buyer's IP, forwarded as `Shopify-Storefront-Buyer-IP`. Required\n * for server-side calls with private tokens and recommended for all SSR.\n * See the class doc-comment.\n */\n buyerIp?: string;\n /**\n * ISO-3166 alpha-2 country code passed as `@inContext(country: ...)`.\n * Drives Shopify Markets pricing, currency, and availability. Omit for\n * the shop's default market.\n */\n country?: string;\n /**\n * ISO-639-1 language code passed as `@inContext(language: ...)`.\n * Translations come from the shop's locale settings. Omit for default.\n */\n language?: string;\n /**\n * B2B buyer identity. Either a plain object (token captured at config\n * time) or a callback that resolves per request (recommended for\n * security). When set, the SDK adds `@inContext(buyer: ...)` to queries\n * and the response carries B2B catalog prices for that customer.\n */\n buyer?: BuyerResolver;\n}\n\n// Keep in sync with CLAUDE.md → Shopify API Version Alignment.\nconst DEFAULT_API_VERSION = \"2025-10\";\n\nasync function resolveBuyer(buyer?: BuyerResolver): Promise<BuyerInput | undefined> {\n if (!buyer) return undefined;\n if (typeof buyer === \"function\") {\n return await buyer();\n }\n return buyer;\n}\n\n/**\n * Returns true if any `@inContext` field is set. Used by the query layer\n * to decide whether to emit the directive-wrapped or directive-free\n * variant — keeps cache fragmentation off for the no-context case.\n */\nexport function hasInContext(config: StorefrontClientConfig): boolean {\n return Boolean(config.country || config.language || config.buyer);\n}\n\nexport function createStorefrontClient(\n config: StorefrontClientConfig,\n): StorefrontClient {\n const version = config.apiVersion ?? DEFAULT_API_VERSION;\n const endpoint = `https://${config.shopDomain}/api/${version}/graphql.json`;\n\n return {\n async query<T = unknown>(\n query: string,\n variables?: Record<string, unknown>,\n options?: QueryOptions,\n ): Promise<T> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-Shopify-Storefront-Access-Token\": config.accessToken,\n };\n if (config.buyerIp) {\n headers[\"Shopify-Storefront-Buyer-IP\"] = config.buyerIp;\n }\n\n // Inject @inContext variables when set. The query string template\n // already declares the optional `$country`, `$language`, and\n // `$buyer` variables; the directive is emitted via the caller\n // selecting the *_INCONTEXT query variant.\n const buyer = await resolveBuyer(config.buyer);\n const mergedVariables: Record<string, unknown> = { ...(variables ?? {}) };\n if (config.country) mergedVariables.country = config.country;\n if (config.language) mergedVariables.language = config.language;\n if (buyer) mergedVariables.buyer = buyer;\n\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables: mergedVariables }),\n signal: options?.signal,\n });\n\n if (!response.ok) {\n throw new StorefrontApiError([\n {\n message: `Storefront API error: ${response.status} ${response.statusText}`,\n },\n ]);\n }\n\n const json = (await response.json()) as {\n data?: T;\n errors?: Array<{ message: string }>;\n };\n\n if (json.errors?.length) {\n throw new StorefrontApiError(json.errors);\n }\n\n return json.data as T;\n },\n };\n}\n","/**\n * Storefront API GraphQL queries.\n *\n * Pricing-sensitive queries (BUNDLE_METAOBJECT_QUERY, BUNDLES_FOR_PRODUCT_QUERY)\n * are wrapped with `@inContext(country:, language:, buyer:)` by\n * `withInContext()` below when the SDK config has any context field set.\n * The unwrapped variant stays publicly cacheable; the wrapped variant\n * must be marked `Cache-Control: private` per Hydrogen guidance for\n * buyer-contextual responses.\n *\n * The SDK fetcher selects the variant at request time via `hasInContext()`.\n */\nimport type { MetaobjectField } from \"./types\";\n\n/**\n * Returns the `@inContext`-wrapped variant of `query`. Adds the three\n * optional variables to the parameter list and the directive to the\n * operation root. Variable substitution itself is handled by the\n * Storefront client which injects `country`, `language`, `buyer` into\n * the request variables.\n *\n * Handles both arg-list (`query Name($x: Int) { ... }`) and zero-arg\n * (`query Name { ... }`) operations. Mutations are not supported —\n * adjust if one ever needs `@inContext`.\n */\nexport function withInContext(query: string): string {\n return query.replace(\n /query\\s+(\\w+)\\s*(?:\\(([^)]*)\\))?\\s*\\{/,\n (_match, name, args) => {\n const trimmed = (args || \"\").trim();\n const extra = \"$country: CountryCode, $language: LanguageCode, $buyer: BuyerInput\";\n const combined = trimmed ? `${trimmed}, ${extra}` : extra;\n return `query ${name}(${combined}) @inContext(country: $country, language: $language, buyer: $buyer) {`;\n },\n );\n}\n\nexport const BUNDLE_METAOBJECT_QUERY = `#graphql\n query BundleMetaobject($id: ID!) {\n metaobject(id: $id) {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Shop-level custom CSS metafield query. The SDK fetches this alongside\n * bundle data and auto-injects the result as a scoped <style> tag so\n * headless storefronts get CSS parity with classic-theme merchants.\n */\nexport const SHOP_CUSTOM_CSS_QUERY = `#graphql\n query ShopCustomCss {\n shop {\n metafield(namespace: \"$app\", key: \"custom_css\") {\n value\n }\n }\n }\n`;\n\nexport interface ShopCustomCssResponse {\n shop: {\n metafield: { value: string | null } | null;\n } | null;\n}\n\n/**\n * Product-aware bundle lookup. Mirrors the pattern the classic Liquid theme\n * block uses (`product.metafields[\"$app\"].active_bundle_ids`): one query\n * returns every bundle that applies to the current product, fully resolved\n * (nested product/collection references included) so the caller gets the\n * same shape as `parseMetaobjectBundleStrict` expects.\n *\n * The product handle is the natural input on a headless storefront — every\n * canonical Shopify product URL is `/products/<handle>`, so the widget can\n * often derive it from `window.location` without merchant input.\n *\n * Complexity: the nested references (bundle → products → variants) are deep\n * but stay within the Storefront API budget for token-based queries at\n * realistic bundle sizes (≤10 bundles per product, ≤50 products per bundle).\n */\nexport const BUNDLES_FOR_PRODUCT_QUERY = `#graphql\n query BundlesForProduct($handle: String!) {\n product(handle: $handle) {\n id\n handle\n title\n metafield(namespace: \"$app\", key: \"active_bundle_ids\") {\n references(first: 10) {\n nodes {\n ... on Metaobject {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n currentlyNotInStock\n quantityAvailable\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n unitPrice { amount currencyCode }\n unitPriceMeasurement { referenceValue referenceUnit }\n selectedOptions { name value }\n image { url altText }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Return shape for BUNDLES_FOR_PRODUCT_QUERY. The inner `fields` array\n * reuses `MetaobjectField` verbatim so callers can pass it straight to\n * `parseMetaobjectBundleStrict` without a type cast — the same shape the\n * BUNDLE_METAOBJECT_QUERY response produces for a single bundle.\n */\nexport interface BundlesForProductResponse {\n product: {\n id: string;\n handle: string;\n title: string;\n metafield: {\n references: {\n nodes: Array<{\n id: string;\n type: string;\n fields: MetaobjectField[];\n }>;\n };\n } | null;\n } | null;\n}\n\n/**\n * Tokenless cart mutations. Storefront API docs: \"Cart (read/write)\" is\n * available on the Tokenless Access tier. The widget's default-cart path\n * uses these so merchants don't need a cart implementation.\n */\nexport const CART_CREATE_MUTATION = `#graphql\n mutation CartCreate($input: CartInput!) {\n cartCreate(input: $input) {\n cart { id checkoutUrl }\n userErrors { field message }\n }\n }\n`;\n\nexport const CART_LINES_ADD_MUTATION = `#graphql\n mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $lines) {\n cart { id checkoutUrl }\n userErrors { field message }\n }\n }\n`;\n\n/**\n * Inner payload shape shared by both cart mutations. Not a GraphQL\n * response on its own — see `CartCreateResponse` and `CartLinesAddResponse`\n * for the correctly-wrapped top-level response shapes.\n */\nexport interface CartMutationPayload {\n cart: { id: string; checkoutUrl: string } | null;\n userErrors: Array<{ field: string[] | null; message: string }>;\n}\n\n/** Top-level response shape for `CART_CREATE_MUTATION`. */\nexport interface CartCreateResponse {\n cartCreate: CartMutationPayload;\n}\n\n/** Top-level response shape for `CART_LINES_ADD_MUTATION`. */\nexport interface CartLinesAddResponse {\n cartLinesAdd: CartMutationPayload;\n}\n","/**\n * Stable cache-key helper for SWR / TanStack Query / Hydrogen consumers.\n *\n * Buyer-contextual queries (Storefront `@inContext(buyer:)`) must NEVER\n * be shared across users — per Hydrogen's caching guidance, set\n * Cache-Control: private on those responses and ensure your cache key\n * includes a buyer fingerprint, never the raw token.\n *\n * Example usage with TanStack Query:\n * queryKey: getCacheKey(\"BundleMetaobject\", { id }, {\n * shopDomain, country, language, buyer,\n * })\n */\nimport type { BuyerInput } from \"./client\";\n\nexport interface CacheKeyContext {\n shopDomain: string;\n country?: string;\n language?: string;\n /**\n * Pass the resolved BuyerInput or undefined. Never pass a function —\n * the consumer is responsible for resolving lazy buyers before keying.\n * The raw `customerAccessToken` is hashed before being included in the\n * key so it doesn't appear in devtools / log output.\n */\n buyer?: BuyerInput;\n}\n\nexport function getCacheKey(\n queryName: string,\n variables: Record<string, unknown>,\n ctx: CacheKeyContext,\n): string[] {\n return [\n ctx.shopDomain,\n queryName,\n stableStringify(variables),\n ctx.country ?? \"_\",\n ctx.language ?? \"_\",\n ctx.buyer ? buyerFingerprint(ctx.buyer) : \"_\",\n ];\n}\n\n/**\n * Deterministic JSON.stringify with sorted keys. Avoids cache misses caused\n * by object property order varying across consumers.\n */\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) {\n return \"[\" + value.map(stableStringify).join(\",\") + \"]\";\n }\n const entries = Object.entries(value as Record<string, unknown>).sort(\n ([a], [b]) => a.localeCompare(b),\n );\n return (\n \"{\" +\n entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(\",\") +\n \"}\"\n );\n}\n\n/**\n * Lightweight non-cryptographic hash of the buyer token + optional company\n * location. Stable across runs, fast, and good enough to discriminate\n * cache entries per buyer without leaking the raw token. Don't use this\n * for security purposes — it's purely for cache keying.\n */\nfunction buyerFingerprint(buyer: BuyerInput): string {\n const input = `${buyer.customerAccessToken}:${buyer.companyLocationId ?? \"\"}`;\n // FNV-1a 32-bit\n let hash = 2166136261;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 16777619);\n }\n // Unsigned hex, 8 chars\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n","/**\n * Bundle types for headless rendering.\n *\n * `ParsedBundle` is a discriminated union keyed on `bundleType` so consumers\n * can narrow the type via a switch statement or conditional check:\n *\n * function render(bundle: ParsedBundle) {\n * switch (bundle.bundleType) {\n * case \"fixed\": // bundle.products is load-bearing\n * case \"volume\": // bundle.volumeTiers is load-bearing\n * case \"mix_match\": // bundle.minQuantity / maxQuantity are load-bearing\n * }\n * }\n *\n * Each variant carries ONLY the fields it actually uses. Fields that are\n * always present (id, title, status, schedule, widget config, A/B test\n * metadata) live on the shared `BundleBase` type.\n *\n * Locked for v2.0.0 — widening or narrowing these discriminants is a\n * breaking change. `WidgetConfig` widened in 2.0.0 to expose the full\n * admin-editor shape; see WidgetConfig for the eight sub-section structure.\n */\nimport type { Product } from \"../storefront-api/types\";\n\nexport type BundleType = \"fixed\" | \"mix_match\" | \"volume\";\n/**\n * `archived` is a system-owned status set by the duplicate-bundle A/B model\n * when a losing variant is promoted out of a test (see app/lib/ab-link-group.server.ts\n * `promoteWinner`). The SDK parser filters archived bundles upstream — they\n * never reach a rendered component — but the union must list the value so\n * raw metaobject reads round-trip cleanly through `parseMetaobjectBundleStrict`.\n *\n * `sync_failed` represents a bundle whose three-layer Metaobject→Discount→Prisma\n * write didn't complete; the admin form shows a \"didn't save properly\" banner\n * and merchants resave to retry. The SDK skips rendering these too.\n */\nexport type BundleStatus =\n | \"active\"\n | \"draft\"\n | \"inactive\"\n | \"out_of_stock\"\n | \"archived\"\n | \"sync_failed\";\n\nexport interface DiscountConfig {\n discountType: \"percentage\" | \"fixed_amount\";\n discountValue: number;\n allowStacking: boolean;\n}\n\n/**\n * Widget configuration as stored in the bundle metaobject's `widget_config`\n * field. Structured by visual area to mirror the in-app editor's accordion.\n *\n * The web component (`<lime-bundle>`) applies every value as a `--lb-*` CSS\n * custom property so the rendered widget matches what the merchant saw in\n * the editor. The React SDK exposes this object unchanged on\n * `ParsedBundle.widgetConfig`; whether and how to apply it is up to the\n * developer.\n */\nexport interface HeaderConfig {\n textColor: string;\n headerStyle: \"solid\" | \"gradient\";\n gradientStart: string;\n gradientEnd: string;\n saveBadgeBgColor: string;\n saveBadgeTextColor: string;\n saveBadgeBorderColor: string;\n saveBadgeBorderWidth: number;\n saveBadgeBorderRadius: number;\n countdownBgColor: string;\n countdownTextColor: string;\n}\n\nexport interface LayoutConfig {\n backgroundColor: string;\n borderColor: string;\n borderWidth: number;\n borderRadius: number;\n}\n\n/**\n * Thumbnail aspect-ratio choice. Drives a derived block in\n * applyWidgetConfigVars that sets three CSS vars\n * (--lb-thumbnail-aspect-ratio, --lb-thumbnail-img-fit,\n * --lb-thumbnail-img-height). \"original\" disables the aspect crop and\n * lets each image render at its intrinsic ratio.\n */\nexport type ThumbnailRatio = \"square\" | \"tall\" | \"wide\" | \"original\";\n\nexport interface ProductListConfig {\n textColor: string;\n imageBorderWidth: number;\n imageBorderColor: string;\n imageBorderRadius: number;\n thumbnailRatio: ThumbnailRatio;\n variantBorderWidth: number;\n variantBorderColor: string;\n variantBorderRadius: number;\n showPrice: boolean;\n showCompareAtPrice: boolean;\n showCountBubble: boolean;\n countBubbleBgColor: string;\n countBubbleTextColor: string;\n}\n\nexport interface PricingConfig {\n showSaveBadge: boolean;\n showComparePrice: boolean;\n showPerUnitPrice: boolean;\n showItemCount: boolean;\n showCompareAtPrice: boolean;\n}\n\nexport interface SavingsBarConfig {\n visible: boolean;\n bgColor: string;\n textColor: string;\n borderWidth: number;\n borderColor: string;\n borderRadius: number;\n}\n\nexport interface CtaConfig {\n ctaText: string;\n primaryColor: string;\n buttonTextColor: string;\n borderWidth: number;\n borderColor: string;\n borderRadius: number;\n}\n\nexport interface CountdownConfig {\n showCountdown: boolean;\n}\n\nexport interface PopularBadgeConfig {\n visible: boolean;\n text: string;\n /** Optional — merchant may not pick a tier. */\n tierIndex?: number;\n bgColor: string;\n textColor: string;\n borderWidth: number;\n borderColor: string;\n borderRadius: number;\n}\n\nexport interface WidgetConfig {\n header: HeaderConfig;\n layout: LayoutConfig;\n productList: ProductListConfig;\n pricing: PricingConfig;\n cta: CtaConfig;\n savingsBar: SavingsBarConfig;\n countdown: CountdownConfig;\n popularBadge: PopularBadgeConfig;\n outOfStockBehavior: \"show_greyed_out\" | \"hide\";\n /** Show \"Only X left\" badge when remaining stock is at or below\n * `lowStockThreshold`. Hidden silently when the Storefront token\n * lacks `unauthenticated_read_product_inventory`. */\n showLowStockBadge: boolean;\n /** Threshold (1-99) below which the low-stock badge appears. */\n lowStockThreshold: number;\n lowStockBgColor: string;\n lowStockTextColor: string;\n\n // --- Mix-match picker section ---\n showSearch: boolean;\n /**\n * When true (default), the picker modal renders a per-pick qty stepper so\n * the shopper can choose how many of each product to add. When false, the\n * stepper is hidden and Add adds qty = `productRules[productId]?.min ?? 1`.\n */\n mixMatchShowQuantitySelector: boolean;\n pickerBgColor: string;\n pickerTextColor: string;\n pickerBorderWidth: number;\n pickerBorderColor: string;\n pickerBorderRadius: number;\n pickerSearchBorderWidth: number;\n pickerSearchBorderColor: string;\n pickerSearchBorderRadius: number;\n pickerProductBorderWidth: number;\n pickerProductBorderColor: string;\n pickerProductBorderRadius: number;\n /** Aspect ratio for product thumbnails inside the picker modal —\n * independent of the main widget's `productList.thumbnailRatio`. */\n pickerThumbnailRatio: ThumbnailRatio;\n pickerShowCountBubble: boolean;\n pickerCountBubbleBgColor: string;\n pickerCountBubbleTextColor: string;\n pickerAddBgColor: string;\n pickerAddLabelColor: string;\n pickerAddBorderWidth: number;\n pickerAddBorderColor: string;\n pickerAddBorderRadius: number;\n pickerVariantBorderWidth: number;\n pickerVariantBorderColor: string;\n pickerVariantBorderRadius: number;\n pickerQtyStepperBorderWidth: number;\n pickerQtyStepperBorderColor: string;\n pickerQtyStepperBorderRadius: number;\n\n // --- Volume tier section ---\n tierBorderColor: string;\n tierBorderWidth: number;\n tierBorderRadius: number;\n tierSelectedBorderColor: string;\n tierSelectedBorderWidth: number;\n defaultTier: \"first\" | \"best_value\" | number;\n}\n\n/**\n * A single volume-bundle tier row. `discountType` lives at the bundle\n * level (`bundle.discountConfig.discountType`) — each tier carries only\n * the magnitude in whichever field matches that type:\n * - `percentage` is a whole-number (e.g. 10 for 10% off)\n * - `amount` is the per-unit fixed amount off, in the shop's currency\n *\n * Exactly one of `percentage` or `amount` is present per tier, determined\n * by the parent bundle's `discountType`. This mirrors the on-metaobject\n * shape — see `app/lib/bundle-types.ts` VolumeTierSchema.\n */\nexport interface VolumeTier {\n minQuantity: number;\n percentage?: number;\n amount?: number;\n}\n\n/**\n * A/B variant override. Present when an A/B test is active on the bundle\n * and this visitor is bucketed into Variant B. Fields are individually\n * optional — merchants can A/B-test any subset (e.g. just the discount\n * value, or just the title). Fields absent from the override fall back\n * to the base bundle's values.\n *\n * @deprecated Under the duplicate-bundle A/B model each variant is its own\n * ParsedBundle. The overlay shape is unused; kept here only so external\n * SDK consumers that still import the symbol can transition cleanly in the\n * next major. Will be removed in the v2 SDK release.\n */\nexport interface ABVariantOverrides {\n title?: string;\n description?: string;\n discountConfig?: DiscountConfig;\n volumeTiers?: VolumeTier[];\n}\n\n/**\n * Reference to a single variant in a link group. Stored on the primary\n * bundle's metaobject as `link_group_variants` JSON; mirrors\n * `ABTest.variantWeights` server-side.\n */\nexport interface LinkGroupVariantRef {\n /** Variant bundle's metaobject GID. */\n metaobjectId: string;\n /** Integer 0–100; weights across a link group sum to exactly 100. */\n weight: number;\n}\n\n/** Membership metadata for a bundle that participates in a link-group A/B test. */\nexport interface LinkGroupMembership {\n /** UUID shared across every variant in the test. */\n id: string;\n /** True when this bundle is the primary of its link group. */\n isPrimary: boolean;\n /**\n * Variant weight map — only populated on the primary's metaobject (the SDK\n * fans assignment out from the primary). Null on non-primary variants and\n * on primaries before a test has been configured.\n */\n variants: LinkGroupVariantRef[] | null;\n}\n\n/** Fields present on every bundle regardless of type. */\ninterface BundleBase {\n id: string;\n title: string;\n /** Customer-facing subtitle rendered under the bundle title. */\n description: string | null;\n status: BundleStatus;\n products: Product[];\n discountConfig: DiscountConfig;\n widgetConfig: WidgetConfig;\n startsAt: string | null;\n endsAt: string | null;\n discountLabel: string | null;\n\n /**\n * Per-product allowed-variant lists, positionally aligned with\n * `products`. When the merchant restricted a product to specific\n * variants, only those GIDs should appear in the picker and dropdown.\n * Inner array empty or whole entry null means \"all available variants\".\n */\n selectedVariantIds: string[][] | null;\n\n /** Per-product bundle quantity, keyed by Shopify Product GID. */\n productQuantities: Record<string, number>;\n /** Per-variant quantity override, keyed by Shopify ProductVariant GID. */\n variantQuantities: Record<string, number>;\n\n /**\n * Link-group A/B test membership. Null when this bundle is not part of\n * any test. The SDK's `useBundleData` hook reads `linkGroup.variants` on\n * the primary to drive variant resolution before render.\n */\n linkGroup: LinkGroupMembership | null;\n\n /**\n * Market visibility scoping.\n * - \"all\" (default): bundle is visible in every Shopify market.\n * - \"specific\": bundle is visible only in the markets listed in marketIds.\n *\n * Consumers querying with a marketId can use isVisibleInMarket() to filter.\n */\n marketVisibility: \"all\" | \"specific\";\n /**\n * Allowed Shopify Market GIDs when marketVisibility = \"specific\".\n * Empty when marketVisibility = \"all\".\n * Example: [\"gid://shopify/Market/1\", \"gid://shopify/Market/2\"]\n */\n marketIds: string[];\n}\n\nexport interface FixedBundleData extends BundleBase {\n bundleType: \"fixed\";\n}\n\nexport interface VolumeBundleData extends BundleBase {\n bundleType: \"volume\";\n volumeTiers: VolumeTier[];\n}\n\n/** Per-product unit-count rule for mix & match bundles. */\nexport interface ProductRule {\n /** Smallest qty the shopper must add when picking this product. */\n min: number;\n /** Largest qty the shopper may add for this pick. >= min. */\n max: number;\n}\n\n/** Default rule applied to products without an explicit entry (e.g. collection-sourced). */\nexport const DEFAULT_PRODUCT_RULE: ProductRule = { min: 1, max: 99 };\n\nexport interface MixMatchBundleData extends BundleBase {\n bundleType: \"mix_match\";\n /** Number of distinct products the shopper must pick to qualify for the discount. */\n minQuantity: number | null;\n /**\n * @deprecated removed from the data model — always null on parsed bundles.\n * The bundle no longer has a top-level total cap; per-product max lives in productRules.\n */\n maxQuantity: number | null;\n /** Per-product unit-count rules keyed by product GID. Missing entries default to DEFAULT_PRODUCT_RULE. */\n productRules: Record<string, ProductRule>;\n}\n\nexport type ParsedBundle =\n | FixedBundleData\n | VolumeBundleData\n | MixMatchBundleData;\n\n/**\n * Thrown when metaobject fields cannot be parsed into a valid ParsedBundle.\n * Distinct from StorefrontApiError so callers can handle \"bundle not\n * renderable\" (inactive, expired, schema mismatch) differently from\n * \"network failure\".\n */\nexport class BundleParseError extends Error {\n constructor(message: string, public readonly reason: BundleParseFailReason) {\n super(message);\n this.name = \"BundleParseError\";\n }\n}\n\nexport type BundleParseFailReason =\n | \"not_found\"\n | \"invalid_type\"\n | \"inactive\"\n | \"not_started\"\n | \"expired\";\n","/**\n * Default widget config + CSS-variable flattener.\n *\n * Mirrors the Zod defaults and `flattenWidgetConfig` in the admin package's\n * `app/lib/bundle-types.ts`. Kept in plain TypeScript so the SDK packages\n * stay Zod-free; merchant-authored config is validated at the admin\n * boundary before it reaches the metaobject, so consumers of this module\n * can trust the JSON shape and just merge against these defaults.\n *\n * If a future merchant-editor field is added:\n * 1. Update `WidgetConfig` in types.ts\n * 2. Add the default here\n * 3. Add the flattened CSS-var key below\n * 4. Consume the var in packages/widget/src/styles/*.css\n */\nimport type { WidgetConfig } from \"./types\";\n\nexport const WIDGET_CONFIG_DEFAULTS: WidgetConfig = {\n header: {\n textColor: \"#555555\",\n headerStyle: \"solid\",\n gradientStart: \"#C1E67A\",\n gradientEnd: \"#B2D572\",\n saveBadgeBgColor: \"#C1E67A\",\n saveBadgeTextColor: \"#555555\",\n saveBadgeBorderColor: \"#555555\",\n saveBadgeBorderWidth: 2,\n saveBadgeBorderRadius: 4,\n countdownBgColor: \"#B2D57247\",\n countdownTextColor: \"#555555\",\n },\n layout: {\n backgroundColor: \"#FCFCFC\",\n borderColor: \"#E5E5E5\",\n borderWidth: 1,\n borderRadius: 8,\n },\n productList: {\n textColor: \"#555555\",\n imageBorderWidth: 0,\n imageBorderColor: \"#E5E5E5\",\n imageBorderRadius: 4,\n thumbnailRatio: \"square\",\n variantBorderWidth: 1,\n variantBorderColor: \"#E5E5E5\",\n variantBorderRadius: 4,\n showPrice: true,\n showCompareAtPrice: true,\n showCountBubble: true,\n countBubbleBgColor: \"#E9F1D9\",\n countBubbleTextColor: \"#555555\",\n },\n pricing: {\n showSaveBadge: true,\n showComparePrice: true,\n showPerUnitPrice: true,\n showItemCount: true,\n showCompareAtPrice: true,\n },\n cta: {\n ctaText: \"Add to cart\",\n primaryColor: \"#C1E67A\",\n buttonTextColor: \"#555555\",\n borderWidth: 0,\n borderColor: \"#C1E67A\",\n borderRadius: 4,\n },\n savingsBar: {\n visible: true,\n bgColor: \"#B2D57247\",\n textColor: \"#555555\",\n borderWidth: 0,\n borderColor: \"#B2D57247\",\n borderRadius: 4,\n },\n countdown: {\n showCountdown: true,\n },\n popularBadge: {\n visible: true,\n text: \"Most Popular\",\n bgColor: \"#1A1A1A\",\n textColor: \"#FFFFFF\",\n borderWidth: 0,\n borderColor: \"#1A1A1A\",\n borderRadius: 12,\n },\n outOfStockBehavior: \"show_greyed_out\",\n showLowStockBadge: true,\n lowStockThreshold: 10,\n lowStockBgColor: \"#FEF3C7\",\n lowStockTextColor: \"#92400E\",\n\n showSearch: true,\n mixMatchShowQuantitySelector: true,\n pickerBgColor: \"#FCFCFC\",\n pickerTextColor: \"#555555\",\n pickerBorderWidth: 0,\n pickerBorderColor: \"#E5E5E5\",\n pickerBorderRadius: 8,\n pickerSearchBorderWidth: 1,\n pickerSearchBorderColor: \"#E5E5E5\",\n pickerSearchBorderRadius: 8,\n pickerProductBorderWidth: 0,\n pickerProductBorderColor: \"#E5E5E5\",\n pickerProductBorderRadius: 8,\n pickerThumbnailRatio: \"square\",\n pickerShowCountBubble: true,\n pickerCountBubbleBgColor: \"#E9F1D9\",\n pickerCountBubbleTextColor: \"#555555\",\n pickerAddBgColor: \"#C1E67A\",\n pickerAddLabelColor: \"#555555\",\n pickerAddBorderWidth: 0,\n pickerAddBorderColor: \"#C1E67A\",\n pickerAddBorderRadius: 4,\n pickerVariantBorderWidth: 1,\n pickerVariantBorderColor: \"#555555\",\n pickerVariantBorderRadius: 4,\n pickerQtyStepperBorderWidth: 1,\n pickerQtyStepperBorderColor: \"#555555\",\n pickerQtyStepperBorderRadius: 4,\n\n tierBorderColor: \"#E5E5E5\",\n tierBorderWidth: 1,\n tierBorderRadius: 12,\n tierSelectedBorderColor: \"#1A1A1A\",\n tierSelectedBorderWidth: 2,\n defaultTier: \"first\",\n};\n\n/**\n * Merge a partial (possibly stale-schema) widget config against the\n * defaults, so downstream renderers can trust every field. Top-level and\n * nested-section fields both merge shallow; unknown keys on nested sections\n * are preserved (but ignored by the renderer). This is what the parser\n * calls after pulling `widget_config` JSON from the metaobject.\n */\nexport function mergeWidgetConfig(raw: unknown): WidgetConfig {\n if (!raw || typeof raw !== \"object\") {\n return WIDGET_CONFIG_DEFAULTS;\n }\n const input = raw as Partial<WidgetConfig> & Record<string, unknown>;\n return {\n ...WIDGET_CONFIG_DEFAULTS,\n ...input,\n defaultTier: sanitizeDefaultTier(input.defaultTier),\n header: { ...WIDGET_CONFIG_DEFAULTS.header, ...(input.header ?? {}) },\n layout: { ...WIDGET_CONFIG_DEFAULTS.layout, ...(input.layout ?? {}) },\n productList: {\n ...WIDGET_CONFIG_DEFAULTS.productList,\n ...(input.productList ?? {}),\n },\n pricing: {\n ...WIDGET_CONFIG_DEFAULTS.pricing,\n ...(input.pricing ?? {}),\n },\n cta: { ...WIDGET_CONFIG_DEFAULTS.cta, ...(input.cta ?? {}) },\n savingsBar: {\n ...WIDGET_CONFIG_DEFAULTS.savingsBar,\n ...(input.savingsBar ?? {}),\n },\n countdown: {\n ...WIDGET_CONFIG_DEFAULTS.countdown,\n ...(input.countdown ?? {}),\n },\n popularBadge: {\n ...WIDGET_CONFIG_DEFAULTS.popularBadge,\n ...(input.popularBadge ?? {}),\n },\n };\n}\n\nfunction sanitizeDefaultTier(\n raw: unknown,\n): \"first\" | \"best_value\" | number {\n if (raw === \"first\" || raw === \"best_value\") return raw;\n if (typeof raw === \"number\" && Number.isInteger(raw) && raw >= 0) return raw;\n return WIDGET_CONFIG_DEFAULTS.defaultTier;\n}\n\n/**\n * Flatten the nested WidgetConfig to the CSS-custom-property keys consumed\n * by `packages/widget/src/styles/*.css`. Keys match the `--lb-*` variable\n * names (without the `--lb-` prefix). Keep in lockstep with the matching\n * function in `app/lib/bundle-types.ts` — the admin's in-app preview and\n * the shipped web component both read the same variable names, so drift\n * between the two maps is a merchant-visible rendering bug.\n */\nexport function flattenWidgetConfig(\n config: WidgetConfig,\n): Record<string, string | number | boolean> {\n return {\n primaryColor: config.cta.primaryColor,\n backgroundColor: config.layout.backgroundColor,\n textColor: config.productList.textColor,\n borderColor: config.layout.borderColor,\n borderWidth: config.layout.borderWidth,\n buttonTextColor: config.cta.buttonTextColor,\n ctaBorderWidth: config.cta.borderWidth,\n ctaBorderColor: config.cta.borderColor,\n ctaBorderRadius: config.cta.borderRadius,\n savingsBarBgColor: config.savingsBar.bgColor,\n savingsBarTextColor: config.savingsBar.textColor,\n savingsBarBorderWidth: config.savingsBar.borderWidth,\n savingsBarBorderColor: config.savingsBar.borderColor,\n savingsBarBorderRadius: config.savingsBar.borderRadius,\n borderRadius: config.layout.borderRadius,\n headerTextColor: config.header.textColor,\n saveBadgeBgColor: config.header.saveBadgeBgColor,\n saveBadgeTextColor: config.header.saveBadgeTextColor,\n saveBadgeBorderColor: config.header.saveBadgeBorderColor,\n saveBadgeBorderWidth: config.header.saveBadgeBorderWidth,\n saveBadgeBorderRadius: config.header.saveBadgeBorderRadius,\n countdownBgColor: config.header.countdownBgColor,\n countdownTextColor: config.header.countdownTextColor,\n headerStyle: config.header.headerStyle,\n headerGradientStart: config.header.gradientStart,\n headerGradientEnd: config.header.gradientEnd,\n showSaveBadge: config.pricing.showSaveBadge,\n imageBorderWidth: config.productList.imageBorderWidth,\n imageBorderColor: config.productList.imageBorderColor,\n imageBorderRadius: config.productList.imageBorderRadius,\n thumbnailRatio: config.productList.thumbnailRatio,\n variantBorderWidth: config.productList.variantBorderWidth,\n variantBorderColor: config.productList.variantBorderColor,\n variantBorderRadius: config.productList.variantBorderRadius,\n countBubbleBgColor: config.productList.countBubbleBgColor,\n countBubbleTextColor: config.productList.countBubbleTextColor,\n showProductPrice: config.productList.showPrice,\n showProductCompareAtPrice: config.productList.showCompareAtPrice,\n showCountBubble: config.productList.showCountBubble,\n showCountdown: config.countdown.showCountdown,\n ctaText: config.cta.ctaText,\n outOfStockBehavior: config.outOfStockBehavior,\n showLowStockBadge: config.showLowStockBadge,\n lowStockThreshold: config.lowStockThreshold,\n lowStockBgColor: config.lowStockBgColor,\n lowStockTextColor: config.lowStockTextColor,\n showComparePrice: config.pricing.showComparePrice,\n showPerUnitPrice: config.pricing.showPerUnitPrice,\n showItemCount: config.pricing.showItemCount,\n showCompareAtPrice: config.pricing.showCompareAtPrice,\n showMostPopular: config.popularBadge.visible,\n popularBadgeText: config.popularBadge.text,\n popularBadgeBgColor: config.popularBadge.bgColor,\n popularBadgeTextColor: config.popularBadge.textColor,\n popularBadgeBorderWidth: config.popularBadge.borderWidth,\n popularBadgeBorderColor: config.popularBadge.borderColor,\n popularBadgeBorderRadius: config.popularBadge.borderRadius,\n showSearch: config.showSearch,\n pickerBgColor: config.pickerBgColor,\n pickerTextColor: config.pickerTextColor,\n pickerBorderWidth: config.pickerBorderWidth,\n pickerBorderColor: config.pickerBorderColor,\n pickerBorderRadius: config.pickerBorderRadius,\n pickerSearchBorderWidth: config.pickerSearchBorderWidth,\n pickerSearchBorderColor: config.pickerSearchBorderColor,\n pickerSearchBorderRadius: config.pickerSearchBorderRadius,\n pickerProductBorderWidth: config.pickerProductBorderWidth,\n pickerProductBorderColor: config.pickerProductBorderColor,\n pickerProductBorderRadius: config.pickerProductBorderRadius,\n pickerThumbnailRatio: config.pickerThumbnailRatio,\n pickerShowCountBubble: config.pickerShowCountBubble,\n pickerCountBubbleBgColor: config.pickerCountBubbleBgColor,\n pickerCountBubbleTextColor: config.pickerCountBubbleTextColor,\n pickerAddBgColor: config.pickerAddBgColor,\n pickerAddLabelColor: config.pickerAddLabelColor,\n pickerAddBorderWidth: config.pickerAddBorderWidth,\n pickerAddBorderColor: config.pickerAddBorderColor,\n pickerAddBorderRadius: config.pickerAddBorderRadius,\n pickerVariantBorderWidth: config.pickerVariantBorderWidth,\n pickerVariantBorderColor: config.pickerVariantBorderColor,\n pickerVariantBorderRadius: config.pickerVariantBorderRadius,\n pickerQtyStepperBorderWidth: config.pickerQtyStepperBorderWidth,\n pickerQtyStepperBorderColor: config.pickerQtyStepperBorderColor,\n pickerQtyStepperBorderRadius: config.pickerQtyStepperBorderRadius,\n tierBorderColor: config.tierBorderColor,\n tierBorderWidth: config.tierBorderWidth,\n tierBorderRadius: config.tierBorderRadius,\n tierSelectedBorderColor: config.tierSelectedBorderColor,\n tierSelectedBorderWidth: config.tierSelectedBorderWidth,\n defaultTier: config.defaultTier,\n ...(config.popularBadge.tierIndex !== undefined && {\n mostPopularTierIndex: config.popularBadge.tierIndex,\n }),\n };\n}\n\n/**\n * Map from `flattenWidgetConfig` keys to the actual CSS custom property\n * names consumed by the `bundle-*.css` stylesheets. The names are bespoke,\n * not mechanical, so we can't derive them with a naive camel-to-kebab\n * transform. Mirror of `CSS_VAR_MAP` in\n * `app/components/WidgetEditorPreview.tsx`.\n */\nexport const CSS_VAR_MAP: Record<string, string> = {\n primaryColor: \"--lb-primary-color\",\n backgroundColor: \"--lb-bg\",\n textColor: \"--lb-text\",\n imageBorderWidth: \"--lb-image-border-width\",\n imageBorderColor: \"--lb-image-border-color\",\n imageBorderRadius: \"--lb-image-border-radius\",\n variantBorderWidth: \"--lb-variant-border-width\",\n variantBorderColor: \"--lb-variant-border-color\",\n variantBorderRadius: \"--lb-variant-radius\",\n countBubbleBgColor: \"--lb-qty-badge-bg\",\n countBubbleTextColor: \"--lb-qty-badge-color\",\n borderColor: \"--lb-border\",\n borderWidth: \"--lb-border-width\",\n buttonTextColor: \"--lb-btn-text\",\n borderRadius: \"--lb-radius\",\n headerTextColor: \"--lb-header-text\",\n saveBadgeBgColor: \"--lb-save-badge-bg\",\n saveBadgeTextColor: \"--lb-save-badge-text\",\n saveBadgeBorderColor: \"--lb-save-badge-border-color\",\n saveBadgeBorderWidth: \"--lb-save-badge-border-width\",\n saveBadgeBorderRadius: \"--lb-save-badge-radius\",\n countdownBgColor: \"--lb-countdown-bg\",\n countdownTextColor: \"--lb-countdown-text\",\n ctaBorderWidth: \"--lb-cta-border-width\",\n ctaBorderColor: \"--lb-cta-border-color\",\n ctaBorderRadius: \"--lb-cta-radius\",\n savingsBarBgColor: \"--lb-savings-bar-bg\",\n savingsBarTextColor: \"--lb-savings-bar-text\",\n savingsBarBorderWidth: \"--lb-savings-bar-border-width\",\n savingsBarBorderColor: \"--lb-savings-bar-border-color\",\n savingsBarBorderRadius: \"--lb-savings-bar-radius\",\n tierBorderColor: \"--lb-tier-border-color\",\n tierBorderWidth: \"--lb-tier-border-width\",\n tierBorderRadius: \"--lb-tier-radius\",\n tierSelectedBorderColor: \"--lb-tier-selected-border-color\",\n tierSelectedBorderWidth: \"--lb-tier-selected-border-width\",\n popularBadgeBgColor: \"--lb-popular-badge-bg\",\n popularBadgeTextColor: \"--lb-popular-badge-text\",\n popularBadgeBorderWidth: \"--lb-popular-badge-border-width\",\n popularBadgeBorderColor: \"--lb-popular-badge-border-color\",\n popularBadgeBorderRadius: \"--lb-popular-badge-radius\",\n headerGradientStart: \"--lb-header-start\",\n headerGradientEnd: \"--lb-header-end\",\n pickerBgColor: \"--lb-picker-bg\",\n pickerTextColor: \"--lb-picker-text\",\n pickerBorderWidth: \"--lb-picker-border-width\",\n pickerBorderColor: \"--lb-picker-border-color\",\n pickerBorderRadius: \"--lb-picker-radius\",\n pickerSearchBorderWidth: \"--lb-picker-search-border-width\",\n pickerSearchBorderColor: \"--lb-picker-search-border-color\",\n pickerSearchBorderRadius: \"--lb-picker-search-radius\",\n pickerProductBorderWidth: \"--lb-picker-product-border-width\",\n pickerProductBorderColor: \"--lb-picker-product-border-color\",\n pickerProductBorderRadius: \"--lb-picker-product-radius\",\n pickerCountBubbleBgColor: \"--lb-picker-qty-badge-bg\",\n pickerCountBubbleTextColor: \"--lb-picker-qty-badge-color\",\n pickerAddBgColor: \"--lb-picker-add-bg\",\n pickerAddLabelColor: \"--lb-picker-add-label\",\n pickerAddBorderWidth: \"--lb-picker-add-border-width\",\n pickerAddBorderColor: \"--lb-picker-add-border-color\",\n pickerAddBorderRadius: \"--lb-picker-add-radius\",\n pickerVariantBorderWidth: \"--lb-picker-variant-border-width\",\n pickerVariantBorderColor: \"--lb-picker-variant-border-color\",\n pickerVariantBorderRadius: \"--lb-picker-variant-radius\",\n pickerQtyStepperBorderWidth: \"--lb-picker-qty-stepper-border-width\",\n pickerQtyStepperBorderColor: \"--lb-picker-qty-stepper-border-color\",\n pickerQtyStepperBorderRadius: \"--lb-picker-qty-stepper-radius\",\n};\n\n/** Flat keys whose numeric value should be serialized with a `px` unit. */\nexport const PX_KEYS: ReadonlySet<string> = new Set([\n \"borderRadius\",\n \"borderWidth\",\n \"saveBadgeBorderWidth\",\n \"saveBadgeBorderRadius\",\n \"tierBorderWidth\",\n \"tierBorderRadius\",\n \"tierSelectedBorderWidth\",\n \"savingsBarBorderWidth\",\n \"savingsBarBorderRadius\",\n \"ctaBorderWidth\",\n \"ctaBorderRadius\",\n \"popularBadgeBorderWidth\",\n \"popularBadgeBorderRadius\",\n \"imageBorderWidth\",\n \"imageBorderRadius\",\n \"variantBorderWidth\",\n \"variantBorderRadius\",\n \"pickerBorderWidth\",\n \"pickerBorderRadius\",\n \"pickerSearchBorderWidth\",\n \"pickerSearchBorderRadius\",\n \"pickerProductBorderWidth\",\n \"pickerProductBorderRadius\",\n \"pickerAddBorderWidth\",\n \"pickerAddBorderRadius\",\n \"pickerVariantBorderWidth\",\n \"pickerVariantBorderRadius\",\n \"pickerQtyStepperBorderWidth\",\n \"pickerQtyStepperBorderRadius\",\n]);\n\n/**\n * Apply a WidgetConfig to a DOM element as CSS custom properties.\n *\n * Sets one `--lb-*` property per entry in CSS_VAR_MAP, px-suffixing numeric\n * dimensions, plus the four boolean toggles that the Liquid template emits\n * as display values (`block`, `inline`, `flex`, or `none`):\n *\n * - `--lb-product-price-display` from productList.showPrice\n * - `--lb-product-compare-display` from productList.showCompareAtPrice\n * - `--lb-qty-badge-display` from productList.showCountBubble\n * - `--lb-picker-qty-badge-display` from pickerShowCountBubble\n *\n * Anything the merchant didn't explicitly configure falls through to the\n * CSS file's own fallback value, so this call is safe on any element.\n */\nexport function applyWidgetConfigVars(\n el: { style: CSSStyleDeclaration },\n config: WidgetConfig,\n): void {\n const flat = flattenWidgetConfig(config);\n\n for (const [flatKey, cssVar] of Object.entries(CSS_VAR_MAP)) {\n const value = flat[flatKey];\n if (value === undefined || value === null) continue;\n const serialized = PX_KEYS.has(flatKey) ? `${value}px` : String(value);\n el.style.setProperty(cssVar, serialized);\n }\n\n // Conditional display toggles — merchant-configurable booleans that the\n // Liquid template emits as explicit display-mode CSS vars. We mirror that\n // exactly so the shipped bundle-*.css rules (which read these vars) hide\n // or show the right elements.\n el.style.setProperty(\n \"--lb-product-price-display\",\n config.productList.showPrice ? \"block\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-product-compare-display\",\n config.productList.showCompareAtPrice ? \"inline\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-qty-badge-display\",\n config.productList.showCountBubble ? \"flex\" : \"none\",\n );\n el.style.setProperty(\n \"--lb-picker-qty-badge-display\",\n config.pickerShowCountBubble ? \"flex\" : \"none\",\n );\n\n // Variant dropdown chevrons — embed the border color into the SVG stroke\n // so the arrow auto-matches. Mirrors the Liquid template in\n // `extensions/bundle-theme/blocks/bundle-widget.liquid` and the admin\n // preview helper in `app/components/WidgetEditorPreview.tsx`.\n el.style.setProperty(\n \"--lb-variant-chevron\",\n variantChevronUrl(config.productList.variantBorderColor),\n );\n el.style.setProperty(\n \"--lb-picker-variant-chevron\",\n variantChevronUrl(config.pickerVariantBorderColor),\n );\n\n // Header background — derived from headerStyle choice.\n if (config.header.headerStyle === \"solid\") {\n el.style.setProperty(\"--lb-header-bg\", config.header.gradientStart);\n } else {\n el.style.setProperty(\n \"--lb-header-bg\",\n `linear-gradient(135deg, ${config.header.gradientStart}, ${config.header.gradientEnd})`,\n );\n }\n\n // Thumbnail ratio — one merchant enum drives three CSS vars. \"original\"\n // disables aspect-ratio cropping (img renders at intrinsic ratio); the\n // other three values force a fixed aspect with object-fit: cover.\n const ratioVars = thumbnailRatioVars(config.productList.thumbnailRatio);\n el.style.setProperty(\"--lb-thumbnail-aspect-ratio\", ratioVars.aspectRatio);\n el.style.setProperty(\"--lb-thumbnail-img-fit\", ratioVars.imgFit);\n el.style.setProperty(\"--lb-thumbnail-img-height\", ratioVars.imgHeight);\n\n // Picker-modal thumbnail ratio — independent from the main widget so\n // merchants can e.g. show tall picker thumbs while keeping square main\n // thumbs. Reuses `thumbnailRatioVars` with a different CSS-var prefix.\n const pickerRatioVars = thumbnailRatioVars(config.pickerThumbnailRatio);\n el.style.setProperty(\n \"--lb-picker-thumbnail-aspect-ratio\",\n pickerRatioVars.aspectRatio,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-fit\",\n pickerRatioVars.imgFit,\n );\n el.style.setProperty(\n \"--lb-picker-thumbnail-img-height\",\n pickerRatioVars.imgHeight,\n );\n}\n\n/**\n * Build an inline-SVG `url(...)` value for the variant dropdown chevron,\n * with the given hex color embedded as the stroke. `#` must be URL-encoded\n * inside a data URI. Kept in lockstep with the Liquid template and admin\n * preview helper — any change to the path/size/stroke-width must update\n * all three.\n */\nfunction variantChevronUrl(strokeHex: string): string {\n const encoded = strokeHex.replace(/#/g, \"%23\");\n return `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' fill='none' stroke='${encoded}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")`;\n}\n\n/**\n * Map the merchant-facing thumbnail-ratio choice to the three CSS vars\n * the bundle stylesheets read. Exported so the Liquid template's mirror\n * of this logic stays in lockstep — any change to the value strings here\n * must be reflected in `bundle-widget.liquid` and the admin preview.\n */\nexport function thumbnailRatioVars(ratio: WidgetConfig[\"productList\"][\"thumbnailRatio\"]): {\n aspectRatio: string;\n imgFit: string;\n imgHeight: string;\n} {\n switch (ratio) {\n case \"tall\":\n return { aspectRatio: \"3 / 4\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"wide\":\n return { aspectRatio: \"4 / 3\", imgFit: \"cover\", imgHeight: \"100%\" };\n case \"original\":\n return { aspectRatio: \"auto\", imgFit: \"contain\", imgHeight: \"auto\" };\n case \"square\":\n default:\n return { aspectRatio: \"1 / 1\", imgFit: \"cover\", imgHeight: \"100%\" };\n }\n}\n","/**\n * Parse metaobject fields into a typed ParsedBundle (discriminated union).\n * Returns null if the bundle is not renderable (invalid type, inactive,\n * outside schedule) — callers who want structured reasons for \"not\n * renderable\" should use parseMetaobjectBundleStrict, which throws a\n * BundleParseError with a reason code.\n */\nimport type { MetaobjectField, Product } from \"../storefront-api/types\";\nimport {\n BundleParseError,\n type LinkGroupMembership,\n type LinkGroupVariantRef,\n type ParsedBundle,\n type BundleType,\n type BundleStatus,\n type DiscountConfig,\n type WidgetConfig,\n type VolumeTier,\n type FixedBundleData,\n type VolumeBundleData,\n type MixMatchBundleData,\n} from \"./types\";\nimport { mergeWidgetConfig } from \"./widget-config\";\n\nconst VALID_BUNDLE_TYPES = new Set<BundleType>([\"fixed\", \"mix_match\", \"volume\"]);\nconst ACTIVE_STATUSES = new Set<BundleStatus>([\"active\"]);\n\n/**\n * Parse a metaobject's fields array into a structured ParsedBundle.\n * Returns null if the bundle is not renderable.\n */\nexport function parseMetaobjectBundle(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle | null {\n try {\n return parseMetaobjectBundleStrict(metaobjectId, fields);\n } catch (err) {\n if (err instanceof BundleParseError) return null;\n throw err;\n }\n}\n\n/**\n * Same as parseMetaobjectBundle but throws BundleParseError with a\n * structured reason instead of returning null.\n */\nexport function parseMetaobjectBundleStrict(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle {\n const fieldMap = new Map(fields.map((f) => [f.key, f]));\n\n const title = fieldMap.get(\"title\")?.value ?? \"Bundle\";\n const rawBundleType = fieldMap.get(\"bundle_type\")?.value;\n const rawStatus = fieldMap.get(\"status\")?.value;\n\n if (!rawBundleType || !VALID_BUNDLE_TYPES.has(rawBundleType as BundleType)) {\n throw new BundleParseError(\n `Invalid or missing bundle_type: ${rawBundleType ?? \"null\"}`,\n \"invalid_type\",\n );\n }\n const bundleType = rawBundleType as BundleType;\n\n if (!rawStatus || !ACTIVE_STATUSES.has(rawStatus as BundleStatus)) {\n throw new BundleParseError(\n `Bundle is not active: status=${rawStatus ?? \"null\"}`,\n \"inactive\",\n );\n }\n const status = rawStatus as BundleStatus;\n\n const startsAt = fieldMap.get(\"starts_at\")?.value ?? null;\n const endsAt = fieldMap.get(\"ends_at\")?.value ?? null;\n const now = new Date();\n\n if (startsAt) {\n const start = new Date(startsAt);\n if (Number.isNaN(start.getTime())) {\n throw new BundleParseError(\n `Invalid starts_at: ${startsAt}`,\n \"invalid_type\",\n );\n }\n if (start > now) {\n throw new BundleParseError(\n `Bundle not yet started: starts_at=${startsAt}`,\n \"not_started\",\n );\n }\n }\n if (endsAt) {\n const end = new Date(endsAt);\n if (Number.isNaN(end.getTime())) {\n throw new BundleParseError(\n `Invalid ends_at: ${endsAt}`,\n \"invalid_type\",\n );\n }\n if (end < now) {\n throw new BundleParseError(\n `Bundle has expired: ends_at=${endsAt}`,\n \"expired\",\n );\n }\n }\n\n const products = resolveProducts(fieldMap);\n const discountConfig = parseDiscountConfig(fieldMap);\n const widgetConfig = parseWidgetConfig(fieldMap);\n\n const base = {\n id: metaobjectId,\n title,\n description: fieldMap.get(\"description\")?.value || null,\n status,\n products,\n discountConfig,\n widgetConfig,\n startsAt,\n endsAt,\n discountLabel: fieldMap.get(\"discount_label\")?.value ?? null,\n selectedVariantIds: parseSelectedVariantIds(fieldMap),\n productQuantities: parseNumericRecord(fieldMap, \"product_quantities\"),\n variantQuantities: parseNumericRecord(fieldMap, \"variant_quantities\"),\n linkGroup: parseLinkGroup(fieldMap),\n marketVisibility: parseMarketVisibility(fieldMap),\n marketIds: parseMarketIds(fieldMap),\n };\n\n switch (bundleType) {\n case \"fixed\": {\n const result: FixedBundleData = { ...base, bundleType: \"fixed\" };\n return result;\n }\n case \"volume\": {\n const result: VolumeBundleData = {\n ...base,\n bundleType: \"volume\",\n volumeTiers: parseVolumeTiers(fieldMap),\n };\n return result;\n }\n case \"mix_match\": {\n const result: MixMatchBundleData = {\n ...base,\n bundleType: \"mix_match\",\n minQuantity: parseIntField(fieldMap, \"min_quantity\", { min: 1 }),\n maxQuantity: null,\n productRules: parseProductRules(fieldMap),\n };\n return result;\n }\n }\n}\n\nfunction resolveProducts(fieldMap: Map<string, MetaobjectField>): Product[] {\n const products: Product[] = [];\n\n const productsField = fieldMap.get(\"products\");\n if (productsField?.reference && \"variants\" in productsField.reference) {\n products.push(productsField.reference as Product);\n }\n\n if (productsField?.references?.nodes) {\n for (const node of productsField.references.nodes) {\n if (\"variants\" in node) {\n products.push(node as Product);\n } else if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n const collectionField = fieldMap.get(\"collection\");\n if (collectionField?.references?.nodes) {\n for (const node of collectionField.references.nodes) {\n if (\"products\" in node && node.products?.nodes) {\n products.push(...node.products.nodes);\n }\n }\n }\n\n return products;\n}\n\nfunction parseDiscountConfig(\n fieldMap: Map<string, MetaobjectField>,\n): DiscountConfig {\n return {\n discountType:\n (fieldMap.get(\"discount_type\")?.value as \"percentage\" | \"fixed_amount\") ??\n \"percentage\",\n discountValue: parseFloat(fieldMap.get(\"discount_value\")?.value ?? \"0\"),\n allowStacking: fieldMap.get(\"allow_stacking\")?.value === \"true\",\n };\n}\n\n/**\n * Parse the merchant's widget_config metaobject field. The JSON is authored\n * by the Zod schema in app/lib/bundle-types.ts, so consumers can trust the\n * shape — but old bundles may be missing fields added after they were\n * created. mergeWidgetConfig fills those gaps from WIDGET_CONFIG_DEFAULTS,\n * so every renderer downstream gets a fully populated config.\n */\nfunction parseWidgetConfig(\n fieldMap: Map<string, MetaobjectField>,\n): WidgetConfig {\n return mergeWidgetConfig(parseJsonField(fieldMap, \"widget_config\"));\n}\n\n/**\n * Parse `selected_variant_ids`: a list of per-product variant-GID arrays,\n * positionally aligned with `products`. Returns null when the merchant\n * didn't restrict variants (any entry means full list allowed).\n */\nfunction parseSelectedVariantIds(\n fieldMap: Map<string, MetaobjectField>,\n): string[][] | null {\n const raw = parseJsonField(fieldMap, \"selected_variant_ids\");\n if (!Array.isArray(raw)) return null;\n const result: string[][] = [];\n for (const entry of raw) {\n if (Array.isArray(entry)) {\n result.push(\n entry.filter((x): x is string => typeof x === \"string\"),\n );\n } else {\n result.push([]);\n }\n }\n return result;\n}\n\n/**\n * Parse a JSON record of `{ [GID]: number }`. Used for\n * `product_quantities` and `variant_quantities`. Non-numeric values are\n * dropped; returns an empty object when the field is absent or malformed.\n */\nfunction parseNumericRecord(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): Record<string, number> {\n const raw = parseJsonField(fieldMap, key);\n // Arrays pass `typeof === \"object\"`. Reject them explicitly so a\n // malformed metaobject field (e.g. `[1, 2, 3]` where an object was\n // expected) doesn't produce a `{ \"0\": 1, \"1\": 2, ... }` record.\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const record: Record<string, number> = {};\n for (const [k, v] of Object.entries(raw)) {\n const num = typeof v === \"number\" ? v : Number(v);\n // Defense-in-depth: Zod enforces 0/1–99 at the write path. The parser\n // also clamps at read time so a metafield that bypasses Zod (seed script,\n // direct Admin API edit, manual Shopify CLI) can't feed `Number.MAX_SAFE_INTEGER`\n // downstream into `CartLineInput.quantity` — which would crash Hydrogen\n // or Storefront Cart API with an opaque error. Zero is preserved\n // (merchant-set opt-out); renderers filter zero-qty rows. Negative and\n // out-of-range values are dropped as malformed.\n if (Number.isFinite(num) && num >= 0 && num <= 99) {\n record[k] = Math.floor(num);\n }\n }\n return record;\n}\n\n/**\n * Parse the `product_rules` JSON metaobject field into a record of\n * `{ [productGid]: { min, max } }`. Drops malformed entries. Clamps min/max\n * to 1..99 and ensures `min <= max`. Returns an empty object when the field\n * is absent or unparseable.\n */\nfunction parseProductRules(\n fieldMap: Map<string, MetaobjectField>,\n): Record<string, { min: number; max: number }> {\n const raw = parseJsonField(fieldMap, \"product_rules\");\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return {};\n const out: Record<string, { min: number; max: number }> = {};\n for (const [pid, rule] of Object.entries(raw)) {\n if (!rule || typeof rule !== \"object\" || Array.isArray(rule)) continue;\n const { min, max } = rule as { min?: unknown; max?: unknown };\n const minN = typeof min === \"number\" ? min : Number(min);\n const maxN = typeof max === \"number\" ? max : Number(max);\n if (!Number.isFinite(minN) || !Number.isFinite(maxN)) continue;\n const minClamped = Math.max(1, Math.min(99, Math.floor(minN)));\n const maxClamped = Math.max(minClamped, Math.min(99, Math.floor(maxN)));\n out[pid] = { min: minClamped, max: maxClamped };\n }\n return out;\n}\n\nfunction parseMarketVisibility(\n fieldMap: Map<string, MetaobjectField>,\n): \"all\" | \"specific\" {\n const raw = fieldMap.get(\"market_visibility\")?.value;\n return raw === \"specific\" ? \"specific\" : \"all\";\n}\n\nfunction parseMarketIds(fieldMap: Map<string, MetaobjectField>): string[] {\n const raw = fieldMap.get(\"markets\")?.value;\n if (!raw) return [];\n try {\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return [];\n return parsed.filter((v): v is string => typeof v === \"string\");\n } catch {\n return [];\n }\n}\n\n/**\n * Parse link-group membership from the `link_group_*` metaobject fields. Returns\n * null when the bundle is not in any link group (link_group_id absent). The\n * variant weight list is only populated on the primary (link_group_is_primary\n * truthy) — variant bundles don't carry the list because the SDK always starts\n * resolution from the primary.\n */\nfunction parseLinkGroup(\n fieldMap: Map<string, MetaobjectField>,\n): LinkGroupMembership | null {\n const id = fieldMap.get(\"link_group_id\")?.value;\n if (!id) return null;\n const isPrimary = fieldMap.get(\"link_group_is_primary\")?.value === \"true\";\n let variants: LinkGroupVariantRef[] | null = null;\n if (isPrimary) {\n const raw = parseJsonField(fieldMap, \"link_group_variants\");\n if (Array.isArray(raw)) {\n variants = raw\n .filter((v): v is Record<string, unknown> => typeof v === \"object\" && v !== null)\n .map((v): LinkGroupVariantRef | null => {\n const metaobjectId =\n typeof v.metaobjectId === \"string\" ? v.metaobjectId : null;\n const weight =\n typeof v.weight === \"number\" && Number.isFinite(v.weight)\n ? Math.max(0, Math.min(100, Math.floor(v.weight)))\n : null;\n if (metaobjectId === null || weight === null) return null;\n return { metaobjectId, weight };\n })\n .filter((v): v is LinkGroupVariantRef => v !== null);\n }\n }\n return { id, isPrimary, variants };\n}\n\nfunction parseVolumeTiers(\n fieldMap: Map<string, MetaobjectField>,\n): VolumeTier[] {\n const raw = parseJsonField(fieldMap, \"volume_tiers\");\n if (!Array.isArray(raw)) return [];\n return raw\n .filter(\n (t): t is Record<string, unknown> => typeof t === \"object\" && t !== null,\n )\n .map(parseOneVolumeTier);\n}\n\n// Volume tiers are stored as `{ minQuantity, percentage?, amount? }` on the\n// metaobject (see admin VolumeTierSchema in app/lib/bundle-types.ts). The\n// parent bundle's `discountConfig.discountType` tells renderers which of the\n// two magnitude fields to use for each tier.\nfunction parseOneVolumeTier(t: Record<string, unknown>): VolumeTier {\n const minQty = Number(t.minQuantity ?? 0);\n const tier: VolumeTier = {\n // Reject NaN / Infinity / negatives — same discipline as\n // percentage/amount below. Malformed tiers collapse to 0 so the\n // renderer's bounds checks can drop them deterministically.\n minQuantity: Number.isFinite(minQty) && minQty >= 0 ? minQty : 0,\n };\n if (typeof t.percentage === \"number\" && Number.isFinite(t.percentage)) {\n tier.percentage = t.percentage;\n }\n if (typeof t.amount === \"number\" && Number.isFinite(t.amount)) {\n tier.amount = t.amount;\n }\n return tier;\n}\n\nfunction parseJsonField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n): unknown {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\nfunction parseIntField(\n fieldMap: Map<string, MetaobjectField>,\n key: string,\n options: { min?: number } = {},\n): number | null {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n const num = parseInt(value, 10);\n if (isNaN(num)) return null;\n if (options.min !== undefined && num < options.min) return null;\n return num;\n}\n","/**\n * Product-aware bundle fetch — the headless analogue of the classic Liquid\n * theme block.\n *\n * Classic-theme widget reads `product.metafields[\"$app\"].active_bundle_ids`\n * inside Liquid and renders every active bundle that applies. Headless\n * storefronts get the same behaviour via this helper: pass a product\n * handle, receive `ParsedBundle[]` for every active bundle configured\n * against that product.\n *\n * The caller is responsible for knowing the handle — either because their\n * storefront template already knows it (Hydrogen loader, Next.js param),\n * or because the widget auto-detects from `/products/<handle>` URLs.\n *\n * Non-active bundles (draft / inactive / scheduled / expired) are silently\n * skipped rather than throwing — the calling surface is a list, not a\n * single lookup. Surface errors only for genuine failures (network /\n * GraphQL / invalid schema).\n */\nimport { createStorefrontClient } from \"../storefront-api/client\";\nimport {\n BUNDLES_FOR_PRODUCT_QUERY,\n type BundlesForProductResponse,\n} from \"../storefront-api/queries\";\nimport { parseMetaobjectBundleStrict } from \"./parser\";\nimport { BundleParseError, type ParsedBundle } from \"./types\";\nimport type { MetaobjectField } from \"../storefront-api/types\";\n\nexport interface FetchBundlesForProductOptions {\n shopDomain: string;\n storefrontAccessToken: string;\n productHandle: string;\n /** Forwarded as `Shopify-Storefront-Buyer-IP` — required for SSR. */\n buyerIp?: string;\n signal?: AbortSignal;\n apiVersion?: string;\n}\n\n/**\n * Reasons a bundle is expected-not-renderable (skipped silently, not\n * surfaced as an error). Matches `BundleParseFailReason` minus\n * `not_found` / `invalid_type`, which indicate genuine data problems\n * worth propagating.\n */\nconst SKIPPABLE_REASONS = new Set([\"inactive\", \"not_started\", \"expired\"]);\n\nexport async function fetchBundlesForProduct(\n options: FetchBundlesForProductOptions,\n): Promise<ParsedBundle[]> {\n const client = createStorefrontClient({\n shopDomain: options.shopDomain,\n accessToken: options.storefrontAccessToken,\n buyerIp: options.buyerIp,\n apiVersion: options.apiVersion,\n });\n\n const data = await client.query<BundlesForProductResponse>(\n BUNDLES_FOR_PRODUCT_QUERY,\n { handle: options.productHandle },\n { signal: options.signal },\n );\n\n if (!data.product) {\n // Product not found for this handle. The widget's caller (merchant's\n // storefront) is presumably on a valid product page, so this likely\n // means the handle is wrong or the product is unpublished. Return\n // empty rather than throwing — the widget renders nothing in that\n // case, which is the right UX (bundle section invisible).\n return [];\n }\n\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n\n for (const ref of refs) {\n try {\n // The nested references query resolves products + collections to the\n // same shape BUNDLE_METAOBJECT_QUERY returns, so parser.ts doesn't\n // need a new code path.\n const bundle = parseMetaobjectBundleStrict(\n ref.id,\n ref.fields as MetaobjectField[],\n );\n bundles.push(bundle);\n } catch (err) {\n if (err instanceof BundleParseError && SKIPPABLE_REASONS.has(err.reason)) {\n continue;\n }\n throw err;\n }\n }\n\n return bundles;\n}\n","// Volume tier savings calculation. `discountType` lives on the parent\n// bundle (bundle.discountConfig.discountType); each tier carries the\n// magnitude in either `percentage` (whole-number, e.g. 10 = 10%) or\n// `amount` (currency units, e.g. 5.00 = $5 off per unit).\nimport type { VolumeTier } from \"./types\";\nimport type { DiscountConfig } from \"./types\";\n\nexport interface TierSavings {\n tier: VolumeTier;\n unitPrice: number;\n savings: number;\n savingsPercent: number;\n isActive: boolean;\n}\n\n/**\n * Calculate savings for each tier at a given base price and quantity.\n */\nexport function calculateTierSavings(\n tiers: VolumeTier[],\n basePrice: number,\n currentQuantity: number,\n discountType: DiscountConfig[\"discountType\"],\n): TierSavings[] {\n // Sort tiers by minQuantity ascending\n const sorted = [...tiers].sort((a, b) => a.minQuantity - b.minQuantity);\n\n return sorted.map((tier) => {\n const discount =\n discountType === \"percentage\"\n ? basePrice * ((tier.percentage ?? 0) / 100)\n : (tier.amount ?? 0);\n\n const unitPrice = Math.max(0, basePrice - discount);\n const savings = basePrice - unitPrice;\n const savingsPercent = basePrice > 0 ? (savings / basePrice) * 100 : 0;\n const isActive = currentQuantity >= tier.minQuantity;\n\n return { tier, unitPrice, savings, savingsPercent, isActive };\n });\n}\n\n/**\n * Get the active tier for a given quantity.\n * Returns the highest tier where quantity >= minQuantity.\n */\nexport function getActiveTier(\n tiers: VolumeTier[],\n quantity: number,\n): VolumeTier | null {\n const sorted = [...tiers].sort((a, b) => b.minQuantity - a.minQuantity);\n return sorted.find((t) => quantity >= t.minQuantity) ?? null;\n}\n","/**\n * Quantity constraint validation for mix-and-match bundles.\n */\n\nexport interface QuantityValidation {\n valid: boolean;\n totalQuantity: number;\n message: string | null;\n}\n\nexport function validateQuantity(\n totalQuantity: number,\n minQuantity: number | null,\n maxQuantity: number | null,\n): QuantityValidation {\n if (minQuantity !== null && totalQuantity < minQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at least ${minQuantity} item${minQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n if (maxQuantity !== null && totalQuantity > maxQuantity) {\n return {\n valid: false,\n totalQuantity,\n message: `Select at most ${maxQuantity} item${maxQuantity !== 1 ? \"s\" : \"\"}`,\n };\n }\n return { valid: true, totalQuantity, message: null };\n}\n","/**\n * Canonical resolver for a bundle's per-line quantity.\n *\n * Bundle configs carry two qty fields: `productQuantities` (keyed by product\n * GID) and `variantQuantities` (keyed by variant GID, overrides product-level\n * when the customer picks that variant). The admin form strips product-level\n * entries that equal the default 1, so downstream consumers must consult\n * variantQuantities first and fall back to productQuantities then to 1.\n *\n * Before this helper existed, the fallback chain was reimplemented in five\n * places (React FixedBundle, React MixMatchBundle, headless widget fixed\n * renderer, Liquid mix-match IIFE, and the Rust discount function). They\n * drifted — PR #147 fixed the resulting cart-line split. Use this helper\n * everywhere the config is available to keep the TS/React/widget runtimes\n * in lockstep. The Liquid IIFE and Rust function have their own\n * implementations pinned by parity tests.\n */\n\nexport interface BundleQtySource {\n variantQuantities: Record<string, number>;\n productQuantities: Record<string, number>;\n}\n\n/**\n * Returns the configured qty for a cart line: `variantQuantities[variantId]`\n * if present, else `productQuantities[productId]`, else 1.\n *\n * Using `!== undefined` (not `??`) for the variant check so that a merchant\n * explicitly setting `0` is honoured — caller is expected to filter zero-qty\n * lines out, matching the headless widget's opt-out semantics.\n */\nexport function resolveBundleQty(\n bundle: BundleQtySource,\n productId: string,\n variantId: string,\n): number {\n const vq = bundle.variantQuantities[variantId];\n if (vq !== undefined) return vq;\n return bundle.productQuantities[productId] ?? 1;\n}\n","/**\n * Analytics event reporter for headless widgets.\n * Fires events to /api/analytics with single retry on failure.\n */\n\nexport interface AnalyticsConfig {\n shopDomain: string;\n appUrl: string; // Base URL of the Lime Bundles app\n}\n\nexport async function reportImpression(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n productId?: string;\n /**\n * Link-group UUID when the bundle is part of an A/B test. Promoted from\n * the prior `abTestId` / `abVariant` pair because each variant is now\n * its own bundle row — `bundleGid` already identifies the assigned\n * variant; `linkGroupId` is the convenience join key for analytics.\n */\n linkGroupId?: string;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_impression\",\n bundleGid: event.bundleGid,\n bundleType: event.bundleType,\n productId: event.productId,\n linkGroupId: event.linkGroupId,\n occurredAt: new Date().toISOString(),\n });\n}\n\nexport async function reportAddToCart(\n config: AnalyticsConfig,\n event: {\n bundleGid: string;\n bundleType: string;\n /**\n * Product GID the widget is rendered on. Optional because multi-product\n * bundles (fixed, mix-match) have no single \"page product\" — the event\n * is already attributed to the bundle via `bundleGid`. Volume bundles\n * typically pass the anchor product's GID. Previously this was required\n * and consumers defaulted to `products[0]?.id ?? \"\"` for multi-product\n * bundles, which was misleading.\n */\n productId?: string;\n quantity: number;\n totalPrice: number;\n linkGroupId?: string;\n },\n): Promise<void> {\n await sendEvent(config, {\n shopDomain: config.shopDomain,\n eventType: \"bundle_add_to_cart\",\n ...event,\n occurredAt: new Date().toISOString(),\n });\n}\n\n/**\n * IntersectionObserver-based impression tracking.\n * Fires once when element is 50% visible, then unobserves.\n */\nexport function observeImpression(\n element: Element,\n callback: () => void,\n): () => void {\n if (typeof IntersectionObserver === \"undefined\") {\n // Fallback: fire immediately if IntersectionObserver not available\n callback();\n return () => {};\n }\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n callback();\n observer.unobserve(entry.target);\n }\n }\n },\n { threshold: 0.5 },\n );\n\n observer.observe(element);\n return () => observer.disconnect();\n}\n\n// --- Internal ---\n\nasync function sendEvent(\n config: AnalyticsConfig,\n payload: Record<string, unknown>,\n): Promise<void> {\n const url = `${config.appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n\n if (!response.ok) {\n // Single retry after 2 seconds\n await new Promise((r) => setTimeout(r, 2000));\n await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n });\n }\n } catch {\n // Fire-and-forget — never block UX\n try {\n // Use sendBeacon as last resort\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n } catch {\n // Silently drop\n }\n }\n}\n","/**\n * Merchant-facing consent gate for A/B bucket persistence.\n *\n * Why this exists:\n *\n * On classic-theme Shopify storefronts, `window.Shopify.customerPrivacy`\n * exposes GDPR consent state. We can gate A/B cookie writes on that.\n *\n * On headless storefronts (Hydrogen, custom Next.js, etc.) there is no\n * universal consent primitive — the merchant is the privacy controller,\n * not Shopify. GDPR Art. 6(1)(a) requires opt-in consent for analytics-\n * related cookies, so the SDK must default to deny and accept explicit\n * opt-in from the merchant.\n *\n * Usage:\n *\n * import { setConsent } from \"@lime-bundles/core\";\n *\n * // In the merchant's consent-banner accept handler:\n * setConsent(true);\n *\n * // To revoke (e.g. on consent-banner decline):\n * setConsent(false);\n *\n * The A/B assigner calls `hasConsent()` before writing the bucket cookie.\n * Without consent, the assigner returns a stable \"control\" bucket and\n * writes no cookie — no tracking.\n *\n * SSR / edge runtimes:\n *\n * Both functions are no-ops on the server. The module-scoped flag would\n * otherwise leak across requests in SSR (Hydrogen, Next.js Server\n * Components, edge workers), so `setConsent` won't mutate state when\n * `document` is unavailable, and `hasConsent` returns false. Server\n * renders default to the deny path (Variant A, no cookies); the real\n * decision happens on the client when the consent banner accepts.\n */\n\nlet consentGranted = false;\n\n/** Browser-only — module-scope state means consent can't be safely\n * read or written on the server. */\nfunction isBrowser(): boolean {\n return typeof document !== \"undefined\";\n}\n\n/**\n * Merchant signals explicit opt-in (true) or revocation (false) for\n * A/B-related cookie and analytics writes. No-op on the server.\n */\nexport function setConsent(allowed: boolean): void {\n if (!isBrowser()) return;\n consentGranted = allowed;\n}\n\n/**\n * Check if writes are permitted. Returns true when:\n * (a) the merchant has called `setConsent(true)`, OR\n * (b) Shopify's `customerPrivacy.analyticsProcessingAllowed()` returns true\n * (classic-theme / hosted checkout context).\n *\n * Returns false by default (GDPR-safe), and always returns false on\n * the server.\n */\nexport function hasConsent(): boolean {\n if (!isBrowser()) return false;\n if (consentGranted) return true;\n if (typeof window === \"undefined\") return false;\n\n // Type cast — Shopify global isn't typed in headless TS environments.\n const shopifyGlobal = (\n window as unknown as {\n Shopify?: {\n customerPrivacy?: {\n analyticsProcessingAllowed?: () => boolean;\n };\n };\n }\n ).Shopify;\n\n try {\n return shopifyGlobal?.customerPrivacy?.analyticsProcessingAllowed?.() === true;\n } catch {\n return false;\n }\n}\n\n/** @internal Test-only: reset the consent flag between tests. */\nexport function __resetConsent(): void {\n consentGranted = false;\n}\n","/**\n * A/B link-group variant assignment for headless widgets.\n *\n * Under the duplicate-bundle model every test variant is its own bundle row\n * (with its own metaobject + discount node), and the test config is a weighted\n * array of `{ metaobjectId, weight }`. This module picks ONE variant for the\n * current shopper based on a deterministic FNV-1a hash of `sessionId:linkGroupId`.\n *\n * Persistence\n * -----------\n * Writes a first-party `__Host-_lb_ab_${linkGroupId}` cookie carrying the\n * assigned variant's metaobject GID. The `__Host-` prefix is a browser-enforced\n * hardening: requires `Secure`, `Path=/`, no `Domain=` — the cookie is locked\n * to the exact origin that set it. The cookie value is NOT HMAC-signed in\n * this client-side path; the server-side issuance path (app proxy) is the one\n * that signs cookies via `signABAssignmentCookie` (see app/lib/hmac.server.ts).\n *\n * GDPR default: DENY\n * ------------------\n * Returns a null assignment (caller falls through to the primary) and writes\n * no cookie when consent is absent. Storefront-classic merchants get consent\n * from `window.Shopify.customerPrivacy` via the `hasConsent()` gate; headless\n * merchants must call `setConsent(true)` from `@lime-bundles/core` in their\n * own consent-banner handler.\n *\n * Determinism + parity\n * --------------------\n * Same `sessionId + linkGroupId` always produces the same bucket: a shopper\n * who revokes consent and re-grants it later still lands on the same variant.\n * The FNV-1a constants below are mirrored in `extensions/bundle-theme/assets/bundle-widget.js`\n * and exercised by the parity test in P8 so analytics and storefront agree.\n */\nimport { hasConsent } from \"./consent\";\n\nconst SESSION_COOKIE_NAME = \"lb_session\";\nconst SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\nconst AB_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\n\n/** One variant entry from `ABTest.variantWeights` JSON. */\nexport interface LinkGroupVariantWeight {\n /** Shopify metaobject GID — the identity used in the cookie. */\n metaobjectId: string;\n /** Integer 0-100; weights across a link group sum to exactly 100. */\n weight: number;\n}\n\nexport interface LinkGroupAssignment {\n /** Metaobject GID of the assigned variant. */\n variantMetaobjectId: string;\n /** Whether the assignment was persisted (cookie + consent granted). */\n persisted: boolean;\n /**\n * True when this assignment was returned because the test was marked\n * force-live for a specific variant — the cookie is NOT updated in that\n * case so the original split assignment is preserved if force-live clears.\n */\n forcedLive: boolean;\n}\n\nfunction abCookieName(linkGroupId: string): string {\n // __Host- prefix requires Secure + Path=/ + no Domain attribute. Browsers\n // reject the cookie at write time when any of those are missing, so the\n // prefix doubles as a defense-in-depth check against misconfigured\n // deployments.\n return `__Host-_lb_ab_${linkGroupId}`;\n}\n\nexport interface GetAssignmentOptions {\n /**\n * Optional force-live override. When set, returns this metaobjectId with\n * forcedLive=true and bypasses both the cookie cache and the FNV-1a bucket\n * walk. Servers stamp `data-link-group-force-live` on the widget root\n * when `ABTest.trafficMode === \"force_live\"`; the caller passes it through\n * here. Precedence: force-live > cookie > FNV-1a > primary fallback.\n */\n forceLiveMetaobjectId?: string;\n}\n\n/**\n * Resolve the variant for the current shopper.\n *\n * Order of precedence (deepen plan Appendix A):\n * 1. force-live override (caller-supplied) — never written to the cookie.\n * 2. Existing __Host-_lb_ab_<linkGroupId> cookie value, if it's still in\n * the variant list (a winner-promotion archives losers; their cookie\n * values must fall through to a fresh assignment).\n * 3. FNV-1a cumulative-weight walk over `variants`.\n * 4. null — caller falls through to the primary.\n */\nexport async function getLinkGroupAssignment(\n linkGroupId: string,\n variants: readonly LinkGroupVariantWeight[],\n options: GetAssignmentOptions = {},\n): Promise<LinkGroupAssignment | null> {\n if (typeof document === \"undefined\") return null; // SSR guard\n if (variants.length === 0) return null;\n\n const validGids = new Set(variants.map((v) => v.metaobjectId));\n\n // 1. Force-live override — must be a real variant in the group.\n if (options.forceLiveMetaobjectId && validGids.has(options.forceLiveMetaobjectId)) {\n return {\n variantMetaobjectId: options.forceLiveMetaobjectId,\n persisted: false,\n forcedLive: true,\n };\n }\n\n // 2. Existing cookie — still valid if its GID is in the live variants list.\n const existing = readABCookie(linkGroupId);\n if (existing && validGids.has(existing)) {\n return { variantMetaobjectId: existing, persisted: true, forcedLive: false };\n }\n\n // 3. FNV-1a walk — requires consent before we touch any tracking cookie.\n if (!hasConsent()) {\n return null;\n }\n const sessionId = getOrCreateSessionId();\n const assigned = pickVariantFromWeights(sessionId, linkGroupId, variants);\n if (!assigned) return null;\n\n writeABCookie(linkGroupId, assigned);\n\n return { variantMetaobjectId: assigned, persisted: true, forcedLive: false };\n}\n\nfunction readABCookie(linkGroupId: string): string | null {\n if (typeof document === \"undefined\") return null;\n const name = abCookieName(linkGroupId);\n const prefix = `${name}=`;\n const cookies = document.cookie.split(\";\").map((c) => c.trim());\n const raw = cookies.find((c) => c.startsWith(prefix));\n if (!raw) return null;\n const value = raw.substring(prefix.length);\n // Value is the variant's metaobject GID (URL-encoded `gid://shopify/Metaobject/N`).\n // Decode + sanity-check shape — never trust shopper-edited cookies blindly.\n let decoded: string;\n try {\n decoded = decodeURIComponent(value);\n } catch {\n return null;\n }\n if (!decoded.startsWith(\"gid://shopify/Metaobject/\")) return null;\n return decoded;\n}\n\nfunction writeABCookie(linkGroupId: string, variantMetaobjectId: string): void {\n if (typeof document === \"undefined\") return;\n const name = abCookieName(linkGroupId);\n const value = encodeURIComponent(variantMetaobjectId);\n // __Host- prefix requires Secure + Path=/ + no Domain. SameSite=Lax matches\n // the server-side cookie issuance pattern (signABAssignmentCookie consumers).\n document.cookie =\n `${name}=${value}; Path=/; Max-Age=${AB_COOKIE_MAX_AGE}; ` +\n `SameSite=Lax; Secure`;\n}\n\nfunction getOrCreateSessionId(): string {\n if (typeof document === \"undefined\") return generateUUID();\n\n const cookies = document.cookie.split(\";\").map((c) => c.trim());\n const existing = cookies\n .find((c) => c.startsWith(`${SESSION_COOKIE_NAME}=`))\n ?.split(\"=\")[1];\n\n if (existing) return existing;\n\n const id = generateUUID();\n // `Secure` gates the cookie to HTTPS origins. Local dev on http://localhost\n // is a no-op rather than a rejection, so this is safe on both prod + dev.\n document.cookie = `${SESSION_COOKIE_NAME}=${id}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;\n return id;\n}\n\nfunction generateUUID(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * FNV-1a cumulative-weight walk. Mirrors `LB.abTest.assignVariantFromWeights`\n * in extensions/bundle-theme/assets/bundle-widget.js — keep the two in lockstep\n * (the parity golden test in P8 enforces this).\n */\nexport function pickVariantFromWeights(\n sessionId: string,\n linkGroupId: string,\n variants: readonly LinkGroupVariantWeight[],\n): string | null {\n if (variants.length === 0) return null;\n const bucket = fnv1a(`${sessionId}:${linkGroupId}`) % 10000;\n let cumulative = 0;\n for (const v of variants) {\n if (\n typeof v?.metaobjectId !== \"string\" ||\n typeof v?.weight !== \"number\"\n ) {\n continue;\n }\n cumulative += Math.max(0, v.weight) * 100; // 0..100 → 0..10000\n if (bucket < cumulative) return v.metaobjectId;\n }\n // Cap fallback: last variant. Matches the storefront widget JS so analytics\n // and SDK agree under degenerate inputs.\n return variants[variants.length - 1].metaobjectId;\n}\n\nexport function fnv1a(input: string): number {\n let hash = 0x811c9dc5; // FNV offset basis\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193); // FNV prime\n }\n return hash >>> 0;\n}\n","/**\n * CSS sanitization for merchant-authored custom CSS.\n *\n * Kept in lockstep with `app/lib/sanitize-css.ts`. When updating one,\n * update the other — the app-side sanitizer runs on merchant INPUT when\n * CSS is saved to the shop metafield, and this SDK-side sanitizer runs\n * on OUTPUT before the CSS is injected into a <style> tag on the\n * storefront. Both must enforce the same rules; a drift means either\n * the merchant's saved CSS fails to render on headless, or malicious\n * CSS sneaks through one side but not the other.\n *\n * 5-step pipeline:\n * 1. Strip comments (prevents keyword obfuscation like @im/**\\/port)\n * 2. Decode unicode escapes (prevents \\0040import → @import bypass)\n * 3. Strip HTML angle brackets (prevents </style><script> breakout)\n * 4. Reject blocked CSS features (@import, expression(), behavior, ...)\n * 5. Allowlist url() values (https://, relative, fragment only)\n */\n\nexport const MAX_CSS_LENGTH = 10_000;\n\nconst BLOCKED_PATTERNS: ReadonlyArray<[RegExp, string]> = [\n [/@import/i, \"@import rules\"],\n [/@charset/i, \"@charset declarations\"],\n [/expression\\s*\\(/i, \"CSS expressions\"],\n [/-moz-binding/i, \"-moz-binding\"],\n [/-webkit-binding/i, \"-webkit-binding\"],\n [/behavior\\s*:/i, \"behavior property\"],\n];\n\nconst SAFE_URL_VALUE = /^(https:|\\/[^/]|\\.\\/|\\.\\.\\/|#)/;\n\nexport type SanitizeResult =\n | { ok: true; css: string }\n | { ok: false; error: string };\n\nexport function sanitizeCustomCss(raw: string): SanitizeResult {\n if (raw.length > MAX_CSS_LENGTH) {\n return {\n ok: false,\n error: `CSS exceeds ${MAX_CSS_LENGTH.toLocaleString(\"en-US\")} character limit`,\n };\n }\n\n let css = raw.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\");\n\n try {\n // 1. Decode hex unicode escapes: \\XXXXXX → codepoint. CSS lets authors\n // obfuscate keywords this way (e.g. `\\0040import` → `@import`).\n css = css.replace(/\\\\([0-9a-fA-F]{1,6})[ \\t\\n\\f]?/g, (_, hex) => {\n const codePoint = parseInt(hex, 16);\n if (codePoint < 0 || codePoint > 0x10ffff) return \"\\uFFFD\";\n return String.fromCodePoint(codePoint);\n });\n // 2. Fold backslash + newline (line continuation per CSS spec).\n css = css.replace(/\\\\\\r?\\n/g, \"\");\n // 3. Decode character escapes: \\c → c (backslash + non-hex non-newline).\n // Without this, `@\\i\\mport` would slip past our keyword blocklist.\n css = css.replace(/\\\\([^\\n\\r\\f0-9a-fA-F])/g, \"$1\");\n } catch {\n return {\n ok: false,\n error: \"CSS contains invalid unicode escape sequences\",\n };\n }\n\n css = css.replace(/</g, \"\").replace(/>/g, \"\");\n\n for (const [pattern, label] of BLOCKED_PATTERNS) {\n if (pattern.test(css)) {\n return { ok: false, error: `CSS contains blocked pattern: ${label}` };\n }\n }\n\n // url() extraction: match ANY opening paren → closing paren content,\n // regardless of whether quotes match. Previous regex used `\\1` backref\n // which allowed malformed inputs like `url('javascript:alert(1))` to\n // slip past (mismatched quote → no match → no validation). Now every\n // url(...) is extracted and validated.\n const urlPattern = /url\\s*\\(\\s*([\\s\\S]*?)\\s*\\)/gi;\n let urlMatch: RegExpExecArray | null;\n while ((urlMatch = urlPattern.exec(css)) !== null) {\n let urlValue = urlMatch[1].trim();\n // Strip a single matching or unmatched quote from either end.\n if (urlValue.startsWith(\"'\") || urlValue.startsWith('\"')) {\n urlValue = urlValue.slice(1);\n }\n if (urlValue.endsWith(\"'\") || urlValue.endsWith('\"')) {\n urlValue = urlValue.slice(0, -1);\n }\n urlValue = urlValue.trim();\n if (urlValue && !SAFE_URL_VALUE.test(urlValue)) {\n return {\n ok: false,\n error: \"CSS url() values must use https:// or relative paths\",\n };\n }\n }\n\n return { ok: true, css };\n}\n","/**\n * Merchant custom-CSS injection into the storefront DOM.\n *\n * The headless SDK fetches `$app:custom_css` from the shop metafield\n * alongside bundle data, runs the sanitizer, and injects the output as\n * a <style> tag scoped to a deterministic id. Dedup is by id — if a\n * merchant has two <lime-bundle> elements on the same page, only one\n * <style> tag appears.\n *\n * SSR-safe: no-op when `document` is undefined.\n */\nimport { sanitizeCustomCss } from \"./sanitize\";\n\nconst STYLE_ID_PREFIX = \"lb-custom-css-\";\n\n/**\n * Inject merchant CSS into the document head. Idempotent — calling\n * repeatedly with the same shop updates the existing <style> tag in\n * place rather than appending new ones.\n *\n * Returns true if CSS was injected (or updated), false if skipped\n * (SSR, empty CSS, or sanitization rejected the input).\n */\nexport function injectCustomCss(\n shopDomain: string,\n rawCss: string | null | undefined,\n): boolean {\n if (typeof document === \"undefined\") return false;\n if (!rawCss) return false;\n\n const sanitized = sanitizeCustomCss(rawCss);\n if (!sanitized.ok) return false;\n if (!sanitized.css.trim()) return false;\n\n const id = STYLE_ID_PREFIX + simpleHash(shopDomain);\n\n let style = document.getElementById(id) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement(\"style\");\n style.id = id;\n style.setAttribute(\"data-lime-bundles\", \"custom-css\");\n document.head.appendChild(style);\n }\n\n // Only touch textContent when it actually changed — avoids triggering\n // layout or style-recalc cycles on repeat invocations.\n if (style.textContent !== sanitized.css) {\n style.textContent = sanitized.css;\n }\n return true;\n}\n\n/** FNV-1a — short, fast, DOM-id-safe hash. Not cryptographic. */\nfunction simpleHash(input: string): string {\n let hash = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n hash ^= input.charCodeAt(i);\n hash = Math.imul(hash, 0x01000193);\n }\n return (hash >>> 0).toString(16);\n}\n","/**\n * Price formatting utilities.\n */\nimport type {\n Money,\n UnitPriceMeasurement,\n UnitPriceMeasurementUnit,\n} from \"../storefront-api/types\";\n\nexport function formatMoney(amount: string | number, currencyCode: string): string {\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(num);\n } catch {\n return `${currencyCode} ${num.toFixed(2)}`;\n }\n}\n\nexport function calculateDiscount(\n price: number,\n discountType: \"percentage\" | \"fixed_amount\",\n discountValue: number,\n): number {\n if (discountType === \"percentage\") {\n return Math.max(0, price * (1 - discountValue / 100));\n }\n return Math.max(0, price - discountValue);\n}\n\nconst UNIT_LABEL: Record<UnitPriceMeasurementUnit, string> = {\n CL: \"cl\",\n CM: \"cm\",\n FLOZ: \"fl oz\",\n FT: \"ft\",\n FT2: \"ft²\",\n G: \"g\",\n GAL: \"gal\",\n IN: \"in\",\n ITEM: \"each\",\n KG: \"kg\",\n L: \"l\",\n LB: \"lb\",\n M: \"m\",\n M2: \"m²\",\n M3: \"m³\",\n MG: \"mg\",\n ML: \"ml\",\n MM: \"mm\",\n OZ: \"oz\",\n PT: \"pt\",\n QT: \"qt\",\n UNKNOWN: \"\",\n YD: \"yd\",\n};\n\n/**\n * Format a Storefront `unitPrice` + `unitPriceMeasurement` pair as a\n * compact display string (\"$0.50/100ml\", \"$12.99/kg\", \"$2.00/each\").\n *\n * Returns `null` when either input is absent — the merchant hasn't\n * configured unit pricing in the Shopify admin and no UI should render.\n *\n * The reference value is omitted when it equals 1 (\"$X.XX/kg\" rather\n * than \"$X.XX/1kg\") to match the format Shopify's `money` filter +\n * `unit_price_with_measurement` produce in Liquid themes.\n */\nexport function formatUnitPrice(\n unitPrice: Money | null | undefined,\n measurement: UnitPriceMeasurement | null | undefined,\n fallbackCurrency?: string,\n): string | null {\n if (!unitPrice || !measurement || measurement.referenceUnit === \"UNKNOWN\") {\n return null;\n }\n\n // Guard against malformed Money.amount strings — Storefront responses\n // are always valid decimals, but a non-finite number would silently\n // render \"$NaN\" via Intl.NumberFormat.format(NaN).\n const amount = parseFloat(unitPrice.amount);\n if (!Number.isFinite(amount)) return null;\n\n // `?? \"\"` + `if (!label)` is defence against Shopify adding enum values\n // we don't yet know about — the type-level lookup would still succeed\n // but the runtime value would be undefined.\n const label = UNIT_LABEL[measurement.referenceUnit] ?? \"\";\n if (!label) return null;\n\n const currency = unitPrice.currencyCode || fallbackCurrency || \"USD\";\n const formatted = formatMoney(amount, currency);\n const value = measurement.referenceValue;\n const measurementText = value === 1 ? label : `${value}${label}`;\n return `${formatted}/${measurementText}`;\n}\n","// Integer-cent arithmetic that matches Shopify's Discount Function\n// per-unit floor rounding — naive `total * (1 - discount)` drifts a\n// cent or two from what the customer actually pays.\nimport type { FixedBundleData, DiscountConfig } from \"../bundle/types\";\n\nexport interface PricingRow {\n /** Pre-discount unit price in cents. */\n unitCents: number;\n /** Quantity of this product in the bundle. */\n qty: number;\n /** Pre-discount line subtotal in cents (`unitCents * qty`). */\n lineCents: number;\n /** Variant's compare-at price in cents, if different from unit. */\n compareCents: number | null;\n}\n\nexport interface FixedBundlePricing {\n rows: PricingRow[];\n /** Pre-discount bundle total in cents. */\n totalCents: number;\n /** Post-discount bundle total in cents. */\n saleCents: number;\n /** Savings in cents — always `max(0, totalCents - saleCents)`. */\n savingsCents: number;\n /** Pre-formatted header badge text (e.g. `-75%`, `-$12.00`). Empty when `pricing.showSaveBadge` is false or no savings. */\n headerBadge: string;\n currency: string;\n}\n\n/**\n * Parse a money amount (Storefront API returns major-unit strings like\n * \"749.95\") into integer cents. Safe on numbers, null, undefined.\n * Rounds to the nearest cent to absorb float-representation drift.\n */\nexport function parseCents(\n amount: string | number | null | undefined,\n): number {\n if (amount === null || amount === undefined) return 0;\n const num = typeof amount === \"string\" ? parseFloat(amount) : amount;\n if (!Number.isFinite(num)) return 0;\n return Math.round(num * 100);\n}\n\n/**\n * Format integer cents as localized currency. Falls back to\n * `CUR 12.34` when the runtime's Intl.NumberFormat rejects the code.\n */\nexport function formatCents(cents: number, currencyCode: string): string {\n try {\n return new Intl.NumberFormat(undefined, {\n style: \"currency\",\n currency: currencyCode,\n }).format(cents / 100);\n } catch {\n return `${currencyCode} ${(cents / 100).toFixed(2)}`;\n }\n}\n\n/**\n * Apply a percentage discount to a single unit with floor rounding —\n * matches the rounding Shopify's Discount Function performs at checkout.\n * For a bundle total, apply this per-unit and sum, rather than computing\n * a single discount on the totalled line — those paths can differ by a\n * cent or two when the per-unit price has odd cents.\n */\nexport function percentageDiscountUnit(\n unitCents: number,\n percent: number,\n): number {\n return Math.max(0, unitCents - Math.floor((unitCents * percent) / 100));\n}\n\n/**\n * Compute the full pricing snapshot for a fixed bundle. Handles\n * percentage and fixed-amount discount types, per-product quantities,\n * and the header badge format that matches the Liquid theme block.\n *\n * `productQuantities` defaults to `bundle.productQuantities` (the\n * merchant-configured map); product IDs missing from that map fall back\n * to qty 1 via the `?? 1` in the row loop.\n * The bundle's `widgetConfig.pricing.showSaveBadge` is read unless\n * overridden.\n */\nexport function computeFixedPricing(\n bundle: FixedBundleData,\n productQuantities: Record<string, number> = bundle.productQuantities,\n showSaveBadge: boolean = bundle.widgetConfig.pricing.showSaveBadge,\n): FixedBundlePricing {\n const { discountType, discountValue } = bundle.discountConfig;\n const rows: PricingRow[] = [];\n let totalCents = 0;\n let saleCents = 0;\n\n for (const product of bundle.products) {\n const variant =\n product.variants.nodes.find((v) => v.availableForSale) ??\n product.variants.nodes[0];\n if (!variant) continue;\n\n const unitCents = parseCents(variant.price.amount);\n const qty = productQuantities[product.id] ?? 1;\n const lineCents = unitCents * qty;\n const compareCents = variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null;\n\n rows.push({ unitCents, qty, lineCents, compareCents });\n totalCents += lineCents;\n\n if (discountType === \"percentage\") {\n const perUnit = percentageDiscountUnit(unitCents, discountValue);\n saleCents += perUnit * qty;\n } else {\n saleCents += lineCents;\n }\n }\n\n if (discountType === \"fixed_amount\") {\n saleCents = Math.max(0, totalCents - Math.round(discountValue * 100));\n }\n\n const savingsCents = Math.max(0, totalCents - saleCents);\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n let headerBadge = \"\";\n if (showSaveBadge && savingsCents > 0) {\n if (discountType === \"percentage\" && discountValue > 0) {\n headerBadge = `-${Math.round(discountValue)}%`;\n } else if (discountType === \"fixed_amount\" && discountValue > 0) {\n headerBadge = `-${formatCents(\n Math.round(discountValue * 100),\n currency,\n )}`;\n }\n }\n\n return { rows, totalCents, saleCents, savingsCents, headerBadge, currency };\n}\n\n/**\n * Pure helper: compute the sale-price total for an arbitrary set of\n * cart lines under a discount config. Useful for mix-match bundles\n * where the lines are user-selected rather than bundle-configured.\n * Applies percentage discount on the aggregated total (mix-match uses\n * `applies_to_each_item: false`), matching the Liquid block's behaviour.\n */\nexport function computeBundleSaleCents(\n totalCents: number,\n discount: DiscountConfig,\n): number {\n if (discount.discountType === \"percentage\") {\n const off = Math.floor((totalCents * discount.discountValue) / 100);\n return Math.max(0, totalCents - off);\n }\n return Math.max(0, totalCents - Math.round(discount.discountValue * 100));\n}\n","/**\n * Append Shopify CDN image-transform params to a product-image URL so the\n * browser downloads a correctly-sized asset. Mirrors what Liquid's\n * `image_url: width:..., height:..., crop:...` filter chain produces.\n * Non-Shopify CDNs silently ignore the extra params — graceful fallback.\n */\nexport interface ImageTransform {\n width?: number;\n height?: number;\n crop?: \"center\" | \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\n/**\n * Standard thumbnail size for bundle product rows and picker tiles.\n * Matches the Liquid theme's `image_url: width: 152, height: 152,\n * crop: 'center'`. CSS caps display size around 76px; 152 gives\n * retina-quality rendering on 2x displays.\n */\nexport const THUMB_PX = 152;\n\nexport function transformImageUrl(\n url: string | null | undefined,\n t: ImageTransform = {},\n): string {\n if (!url) return \"\";\n try {\n const u = new URL(url);\n if (t.width !== undefined) u.searchParams.set(\"width\", String(t.width));\n if (t.height !== undefined) u.searchParams.set(\"height\", String(t.height));\n if (t.crop) u.searchParams.set(\"crop\", t.crop);\n return u.toString();\n } catch {\n return url;\n }\n}\n","/**\n * Pure countdown formatter — returns the `Nd HHh MMm SSs` / `HHh MMm SSs`\n * string given a millisecond remaining. SSR-safe, no DOM, no timers.\n *\n * A developer building their own widget wires their own `setInterval`\n * + React state and calls this to format the label. The web component's\n * `renderCountdown` uses this internally.\n */\nexport function formatCountdown(msRemaining: number): string {\n if (!Number.isFinite(msRemaining) || msRemaining <= 0) return \"\";\n const totalSeconds = Math.floor(msRemaining / 1000);\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = totalSeconds % 60;\n const pad = (n: number) => String(n).padStart(2, \"0\");\n if (days > 0) {\n return `${days}d ${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n }\n return `${pad(hours)}h ${pad(minutes)}m ${pad(seconds)}s`;\n}\n","// Public surface — only the algorithm and its types. Layout-tuning\n// constants (item height, list padding, max visible items) live inline\n// in each consumer so the SDK contract isn't locked into specific\n// numeric values that might change.\nexport {\n computePosition,\n type DropdownPlacement,\n type ComputePositionArgs,\n type ComputePositionResult,\n} from \"./compute-position\";\n\nexport {\n handleKey,\n type DropdownKeyEvent,\n type DropdownKeyState,\n type DropdownAction,\n} from \"./keyboard\";\n\nexport {\n pushTypeAheadChar,\n emptyTypeAheadState,\n type TypeAheadState,\n type TypeAheadOption,\n type PushCharResult,\n} from \"./type-ahead\";\n","/**\n * Pure positioning helper for an accessible dropdown popover.\n *\n * Given a trigger rect and the viewport height, decide whether the panel\n * should open above or below the trigger and how tall it can be. Used by\n * the Liquid theme dropdown, the `<lime-bundle>` web component, and the\n * React `<VariantDropdown>` so all three surfaces agree.\n *\n * The function is intentionally framework-agnostic — caller passes plain\n * numbers (typically from `getBoundingClientRect()` and `window.innerHeight`)\n * and applies the returned `position: fixed` coordinates itself.\n */\n\nexport const DEFAULT_TRIGGER_MARGIN = 4;\n\nexport type DropdownPlacement = \"up\" | \"down\";\n\nexport interface ComputePositionArgs {\n trigger: { top: number; bottom: number; left: number; width: number };\n viewportHeight: number;\n desiredHeight: number;\n margin?: number;\n /**\n * Optional viewport-coordinate bounds of the nearest clipping ancestor\n * (e.g. a `overflow:auto` product list). When provided, placement is\n * decided against these bounds instead of the full viewport so a panel\n * inside a short scroll container flips upward before it would clip.\n *\n * Bounds are intersected with the viewport (`[0, viewportHeight]`) so a\n * caller that passes the raw `getBoundingClientRect()` of an ancestor\n * still gets a sane result when the ancestor itself extends off-screen.\n */\n clip?: { top: number; bottom: number };\n}\n\nexport interface ComputePositionResult {\n placement: DropdownPlacement;\n maxHeight: number;\n offsetTop: number;\n offsetLeft: number;\n width: number;\n}\n\nexport function computePosition({\n trigger,\n viewportHeight,\n desiredHeight,\n margin = DEFAULT_TRIGGER_MARGIN,\n clip,\n}: ComputePositionArgs): ComputePositionResult {\n const upperBound = clip\n ? Math.min(viewportHeight, clip.bottom)\n : viewportHeight;\n const lowerBound = clip ? Math.max(0, clip.top) : 0;\n const spaceBelow = Math.max(0, upperBound - trigger.bottom - margin);\n const spaceAbove = Math.max(0, trigger.top - lowerBound - margin);\n\n // Prefer downward unless the panel would clip AND upward has more room.\n // Matches native <select> behaviour on most platforms.\n const placement: DropdownPlacement =\n desiredHeight <= spaceBelow\n ? \"down\"\n : spaceAbove > spaceBelow\n ? \"up\"\n : \"down\";\n\n const available = placement === \"down\" ? spaceBelow : spaceAbove;\n // Cap maxHeight at the actual available space so the panel never spills\n // outside the viewport. When `available` is less than the desired height\n // the listbox shrinks and its `overflow-y: auto` scrolls — better than\n // overflowing the viewport with a fixed-minimum tall panel.\n const maxHeight = Math.min(desiredHeight, available);\n\n const offsetTop =\n placement === \"down\"\n ? trigger.bottom + margin\n : trigger.top - margin - maxHeight;\n\n return {\n placement,\n maxHeight,\n offsetTop,\n offsetLeft: trigger.left,\n width: trigger.width,\n };\n}\n","/**\n * Keyboard event → action discriminator for an accessible dropdown.\n *\n * The DOM glue layer (Liquid asset, web component, React component) calls\n * `handleKey(event, state)` with the event details and current state, then\n * applies the returned action. This keeps WAI-ARIA combobox semantics in a\n * single, unit-testable place.\n *\n * Pattern follows the WAI-ARIA Authoring Practices \"Combobox with Listbox\n * Popup, manual selection\" pattern.\n */\n\nexport interface DropdownKeyEvent {\n key: string;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n shiftKey?: boolean;\n}\n\nexport interface DropdownKeyState {\n isOpen: boolean;\n activeIndex: number;\n selectedIndex: number;\n options: ReadonlyArray<{ disabled: boolean }>;\n}\n\nexport type DropdownAction =\n | {\n type: \"open\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"close\";\n commit: false;\n restoreFocus: boolean;\n preventDefault: boolean;\n }\n | {\n type: \"move-active\";\n activeIndex: number;\n preventDefault: true;\n }\n | {\n type: \"commit\";\n index: number;\n preventDefault: true;\n }\n | {\n type: \"type-ahead\";\n char: string;\n preventDefault: false;\n }\n | {\n type: \"passthrough\";\n preventDefault: boolean;\n };\n\nconst PASSTHROUGH: DropdownAction = {\n type: \"passthrough\",\n preventDefault: false,\n};\n\nfunction firstEnabled(options: ReadonlyArray<{ disabled: boolean }>): number {\n for (let i = 0; i < options.length; i++) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction lastEnabled(options: ReadonlyArray<{ disabled: boolean }>): number {\n for (let i = options.length - 1; i >= 0; i--) {\n if (!options[i].disabled) return i;\n }\n return -1;\n}\n\nfunction nextEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from + step) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from; // all disabled — stay put\n}\n\nfunction prevEnabled(\n options: ReadonlyArray<{ disabled: boolean }>,\n from: number,\n): number {\n if (options.length === 0) return -1;\n for (let step = 1; step <= options.length; step++) {\n const idx = (from - step + options.length) % options.length;\n if (!options[idx].disabled) return idx;\n }\n return from;\n}\n\nfunction isPrintable(key: string): boolean {\n return key.length === 1 && key !== \" \" && /\\S/.test(key);\n}\n\nexport function handleKey(\n event: DropdownKeyEvent,\n state: DropdownKeyState,\n): DropdownAction {\n // Ignore modifier-key combos (browser shortcuts, etc).\n if (event.ctrlKey || event.metaKey || event.altKey) return PASSTHROUGH;\n\n const { key } = event;\n const { isOpen, activeIndex, selectedIndex, options } = state;\n\n // Closed → key opens (or noop)\n if (!isOpen) {\n switch (key) {\n case \"Enter\":\n case \" \":\n case \"ArrowDown\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : lastEnabled(options),\n preventDefault: true,\n };\n default:\n if (isPrintable(key)) {\n return {\n type: \"open\",\n activeIndex:\n selectedIndex >= 0 && !options[selectedIndex]?.disabled\n ? selectedIndex\n : firstEnabled(options),\n preventDefault: true,\n };\n }\n return PASSTHROUGH;\n }\n }\n\n // Open → navigation, commit, close\n switch (key) {\n case \"ArrowDown\":\n return {\n type: \"move-active\",\n activeIndex: nextEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"ArrowUp\":\n return {\n type: \"move-active\",\n activeIndex: prevEnabled(options, activeIndex),\n preventDefault: true,\n };\n case \"Home\":\n return {\n type: \"move-active\",\n activeIndex: firstEnabled(options),\n preventDefault: true,\n };\n case \"End\":\n return {\n type: \"move-active\",\n activeIndex: lastEnabled(options),\n preventDefault: true,\n };\n case \"Enter\":\n case \" \":\n if (activeIndex >= 0 && !options[activeIndex]?.disabled) {\n return {\n type: \"commit\",\n index: activeIndex,\n preventDefault: true,\n };\n }\n // Enter while open with no committable target — swallow it. The\n // dropdown lives inside a Shopify product-page <form>, so a\n // bubbling Enter would submit the page form. WAI-ARIA combobox\n // pattern says the popup should absorb Enter regardless.\n return { type: \"passthrough\", preventDefault: true };\n case \"Escape\":\n return {\n type: \"close\",\n commit: false,\n restoreFocus: true,\n preventDefault: true,\n };\n case \"Tab\":\n // Don't preventDefault — let the browser advance focus naturally.\n // Don't restoreFocus either: stealing focus back to the trigger\n // would interfere with the browser's tab-advance to the next\n // focusable element.\n return {\n type: \"close\",\n commit: false,\n restoreFocus: false,\n preventDefault: false,\n };\n default:\n if (isPrintable(key)) {\n return { type: \"type-ahead\", char: key, preventDefault: false };\n }\n return PASSTHROUGH;\n }\n}\n","/**\n * Type-ahead matching for dropdown listboxes.\n *\n * The caller drives `now` (typically `Date.now()`) so the function stays\n * pure and deterministic in tests. After `RESET_MS` of inactivity the\n * buffer clears; otherwise additional chars accumulate so the user can\n * type \"me\" to jump from \"Mango\" to \"Medium\" without overshoot.\n */\n\nexport const TYPE_AHEAD_RESET_MS = 500;\n\nexport interface TypeAheadState {\n buffer: string;\n lastTime: number;\n}\n\nexport interface TypeAheadOption {\n disabled: boolean;\n label: string;\n}\n\nexport function emptyTypeAheadState(): TypeAheadState {\n return { buffer: \"\", lastTime: 0 };\n}\n\nexport interface PushCharResult {\n newState: TypeAheadState;\n matchedIndex: number | null;\n}\n\nexport function pushTypeAheadChar(\n state: TypeAheadState,\n char: string,\n now: number,\n options: ReadonlyArray<TypeAheadOption>,\n resetMs: number = TYPE_AHEAD_RESET_MS,\n): PushCharResult {\n if (char.length !== 1) {\n return { newState: state, matchedIndex: null };\n }\n\n const expired = now - state.lastTime > resetMs;\n const buffer = (expired ? \"\" : state.buffer) + char.toLowerCase();\n const newState: TypeAheadState = { buffer, lastTime: now };\n\n for (let i = 0; i < options.length; i++) {\n const opt = options[i];\n if (opt.disabled) continue;\n if (opt.label.toLowerCase().startsWith(buffer)) {\n return { newState, matchedIndex: i };\n }\n }\n\n return { newState, matchedIndex: null };\n}\n","/**\n * Variant resolution + availability cascade for per-option pickers.\n *\n * Mirrors the algorithms in `extensions/bundle-theme/assets/bundle-fixed.js`\n * (`findVariantByOptions`, `isOptionValueAvailable`) so the React component\n * and web component renderer share the same source of truth as the Liquid\n * widget. Liquid keeps its inlined copy because theme assets cannot import\n * npm — a CI parity guard prevents drift.\n *\n * Inputs are normalized to positional option-value arrays. Storefront API\n * consumers can derive this with `v.selectedOptions.map(o => o.value)`.\n */\n\nexport interface PickerVariant {\n id: string;\n /** Positional option values: index i matches the product's option-position i+1. */\n optionValues: string[];\n /** True when the variant has stock and is purchasable. */\n available: boolean;\n}\n\n/**\n * Returns the variant whose positional options exactly match the selected\n * values, or `null` if none. Caller decides what to do on miss (typically\n * fall back to the first available variant).\n */\nexport function findVariantByOptions<V extends PickerVariant>(\n variants: ReadonlyArray<V>,\n optionValues: ReadonlyArray<string>,\n): V | null {\n if (variants.length === 0) return null;\n for (const v of variants) {\n if (v.optionValues.length !== optionValues.length) continue;\n let match = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (v.optionValues[j] !== optionValues[j]) {\n match = false;\n break;\n }\n }\n if (match) return v;\n }\n return null;\n}\n\n/**\n * True iff some in-stock variant pairs `value` at `optionIndex` with the\n * currently selected values at every other option index. Drives the\n * disabled state of options in cascading pickers (e.g. selecting Color=Red\n * may disable Size=XL if no Red XL variant exists or it's sold out).\n */\nexport function isOptionValueAvailable(\n variants: ReadonlyArray<PickerVariant>,\n optionIndex: number,\n value: string,\n selected: ReadonlyArray<string>,\n): boolean {\n for (const v of variants) {\n if (!v.available) continue;\n if (v.optionValues[optionIndex] !== value) continue;\n let ok = true;\n for (let j = 0; j < v.optionValues.length; j++) {\n if (j === optionIndex) continue;\n if (v.optionValues[j] !== selected[j]) {\n ok = false;\n break;\n }\n }\n if (ok) return true;\n }\n return false;\n}\n\n/**\n * Convenience helper for Storefront API consumers. Converts a Storefront\n * `ProductVariant` (with `selectedOptions: [{name, value}]`) into the\n * normalized `PickerVariant` shape.\n */\nexport function toPickerVariant<\n T extends {\n id: string;\n selectedOptions: ReadonlyArray<{ value: string }>;\n availableForSale: boolean;\n },\n>(variant: T): PickerVariant {\n return {\n id: variant.id,\n optionValues: variant.selectedOptions.map((o) => o.value),\n available: variant.availableForSale,\n };\n}\n","/**\n * Storefront-side fulfillability predicate.\n *\n * Mirrors `app/lib/inventory.server.ts:isFulfillable` but reads from a\n * Storefront-shaped variant (no Admin types, no Node imports). Same\n * semantics: a tracked + DENY variant with insufficient stock fails;\n * everything else succeeds.\n *\n * Used by the widget renderers and React components to gate add-to-cart\n * eligibility, badge rendering, and stepper caps. Lives in `core` so the\n * Liquid widget (via `@lime-bundles/widget`) and React consumers (via\n * `@lime-bundles/react`) draw from one source of truth.\n *\n * Storefront API limitations:\n * - `quantityAvailable` is `null` when the merchant has not granted\n * `unauthenticated_read_product_inventory` to the Storefront token.\n * In that case, treat the variant as fulfillable (boolean fallback)\n * rather than blocking the cart on a permission gap.\n * - `currentlyNotInStock` is true for backordered variants (zero\n * stock + `inventoryPolicy = CONTINUE`). Fulfillable for cart\n * purposes — Shopify's checkout still accepts them.\n */\n\n/**\n * Minimal variant shape the predicate needs. Wider Storefront variants\n * (with title, price, etc.) are accepted via structural typing.\n */\nexport interface StorefrontVariantStock {\n availableForSale: boolean;\n /** Optional — Storefront API field, may be undefined on hand-built mocks. */\n currentlyNotInStock?: boolean;\n /** Null when the Storefront token lacks the inventory scope, or when\n * the variant is untracked. Treat as \"unknown stock — defer to\n * availableForSale\". */\n quantityAvailable?: number | null;\n}\n\n/**\n * True when a customer can add `requiredQty` units of this variant to\n * their cart and pass Shopify's checkout-time stock check.\n *\n * - `availableForSale = false` → never fulfillable (Shopify will reject).\n * - `currentlyNotInStock = true` → backordered (zero stock + CONTINUE\n * inventory policy). Shopify will accept the cart line for any qty,\n * so treat as fulfillable. Mirrors the Admin-side predicate's\n * CONTINUE branch in [app/lib/inventory.server.ts].\n * - `quantityAvailable` is `null` / `undefined` → defer to the boolean\n * (the merchant's Storefront token may be missing\n * `unauthenticated_read_product_inventory`, or the variant is\n * untracked).\n * - Otherwise → require `quantityAvailable >= requiredQty`.\n */\nexport function isFulfillable(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): boolean {\n if (!variant.availableForSale) return false;\n if (variant.currentlyNotInStock) return true;\n if (variant.quantityAvailable == null) return true;\n return variant.quantityAvailable >= requiredQty;\n}\n\n/**\n * Whether the \"Only X left\" low-stock badge should render for this\n * variant. False when:\n * - The merchant disabled the badge (`enabled = false`).\n * - We don't know the quantity (`quantityAvailable` is null/undefined).\n * - The variant is below `requiredQty` (the renderer treats this as\n * out-of-stock instead — no double signaling).\n * - The variant has more stock than the threshold (no badge needed).\n */\nexport function shouldShowLowStockBadge(\n variant: StorefrontVariantStock,\n requiredQty: number,\n threshold: number,\n enabled: boolean,\n): boolean {\n if (!enabled) return false;\n if (variant.quantityAvailable == null) return false;\n if (variant.quantityAvailable < requiredQty) return false;\n return variant.quantityAvailable <= threshold;\n}\n\n/**\n * Maximum number of bundles the customer can request given a variant's\n * stock and the per-bundle required quantity. Used to cap quantity\n * steppers — without a cap, a customer can repeatedly press \"+\" until\n * the cart-add request fails at Shopify's checkout-time stock check.\n *\n * Returns `Infinity` when stock is unknown or unbounded (the boolean\n * fallback path) — callers should guard with the existing UI ceiling\n * (e.g. `Math.min(maxBundlesPerOrder, maxBundlesByStock)`).\n */\nexport function maxBundlesByStock(\n variant: StorefrontVariantStock,\n requiredQty: number,\n): number {\n if (variant.quantityAvailable == null) return Number.POSITIVE_INFINITY;\n if (requiredQty <= 0) return Number.POSITIVE_INFINITY;\n return Math.floor(variant.quantityAvailable / requiredQty);\n}\n\n/**\n * Largest qty the shopper may add to a mix-and-match bundle pick for a given\n * variant, after subtracting any units they've already allocated to this\n * bundle. Returns 0 when the variant isn't fulfillable.\n *\n * Bounded by:\n * - the merchant's per-product `productMax` (rule.max)\n * - the variant's `quantityAvailable` (when known)\n * Backordered variants (`currentlyNotInStock` with policy CONTINUE) bypass\n * the stock cap and rely on `productMax`.\n */\nexport function maxAddableQuantity(\n variant: StorefrontVariantStock,\n productMax: number,\n alreadyInBundle: number,\n): number {\n if (!variant.availableForSale) return 0;\n // Backorder: stock cap doesn't apply.\n if (variant.currentlyNotInStock) {\n return Math.max(0, productMax - alreadyInBundle);\n }\n const stockCap =\n variant.quantityAvailable == null\n ? Number.POSITIVE_INFINITY\n : variant.quantityAvailable;\n return Math.max(0, Math.min(productMax, stockCap) - alreadyInBundle);\n}\n","/**\n * TypeScript bind helper for the custom variant-picker dropdown inside the\n * `<lime-bundle>` web component. Uses pure algorithms from\n * `@lime-bundles/core/dropdown` and adds the DOM glue for shadow-DOM use.\n *\n * Mirrors the contract of the vanilla theme asset\n * `extensions/bundle-theme/assets/bundle-dropdown.js`. The native `<select>`\n * stays in DOM as the canonical state holder; the custom UI dispatches\n * synthetic `change` events on commit so existing renderer change-handlers\n * work unchanged.\n */\nimport { dropdown } from \"@lime-bundles/core\";\n\nconst { computePosition, emptyTypeAheadState, handleKey, pushTypeAheadChar } =\n dropdown;\n\n// Layout-tuning constants for panel-height calculation. Inlined here\n// rather than imported from core so they stay tweakable without locking\n// numeric values into the public SDK contract.\nconst ITEM_HEIGHT_PX = 32;\nconst LIST_PAD_Y = 8;\nconst MAX_VISIBLE_ITEMS = 8;\ntype DropdownAction = dropdown.DropdownAction;\ntype TypeAheadState = dropdown.TypeAheadState;\n\nexport interface DropdownInstance {\n readonly shell: HTMLElement;\n readonly listbox: HTMLElement;\n readonly select: HTMLSelectElement;\n close(): void;\n destroy(): void;\n}\n\n// Single module-level set of currently open dropdowns. Document-level\n// listeners are attached on the 0→1 transition and detached on 1→0.\n// `composedPath()` lets one listener correctly identify hits across any\n// number of shadow roots — events bubble out of shadow with retargeted\n// `event.target`, but composedPath still surfaces the original element.\n// `bind-dropdown` keeps a module-level list of currently-open instances so\n// document-level listeners are reference-counted (one set of listeners\n// across N dropdowns). Note for test authors: a test that opens a\n// dropdown without calling `inst.destroy()` in cleanup will leak document\n// listeners across cases — call __resetDropdownsForTest() in beforeEach\n// or always tear down via the returned instance.\nconst openInstances: DropdownInstance[] = [];\n\n// Outside-click and ancestor-scroll both close any open dropdown whose\n// shell + listbox aren't in the event path. Same handler for both.\nfunction closeOutsideEvent(event: Event) {\n const path = event.composedPath();\n for (let i = openInstances.length - 1; i >= 0; i--) {\n const inst = openInstances[i];\n if (!path.includes(inst.shell) && !path.includes(inst.listbox)) {\n inst.close();\n }\n }\n}\n\nfunction onDocResize() {\n for (let i = openInstances.length - 1; i >= 0; i--) openInstances[i].close();\n}\n\nlet docListenersAttached = false;\nfunction attachDocumentListeners() {\n if (docListenersAttached) return;\n document.addEventListener(\"pointerdown\", closeOutsideEvent, true);\n window.addEventListener(\"scroll\", closeOutsideEvent, true);\n window.addEventListener(\"resize\", onDocResize);\n docListenersAttached = true;\n}\n\nfunction detachDocumentListeners() {\n if (!docListenersAttached || openInstances.length > 0) return;\n document.removeEventListener(\"pointerdown\", closeOutsideEvent, true);\n window.removeEventListener(\"scroll\", closeOutsideEvent, true);\n window.removeEventListener(\"resize\", onDocResize);\n docListenersAttached = false;\n}\n\n/** Test-only reset hook. Vitest caches module imports across cases; a\n * test that opens a dropdown without destroying it would leak document\n * listeners and stale entries in openInstances into subsequent tests.\n * Call this in beforeEach when a test exercises bindDropdown directly. */\nexport function __resetDropdownsForTest(): void {\n while (openInstances.length > 0) {\n openInstances[openInstances.length - 1].destroy();\n }\n detachDocumentListeners();\n}\n\ntype OptionState = dropdown.TypeAheadOption;\n\nfunction readOptions(select: HTMLSelectElement): OptionState[] {\n const out: OptionState[] = [];\n for (let i = 0; i < select.options.length; i++) {\n const o = select.options[i];\n out.push({ disabled: o.disabled, label: o.textContent || o.value });\n }\n return out;\n}\n\nfunction firstEnabled(opts: OptionState[]): number {\n for (let i = 0; i < opts.length; i++) if (!opts[i].disabled) return i;\n return -1;\n}\n\ntype SelectWithInstance = HTMLSelectElement & {\n __lbDropdownInstance?: DropdownInstance;\n};\n\n// Walk up to find the nearest ancestor that clips overflow on the Y axis.\n// Used so placement flips upward before the listbox would be hidden by a\n// scrollable container (`.lb-fixed__products` / `.lb-bundle__products`).\n// Stops at body/html — past that, the viewport bound is correct.\nfunction findScrollableAncestor(el: Element): HTMLElement | null {\n const win = el.ownerDocument?.defaultView;\n if (!win) return null;\n let cur: Element | null = el.parentElement;\n while (cur && cur !== el.ownerDocument.body) {\n const style = win.getComputedStyle(cur);\n const overflowY = style.overflowY;\n if (\n overflowY === \"auto\" ||\n overflowY === \"scroll\" ||\n overflowY === \"hidden\"\n ) {\n return cur as HTMLElement;\n }\n cur = cur.parentElement;\n }\n return null;\n}\n\nconst VARIANT_SELECT_CLASSES = [\n \"lb-bundle-variant-select\",\n \"lb-mix-match__variant-select\",\n] as const;\n\nconst BIND_SELECTOR = VARIANT_SELECT_CLASSES.map(\n (c) => `select.${c}:not(.lb-dropdown-state)`,\n).join(\", \");\n\nexport function bindDropdown(\n select: HTMLSelectElement,\n): DropdownInstance | null {\n const slot = select as SelectWithInstance;\n if (select.classList.contains(\"lb-dropdown-state\")) {\n return slot.__lbDropdownInstance ?? null;\n }\n\n const doc = select.ownerDocument;\n const rootNode = select.getRootNode() as ShadowRoot | Document;\n const labelText = select.getAttribute(\"aria-label\") ?? \"\";\n const idBase = `lb-dd-${Math.random().toString(36).slice(2, 9)}`;\n\n select.classList.add(\"lb-dropdown-state\");\n select.setAttribute(\"aria-hidden\", \"true\");\n select.setAttribute(\"tabindex\", \"-1\");\n\n const shell = doc.createElement(\"div\");\n shell.className = \"lb-dropdown\";\n shell.setAttribute(\"data-lb-dropdown\", \"\");\n\n const trigger = doc.createElement(\"button\");\n trigger.type = \"button\";\n trigger.className = \"lb-dropdown-trigger\";\n trigger.setAttribute(\"role\", \"combobox\");\n trigger.setAttribute(\"aria-haspopup\", \"listbox\");\n trigger.setAttribute(\"aria-expanded\", \"false\");\n const listboxId = `${idBase}-listbox`;\n trigger.setAttribute(\"aria-controls\", listboxId);\n if (labelText) trigger.setAttribute(\"aria-label\", labelText);\n\n const triggerLabel = doc.createElement(\"span\");\n triggerLabel.className = \"lb-dropdown-trigger-value\";\n\n const chevron = doc.createElement(\"span\");\n chevron.className = \"lb-dropdown-chevron\";\n chevron.setAttribute(\"aria-hidden\", \"true\");\n\n trigger.appendChild(triggerLabel);\n trigger.appendChild(chevron);\n\n const listbox = doc.createElement(\"ul\");\n listbox.id = listboxId;\n listbox.className = \"lb-dropdown-listbox\";\n listbox.setAttribute(\"role\", \"listbox\");\n if (labelText) listbox.setAttribute(\"aria-label\", labelText);\n listbox.hidden = true;\n\n shell.appendChild(trigger);\n select.parentNode?.insertBefore(shell, select.nextSibling);\n // The mix-match modal applies translateY for its slide-in animation,\n // which turns position:fixed into relative-to-modal. Portal the\n // listbox up to the overlay (carries per-bundle --lb-* variables AND\n // has no transform of its own) only when the trigger is inside a\n // modal. For the main widget, leave the listbox inside the shell —\n // there's no transformed ancestor to escape.\n const modalOverlay = select.closest(\"[data-modal-overlay]\");\n if (modalOverlay) {\n modalOverlay.appendChild(listbox);\n listbox.setAttribute(\"data-lb-dropdown-portal\", \"\");\n } else {\n shell.appendChild(listbox);\n }\n\n let isOpen = false;\n let activeIndex = -1;\n let typeAhead: TypeAheadState = emptyTypeAheadState();\n let optionEls: HTMLLIElement[] = [];\n let instance: DropdownInstance; // eslint-disable-line prefer-const\n\n function syncFromSelect() {\n // Mirror the underlying select's disabled state to the trigger\n // button. Without this, a row that disables its <select> (e.g.\n // mix-match products already in the bundle) leaves the custom\n // dropdown trigger focusable and clickable — the visible chrome\n // disagrees with the form control's actual state.\n trigger.disabled = select.disabled;\n const opts = readOptions(select);\n const idx = select.selectedIndex;\n triggerLabel.textContent = idx >= 0 && opts[idx] ? opts[idx].label : \"\";\n\n while (listbox.firstChild) listbox.removeChild(listbox.firstChild);\n optionEls = [];\n\n for (let i = 0; i < opts.length; i++) {\n const li = doc.createElement(\"li\");\n li.id = `${idBase}-opt-${i}`;\n li.className = \"lb-dropdown-option\";\n li.setAttribute(\"role\", \"option\");\n li.setAttribute(\"aria-selected\", i === idx ? \"true\" : \"false\");\n if (opts[i].disabled) li.setAttribute(\"aria-disabled\", \"true\");\n li.setAttribute(\"data-value\", select.options[i].value);\n li.setAttribute(\"data-index\", String(i));\n li.textContent = opts[i].label;\n listbox.appendChild(li);\n optionEls.push(li);\n }\n }\n\n function setActive(newIndex: number) {\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = newIndex;\n if (newIndex >= 0 && optionEls[newIndex]) {\n const li = optionEls[newIndex];\n li.classList.add(\"is-active\");\n trigger.setAttribute(\"aria-activedescendant\", li.id);\n // Scroll within the listbox only — Element.scrollIntoView falls\n // through to the document scroll when the listbox itself isn't\n // overflowing, which can yank the page when the active option's\n // viewport position differs from its offsetTop-relative position.\n const liTop = li.offsetTop;\n const liBottom = liTop + li.offsetHeight;\n const visTop = listbox.scrollTop;\n const visBottom = visTop + listbox.clientHeight;\n if (liTop < visTop) {\n listbox.scrollTop = liTop;\n } else if (liBottom > visBottom) {\n listbox.scrollTop = liBottom - listbox.clientHeight;\n }\n } else {\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n }\n }\n\n function position(): boolean {\n const rect = trigger.getBoundingClientRect();\n if (rect.width === 0) return false;\n const visibleCount = Math.min(optionEls.length || 1, MAX_VISIBLE_ITEMS);\n const desiredHeight = visibleCount * ITEM_HEIGHT_PX + LIST_PAD_Y;\n // Always clip placement to the trigger's nearest scrollable ancestor.\n // In-shell (main widget): clips to the bundle's product list so the\n // panel flips upward before it would hide behind the widget footer.\n // Portaled (modal context): clips to the modal's scrollable list\n // (e.g. `.lb-mix-match__modal-list`) so the panel flips upward when\n // the trigger sits near the modal's bottom edge — even if there's\n // viewport room below. Without this clip, position:fixed listboxes\n // attached to the overlay would open downward and overflow the modal.\n const scrollable = findScrollableAncestor(trigger);\n const clip = scrollable\n ? (() => {\n const r = scrollable.getBoundingClientRect();\n return { top: r.top, bottom: r.bottom };\n })()\n : undefined;\n const result = computePosition({\n trigger: {\n top: rect.top,\n bottom: rect.bottom,\n left: rect.left,\n width: rect.width,\n },\n viewportHeight: window.innerHeight,\n desiredHeight,\n clip,\n });\n listbox.setAttribute(\"data-placement\", result.placement);\n listbox.style.maxHeight = `${result.maxHeight}px`;\n // In-shell (main widget) case: CSS [data-placement] selectors\n // anchor the listbox against the position:relative shell. Only the\n // portaled (modal) case needs inline viewport coords.\n if (listbox.hasAttribute(\"data-lb-dropdown-portal\")) {\n listbox.style.top = `${result.offsetTop}px`;\n listbox.style.left = `${result.offsetLeft}px`;\n listbox.style.width = `${result.width}px`;\n }\n return true;\n }\n\n function open() {\n if (isOpen) return;\n // Close any other open dropdown first — single-open semantics.\n for (let i = openInstances.length - 1; i >= 0; i--) {\n if (openInstances[i] !== instance) openInstances[i].close();\n }\n isOpen = true;\n listbox.hidden = false;\n trigger.setAttribute(\"aria-expanded\", \"true\");\n if (!position()) {\n requestAnimationFrame(() => position());\n }\n const opts = readOptions(select);\n const selIdx = select.selectedIndex;\n if (selIdx >= 0 && opts[selIdx] && !opts[selIdx].disabled) {\n setActive(selIdx);\n } else {\n setActive(firstEnabled(opts));\n }\n openInstances.push(instance);\n if (openInstances.length === 1) attachDocumentListeners();\n }\n\n function close(restoreFocus: boolean) {\n if (!isOpen) return;\n isOpen = false;\n listbox.hidden = true;\n trigger.setAttribute(\"aria-expanded\", \"false\");\n trigger.setAttribute(\"aria-activedescendant\", \"\");\n if (activeIndex >= 0 && optionEls[activeIndex]) {\n optionEls[activeIndex].classList.remove(\"is-active\");\n }\n activeIndex = -1;\n const idx = openInstances.indexOf(instance);\n if (idx >= 0) openInstances.splice(idx, 1);\n if (openInstances.length === 0) detachDocumentListeners();\n if (restoreFocus) trigger.focus();\n }\n\n function commit(index: number) {\n const opt = select.options[index];\n if (!opt || opt.disabled) return;\n if (select.value !== opt.value) {\n select.value = opt.value;\n const event = new Event(\"change\", { bubbles: true });\n select.dispatchEvent(event);\n }\n syncFromSelect();\n close(true);\n }\n\n function applyAction(action: DropdownAction) {\n switch (action.type) {\n case \"open\":\n open();\n if (action.activeIndex >= 0) setActive(action.activeIndex);\n return;\n case \"close\":\n close(action.restoreFocus);\n return;\n case \"move-active\":\n setActive(action.activeIndex);\n return;\n case \"commit\":\n commit(action.index);\n return;\n case \"type-ahead\": {\n const opts = readOptions(select);\n const result = pushTypeAheadChar(\n typeAhead,\n action.char,\n Date.now(),\n opts,\n );\n typeAhead = result.newState;\n if (result.matchedIndex !== null) {\n if (!isOpen) open();\n setActive(result.matchedIndex);\n }\n return;\n }\n case \"passthrough\":\n return;\n default: {\n const _exhaustive: never = action;\n void _exhaustive;\n }\n }\n }\n\n function onKeydown(event: KeyboardEvent) {\n const opts = readOptions(select);\n const action = handleKey(\n {\n key: event.key,\n ctrlKey: event.ctrlKey,\n metaKey: event.metaKey,\n altKey: event.altKey,\n shiftKey: event.shiftKey,\n },\n {\n isOpen,\n activeIndex,\n selectedIndex: select.selectedIndex,\n options: opts,\n },\n );\n if (action.preventDefault) event.preventDefault();\n applyAction(action);\n }\n\n function onTriggerClick(event: MouseEvent) {\n event.preventDefault();\n if (isOpen) close(false);\n else open();\n }\n\n function onListboxClick(event: MouseEvent) {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList?.contains(\"lb-dropdown-option\")) {\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n if (!Number.isNaN(idx)) {\n commit(idx);\n return;\n }\n }\n target = target.parentElement;\n }\n }\n\n function onListboxMousemove(event: MouseEvent) {\n let target = event.target as HTMLElement | null;\n while (target && target !== listbox) {\n if (target.classList?.contains(\"lb-dropdown-option\")) {\n if (target.getAttribute(\"aria-disabled\") === \"true\") return;\n const idx = parseInt(target.getAttribute(\"data-index\") ?? \"\", 10);\n if (!Number.isNaN(idx) && idx !== activeIndex) setActive(idx);\n return;\n }\n target = target.parentElement;\n }\n }\n\n function onShellFocusout() {\n // In shadow DOM, document.activeElement returns the shadow host;\n // rootNode.activeElement returns the actual focused element inside\n // the shadow tree. Falls back to document.activeElement in the\n // light-DOM (non-shadow) case.\n setTimeout(() => {\n if (!isOpen) return;\n const active = rootNode.activeElement ?? doc.activeElement;\n if (!shell.contains(active)) close(false);\n }, 0);\n }\n\n function onSelectChange() {\n syncFromSelect();\n }\n\n const observer = new MutationObserver(() => {\n syncFromSelect();\n });\n observer.observe(select, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"disabled\", \"value\", \"selected\"],\n });\n\n // Prevent mousedown on a non-focusable <li> from blurring the trigger.\n // Without this, focusout fires on the shell and queues a setTimeout(0)\n // that closes the dropdown — and on desktop the close runs before the\n // synthesized click event, so onListboxClick never sees the option and\n // commit never runs. Mobile is unaffected because touchstart doesn't\n // shift focus.\n const onListboxMousedown = (event: MouseEvent) => event.preventDefault();\n\n trigger.addEventListener(\"click\", onTriggerClick);\n trigger.addEventListener(\"keydown\", onKeydown);\n shell.addEventListener(\"focusout\", onShellFocusout);\n listbox.addEventListener(\"mousedown\", onListboxMousedown);\n listbox.addEventListener(\"click\", onListboxClick);\n listbox.addEventListener(\"mousemove\", onListboxMousemove);\n select.addEventListener(\"change\", onSelectChange);\n\n function destroy() {\n if (isOpen) close(false);\n observer.disconnect();\n trigger.removeEventListener(\"click\", onTriggerClick);\n trigger.removeEventListener(\"keydown\", onKeydown);\n shell.removeEventListener(\"focusout\", onShellFocusout);\n listbox.removeEventListener(\"mousedown\", onListboxMousedown);\n listbox.removeEventListener(\"click\", onListboxClick);\n listbox.removeEventListener(\"mousemove\", onListboxMousemove);\n select.removeEventListener(\"change\", onSelectChange);\n if (shell.parentNode) shell.parentNode.removeChild(shell);\n if (listbox.parentNode) listbox.parentNode.removeChild(listbox);\n select.classList.remove(\"lb-dropdown-state\");\n select.removeAttribute(\"aria-hidden\");\n select.removeAttribute(\"tabindex\");\n delete slot.__lbDropdownInstance;\n }\n\n instance = {\n shell,\n listbox,\n select,\n close: () => close(false),\n destroy,\n };\n slot.__lbDropdownInstance = instance;\n\n syncFromSelect();\n return instance;\n}\n\nexport function bindAllDropdowns(root: ParentNode): DropdownInstance[] {\n const selects = root.querySelectorAll(BIND_SELECTOR);\n const instances: DropdownInstance[] = [];\n selects.forEach((sel) => {\n const inst = bindDropdown(sel as HTMLSelectElement);\n if (inst) instances.push(inst);\n });\n return instances;\n}\n\nexport function unbindAllDropdowns(root: ParentNode): void {\n const bound = root.querySelectorAll(\"select.lb-dropdown-state\");\n bound.forEach((sel) => {\n const inst = (sel as SelectWithInstance).__lbDropdownInstance;\n if (inst) inst.destroy();\n });\n}\n","// Matches the DOM/class structure of the Liquid `bundle-widget.liquid`'s\n// `.lb-bundle-countdown` bar so the shared CSS themes both renderers.\nimport { formatCountdown } from \"@lime-bundles/core\";\n\nexport interface CountdownHandle {\n /** DOM element to append to the widget. */\n el: HTMLElement;\n /** Call on widget disconnect to clear the tick interval. */\n stop: () => void;\n}\n\nexport function renderCountdown(endsAtIso: string): CountdownHandle | null {\n const parsed = parseIso(endsAtIso);\n if (parsed === null) return null;\n // Already expired — caller doesn't append anything.\n if (parsed <= Date.now()) return null;\n const target: number = parsed;\n\n const wrap = document.createElement(\"div\");\n wrap.className = \"lb-bundle-countdown\";\n wrap.setAttribute(\"data-countdown\", \"\");\n\n const labelWrap = document.createElement(\"div\");\n labelWrap.className = \"lb-bundle-countdown__label\";\n const labelText = document.createElement(\"span\");\n labelText.textContent = \"Ends in\";\n labelWrap.appendChild(labelText);\n wrap.appendChild(labelWrap);\n\n const timer = document.createElement(\"span\");\n timer.className = \"lb-bundle-countdown__timer\";\n timer.setAttribute(\"data-countdown-timer\", \"\");\n wrap.appendChild(timer);\n\n let intervalId: ReturnType<typeof setInterval> | null = null;\n\n function tick() {\n const msLeft = target - Date.now();\n if (msLeft <= 0) {\n wrap.style.display = \"none\";\n stop();\n return;\n }\n timer.textContent = formatCountdown(msLeft);\n }\n\n function stop() {\n if (intervalId !== null) {\n clearInterval(intervalId);\n intervalId = null;\n }\n }\n\n tick();\n intervalId = setInterval(tick, 1000);\n\n return { el: wrap, stop };\n}\n\nfunction parseIso(iso: string): number | null {\n const t = Date.parse(iso);\n return Number.isFinite(t) ? t : null;\n}\n","/**\n * Builds the shared `.lb-bundle-cta` button structure used by all three\n * widget renderers (fixed, volume, mix_match).\n *\n * The button has two children in a 1×1 CSS grid (see\n * packages/widget/src/styles/bundle-css.ts):\n *\n * <button class=\"lb-bundle-cta\" data-add-bundle>\n * <span class=\"lb-cta-label\" data-cta-label>{label}</span>\n * <span class=\"lb-cta-spinner\" data-cta-spinner aria-hidden=\"true\">…</span>\n * </button>\n *\n * The spinner is visible only when the button has `data-loading=\"true\"`.\n * The SDK itself doesn't toggle that attribute — merchants opt into the\n * loading-state affordance by setting it during their async cart mutation:\n *\n * el.querySelector('[data-add-bundle]').setAttribute('data-loading', 'true');\n * try { await cart.linesAdd(…); }\n * finally { el.querySelector('[data-add-bundle]').removeAttribute('data-loading'); }\n *\n * Keeping the SDK neutral on timing (BYO-cart contract) means no additive\n * Promise API surface needs to change.\n */\nexport function buildCtaButton(label: string): HTMLButtonElement {\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.className = \"lb-bundle-cta\";\n button.setAttribute(\"data-add-bundle\", \"\");\n\n const labelSpan = document.createElement(\"span\");\n labelSpan.className = \"lb-cta-label\";\n labelSpan.setAttribute(\"data-cta-label\", \"\");\n labelSpan.textContent = label;\n button.appendChild(labelSpan);\n\n const spinnerSpan = document.createElement(\"span\");\n spinnerSpan.className = \"lb-cta-spinner\";\n spinnerSpan.setAttribute(\"data-cta-spinner\", \"\");\n spinnerSpan.setAttribute(\"aria-hidden\", \"true\");\n // Same markup as snippets/lb-cta-spinner.liquid — keep in sync.\n spinnerSpan.innerHTML =\n '<svg viewBox=\"0 0 24 24\" width=\"20\" height=\"20\" fill=\"none\" ' +\n 'stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\">' +\n '<path d=\"M12 2a10 10 0 0 1 10 10\" /></svg>';\n button.appendChild(spinnerSpan);\n\n return button;\n}\n\n/**\n * Updates the visible label text on a button built by `buildCtaButton`.\n * Targets the `[data-cta-label]` span so the sibling spinner isn't\n * clobbered — a plain `button.textContent = \"...\"` would destroy every\n * child, including the spinner span.\n *\n * If `[data-cta-label]` isn't found, this function is a no-op rather than\n * falling back to `button.textContent`. Buttons that omit the label span\n * either (a) also omit the spinner (nothing to destroy — but also nothing\n * the caller needs to worry about; returning early is fine) or (b) were\n * mutated in-flight by an adapter that should have kept the label. Either\n * way, overwriting `button.textContent` is strictly harmful: it either\n * silently nukes a spinner we're trying to preserve, or replaces whatever\n * structure the adapter built. Callers needing a plain-button text update\n * should write `button.textContent = ...` themselves.\n */\nexport function setCtaLabel(button: HTMLButtonElement, text: string): void {\n const label = button.querySelector<HTMLElement>(\"[data-cta-label]\");\n if (label) {\n label.textContent = text;\n }\n}\n","/**\n * Shared DOM helper for the widget renderers. Keep this internal to\n * `packages/widget/src/renderers/`; it intentionally isn't re-exported\n * from the package's public entrypoint.\n */\nexport function el(\n tag: string,\n className: string,\n attrs: Record<string, string> = {},\n): HTMLElement {\n const node = document.createElement(tag);\n if (className) node.className = className;\n for (const [k, v] of Object.entries(attrs)) {\n node.setAttribute(k, v);\n }\n return node;\n}\n","/**\n * DOM renderer for fixed bundles.\n *\n * Emits the class names and structure of `lb-fixed.liquid` so the ported\n * `bundle-fixed.css` themes it. Features:\n * - Variant dropdown per product (when the product has >1 available\n * variants, filtered by merchant's `selected_variant_ids`). Changing\n * a variant live-updates its row price, the bundle total, the\n * header save badge, and the savings bar.\n * - Merchant `productQuantities` + `variantQuantities` honoured.\n * - Out-of-stock behaviour (`hide` / `show_greyed_out`) applied per\n * product.\n * - Bundle-level guard: fixed bundles are all-or-nothing, so the\n * entire widget hides if any product has no available variants.\n *\n * Pricing matches Shopify's per-unit floor rounding via the `pricing`\n * helper, so what the widget shows equals what the customer pays at\n * checkout.\n */\nimport {\n resolveBundleQty,\n isVariantFulfillable,\n shouldShowLowStockBadge,\n type CartLineInput,\n type FixedBundleData,\n type Product,\n type ProductVariant,\n} from \"@lime-bundles/core\";\nimport { bindAllDropdowns, unbindAllDropdowns } from \"../dropdown/bind-dropdown\";\nimport {\n computeFixedPricing,\n formatCents,\n formatUnitPrice,\n parseCents,\n} from \"./pricing\";\nimport { renderCountdown } from \"./countdown\";\nimport { buildCtaButton } from \"./cta-button\";\nimport { el } from \"./dom\";\nimport { THUMB_PX, transformImageUrl } from \"./image\";\n\nconst PLACEHOLDER_THUMB_SVG = `\n<svg class=\"lb-bundle-placeholder-icon\" viewBox=\"0 0 28 28\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" focusable=\"false\">\n <rect x=\"4\" y=\"4\" width=\"20\" height=\"20\" rx=\"3\"></rect>\n <line x1=\"4\" y1=\"20\" x2=\"24\" y2=\"20\"></line>\n <circle cx=\"10\" cy=\"12\" r=\"2\"></circle>\n</svg>`;\n\ninterface ProductRowState {\n product: Product;\n /** Variants eligible for this row (intersection of available + merchant selection). */\n eligibleVariants: ProductVariant[];\n /** Currently selected variant; null when none are available (OOS). */\n selected: ProductVariant | null;\n /** Quantity applied to this row (merchant product/variant qty or 1). */\n qty: number;\n /** Whether the product has zero available variants. */\n isOos: boolean;\n}\n\nexport function renderFixedBundle(\n container: HTMLElement,\n bundle: FixedBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n) {\n const wc = bundle.widgetConfig;\n\n // Per-variant quantity lookup — delegated to the canonical resolver in\n // `@lime-bundles/core` so the fallback chain stays in lockstep with the\n // React SDK and the server-side discount metafield producer.\n const qtyFor = (productId: string, variantId: string): number =>\n resolveBundleQty(bundle, productId, variantId);\n\n // Build row state honouring merchant selections + OOS behaviour.\n const rows: ProductRowState[] = [];\n let oosCount = 0;\n bundle.products.forEach((product, idx) => {\n const row = buildRowState(bundle, product, idx);\n // Merchant explicitly set productQuantities/variantQuantities to 0 —\n // skip the row entirely. Treated as opt-out, not as a zero-quantity\n // line in pricing.\n if (row.qty === 0) return;\n if (row.isOos) {\n oosCount++;\n if (wc.outOfStockBehavior === \"hide\") return; // skip the row entirely\n }\n rows.push(row);\n });\n\n // Bundle-level guard: fixed bundles are all-or-nothing. Even in\n // \"show_greyed_out\" mode, if any product has no stock, disable the CTA\n // and render a warning. In \"hide\" mode, if we lost any rows, bail out\n // entirely — matches Liquid behaviour.\n if (rows.length === 0) return;\n\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n const root = el(\"div\", \"lb-fixed\", {\n \"data-discount-type\": bundle.discountConfig.discountType,\n \"data-discount-value\": String(bundle.discountConfig.discountValue),\n });\n\n // --- Header (title + subtitle + save badge) ---\n const headerHandle = renderHeader(bundle, currency);\n root.appendChild(headerHandle.el);\n\n // --- Countdown ---\n if (wc.countdown.showCountdown && bundle.endsAt) {\n const countdown = renderCountdown(bundle.endsAt);\n if (countdown) {\n root.appendChild(countdown.el);\n onCleanup?.(countdown.stop);\n }\n }\n\n // --- Product list ---\n const list = el(\"div\", \"lb-fixed__products\");\n const rowHandles: Array<ReturnType<typeof renderProductRow>> = [];\n rows.forEach((rowState) => {\n const handle = renderProductRow(\n rowState,\n currency,\n qtyFor,\n {\n enabled: wc.showLowStockBadge,\n threshold: wc.lowStockThreshold,\n },\n () => {\n // Variant change → recompute pricing.\n updatePricing();\n },\n );\n rowHandles.push(handle);\n list.appendChild(handle.el);\n });\n root.appendChild(list);\n\n root.appendChild(el(\"div\", \"lb-bundle-divider\"));\n\n // --- Pricing row + savings bar ---\n const pricingHandle = renderPricingRow(bundle);\n root.appendChild(pricingHandle.el);\n const savingsBarHandle = wc.savingsBar.visible ? renderSavingsBar() : null;\n if (savingsBarHandle) root.appendChild(savingsBarHandle.el);\n\n // --- CTA ---\n const cta = renderCta(bundle, oosCount, () => {\n const lines: CartLineInput[] = rows\n .filter((r) => r.selected)\n .map((r) => ({\n merchandiseId: r.selected!.id,\n quantity: r.qty,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n }));\n if (lines.length === 0) return;\n onAddToCart(lines);\n });\n root.appendChild(cta);\n\n root.appendChild(\n el(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n container.appendChild(root);\n\n // Replace native <select> popup chrome with our accessible custom dropdown.\n // Native <select> stays in DOM as state holder; the change handler attached\n // above continues to fire on commit.\n bindAllDropdowns(root);\n onCleanup?.(() => unbindAllDropdowns(root));\n\n updatePricing();\n\n function updatePricing() {\n const totalCents = rows.reduce((sum, r) => {\n if (!r.selected) return sum;\n const unit = parseCents(r.selected.price.amount);\n return sum + unit * r.qty;\n }, 0);\n const saleCents = computeSale(totalCents, bundle.discountConfig, rows);\n const savingsCents = Math.max(0, totalCents - saleCents);\n\n pricingHandle.update({ totalCents, saleCents, savingsCents, currency });\n if (savingsBarHandle) {\n savingsBarHandle.update({ savingsCents, currency });\n }\n headerHandle.refresh(\n deriveHeaderBadge(bundle, totalCents, saleCents, currency),\n );\n }\n}\n\n// --- State ---\n\nfunction buildRowState(\n bundle: FixedBundleData,\n product: Product,\n productIndex: number,\n): ProductRowState {\n const selectedVariantIds =\n bundle.selectedVariantIds?.[productIndex] ?? null;\n\n // All variants the merchant scoped into the bundle. Sold-out variants are\n // INCLUDED here so the per-option dropdowns render them as disabled\n // options (same UX as unavailable combinations) rather than hiding them\n // from the picker. The initial `selected` still prefers an in-stock\n // variant so the default shown is purchasable.\n const merchantScoped =\n selectedVariantIds && selectedVariantIds.length > 0\n ? product.variants.nodes.filter((v) => selectedVariantIds.includes(v.id))\n : product.variants.nodes;\n\n // \"In stock\" here means fulfillable for THIS variant's required quantity —\n // a variant with 3 units and required qty 5 is unfulfillable, even though\n // `availableForSale` is true. See packages/core/src/inventory/predicate.ts.\n const firstInStock =\n merchantScoped.find((v) =>\n isVariantFulfillable(v, resolveBundleQty(bundle, product.id, v.id)),\n ) ?? null;\n const eligibleVariants = merchantScoped;\n const isOos = !firstInStock;\n const selected = firstInStock ?? merchantScoped[0] ?? null;\n const qty = selected ? resolveBundleQty(bundle, product.id, selected.id) : 1;\n\n return { product, eligibleVariants, selected, qty, isOos };\n}\n\n// --- Section renderers ---\n\ninterface HeaderHandle {\n el: HTMLElement;\n /** Update the save-badge text when pricing changes. */\n refresh: (badgeText: string) => void;\n}\n\nfunction renderHeader(\n bundle: FixedBundleData,\n currency: string,\n): HeaderHandle {\n const wc = bundle.widgetConfig;\n const header = el(\"div\", \"lb-bundle-header\");\n const content = el(\"div\", \"lb-bundle-header__content\");\n\n const title = el(\"h3\", \"lb-bundle-title\");\n title.textContent = bundle.title;\n content.appendChild(title);\n\n if (bundle.description) {\n const subtitle = el(\"p\", \"lb-bundle-subtitle\");\n subtitle.textContent = bundle.description;\n content.appendChild(subtitle);\n }\n header.appendChild(content);\n\n const badgeEl = el(\"span\", \"lb-bundle-header__badge\", {\n \"data-header-badge\": \"\",\n });\n header.appendChild(badgeEl);\n\n // Placeholder initial badge — updated by refresh() from updatePricing().\n const initialPricing = computeFixedPricing(\n bundle,\n bundle.productQuantities,\n wc.pricing.showSaveBadge,\n );\n if (initialPricing.headerBadge) {\n badgeEl.textContent = initialPricing.headerBadge;\n } else {\n badgeEl.style.display = \"none\";\n }\n void currency;\n\n return {\n el: header,\n refresh(badgeText) {\n if (!wc.pricing.showSaveBadge) {\n badgeEl.style.display = \"none\";\n return;\n }\n if (badgeText) {\n badgeEl.textContent = badgeText;\n badgeEl.style.display = \"\";\n } else {\n badgeEl.style.display = \"none\";\n }\n },\n };\n}\n\nfunction deriveHeaderBadge(\n bundle: FixedBundleData,\n totalCents: number,\n saleCents: number,\n currency: string,\n): string {\n if (!bundle.widgetConfig.pricing.showSaveBadge) return \"\";\n const savings = totalCents - saleCents;\n if (savings <= 0) return \"\";\n const dc = bundle.discountConfig;\n if (dc.discountType === \"percentage\" && dc.discountValue > 0) {\n return `-${Math.round(dc.discountValue)}%`;\n }\n if (dc.discountType === \"fixed_amount\" && dc.discountValue > 0) {\n return `-${formatCents(Math.round(dc.discountValue * 100), currency)}`;\n }\n return `-${formatCents(savings, currency)}`;\n}\n\ninterface ProductRowHandle {\n el: HTMLElement;\n state: ProductRowState;\n}\n\nfunction renderProductRow(\n state: ProductRowState,\n currency: string,\n qtyFor: (productId: string, variantId: string) => number,\n lowStock: { enabled: boolean; threshold: number },\n onVariantChange: () => void,\n): ProductRowHandle {\n const rowEl = el(\n \"div\",\n state.isOos\n ? \"lb-bundle-product-row lb-bundle-product-row--oos\"\n : \"lb-bundle-product-row\",\n {\n \"data-product-id\": state.product.id.replace(/^.*\\//, \"\"),\n ...(state.isOos ? { \"aria-disabled\": \"true\" } : {}),\n },\n );\n\n // Prefer the selected variant's image so the thumb tracks colour-swatch\n // selections; falls back to product hero, then placeholder.\n const thumb = el(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n const initialThumbImage =\n state.selected?.image ?? state.product.featuredImage ?? null;\n let thumbImg: HTMLImageElement | null = null;\n if (initialThumbImage) {\n thumbImg = document.createElement(\"img\");\n thumbImg.src = transformImageUrl(initialThumbImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = initialThumbImage.altText ?? state.product.title;\n thumbImg.width = THUMB_PX;\n thumbImg.height = THUMB_PX;\n thumbImg.loading = \"lazy\";\n thumb.appendChild(thumbImg);\n } else {\n thumb.insertAdjacentHTML(\"beforeend\", PLACEHOLDER_THUMB_SVG);\n }\n let qtyBadgeRef: HTMLElement | null = null;\n if (!state.isOos) {\n qtyBadgeRef = el(\"span\", \"lb-bundle-qty-badge\", {\n \"data-qty-badge\": \"\",\n });\n qtyBadgeRef.textContent = String(state.qty);\n thumb.appendChild(qtyBadgeRef);\n }\n rowEl.appendChild(thumb);\n\n // Info column\n const info = el(\"div\", \"lb-bundle-product-info\");\n const name = document.createElement(\"a\");\n name.className = \"lb-bundle-product-name\";\n name.href = `/products/${state.product.handle}`;\n name.textContent = state.product.title;\n info.appendChild(name);\n\n if (state.isOos) {\n const oosLabel = el(\"span\", \"lb-bundle-oos-label\");\n oosLabel.textContent = \"Out of stock\";\n info.appendChild(oosLabel);\n } else if (state.selected) {\n // Read-only variant text right under the title — same treatment as\n // the mix-match widget's filled-slot variant. Only renders when\n // the merchant pinned a single variant of a multi-variant product\n // (no picker would render in that case). Suppressed for products\n // with the default single variant.\n const isSinglePinnedVariant =\n state.eligibleVariants.length === 1 &&\n state.product.variants.nodes.length > 1;\n if (isSinglePinnedVariant) {\n const badge = el(\"span\", \"lb-bundle-variant-badge\");\n badge.textContent = state.eligibleVariants[0].title;\n info.appendChild(badge);\n }\n\n // Price row — updated on variant change.\n const prices = el(\"span\", \"lb-bundle-product-prices\");\n const compare = el(\"span\", \"lb-bundle-product-compare-price\", {\n \"data-product-compare-price\": \"\",\n });\n const priceEl = el(\"span\", \"lb-bundle-product-price\", {\n \"data-product-price\": \"\",\n });\n prices.appendChild(compare);\n prices.appendChild(priceEl);\n info.appendChild(prices);\n\n // Unit price (e.g. \"$0.50/100ml\") — sibling of the price row so it sits\n // on its own line beneath the price. Hidden when the merchant hasn't\n // set unit pricing in the Shopify admin.\n const unitPriceEl = el(\"span\", \"lb-bundle-product-unit-price\", {\n \"data-product-unit-price\": \"\",\n });\n unitPriceEl.setAttribute(\"hidden\", \"\");\n info.appendChild(unitPriceEl);\n\n // Low-stock badge — updated alongside price on variant change. Hidden\n // when the threshold isn't met or quantityAvailable is unknown.\n const lowStockEl = el(\"span\", \"lb-bundle-low-stock-badge\", {\n \"data-low-stock-badge\": \"\",\n });\n lowStockEl.setAttribute(\"hidden\", \"\");\n info.appendChild(lowStockEl);\n\n const applyVariantToRow = (variant: ProductVariant) => {\n const unit = parseCents(variant.price.amount);\n priceEl.textContent = formatCents(unit, currency);\n if (variant.compareAtPrice) {\n const cmp = parseCents(variant.compareAtPrice.amount);\n if (cmp > unit) {\n compare.textContent = formatCents(cmp, currency);\n compare.removeAttribute(\"hidden\");\n } else {\n compare.setAttribute(\"hidden\", \"\");\n }\n } else {\n compare.setAttribute(\"hidden\", \"\");\n }\n const unitText = formatUnitPrice(\n variant.unitPrice,\n variant.unitPriceMeasurement,\n currency,\n );\n if (unitText) {\n unitPriceEl.textContent = unitText;\n unitPriceEl.removeAttribute(\"hidden\");\n } else {\n unitPriceEl.setAttribute(\"hidden\", \"\");\n }\n const nextImage = variant.image ?? state.product.featuredImage;\n if (thumbImg && nextImage) {\n thumbImg.src = transformImageUrl(nextImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = nextImage.altText ?? state.product.title;\n }\n\n const required = qtyFor(state.product.id, variant.id);\n if (\n shouldShowLowStockBadge(\n variant,\n required,\n lowStock.threshold,\n lowStock.enabled,\n )\n ) {\n lowStockEl.textContent = `Only ${variant.quantityAvailable} left`;\n lowStockEl.removeAttribute(\"hidden\");\n } else {\n lowStockEl.setAttribute(\"hidden\", \"\");\n }\n };\n\n applyVariantToRow(state.selected);\n\n // Per-option dropdowns when more than one eligible variant exists —\n // Shopify's recommended approach via product.options_with_values (here\n // derived from variants[].selectedOptions since the Storefront API gives\n // us that). Values that don't combine with the current selection of other\n // options are disabled (Dawn-style availability) so the customer sees\n // what's possible instead of the variant silently jumping combos.\n if (state.eligibleVariants.length > 1) {\n const optionNames: string[] = state.eligibleVariants[0].selectedOptions.map(\n (o) => o.name,\n );\n const productIdTail = state.product.id.replace(/^.*\\//, \"\");\n const optionSelects: HTMLSelectElement[] = [];\n\n const resolveVariant = (values: string[]) =>\n state.eligibleVariants.find(\n (v) =>\n v.selectedOptions.every((o, i) => o.value === values[i]) &&\n v.selectedOptions.length === values.length,\n ) ?? null;\n\n const syncSelectsToVariant = (variant: ProductVariant) => {\n variant.selectedOptions.forEach((o, i) => {\n const sel = optionSelects[i];\n if (sel && sel.value !== o.value) sel.value = o.value;\n });\n };\n\n const isValueAvailable = (\n optionIndex: number,\n value: string,\n selected: string[],\n ): boolean =>\n state.eligibleVariants.some((v) => {\n if (!isVariantFulfillable(v, qtyFor(state.product.id, v.id))) return false;\n if (v.selectedOptions[optionIndex]?.value !== value) return false;\n return v.selectedOptions.every(\n (o, i) => i === optionIndex || o.value === selected[i],\n );\n });\n\n const recomputeDisabled = (selected: string[]) => {\n optionSelects.forEach((sel, i) => {\n Array.from(sel.options).forEach((opt) => {\n opt.disabled = !isValueAvailable(i, opt.value, selected);\n });\n });\n };\n\n const handleChange = () => {\n const values = optionSelects.map((s) => s.value);\n const variant = resolveVariant(values);\n if (!variant) {\n // Disabled combo reached (keyboard nav edge case) — revert selects\n // to the previously selected variant rather than jumping.\n if (state.selected) {\n syncSelectsToVariant(state.selected);\n recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));\n }\n return;\n }\n state.selected = variant;\n state.qty = qtyFor(state.product.id, variant.id);\n if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);\n applyVariantToRow(variant);\n recomputeDisabled(variant.selectedOptions.map((o) => o.value));\n onVariantChange();\n };\n\n const groupsContainer = el(\"div\", \"lb-bundle-variant-option-groups\");\n optionNames.forEach((name, position) => {\n const group = el(\"div\", \"lb-bundle-variant-option-group\");\n\n const label = el(\"span\", \"lb-bundle-variant-option-label\");\n label.textContent = name;\n group.appendChild(label);\n\n const select = document.createElement(\"select\");\n select.className = \"lb-bundle-variant-select\";\n select.setAttribute(\"data-variant-option\", \"\");\n select.setAttribute(\"data-option-position\", String(position + 1));\n select.name = `lb-variant-${productIdTail}-${position + 1}`;\n select.setAttribute(\"aria-label\", name);\n\n const seen = new Set<string>();\n state.eligibleVariants.forEach((v) => {\n const value = v.selectedOptions[position]?.value;\n if (!value || seen.has(value)) return;\n seen.add(value);\n const opt = document.createElement(\"option\");\n opt.value = value;\n opt.textContent = value;\n if (state.selected?.selectedOptions[position]?.value === value) {\n opt.selected = true;\n }\n select.appendChild(opt);\n });\n\n select.addEventListener(\"change\", handleChange);\n optionSelects.push(select);\n group.appendChild(select);\n groupsContainer.appendChild(group);\n });\n info.appendChild(groupsContainer);\n\n if (state.selected) {\n recomputeDisabled(state.selected.selectedOptions.map((o) => o.value));\n }\n }\n // Single-variant case is handled above the price row, immediately\n // under the product title (same pattern as the mix-match widget).\n }\n\n rowEl.appendChild(info);\n return { el: rowEl, state };\n}\n\ninterface PricingHandle {\n el: HTMLElement;\n update: (p: {\n totalCents: number;\n saleCents: number;\n savingsCents: number;\n currency: string;\n }) => void;\n}\n\nfunction renderPricingRow(bundle: FixedBundleData): PricingHandle {\n const row = el(\"div\", \"lb-bundle-pricing\");\n const label = el(\"span\", \"lb-bundle-pricing__label\");\n label.textContent = \"Bundle price\";\n row.appendChild(label);\n\n const prices = el(\"span\", \"lb-bundle-pricing__prices\");\n const compare = el(\"span\", \"lb-bundle-compare-price\", {\n \"data-compare-price\": \"\",\n });\n compare.style.display = \"none\";\n prices.appendChild(compare);\n const sale = el(\"span\", \"lb-bundle-sale-price\", {\n \"data-sale-price\": \"\",\n });\n prices.appendChild(sale);\n row.appendChild(prices);\n\n return {\n el: row,\n update({ totalCents, saleCents, savingsCents, currency }) {\n sale.textContent = formatCents(saleCents, currency);\n if (bundle.widgetConfig.pricing.showCompareAtPrice && savingsCents > 0) {\n compare.textContent = formatCents(totalCents, currency);\n compare.style.display = \"\";\n } else {\n compare.style.display = \"none\";\n }\n },\n };\n}\n\ninterface SavingsBarHandle {\n el: HTMLElement;\n update: (p: { savingsCents: number; currency: string }) => void;\n}\n\nfunction renderSavingsBar(): SavingsBarHandle {\n const bar = el(\"div\", \"lb-bundle-savings-bar\", {\n \"data-savings-bar\": \"\",\n });\n const label = document.createElement(\"span\");\n label.textContent = \"You save\";\n bar.appendChild(label);\n const amount = el(\"span\", \"\", { \"data-savings-amount\": \"\" });\n bar.appendChild(amount);\n return {\n el: bar,\n update({ savingsCents, currency }) {\n if (savingsCents <= 0) {\n bar.style.display = \"none\";\n return;\n }\n bar.style.display = \"\";\n amount.textContent = formatCents(savingsCents, currency);\n },\n };\n}\n\nfunction renderCta(\n bundle: FixedBundleData,\n oosCount: number,\n onClick: () => void,\n): HTMLElement {\n const label =\n oosCount > 0\n ? `${oosCount} item${oosCount === 1 ? \"\" : \"s\"} out of stock`\n : bundle.widgetConfig.cta.ctaText || \"Add to cart\";\n const button = buildCtaButton(label);\n if (oosCount > 0) {\n button.disabled = true;\n } else {\n button.addEventListener(\"click\", () => {\n if (button.disabled) return;\n onClick();\n });\n }\n return button;\n}\n\n// --- Pricing helpers ---\n\nfunction computeSale(\n totalCents: number,\n discount: FixedBundleData[\"discountConfig\"],\n rows: ProductRowState[],\n): number {\n if (discount.discountType === \"percentage\") {\n // Per-unit floor rounding — matches Shopify Discount Function.\n let saleCents = 0;\n for (const r of rows) {\n if (!r.selected) continue;\n const unit = parseCents(r.selected.price.amount);\n const off = Math.floor((unit * discount.discountValue) / 100);\n const perUnit = Math.max(0, unit - off);\n saleCents += perUnit * r.qty;\n }\n return saleCents;\n }\n // fixed_amount: total minus absolute discount (clamped >= 0).\n return Math.max(0, totalCents - Math.round(discount.discountValue * 100));\n}\n\n","/**\n * DOM renderer for mix-and-match bundles.\n *\n * Mirrors `extensions/bundle-theme/snippets/lb-mix-match.liquid` and its\n * associated picker-modal JS. Merchant flow:\n *\n * 1. Widget renders `bundle.minQuantity` empty slots (one per distinct\n * product the shopper must pick).\n * 2. Clicking a slot opens the picker modal with the eligible products.\n * 3. Inside the modal, each product card shows a variant select, a qty\n * stepper (when `widgetConfig.mixMatchShowQuantitySelector !== false`),\n * and an Add button. The stepper is bounded by the merchant's\n * `productRules[productId]` (`{ min, max }`) and the variant's stock\n * via `maxAddableQuantity`.\n * 4. Adding a pick fills a slot with the chosen variant and quantity.\n * Remove-x on a filled slot empties it again.\n * 5. CTA unlocks when distinct picks ≥ `bundle.minQuantity`. On click,\n * selections are aggregated by `(productId, variantId)` into one cart\n * line per variant with summed quantity.\n *\n * Class names match the Liquid template one-for-one so the ported\n * bundle-mix-match.css styles this DOM without changes.\n */\nimport type {\n CartLineInput,\n MixMatchBundleData,\n Product,\n ProductRule,\n ProductVariant,\n} from \"@lime-bundles/core\";\nimport {\n DEFAULT_PRODUCT_RULE,\n isVariantFulfillable,\n maxAddableQuantity,\n shouldShowLowStockBadge,\n} from \"@lime-bundles/core\";\nimport {\n computeBundleSaleCents,\n formatCents,\n formatUnitPrice,\n parseCents,\n} from \"./pricing\";\nimport { bindAllDropdowns, unbindAllDropdowns } from \"../dropdown/bind-dropdown\";\nimport { renderCountdown } from \"./countdown\";\nimport { buildCtaButton, setCtaLabel } from \"./cta-button\";\nimport { el } from \"./dom\";\nimport { THUMB_PX, transformImageUrl } from \"./image\";\n\ninterface EligibleProduct {\n product: Product;\n variants: ProductVariant[];\n firstAvailableVariant: ProductVariant | null;\n isOos: boolean;\n}\n\n/**\n * One pick by the shopper. A pick is a unique (productId, variantId) row\n * in the slot list with its own merchant-bounded quantity. Two picks of\n * the same variant are not allowed — the shopper bumps the stepper instead\n * (the slot's row is replaced when re-added). The CartLineInput aggregator\n * still defends against duplicate (productId, variantId) entries by summing.\n */\ninterface Selection {\n productId: string;\n productTitle: string;\n variantId: string;\n variantTitle: string;\n imageUrl: string | null;\n priceCents: number;\n compareCents: number | null;\n /** Pre-formatted unit price (\"$0.50/100ml\") for the filled-slot view. */\n unitPriceLabel: string | null;\n /** Shopper-chosen qty, clamped to `[rule.min, maxAddableQuantity(...)]`. */\n quantity: number;\n}\n\nconst PLACEHOLDER_THUMB_SVG = `\n<svg class=\"lb-bundle-placeholder-icon\" viewBox=\"0 0 28 28\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\" focusable=\"false\">\n <rect x=\"4\" y=\"4\" width=\"20\" height=\"20\" rx=\"3\"></rect>\n <line x1=\"4\" y1=\"20\" x2=\"24\" y2=\"20\"></line>\n <circle cx=\"10\" cy=\"12\" r=\"2\"></circle>\n</svg>`;\n\nconst PLUS_ICON_SVG = `\n<svg width=\"18\" height=\"18\" viewBox=\"0 0 18 18\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"9\" y1=\"3\" x2=\"9\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <line x1=\"3\" y1=\"9\" x2=\"15\" y2=\"9\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\nconst CLOSE_ICON_SVG = `\n<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"5\" y1=\"5\" x2=\"15\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <line x1=\"15\" y1=\"5\" x2=\"5\" y2=\"15\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\nconst SEARCH_CLEAR_ICON_SVG = `\n<svg width=\"16\" height=\"16\" viewBox=\"0 0 20 20\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M14.348 5.652a.5.5 0 0 0-.707 0L10 9.293 6.36 5.652a.5.5 0 1 0-.708.707L9.293 10l-3.641 3.641a.5.5 0 0 0 .708.707L10 10.707l3.641 3.641a.5.5 0 0 0 .707-.707L10.707 10l3.641-3.641a.5.5 0 0 0 0-.707z\"/>\n</svg>`;\n\nconst STEPPER_MINUS_SVG = `\n<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"3\" y1=\"7\" x2=\"11\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\nconst STEPPER_PLUS_SVG = `\n<svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <line x1=\"7\" y1=\"3\" x2=\"7\" y2=\"11\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n <line x1=\"3\" y1=\"7\" x2=\"11\" y2=\"7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/>\n</svg>`;\n\n/** Resolve the merchant rule for a product, applying the runtime default. */\nfunction ruleFor(bundle: MixMatchBundleData, productId: string): ProductRule {\n return bundle.productRules[productId] ?? DEFAULT_PRODUCT_RULE;\n}\n\n/** Sum of quantities the shopper has already allocated to this variant. */\nfunction alreadyInBundleFor(\n selections: Selection[],\n productId: string,\n variantId: string,\n): number {\n let sum = 0;\n for (const s of selections) {\n if (s.productId === productId && s.variantId === variantId) sum += s.quantity;\n }\n return sum;\n}\n\nexport function renderMixMatchBundle(\n container: HTMLElement,\n bundle: MixMatchBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n) {\n const wc = bundle.widgetConfig;\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n const requiredQty = bundle.minQuantity ?? 1;\n const showQtySelector = wc.mixMatchShowQuantitySelector !== false;\n\n // Build eligible-products list, honoring outOfStockBehavior. A product is\n // eligible when at least one of its variants can satisfy `rule.min` units.\n const eligible = buildEligibleProducts(bundle, wc.outOfStockBehavior);\n const inStockCount = eligible.filter((e) => !e.isOos).length;\n\n // Bundle visibility guard: if we can't possibly satisfy requiredQty from\n // distinct in-stock products, don't render the widget at all. Matches\n // Liquid.\n if (inStockCount < requiredQty) return;\n\n const selections: Selection[] = [];\n const root = el(\"div\", \"lb-mix-match\", {\n \"data-required-quantity\": String(requiredQty),\n });\n\n // --- Header ---\n const header = renderHeader(bundle);\n root.appendChild(header);\n\n // --- Countdown ---\n if (wc.countdown.showCountdown && bundle.endsAt) {\n const countdown = renderCountdown(bundle.endsAt);\n if (countdown) {\n root.appendChild(countdown.el);\n onCleanup?.(countdown.stop);\n }\n }\n\n // --- Progress bar ---\n const progress = renderProgress(requiredQty);\n root.appendChild(progress.el);\n\n // --- Slots ---\n const slotsContainer = el(\"div\", \"lb-mix-match__slots\", {\n \"data-selection-slots\": \"\",\n });\n root.appendChild(slotsContainer);\n\n root.appendChild(el(\"div\", \"lb-bundle-divider\"));\n\n // --- Pricing (hidden until first selection) ---\n const pricingSection = renderPricingSection(wc.pricing.showCompareAtPrice);\n root.appendChild(pricingSection.el);\n\n const savingsBar = wc.savingsBar.visible ? renderSavingsBar() : null;\n if (savingsBar) root.appendChild(savingsBar.el);\n\n // --- CTA ---\n // Built before the modal so the picker's getFocusAfterAdd closure can\n // refer to it as a fallback when every slot is filled.\n const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);\n cta.disabled = true;\n cta.addEventListener(\"click\", () => {\n if (cta.disabled) return;\n onAddToCart(buildCartLines(selections, bundle));\n });\n\n // --- Modal overlay ---\n const modal = renderModal(bundle, eligible, currency, {\n showSearch: wc.showSearch,\n showQtySelector,\n selections,\n onAdd: (product, variant, quantity) =>\n addSelection(product, variant, quantity),\n isComplete: () => selections.length >= requiredQty,\n getFocusAfterAdd: () => {\n // afterMutation has already re-rendered slots, so the first\n // .lb-mix-match__slot--empty is wherever focus should go next. When\n // every slot is filled, fall back to the now-enabled CTA so Enter\n // adds the whole bundle to cart.\n const empty = slotsContainer.querySelector<HTMLElement>(\n \".lb-mix-match__slot--empty\",\n );\n return empty ?? cta;\n },\n });\n root.appendChild(modal.el);\n\n root.appendChild(cta);\n\n root.appendChild(\n el(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n container.appendChild(root);\n\n // Custom dropdown teardown — bind happens lazily inside buildRows().\n onCleanup?.(() => unbindAllDropdowns(root));\n\n // Initial render. afterMutation() handles slots, progress, pricing,\n // savings bar, placeholder, modal refreshCounts, and CTA in one place.\n // Safe to call here: modal.refreshCounts is a no-op while the picker\n // is closed (productRows is built lazily on first open).\n afterMutation();\n\n // --- Mutation helpers (closures over local state) ---\n\n function addSelection(\n product: Product,\n variant: ProductVariant,\n quantity: number,\n ) {\n if (selections.length >= requiredQty) return;\n const rule = ruleFor(bundle, product.id);\n const cap = maxAddableQuantity(\n variant,\n rule.max,\n alreadyInBundleFor(selections, product.id, variant.id),\n );\n // Refuse the pick when stock can't satisfy the per-product minimum —\n // otherwise the slot would carry more units than the variant has, and\n // Shopify's checkout-time inventory check would fail with an opaque\n // \"out of stock\" error after the customer hit Add to cart.\n if (cap < rule.min) return;\n // Clamp defensively. The picker stepper enforces these bounds, but\n // a malformed Add (e.g. keyboard event before stepper init) shouldn't\n // bypass them.\n const clamped = Math.max(rule.min, Math.min(quantity, cap));\n selections.push({\n productId: product.id,\n productTitle: product.title,\n variantId: variant.id,\n variantTitle: variant.title,\n imageUrl: variant.image?.url ?? product.featuredImage?.url ?? null,\n priceCents: parseCents(variant.price.amount),\n compareCents: variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null,\n unitPriceLabel: formatUnitPrice(\n variant.unitPrice,\n variant.unitPriceMeasurement,\n currency,\n ),\n quantity: clamped,\n });\n afterMutation();\n }\n\n function removeSlotAt(index: number) {\n if (index < 0 || index >= selections.length) return;\n selections.splice(index, 1);\n afterMutation();\n }\n\n function afterMutation() {\n renderSlots();\n progress.update(selections.length);\n pricingSection.update(selections, bundle, currency);\n if (savingsBar) savingsBar.update(selections, bundle, currency);\n modal.refreshCounts();\n updateCta();\n }\n\n function renderSlots() {\n slotsContainer.innerHTML = \"\";\n const totalSlots = Math.max(requiredQty, selections.length);\n for (let i = 0; i < totalSlots; i++) {\n const selection = selections[i];\n if (selection) {\n slotsContainer.appendChild(\n renderFilledSlot(selection, i, currency, () => removeSlotAt(i)),\n );\n } else {\n slotsContainer.appendChild(\n renderEmptySlot(i, () => modal.open()),\n );\n }\n }\n }\n\n function updateCta() {\n // Target the label span, not the button itself — replacing the button's\n // textContent would destroy the sibling spinner span built by\n // buildCtaButton. See packages/widget/src/renderers/cta-button.ts.\n const count = selections.length;\n if (count < requiredQty) {\n cta.disabled = true;\n setCtaLabel(cta, `Select ${requiredQty - count} more to unlock`);\n } else {\n cta.disabled = false;\n setCtaLabel(cta, wc.cta.ctaText || \"Add to cart\");\n }\n }\n}\n\n/**\n * Aggregate selections into one CartLineInput per (productId, variantId).\n * Matches the discount-function attribution contract: every line carries\n * `_lime_bundle_gid` and `_lime_bundle_type` so the orders/create webhook\n * can map purchases back to this bundle.\n */\nfunction buildCartLines(\n selections: Selection[],\n bundle: MixMatchBundleData,\n): CartLineInput[] {\n const grouped = new Map<string, { variantId: string; quantity: number }>();\n for (const s of selections) {\n const key = `${s.productId}::${s.variantId}`;\n const existing = grouped.get(key);\n if (existing) {\n existing.quantity += s.quantity;\n } else {\n grouped.set(key, { variantId: s.variantId, quantity: s.quantity });\n }\n }\n return Array.from(grouped.values()).map((line) => ({\n merchandiseId: line.variantId,\n quantity: line.quantity,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n }));\n}\n\n// --- Section renderers ---\n\nfunction renderHeader(bundle: MixMatchBundleData): HTMLElement {\n const wc = bundle.widgetConfig;\n const header = el(\"div\", \"lb-bundle-header\");\n const content = el(\"div\", \"lb-bundle-header__content\");\n const title = el(\"h3\", \"lb-bundle-title\");\n title.textContent = bundle.title;\n content.appendChild(title);\n header.appendChild(content);\n\n if (wc.pricing.showSaveBadge) {\n const { discountType, discountValue } = bundle.discountConfig;\n let label: string | null = null;\n if (discountType === \"percentage\" && discountValue > 0) {\n label = `-${Math.round(discountValue)}%`;\n } else if (discountType === \"fixed_amount\" && discountValue > 0) {\n label = `-${formatCents(\n Math.round(discountValue * 100),\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\",\n )}`;\n }\n if (label) {\n const badge = el(\"span\", \"lb-bundle-header__badge\");\n badge.textContent = label;\n header.appendChild(badge);\n }\n }\n return header;\n}\n\nfunction renderProgress(requiredQty: number) {\n const wrap = el(\"div\", \"lb-mix-match__progress\");\n const labels = el(\"div\", \"lb-mix-match__progress-labels\");\n const count = el(\"span\", \"lb-mix-match__progress-count\", {\n \"data-progress-count\": \"\",\n \"aria-live\": \"polite\",\n });\n count.textContent = `0 of ${requiredQty} selected`;\n labels.appendChild(count);\n const remaining = el(\"span\", \"lb-mix-match__progress-remaining\", {\n \"data-progress-remaining\": \"\",\n \"aria-live\": \"polite\",\n });\n remaining.textContent = `${requiredQty} more to go`;\n labels.appendChild(remaining);\n wrap.appendChild(labels);\n\n const track = el(\"div\", \"lb-mix-match__progress-track\", {\n role: \"progressbar\",\n \"aria-valuenow\": \"0\",\n \"aria-valuemin\": \"0\",\n \"aria-valuemax\": String(requiredQty),\n });\n const fill = el(\"div\", \"lb-mix-match__progress-fill\", {\n \"data-progress-fill\": \"\",\n });\n fill.style.width = \"0%\";\n track.appendChild(fill);\n wrap.appendChild(track);\n\n function update(selected: number) {\n const pct = Math.min(100, (selected / requiredQty) * 100);\n count.textContent = `${selected} of ${requiredQty} selected`;\n if (selected >= requiredQty) {\n remaining.textContent = \"Complete\";\n } else {\n remaining.textContent = `${requiredQty - selected} more to go`;\n }\n fill.style.width = `${pct}%`;\n track.setAttribute(\"aria-valuenow\", String(Math.min(selected, requiredQty)));\n }\n\n return { el: wrap, update };\n}\n\nfunction renderEmptySlot(index: number, onClick: () => void): HTMLElement {\n const slot = el(\n \"div\",\n \"lb-bundle-product-row lb-mix-match__slot lb-mix-match__slot--empty\",\n {\n \"data-slot\": String(index + 1),\n tabindex: \"0\",\n role: \"button\",\n \"aria-label\": \"Add a product to the bundle\",\n },\n );\n const thumb = el(\"div\", \"lb-mix-match__empty-thumb\");\n thumb.innerHTML = PLUS_ICON_SVG;\n slot.appendChild(thumb);\n const text = el(\"span\", \"lb-mix-match__empty-text\");\n text.textContent = \"Choose an item\";\n slot.appendChild(text);\n slot.addEventListener(\"click\", onClick);\n slot.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onClick();\n }\n });\n return slot;\n}\n\nfunction renderFilledSlot(\n selection: Selection,\n index: number,\n currency: string,\n onRemove: () => void,\n): HTMLElement {\n const slot = el(\n \"div\",\n \"lb-bundle-product-row lb-mix-match__slot lb-mix-match__slot--filled\",\n { \"data-slot\": String(index + 1) },\n );\n const thumb = el(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n if (selection.imageUrl) {\n const img = document.createElement(\"img\");\n img.src = transformImageUrl(selection.imageUrl, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n img.alt = selection.productTitle;\n img.width = THUMB_PX;\n img.height = THUMB_PX;\n img.loading = \"lazy\";\n thumb.appendChild(img);\n } else {\n thumb.insertAdjacentHTML(\"beforeend\", PLACEHOLDER_THUMB_SVG);\n }\n const qtyBadge = el(\"span\", \"lb-bundle-qty-badge\");\n // Filled-slot badge shows the shopper's chosen qty as \"× N\", which makes\n // it visually distinct from the picker's per-product qty stepper. Bare\n // \"1\" on a square thumbnail looks like a placeholder digit.\n qtyBadge.textContent = `×${selection.quantity}`;\n thumb.appendChild(qtyBadge);\n slot.appendChild(thumb);\n\n const info = el(\"div\", \"lb-mix-match__filled-info\");\n const title = el(\"span\", \"lb-mix-match__filled-title\");\n title.textContent = selection.productTitle;\n info.appendChild(title);\n if (selection.variantTitle && selection.variantTitle !== \"Default Title\") {\n const variant = el(\"span\", \"lb-mix-match__filled-variant\");\n variant.textContent = selection.variantTitle;\n info.appendChild(variant);\n }\n const linePrice = selection.priceCents * selection.quantity;\n const lineCompare =\n selection.compareCents !== null\n ? selection.compareCents * selection.quantity\n : null;\n const priceWrap = el(\"span\", \"lb-mix-match__filled-price\");\n if (lineCompare !== null && lineCompare > linePrice) {\n const compare = el(\"span\", \"lb-mix-match__filled-compare\");\n compare.textContent = formatCents(lineCompare, currency);\n priceWrap.appendChild(compare);\n }\n const priceEl = document.createElement(\"span\");\n priceEl.textContent = formatCents(linePrice, currency);\n priceWrap.appendChild(priceEl);\n info.appendChild(priceWrap);\n if (selection.unitPriceLabel) {\n const unitPrice = el(\"span\", \"lb-bundle-product-unit-price\");\n unitPrice.textContent = selection.unitPriceLabel;\n info.appendChild(unitPrice);\n }\n slot.appendChild(info);\n\n const remove = document.createElement(\"button\");\n remove.type = \"button\";\n remove.className = \"lb-mix-match__slot-remove\";\n remove.setAttribute(\"aria-label\", `Remove ${selection.productTitle}`);\n remove.innerHTML = CLOSE_ICON_SVG;\n remove.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n onRemove();\n });\n slot.appendChild(remove);\n return slot;\n}\n\nfunction renderPricingSection(showCompareAtPrice: boolean) {\n const wrap = el(\"div\", \"lb-bundle-pricing\", { \"data-pricing-section\": \"\" });\n wrap.style.display = \"none\";\n const label = el(\"span\", \"lb-bundle-pricing__label\");\n label.textContent = \"Bundle price\";\n wrap.appendChild(label);\n const prices = el(\"span\", \"lb-bundle-pricing__prices\");\n const compare = el(\"span\", \"lb-bundle-compare-price\", {\n \"data-compare-price\": \"\",\n });\n if (showCompareAtPrice) prices.appendChild(compare);\n const sale = el(\"span\", \"lb-bundle-sale-price\", { \"data-sale-price\": \"\" });\n prices.appendChild(sale);\n wrap.appendChild(prices);\n\n function update(\n selections: Selection[],\n bundle: MixMatchBundleData,\n currency: string,\n ) {\n if (selections.length === 0) {\n wrap.style.display = \"none\";\n return;\n }\n wrap.style.display = \"\";\n const totalCents = selections.reduce(\n (s, sel) => s + sel.priceCents * sel.quantity,\n 0,\n );\n const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);\n if (showCompareAtPrice && totalCents > saleCents) {\n compare.textContent = formatCents(totalCents, currency);\n compare.style.display = \"\";\n } else {\n compare.style.display = \"none\";\n }\n sale.textContent = formatCents(saleCents, currency);\n }\n\n return { el: wrap, update };\n}\n\nfunction renderSavingsBar() {\n const wrap = el(\"div\", \"lb-bundle-savings-bar\", {\n \"data-savings-bar\": \"\",\n });\n wrap.style.display = \"none\";\n const labelEl = document.createElement(\"span\");\n labelEl.textContent = \"You save\";\n wrap.appendChild(labelEl);\n const amount = el(\"span\", \"\", { \"data-savings-amount\": \"\" });\n wrap.appendChild(amount);\n\n function update(\n selections: Selection[],\n bundle: MixMatchBundleData,\n currency: string,\n ) {\n if (selections.length === 0) {\n wrap.style.display = \"none\";\n return;\n }\n const totalCents = selections.reduce(\n (s, sel) => s + sel.priceCents * sel.quantity,\n 0,\n );\n const saleCents = computeBundleSaleCents(totalCents, bundle.discountConfig);\n const savings = Math.max(0, totalCents - saleCents);\n if (savings <= 0) {\n wrap.style.display = \"none\";\n return;\n }\n wrap.style.display = \"\";\n amount.textContent = formatCents(savings, currency);\n }\n\n return { el: wrap, update };\n}\n\n// --- Picker modal ---\n\ninterface ModalHandlers {\n showSearch: boolean;\n showQtySelector: boolean;\n /** Live reference to the parent's selections. Read inside the modal to\n * compute remaining stock per variant (`maxAddableQuantity`). */\n selections: Selection[];\n onAdd: (product: Product, variant: ProductVariant, quantity: number) => void;\n isComplete: () => boolean;\n /** Where to send focus after Add closes the modal. The slot the modal was\n * opened from has been rebuilt as a filled slot (no tabindex/role) by the\n * parent's afterMutation, so close()'s default restore would .focus() a\n * non-focusable element and the browser would silently fall back to\n * <body> — dumping keyboard users at the address bar. The parent picks\n * the next empty slot, or the CTA when the bundle is complete. */\n getFocusAfterAdd: () => HTMLElement | null;\n}\n\nfunction renderModal(\n bundle: MixMatchBundleData,\n eligible: EligibleProduct[],\n currency: string,\n handlers: ModalHandlers,\n) {\n const overlay = el(\"div\", \"lb-mix-match__modal-overlay\", {\n \"data-modal-overlay\": \"\",\n \"data-bundle-gid\": bundle.id,\n });\n overlay.style.display = \"none\";\n\n const modal = el(\"div\", \"lb-mix-match__modal\", {\n role: \"dialog\",\n \"aria-modal\": \"true\",\n \"aria-labelledby\": `lb-modal-title-${sanitizeId(bundle.id)}`,\n tabindex: \"-1\",\n });\n\n // Header\n const modalHeader = el(\"div\", \"lb-mix-match__modal-header\");\n const modalTitle = el(\"h4\", \"lb-mix-match__modal-title\", {\n id: `lb-modal-title-${sanitizeId(bundle.id)}`,\n });\n modalTitle.textContent = \"Pick an item\";\n modalHeader.appendChild(modalTitle);\n const closeBtn = document.createElement(\"button\");\n closeBtn.type = \"button\";\n closeBtn.className = \"lb-mix-match__modal-close\";\n closeBtn.setAttribute(\"data-modal-close\", \"\");\n closeBtn.setAttribute(\"aria-label\", \"Close\");\n closeBtn.innerHTML = CLOSE_ICON_SVG;\n closeBtn.addEventListener(\"click\", close);\n modalHeader.appendChild(closeBtn);\n modal.appendChild(modalHeader);\n\n // Search\n let searchInput: HTMLInputElement | null = null;\n let searchClearBtn: HTMLButtonElement | null = null;\n if (handlers.showSearch) {\n const searchWrap = el(\"div\", \"lb-mix-match__modal-search\");\n searchInput = document.createElement(\"input\");\n searchInput.type = \"text\";\n searchInput.className = \"lb-mix-match__modal-search-input\";\n searchInput.setAttribute(\"data-modal-search\", \"\");\n searchInput.setAttribute(\"role\", \"searchbox\");\n searchInput.setAttribute(\"aria-label\", \"Search products\");\n searchInput.setAttribute(\"placeholder\", \"Search products\");\n searchInput.autocomplete = \"off\";\n searchInput.addEventListener(\"input\", () => applySearch());\n searchWrap.appendChild(searchInput);\n\n searchClearBtn = document.createElement(\"button\");\n searchClearBtn.type = \"button\";\n searchClearBtn.className = \"lb-mix-match__modal-search-clear\";\n searchClearBtn.setAttribute(\"data-modal-search-clear\", \"\");\n searchClearBtn.setAttribute(\"aria-label\", \"Clear search\");\n searchClearBtn.style.display = \"none\";\n searchClearBtn.innerHTML = SEARCH_CLEAR_ICON_SVG;\n searchClearBtn.addEventListener(\"click\", () => {\n if (!searchInput) return;\n searchInput.value = \"\";\n applySearch();\n searchInput.focus();\n });\n searchWrap.appendChild(searchClearBtn);\n modal.appendChild(searchWrap);\n }\n\n // Product list\n const list = el(\"div\", \"lb-mix-match__modal-list\", {\n \"data-modal-list\": \"\",\n });\n modal.appendChild(list);\n\n const empty = el(\"div\", \"lb-mix-match__modal-empty\", {\n \"data-modal-empty\": \"\",\n });\n empty.style.display = \"none\";\n const emptyText = document.createElement(\"p\");\n emptyText.textContent = \"No products match your search.\";\n empty.appendChild(emptyText);\n modal.appendChild(empty);\n\n const live = el(\"span\", \"lb-visually-hidden\", {\n \"data-modal-live\": \"\",\n \"aria-live\": \"polite\",\n });\n modal.appendChild(live);\n\n overlay.appendChild(modal);\n\n // Build product rows lazily on first open.\n let rowsBuilt = false;\n const productRows: Array<{\n el: HTMLElement;\n product: Product;\n variant: ProductVariant;\n /** Recompute stepper bounds + count badge from current selections. */\n refreshFromSelections: () => void;\n }> = [];\n\n function buildRows() {\n if (rowsBuilt) return;\n rowsBuilt = true;\n list.innerHTML = \"\";\n\n eligible.forEach((ep) => {\n const rule = ruleFor(bundle, ep.product.id);\n\n // Mirrors the Liquid picker-modal pattern in lb-mix-match.liquid:\n // - title/price/unit-price are <p> elements so they stack as blocks.\n // - <select> appears for products with >1 variant; the add button\n // dispatches the currently-selected variant.\n // - Variants that can't satisfy `rule.min` units are sold-out for\n // this bundle's purposes.\n const availableVariants = ep.variants;\n const firstAvailVariant =\n ep.variants.find((v) => isVariantFulfillable(v, rule.min)) ??\n ep.firstAvailableVariant ??\n ep.variants[0];\n if (!firstAvailVariant) return;\n\n let currentVariant = firstAvailVariant;\n\n const productEl = el(\n \"div\",\n ep.isOos\n ? \"lb-mix-match__modal-product lb-mix-match__modal-product--sold-out\"\n : \"lb-mix-match__modal-product\",\n { \"data-product-id\": ep.product.id.replace(/^.*\\//, \"\") },\n );\n\n const thumb = el(\"div\", \"lb-mix-match__modal-product-thumb\");\n const initialThumbVariant =\n ep.variants.find((v) => isVariantFulfillable(v, rule.min)) ??\n ep.variants[0] ??\n null;\n const initialThumbImage =\n initialThumbVariant?.image ?? ep.product.featuredImage ?? null;\n let thumbImg: HTMLImageElement | null = null;\n if (initialThumbImage) {\n thumbImg = document.createElement(\"img\");\n thumbImg.src = transformImageUrl(initialThumbImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = initialThumbImage.altText ?? ep.product.title;\n thumbImg.width = THUMB_PX;\n thumbImg.height = THUMB_PX;\n thumbImg.loading = \"lazy\";\n thumb.appendChild(thumbImg);\n } else {\n thumb.insertAdjacentHTML(\"beforeend\", PLACEHOLDER_THUMB_SVG);\n }\n // Count badge shows how many units of the *current* variant the\n // shopper has already pinned to this bundle. Hidden at zero.\n const countBadge = el(\"span\", \"lb-bundle-qty-badge\");\n countBadge.hidden = true;\n thumb.appendChild(countBadge);\n productEl.appendChild(thumb);\n\n const info = el(\"div\", \"lb-mix-match__modal-product-info\");\n const title = el(\"p\", \"lb-mix-match__modal-product-title\");\n title.textContent = ep.product.title;\n info.appendChild(title);\n\n const price = el(\"p\", \"lb-mix-match__modal-product-price\");\n // Picker price reflects single-unit price — the stepper shows\n // multiplier separately. Keeps the price label stable as the\n // shopper bumps the stepper.\n price.textContent = formatCents(parseCents(currentVariant.price.amount), currency);\n info.appendChild(price);\n\n const unitPrice = el(\n \"p\",\n \"lb-mix-match__modal-product-unit-price lb-bundle-product-unit-price\",\n );\n const initialUnitText = formatUnitPrice(\n currentVariant.unitPrice,\n currentVariant.unitPriceMeasurement,\n currency,\n );\n if (initialUnitText) {\n unitPrice.textContent = initialUnitText;\n } else {\n unitPrice.hidden = true;\n }\n info.appendChild(unitPrice);\n\n // Low-stock badge — re-rendered on variant change.\n const lowStockBadge = el(\"span\", \"lb-bundle-low-stock-badge\", {\n \"data-low-stock-badge\": \"\",\n });\n lowStockBadge.hidden = true;\n info.appendChild(lowStockBadge);\n const refreshLowStockBadge = (variant: ProductVariant) => {\n if (\n shouldShowLowStockBadge(\n variant,\n rule.min,\n bundle.widgetConfig.lowStockThreshold,\n bundle.widgetConfig.showLowStockBadge,\n )\n ) {\n lowStockBadge.textContent = `Only ${variant.quantityAvailable} left`;\n lowStockBadge.hidden = false;\n } else {\n lowStockBadge.hidden = true;\n }\n };\n refreshLowStockBadge(currentVariant);\n\n // Per-pick quantity stepper sits inside info between the low-stock\n // badge and the variant picker so the modal reads price → unit\n // price → qty → variant. Built before the variant logic so its\n // bounds closure can pick up later currentVariant updates from\n // the variant select handlers via the shared `currentVariant`\n // binding. Mirrors the Liquid template structure.\n const computeStepperBounds = () => {\n const already = alreadyInBundleFor(\n handlers.selections,\n ep.product.id,\n currentVariant.id,\n );\n const cap = maxAddableQuantity(currentVariant, rule.max, already);\n // Stepper max can never drop below `rule.min` while the variant is\n // fulfillable — but if `cap` is below `rule.min` (e.g. only 1 unit\n // left and rule.min=2) we surface that by disabling Add and\n // pinning the stepper at rule.min.\n const max = Math.max(rule.min, cap);\n return { min: rule.min, max, cap };\n };\n\n let stepper: ReturnType<typeof renderQtyStepper> | null = null;\n let qtyGroup: HTMLElement | null = null;\n if (!ep.isOos && handlers.showQtySelector) {\n // Wrap stepper in a .lb-bundle-variant-option-group so it\n // inherits the same flex-column layout the variant pickers use.\n // The compound .lb-mix-match__qty-stepper-group class is the\n // qty-stepper hook; the group is appended to the action-row\n // wrapper below (next to the Add button) — not to info — so the\n // stepper sits left of the Add button.\n qtyGroup = el(\n \"div\",\n \"lb-bundle-variant-option-group lb-mix-match__qty-stepper-group\",\n );\n stepper = renderQtyStepper({\n initial: rule.min,\n getBounds: () => {\n const { min, max } = computeStepperBounds();\n return { min, max };\n },\n });\n qtyGroup.appendChild(stepper.el);\n }\n\n const row = {\n el: productEl,\n product: ep.product,\n variant: firstAvailVariant,\n refreshFromSelections: () => {},\n };\n\n // Lifted from the multi-variant branch below so refreshAddState can\n // propagate the row-disabled state into each select. The custom\n // dropdown trigger reads `select.disabled` via its MutationObserver\n // (see bind-dropdown.ts) so it follows automatically.\n const optionSelects: HTMLSelectElement[] = [];\n\n // Per-option dropdowns (Shopify's recommended pattern — one <select>\n // per product option). Values that don't combine with the currently\n // selected values are disabled, so the customer gets clear feedback.\n if (availableVariants.length > 1) {\n const optionNames: string[] = availableVariants[0].selectedOptions.map(\n (o) => o.name,\n );\n const productIdTail = ep.product.id.replace(/^.*\\//, \"\");\n\n const resolveVariant = (values: string[]): ProductVariant | null =>\n availableVariants.find(\n (v) =>\n v.selectedOptions.length === values.length &&\n v.selectedOptions.every((o, i) => o.value === values[i]),\n ) ?? null;\n\n const syncSelectsToVariant = (v: ProductVariant) => {\n v.selectedOptions.forEach((o, i) => {\n const sel = optionSelects[i];\n if (sel && sel.value !== o.value) sel.value = o.value;\n });\n };\n\n const isValueAvailable = (\n optionIndex: number,\n value: string,\n selected: string[],\n ): boolean =>\n availableVariants.some((v) => {\n if (!isVariantFulfillable(v, rule.min)) return false;\n if (v.selectedOptions[optionIndex]?.value !== value) return false;\n return v.selectedOptions.every(\n (o, i) => i === optionIndex || o.value === selected[i],\n );\n });\n\n const recomputeDisabled = (selected: string[]) => {\n optionSelects.forEach((sel, i) => {\n Array.from(sel.options).forEach((opt) => {\n opt.disabled = !isValueAvailable(i, opt.value, selected);\n });\n });\n };\n\n const handleChange = () => {\n const values = optionSelects.map((s) => s.value);\n const next = resolveVariant(values);\n if (!next) {\n // Disabled combo reached via keyboard — revert selects.\n syncSelectsToVariant(currentVariant);\n recomputeDisabled(\n currentVariant.selectedOptions.map((o) => o.value),\n );\n return;\n }\n currentVariant = next;\n row.variant = next;\n price.textContent = formatCents(parseCents(currentVariant.price.amount), currency);\n const nextUnitText = formatUnitPrice(\n currentVariant.unitPrice,\n currentVariant.unitPriceMeasurement,\n currency,\n );\n if (nextUnitText) {\n unitPrice.textContent = nextUnitText;\n unitPrice.hidden = false;\n } else {\n unitPrice.textContent = \"\";\n unitPrice.hidden = true;\n }\n // Swap the row thumbnail to the picked variant's image when it\n // has one.\n const nextImage = currentVariant.image ?? ep.product.featuredImage;\n if (thumbImg && nextImage) {\n thumbImg.src = transformImageUrl(nextImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n thumbImg.alt = nextImage.altText ?? ep.product.title;\n }\n recomputeDisabled(next.selectedOptions.map((o) => o.value));\n refreshLowStockBadge(currentVariant);\n row.refreshFromSelections();\n };\n\n const groupsContainer = el(\"div\", \"lb-bundle-variant-option-groups\");\n optionNames.forEach((name, position) => {\n const group = el(\"div\", \"lb-bundle-variant-option-group\");\n\n const label = el(\"span\", \"lb-bundle-variant-option-label\");\n label.textContent = name;\n group.appendChild(label);\n\n const select = document.createElement(\"select\");\n select.className = \"lb-mix-match__variant-select\";\n select.setAttribute(\"data-variant-option\", \"\");\n select.setAttribute(\"data-option-position\", String(position + 1));\n select.name = `lb-variant-${productIdTail}-${position + 1}`;\n select.setAttribute(\"aria-label\", name);\n\n const seen = new Set<string>();\n availableVariants.forEach((v) => {\n const value = v.selectedOptions[position]?.value;\n if (!value || seen.has(value)) return;\n seen.add(value);\n const opt = document.createElement(\"option\");\n opt.value = value;\n opt.textContent = value;\n if (firstAvailVariant.selectedOptions[position]?.value === value) {\n opt.selected = true;\n }\n select.appendChild(opt);\n });\n\n select.addEventListener(\"change\", handleChange);\n optionSelects.push(select);\n group.appendChild(select);\n groupsContainer.appendChild(group);\n });\n info.appendChild(groupsContainer);\n\n recomputeDisabled(\n firstAvailVariant.selectedOptions.map((o) => o.value),\n );\n } else if (\n availableVariants.length === 1 &&\n firstAvailVariant.title !== \"Default Title\"\n ) {\n const variantLabel = el(\"span\", \"lb-mix-match__filled-variant\");\n variantLabel.textContent = firstAvailVariant.title;\n info.appendChild(variantLabel);\n }\n\n if (ep.isOos) {\n const soldOut = el(\"span\", \"lb-mix-match__modal-sold-out-label\");\n soldOut.textContent = \"Sold out\";\n info.appendChild(soldOut);\n }\n productEl.appendChild(info);\n\n // --- Quantity stepper + Add button ---\n // Stepper rendering happened earlier (right after the low-stock\n // badge, before the variant picker). The action row sits at the\n // bottom of the info column with a CSS margin-top so the modal\n // reads thumb | (title → price → variants → [stepper] [Add]).\n // Mirrors the Liquid template ordering in extensions/bundle-theme/\n // snippets/lb-mix-match.liquid + bundle-mix-match.js.\n let addBtn: HTMLButtonElement | null = null;\n\n const refreshAddState = () => {\n if (!addBtn) {\n // OOS row — addBtn is never built (see `if (!ep.isOos)` below).\n // Lock the picker controls so keyboard users can't operate the\n // variant dropdowns on a sold-out row. Stepper is also never\n // built for OOS (gated by !ep.isOos in qty-stepper branch);\n // the guard is defensive in case that gating changes.\n optionSelects.forEach((sel) => {\n sel.disabled = true;\n });\n if (stepper) stepper.setExternallyDisabled(true);\n return;\n }\n const { cap } = computeStepperBounds();\n const already = alreadyInBundleFor(\n handlers.selections,\n ep.product.id,\n currentVariant.id,\n );\n // Update the in-thumb count badge to show the live aggregate qty.\n if (already > 0) {\n countBadge.textContent = String(already);\n countBadge.hidden = false;\n } else {\n countBadge.hidden = true;\n }\n // The discount counts distinct products, so re-adding the same\n // product across slots wastes them without advancing the bundle's\n // pick count. When this product (any variant) is already in a\n // slot, mark the row \"Added\" and disable Add — product-in-bundle\n // takes priority over the stock-based disable below.\n const productInBundle = handlers.selections.some(\n (s) => s.productId === ep.product.id,\n );\n if (productInBundle) {\n addBtn.disabled = true;\n addBtn.textContent = \"Added\";\n // Per-row accessible name — without this every Add button in\n // the modal announces as just \"Add, button\" and a screen reader\n // user can't tell which product they're about to add.\n addBtn.setAttribute(\"aria-label\", `Added ${ep.product.title}`);\n productEl.classList.add(\"lb-mix-match__modal-product--in-bundle\");\n } else {\n addBtn.textContent = \"Add\";\n addBtn.setAttribute(\"aria-label\", `Add ${ep.product.title}`);\n productEl.classList.remove(\"lb-mix-match__modal-product--in-bundle\");\n // Add disabled when: bundle is already full, or variant can't\n // accept another rule.min units.\n addBtn.disabled = handlers.isComplete() || cap < rule.min;\n }\n // Propagate row-disabled state to the picker controls so keyboard\n // users can't operate dropdowns / stepper buttons on a row whose\n // pick can't land. The dropdown trigger picks up `select.disabled`\n // via its MutationObserver in bind-dropdown.ts.\n const rowDisabled = addBtn.disabled;\n optionSelects.forEach((sel) => {\n sel.disabled = rowDisabled;\n });\n if (stepper) stepper.setExternallyDisabled(rowDisabled);\n };\n\n if (!ep.isOos) {\n const actions = el(\"div\", \"lb-mix-match__modal-product-actions\");\n\n // Stepper sits to the left of the Add button (when the merchant\n // enabled the qty selector). When disabled, the Add button takes\n // the full action-row width via `flex: 1` in CSS.\n if (qtyGroup) actions.appendChild(qtyGroup);\n\n addBtn = document.createElement(\"button\");\n addBtn.type = \"button\";\n addBtn.className = \"lb-mix-match__modal-add\";\n addBtn.textContent = \"Add\";\n addBtn.setAttribute(\"aria-label\", `Add ${ep.product.title}`);\n addBtn.addEventListener(\"click\", () => {\n if (!addBtn || addBtn.disabled) return;\n if (handlers.isComplete()) return;\n // When the stepper is hidden, Add adds qty = rule.min (per spec).\n const qty = stepper ? stepper.value() : rule.min;\n handlers.onAdd(ep.product, currentVariant, qty);\n // Reset stepper to rule.min so the next pick of this product\n // doesn't carry over the previous shopper-chosen qty.\n if (stepper) stepper.reset(rule.min);\n // Override the close-time focus restore so keyboard users land on\n // the next empty slot (or the CTA when the bundle is complete)\n // instead of <body>. See ModalHandlers.getFocusAfterAdd.\n const nextFocus = handlers.getFocusAfterAdd();\n if (nextFocus) lastFocused = nextFocus;\n close();\n });\n actions.appendChild(addBtn);\n info.appendChild(actions);\n }\n\n row.refreshFromSelections = () => {\n if (stepper) stepper.refresh();\n refreshAddState();\n };\n // Initialise the badge + Add disabled state.\n row.refreshFromSelections();\n\n productRows.push(row);\n\n list.appendChild(productEl);\n });\n\n // Replace native <select> popup chrome with our accessible custom dropdown.\n bindAllDropdowns(list);\n\n refreshCounts();\n }\n\n function applySearch() {\n if (!searchInput) return;\n const query = searchInput.value.trim().toLowerCase();\n if (searchClearBtn) {\n searchClearBtn.style.display = query ? \"\" : \"none\";\n }\n let visibleCount = 0;\n productRows.forEach((row) => {\n const match = !query || row.product.title.toLowerCase().includes(query);\n row.el.style.display = match ? \"\" : \"none\";\n if (match) visibleCount++;\n });\n empty.style.display = visibleCount === 0 && query ? \"\" : \"none\";\n }\n\n // Focus trap + keyboard handling\n let lastFocused: Element | null = null;\n function onKeydown(e: KeyboardEvent) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n close();\n return;\n }\n if (e.key === \"Tab\") {\n trapFocus(e, modal);\n }\n }\n\n let isOpen = false;\n\n function open() {\n if (isOpen) return;\n if (handlers.isComplete()) return;\n isOpen = true;\n buildRows();\n lastFocused = (overlay.getRootNode() as Document | ShadowRoot)\n .activeElement;\n overlay.style.display = \"\";\n overlay.classList.add(\"lb-mix-match__modal-overlay--open\");\n modal.focus();\n document.addEventListener(\"keydown\", onKeydown);\n overlay.addEventListener(\"click\", onOverlayClick);\n }\n\n function close() {\n if (!isOpen) return;\n isOpen = false;\n overlay.classList.remove(\"lb-mix-match__modal-overlay--open\");\n overlay.style.display = \"none\";\n document.removeEventListener(\"keydown\", onKeydown);\n overlay.removeEventListener(\"click\", onOverlayClick);\n if (lastFocused instanceof HTMLElement) {\n lastFocused.focus();\n }\n }\n\n function onOverlayClick(e: MouseEvent) {\n if (e.target === overlay) close();\n }\n\n function refreshCounts() {\n productRows.forEach((r) => r.refreshFromSelections());\n }\n\n return { el: overlay, open, close, refreshCounts };\n}\n\n// --- Quantity stepper ---\n\ninterface QtyStepper {\n el: HTMLElement;\n /** Current value (clamped to bounds). */\n value: () => number;\n /** Reset to a known qty (clamped to current bounds). */\n reset: (qty: number) => void;\n /** Re-evaluate bounds — disable / clamp when the cap drops. */\n refresh: () => void;\n /** Override — disable both buttons regardless of bounds. Used when the\n * row's Add is disabled (product already in bundle / out-of-stock) so\n * keyboard users can't operate a stepper whose pick can't land. */\n setExternallyDisabled: (disabled: boolean) => void;\n}\n\nfunction renderQtyStepper(opts: {\n initial: number;\n getBounds: () => { min: number; max: number };\n}): QtyStepper {\n const wrap = el(\"div\", \"lb-mix-match__qty-stepper\", {\n role: \"group\",\n \"aria-label\": \"Quantity\",\n });\n const minus = document.createElement(\"button\");\n minus.type = \"button\";\n minus.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--minus\";\n minus.setAttribute(\"aria-label\", \"Decrease quantity\");\n minus.innerHTML = STEPPER_MINUS_SVG;\n wrap.appendChild(minus);\n\n const valueEl = el(\"span\", \"lb-mix-match__qty-stepper-value\", {\n \"aria-live\": \"polite\",\n });\n wrap.appendChild(valueEl);\n\n const plus = document.createElement(\"button\");\n plus.type = \"button\";\n plus.className =\n \"lb-mix-match__qty-stepper-button lb-mix-match__qty-stepper-button--plus\";\n plus.setAttribute(\"aria-label\", \"Increase quantity\");\n plus.innerHTML = STEPPER_PLUS_SVG;\n wrap.appendChild(plus);\n\n let current = clamp(opts.initial, opts.getBounds());\n let externallyDisabled = false;\n\n function clamp(n: number, b: { min: number; max: number }): number {\n return Math.max(b.min, Math.min(b.max, n));\n }\n\n function paint() {\n const b = opts.getBounds();\n current = clamp(current, b);\n valueEl.textContent = String(current);\n minus.disabled = externallyDisabled || current <= b.min;\n plus.disabled = externallyDisabled || current >= b.max;\n }\n\n minus.addEventListener(\"click\", () => {\n const b = opts.getBounds();\n current = clamp(current - 1, b);\n paint();\n });\n plus.addEventListener(\"click\", () => {\n const b = opts.getBounds();\n current = clamp(current + 1, b);\n paint();\n });\n\n paint();\n\n return {\n el: wrap,\n value: () => current,\n reset(qty) {\n current = clamp(qty, opts.getBounds());\n paint();\n },\n refresh: paint,\n setExternallyDisabled(disabled) {\n externallyDisabled = disabled;\n paint();\n },\n };\n}\n\n// --- Helpers ---\n\nfunction buildEligibleProducts(\n bundle: MixMatchBundleData,\n oosBehavior: \"show_greyed_out\" | \"hide\",\n): EligibleProduct[] {\n const result: EligibleProduct[] = [];\n const seen = new Set<string>();\n for (const product of bundle.products) {\n if (seen.has(product.id)) continue;\n seen.add(product.id);\n const rule = ruleFor(bundle, product.id);\n // A variant is \"available for this bundle\" when it can fulfil at least\n // `rule.min` units. Stricter rules raise the bar (e.g. min=3 + only\n // 2 in stock = sold out for this bundle's purposes).\n const available = product.variants.nodes.filter((v) =>\n isVariantFulfillable(v, rule.min),\n );\n const isOos = available.length === 0;\n if (isOos && oosBehavior === \"hide\") continue;\n result.push({\n product,\n variants: product.variants.nodes,\n firstAvailableVariant: available[0] ?? null,\n isOos,\n });\n }\n return result;\n}\n\nfunction trapFocus(e: KeyboardEvent, container: HTMLElement) {\n const focusables = container.querySelectorAll<HTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n );\n if (focusables.length === 0) return;\n const first = focusables[0];\n const last = focusables[focusables.length - 1];\n const active = (container.getRootNode() as Document | ShadowRoot)\n .activeElement;\n if (e.shiftKey && active === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && active === last) {\n e.preventDefault();\n first.focus();\n }\n}\n\nfunction sanitizeId(gid: string): string {\n return gid.replace(/[^a-zA-Z0-9_-]/g, \"-\");\n}\n","/**\n * DOM renderer for volume bundles.\n *\n * Mirrors `extensions/bundle-theme/snippets/lb-volume.liquid`. Each tier is a\n * radio-styled card; clicking one updates the pricing row and recalculates\n * the total. Add bundle dispatches the active tier's quantity for the first\n * available variant.\n */\nimport type {\n CartLineInput,\n DiscountConfig,\n VolumeBundleData,\n VolumeTier,\n} from \"@lime-bundles/core\";\nimport { isVariantFulfillable, shouldShowLowStockBadge } from \"@lime-bundles/core\";\nimport { formatCents, parseCents } from \"./pricing\";\nimport { renderCountdown } from \"./countdown\";\nimport { buildCtaButton } from \"./cta-button\";\nimport { el } from \"./dom\";\n\ninterface ResolvedTier {\n tier: VolumeTier;\n index: number;\n qty: number;\n /** Per-unit price after applying this tier's discount, in cents. */\n pricePerUnitCents: number;\n /** Pre-discount per-unit baseline in cents. */\n basePricePerUnitCents: number;\n}\n\nexport function renderVolumeBundle(\n container: HTMLElement,\n bundle: VolumeBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n onCleanup?: (fn: () => void) => void,\n) {\n const wc = bundle.widgetConfig;\n const product = bundle.products[0];\n // For volume bundles, the natural per-bundle floor is the smallest\n // tier's minQuantity — anything below that can't even buy the cheapest\n // tier. Variants that satisfy that floor are \"fulfillable\" for visibility\n // and CTA-enable purposes.\n const minTierQty = bundle.volumeTiers[0]?.minQuantity ?? 1;\n const variant = product?.variants.nodes.find((v) =>\n isVariantFulfillable(v, minTierQty),\n );\n\n // Bundle visibility guard: in \"hide\" mode, don't render the widget at\n // all if the product has no available variants. Matches Liquid behaviour.\n if (!variant && wc.outOfStockBehavior === \"hide\") return;\n\n const basePriceCents = variant ? parseCents(variant.price.amount) : 0;\n const currency = variant?.price.currencyCode ?? \"USD\";\n\n // Discount shape: bundle.discountConfig.discountType selects which field\n // on each tier carries the magnitude. \"percentage\" → tier.percentage (a\n // whole-number e.g. 10); \"fixed_amount\" → tier.amount (currency units\n // e.g. 5.00). Missing fields fall through to zero (tier renders at base\n // price — merchant config error, not a crash path).\n const discountType = bundle.discountConfig.discountType;\n\n const resolved = bundle.volumeTiers.map<ResolvedTier>((tier, index) => {\n let perUnit: number;\n if (discountType === \"fixed_amount\") {\n const amt = Math.round((tier.amount ?? 0) * 100);\n perUnit = Math.max(0, basePriceCents - amt);\n } else {\n const pct = tier.percentage ?? 0;\n const discount = Math.floor((basePriceCents * pct) / 100);\n perUnit = Math.max(0, basePriceCents - discount);\n }\n return {\n tier,\n index,\n qty: tier.minQuantity,\n pricePerUnitCents: perUnit,\n basePricePerUnitCents: basePriceCents,\n };\n });\n\n const bestTierIndex = pickBestTierIndex(resolved);\n let selectedIndex = wc.defaultTier === \"best_value\" ? bestTierIndex : 0;\n if (typeof wc.defaultTier === \"number\") {\n selectedIndex = clamp(wc.defaultTier, 0, resolved.length - 1);\n }\n\n const popularIndex =\n wc.popularBadge.tierIndex !== undefined\n ? clamp(wc.popularBadge.tierIndex, 0, resolved.length - 1)\n : bestTierIndex;\n\n const root = el(\"div\", \"lb-volume\");\n root.appendChild(\n renderHeader(bundle, resolved, selectedIndex, currency, discountType),\n );\n\n if (wc.countdown.showCountdown && bundle.endsAt) {\n const countdown = renderCountdown(bundle.endsAt);\n if (countdown) {\n root.appendChild(countdown.el);\n onCleanup?.(countdown.stop);\n }\n }\n\n const tierGroup = el(\"div\", \"lb-volume__tiers\", {\n role: \"radiogroup\",\n \"aria-label\": \"Quantity tiers\",\n \"data-tier-group\": \"\",\n });\n\n resolved.forEach((r) => {\n const tierEl = renderTierCard(\n r,\n r.index === selectedIndex,\n currency,\n wc.popularBadge.visible && r.index === popularIndex\n ? wc.popularBadge.text\n : null,\n wc.pricing.showComparePrice,\n wc.pricing.showPerUnitPrice,\n );\n tierEl.addEventListener(\"click\", () => selectTier(r.index));\n // Radiogroup keyboard contract: arrow keys move focus + selection\n // between siblings; Space/Enter activates the focused tier.\n tierEl.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n selectTier(r.index);\n return;\n }\n if (\n e.key === \"ArrowDown\" ||\n e.key === \"ArrowRight\" ||\n e.key === \"ArrowUp\" ||\n e.key === \"ArrowLeft\"\n ) {\n e.preventDefault();\n const delta =\n e.key === \"ArrowDown\" || e.key === \"ArrowRight\" ? 1 : -1;\n const next = (r.index + delta + resolved.length) % resolved.length;\n selectTier(next);\n const target = tierGroup.children[next] as HTMLElement | undefined;\n target?.focus();\n }\n });\n tierGroup.appendChild(tierEl);\n });\n\n root.appendChild(tierGroup);\n root.appendChild(el(\"div\", \"lb-bundle-divider\"));\n\n let pricingEl = renderPricingRow(\n resolved,\n selectedIndex,\n currency,\n wc.pricing.showItemCount,\n wc.pricing.showCompareAtPrice,\n );\n root.appendChild(pricingEl);\n\n let savingsBarEl: HTMLElement | null = wc.savingsBar.visible\n ? renderSavingsBar(resolved, selectedIndex, currency)\n : null;\n if (savingsBarEl) root.appendChild(savingsBarEl);\n\n const cta = renderCta(bundle, () => {\n if (!variant) return;\n const r = resolved[selectedIndex];\n if (!r) return;\n onAddToCart([\n {\n merchandiseId: variant.id,\n quantity: r.qty,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n },\n ]);\n });\n // Low-stock badge — shown when the variant has stock at or below the\n // merchant-configured threshold. Hidden when stock is unknown\n // (Storefront token without read_product_inventory) or above threshold.\n if (\n variant &&\n shouldShowLowStockBadge(\n variant,\n minTierQty,\n wc.lowStockThreshold,\n wc.showLowStockBadge,\n )\n ) {\n const lowStock = el(\"span\", \"lb-bundle-low-stock-badge\", {\n \"data-low-stock-badge\": \"\",\n });\n lowStock.textContent = `Only ${variant.quantityAvailable} left`;\n root.appendChild(lowStock);\n }\n root.appendChild(cta);\n\n root.appendChild(\n el(\"p\", \"lb-bundle-error\", { \"data-error\": \"\", \"aria-live\": \"polite\" }),\n );\n root.appendChild(\n el(\"span\", \"lb-visually-hidden\", {\n \"data-status\": \"\",\n \"aria-live\": \"polite\",\n }),\n );\n\n container.appendChild(root);\n\n function selectTier(idx: number) {\n if (idx === selectedIndex || idx < 0 || idx >= resolved.length) return;\n selectedIndex = idx;\n Array.from(tierGroup.children).forEach((card, i) => {\n card.setAttribute(\"aria-checked\", String(i === idx));\n (card as HTMLElement).tabIndex = i === idx ? 0 : -1;\n });\n const newPricing = renderPricingRow(\n resolved,\n idx,\n currency,\n wc.pricing.showItemCount,\n wc.pricing.showCompareAtPrice,\n );\n pricingEl.replaceWith(newPricing);\n pricingEl = newPricing;\n if (savingsBarEl) {\n const newBar = renderSavingsBar(resolved, idx, currency);\n savingsBarEl.replaceWith(newBar);\n savingsBarEl = newBar;\n }\n // Keep the header save-badge text in sync with the selected tier.\n // Hidden when showSaveBadge is off (the element doesn't exist), or\n // when the tier has no discount (badgeFor returns null).\n const badgeEl = root.querySelector<HTMLElement>(\"[data-header-badge]\");\n if (badgeEl) {\n const label = badgeFor(resolved[idx], currency, discountType);\n if (label) {\n badgeEl.textContent = label;\n badgeEl.style.display = \"\";\n } else {\n badgeEl.style.display = \"none\";\n }\n }\n }\n}\n\n// --- Section renderers ---\n\nfunction renderHeader(\n bundle: VolumeBundleData,\n resolved: ResolvedTier[],\n selectedIndex: number,\n currency: string,\n discountType: DiscountConfig[\"discountType\"],\n): HTMLElement {\n const wc = bundle.widgetConfig;\n const header = el(\"div\", \"lb-bundle-header\");\n const content = el(\"div\", \"lb-bundle-header__content\");\n const title = el(\"h3\", \"lb-bundle-title\");\n title.textContent = bundle.title;\n content.appendChild(title);\n header.appendChild(content);\n\n if (wc.pricing.showSaveBadge) {\n const badge = badgeFor(resolved[selectedIndex], currency, discountType);\n if (badge) {\n const badgeEl = el(\"span\", \"lb-bundle-header__badge\", {\n \"data-header-badge\": \"\",\n });\n badgeEl.textContent = badge;\n header.appendChild(badgeEl);\n }\n }\n return header;\n}\n\nfunction renderTierCard(\n r: ResolvedTier,\n isSelected: boolean,\n currency: string,\n popularLabel: string | null,\n showComparePrice: boolean,\n showPerUnitPrice: boolean,\n): HTMLElement {\n const tier = el(\"div\", \"lb-volume__tier\", {\n role: \"radio\",\n \"aria-checked\": String(isSelected),\n tabindex: isSelected ? \"0\" : \"-1\",\n \"data-tier-index\": String(r.index),\n \"data-tier-qty\": String(r.qty),\n });\n\n const radio = el(\"span\", \"lb-volume__radio\");\n radio.appendChild(el(\"span\", \"lb-volume__radio-dot\"));\n tier.appendChild(radio);\n\n const grid = el(\"span\", \"lb-volume__tier-grid\");\n const label = el(\"span\", \"lb-volume__tier-label\");\n label.textContent = `Buy ${r.qty}`;\n grid.appendChild(label);\n\n const price = el(\"span\", \"lb-volume__tier-price\");\n if (showComparePrice && r.pricePerUnitCents < r.basePricePerUnitCents) {\n const compare = el(\"span\", \"lb-volume__tier-compare\");\n compare.textContent = formatCents(r.basePricePerUnitCents, currency);\n price.appendChild(compare);\n }\n if (showPerUnitPrice) {\n const each = document.createElement(\"span\");\n each.setAttribute(\"data-tier-price-each\", \"\");\n each.textContent = formatCents(r.pricePerUnitCents, currency);\n price.appendChild(each);\n const unit = el(\"span\", \"lb-volume__tier-unit\");\n unit.textContent = \" each\";\n price.appendChild(unit);\n }\n grid.appendChild(price);\n tier.appendChild(grid);\n\n const badge = el(\"span\", \"lb-volume__tier-badge\");\n if (popularLabel) {\n badge.textContent = popularLabel;\n } else {\n badge.style.display = \"none\";\n }\n tier.appendChild(badge);\n\n return tier;\n}\n\nfunction renderPricingRow(\n resolved: ResolvedTier[],\n selectedIndex: number,\n currency: string,\n showItemCount: boolean,\n showCompareAtPrice: boolean,\n): HTMLElement {\n const r = resolved[selectedIndex];\n const totalCents = r ? r.pricePerUnitCents * r.qty : 0;\n const undiscountedCents = r ? r.basePricePerUnitCents * r.qty : 0;\n const savings = Math.max(0, undiscountedCents - totalCents);\n\n const row = el(\"div\", \"lb-bundle-pricing\");\n const label = el(\"span\", \"lb-bundle-pricing__label\", {\n \"data-total-label\": \"\",\n });\n label.textContent = \"Total\";\n if (showItemCount && r) {\n const count = document.createElement(\"span\");\n count.setAttribute(\"data-item-count\", \"\");\n count.textContent = ` (${r.qty} item${r.qty === 1 ? \"\" : \"s\"})`;\n label.appendChild(count);\n }\n row.appendChild(label);\n\n const prices = el(\"span\", \"lb-bundle-pricing__prices\");\n if (showCompareAtPrice && savings > 0) {\n const compare = el(\"span\", \"lb-bundle-compare-price\", {\n \"data-compare-price\": \"\",\n });\n compare.textContent = formatCents(undiscountedCents, currency);\n prices.appendChild(compare);\n }\n const sale = el(\"span\", \"lb-bundle-sale-price\", { \"data-total-price\": \"\" });\n sale.textContent = formatCents(totalCents, currency);\n prices.appendChild(sale);\n row.appendChild(prices);\n return row;\n}\n\nfunction renderSavingsBar(\n resolved: ResolvedTier[],\n selectedIndex: number,\n currency: string,\n): HTMLElement {\n const r = resolved[selectedIndex];\n const totalCents = r ? r.pricePerUnitCents * r.qty : 0;\n const undiscountedCents = r ? r.basePricePerUnitCents * r.qty : 0;\n const savings = Math.max(0, undiscountedCents - totalCents);\n\n const bar = el(\"div\", \"lb-bundle-savings-bar\", { \"data-savings-bar\": \"\" });\n if (savings <= 0) bar.style.display = \"none\";\n const labelEl = document.createElement(\"span\");\n labelEl.textContent = \"You save\";\n bar.appendChild(labelEl);\n const amount = el(\"span\", \"\", { \"data-savings-amount\": \"\" });\n amount.textContent = formatCents(savings, currency);\n bar.appendChild(amount);\n return bar;\n}\n\nfunction renderCta(\n bundle: VolumeBundleData,\n onClick: () => void,\n): HTMLElement {\n const product = bundle.products[0];\n const minTierQty = bundle.volumeTiers[0]?.minQuantity ?? 1;\n const isAvailable = product?.variants.nodes.some((v) =>\n isVariantFulfillable(v, minTierQty),\n );\n const label = isAvailable\n ? bundle.widgetConfig.cta.ctaText || \"Add to cart\"\n : \"Sold out\";\n const button = buildCtaButton(label);\n if (!isAvailable) button.disabled = true;\n button.addEventListener(\"click\", () => {\n if (button.disabled) return;\n onClick();\n });\n return button;\n}\n\n// --- Helpers ---\n\nfunction badgeFor(\n resolved: ResolvedTier | undefined,\n currency: string,\n discountType: DiscountConfig[\"discountType\"],\n): string | null {\n if (!resolved) return null;\n const { tier } = resolved;\n if (discountType === \"fixed_amount\") {\n const amount = tier.amount ?? 0;\n if (amount > 0) return `-${formatCents(Math.round(amount * 100), currency)}`;\n return null;\n }\n if (discountType === \"percentage\") {\n const pct = tier.percentage ?? 0;\n if (pct > 0) return `-${Math.round(pct)}%`;\n return null;\n }\n return null;\n}\n\nfunction pickBestTierIndex(resolved: ResolvedTier[]): number {\n let bestSavings = 0;\n let bestIndex = 0;\n resolved.forEach((r, i) => {\n const savings = r.basePricePerUnitCents - r.pricePerUnitCents;\n if (savings > bestSavings) {\n bestSavings = savings;\n bestIndex = i;\n }\n });\n return bestIndex;\n}\n\nfunction clamp(n: number, min: number, max: number): number {\n return Math.max(min, Math.min(max, n));\n}\n\n","/**\n * Input-mode tracker — toggles `using-mouse` / `using-keyboard` classes on a\n * target element so CSS can scope focus styles by the customer's current\n * input device. Default is mouse; the first keyboard-navigation keypress\n * (Tab, arrow keys, Enter, Space, Escape, Home/End/PageUp/PageDown) flips\n * to keyboard mode, and the next pointer click flips back.\n *\n * Multiple targets share one pair of document-level listeners — installed\n * on the first `trackInputMode` call, removed when the last target is\n * released. Safe to call across every widget instance on a page without\n * stacking listeners.\n *\n * CSS shape (see bundle-base.css):\n * .using-mouse .lb-bundle-widget :focus { outline: none; }\n *\n * The Liquid theme mirrors this behaviour from `bundle-widget.js` against\n * `document.documentElement` so classic and headless storefronts render the\n * same focus rings.\n */\n\nconst NAV_KEYS = new Set([\n \"Tab\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Home\",\n \"End\",\n \"PageUp\",\n \"PageDown\",\n \"Enter\",\n \" \",\n \"Escape\",\n]);\n\nconst targets = new Set<HTMLElement>();\nlet listenersAttached = false;\n\nfunction setAll(on: \"using-mouse\" | \"using-keyboard\"): void {\n const off = on === \"using-mouse\" ? \"using-keyboard\" : \"using-mouse\";\n for (const el of targets) {\n el.classList.add(on);\n el.classList.remove(off);\n }\n}\n\nfunction onKeyDown(e: KeyboardEvent): void {\n if (NAV_KEYS.has(e.key)) setAll(\"using-keyboard\");\n}\n\nfunction onPointerDown(): void {\n setAll(\"using-mouse\");\n}\n\nfunction attachListeners(): void {\n if (listenersAttached) return;\n listenersAttached = true;\n document.addEventListener(\"keydown\", onKeyDown, true);\n document.addEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nfunction detachListeners(): void {\n if (!listenersAttached) return;\n listenersAttached = false;\n document.removeEventListener(\"keydown\", onKeyDown, true);\n document.removeEventListener(\"pointerdown\", onPointerDown, true);\n}\n\nexport function trackInputMode(target: HTMLElement): () => void {\n target.classList.add(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n targets.add(target);\n attachListeners();\n\n return () => {\n targets.delete(target);\n target.classList.remove(\"using-mouse\");\n target.classList.remove(\"using-keyboard\");\n if (targets.size === 0) detachListeners();\n };\n}\n","/**\n * Base + type-specific widget CSS, inlined as template literals so the web\n * component can dump them into its shadow root. Source of truth for these\n * rules is `extensions/bundle-theme/assets/*.css` — the classic Shopify\n * theme app block reads the same files. Keep the two in lockstep; the\n * bundle-css-parity.test.ts golden test enforces byte equality.\n *\n * `BUNDLE_SKELETON_CSS` (at the bottom of this file) is intentionally\n * web-component-only. The theme app block never renders a loading\n * state — its Liquid render is synchronous on the server — so the\n * skeleton styles would be dead rules there. Excluding from parity.\n */\nexport const BUNDLE_BASE_CSS = `/* Lime Bundles — shared base styles for all bundle widget types */\n\n.lb-bundle-widget.lb-bundle-widget,\n.lb-bundle-widget.lb-bundle-widget * {\n line-height: normal;\n}\n\n.lb-bundle-widget {\n /* Internal CSS-only vars (not merchant-configurable). */\n --lb-thumbnail-bg: #F0F0F0;\n --lb-widget-pad: 20px;\n --lb-progress-color: var(--lb-primary-color);\n /* Cap on the per-bundle product/slot/tier list height — keeps long\n bundles from pushing the CTA off-screen. The list scrolls\n internally with the same custom 4px scrollbar as the variant\n dropdown when content exceeds this. */\n --lb-list-max-height: 360px;\n\n font-family: inherit;\n font-size: 16px;\n background: var(--lb-bg);\n border: var(--lb-border-width) solid var(--lb-border);\n border-radius: var(--lb-radius);\n padding: var(--lb-widget-pad) var(--lb-widget-pad) 20px;\n box-sizing: border-box;\n /* Cap the widget at a comfortable reading width on desktop. Below\n 440px viewports the container is already narrower than the cap,\n so the rule is inert on mobile. */\n max-width: 440px;\n}\n\n/* Countdown timer bar — sits below the gradient header */\n.lb-bundle-countdown {\n margin: 0 calc(-1 * var(--lb-widget-pad)) 20px;\n padding: 12px 20px;\n background: var(--lb-countdown-bg);\n border-top: 1px solid color-mix(in srgb, var(--lb-text) 6%, transparent);\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.lb-bundle-countdown__label {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.lb-bundle-countdown__label svg {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__label span {\n font-size: 12px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-countdown-text);\n}\n\n.lb-bundle-countdown__timer {\n font-family: 'SF Mono', 'Roboto Mono', ui-monospace, monospace;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n color: var(--lb-countdown-text);\n letter-spacing: 0.02em;\n}\n\n/* Hide wrapper when the inner snippet rendered nothing (product OOS / unfulfillable) */\n.lb-bundle-widget:not(:has(.lb-fixed, .lb-mix-match, .lb-volume)) {\n display: none;\n}\n\n/* Sibling widget spacing — separates multiple bundles on the same product page.\n Uses \\`~\\` (general sibling) rather than \\`+\\` (adjacent) because each Liquid\n loop iteration emits a {% style %} block before its widget div, so the\n rendered DOM alternates <style><widget><style><widget>. The \\`+\\` combinator\n requires immediate adjacency and would match nothing; \\`~\\` matches every\n widget after the first regardless of elements between. Single-widget pages\n stay unaffected (no prior \\`.lb-bundle-widget\\` sibling to match against).\n Only \\`margin-top\\` — do NOT override \\`padding-top\\` here. The gradient header\n uses \\`margin-top: calc(-1 * var(--lb-widget-pad))\\` to reach the widget's\n inner border edge, assuming padding-top == --lb-widget-pad. Changing\n padding-top on the subsequent widget breaks that math and leaves a visible\n gap above the header. */\n.lb-bundle-widget ~ .lb-bundle-widget {\n margin-top: 24px;\n}\n\n/* Header — gradient banner with title + savings badge */\n.lb-bundle-header {\n margin: calc(-1 * var(--lb-widget-pad)) calc(-1 * var(--lb-widget-pad)) 20px;\n padding: 20px 20px;\n background: var(--lb-header-bg);\n /* Match the widget's inner border curve so there's no background gap at the top corners. */\n border-radius: max(0px, calc(var(--lb-radius) - var(--lb-border-width))) max(0px, calc(var(--lb-radius) - var(--lb-border-width))) 0 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n}\n\n/* When countdown follows header, remove header bottom margin */\n.lb-bundle-header:has(+ .lb-bundle-countdown) {\n margin-bottom: 0;\n}\n\n.lb-bundle-header__content {\n flex: 1;\n min-width: 0;\n}\n\n.lb-bundle-title {\n font-size: 20px;\n font-weight: 700;\n line-height: 28px;\n letter-spacing: -0.02em;\n color: var(--lb-text);\n margin: 0;\n}\n\n.lb-bundle-header .lb-bundle-title {\n color: var(--lb-header-text);\n}\n\n.lb-bundle-subtitle {\n font-size: 16px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 4px 0 0;\n}\n\n.lb-bundle-header .lb-bundle-subtitle {\n color: var(--lb-header-text);\n opacity: 0.85;\n margin-top: 8px;\n}\n\n.lb-bundle-header:has(.lb-bundle-subtitle) {\n align-items: flex-start;\n}\n\n.lb-bundle-header__badge {\n background: var(--lb-save-badge-bg);\n color: var(--lb-save-badge-text);\n border: var(--lb-save-badge-border-width) solid var(--lb-save-badge-border-color);\n font-size: 16px;\n font-weight: 700;\n line-height: 1;\n padding: 4px 12px;\n border-radius: var(--lb-save-badge-radius);\n white-space: nowrap;\n flex-shrink: 0;\n}\n\n/* Override Dawn's \\`div:empty { display: none }\\` reset for decorative elements */\n.lb-bundle-divider:empty,\n.lb-mix-match__progress-fill:empty {\n display: block;\n}\n\n/* Divider */\n.lb-bundle-divider {\n height: 1px;\n background: color-mix(in srgb, var(--lb-text) 7%, transparent);\n margin: 16px 0;\n}\n\n/* Product rows */\n.lb-bundle-product-row {\n display: flex;\n gap: 20px;\n padding: 12px 0;\n}\n\n.lb-bundle-thumbnail {\n position: relative;\n width: 48px;\n min-width: 48px;\n /* Aspect-ratio comes from the merchant \\`thumbnailRatio\\` enum via Liquid;\n \"original\" sets it to \\`auto\\` so the box sizes to the image's intrinsic\n ratio. Default keeps the historical 1:1 behaviour. */\n aspect-ratio: var(--lb-thumbnail-aspect-ratio, 1 / 1);\n background: var(--lb-thumbnail-bg);\n border-radius: 8px;\n overflow: hidden;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-bundle-thumbnail img {\n width: 100%;\n /* Height + fit come from the same merchant enum; \"original\" sets them to\n \\`auto\\` / \\`contain\\` so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-thumbnail-img-height, 100%);\n object-fit: var(--lb-thumbnail-img-fit, cover);\n}\n\n.lb-bundle-thumbnail svg {\n width: 28px;\n height: 28px;\n color: color-mix(in srgb, var(--lb-text) 35%, transparent);\n}\n\n.lb-bundle-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-bundle-product-name {\n font-size: 16px;\n font-weight: 700;\n line-height: 20px;\n color: var(--lb-text);\n margin: 0;\n text-decoration: none;\n display: block;\n}\n\n.lb-bundle-product-name:hover {\n text-decoration: underline;\n}\n\na.lb-bundle-product-name:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n border-radius: 2px;\n}\n\n.lb-bundle-product-price {\n /* --lb-product-price-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-price-display, block);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: var(--lb-text);\n}\n\n.lb-bundle-product-prices {\n display: flex;\n align-items: baseline;\n gap: 6px;\n flex-wrap: wrap;\n margin-top: 2px;\n}\n\n.lb-bundle-product-compare-price {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 14px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n}\n\n/* Setting \\`display\\` above outranks the UA [hidden] rule — restore it so rows\n without a compare-at price don't leave a phantom flex item + gap. */\n.lb-bundle-product-compare-price[hidden] {\n display: none;\n}\n\n/* Unit price (e.g. \"$0.50/100ml\") — only rendered when the merchant has\n configured unit pricing on the variant in the Shopify admin. No merchant\n toggle: present in admin → shown; absent → hidden. Styled as muted\n secondary text beneath the price row so it doesn't compete visually. */\n.lb-bundle-product-unit-price {\n display: block;\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-top: 2px;\n}\n\n.lb-bundle-product-unit-price[hidden] {\n display: none;\n}\n\n/* Read-only variant text shown below the product title when only one\n variant is in scope (single-variant product, or merchant pinned a\n single variant). Same visual treatment as the mix-match filled\n slot's variant text — see .lb-mix-match__filled-variant in\n bundle-mix-match.css. Both share this rule via the comma selector\n so the storefront UX stays consistent across bundle types. */\n.lb-bundle-variant-badge,\n.lb-mix-match__slot--filled .lb-mix-match__filled-variant {\n display: block;\n font-size: 12px;\n line-height: 20px;\n margin-top: 2px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n/* Per-option variant pickers. Each option's label + select sit inside a\n .lb-bundle-variant-option-group (flex column, 2px gap between label and\n select); the groups stack inside a .lb-bundle-variant-option-groups\n parent (flex column, 12px gap between groups). The parent owns the top\n offset from the preceding unit-price line, so individual labels and\n selects don't carry their own vertical margins. */\n.lb-bundle-variant-option-groups {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.lb-bundle-variant-option-group {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.lb-bundle-variant-option-label {\n display: block;\n margin: 0;\n font-size: 12px;\n line-height: 16px;\n font-weight: 600;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n margin-top: 8px;\n}\n\n/* Desktop: lay variant option groups in a wrapping row inside the modal\n (Size + Color side-by-side). Mobile keeps the column stack from the\n base rule above. The 768px breakpoint mirrors bundle-mix-match.css. */\n@media (min-width: 768px) {\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups {\n flex-direction: row;\n flex-wrap: wrap;\n }\n /* Direct children only — the qty-stepper-group is also a\n .lb-bundle-variant-option-group but lives in the actions wrapper\n and must keep its natural width. */\n .lb-mix-match__modal-product-info .lb-bundle-variant-option-groups > .lb-bundle-variant-option-group {\n flex: 1;\n }\n}\n\n/* Focus-ring suppression for mouse users. A small JS helper — see\n packages/widget/src/utils/input-mode.ts and bundle-widget.js — toggles\n .using-mouse / .using-keyboard on the widget root (or html in the Liquid\n path) based on the customer's current input device. Default is mouse, so\n click-to-focus doesn't leave a keyboard-style ring. The modal overlay\n gets its own selector because the Liquid path reparents it to body,\n outside the widget root. */\n.using-mouse .lb-bundle-widget :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-bundle-widget :focus-visible:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus:not([aria-checked=\"true\"]),\n.using-mouse .lb-mix-match__modal-overlay :focus-visible:not([aria-checked=\"true\"]) {\n outline: none;\n outline-offset: 0;\n box-shadow: none;\n}\n\n.lb-bundle-quantity {\n font-size: 12px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin-left: 8px;\n white-space: nowrap;\n}\n\n/* Pricing row — label left, prices right */\n.lb-bundle-pricing {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n padding: 4px 0 8px;\n gap: 12px;\n}\n\n.lb-bundle-pricing__label {\n font-size: 16px;\n font-weight: 400;\n line-height: 20px;\n color: var(--lb-text);\n white-space: nowrap;\n}\n\n.lb-bundle-pricing__prices {\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.lb-bundle-sale-price {\n font-size: 20px;\n font-weight: 700;\n line-height: 1;\n letter-spacing: -0.02em;\n color: var(--lb-text);\n}\n\n.lb-bundle-compare-price {\n font-size: 16px;\n font-weight: 400;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n}\n\n/* Savings bar — green banner below pricing */\n.lb-bundle-savings-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--lb-savings-bar-bg);\n color: var(--lb-savings-bar-text);\n border: var(--lb-savings-bar-border-width) solid var(--lb-savings-bar-border-color);\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n padding: 8px 12px;\n border-radius: var(--lb-savings-bar-radius);\n margin-bottom: 12px;\n}\n\n/* Quantity badge — overlay on thumbnail top-right */\n.lb-bundle-qty-badge.lb-bundle-qty-badge {\n position: absolute;\n top: -8px;\n right: -8px;\n /* --lb-qty-badge-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-qty-badge-display, flex);\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--lb-qty-badge-bg);\n border: var(--lb-image-border-width) solid var(--lb-image-border-color);\n color: var(--lb-qty-badge-color);\n font-size: 12px;\n font-weight: 700;\n line-height: 0;\n text-align: center;\n z-index: 1;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);\n}\n\n/* Override Dawn's \\`div:empty\\` for savings bar when hidden */\n.lb-bundle-savings-bar:empty {\n display: none;\n}\n\n/* CTA button.\n * Label and spinner share a single 1×1 grid cell so the button's intrinsic\n * width/height stays fixed when swapping between them — no layout shift when\n * entering the loading state. Visibility (not display) is used so the hidden\n * child still contributes to the cell's min-content sizing. See the\n * [data-loading=\"true\"] rules below. */\n.lb-bundle-cta {\n display: grid;\n grid-template-rows: 1fr;\n grid-template-columns: 1fr;\n width: 100%;\n padding: 12px 16px;\n border: var(--lb-cta-border-width) solid var(--lb-cta-border-color);\n border-radius: var(--lb-cta-radius);\n font-size: 16px;\n font-weight: 600;\n line-height: 20px;\n cursor: pointer;\n text-align: center;\n transition: opacity 0.15s ease;\n font-family: inherit;\n}\n\n.lb-bundle-cta:not(:disabled) {\n background: var(--lb-primary-color);\n color: var(--lb-btn-text);\n}\n\n.lb-bundle-cta:not(:disabled):hover {\n opacity: 0.9;\n}\n\n.lb-bundle-cta:disabled {\n background: color-mix(in srgb, var(--lb-primary-color) 35%, var(--lb-bg));\n color: color-mix(in srgb, var(--lb-btn-text) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Both the label and the spinner occupy grid cell (1, 1). Only one is\n * visible at a time; the other keeps its box for sizing but is invisible. */\n.lb-cta-label,\n.lb-cta-spinner {\n grid-row: 1;\n grid-column: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n}\n\n.lb-cta-spinner {\n visibility: hidden;\n}\n\n.lb-cta-spinner svg {\n width: 20px;\n height: 20px;\n animation: lb-cta-spin 0.8s linear infinite;\n}\n\n@keyframes lb-cta-spin {\n to { transform: rotate(360deg); }\n}\n\n.lb-bundle-cta[data-loading=\"true\"] {\n cursor: wait;\n pointer-events: none;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-label {\n visibility: hidden;\n}\n\n.lb-bundle-cta[data-loading=\"true\"] .lb-cta-spinner {\n visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-cta-spinner svg {\n animation-duration: 2.5s;\n }\n}\n\n/* Error message */\n.lb-bundle-error {\n font-size: 16px;\n color: #D72C0D;\n margin-top: 8px;\n display: none;\n}\n\n.lb-bundle-error[data-visible=\"true\"] {\n display: block;\n}\n\n/* Visually hidden — accessible to screen readers only */\n.lb-visually-hidden {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip-path: inset(50%) !important;\n white-space: nowrap !important;\n border: 0 !important;\n}\n\n/* Placeholder SVG icon for missing images */\n.lb-bundle-placeholder-icon {\n width: 28px;\n height: 28px;\n stroke: color-mix(in srgb, var(--lb-text) 35%, transparent);\n stroke-width: 1.5;\n fill: none;\n}\n\n/* Out-of-stock product row */\n.lb-bundle-product-row--oos {\n opacity: 0.5;\n}\n\n.lb-bundle-oos-label {\n font-size: 12px;\n font-weight: 500;\n color: #D72C0D;\n white-space: nowrap;\n margin-left: auto;\n}\n\n/* \"Only X left\" low-stock badge — appears alongside product/variant\n info when ProductVariant.quantityAvailable falls at or below\n wc.lowStockThreshold. Hidden when quantityAvailable is unknown\n (Storefront token without read_product_inventory). */\n.lb-bundle-low-stock-badge {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px;\n font-size: 11px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--lb-low-stock-text);\n background-color: var(--lb-low-stock-bg);\n border-radius: 4px;\n white-space: nowrap;\n}\n\n/* A/B test: hide save badge until JS swaps the label (prevents flash of default) */\n.lb-ab-pending {\n visibility: hidden;\n}\n`;\nexport const BUNDLE_FIXED_CSS = `/* Lime Bundles — Fixed bundle styles */\n\n.lb-fixed__products {\n display: flex;\n flex-direction: column;\n gap: 0;\n margin: 0;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Custom scrollbar — text colour at 15% opacity (thumb) and 5% (track). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-fixed__products::-webkit-scrollbar { width: 4px; }\n.lb-fixed__products::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-fixed__products::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n/* Fixed bundles: product rows */\n.lb-fixed .lb-bundle-product-row {\n gap: 20px;\n align-items: center;\n}\n\n/* Fixed bundles: larger thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */\n.lb-fixed .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-image-border-radius);\n border: var(--lb-image-border-width) solid var(--lb-image-border-color);\n box-sizing: border-box;\n overflow: visible;\n}\n\n.lb-fixed .lb-bundle-thumbnail img {\n border-radius: max(0px, calc(var(--lb-image-border-radius) - var(--lb-image-border-width)));\n border: none;\n}\n\n/* Variant picker select — styled to match the variant badge aesthetic.\n Sits inside .lb-bundle-variant-option-group so vertical spacing is owned\n by the group/groups flex gap, not the select itself. */\n.lb-bundle-variant-select {\n display: inline-block;\n border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);\n border-radius: var(--lb-variant-radius);\n padding: 8px 32px 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n background-image: var(--lb-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n width: 50%;\n max-width: 50%;\n}\n\n.lb-bundle-variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n`;\nexport const BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles — Mix & Match styles */\n\n/* === Slot list ============================================================\n Caps the height of the slot stack so long bundles don't push the CTA off\n the page. Internal scroll with the same custom 4px scrollbar as the\n variant dropdown panel. */\n.lb-mix-match__slots {\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-mix-match__slots::-webkit-scrollbar { width: 4px; }\n.lb-mix-match__slots::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-mix-match__slots::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n/* === Progress Bar === */\n.lb-mix-match__progress {\n margin-bottom: 16px;\n}\n\n.lb-mix-match__progress-labels {\n display: flex;\n justify-content: space-between;\n margin-bottom: 8px;\n}\n\n.lb-mix-match__progress-count {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n}\n\n.lb-mix-match__progress-remaining {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n color: var(--lb-text);\n}\n\n.lb-mix-match__progress-track {\n width: 100%;\n height: 4px;\n background: color-mix(in srgb, var(--lb-text) 10%, transparent);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.lb-mix-match__progress-fill {\n height: 100%;\n background: var(--lb-text);\n border-radius: 4px;\n transition: width 0.3s ease;\n}\n\n/* === Slots === */\n.lb-mix-match__slot {\n cursor: pointer;\n}\n\n.lb-mix-match .lb-bundle-product-row {\n align-items: center;\n}\n\n.lb-mix-match__slot--empty:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n border-radius: 8px;\n}\n\n.lb-mix-match__slot--empty .lb-mix-match__empty-thumb {\n width: 60px;\n height: 60px;\n min-width: 60px;\n /* 2px border is intentionally independent of --lb-image-border-width —\n empty slots always need a visible dashed outline as an affordance,\n regardless of how the merchant has styled populated thumbnails. */\n border: 2px dashed color-mix(in srgb, var(--lb-text) 35%, transparent);\n border-radius: var(--lb-image-border-radius);\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n color: color-mix(in srgb, var(--lb-text) 35%, transparent);\n}\n\n.lb-mix-match__empty-text {\n font-size: 16px;\n line-height: 20px;\n color: color-mix(in srgb, var(--lb-text) 45%, transparent);\n}\n\n/* Filled slot */\n.lb-mix-match__slot--filled {\n cursor: default;\n}\n\n/* Mix-match thumbnails. Height comes from --lb-thumbnail-aspect-ratio\n (set on .lb-bundle-widget via Liquid / applyWidgetConfigVars). */\n.lb-mix-match .lb-bundle-thumbnail {\n width: 60px;\n min-width: 60px;\n border-radius: var(--lb-image-border-radius);\n border: var(--lb-image-border-width) solid var(--lb-image-border-color);\n box-sizing: border-box;\n overflow: visible;\n}\n\n.lb-mix-match .lb-bundle-thumbnail img {\n border-radius: max(0px, calc(var(--lb-image-border-radius) - var(--lb-image-border-width)));\n border: none;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n\n.lb-mix-match__slot--filled .lb-mix-match__filled-title {\n font-size: 16px;\n font-weight: 700;\n line-height: 20px;\n color: var(--lb-text);\n text-decoration: none;\n overflow-wrap: break-word;\n}\n\n.lb-mix-match__slot--filled a.lb-mix-match__filled-title:hover {\n text-decoration: underline;\n}\n\n.lb-mix-match__slot--filled a.lb-mix-match__filled-title:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n border-radius: 2px;\n}\n\n/* .lb-mix-match__filled-variant — styled jointly with\n .lb-bundle-variant-badge above to keep the variant text consistent\n across mix-match and fixed bundle widgets. */\n\n.lb-mix-match__filled-price {\n /* --lb-product-price-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-price-display, block);\n font-size: 12px;\n line-height: 20px;\n margin-top: 4px;\n color: var(--lb-text);\n font-weight: 500;\n}\n\n.lb-mix-match__filled-compare {\n /* --lb-product-compare-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-product-compare-display, inline);\n font-size: 12px;\n font-weight: 400;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n margin-right: 4px;\n}\n\n.lb-mix-match__slot-remove {\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-text);\n padding: 0;\n margin-left: auto;\n border-radius: 8px;\n}\n\n.lb-mix-match__slot-remove:hover {\n color: var(--lb-text);\n}\n\n.lb-mix-match__slot-remove:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n/* === Modal Overlay === */\n.lb-mix-match__modal-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5);\n z-index: 9999;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s ease-out;\n}\n\n.lb-mix-match__modal-overlay--open {\n opacity: 1;\n}\n\n/* === Modal Panel === */\n.lb-mix-match__modal {\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n border-radius: var(--lb-picker-radius);\n border: var(--lb-picker-border-width) solid var(--lb-picker-border-color);\n box-sizing: border-box;\n width: 100%;\n max-width: 520px;\n max-height: 70vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.16);\n transform: translateY(24px);\n transition: transform 0.25s ease-out;\n will-change: transform;\n}\n\n.lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n}\n\n/* === Modal Header === */\n.lb-mix-match__modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 20px 20px 12px;\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-title {\n font-size: 20px;\n font-weight: 600;\n line-height: 24px;\n margin: 0;\n color: inherit;\n}\n\n.lb-mix-match__modal-close {\n min-width: 44px;\n min-height: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-picker-text);\n border-radius: 8px;\n padding: 0;\n margin: -12px -12px -12px 0;\n}\n\n.lb-mix-match__modal-close:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 0;\n box-shadow: none;\n}\n\n/* === Modal Search === */\n.lb-mix-match__modal-search {\n padding: 0 20px 12px;\n position: relative;\n flex-shrink: 0;\n}\n\n.lb-mix-match__modal-search-input {\n width: 100%;\n padding: 12px 40px 12px 16px;\n border: var(--lb-picker-search-border-width) solid var(--lb-picker-search-border-color);\n border-radius: var(--lb-picker-search-radius);\n font-size: 16px;\n line-height: 20px;\n color: var(--lb-picker-text);\n background: var(--lb-picker-bg);\n box-sizing: border-box;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.lb-mix-match__modal-search-input::placeholder {\n color: color-mix(in srgb, var(--lb-picker-text) 50%, transparent);\n}\n\n.lb-mix-match__modal-search-input:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-search-clear {\n position: absolute;\n right: 32px;\n /* Anchor to the input area only — parent has padding-bottom: 12px which would\n otherwise push a top:50% center down by 6px. */\n top: 0;\n bottom: 12px;\n min-width: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--lb-picker-text);\n padding: 0;\n}\n\n/* === Modal Product List === */\n.lb-mix-match__modal-list {\n overflow-y: auto;\n flex: 1;\n padding: 0 20px;\n -webkit-overflow-scrolling: touch;\n}\n\n.lb-mix-match__modal-product {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: start;\n column-gap: 20px;\n padding-top: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid color-mix(in srgb, var(--lb-picker-text) 7%, transparent);\n}\n\n.lb-mix-match__modal-product:last-child {\n border-bottom: none;\n}\n\n.lb-mix-match__modal-product-thumb {\n position: relative;\n width: 60px;\n min-width: 60px;\n /* Modal picker thumbs follow the merchant's pickerThumbnailRatio —\n independent from the main widget's thumbnailRatio so a merchant can\n e.g. show tall picker thumbs with square main thumbs. */\n aspect-ratio: var(--lb-picker-thumbnail-aspect-ratio, 1 / 1);\n border-radius: var(--lb-picker-product-radius);\n border: var(--lb-picker-product-border-width) solid var(--lb-picker-product-border-color);\n box-sizing: border-box;\n overflow: visible;\n background: var(--lb-thumbnail-bg);\n}\n\n/* Qty badge inside the picker modal inherits picker-product border (width + color) plus\n inverted picker bg/text for clear contrast against the modal — always stays round\n (the badge shape is independent of the thumbnail shape). */\n.lb-mix-match__modal-product-thumb .lb-bundle-qty-badge.lb-bundle-qty-badge {\n /* --lb-picker-qty-badge-display fallback is the \"on\" branch; Liquid sets 'none' when merchant disables. */\n display: var(--lb-picker-qty-badge-display, flex);\n background: var(--lb-picker-qty-badge-bg);\n color: var(--lb-picker-qty-badge-color);\n border: var(--lb-picker-product-border-width) solid var(--lb-picker-product-border-color);\n}\n\n.lb-mix-match__modal-product-thumb img {\n width: 100%;\n /* Height + fit come from pickerThumbnailRatio — \"original\" sets both\n to auto/contain so the image renders uncropped at intrinsic ratio. */\n height: var(--lb-picker-thumbnail-img-height, 100%);\n object-fit: var(--lb-picker-thumbnail-img-fit, cover);\n border-radius: max(0px, calc(var(--lb-picker-product-radius) - var(--lb-picker-product-border-width)));\n}\n\n.lb-mix-match__modal-product-info {\n flex: 1;\n min-width: 0;\n}\n\n.lb-mix-match__modal-product-title {\n font-size: 16px;\n font-weight: 700;\n line-height: 20px;\n color: inherit;\n margin: 0;\n}\n\n.lb-mix-match__modal-product-price {\n font-size: 12px;\n line-height: 20px;\n color: inherit;\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-unit-price {\n font-size: 11px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n margin: 2px 0 0;\n}\n\n.lb-mix-match__modal-product-unit-price[hidden] {\n display: none;\n}\n\n.lb-mix-match__variant-select {\n font-size: 12px;\n padding: 4px 24px 4px 8px;\n border: var(--lb-picker-variant-border-width) solid var(--lb-picker-variant-border-color);\n border-radius: var(--lb-picker-variant-radius);\n background-color: var(--lb-picker-bg);\n background-image: var(--lb-picker-variant-chevron);\n background-repeat: no-repeat;\n background-position: right 6px center;\n background-size: 12px;\n color: var(--lb-picker-text);\n font-family: inherit;\n min-height: 32px;\n cursor: pointer;\n width: 50%;\n max-width: 50%;\n appearance: none;\n -webkit-appearance: none;\n}\n\n.lb-mix-match__variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n/* Per-pick quantity stepper inside the picker modal product row.\n Three flex cells (− / number / +) sharing a single outer border —\n driven by its own --lb-picker-qty-stepper-* variables so merchants\n can style the stepper independently of the variant dropdown. The\n cell dividers come from a 1px border on the centre value rather\n than per-button borders, so the rounded outer corners stay clean.\n Mirrors extensions/bundle-theme/assets/bundle-mix-match.css so\n the headless web component matches the Liquid theme widget. */\n/* Quantity label + stepper sit together in a .lb-bundle-variant-option-group\n wrapper so the label-to-stepper gap inherits the same 2px the variant\n pickers use. The wrapper carries the margin-top that spaces the qty\n group from the unit-price / low-stock-badge above; the stepper itself\n has no top margin so the group can be repositioned without coupling. */\n.lb-mix-match__qty-stepper-group {\n /* Override the base .lb-bundle-variant-option-group column flex so the\n stepper child stretches on the cross-axis (vertical) — this is what\n lets the group track the Add button's height in the action row\n without pinning a fixed pixel value. */\n flex-direction: row;\n width: fit-content;\n}\n\n.lb-mix-match__qty-stepper {\n display: inline-flex;\n align-items: stretch;\n flex-shrink: 0;\n border: var(--lb-picker-qty-stepper-border-width) solid var(--lb-picker-qty-stepper-border-color);\n border-radius: var(--lb-picker-qty-stepper-radius);\n background-color: var(--lb-picker-bg);\n overflow: hidden;\n box-sizing: border-box;\n}\n\n.lb-mix-match__qty-stepper-button {\n appearance: none;\n -webkit-appearance: none;\n background: transparent;\n border: none;\n margin: 0;\n padding: 0;\n width: 28px;\n font-family: inherit;\n font-size: 16px;\n font-weight: 500;\n line-height: 1;\n color: var(--lb-picker-text);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.lb-mix-match__qty-stepper-button:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n.lb-mix-match__qty-stepper-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.lb-mix-match__qty-stepper-value {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 36px;\n padding: 0 8px;\n font-family: inherit;\n font-size: 13px;\n font-weight: 500;\n color: var(--lb-picker-text);\n box-sizing: border-box;\n}\n\n/* Action row — qty stepper (left) + Add button (right). Sits at the\n bottom of the info column with a 12px top margin so it visually\n separates from the variant pickers / price block above. When the\n stepper is hidden, Add fills the row via flex: 1 below.\n align-items: stretch so the stepper-group tracks the Add button's\n height (driven by font size + button padding) without a pinned px. */\n.lb-mix-match__modal-product-actions {\n display: flex;\n flex-wrap: wrap;\n align-items: stretch;\n row-gap: 12px;\n column-gap: 8px;\n margin-top: 12px;\n}\n\n.lb-mix-match__modal-add {\n flex: 1;\n padding: 10px 20px;\n background: var(--lb-picker-add-bg);\n color: var(--lb-picker-add-label);\n border: var(--lb-picker-add-border-width) solid var(--lb-picker-add-border-color);\n border-radius: var(--lb-picker-add-radius);\n box-sizing: border-box;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.lb-mix-match__modal-add:hover {\n opacity: 0.9;\n}\n\n.lb-mix-match__modal-add:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-mix-match__modal-add:disabled {\n background: color-mix(in srgb, var(--lb-picker-add-bg) 35%, var(--lb-picker-bg));\n color: color-mix(in srgb, var(--lb-picker-add-label) 85%, transparent);\n cursor: not-allowed;\n}\n\n/* Sold out product row */\n.lb-mix-match__modal-product--sold-out {\n opacity: 0.5;\n}\n\n/* Already in a slot — prevents shoppers from filling multiple slots\n with the same product (which wouldn't qualify the discount: it\n counts distinct products, not slot occupancy). The thumb keeps\n its qty badge so the shopper sees what's already in the bundle. */\n.lb-mix-match__modal-product--in-bundle {\n opacity: 0.5;\n}\n\n.lb-mix-match__modal-product--sold-out .lb-mix-match__modal-sold-out-label {\n font-size: 12px;\n color: inherit;\n font-weight: 500;\n white-space: nowrap;\n}\n\n/* === Modal Empty State === */\n.lb-mix-match__modal-empty {\n padding: 32px 20px;\n text-align: center;\n}\n\n.lb-mix-match__modal-empty p {\n margin: 0;\n font-size: 16px;\n color: var(--lb-picker-text);\n}\n\n/* Hidden utility for search filtering */\n.lb-hidden {\n display: none !important;\n}\n\n/* === Mobile Full-Screen Modal === */\n@media (max-width: 767px) {\n .lb-mix-match__modal-overlay {\n align-items: flex-end;\n }\n\n .lb-mix-match__modal {\n max-width: 100%;\n max-height: 90vh;\n border-radius: 16px 16px 0 0;\n transform: translateY(100%);\n }\n\n .lb-mix-match__modal-overlay--open .lb-mix-match__modal {\n transform: translateY(0);\n }\n\n .lb-mix-match__variant-select {\n width: 80%;\n max-width: 80%;\n }\n}\n\n/* === Reduced Motion === */\n@media (prefers-reduced-motion: reduce) {\n .lb-mix-match__modal-overlay,\n .lb-mix-match__modal,\n .lb-mix-match__progress-fill {\n transition: none;\n }\n}\n`;\nexport const BUNDLE_VOLUME_CSS = `/* Lime Bundles — Volume / Quantity Breaks styles */\n\n.lb-volume__tiers {\n display: flex;\n flex-direction: column;\n gap: 12px;\n max-height: var(--lb-list-max-height);\n overflow-y: auto;\n /* Custom scrollbar — text colour at 15% opacity (thumb) and 5% (track). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-volume__tiers::-webkit-scrollbar { width: 4px; }\n.lb-volume__tiers::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-volume__tiers::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n.lb-volume__tier {\n display: flex;\n align-items: center;\n gap: 12px;\n border: var(--lb-tier-border-width) solid var(--lb-tier-border-color);\n border-radius: var(--lb-tier-radius);\n padding: 12px 16px;\n cursor: pointer;\n position: relative;\n transition: border-color 0.15s ease;\n}\n\n.lb-volume__tier:hover {\n border-color: var(--lb-tier-selected-border-color);\n}\n\n/* Suppress the UA default focus outline + Dawn's focus shadow so a\n freshly-clicked selected tier doesn't briefly show the 1px focus ring\n or shadow on top of (or in place of) the custom selected-state\n outline below. Keyboard focus is still indicated by the\n :focus-visible rule. */\n.lb-volume__tier:focus {\n outline: none;\n box-shadow: none;\n}\n\n/* Keyboard focus indicator — explicitly excludes the selected tier so\n the selected-state outline rule below has full ownership of the\n outline property when both states apply at once. */\n.lb-volume__tier:focus-visible:not([aria-checked=\"true\"]) {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: -2px;\n box-shadow: none;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] {\n border-color: var(--lb-tier-selected-border-color);\n outline: var(--lb-tier-selected-border-width) solid var(--lb-tier-selected-border-color);\n outline-offset: calc(-1 * var(--lb-tier-selected-border-width));\n}\n\n.lb-volume__radio {\n width: 20px;\n height: 20px;\n min-width: 20px;\n border: 2px solid var(--lb-text);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: border-color 0.15s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio {\n border-color: var(--lb-text);\n}\n\n.lb-volume__radio-dot {\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background: transparent;\n transition: background 0.15s ease;\n}\n\n.lb-volume__tier[aria-checked=\"true\"] .lb-volume__radio-dot {\n background: var(--lb-text);\n}\n\n/* Tier content: 2x2 grid layout */\n.lb-volume__tier-grid {\n flex: 1;\n display: grid;\n row-gap: 4px;\n align-items: center;\n}\n\n.lb-volume__tier-label {\n font-size: 16px;\n font-weight: 700;\n color: var(--lb-text);\n}\n\n.lb-volume__tier-badge {\n flex-shrink: 0;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-popular-badge-text);\n background: var(--lb-popular-badge-bg);\n border: var(--lb-popular-badge-border-width) solid var(--lb-popular-badge-border-color);\n border-radius: var(--lb-popular-badge-radius);\n padding: 4px 8px;\n}\n\n.lb-volume__tier-price {\n grid-column: 1 / -1;\n font-size: 14px;\n font-weight: 500;\n color: var(--lb-text);\n}\n\n.lb-volume__tier-unit {\n font-size: 12px;\n color: var(--lb-text);\n}\n\n/* Compare-at (strikethrough) price */\n.lb-volume__tier-compare {\n font-size: 14px;\n line-height: 16px;\n color: color-mix(in srgb, var(--lb-text) 60%, transparent);\n text-decoration: line-through;\n margin-right: 4px;\n}\n\n\n`;\n\nexport const BUNDLE_DROPDOWN_CSS = `/**\n * Lime Bundles — Custom variant-picker dropdown styling.\n *\n * Reuses existing CSS variables: no new merchant-configurable surface.\n * --lb-variant-border-{width,color}, --lb-variant-radius, --lb-variant-chevron\n * --lb-bg, --lb-text, --lb-primary-color\n *\n * Mix-match modal context overrides via .lb-mix-match__modal scope to use\n * --lb-picker-variant-* and --lb-picker-bg.\n */\n\n/* Hide the native <select> while keeping it form-serializable and focusable\n programmatically. The .lb-dropdown-state marker is added by JS at bind\n time, so this rule matches every variant-select class (main widget,\n mix-match modal, future bundle types). aria-hidden + tabindex=-1\n (also set in JS) remove it from the accessibility tree. */\n.lb-dropdown-state {\n position: absolute !important;\n width: 1px !important;\n height: 1px !important;\n padding: 0 !important;\n margin: -1px !important;\n overflow: hidden !important;\n clip: rect(0 0 0 0) !important;\n white-space: nowrap !important;\n border: 0 !important;\n pointer-events: none !important;\n}\n\n/* Shell fills its parent column. */\n.lb-dropdown {\n position: relative;\n display: inline-block;\n width: 100%;\n max-width: 100%;\n font-family: inherit;\n}\n\n/* Trigger styled identically to the closed-state native select */\n.lb-dropdown-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);\n border-radius: var(--lb-variant-radius);\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text);\n background: var(--lb-bg);\n font-family: inherit;\n cursor: pointer;\n appearance: none;\n -webkit-appearance: none;\n text-align: start;\n transition: border-color 120ms ease;\n}\n\n.lb-dropdown-trigger:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n box-shadow: none;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] {\n border-color: var(--lb-text);\n}\n\n.lb-dropdown-trigger-value {\n flex: 1 1 auto;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n text-align: start;\n}\n\n.lb-dropdown-chevron {\n flex: 0 0 auto;\n width: 12px;\n height: 12px;\n background: var(--lb-variant-chevron) center / contain no-repeat;\n transition: transform 120ms ease;\n}\n\n.lb-dropdown-trigger[aria-expanded=\"true\"] .lb-dropdown-chevron {\n transform: rotate(180deg);\n}\n\n/* Popover panel — position: absolute against the .lb-dropdown shell\n (already position: relative). Top/left/width come from CSS so we\n never depend on JS having set inline coords by the time the panel\n becomes visible. JS only sets max-height. */\n.lb-dropdown-listbox {\n position: absolute;\n left: 0;\n /* Default to below-trigger placement so the panel doesn't overlap the\n trigger if data-placement is missing for any reason. The explicit\n [data-placement=\"down\"|\"up\"] rules below override this. */\n top: calc(100% + 4px);\n width: 100%;\n z-index: 9999;\n margin: 0;\n padding: 4px 0;\n list-style: none;\n background: var(--lb-bg);\n color: var(--lb-text);\n border: var(--lb-variant-border-width) solid var(--lb-variant-border-color);\n border-radius: var(--lb-variant-radius);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);\n overflow-y: auto;\n overflow-x: hidden;\n /* Custom scrollbar — text colour at 15% opacity (thumb) and 5% (track). */\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--lb-text) 15%, transparent)\n color-mix(in srgb, var(--lb-text) 2%, transparent);\n animation: lb-dropdown-in-down 120ms ease-out;\n transform-origin: top center;\n}\n\n.lb-dropdown-listbox[data-placement=\"down\"] {\n top: calc(100% + 4px);\n}\n\n.lb-dropdown-listbox[data-placement=\"up\"] {\n top: auto;\n bottom: calc(100% + 4px);\n animation-name: lb-dropdown-in-up;\n transform-origin: bottom center;\n}\n\n/* When portaled out of the .lb-dropdown shell (mix-match modal context:\n .lb-mix-match__modal applies translateY which would otherwise trap\n position:fixed), switch to fixed and let JS set viewport coords. */\n.lb-dropdown-listbox[data-lb-dropdown-portal] {\n position: fixed;\n top: auto;\n left: auto;\n bottom: auto;\n width: auto;\n}\n\n/* Custom scrollbar — Webkit/Blink: exact 4px width */\n.lb-dropdown-listbox::-webkit-scrollbar {\n width: 4px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n border-radius: 2px;\n}\n.lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-text) 15%, transparent);\n border-radius: 2px;\n}\n\n/* Options */\n.lb-dropdown-option {\n padding: 8px 12px;\n font-size: 12px;\n line-height: 16px;\n cursor: pointer;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--lb-text);\n}\n\n.lb-dropdown-option[aria-selected=\"true\"] {\n font-weight: 600;\n}\n\n.lb-dropdown-option.is-active,\n.lb-dropdown-option:hover:not([aria-disabled=\"true\"]) {\n background: color-mix(in srgb, var(--lb-text) 2%, transparent);\n}\n\n.lb-dropdown-option[aria-disabled=\"true\"] {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n/* Animations */\n@keyframes lb-dropdown-in-down {\n from { opacity: 0; transform: translateY(-4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@keyframes lb-dropdown-in-up {\n from { opacity: 0; transform: translateY(4px) scale(0.98); }\n to { opacity: 1; transform: translateY(0) scale(1); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-dropdown-listbox { animation: none; }\n .lb-dropdown-chevron { transition: none; }\n .lb-dropdown-trigger { transition: none; }\n}\n\n/* Forced-colors mode (Windows high-contrast) */\n@media (forced-colors: active) {\n .lb-dropdown-trigger {\n border-color: ButtonBorder;\n color: ButtonText;\n background: ButtonFace;\n }\n .lb-dropdown-listbox {\n border-color: ButtonBorder;\n background: Canvas;\n color: CanvasText;\n }\n .lb-dropdown-option.is-active {\n background: Highlight;\n color: HighlightText;\n }\n}\n\n/* Mix-match modal context — use picker-scoped variables.\n No CSS fallbacks: --lb-picker-* are always emitted by bundle-widget.liquid\n because WidgetConfig.parse() fully hydrates the merchant config.\n See docs/solutions/ui-bugs/widget-css-single-source-defaults.md. */\n.lb-mix-match__modal .lb-dropdown-trigger {\n border-color: var(--lb-picker-variant-border-color);\n border-width: var(--lb-picker-variant-border-width);\n border-radius: var(--lb-picker-variant-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n}\n\n.lb-mix-match__modal .lb-dropdown-chevron {\n background-image: var(--lb-picker-variant-chevron);\n}\n\n/* Listbox is portaled out of the transformed .lb-mix-match__modal up to\n its [data-modal-overlay] parent, so picker-scoped rules anchor on the\n overlay attribute, not the modal class. */\n[data-modal-overlay] > .lb-dropdown-listbox {\n border-color: var(--lb-picker-variant-border-color);\n border-width: var(--lb-picker-variant-border-width);\n border-radius: var(--lb-picker-variant-radius);\n background: var(--lb-picker-bg);\n color: var(--lb-picker-text);\n scrollbar-color: color-mix(in srgb, var(--lb-picker-text) 15%, transparent)\n color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n}\n\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-track {\n background: color-mix(in srgb, var(--lb-picker-text) 2%, transparent);\n border-radius: 2px;\n}\n[data-modal-overlay] > .lb-dropdown-listbox::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--lb-picker-text) 15%, transparent);\n}\n`;\n\n/**\n * Skeleton styles for the web component's loading state. Rendered\n * synchronously in connectedCallback → renderLoading() so the host\n * element has intrinsic size from render-0 and doesn't shift layout\n * when the real bundle paints. Web-component only (see file header).\n */\nexport const BUNDLE_SKELETON_CSS = `/* Lime Bundles — web-component loading skeleton (not mirrored to theme assets) */\n\n.lb-bundle-widget--loading {\n display: block;\n padding: var(--lb-widget-pad, 20px);\n border: 1px solid var(--lb-border, #E5E5E5);\n border-radius: var(--lb-radius, 12px);\n background: var(--lb-bg, #FFFFFF);\n /* Contain layout/paint so the skeleton doesn't influence ancestor\n layout once the real content swaps in. */\n contain: layout paint;\n}\n\n.lb-bundle-widget--loading .lb-skeleton {\n background: linear-gradient(\n 90deg,\n rgba(0, 0, 0, 0.06) 0%,\n rgba(0, 0, 0, 0.10) 50%,\n rgba(0, 0, 0, 0.06) 100%\n );\n background-size: 200% 100%;\n border-radius: 6px;\n animation: lb-skeleton-pulse 1.4s ease-in-out infinite;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--title {\n height: 28px;\n width: 60%;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--products {\n display: grid;\n gap: 12px;\n margin-bottom: 16px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--row {\n display: grid;\n grid-template-columns: 56px 1fr 60px;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--thumb {\n height: 56px;\n width: 56px;\n border-radius: 8px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line {\n height: 14px;\n border-radius: 4px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--line + .lb-skeleton--line {\n margin-top: 8px;\n width: 70%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--price {\n height: 20px;\n width: 60px;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--footer {\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid var(--lb-border, #E5E5E5);\n display: grid;\n grid-template-columns: 1fr auto;\n gap: 12px;\n align-items: center;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--total {\n height: 24px;\n width: 40%;\n}\n\n.lb-bundle-widget--loading .lb-skeleton--cta {\n height: 44px;\n width: 140px;\n border-radius: var(--lb-cta-radius, 8px);\n}\n\n@keyframes lb-skeleton-pulse {\n 0% { background-position: 0% 50%; }\n 100% { background-position: -200% 50%; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .lb-bundle-widget--loading .lb-skeleton {\n animation: none;\n }\n}\n`;\n\n","/**\n * <lime-bundle> Web Component — renders Lime Bundles on any storefront.\n *\n * ## The two modes\n *\n * Single-bundle (pinned):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * bundle-gid=\"gid://shopify/Metaobject/42\"\n * ></lime-bundle>\n *\n * Product-aware (matches classic Liquid theme block behaviour — one snippet\n * on the product page renders every active bundle configured against that\n * product):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * ></lime-bundle>\n *\n * Product resolution cascade (when no `bundle-gid` is set):\n * 1. explicit `product-handle` attribute\n * 2. <meta name=\"shopify:product-handle\" content=\"...\"> on the page\n * 3. /products/<handle> segment of window.location.pathname\n * 4. fallthrough: renders nothing, fires `lime-bundle:error`\n *\n * ## Add-to-cart behaviour\n *\n * Merchants who do nothing get a default: the widget calls Shopify's\n * Storefront Cart API (tokenless — no extra scopes required) and redirects\n * the browser to the returned checkoutUrl. One-click-to-checkout is the\n * right UX for most merchants pasting the widget into Webflow / Wix /\n * Squarespace / static HTML.\n *\n * Merchants with their own cart state (Hydrogen's useCart, a custom cart\n * drawer, etc.) opt out by attaching a listener that calls\n * `event.preventDefault()`:\n *\n * document.querySelector(\"lime-bundle\").addEventListener(\n * \"lime-bundle:add-to-cart\",\n * (ev) => {\n * ev.preventDefault(); // suppress the default redirect\n * myCart.linesAdd(ev.detail.lines);\n * },\n * );\n *\n * The event is always dispatched; only the default action is conditional.\n */\nimport {\n createStorefrontClient,\n BUNDLE_METAOBJECT_QUERY,\n BUNDLES_FOR_PRODUCT_QUERY,\n CART_CREATE_MUTATION,\n CART_LINES_ADD_MUTATION,\n SHOP_CUSTOM_CSS_QUERY,\n parseMetaobjectBundle,\n observeImpression,\n reportImpression,\n reportAddToCart,\n injectCustomCss,\n sanitizeCustomCss,\n getLinkGroupAssignment,\n type ParsedBundle,\n type BundleMetaobjectResponse,\n type BundlesForProductResponse,\n type CartCreateResponse,\n type CartLinesAddResponse,\n type ShopCustomCssResponse,\n type CartLineInput,\n type StorefrontClient,\n type BuyerResolver,\n hasInContext,\n withInContext,\n} from \"@lime-bundles/core\";\nimport { renderFixedBundle } from \"./renderers/fixed\";\nimport { renderMixMatchBundle } from \"./renderers/mix-match\";\nimport { renderVolumeBundle } from \"./renderers/volume\";\nimport { applyWidgetConfigVars } from \"@lime-bundles/core\";\nimport { trackInputMode } from \"./utils/input-mode\";\nimport {\n BUNDLE_BASE_CSS,\n BUNDLE_DROPDOWN_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_SKELETON_CSS,\n BUNDLE_VOLUME_CSS,\n} from \"./styles/bundle-css\";\n\n/**\n * Per-shop localStorage key for the active cart id. Scoping by shop domain\n * keeps the cart isolated when a single browser visits multiple Lime-\n * Bundles-powered storefronts (rare, but correct).\n */\nconst cartStorageKey = (shopDomain: string) => `lb_cart_id:${shopDomain}`;\n\n/**\n * Resolve the current product handle from the page. Runs the cascade\n * documented on LimeBundleElement and returns null if no source matches.\n */\nfunction resolveProductHandle(explicit: string | null): string | null {\n if (explicit) return explicit.trim() || null;\n\n if (typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>(\n 'meta[name=\"shopify:product-handle\"]',\n );\n if (meta?.content) return meta.content.trim() || null;\n }\n\n if (typeof window !== \"undefined\") {\n const match = window.location.pathname.match(/\\/products\\/([^/?#]+)/);\n if (match?.[1]) return decodeURIComponent(match[1]);\n }\n\n return null;\n}\n\nexport class LimeBundleElement extends HTMLElement {\n static observedAttributes = [\n \"shop-domain\",\n \"storefront-token\",\n \"bundle-gid\",\n \"product-handle\",\n \"app-url\",\n \"analytics\",\n \"locale\",\n \"country\",\n \"language\",\n \"market-id\",\n ];\n\n /**\n * B2B buyer identity. Set programmatically — `element.buyer = {...}` or\n * `element.buyer = () => fetchToken()`. NEVER expose as an HTML attribute\n * because the customer access token would land in DOM snapshots (Sentry,\n * analytics, browser extensions) and Referer headers. See the package\n * README (\"Markets & B2B\").\n */\n buyer: BuyerResolver | undefined = undefined;\n\n private shadow: ShadowRoot;\n private bundles: ParsedBundle[] = [];\n private abortController: AbortController | null = null;\n private impressionCleanups: Array<() => void> = [];\n /**\n * Tick / observer / listener cleanups registered by individual renderers\n * (e.g. countdown setInterval handles). Flushed in disconnectedCallback\n * so we never leak timers or event listeners when the widget is removed.\n */\n private renderCleanups: Array<() => void> = [];\n /**\n * Merchant custom CSS fetched from the shop metafield. Injected inside\n * the shadow root alongside the bundle stylesheets so selectors like\n * `.lb-bundle-widget { ... }` reach the widget's DOM.\n */\n private shopCustomCss: string | null = null;\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n // `fetchBundle()` calls `renderLoading()` synchronously before any\n // await — the skeleton paints on the first frame, reserving\n // layout space before the Storefront API call resolves.\n this.fetchBundle();\n }\n\n disconnectedCallback() {\n this.abortController?.abort();\n this.teardownImpressions();\n this.teardownRenderers();\n }\n\n private teardownImpressions() {\n for (const cleanup of this.impressionCleanups) cleanup();\n this.impressionCleanups = [];\n }\n\n private teardownRenderers() {\n for (const cleanup of this.renderCleanups) cleanup();\n this.renderCleanups = [];\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue || !this.isConnected) return;\n if (\n name === \"bundle-gid\" ||\n name === \"product-handle\" ||\n name === \"shop-domain\" ||\n name === \"storefront-token\"\n ) {\n if (this.shopDomain && this.storefrontToken) {\n this.fetchBundle();\n }\n }\n }\n\n private get shopDomain(): string {\n return this.getAttribute(\"shop-domain\") ?? \"\";\n }\n\n private get storefrontToken(): string {\n return this.getAttribute(\"storefront-token\") ?? \"\";\n }\n\n private get bundleGid(): string {\n return this.getAttribute(\"bundle-gid\") ?? \"\";\n }\n\n private get productHandleAttr(): string {\n return this.getAttribute(\"product-handle\") ?? \"\";\n }\n\n private get appUrl(): string {\n return this.getAttribute(\"app-url\") ?? \"\";\n }\n\n private get analyticsEnabled(): boolean {\n return this.getAttribute(\"analytics\") !== \"false\";\n }\n\n private get country(): string | undefined {\n return this.getAttribute(\"country\") ?? undefined;\n }\n\n private get language(): string | undefined {\n return this.getAttribute(\"language\") ?? undefined;\n }\n\n private get marketId(): string | undefined {\n return this.getAttribute(\"market-id\") ?? undefined;\n }\n\n /**\n * Returns the @inContext-wrapped query when any context field is set;\n * otherwise the plain query. Keeps responses publicly cacheable when\n * no buyer/country/language is set.\n */\n private wrapQueryForContext(query: string): string {\n return hasInContext({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.buyer,\n })\n ? withInContext(query)\n : query;\n }\n\n /**\n * Returns true if this bundle should be hidden for the current market.\n * For \"all\" bundles, always returns false (visible). For \"specific\"\n * bundles, returns true when no marketId was set or when the bundle's\n * marketIds doesn't include the configured market.\n */\n private isMarketHidden(bundle: ParsedBundle): boolean {\n if (bundle.marketVisibility !== \"specific\") return false;\n if (!this.marketId) return true;\n return !bundle.marketIds.includes(this.marketId);\n }\n\n private async fetchBundle() {\n if (!this.shopDomain || !this.storefrontToken) {\n this.renderError(\n \"Missing required attributes: shop-domain, storefront-token\",\n );\n return;\n }\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n this.renderLoading();\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n country: this.country,\n language: this.language,\n buyer: this.buyer,\n });\n\n try {\n // Fire bundle query BEFORE CSS so order-sensitive call logs (and any\n // test harness that mocks fetch with sequential mockResolvedValueOnce)\n // see the bundle as call #1, CSS as call #2. Parallelism preserved\n // via Promise.all at the await site below.\n let bundlePromise: Promise<void>;\n let singleBundleMode = false;\n if (this.bundleGid) {\n singleBundleMode = true;\n bundlePromise = this.fetchSingleBundle(client, controller.signal);\n } else {\n const handle = resolveProductHandle(this.productHandleAttr);\n if (!handle) {\n this.teardownImpressions();\n this.renderError(\n \"No bundle-gid or product-handle provided, and the current URL doesn't match /products/<handle>.\",\n );\n return;\n }\n bundlePromise = this.fetchProductBundles(\n client,\n controller.signal,\n handle,\n );\n }\n\n // CSS fetch kicks off AFTER bundle fetch for deterministic call order.\n // Best-effort — widget renders without merchant styling if it fails.\n const cssPromise = client\n .query<ShopCustomCssResponse>(SHOP_CUSTOM_CSS_QUERY, undefined, {\n signal: controller.signal,\n })\n .catch(() => null);\n\n await bundlePromise;\n if (controller.signal.aborted) return;\n\n // Single-bundle mode with an explicit GID that didn't resolve is a\n // genuine error — the merchant pinned a specific bundle and it's\n // missing. Product-handle mode with zero bundles is NOT an error:\n // the product legitimately has no bundles configured; widget stays\n // invisible (mirrors classic-theme Liquid block behaviour).\n if (singleBundleMode && this.bundles.length === 0) {\n this.teardownImpressions();\n this.renderError(\"Bundle not found\");\n return;\n }\n\n const css = await cssPromise;\n if (css?.shop?.metafield?.value) {\n // Classic storefronts get the CSS in document.head; the web\n // component also stores the sanitized CSS for its own shadow\n // root injection below, since shadow DOM blocks inherited styles.\n injectCustomCss(this.shopDomain, css.shop.metafield.value);\n const sanitized = sanitizeCustomCss(css.shop.metafield.value);\n if (sanitized.ok) this.shopCustomCss = sanitized.css;\n }\n\n // Resolve link-group variants for any bundle that's the primary of an\n // active test. Awaited so bundles render with the assigned variant on\n // first paint (no flash of the primary). Individual failures inside\n // applyLinkGroupVariants fall back to the primary silently.\n await this.applyLinkGroupVariants();\n\n this.renderBundles();\n } catch (err) {\n if (controller.signal.aborted) return;\n this.bundles = [];\n this.teardownImpressions();\n this.renderError(\n err instanceof Error ? err.message : \"Failed to load bundle\",\n );\n }\n }\n\n /**\n * Resolve the visitor's assigned variant for every primary bundle in a link\n * group. Runs in parallel; any assignment failure logs internally and still\n * renders the primary (safe default). The `getLinkGroupAssignment` helper\n * persists the bucket via a first-party cookie; variant attribution is\n * recorded server-side from the cart-line `_lime_bundle_gid` + `_lime_link_group`\n * attributes the cart-add path stamps when an assignment lands on a non-primary\n * variant.\n *\n * When the assignment points to a non-primary variant the SDK swaps in that\n * variant's `ParsedBundle` from the primary's `linkGroup.variants` list. The\n * variant data must already be present in the storefront query response — the\n * SDK does NOT issue a separate metaobject fetch on the hot path.\n */\n private async applyLinkGroupVariants(): Promise<void> {\n if (this.bundles.length === 0) return;\n\n // Build a lookup so assignment can swap to a variant we've already\n // fetched in the same storefront query. `bundleLookupByMetaobjectId` is\n // built from `this.bundles` because variants of a link group always come\n // back together via the BundlesForProduct query (the primary's\n // link_group_variants advertises them and they're included by the\n // existing query selection — no extra round-trip needed).\n const bundleLookupByMetaobjectId = new Map<string, ParsedBundle>();\n for (const b of this.bundles) {\n bundleLookupByMetaobjectId.set(b.id, b);\n }\n\n const results = await Promise.all(\n this.bundles.map(async (bundle) => {\n const lg = bundle.linkGroup;\n if (!lg || !lg.isPrimary || !lg.variants || lg.variants.length < 2) {\n return bundle;\n }\n try {\n const assignment = await getLinkGroupAssignment(lg.id, lg.variants);\n if (!assignment) return bundle;\n if (assignment.variantMetaobjectId === bundle.id) return bundle;\n const swap = bundleLookupByMetaobjectId.get(\n assignment.variantMetaobjectId,\n );\n if (!swap) return bundle;\n return swap;\n } catch (err) {\n // Surface link-group failures so misconfigured tests aren't invisible.\n // The primary still renders — the customer is never blocked.\n // eslint-disable-next-line no-console\n console.warn(\n `[lime-bundle] link-group assignment failed for bundle ${bundle.id}; falling back to primary.`,\n err,\n );\n return bundle;\n }\n }),\n );\n this.bundles = results;\n }\n\n private async fetchSingleBundle(\n client: StorefrontClient,\n signal: AbortSignal,\n ): Promise<void> {\n const data = await client.query<BundleMetaobjectResponse>(\n this.wrapQueryForContext(BUNDLE_METAOBJECT_QUERY),\n { id: this.bundleGid },\n { signal },\n );\n if (!data.metaobject) {\n this.bundles = [];\n return;\n }\n const parsed = parseMetaobjectBundle(\n data.metaobject.id,\n data.metaobject.fields,\n );\n this.bundles = parsed && !this.isMarketHidden(parsed) ? [parsed] : [];\n }\n\n private async fetchProductBundles(\n client: StorefrontClient,\n signal: AbortSignal,\n productHandle: string,\n ): Promise<void> {\n // fetchBundlesForProduct re-creates its own client; skip that indirection\n // and reuse the one we already built so the request shares the same\n // AbortSignal and header config.\n const data = await client.query<BundlesForProductResponse>(\n this.wrapQueryForContext(BUNDLES_FOR_PRODUCT_QUERY),\n { handle: productHandle },\n { signal },\n );\n if (!data.product) {\n this.bundles = [];\n return;\n }\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n for (const ref of refs) {\n const parsed = parseMetaobjectBundle(ref.id, ref.fields);\n if (parsed && !this.isMarketHidden(parsed)) bundles.push(parsed);\n }\n this.bundles = bundles;\n }\n\n /**\n * Dispatch add-to-cart with a cancelable event, then — unless a listener\n * called preventDefault — execute the default cart-and-checkout flow.\n *\n * `fire-and-forget` against `reportAddToCart` runs regardless so merchants\n * with BYO cart still get analytics.\n */\n private handleAddToCart = async (\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): Promise<void> => {\n const ev = new CustomEvent(\"lime-bundle:add-to-cart\", {\n detail: { lines },\n bubbles: true,\n composed: true,\n cancelable: true,\n });\n // dispatchEvent returns false if preventDefault() was called on a\n // cancelable event. That's how merchants opt out of the default flow.\n const allowDefault = this.dispatchEvent(ev);\n\n // Analytics fire regardless of which cart path runs.\n this.reportAddToCartEvent(bundle, lines);\n\n if (allowDefault) {\n await this.defaultAddToCart(lines);\n }\n };\n\n /**\n * Default cart flow: Shopify's Storefront Cart API is tokenless, so we\n * don't need any additional scopes. Persist the cart ID in localStorage\n * so subsequent adds on the same browser session join the existing cart\n * instead of creating a new one every click.\n */\n private async defaultAddToCart(lines: CartLineInput[]): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n const storage = window.localStorage;\n const key = cartStorageKey(this.shopDomain);\n const existingCartId = storage?.getItem(key) ?? null;\n\n try {\n let checkoutUrl: string | null = null;\n\n if (existingCartId) {\n const res = await client.query<CartLinesAddResponse>(\n CART_LINES_ADD_MUTATION,\n { cartId: existingCartId, lines },\n );\n const payload = res.cartLinesAdd;\n if (payload?.userErrors?.length) {\n // Cart GID expired or was merged on Shopify's side — fall back to\n // cartCreate below. This happens after ~10 days of inactivity.\n storage?.removeItem(key);\n } else if (payload?.cart) {\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (!checkoutUrl) {\n const res = await client.query<CartCreateResponse>(\n CART_CREATE_MUTATION,\n { input: { lines } },\n );\n const payload = res.cartCreate;\n if (payload?.cart) {\n storage?.setItem(key, payload.cart.id);\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (checkoutUrl) {\n window.location.assign(checkoutUrl);\n } else {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message: \"Cart creation failed\", code: \"CART_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n }\n } catch (err) {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: {\n message:\n err instanceof Error ? err.message : \"Cart mutation failed\",\n code: \"CART_ERROR\",\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n }\n\n private reportAddToCartEvent(\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): void {\n if (!this.analyticsEnabled || !this.appUrl) return;\n\n const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);\n const totalPrice = lines.reduce((sum, line) => {\n const product = bundle.products.find((p) =>\n p.variants.nodes.some((v) => v.id === line.merchandiseId),\n );\n const variant = product?.variants.nodes.find(\n (v) => v.id === line.merchandiseId,\n );\n const price = variant ? parseFloat(variant.price.amount) : 0;\n return sum + price * line.quantity;\n }, 0);\n\n reportAddToCart(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n productId: bundle.products[0]?.id ?? \"\",\n quantity,\n totalPrice: Math.round(totalPrice * 100) / 100,\n },\n );\n }\n\n private renderBundles() {\n this.teardownImpressions();\n this.teardownRenderers();\n this.shadow.innerHTML = \"\";\n\n // Bundle the base stylesheet and every type-specific sheet in one <style>.\n // This matches the Liquid theme app block's output (base.css +\n // bundle-{fixed,mix-match,volume}.css) so the shipped widget honours the\n // same selectors a merchant already styled against. Shadow DOM scope\n // keeps these rules from leaking out; the ported styles all target\n // `.lb-bundle-widget` descendants so there's no global bleed.\n const style = document.createElement(\"style\");\n style.textContent = [\n BUNDLE_BASE_CSS,\n BUNDLE_FIXED_CSS,\n BUNDLE_MIX_MATCH_CSS,\n BUNDLE_VOLUME_CSS,\n BUNDLE_DROPDOWN_CSS,\n ].join(\"\\n\");\n this.shadow.appendChild(style);\n\n // Merchant custom CSS — injected AFTER the built-in stylesheets so\n // the merchant's rules override defaults. Sanitized upstream via\n // sanitizeCustomCss (strips < > and script-safety patterns).\n if (this.shopCustomCss) {\n const customStyle = document.createElement(\"style\");\n customStyle.setAttribute(\"data-lime-bundles\", \"shop-custom-css\");\n customStyle.textContent = this.shopCustomCss;\n this.shadow.appendChild(customStyle);\n }\n\n // Empty state: the shadow root holds only the <style> tag — nothing\n // visible. Matches Liquid theme UX where a product with no bundles\n // simply shows no block. We still fire `lime-bundle:loaded` below so\n // consumers know the async work completed (important for test\n // synchronisation and for merchants who want to hide a parent\n // placeholder once the widget has decided whether to render).\n\n for (const bundle of this.bundles) {\n const container = document.createElement(\"div\");\n container.className = \"lb-bundle-widget\";\n container.setAttribute(\"role\", \"region\");\n container.setAttribute(\"aria-label\", bundle.title);\n container.setAttribute(\"data-bundle-type\", bundle.bundleType);\n container.setAttribute(\"data-bundle-gid\", bundle.id);\n\n // Emit every merchant-configurable --lb-* property so the ported CSS\n // renders the widget with the look the merchant set in the admin\n // editor.\n applyWidgetConfigVars(container, bundle.widgetConfig);\n\n // Toggle using-mouse / using-keyboard on this container so the CSS\n // focus-ring rules match the customer's current input device.\n this.renderCleanups.push(trackInputMode(container));\n\n const dispatch = (lines: CartLineInput[]) =>\n this.handleAddToCart(bundle, lines);\n const registerCleanup = (fn: () => void) =>\n this.renderCleanups.push(fn);\n\n switch (bundle.bundleType) {\n case \"fixed\":\n renderFixedBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"mix_match\":\n renderMixMatchBundle(container, bundle, dispatch, registerCleanup);\n break;\n case \"volume\":\n renderVolumeBundle(container, bundle, dispatch, registerCleanup);\n break;\n }\n\n this.shadow.appendChild(container);\n this.setupImpressionFor(bundle, container);\n }\n\n const first = this.bundles[0];\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:loaded\", {\n detail: {\n bundleCount: this.bundles.length,\n bundleTypes: this.bundles.map((b) => b.bundleType),\n // Legacy fields — meaningful only in single-bundle mode. Preserved\n // for merchants who attached listeners against the pre-1.0 shape.\n bundleType: first?.bundleType,\n title: first?.title,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private setupImpressionFor(bundle: ParsedBundle, element: Element) {\n if (!this.analyticsEnabled || !this.appUrl) return;\n const cleanup = observeImpression(element, () => {\n reportImpression(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n },\n );\n });\n this.impressionCleanups.push(cleanup);\n }\n\n private renderLoading() {\n // Synchronous paint on every mount (before the Storefront fetch\n // resolves) so the host element has intrinsic size from render-0\n // and doesn't shift layout when the real bundle swaps in.\n this.shadow.innerHTML = `\n <style>${BUNDLE_BASE_CSS}</style>\n <style>${BUNDLE_SKELETON_CSS}</style>\n <div class=\"lb-bundle-widget lb-bundle-widget--loading\" aria-busy=\"true\" aria-label=\"Loading bundle\">\n <div class=\"lb-skeleton lb-skeleton--title\"></div>\n <div class=\"lb-skeleton--products\">\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n <div class=\"lb-skeleton--row\">\n <div class=\"lb-skeleton lb-skeleton--thumb\"></div>\n <div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n <div class=\"lb-skeleton lb-skeleton--line\"></div>\n </div>\n <div class=\"lb-skeleton lb-skeleton--price\"></div>\n </div>\n </div>\n <div class=\"lb-skeleton--footer\">\n <div class=\"lb-skeleton lb-skeleton--total\"></div>\n <div class=\"lb-skeleton lb-skeleton--cta\"></div>\n </div>\n </div>\n `;\n }\n\n private renderError(message: string) {\n this.shadow.innerHTML = \"\";\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message, code: \"LOAD_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n}\n\n// Re-export the helper so consumers of the widget package can import it\n// when they want to share the URL-detection logic with their own code.\n"],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAimBlBC,GAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6EnBC,GAAuB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAynBvBC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8IpBC,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAqQtBC,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;A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","names":["src_exports","__export","LimeBundleElement","trackInputMode","StorefrontApiError","errors","e","DEFAULT_API_VERSION","resolveBuyer","buyer","hasInContext","config","createStorefrontClient","version","endpoint","query","variables","options","headers","mergedVariables","response","json","withInContext","_match","name","args","trimmed","extra","combined","BUNDLE_METAOBJECT_QUERY","SHOP_CUSTOM_CSS_QUERY","BUNDLES_FOR_PRODUCT_QUERY","CART_CREATE_MUTATION","CART_LINES_ADD_MUTATION","DEFAULT_PRODUCT_RULE","BundleParseError","message","reason","WIDGET_CONFIG_DEFAULTS","mergeWidgetConfig","raw","input","sanitizeDefaultTier","flattenWidgetConfig","config","CSS_VAR_MAP","PX_KEYS","applyWidgetConfigVars","el","flat","flatKey","cssVar","value","serialized","variantChevronUrl","ratioVars","thumbnailRatioVars","pickerRatioVars","strokeHex","ratio","VALID_BUNDLE_TYPES","ACTIVE_STATUSES","parseMetaobjectBundle","metaobjectId","fields","parseMetaobjectBundleStrict","err","fieldMap","f","title","rawBundleType","rawStatus","bundleType","status","startsAt","endsAt","now","start","end","products","resolveProducts","discountConfig","parseDiscountConfig","widgetConfig","parseWidgetConfig","base","parseSelectedVariantIds","parseNumericRecord","parseLinkGroup","parseMarketVisibility","parseMarketIds","parseVolumeTiers","parseIntField","parseProductRules","productsField","node","collectionField","parseJsonField","result","entry","x","key","record","k","v","num","out","pid","rule","min","max","minN","maxN","minClamped","maxClamped","parsed","id","isPrimary","variants","weight","t","parseOneVolumeTier","minQty","tier","options","resolveBundleQty","bundle","productId","variantId","vq","reportImpression","config","event","sendEvent","reportAddToCart","observeImpression","element","callback","observer","entries","entry","payload","url","body","r","consentGranted","isBrowser","hasConsent","isBrowser","consentGranted","shopifyGlobal","SESSION_COOKIE_NAME","SESSION_COOKIE_MAX_AGE","AB_COOKIE_MAX_AGE","abCookieName","linkGroupId","getLinkGroupAssignment","variants","options","validGids","v","existing","readABCookie","sessionId","getOrCreateSessionId","assigned","pickVariantFromWeights","writeABCookie","prefix","raw","c","value","decoded","variantMetaobjectId","name","generateUUID","id","r","bucket","fnv1a","cumulative","input","hash","i","MAX_CSS_LENGTH","BLOCKED_PATTERNS","SAFE_URL_VALUE","sanitizeCustomCss","css","_","hex","codePoint","pattern","label","urlPattern","urlMatch","urlValue","STYLE_ID_PREFIX","injectCustomCss","shopDomain","rawCss","sanitized","simpleHash","style","formatMoney","amount","currencyCode","num","UNIT_LABEL","formatUnitPrice","unitPrice","measurement","fallbackCurrency","amount","label","currency","formatted","formatMoney","value","measurementText","parseCents","num","formatCents","cents","currencyCode","percentageDiscountUnit","unitCents","percent","computeFixedPricing","bundle","productQuantities","showSaveBadge","discountType","discountValue","rows","totalCents","saleCents","product","variant","v","qty","lineCents","compareCents","perUnit","savingsCents","headerBadge","computeBundleSaleCents","discount","off","THUMB_PX","transformImageUrl","url","u","formatCountdown","msRemaining","totalSeconds","days","hours","minutes","seconds","pad","n","dropdown_exports","__export","computePosition","emptyTypeAheadState","handleKey","pushTypeAheadChar","DEFAULT_TRIGGER_MARGIN","trigger","viewportHeight","desiredHeight","margin","clip","upperBound","lowerBound","spaceBelow","spaceAbove","placement","maxHeight","offsetTop","PASSTHROUGH","firstEnabled","options","i","lastEnabled","nextEnabled","from","step","idx","prevEnabled","isPrintable","key","event","state","isOpen","activeIndex","selectedIndex","TYPE_AHEAD_RESET_MS","char","now","resetMs","buffer","newState","opt","picker_exports","findVariantByOptions","isOptionValueAvailable","toPickerVariant","variants","optionValues","match","j","optionIndex","selected","ok","o","isFulfillable","requiredQty","shouldShowLowStockBadge","threshold","enabled","maxAddableQuantity","variant","productMax","alreadyInBundle","stockCap","computePosition","emptyTypeAheadState","handleKey","pushTypeAheadChar","dropdown_exports","ITEM_HEIGHT_PX","LIST_PAD_Y","MAX_VISIBLE_ITEMS","openInstances","closeOutsideEvent","event","path","i","inst","onDocResize","docListenersAttached","attachDocumentListeners","detachDocumentListeners","readOptions","select","out","i","firstEnabled","opts","findScrollableAncestor","el","win","cur","overflowY","VARIANT_SELECT_CLASSES","BIND_SELECTOR","c","bindDropdown","slot","doc","rootNode","labelText","idBase","shell","trigger","listboxId","triggerLabel","chevron","listbox","modalOverlay","isOpen","activeIndex","typeAhead","emptyTypeAheadState","optionEls","instance","syncFromSelect","idx","li","setActive","newIndex","liTop","liBottom","visTop","visBottom","position","rect","desiredHeight","MAX_VISIBLE_ITEMS","ITEM_HEIGHT_PX","LIST_PAD_Y","scrollable","clip","r","result","computePosition","open","openInstances","selIdx","attachDocumentListeners","close","restoreFocus","detachDocumentListeners","commit","index","opt","event","applyAction","action","pushTypeAheadChar","_exhaustive","onKeydown","handleKey","onTriggerClick","onListboxClick","target","onListboxMousemove","onShellFocusout","active","onSelectChange","observer","onListboxMousedown","destroy","bindAllDropdowns","root","selects","instances","sel","inst","unbindAllDropdowns","renderCountdown","endsAtIso","parsed","parseIso","target","wrap","labelWrap","labelText","timer","intervalId","tick","msLeft","stop","formatCountdown","iso","buildCtaButton","label","button","labelSpan","spinnerSpan","setCtaLabel","text","el","tag","className","attrs","node","k","v","PLACEHOLDER_THUMB_SVG","renderFixedBundle","container","bundle","onAddToCart","onCleanup","wc","qtyFor","productId","variantId","resolveBundleQty","rows","oosCount","product","idx","row","buildRowState","currency","root","el","headerHandle","renderHeader","countdown","renderCountdown","list","rowHandles","rowState","handle","renderProductRow","updatePricing","pricingHandle","renderPricingRow","savingsBarHandle","renderSavingsBar","cta","renderCta","lines","r","bindAllDropdowns","unbindAllDropdowns","totalCents","sum","unit","parseCents","saleCents","computeSale","savingsCents","deriveHeaderBadge","productIndex","selectedVariantIds","merchantScoped","v","firstInStock","isFulfillable","eligibleVariants","isOos","selected","qty","header","content","title","subtitle","badgeEl","initialPricing","computeFixedPricing","badgeText","savings","dc","formatCents","state","lowStock","onVariantChange","rowEl","thumb","initialThumbImage","thumbImg","transformImageUrl","THUMB_PX","qtyBadgeRef","info","name","oosLabel","badge","prices","compare","priceEl","unitPriceEl","lowStockEl","applyVariantToRow","variant","cmp","unitText","formatUnitPrice","nextImage","required","shouldShowLowStockBadge","optionNames","o","productIdTail","optionSelects","resolveVariant","values","i","syncSelectsToVariant","sel","isValueAvailable","optionIndex","value","recomputeDisabled","opt","handleChange","s","groupsContainer","position","group","label","select","seen","sale","bar","amount","onClick","button","buildCtaButton","discount","off","perUnit","PLACEHOLDER_THUMB_SVG","PLUS_ICON_SVG","CLOSE_ICON_SVG","SEARCH_CLEAR_ICON_SVG","STEPPER_MINUS_SVG","STEPPER_PLUS_SVG","ruleFor","bundle","productId","DEFAULT_PRODUCT_RULE","alreadyInBundleFor","selections","variantId","sum","s","renderMixMatchBundle","container","onAddToCart","onCleanup","wc","currency","requiredQty","showQtySelector","eligible","buildEligibleProducts","e","root","el","header","renderHeader","countdown","renderCountdown","progress","renderProgress","slotsContainer","pricingSection","renderPricingSection","savingsBar","renderSavingsBar","cta","buildCtaButton","buildCartLines","modal","renderModal","product","variant","quantity","addSelection","unbindAllDropdowns","afterMutation","rule","cap","maxAddableQuantity","clamped","parseCents","formatUnitPrice","removeSlotAt","index","renderSlots","updateCta","totalSlots","i","selection","renderFilledSlot","renderEmptySlot","count","setCtaLabel","grouped","key","existing","line","content","title","discountType","discountValue","label","formatCents","badge","wrap","labels","remaining","track","fill","update","selected","pct","onClick","slot","thumb","text","onRemove","img","transformImageUrl","THUMB_PX","qtyBadge","info","linePrice","lineCompare","priceWrap","compare","priceEl","unitPrice","remove","showCompareAtPrice","prices","sale","totalCents","sel","saleCents","computeBundleSaleCents","labelEl","amount","savings","handlers","overlay","sanitizeId","modalHeader","modalTitle","closeBtn","close","searchInput","searchClearBtn","searchWrap","applySearch","list","empty","emptyText","live","rowsBuilt","productRows","buildRows","ep","availableVariants","firstAvailVariant","v","isFulfillable","currentVariant","productEl","initialThumbImage","thumbImg","countBadge","price","initialUnitText","lowStockBadge","refreshLowStockBadge","shouldShowLowStockBadge","computeStepperBounds","already","max","stepper","qtyGroup","renderQtyStepper","min","row","optionSelects","optionNames","o","productIdTail","resolveVariant","values","syncSelectsToVariant","isValueAvailable","optionIndex","value","recomputeDisabled","opt","handleChange","next","nextUnitText","nextImage","groupsContainer","name","position","group","select","seen","variantLabel","soldOut","addBtn","refreshAddState","rowDisabled","actions","qty","nextFocus","lastFocused","bindAllDropdowns","refreshCounts","query","visibleCount","match","onKeydown","trapFocus","isOpen","open","onOverlayClick","r","opts","minus","valueEl","plus","current","clamp","externallyDisabled","n","b","paint","disabled","oosBehavior","result","available","isOos","focusables","first","last","active","gid","renderVolumeBundle","container","bundle","onAddToCart","onCleanup","wc","product","minTierQty","variant","v","isFulfillable","basePriceCents","parseCents","currency","discountType","resolved","tier","index","perUnit","amt","pct","discount","bestTierIndex","pickBestTierIndex","selectedIndex","clamp","popularIndex","root","el","renderHeader","countdown","renderCountdown","tierGroup","r","tierEl","renderTierCard","selectTier","e","delta","next","pricingEl","renderPricingRow","savingsBarEl","renderSavingsBar","cta","renderCta","shouldShowLowStockBadge","lowStock","idx","card","i","newPricing","newBar","badgeEl","label","badgeFor","header","content","title","badge","isSelected","popularLabel","showComparePrice","showPerUnitPrice","radio","grid","price","compare","formatCents","each","unit","showItemCount","showCompareAtPrice","totalCents","undiscountedCents","savings","row","count","prices","sale","bar","labelEl","amount","onClick","isAvailable","button","buildCtaButton","bestSavings","bestIndex","n","min","max","NAV_KEYS","targets","listenersAttached","setAll","on","off","el","onKeyDown","onPointerDown","attachListeners","detachListeners","trackInputMode","target","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_DROPDOWN_CSS","BUNDLE_SKELETON_CSS","cartStorageKey","shopDomain","resolveProductHandle","explicit","meta","match","LimeBundleElement","cleanup","name","oldValue","newValue","query","hasInContext","withInContext","bundle","controller","client","createStorefrontClient","bundlePromise","singleBundleMode","handle","cssPromise","SHOP_CUSTOM_CSS_QUERY","css","injectCustomCss","sanitized","sanitizeCustomCss","err","bundleLookupByMetaobjectId","b","results","lg","assignment","getLinkGroupAssignment","swap","signal","data","BUNDLE_METAOBJECT_QUERY","parsed","parseMetaobjectBundle","productHandle","BUNDLES_FOR_PRODUCT_QUERY","refs","bundles","ref","lines","ev","allowDefault","storage","key","existingCartId","checkoutUrl","payload","CART_LINES_ADD_MUTATION","CART_CREATE_MUTATION","quantity","sum","l","totalPrice","line","variant","v","price","reportAddToCart","style","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_DROPDOWN_CSS","customStyle","container","applyWidgetConfigVars","trackInputMode","dispatch","registerCleanup","fn","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","first","element","observeImpression","reportImpression","BUNDLE_SKELETON_CSS","message","LimeBundleElement"]}