@lime-bundles/widget 2.2.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +36 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -12
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +33 -24
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../core/src/storefront-api/client.ts","../../core/src/storefront-api/queries.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/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","../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/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 };\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\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\n// Keep in sync with CLAUDE.md → Shopify API Version Alignment.\nconst DEFAULT_API_VERSION = \"2025-10\";\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 const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables }),\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 */\nimport type { MetaobjectField } from \"./types\";\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 * 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\nexport interface ProductListConfig {\n textColor: string;\n imageBorderWidth: number;\n imageBorderColor: string;\n imageBorderRadius: number;\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\n // --- Mix-match picker section ---\n showSearch: 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 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\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\nexport interface FixedBundleData extends BundleBase {\n bundleType: \"fixed\";\n}\n\nexport interface VolumeBundleData extends BundleBase {\n bundleType: \"volume\";\n volumeTiers: VolumeTier[];\n}\n\nexport interface MixMatchBundleData extends BundleBase {\n bundleType: \"mix_match\";\n minQuantity: number | null;\n maxQuantity: number | null;\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: \"#FFFFFF\",\n headerStyle: \"gradient\",\n gradientStart: \"#C62828\",\n gradientEnd: \"#AD1457\",\n saveBadgeBgColor: \"#FFFFFF\",\n saveBadgeTextColor: \"#AD1457\",\n saveBadgeBorderColor: \"#FFFFFF\",\n saveBadgeBorderWidth: 0,\n saveBadgeBorderRadius: 8,\n countdownBgColor: \"#FFF0F3\",\n countdownTextColor: \"#C62828\",\n },\n layout: {\n backgroundColor: \"#FFFFFF\",\n borderColor: \"#E5E5E5\",\n borderWidth: 1,\n borderRadius: 12,\n },\n productList: {\n textColor: \"#1A1A1A\",\n imageBorderWidth: 0,\n imageBorderColor: \"#E5E5E5\",\n imageBorderRadius: 8,\n variantBorderWidth: 1,\n variantBorderColor: \"#E5E5E5\",\n variantBorderRadius: 8,\n showPrice: true,\n showCompareAtPrice: true,\n showCountBubble: true,\n countBubbleBgColor: \"#1A1A1A\",\n countBubbleTextColor: \"#FFFFFF\",\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: \"#1A1A1A\",\n buttonTextColor: \"#FFFFFF\",\n borderWidth: 0,\n borderColor: \"#1A1A1A\",\n borderRadius: 12,\n },\n savingsBar: {\n visible: true,\n bgColor: \"#EDFBF1\",\n textColor: \"#2DB554\",\n borderWidth: 0,\n borderColor: \"#2DB554\",\n borderRadius: 8,\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\n showSearch: true,\n pickerBgColor: \"#FFFFFF\",\n pickerTextColor: \"#1A1A1A\",\n pickerBorderWidth: 0,\n pickerBorderColor: \"#E5E5E5\",\n pickerBorderRadius: 16,\n pickerSearchBorderWidth: 1,\n pickerSearchBorderColor: \"#E5E5E5\",\n pickerSearchBorderRadius: 8,\n pickerProductBorderWidth: 0,\n pickerProductBorderColor: \"#E5E5E5\",\n pickerProductBorderRadius: 8,\n pickerShowCountBubble: true,\n pickerCountBubbleBgColor: \"#1A1A1A\",\n pickerCountBubbleTextColor: \"#FFFFFF\",\n pickerAddBgColor: \"#1A1A1A\",\n pickerAddLabelColor: \"#FFFFFF\",\n pickerAddBorderWidth: 0,\n pickerAddBorderColor: \"#1A1A1A\",\n pickerAddBorderRadius: 8,\n pickerVariantBorderWidth: 1,\n pickerVariantBorderColor: \"#1A1A1A\",\n pickerVariantBorderRadius: 8,\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 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 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 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 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};\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]);\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 // 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","/**\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 };\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: parseIntField(fieldMap, \"max_quantity\", { min: 1 }),\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 // Preserve zero (merchant-set opt-out) — renderers filter zero-qty\n // rows. Negative values are rejected as malformed.\n if (Number.isFinite(num) && num >= 0) record[k] = num;\n }\n return record;\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 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","// 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 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 * never calls `/api/ab-assign` — no cookie, no network fetch, no tracking.\n */\n\nlet consentGranted = false;\n\n/**\n * Merchant signals explicit opt-in (true) or revocation (false) for\n * A/B-related cookie and analytics writes.\n */\nexport function setConsent(allowed: boolean): void {\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).\n */\nexport function hasConsent(): boolean {\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. Same with the server-side `expectedVariant()` check on\n * `/api/ab-assign`.\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 `__Host-_lb_ab_${bundleId}`\n * cookie and fires `/api/ab-assign` for server-side persistence.\n */\nexport async function getABTestAssignment(\n appUrl: string,\n shopDomain: string,\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 // Fire-and-forget server-side assignment persistence.\n try {\n fetch(`${appUrl}/api/ab-assign`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n shopDomain,\n testId,\n sessionId,\n variant,\n }),\n }).catch(() => {});\n } catch {\n // Silently ignore — persistence is best-effort.\n }\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.\n * Must match the server-side expectedVariant() in ab-test-assignment.server.ts.\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 */\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","// 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","// 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 type {\n CartLineInput,\n FixedBundleData,\n Product,\n ProductVariant,\n} from \"@lime-bundles/core\";\nimport {\n computeFixedPricing,\n formatCents,\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 — honours the merchant's optional\n // `variantQuantities` override, falling back to the product-level qty.\n const qtyFor = (productId: string, variantId: string): number => {\n const vq = bundle.variantQuantities[variantId];\n if (vq !== undefined) return vq;\n return bundle.productQuantities[productId] ?? 1;\n };\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(rowState, currency, qtyFor, () => {\n // Variant change → recompute pricing.\n updatePricing();\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 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 that are available AND (no merchant filter OR in the filter).\n const available = product.variants.nodes.filter(\n (v) => v.availableForSale,\n );\n const eligibleVariants =\n selectedVariantIds && selectedVariantIds.length > 0\n ? available.filter((v) => selectedVariantIds.includes(v.id))\n : available;\n\n const isOos = eligibleVariants.length === 0;\n const selected = eligibleVariants[0] ?? null;\n\n const productQty = bundle.productQuantities[product.id] ?? 1;\n const variantQty = selected\n ? bundle.variantQuantities[selected.id]\n : undefined;\n const qty = variantQty ?? productQty;\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 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 // Thumbnail + qty badge\n const thumb = el(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n if (state.product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = transformImageUrl(state.product.featuredImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n img.alt = state.product.featuredImage.altText ?? state.product.title;\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 // Qty badge — reference is captured so on-variant-change can live-update\n // the count when a merchant set per-variant quantity overrides.\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 // 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 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 };\n\n applyVariantToRow(state.selected);\n\n // Variant dropdown when more than one eligible variant exists.\n if (state.eligibleVariants.length > 1) {\n const select = document.createElement(\"select\");\n select.className = \"lb-bundle-variant-select\";\n select.setAttribute(\"data-variant-select\", \"\");\n select.setAttribute(\n \"aria-label\",\n `Select variant for ${state.product.title}`,\n );\n state.eligibleVariants.forEach((variant) => {\n const opt = document.createElement(\"option\");\n opt.value = variant.id;\n opt.textContent = variant.title;\n if (variant.id === state.selected?.id) opt.selected = true;\n select.appendChild(opt);\n });\n select.addEventListener(\"change\", () => {\n const variant = state.eligibleVariants.find(\n (v) => v.id === select.value,\n );\n if (!variant) return;\n state.selected = variant;\n // Honour per-variant quantity overrides so the thumbnail badge\n // and the bundle total track the merchant's config.\n state.qty = qtyFor(state.product.id, variant.id);\n if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);\n applyVariantToRow(variant);\n onVariantChange();\n });\n info.appendChild(select);\n } else if (\n state.eligibleVariants.length === 1 &&\n state.product.variants.nodes.length > 1\n ) {\n // Single allowed variant but the product has multiple — render a\n // read-only badge so the customer sees which one's going in the bundle.\n const badge = el(\"span\", \"lb-bundle-variant-badge\");\n badge.textContent = state.eligibleVariants[0].title;\n info.appendChild(badge);\n }\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 `minQuantity` empty slots with a live progress bar.\n * 2. Clicking a slot opens the picker modal with the eligible products.\n * 3. Inside the modal, each product has a quantity stepper and a count\n * bubble. Adjusting stepper values adds or removes slots.\n * 4. Progress bar, slot contents, pricing, and CTA update live.\n * 5. When `minQuantity` is reached, the CTA unlocks. Customer clicks,\n * cart lines dispatch.\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 ProductVariant,\n} from \"@lime-bundles/core\";\nimport { computeBundleSaleCents, formatCents, parseCents } from \"./pricing\";\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\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}\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\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 maxQty = bundle.maxQuantity ?? requiredQty;\n\n // Build eligible-products list, honoring outOfStockBehavior.\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 minQuantity from\n // in-stock products, don't render the widget at all. Matches 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 \"data-max-quantity\": String(maxQty),\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\n ? renderSavingsBar()\n : null;\n if (savingsBar) root.appendChild(savingsBar.el);\n\n // --- Price placeholder (shown until first selection) ---\n const placeholder = el(\"div\", \"lb-mix-match__price-placeholder\", {\n \"data-price-placeholder\": \"\",\n });\n const placeholderText = el(\n \"span\",\n \"lb-mix-match__price-placeholder-text\",\n );\n placeholderText.textContent = `Select ${requiredQty} items to see price`;\n placeholder.appendChild(placeholderText);\n root.appendChild(placeholder);\n\n // --- Modal overlay ---\n const modal = renderModal(bundle, eligible, currency, {\n showSearch: wc.showSearch,\n onAdd: (product, variant) => addSelection(product, variant),\n onRemove: (productId, variantId) => removeSelection(productId, variantId),\n countFor: (productId, variantId) =>\n selections.filter(\n (s) => s.productId === productId && s.variantId === variantId,\n ).length,\n isOverMax: () => selections.length >= maxQty,\n });\n root.appendChild(modal.el);\n\n // --- CTA ---\n const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);\n cta.disabled = true;\n cta.addEventListener(\"click\", () => {\n if (cta.disabled) return;\n const lines: CartLineInput[] = selections.map((s) => ({\n merchandiseId: s.variantId,\n quantity: 1,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n }));\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 // Seed slot 0 with the first eligible in-stock product so the widget\n // opens \"live\" — matches the Liquid theme block's default behaviour.\n // Customers can swap or remove via the picker as usual.\n const firstEligible = eligible.find((ep) => !ep.isOos);\n const firstVariant =\n firstEligible?.firstAvailableVariant ?? firstEligible?.variants[0];\n if (firstEligible && firstVariant) {\n selections.push({\n productId: firstEligible.product.id,\n productTitle: firstEligible.product.title,\n variantId: firstVariant.id,\n variantTitle: firstVariant.title,\n imageUrl: firstEligible.product.featuredImage?.url ?? null,\n priceCents: parseCents(firstVariant.price.amount),\n compareCents: firstVariant.compareAtPrice\n ? parseCents(firstVariant.compareAtPrice.amount)\n : null,\n });\n }\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(product: Product, variant: ProductVariant) {\n if (selections.length >= maxQty) return;\n selections.push({\n productId: product.id,\n productTitle: product.title,\n variantId: variant.id,\n variantTitle: variant.title,\n imageUrl: product.featuredImage?.url ?? null,\n priceCents: parseCents(variant.price.amount),\n compareCents: variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null,\n });\n afterMutation();\n }\n\n function removeSelection(productId: string, variantId: string) {\n const idx = selections.findIndex(\n (s) => s.productId === productId && s.variantId === variantId,\n );\n if (idx === -1) return;\n selections.splice(idx, 1);\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 placeholder.style.display = selections.length === 0 ? \"\" : \"none\";\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// --- 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 });\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 });\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 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 priceWrap = el(\"span\", \"lb-mix-match__filled-price\");\n if (\n selection.compareCents &&\n selection.compareCents > selection.priceCents\n ) {\n const compare = el(\"span\", \"lb-mix-match__filled-compare\");\n compare.textContent = formatCents(selection.compareCents, currency);\n priceWrap.appendChild(compare);\n }\n const priceEl = document.createElement(\"span\");\n priceEl.textContent = formatCents(selection.priceCents, currency);\n priceWrap.appendChild(priceEl);\n info.appendChild(priceWrap);\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((s, sel) => s + sel.priceCents, 0);\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((s, sel) => s + sel.priceCents, 0);\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 onAdd: (product: Product, variant: ProductVariant) => void;\n onRemove: (productId: string, variantId: string) => void;\n countFor: (productId: string, variantId: string) => number;\n isOverMax: () => boolean;\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 updateCount: () => void;\n }> = [];\n\n function buildRows() {\n if (rowsBuilt) return;\n rowsBuilt = true;\n list.innerHTML = \"\";\n\n eligible.forEach((ep) => {\n const variant = ep.firstAvailableVariant ?? ep.variants[0];\n if (!variant) return;\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 if (ep.product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = transformImageUrl(ep.product.featuredImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n img.alt = ep.product.featuredImage.altText ?? ep.product.title;\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 productEl.appendChild(thumb);\n\n const info = el(\"div\", \"lb-mix-match__modal-product-info\");\n const title = el(\"span\", \"lb-mix-match__modal-product-title\");\n title.textContent = ep.product.title;\n info.appendChild(title);\n const price = el(\"span\", \"lb-mix-match__modal-product-price\");\n price.textContent = formatCents(parseCents(variant.price.amount), currency);\n info.appendChild(price);\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 if (!ep.isOos) {\n const addBtn = document.createElement(\"button\");\n addBtn.type = \"button\";\n addBtn.className = \"lb-mix-match__modal-add\";\n addBtn.setAttribute(\"aria-label\", `Add ${ep.product.title}`);\n addBtn.innerHTML = PLUS_ICON_SVG;\n const countBadge = el(\"span\", \"lb-bundle-qty-badge\");\n countBadge.style.display = \"none\";\n addBtn.appendChild(countBadge);\n addBtn.addEventListener(\"click\", () => {\n if (handlers.isOverMax()) return;\n handlers.onAdd(ep.product, variant);\n });\n productEl.appendChild(addBtn);\n\n // Right-click / long-press / standard remove: plain X shown when count > 0\n const removeBtn = document.createElement(\"button\");\n removeBtn.type = \"button\";\n removeBtn.className = \"lb-mix-match__slot-remove\";\n removeBtn.setAttribute(\"aria-label\", `Remove one ${ep.product.title}`);\n removeBtn.style.display = \"none\";\n removeBtn.innerHTML = CLOSE_ICON_SVG;\n removeBtn.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handlers.onRemove(ep.product.id, variant.id);\n });\n productEl.appendChild(removeBtn);\n\n productRows.push({\n el: productEl,\n product: ep.product,\n variant,\n updateCount: () => {\n const count = handlers.countFor(ep.product.id, variant.id);\n if (count > 0) {\n countBadge.textContent = String(count);\n countBadge.style.display = \"\";\n removeBtn.style.display = \"\";\n } else {\n countBadge.style.display = \"none\";\n removeBtn.style.display = \"none\";\n }\n },\n });\n } else {\n // OOS items still need a productRows entry so search filters them\n // out. There's no add/remove UI and nothing to update on count\n // change, so updateCount is a no-op.\n productRows.push({\n el: productEl,\n product: ep.product,\n variant,\n updateCount: () => {},\n });\n }\n\n list.appendChild(productEl);\n });\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.isOverMax()) 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.updateCount());\n }\n\n return { el: overlay, open, close, refreshCounts };\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 available = product.variants.nodes.filter((v) => v.availableForSale);\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","/**\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 { 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 const variant = product?.variants.nodes.find((v) => v.availableForSale);\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 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 isAvailable = product?.variants.nodes.some((v) => v.availableForSale);\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 * 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-text-muted: #666666;\n --lb-thumbnail-bg: #F0F0F0;\n --lb-widget-pad: 20px;\n --lb-progress-color: var(--lb-primary-color);\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 rgba(0, 0, 0, 0.06);\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/* Adjacent widget spacing — separates multiple bundles on the same product page */\n.lb-bundle-widget + .lb-bundle-widget {\n margin-top: 24px;\n padding-top: 24px;\n border-top: 1px solid var(--lb-border);\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: var(--lb-text-muted);\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: 12px;\n padding: 12px 0;\n}\n\n.lb-bundle-thumbnail {\n position: relative;\n width: 48px;\n height: 48px;\n min-width: 48px;\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: 100%;\n object-fit: cover;\n}\n\n.lb-bundle-thumbnail svg {\n width: 28px;\n height: 28px;\n color: #BBBBBB;\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\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: 12px;\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: 12px;\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.lb-bundle-variant-badge {\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 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text-muted);\n margin-top: 8px;\n}\n\n.lb-bundle-quantity {\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text-muted);\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: 12px 16px;\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: 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: #BBBBBB;\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/* 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}\n\n/* Fixed bundles: product rows */\n.lb-fixed .lb-bundle-product-row {\n gap: 16px;\n align-items: center;\n}\n\n/* Fixed bundles: larger thumbnails */\n.lb-fixed .lb-bundle-thumbnail {\n width: 60px;\n height: 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.lb-bundle-variant-select {\n margin-top: 8px;\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 max-width: 100%;\n}\n\n.lb-bundle-variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n}\n`;\nexport const BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles — Mix & Match styles */\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 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 border: var(--lb-image-border-width) dashed color-mix(in srgb, var(--lb-text) 20%, 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.lb-mix-match .lb-bundle-thumbnail {\n width: 60px;\n height: 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 .lb-mix-match__filled-variant {\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.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}\n\n/* Price placeholder */\n.lb-mix-match__price-placeholder {\n padding: 4px 0 16px;\n text-align: center;\n}\n\n.lb-mix-match__price-placeholder-text {\n font-size: 16px;\n color: var(--lb-text);\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: 480px;\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: 2px;\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 {\n outline: none;\n box-shadow: 0 0 0 1px var(--lb-primary-color);\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: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 0;\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: 48px;\n height: 48px;\n min-width: 48px;\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: #F0F0F0;\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: 100%;\n object-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: 4px 0 0;\n}\n\n.lb-mix-match__variant-select {\n font-size: 12px;\n margin: 4px 0 0;\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 max-width: 120px;\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}\n\n.lb-mix-match__modal-add {\n padding: 8px 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 white-space: nowrap;\n flex-shrink: 0;\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}\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.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\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}\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: color-mix(in srgb, var(--lb-tier-border-color) 50%, black);\n}\n\n.lb-volume__tier:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\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: 12px;\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: 12px;\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\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} 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 {\n BUNDLE_BASE_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 ];\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 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 });\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 also persists the bucket\n * via a first-party cookie + POSTs to /api/ab-assign for server-side\n * analytics.\n */\n private async applyABVariants(): Promise<void> {\n if (!this.appUrl || 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 this.appUrl,\n this.shopDomain,\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 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 ? [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 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) 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 ].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 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,EAgelBC,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,EA0DnBC,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,EAuevBC,GAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","names":["src_exports","__export","LimeBundleElement","StorefrontApiError","errors","e","DEFAULT_API_VERSION","createStorefrontClient","config","version","endpoint","query","variables","options","headers","response","json","BUNDLE_METAOBJECT_QUERY","SHOP_CUSTOM_CSS_QUERY","BUNDLES_FOR_PRODUCT_QUERY","CART_CREATE_MUTATION","CART_LINES_ADD_MUTATION","BundleParseError","message","reason","WIDGET_CONFIG_DEFAULTS","mergeWidgetConfig","raw","input","sanitizeDefaultTier","flattenWidgetConfig","CSS_VAR_MAP","PX_KEYS","applyWidgetConfigVars","el","flat","flatKey","cssVar","value","serialized","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","parseVolumeTiers","parseIntField","productsField","node","collectionField","parseJsonField","result","entry","x","key","record","k","v","num","overrides","description","discountType","discountValueRaw","abTiers","t","parseOneVolumeTier","minQty","tier","applyABVariantB","bundle","overrides","title","description","discountConfig","reportImpression","config","event","sendEvent","reportAddToCart","observeImpression","element","callback","observer","entries","entry","payload","url","body","r","consentGranted","hasConsent","consentGranted","shopifyGlobal","SESSION_COOKIE_NAME","SESSION_COOKIE_MAX_AGE","AB_COOKIE_MAX_AGE","abCookieName","bundleId","getABTestAssignment","appUrl","shopDomain","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","rawCss","sanitized","simpleHash","style","parseCents","amount","num","formatCents","cents","currencyCode","percentageDiscountUnit","unitCents","percent","computeFixedPricing","bundle","productQuantities","showSaveBadge","discountType","discountValue","rows","totalCents","saleCents","product","variant","v","qty","lineCents","compareCents","perUnit","savingsCents","currency","headerBadge","computeBundleSaleCents","discount","off","THUMB_PX","transformImageUrl","url","u","formatCountdown","msRemaining","totalSeconds","days","hours","minutes","seconds","pad","n","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","vq","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","totalCents","sum","unit","parseCents","saleCents","computeSale","savingsCents","deriveHeaderBadge","productIndex","selectedVariantIds","available","v","eligibleVariants","isOos","selected","productQty","qty","header","content","title","subtitle","badgeEl","initialPricing","computeFixedPricing","badgeText","savings","dc","formatCents","state","onVariantChange","rowEl","thumb","img","transformImageUrl","THUMB_PX","qtyBadgeRef","info","name","oosLabel","prices","compare","priceEl","applyVariantToRow","variant","cmp","select","opt","badge","label","sale","bar","amount","onClick","button","buildCtaButton","discount","off","perUnit","PLACEHOLDER_THUMB_SVG","PLUS_ICON_SVG","CLOSE_ICON_SVG","SEARCH_CLEAR_ICON_SVG","renderMixMatchBundle","container","bundle","onAddToCart","onCleanup","wc","currency","requiredQty","maxQty","eligible","buildEligibleProducts","e","selections","root","el","header","renderHeader","countdown","renderCountdown","progress","renderProgress","slotsContainer","pricingSection","renderPricingSection","savingsBar","renderSavingsBar","placeholder","placeholderText","modal","renderModal","product","variant","addSelection","productId","variantId","removeSelection","s","cta","buildCtaButton","lines","firstEligible","ep","firstVariant","parseCents","afterMutation","idx","removeSlotAt","index","renderSlots","updateCta","totalSlots","i","selection","renderFilledSlot","renderEmptySlot","count","setCtaLabel","content","title","discountType","discountValue","label","formatCents","badge","wrap","labels","remaining","track","fill","update","selected","pct","onClick","slot","thumb","text","onRemove","img","transformImageUrl","THUMB_PX","info","priceWrap","compare","priceEl","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","productEl","price","soldOut","addBtn","countBadge","removeBtn","refreshCounts","query","visibleCount","row","match","lastFocused","onKeydown","trapFocus","isOpen","open","onOverlayClick","r","oosBehavior","result","seen","available","v","isOos","focusables","first","last","active","gid","renderVolumeBundle","container","bundle","onAddToCart","onCleanup","wc","variant","v","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","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","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_SKELETON_CSS","cartStorageKey","shopDomain","resolveProductHandle","explicit","meta","match","LimeBundleElement","cleanup","name","oldValue","newValue","controller","client","createStorefrontClient","bundlePromise","singleBundleMode","handle","cssPromise","SHOP_CUSTOM_CSS_QUERY","css","injectCustomCss","sanitized","sanitizeCustomCss","err","results","bundle","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","p","v","price","reportAddToCart","style","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","customStyle","container","applyWidgetConfigVars","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/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","../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/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 };\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\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\n// Keep in sync with CLAUDE.md → Shopify API Version Alignment.\nconst DEFAULT_API_VERSION = \"2025-10\";\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 const response = await fetch(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables }),\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 */\nimport type { MetaobjectField } from \"./types\";\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\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 * 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\nexport interface ProductListConfig {\n textColor: string;\n imageBorderWidth: number;\n imageBorderColor: string;\n imageBorderRadius: number;\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\n // --- Mix-match picker section ---\n showSearch: 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 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\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\nexport interface FixedBundleData extends BundleBase {\n bundleType: \"fixed\";\n}\n\nexport interface VolumeBundleData extends BundleBase {\n bundleType: \"volume\";\n volumeTiers: VolumeTier[];\n}\n\nexport interface MixMatchBundleData extends BundleBase {\n bundleType: \"mix_match\";\n minQuantity: number | null;\n maxQuantity: number | null;\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: \"#FFFFFF\",\n headerStyle: \"gradient\",\n gradientStart: \"#C62828\",\n gradientEnd: \"#AD1457\",\n saveBadgeBgColor: \"#FFFFFF\",\n saveBadgeTextColor: \"#AD1457\",\n saveBadgeBorderColor: \"#FFFFFF\",\n saveBadgeBorderWidth: 0,\n saveBadgeBorderRadius: 8,\n countdownBgColor: \"#FFF0F3\",\n countdownTextColor: \"#C62828\",\n },\n layout: {\n backgroundColor: \"#FFFFFF\",\n borderColor: \"#E5E5E5\",\n borderWidth: 1,\n borderRadius: 12,\n },\n productList: {\n textColor: \"#1A1A1A\",\n imageBorderWidth: 0,\n imageBorderColor: \"#E5E5E5\",\n imageBorderRadius: 8,\n variantBorderWidth: 1,\n variantBorderColor: \"#E5E5E5\",\n variantBorderRadius: 8,\n showPrice: true,\n showCompareAtPrice: true,\n showCountBubble: true,\n countBubbleBgColor: \"#1A1A1A\",\n countBubbleTextColor: \"#FFFFFF\",\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: \"#1A1A1A\",\n buttonTextColor: \"#FFFFFF\",\n borderWidth: 0,\n borderColor: \"#1A1A1A\",\n borderRadius: 12,\n },\n savingsBar: {\n visible: true,\n bgColor: \"#EDFBF1\",\n textColor: \"#2DB554\",\n borderWidth: 0,\n borderColor: \"#2DB554\",\n borderRadius: 8,\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\n showSearch: true,\n pickerBgColor: \"#FFFFFF\",\n pickerTextColor: \"#1A1A1A\",\n pickerBorderWidth: 0,\n pickerBorderColor: \"#E5E5E5\",\n pickerBorderRadius: 16,\n pickerSearchBorderWidth: 1,\n pickerSearchBorderColor: \"#E5E5E5\",\n pickerSearchBorderRadius: 8,\n pickerProductBorderWidth: 0,\n pickerProductBorderColor: \"#E5E5E5\",\n pickerProductBorderRadius: 8,\n pickerShowCountBubble: true,\n pickerCountBubbleBgColor: \"#1A1A1A\",\n pickerCountBubbleTextColor: \"#FFFFFF\",\n pickerAddBgColor: \"#1A1A1A\",\n pickerAddLabelColor: \"#FFFFFF\",\n pickerAddBorderWidth: 0,\n pickerAddBorderColor: \"#1A1A1A\",\n pickerAddBorderRadius: 8,\n pickerVariantBorderWidth: 1,\n pickerVariantBorderColor: \"#1A1A1A\",\n pickerVariantBorderRadius: 8,\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 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 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 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 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};\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]);\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 // 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","/**\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 };\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: parseIntField(fieldMap, \"max_quantity\", { min: 1 }),\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 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 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 * never calls `/api/ab-assign` — no cookie, no network fetch, no tracking.\n */\n\nlet consentGranted = false;\n\n/**\n * Merchant signals explicit opt-in (true) or revocation (false) for\n * A/B-related cookie and analytics writes.\n */\nexport function setConsent(allowed: boolean): void {\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).\n */\nexport function hasConsent(): boolean {\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. Same with the server-side `expectedVariant()` check on\n * `/api/ab-assign`.\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 `__Host-_lb_ab_${bundleId}`\n * cookie and fires `/api/ab-assign` for server-side persistence.\n */\nexport async function getABTestAssignment(\n appUrl: string,\n shopDomain: string,\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 // Fire-and-forget server-side assignment persistence.\n try {\n fetch(`${appUrl}/api/ab-assign`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n shopDomain,\n testId,\n sessionId,\n variant,\n }),\n }).catch(() => {});\n } catch {\n // Silently ignore — persistence is best-effort.\n }\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.\n * Must match the server-side expectedVariant() in ab-test-assignment.server.ts.\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 */\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","// 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","// 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 type CartLineInput,\n type FixedBundleData,\n type Product,\n type ProductVariant,\n} from \"@lime-bundles/core\";\nimport {\n computeFixedPricing,\n formatCents,\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(rowState, currency, qtyFor, () => {\n // Variant change → recompute pricing.\n updatePricing();\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 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 that are available AND (no merchant filter OR in the filter).\n const available = product.variants.nodes.filter(\n (v) => v.availableForSale,\n );\n const eligibleVariants =\n selectedVariantIds && selectedVariantIds.length > 0\n ? available.filter((v) => selectedVariantIds.includes(v.id))\n : available;\n\n const isOos = eligibleVariants.length === 0;\n const selected = eligibleVariants[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 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 // Thumbnail + qty badge\n const thumb = el(\"div\", \"lb-bundle-thumbnail\", { \"data-thumbnail\": \"\" });\n if (state.product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = transformImageUrl(state.product.featuredImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n img.alt = state.product.featuredImage.altText ?? state.product.title;\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 // Qty badge — reference is captured so on-variant-change can live-update\n // the count when a merchant set per-variant quantity overrides.\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 // 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 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 };\n\n applyVariantToRow(state.selected);\n\n // Variant dropdown when more than one eligible variant exists.\n if (state.eligibleVariants.length > 1) {\n const select = document.createElement(\"select\");\n select.className = \"lb-bundle-variant-select\";\n select.setAttribute(\"data-variant-select\", \"\");\n select.setAttribute(\n \"aria-label\",\n `Select variant for ${state.product.title}`,\n );\n state.eligibleVariants.forEach((variant) => {\n const opt = document.createElement(\"option\");\n opt.value = variant.id;\n opt.textContent = variant.title;\n if (variant.id === state.selected?.id) opt.selected = true;\n select.appendChild(opt);\n });\n select.addEventListener(\"change\", () => {\n const variant = state.eligibleVariants.find(\n (v) => v.id === select.value,\n );\n if (!variant) return;\n state.selected = variant;\n // Honour per-variant quantity overrides so the thumbnail badge\n // and the bundle total track the merchant's config.\n state.qty = qtyFor(state.product.id, variant.id);\n if (qtyBadgeRef) qtyBadgeRef.textContent = String(state.qty);\n applyVariantToRow(variant);\n onVariantChange();\n });\n info.appendChild(select);\n } else if (\n state.eligibleVariants.length === 1 &&\n state.product.variants.nodes.length > 1\n ) {\n // Single allowed variant but the product has multiple — render a\n // read-only badge so the customer sees which one's going in the bundle.\n const badge = el(\"span\", \"lb-bundle-variant-badge\");\n badge.textContent = state.eligibleVariants[0].title;\n info.appendChild(badge);\n }\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 `minQuantity` empty slots with a live progress bar.\n * 2. Clicking a slot opens the picker modal with the eligible products.\n * 3. Inside the modal, each product has a quantity stepper and a count\n * bubble. Adjusting stepper values adds or removes slots.\n * 4. Progress bar, slot contents, pricing, and CTA update live.\n * 5. When `minQuantity` is reached, the CTA unlocks. Customer clicks,\n * cart lines dispatch.\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 ProductVariant,\n} from \"@lime-bundles/core\";\nimport { computeBundleSaleCents, formatCents, parseCents } from \"./pricing\";\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\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}\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\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 maxQty = bundle.maxQuantity ?? requiredQty;\n\n // Build eligible-products list, honoring outOfStockBehavior.\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 minQuantity from\n // in-stock products, don't render the widget at all. Matches 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 \"data-max-quantity\": String(maxQty),\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\n ? renderSavingsBar()\n : null;\n if (savingsBar) root.appendChild(savingsBar.el);\n\n // --- Price placeholder (shown until first selection) ---\n const placeholder = el(\"div\", \"lb-mix-match__price-placeholder\", {\n \"data-price-placeholder\": \"\",\n });\n const placeholderText = el(\n \"span\",\n \"lb-mix-match__price-placeholder-text\",\n );\n placeholderText.textContent = `Select ${requiredQty} items to see price`;\n placeholder.appendChild(placeholderText);\n root.appendChild(placeholder);\n\n // --- Modal overlay ---\n const modal = renderModal(bundle, eligible, currency, {\n showSearch: wc.showSearch,\n onAdd: (product, variant) => addSelection(product, variant),\n onRemove: (productId, variantId) => removeSelection(productId, variantId),\n countFor: (productId, variantId) =>\n selections.filter(\n (s) => s.productId === productId && s.variantId === variantId,\n ).length,\n isOverMax: () => selections.length >= maxQty,\n });\n root.appendChild(modal.el);\n\n // --- CTA ---\n const cta = buildCtaButton(`Select ${requiredQty} items to unlock`);\n cta.disabled = true;\n cta.addEventListener(\"click\", () => {\n if (cta.disabled) return;\n const lines: CartLineInput[] = selections.map((s) => ({\n merchandiseId: s.variantId,\n quantity: 1,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n }));\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 // Seed slot 0 with the first eligible in-stock product so the widget\n // opens \"live\" — matches the Liquid theme block's default behaviour.\n // Customers can swap or remove via the picker as usual.\n const firstEligible = eligible.find((ep) => !ep.isOos);\n const firstVariant =\n firstEligible?.firstAvailableVariant ?? firstEligible?.variants[0];\n if (firstEligible && firstVariant) {\n selections.push({\n productId: firstEligible.product.id,\n productTitle: firstEligible.product.title,\n variantId: firstVariant.id,\n variantTitle: firstVariant.title,\n imageUrl: firstEligible.product.featuredImage?.url ?? null,\n priceCents: parseCents(firstVariant.price.amount),\n compareCents: firstVariant.compareAtPrice\n ? parseCents(firstVariant.compareAtPrice.amount)\n : null,\n });\n }\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(product: Product, variant: ProductVariant) {\n if (selections.length >= maxQty) return;\n selections.push({\n productId: product.id,\n productTitle: product.title,\n variantId: variant.id,\n variantTitle: variant.title,\n imageUrl: product.featuredImage?.url ?? null,\n priceCents: parseCents(variant.price.amount),\n compareCents: variant.compareAtPrice\n ? parseCents(variant.compareAtPrice.amount)\n : null,\n });\n afterMutation();\n }\n\n function removeSelection(productId: string, variantId: string) {\n const idx = selections.findIndex(\n (s) => s.productId === productId && s.variantId === variantId,\n );\n if (idx === -1) return;\n selections.splice(idx, 1);\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 placeholder.style.display = selections.length === 0 ? \"\" : \"none\";\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// --- 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 });\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 });\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 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 priceWrap = el(\"span\", \"lb-mix-match__filled-price\");\n if (\n selection.compareCents &&\n selection.compareCents > selection.priceCents\n ) {\n const compare = el(\"span\", \"lb-mix-match__filled-compare\");\n compare.textContent = formatCents(selection.compareCents, currency);\n priceWrap.appendChild(compare);\n }\n const priceEl = document.createElement(\"span\");\n priceEl.textContent = formatCents(selection.priceCents, currency);\n priceWrap.appendChild(priceEl);\n info.appendChild(priceWrap);\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((s, sel) => s + sel.priceCents, 0);\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((s, sel) => s + sel.priceCents, 0);\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 onAdd: (product: Product, variant: ProductVariant) => void;\n onRemove: (productId: string, variantId: string) => void;\n countFor: (productId: string, variantId: string) => number;\n isOverMax: () => boolean;\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 updateCount: () => void;\n }> = [];\n\n function buildRows() {\n if (rowsBuilt) return;\n rowsBuilt = true;\n list.innerHTML = \"\";\n\n eligible.forEach((ep) => {\n const variant = ep.firstAvailableVariant ?? ep.variants[0];\n if (!variant) return;\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 if (ep.product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = transformImageUrl(ep.product.featuredImage.url, {\n width: THUMB_PX,\n height: THUMB_PX,\n crop: \"center\",\n });\n img.alt = ep.product.featuredImage.altText ?? ep.product.title;\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 productEl.appendChild(thumb);\n\n const info = el(\"div\", \"lb-mix-match__modal-product-info\");\n const title = el(\"span\", \"lb-mix-match__modal-product-title\");\n title.textContent = ep.product.title;\n info.appendChild(title);\n const price = el(\"span\", \"lb-mix-match__modal-product-price\");\n price.textContent = formatCents(parseCents(variant.price.amount), currency);\n info.appendChild(price);\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 if (!ep.isOos) {\n const addBtn = document.createElement(\"button\");\n addBtn.type = \"button\";\n addBtn.className = \"lb-mix-match__modal-add\";\n addBtn.setAttribute(\"aria-label\", `Add ${ep.product.title}`);\n addBtn.innerHTML = PLUS_ICON_SVG;\n const countBadge = el(\"span\", \"lb-bundle-qty-badge\");\n countBadge.style.display = \"none\";\n addBtn.appendChild(countBadge);\n addBtn.addEventListener(\"click\", () => {\n if (handlers.isOverMax()) return;\n handlers.onAdd(ep.product, variant);\n });\n productEl.appendChild(addBtn);\n\n // Right-click / long-press / standard remove: plain X shown when count > 0\n const removeBtn = document.createElement(\"button\");\n removeBtn.type = \"button\";\n removeBtn.className = \"lb-mix-match__slot-remove\";\n removeBtn.setAttribute(\"aria-label\", `Remove one ${ep.product.title}`);\n removeBtn.style.display = \"none\";\n removeBtn.innerHTML = CLOSE_ICON_SVG;\n removeBtn.addEventListener(\"click\", (e) => {\n e.stopPropagation();\n handlers.onRemove(ep.product.id, variant.id);\n });\n productEl.appendChild(removeBtn);\n\n productRows.push({\n el: productEl,\n product: ep.product,\n variant,\n updateCount: () => {\n const count = handlers.countFor(ep.product.id, variant.id);\n if (count > 0) {\n countBadge.textContent = String(count);\n countBadge.style.display = \"\";\n removeBtn.style.display = \"\";\n } else {\n countBadge.style.display = \"none\";\n removeBtn.style.display = \"none\";\n }\n },\n });\n } else {\n // OOS items still need a productRows entry so search filters them\n // out. There's no add/remove UI and nothing to update on count\n // change, so updateCount is a no-op.\n productRows.push({\n el: productEl,\n product: ep.product,\n variant,\n updateCount: () => {},\n });\n }\n\n list.appendChild(productEl);\n });\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.isOverMax()) 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.updateCount());\n }\n\n return { el: overlay, open, close, refreshCounts };\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 available = product.variants.nodes.filter((v) => v.availableForSale);\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","/**\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 { 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 const variant = product?.variants.nodes.find((v) => v.availableForSale);\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 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 isAvailable = product?.variants.nodes.some((v) => v.availableForSale);\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 * 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-text-muted: #666666;\n --lb-thumbnail-bg: #F0F0F0;\n --lb-widget-pad: 20px;\n --lb-progress-color: var(--lb-primary-color);\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 rgba(0, 0, 0, 0.06);\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: var(--lb-text-muted);\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: 12px;\n padding: 12px 0;\n}\n\n.lb-bundle-thumbnail {\n position: relative;\n width: 48px;\n height: 48px;\n min-width: 48px;\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: 100%;\n object-fit: cover;\n}\n\n.lb-bundle-thumbnail svg {\n width: 28px;\n height: 28px;\n color: #BBBBBB;\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\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: 12px;\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: 12px;\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.lb-bundle-variant-badge {\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 12px;\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text-muted);\n margin-top: 8px;\n}\n\n.lb-bundle-quantity {\n font-size: 12px;\n line-height: 16px;\n color: var(--lb-text-muted);\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: 12px 16px;\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: 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: #BBBBBB;\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/* 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}\n\n/* Fixed bundles: product rows */\n.lb-fixed .lb-bundle-product-row {\n gap: 16px;\n align-items: center;\n}\n\n/* Fixed bundles: larger thumbnails */\n.lb-fixed .lb-bundle-thumbnail {\n width: 60px;\n height: 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.lb-bundle-variant-select {\n margin-top: 8px;\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 max-width: 100%;\n}\n\n.lb-bundle-variant-select:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\n}\n`;\nexport const BUNDLE_MIX_MATCH_CSS = `/* Lime Bundles — Mix & Match styles */\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 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 border: var(--lb-image-border-width) dashed color-mix(in srgb, var(--lb-text) 20%, 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.lb-mix-match .lb-bundle-thumbnail {\n width: 60px;\n height: 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 .lb-mix-match__filled-variant {\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.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}\n\n/* Price placeholder */\n.lb-mix-match__price-placeholder {\n padding: 4px 0 16px;\n text-align: center;\n}\n\n.lb-mix-match__price-placeholder-text {\n font-size: 16px;\n color: var(--lb-text);\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: 480px;\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: 2px;\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 {\n outline: none;\n box-shadow: 0 0 0 1px var(--lb-primary-color);\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: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 0;\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: 48px;\n height: 48px;\n min-width: 48px;\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: #F0F0F0;\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: 100%;\n object-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: 4px 0 0;\n}\n\n.lb-mix-match__variant-select {\n font-size: 12px;\n margin: 4px 0 0;\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 max-width: 120px;\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}\n\n.lb-mix-match__modal-add {\n padding: 8px 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 white-space: nowrap;\n flex-shrink: 0;\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}\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.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\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}\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: color-mix(in srgb, var(--lb-tier-border-color) 50%, black);\n}\n\n.lb-volume__tier:focus-visible {\n outline: 2px solid var(--lb-primary-color);\n outline-offset: 2px;\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: 12px;\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: 12px;\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\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} 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 {\n BUNDLE_BASE_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 ];\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 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 });\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 also persists the bucket\n * via a first-party cookie + POSTs to /api/ab-assign for server-side\n * analytics.\n */\n private async applyABVariants(): Promise<void> {\n if (!this.appUrl || 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 this.appUrl,\n this.shopDomain,\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 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 ? [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 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) 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 ].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 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":"ycAAA,IAAAA,GAAA,GAAAC,GAAAD,GAAA,uBAAAE,ICUO,IAAMC,GAAN,cAAiC,KAAM,CAC5C,YACkBC,EAChB,CACA,MAAMA,EAAO,IAAKC,GAAMA,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,EAF7B,KAAA,OAAAD,EAGhB,KAAK,KAAO,oBACd,CACF,EA4BME,GAAsB,UAErB,SAASC,EACdC,EACkB,CAClB,IAAMC,EAAUD,EAAO,YAAcF,GAC/BI,EAAW,WAAWF,EAAO,UAAU,QAAQC,CAAO,gBAE5D,MAAO,CACL,MAAM,MACJE,EACAC,EACAC,EACY,CACZ,IAAMC,EAAkC,CACtC,eAAgB,mBAChB,oCAAqCN,EAAO,WAC9C,EACIA,EAAO,UACTM,EAAQ,6BAA6B,EAAIN,EAAO,SAGlD,IAAMO,EAAW,MAAM,MAAML,EAAU,CACrC,OAAQ,OACR,QAAAI,EACA,KAAM,KAAK,UAAU,CAAE,MAAAH,EAAO,UAAAC,CAAU,CAAC,EACzC,OAAQC,GAAS,MACnB,CAAC,EAED,GAAI,CAACE,EAAS,GACZ,MAAM,IAAIZ,GAAmB,CAC3B,CACE,QAAS,yBAAyBY,EAAS,MAAM,IAAIA,EAAS,UAAU,EAC1E,CACF,CAAC,EAGH,IAAMC,EAAQ,MAAMD,EAAS,KAAK,EAKlC,GAAIC,EAAK,QAAQ,OACf,MAAM,IAAIb,GAAmBa,EAAK,MAAM,EAG1C,OAAOA,EAAK,IACd,CACF,CACF,CCzFO,IAAMC,GAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2F1BC,GAAwB;;;;;;;;EA+BxBC,GAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6H5BC,GAAuB;;;;;;;EASvBC,GAA0B;;;;;;;ECN1BC,EAAN,cAA+B,KAAM,CAC1C,YAAYC,EAAiCC,EAA+B,CAC1E,MAAMD,CAAO,EAD8B,KAAA,OAAAC,EAE3C,KAAK,KAAO,kBACd,CACF,ECnPaC,EAAuC,CAClD,OAAQ,CACN,UAAW,UACX,YAAa,WACb,cAAe,UACf,YAAa,UACb,iBAAkB,UAClB,mBAAoB,UACpB,qBAAsB,UACtB,qBAAsB,EACtB,sBAAuB,EACvB,iBAAkB,UAClB,mBAAoB,SACtB,EACA,OAAQ,CACN,gBAAiB,UACjB,YAAa,UACb,YAAa,EACb,aAAc,EAChB,EACA,YAAa,CACX,UAAW,UACX,iBAAkB,EAClB,iBAAkB,UAClB,kBAAmB,EACnB,mBAAoB,EACpB,mBAAoB,UACpB,oBAAqB,EACrB,UAAW,GACX,mBAAoB,GACpB,gBAAiB,GACjB,mBAAoB,UACpB,qBAAsB,SACxB,EACA,QAAS,CACP,cAAe,GACf,iBAAkB,GAClB,iBAAkB,GAClB,cAAe,GACf,mBAAoB,EACtB,EACA,IAAK,CACH,QAAS,cACT,aAAc,UACd,gBAAiB,UACjB,YAAa,EACb,YAAa,UACb,aAAc,EAChB,EACA,WAAY,CACV,QAAS,GACT,QAAS,UACT,UAAW,UACX,YAAa,EACb,YAAa,UACb,aAAc,CAChB,EACA,UAAW,CACT,cAAe,EACjB,EACA,aAAc,CACZ,QAAS,GACT,KAAM,eACN,QAAS,UACT,UAAW,UACX,YAAa,EACb,YAAa,UACb,aAAc,EAChB,EACA,mBAAoB,kBAEpB,WAAY,GACZ,cAAe,UACf,gBAAiB,UACjB,kBAAmB,EACnB,kBAAmB,UACnB,mBAAoB,GACpB,wBAAyB,EACzB,wBAAyB,UACzB,yBAA0B,EAC1B,yBAA0B,EAC1B,yBAA0B,UAC1B,0BAA2B,EAC3B,sBAAuB,GACvB,yBAA0B,UAC1B,2BAA4B,UAC5B,iBAAkB,UAClB,oBAAqB,UACrB,qBAAsB,EACtB,qBAAsB,UACtB,sBAAuB,EACvB,yBAA0B,EAC1B,yBAA0B,UAC1B,0BAA2B,EAE3B,gBAAiB,UACjB,gBAAiB,EACjB,iBAAkB,GAClB,wBAAyB,UACzB,wBAAyB,EACzB,YAAa,OACf,EASO,SAASC,GAAkBC,EAA4B,CAC5D,GAAI,CAACA,GAAO,OAAOA,GAAQ,SACzB,OAAOF,EAET,IAAMG,EAAQD,EACd,MAAO,CACL,GAAGF,EACH,GAAGG,EACH,YAAaC,GAAoBD,EAAM,WAAW,EAClD,OAAQ,CAAE,GAAGH,EAAuB,OAAQ,GAAIG,EAAM,QAAU,CAAC,CAAG,EACpE,OAAQ,CAAE,GAAGH,EAAuB,OAAQ,GAAIG,EAAM,QAAU,CAAC,CAAG,EACpE,YAAa,CACX,GAAGH,EAAuB,YAC1B,GAAIG,EAAM,aAAe,CAAC,CAC5B,EACA,QAAS,CACP,GAAGH,EAAuB,QAC1B,GAAIG,EAAM,SAAW,CAAC,CACxB,EACA,IAAK,CAAE,GAAGH,EAAuB,IAAK,GAAIG,EAAM,KAAO,CAAC,CAAG,EAC3D,WAAY,CACV,GAAGH,EAAuB,WAC1B,GAAIG,EAAM,YAAc,CAAC,CAC3B,EACA,UAAW,CACT,GAAGH,EAAuB,UAC1B,GAAIG,EAAM,WAAa,CAAC,CAC1B,EACA,aAAc,CACZ,GAAGH,EAAuB,aAC1B,GAAIG,EAAM,cAAgB,CAAC,CAC7B,CACF,CACF,CAEA,SAASC,GACPF,EACiC,CAEjC,OADIA,IAAQ,SAAWA,IAAQ,cAC3B,OAAOA,GAAQ,UAAY,OAAO,UAAUA,CAAG,GAAKA,GAAO,EAAUA,EAClEF,EAAuB,WAChC,CAUO,SAASK,GACdtB,EAC2C,CAC3C,MAAO,CACL,aAAcA,EAAO,IAAI,aACzB,gBAAiBA,EAAO,OAAO,gBAC/B,UAAWA,EAAO,YAAY,UAC9B,YAAaA,EAAO,OAAO,YAC3B,YAAaA,EAAO,OAAO,YAC3B,gBAAiBA,EAAO,IAAI,gBAC5B,eAAgBA,EAAO,IAAI,YAC3B,eAAgBA,EAAO,IAAI,YAC3B,gBAAiBA,EAAO,IAAI,aAC5B,kBAAmBA,EAAO,WAAW,QACrC,oBAAqBA,EAAO,WAAW,UACvC,sBAAuBA,EAAO,WAAW,YACzC,sBAAuBA,EAAO,WAAW,YACzC,uBAAwBA,EAAO,WAAW,aAC1C,aAAcA,EAAO,OAAO,aAC5B,gBAAiBA,EAAO,OAAO,UAC/B,iBAAkBA,EAAO,OAAO,iBAChC,mBAAoBA,EAAO,OAAO,mBAClC,qBAAsBA,EAAO,OAAO,qBACpC,qBAAsBA,EAAO,OAAO,qBACpC,sBAAuBA,EAAO,OAAO,sBACrC,iBAAkBA,EAAO,OAAO,iBAChC,mBAAoBA,EAAO,OAAO,mBAClC,YAAaA,EAAO,OAAO,YAC3B,oBAAqBA,EAAO,OAAO,cACnC,kBAAmBA,EAAO,OAAO,YACjC,cAAeA,EAAO,QAAQ,cAC9B,iBAAkBA,EAAO,YAAY,iBACrC,iBAAkBA,EAAO,YAAY,iBACrC,kBAAmBA,EAAO,YAAY,kBACtC,mBAAoBA,EAAO,YAAY,mBACvC,mBAAoBA,EAAO,YAAY,mBACvC,oBAAqBA,EAAO,YAAY,oBACxC,mBAAoBA,EAAO,YAAY,mBACvC,qBAAsBA,EAAO,YAAY,qBACzC,iBAAkBA,EAAO,YAAY,UACrC,0BAA2BA,EAAO,YAAY,mBAC9C,gBAAiBA,EAAO,YAAY,gBACpC,cAAeA,EAAO,UAAU,cAChC,QAASA,EAAO,IAAI,QACpB,mBAAoBA,EAAO,mBAC3B,iBAAkBA,EAAO,QAAQ,iBACjC,iBAAkBA,EAAO,QAAQ,iBACjC,cAAeA,EAAO,QAAQ,cAC9B,mBAAoBA,EAAO,QAAQ,mBACnC,gBAAiBA,EAAO,aAAa,QACrC,iBAAkBA,EAAO,aAAa,KACtC,oBAAqBA,EAAO,aAAa,QACzC,sBAAuBA,EAAO,aAAa,UAC3C,wBAAyBA,EAAO,aAAa,YAC7C,wBAAyBA,EAAO,aAAa,YAC7C,yBAA0BA,EAAO,aAAa,aAC9C,WAAYA,EAAO,WACnB,cAAeA,EAAO,cACtB,gBAAiBA,EAAO,gBACxB,kBAAmBA,EAAO,kBAC1B,kBAAmBA,EAAO,kBAC1B,mBAAoBA,EAAO,mBAC3B,wBAAyBA,EAAO,wBAChC,wBAAyBA,EAAO,wBAChC,yBAA0BA,EAAO,yBACjC,yBAA0BA,EAAO,yBACjC,yBAA0BA,EAAO,yBACjC,0BAA2BA,EAAO,0BAClC,sBAAuBA,EAAO,sBAC9B,yBAA0BA,EAAO,yBACjC,2BAA4BA,EAAO,2BACnC,iBAAkBA,EAAO,iBACzB,oBAAqBA,EAAO,oBAC5B,qBAAsBA,EAAO,qBAC7B,qBAAsBA,EAAO,qBAC7B,sBAAuBA,EAAO,sBAC9B,yBAA0BA,EAAO,yBACjC,yBAA0BA,EAAO,yBACjC,0BAA2BA,EAAO,0BAClC,gBAAiBA,EAAO,gBACxB,gBAAiBA,EAAO,gBACxB,iBAAkBA,EAAO,iBACzB,wBAAyBA,EAAO,wBAChC,wBAAyBA,EAAO,wBAChC,YAAaA,EAAO,YACpB,GAAIA,EAAO,aAAa,YAAc,QAAa,CACjD,qBAAsBA,EAAO,aAAa,SAC5C,CACF,CACF,CASO,IAAMuB,GAAsC,CACjD,aAAc,qBACd,gBAAiB,UACjB,UAAW,YACX,iBAAkB,0BAClB,iBAAkB,0BAClB,kBAAmB,2BACnB,mBAAoB,4BACpB,mBAAoB,4BACpB,oBAAqB,sBACrB,mBAAoB,oBACpB,qBAAsB,uBACtB,YAAa,cACb,YAAa,oBACb,gBAAiB,gBACjB,aAAc,cACd,gBAAiB,mBACjB,iBAAkB,qBAClB,mBAAoB,uBACpB,qBAAsB,+BACtB,qBAAsB,+BACtB,sBAAuB,yBACvB,iBAAkB,oBAClB,mBAAoB,sBACpB,eAAgB,wBAChB,eAAgB,wBAChB,gBAAiB,kBACjB,kBAAmB,sBACnB,oBAAqB,wBACrB,sBAAuB,gCACvB,sBAAuB,gCACvB,uBAAwB,0BACxB,gBAAiB,yBACjB,gBAAiB,yBACjB,iBAAkB,mBAClB,wBAAyB,kCACzB,wBAAyB,kCACzB,oBAAqB,wBACrB,sBAAuB,0BACvB,wBAAyB,kCACzB,wBAAyB,kCACzB,yBAA0B,4BAC1B,oBAAqB,oBACrB,kBAAmB,kBACnB,cAAe,iBACf,gBAAiB,mBACjB,kBAAmB,2BACnB,kBAAmB,2BACnB,mBAAoB,qBACpB,wBAAyB,kCACzB,wBAAyB,kCACzB,yBAA0B,4BAC1B,yBAA0B,mCAC1B,yBAA0B,mCAC1B,0BAA2B,6BAC3B,yBAA0B,2BAC1B,2BAA4B,8BAC5B,iBAAkB,qBAClB,oBAAqB,wBACrB,qBAAsB,+BACtB,qBAAsB,+BACtB,sBAAuB,yBACvB,yBAA0B,mCAC1B,yBAA0B,mCAC1B,0BAA2B,4BAC7B,EAGaC,GAA+B,IAAI,IAAI,CAClD,eACA,cACA,uBACA,wBACA,kBACA,mBACA,0BACA,wBACA,yBACA,iBACA,kBACA,0BACA,2BACA,mBACA,oBACA,qBACA,sBACA,oBACA,qBACA,0BACA,2BACA,2BACA,4BACA,uBACA,wBACA,2BACA,2BACF,CAAC,EAiBM,SAASC,GACdC,EACA1B,EACM,CACN,IAAM2B,EAAOL,GAAoBtB,CAAM,EAEvC,OAAW,CAAC4B,EAASC,CAAM,IAAK,OAAO,QAAQN,EAAW,EAAG,CAC3D,IAAMO,EAAQH,EAAKC,CAAO,EAC1B,GAA2BE,GAAU,KAAM,SAC3C,IAAMC,EAAaP,GAAQ,IAAII,CAAO,EAAI,GAAGE,CAAK,KAAO,OAAOA,CAAK,EACrEJ,EAAG,MAAM,YAAYG,EAAQE,CAAU,CACzC,CAMAL,EAAG,MAAM,YACP,6BACA1B,EAAO,YAAY,UAAY,QAAU,MAC3C,EACA0B,EAAG,MAAM,YACP,+BACA1B,EAAO,YAAY,mBAAqB,SAAW,MACrD,EACA0B,EAAG,MAAM,YACP,yBACA1B,EAAO,YAAY,gBAAkB,OAAS,MAChD,EACA0B,EAAG,MAAM,YACP,gCACA1B,EAAO,sBAAwB,OAAS,MAC1C,EAGIA,EAAO,OAAO,cAAgB,QAChC0B,EAAG,MAAM,YAAY,iBAAkB1B,EAAO,OAAO,aAAa,EAElE0B,EAAG,MAAM,YACP,iBACA,2BAA2B1B,EAAO,OAAO,aAAa,KAAKA,EAAO,OAAO,WAAW,GACtF,CAEJ,CCzZA,IAAMgC,GAAqB,IAAI,IAAgB,CAAC,QAAS,YAAa,QAAQ,CAAC,EACzEC,GAAkB,IAAI,IAAkB,CAAC,QAAQ,CAAC,EAMjD,SAASC,EACdC,EACAC,EACqB,CACrB,GAAI,CACF,OAAOC,GAA4BF,EAAcC,CAAM,CACzD,OAASE,EAAK,CACZ,GAAIA,aAAexB,EAAkB,OAAO,KAC5C,MAAMwB,CACR,CACF,CAMO,SAASD,GACdF,EACAC,EACc,CACd,IAAMG,EAAW,IAAI,IAAIH,EAAO,IAAKI,GAAM,CAACA,EAAE,IAAKA,CAAC,CAAC,CAAC,EAEhDC,EAAQF,EAAS,IAAI,OAAO,GAAG,OAAS,SACxCG,EAAgBH,EAAS,IAAI,aAAa,GAAG,MAC7CI,EAAYJ,EAAS,IAAI,QAAQ,GAAG,MAE1C,GAAI,CAACG,GAAiB,CAACV,GAAmB,IAAIU,CAA2B,EACvE,MAAM,IAAI5B,EACR,mCAAmC4B,GAAiB,MAAM,GAC1D,cACF,EAEF,IAAME,EAAaF,EAEnB,GAAI,CAACC,GAAa,CAACV,GAAgB,IAAIU,CAAyB,EAC9D,MAAM,IAAI7B,EACR,gCAAgC6B,GAAa,MAAM,GACnD,UACF,EAEF,IAAME,EAASF,EAETG,EAAWP,EAAS,IAAI,WAAW,GAAG,OAAS,KAC/CQ,EAASR,EAAS,IAAI,SAAS,GAAG,OAAS,KAC3CS,EAAM,IAAI,KAEhB,GAAIF,EAAU,CACZ,IAAMG,EAAQ,IAAI,KAAKH,CAAQ,EAC/B,GAAI,OAAO,MAAMG,EAAM,QAAQ,CAAC,EAC9B,MAAM,IAAInC,EACR,sBAAsBgC,CAAQ,GAC9B,cACF,EAEF,GAAIG,EAAQD,EACV,MAAM,IAAIlC,EACR,qCAAqCgC,CAAQ,GAC7C,aACF,CAEJ,CACA,GAAIC,EAAQ,CACV,IAAMG,EAAM,IAAI,KAAKH,CAAM,EAC3B,GAAI,OAAO,MAAMG,EAAI,QAAQ,CAAC,EAC5B,MAAM,IAAIpC,EACR,oBAAoBiC,CAAM,GAC1B,cACF,EAEF,GAAIG,EAAMF,EACR,MAAM,IAAIlC,EACR,+BAA+BiC,CAAM,GACrC,SACF,CAEJ,CAEA,IAAMI,EAAWC,GAAgBb,CAAQ,EACnCc,EAAiBC,GAAoBf,CAAQ,EAC7CgB,EAAeC,GAAkBjB,CAAQ,EAEzCkB,EAAO,CACX,GAAItB,EACJ,MAAAM,EACA,YAAaF,EAAS,IAAI,aAAa,GAAG,OAAS,KACnD,OAAAM,EACA,SAAAM,EACA,eAAAE,EACA,aAAAE,EACA,SAAAT,EACA,OAAAC,EACA,cAAeR,EAAS,IAAI,gBAAgB,GAAG,OAAS,KACxD,mBAAoBmB,GAAwBnB,CAAQ,EACpD,kBAAmBoB,GAAmBpB,EAAU,oBAAoB,EACpE,kBAAmBoB,GAAmBpB,EAAU,oBAAoB,EACpE,SAAUA,EAAS,IAAI,YAAY,GAAG,OAAS,KAC/C,WAAYqB,GAAgBrB,CAAQ,CACtC,EAEA,OAAQK,EAAY,CAClB,IAAK,QAEH,MADgC,CAAE,GAAGa,EAAM,WAAY,OAAQ,EAGjE,IAAK,SAMH,MALiC,CAC/B,GAAGA,EACH,WAAY,SACZ,YAAaI,GAAiBtB,CAAQ,CACxC,EAGF,IAAK,YAOH,MANmC,CACjC,GAAGkB,EACH,WAAY,YACZ,YAAaK,GAAcvB,EAAU,eAAgB,CAAE,IAAK,CAAE,CAAC,EAC/D,YAAauB,GAAcvB,EAAU,eAAgB,CAAE,IAAK,CAAE,CAAC,CACjE,CAGJ,CACF,CAEA,SAASa,GAAgBb,EAAmD,CAC1E,IAAMY,EAAsB,CAAC,EAEvBY,EAAgBxB,EAAS,IAAI,UAAU,EAK7C,GAJIwB,GAAe,WAAa,aAAcA,EAAc,WAC1DZ,EAAS,KAAKY,EAAc,SAAoB,EAG9CA,GAAe,YAAY,MAC7B,QAAWC,KAAQD,EAAc,WAAW,MACtC,aAAcC,EAChBb,EAAS,KAAKa,CAAe,EACpB,aAAcA,GAAQA,EAAK,UAAU,OAC9Cb,EAAS,KAAK,GAAGa,EAAK,SAAS,KAAK,EAK1C,IAAMC,EAAkB1B,EAAS,IAAI,YAAY,EACjD,GAAI0B,GAAiB,YAAY,MAC/B,QAAWD,KAAQC,EAAgB,WAAW,MACxC,aAAcD,GAAQA,EAAK,UAAU,OACvCb,EAAS,KAAK,GAAGa,EAAK,SAAS,KAAK,EAK1C,OAAOb,CACT,CAEA,SAASG,GACPf,EACgB,CAChB,MAAO,CACL,aACGA,EAAS,IAAI,eAAe,GAAG,OAChC,aACF,cAAe,WAAWA,EAAS,IAAI,gBAAgB,GAAG,OAAS,GAAG,EACtE,cAAeA,EAAS,IAAI,gBAAgB,GAAG,QAAU,MAC3D,CACF,CASA,SAASiB,GACPjB,EACc,CACd,OAAOrB,GAAkBgD,EAAe3B,EAAU,eAAe,CAAC,CACpE,CAOA,SAASmB,GACPnB,EACmB,CACnB,IAAMpB,EAAM+C,EAAe3B,EAAU,sBAAsB,EAC3D,GAAI,CAAC,MAAM,QAAQpB,CAAG,EAAG,OAAO,KAChC,IAAMgD,EAAqB,CAAC,EAC5B,QAAWC,KAASjD,EACd,MAAM,QAAQiD,CAAK,EACrBD,EAAO,KACLC,EAAM,OAAQC,GAAmB,OAAOA,GAAM,QAAQ,CACxD,EAEAF,EAAO,KAAK,CAAC,CAAC,EAGlB,OAAOA,CACT,CAOA,SAASR,GACPpB,EACA+B,EACwB,CACxB,IAAMnD,EAAM+C,EAAe3B,EAAU+B,CAAG,EAIxC,GAAI,CAACnD,GAAO,OAAOA,GAAQ,UAAY,MAAM,QAAQA,CAAG,EAAG,MAAO,CAAC,EACnE,IAAMoD,EAAiC,CAAC,EACxC,OAAW,CAACC,EAAGC,CAAC,IAAK,OAAO,QAAQtD,CAAG,EAAG,CACxC,IAAMuD,EAAM,OAAOD,GAAM,SAAWA,EAAI,OAAOA,CAAC,EAQ5C,OAAO,SAASC,CAAG,GAAKA,GAAO,GAAKA,GAAO,KAC7CH,EAAOC,CAAC,EAAI,KAAK,MAAME,CAAG,EAE9B,CACA,OAAOH,CACT,CAOA,SAASX,GACPrB,EAC2B,CAE3B,GAAI,CADaA,EAAS,IAAI,YAAY,GAAG,MAC9B,OAAO,KAEtB,IAAMoC,EAAgC,CAAC,EACjClC,EAAQF,EAAS,IAAI,UAAU,GAAG,MACpCE,IAAOkC,EAAU,MAAQlC,GAC7B,IAAMmC,EAAcrC,EAAS,IAAI,gBAAgB,GAAG,MAChDqC,IAAaD,EAAU,YAAcC,GAEzC,IAAMC,EAAetC,EAAS,IAAI,kBAAkB,GAAG,MACjDuC,EAAmBvC,EAAS,IAAI,mBAAmB,GAAG,MAC5D,GACEsC,IACCA,IAAiB,cAAgBA,IAAiB,iBACnDC,EACA,CACA,IAAMhD,EAAQ,WAAWgD,CAAgB,EACrC,OAAO,SAAShD,CAAK,IACvB6C,EAAU,eAAiB,CACzB,aAAAE,EACA,cAAe/C,EACf,cAAeS,EAAS,IAAI,gBAAgB,GAAG,QAAU,MAC3D,EAEJ,CAEA,IAAMwC,EAAUb,EAAe3B,EAAU,iBAAiB,EAC1D,OAAI,MAAM,QAAQwC,CAAO,IACvBJ,EAAU,YAAcI,EACrB,OACEC,GACC,OAAOA,GAAM,UAAYA,IAAM,IACnC,EACC,IAAIC,EAAkB,GAGpB,OAAO,KAAKN,CAAS,EAAE,SAAW,EAAI,KAAOA,CACtD,CAEA,SAASd,GACPtB,EACc,CACd,IAAMpB,EAAM+C,EAAe3B,EAAU,cAAc,EACnD,OAAK,MAAM,QAAQpB,CAAG,EACfA,EACJ,OACE6D,GAAoC,OAAOA,GAAM,UAAYA,IAAM,IACtE,EACC,IAAIC,EAAkB,EALO,CAAC,CAMnC,CAMA,SAASA,GAAmBD,EAAwC,CAClE,IAAME,EAAS,OAAOF,EAAE,aAAe,CAAC,EAClCG,EAAmB,CAIvB,YAAa,OAAO,SAASD,CAAM,GAAKA,GAAU,EAAIA,EAAS,CACjE,EACA,OAAI,OAAOF,EAAE,YAAe,UAAY,OAAO,SAASA,EAAE,UAAU,IAClEG,EAAK,WAAaH,EAAE,YAElB,OAAOA,EAAE,QAAW,UAAY,OAAO,SAASA,EAAE,MAAM,IAC1DG,EAAK,OAASH,EAAE,QAEXG,CACT,CAEA,SAASjB,EACP3B,EACA+B,EACS,CACT,IAAMxC,EAAQS,EAAS,IAAI+B,CAAG,GAAG,MACjC,GAAI,CAACxC,EAAO,OAAO,KACnB,GAAI,CACF,OAAO,KAAK,MAAMA,CAAK,CACzB,MAAQ,CACN,OAAO,IACT,CACF,CAEA,SAASgC,GACPvB,EACA+B,EACAjE,EAA4B,CAAC,EACd,CACf,IAAMyB,EAAQS,EAAS,IAAI+B,CAAG,GAAG,MACjC,GAAI,CAACxC,EAAO,OAAO,KACnB,IAAM4C,EAAM,SAAS5C,EAAO,EAAE,EAE9B,OADI,MAAM4C,CAAG,GACTrE,EAAQ,MAAQ,QAAaqE,EAAMrE,EAAQ,IAAY,KACpDqE,CACT,CIhVO,SAASU,EACdC,EACAC,EACAC,EACQ,CACR,IAAMC,EAAKH,EAAO,kBAAkBE,CAAS,EAC7C,OAAIC,IAAO,OAAkBA,EACtBH,EAAO,kBAAkBC,CAAS,GAAK,CAChD,CClCO,SAASG,GAAgBJ,EAAoC,CAClE,IAAMK,EAAYL,EAAO,WACzB,GAAI,CAACK,EAAW,OAAOL,EAGvB,IAAMM,EAAQD,EAAU,OAASL,EAAO,MAClCO,EACJF,EAAU,cAAgB,OACtBA,EAAU,YACVL,EAAO,YACPQ,EAAiBH,EAAU,gBAAkBL,EAAO,eAM1D,OAAQA,EAAO,WAAY,CACzB,IAAK,SACH,MAAO,CACL,GAAGA,EACH,MAAAM,EACA,YAAAC,EACA,eAAAC,EACA,YAAaH,EAAU,aAAeL,EAAO,WAC/C,EACF,IAAK,YACH,MAAO,CAAE,GAAGA,EAAQ,MAAAM,EAAO,YAAAC,EAAa,eAAAC,CAAe,EACzD,IAAK,QACH,MAAO,CAAE,GAAGR,EAAQ,MAAAM,EAAO,YAAAC,EAAa,eAAAC,CAAe,CAC3D,CACF,CCzBA,eAAsBC,GACpBC,EACAC,EAOe,CACf,MAAMC,GAAUF,EAAQ,CACtB,WAAYA,EAAO,WACnB,UAAW,oBACX,UAAWC,EAAM,UACjB,WAAYA,EAAM,WAClB,UAAWA,EAAM,UACjB,SAAUA,EAAM,SAChB,UAAWA,EAAM,UACjB,WAAY,IAAI,KAAK,EAAE,YAAY,CACrC,CAAC,CACH,CAEA,eAAsBE,GACpBH,EACAC,EAiBe,CACf,MAAMC,GAAUF,EAAQ,CACtB,WAAYA,EAAO,WACnB,UAAW,qBACX,GAAGC,EACH,WAAY,IAAI,KAAK,EAAE,YAAY,CACrC,CAAC,CACH,CAMO,SAASG,GACdC,EACAC,EACY,CACZ,GAAI,OAAO,qBAAyB,IAElC,OAAAA,EAAS,EACF,IAAM,CAAC,EAGhB,IAAMC,EAAW,IAAI,qBAClBC,GAAY,CACX,QAAWC,KAASD,EACdC,EAAM,iBACRH,EAAS,EACTC,EAAS,UAAUE,EAAM,MAAM,EAGrC,EACA,CAAE,UAAW,EAAI,CACnB,EAEA,OAAAF,EAAS,QAAQF,CAAO,EACjB,IAAME,EAAS,WAAW,CACnC,CAIA,eAAeL,GACbF,EACAU,EACe,CACf,IAAMC,EAAM,GAAGX,EAAO,MAAM,iBACtBY,EAAO,KAAK,UAAUF,CAAO,EAEnC,GAAI,EACe,MAAM,MAAMC,EAAK,CAChC,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAC,CACF,CAAC,GAEa,KAEZ,MAAM,IAAI,QAASC,GAAM,WAAWA,EAAG,GAAI,CAAC,EAC5C,MAAM,MAAMF,EAAK,CACf,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAC,CACF,CAAC,EAEL,MAAQ,CAEN,GAAI,CAEE,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,MAAQ,CAER,CACF,CACF,CCjGA,IAAIE,GAAiB,GAkBd,SAASC,IAAsB,CACpC,GAAIC,GAAgB,MAAO,GAC3B,GAAI,OAAO,OAAW,IAAa,MAAO,GAG1C,IAAMC,EACJ,OAOA,QAEF,GAAI,CACF,OAAOA,GAAe,iBAAiB,6BAA6B,IAAM,EAC5E,MAAQ,CACN,MAAO,EACT,CACF,CC7CA,IAAMC,GAAsB,aACtBC,GAAyB,IAAU,GAAK,GACxCC,GAAoB,IAAU,GAAK,GAYzC,SAASC,GAAaC,EAA0B,CAI9C,MAAO,iBAAiBA,CAAQ,EAClC,CASA,eAAsBC,GACpBC,EACAC,EACAC,EACAJ,EACkC,CAClC,GAAI,OAAO,SAAa,IAAa,OAAO,KAK5C,IAAMK,EAAWC,GAAaN,CAAQ,EACtC,GAAIK,GAAYA,EAAS,SAAWD,EAClC,MAAO,CAAE,QAASC,EAAS,QAAS,OAAAD,EAAQ,UAAW,EAAK,EAM9D,GAAI,CAACX,GAAW,EACd,MAAO,CAAE,QAAS,IAAK,OAAAW,EAAQ,UAAW,EAAM,EAKlD,IAAMG,EAAYC,GAAqB,EACjCC,EAAUC,GAAaH,EAAWH,CAAM,EAE9CO,GAAcX,EAAUI,EAAQK,CAAO,EAGvC,GAAI,CACF,MAAM,GAAGP,CAAM,iBAAkB,CAC/B,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CACnB,WAAAC,EACA,OAAAC,EACA,UAAAG,EACA,QAAAE,CACF,CAAC,CACH,CAAC,EAAE,MAAM,IAAM,CAAC,CAAC,CACnB,MAAQ,CAER,CAEA,MAAO,CAAE,QAAAA,EAAS,OAAAL,EAAQ,UAAW,EAAK,CAC5C,CAEA,SAASE,GACPN,EAC+C,CAC/C,GAAI,OAAO,SAAa,IAAa,OAAO,KAE5C,IAAMY,EAAS,GADFb,GAAaC,CAAQ,CACZ,IAKhBa,EAJU,SAAS,OAAO,MAAM,GAAG,EAAE,IAAKC,GAAMA,EAAE,KAAK,CAAC,EAI1C,KAAMA,GAAMA,EAAE,WAAWF,CAAM,CAAC,EACpD,GAAI,CAACC,EAAK,OAAO,KACjB,IAAME,EAAQF,EAAI,UAAUD,EAAO,MAAM,EAGnC,CAACH,EAASL,CAAM,EAAIW,EAAM,MAAM,GAAG,EAEzC,OADIN,IAAY,KAAOA,IAAY,KAC/B,CAACL,EAAe,KACb,CAAE,OAAAA,EAAQ,QAAAK,CAAQ,CAC3B,CAEA,SAASE,GACPX,EACAI,EACAK,EACM,CACN,GAAI,OAAO,SAAa,IAAa,OACrC,IAAMO,EAAOjB,GAAaC,CAAQ,EAC5Be,EAAQ,GAAGN,CAAO,IAAIL,CAAM,IAAI,KAAK,IAAI,CAAC,GAEhD,SAAS,OACP,GAAGY,CAAI,IAAID,CAAK,qBAAqBjB,EAAiB,wBAE1D,CAEA,SAASU,IAA+B,CACtC,GAAI,OAAO,SAAa,IAAa,OAAOS,GAAa,EAGzD,IAAMZ,EADU,SAAS,OAAO,MAAM,GAAG,EAAE,IAAKS,GAAMA,EAAE,KAAK,CAAC,EAE3D,KAAMA,GAAMA,EAAE,WAAW,GAAGlB,EAAmB,GAAG,CAAC,GAClD,MAAM,GAAG,EAAE,CAAC,EAEhB,GAAIS,EAAU,OAAOA,EAErB,IAAMa,EAAKD,GAAa,EAKxB,gBAAS,OAAS,GAAGrB,EAAmB,IAAIsB,CAAE,qBAAqBrB,EAAsB,yBAClFqB,CACT,CAEA,SAASD,IAAuB,CAC9B,OAAI,OAAO,OAAW,KAAe,OAAO,WACnC,OAAO,WAAW,EAGpB,uCAAuC,QAAQ,QAAUH,GAAM,CACpE,IAAMK,EAAK,KAAK,OAAO,EAAI,GAAM,EAEjC,OADUL,IAAM,IAAMK,EAAKA,EAAI,EAAO,GAC7B,SAAS,EAAE,CACtB,CAAC,CACH,CAMA,SAAST,GAAaH,EAAmBH,EAA2B,CAClE,IAAMgB,EAAQb,EAAY,IAAMH,EAC5BiB,EAAO,WACX,QAASC,EAAI,EAAGA,EAAIF,EAAM,OAAQE,IAChCD,GAAQD,EAAM,WAAWE,CAAC,EAC1BD,EAAO,KAAK,KAAKA,EAAM,QAAU,EAEnC,OAAQA,IAAS,GAAK,IAAM,EAAI,IAAM,GACxC,CC9JO,IAAME,GAAiB,IAExBC,GAAoD,CACxD,CAAC,WAAY,eAAe,EAC5B,CAAC,YAAa,uBAAuB,EACrC,CAAC,mBAAoB,iBAAiB,EACtC,CAAC,gBAAiB,cAAc,EAChC,CAAC,mBAAoB,iBAAiB,EACtC,CAAC,gBAAiB,mBAAmB,CACvC,EAEMC,GAAiB,iCAMhB,SAASC,EAAkBb,EAA6B,CAC7D,GAAIA,EAAI,OAASU,GACf,MAAO,CACL,GAAI,GACJ,MAAO,eAAeA,GAAe,eAAe,OAAO,CAAC,kBAC9D,EAGF,IAAII,EAAMd,EAAI,QAAQ,oBAAqB,EAAE,EAE7C,GAAI,CAGFc,EAAMA,EAAI,QAAQ,kCAAmC,CAACC,EAAGC,IAAQ,CAC/D,IAAMC,EAAY,SAASD,EAAK,EAAE,EAClC,OAAIC,EAAY,GAAKA,EAAY,QAAiB,SAC3C,OAAO,cAAcA,CAAS,CACvC,CAAC,EAEDH,EAAMA,EAAI,QAAQ,WAAY,EAAE,EAGhCA,EAAMA,EAAI,QAAQ,0BAA2B,IAAI,CACnD,MAAQ,CACN,MAAO,CACL,GAAI,GACJ,MAAO,+CACT,CACF,CAEAA,EAAMA,EAAI,QAAQ,KAAM,EAAE,EAAE,QAAQ,KAAM,EAAE,EAE5C,OAAW,CAACI,EAASC,CAAK,IAAKR,GAC7B,GAAIO,EAAQ,KAAKJ,CAAG,EAClB,MAAO,CAAE,GAAI,GAAO,MAAO,iCAAiCK,CAAK,EAAG,EASxE,IAAMC,EAAa,+BACfC,EACJ,MAAQA,EAAWD,EAAW,KAAKN,CAAG,KAAO,MAAM,CACjD,IAAIQ,EAAWD,EAAS,CAAC,EAAE,KAAK,EAShC,IAPIC,EAAS,WAAW,GAAG,GAAKA,EAAS,WAAW,GAAG,KACrDA,EAAWA,EAAS,MAAM,CAAC,IAEzBA,EAAS,SAAS,GAAG,GAAKA,EAAS,SAAS,GAAG,KACjDA,EAAWA,EAAS,MAAM,EAAG,EAAE,GAEjCA,EAAWA,EAAS,KAAK,EACrBA,GAAY,CAACV,GAAe,KAAKU,CAAQ,EAC3C,MAAO,CACL,GAAI,GACJ,MAAO,sDACT,CAEJ,CAEA,MAAO,CAAE,GAAI,GAAM,IAAAR,CAAI,CACzB,CCvFA,IAAMS,GAAkB,iBAUjB,SAASC,GACdlC,EACAmC,EACS,CAET,GADI,OAAO,SAAa,KACpB,CAACA,EAAQ,MAAO,GAEpB,IAAMC,EAAYb,EAAkBY,CAAM,EAE1C,GADI,CAACC,EAAU,IACX,CAACA,EAAU,IAAI,KAAK,EAAG,MAAO,GAElC,IAAMrB,EAAKkB,GAAkBI,GAAWrC,CAAU,EAE9CsC,EAAQ,SAAS,eAAevB,CAAE,EACtC,OAAKuB,IACHA,EAAQ,SAAS,cAAc,OAAO,EACtCA,EAAM,GAAKvB,EACXuB,EAAM,aAAa,oBAAqB,YAAY,EACpD,SAAS,KAAK,YAAYA,CAAK,GAK7BA,EAAM,cAAgBF,EAAU,MAClCE,EAAM,YAAcF,EAAU,KAEzB,EACT,CAGA,SAASC,GAAWpB,EAAuB,CACzC,IAAIC,EAAO,WACX,QAASC,EAAI,EAAGA,EAAIF,EAAM,OAAQE,IAChCD,GAAQD,EAAM,WAAWE,CAAC,EAC1BD,EAAO,KAAK,KAAKA,EAAM,QAAU,EAEnC,OAAQA,IAAS,GAAG,SAAS,EAAE,CACjC,CE1BO,SAASqB,EACdC,EACQ,CACR,GAAIA,GAAW,KAA8B,MAAO,GACpD,IAAMC,EAAM,OAAOD,GAAW,SAAW,WAAWA,CAAM,EAAIA,EAC9D,OAAK,OAAO,SAASC,CAAG,EACjB,KAAK,MAAMA,EAAM,GAAG,EADO,CAEpC,CAMO,SAASC,EAAYC,EAAeC,EAA8B,CACvE,GAAI,CACF,OAAO,IAAI,KAAK,aAAa,OAAW,CACtC,MAAO,WACP,SAAUA,CACZ,CAAC,EAAE,OAAOD,EAAQ,GAAG,CACvB,MAAQ,CACN,MAAO,GAAGC,CAAY,KAAKD,EAAQ,KAAK,QAAQ,CAAC,CAAC,EACpD,CACF,CASO,SAASE,GACdC,EACAC,EACQ,CACR,OAAO,KAAK,IAAI,EAAGD,EAAY,KAAK,MAAOA,EAAYC,EAAW,GAAG,CAAC,CACxE,CAaO,SAASC,GACdC,EACAC,EAA4CD,EAAO,kBACnDE,EAAyBF,EAAO,aAAa,QAAQ,cACjC,CACpB,GAAM,CAAE,aAAAG,EAAc,cAAAC,CAAc,EAAIJ,EAAO,eACzCK,EAAqB,CAAC,EACxBC,EAAa,EACbC,EAAY,EAEhB,QAAWC,KAAWR,EAAO,SAAU,CACrC,IAAMS,EACJD,EAAQ,SAAS,MAAM,KAAME,GAAMA,EAAE,gBAAgB,GACrDF,EAAQ,SAAS,MAAM,CAAC,EAC1B,GAAI,CAACC,EAAS,SAEd,IAAMZ,EAAYP,EAAWmB,EAAQ,MAAM,MAAM,EAC3CE,EAAMV,EAAkBO,EAAQ,EAAE,GAAK,EACvCI,EAAYf,EAAYc,EACxBE,EAAeJ,EAAQ,eACzBnB,EAAWmB,EAAQ,eAAe,MAAM,EACxC,KAKJ,GAHAJ,EAAK,KAAK,CAAE,UAAAR,EAAW,IAAAc,EAAK,UAAAC,EAAW,aAAAC,CAAa,CAAC,EACrDP,GAAcM,EAEVT,IAAiB,aAAc,CACjC,IAAMW,EAAUlB,GAAuBC,EAAWO,CAAa,EAC/DG,GAAaO,EAAUH,CACzB,MACEJ,GAAaK,CAEjB,CAEIT,IAAiB,iBACnBI,EAAY,KAAK,IAAI,EAAGD,EAAa,KAAK,MAAMF,EAAgB,GAAG,CAAC,GAGtE,IAAMW,EAAe,KAAK,IAAI,EAAGT,EAAaC,CAAS,EACjDS,EACJhB,EAAO,SAAS,CAAC,GAAG,WAAW,gBAAgB,cAAgB,MAE7DiB,EAAc,GAClB,OAAIf,GAAiBa,EAAe,IAC9BZ,IAAiB,cAAgBC,EAAgB,EACnDa,EAAc,IAAI,KAAK,MAAMb,CAAa,CAAC,IAClCD,IAAiB,gBAAkBC,EAAgB,IAC5Da,EAAc,IAAIxB,EAChB,KAAK,MAAMW,EAAgB,GAAG,EAC9BY,CACF,CAAC,KAIE,CAAE,KAAAX,EAAM,WAAAC,EAAY,UAAAC,EAAW,aAAAQ,EAAc,YAAAE,EAAa,SAAAD,CAAS,CAC5E,CASO,SAASE,EACdZ,EACAa,EACQ,CACR,GAAIA,EAAS,eAAiB,aAAc,CAC1C,IAAMC,EAAM,KAAK,MAAOd,EAAaa,EAAS,cAAiB,GAAG,EAClE,OAAO,KAAK,IAAI,EAAGb,EAAac,CAAG,CACrC,CACA,OAAO,KAAK,IAAI,EAAGd,EAAa,KAAK,MAAMa,EAAS,cAAgB,GAAG,CAAC,CAC1E,CC1IO,IAAME,EAAW,IAEjB,SAASC,EACdC,EACA,EAAoB,CAAC,EACb,CACR,GAAI,CAACA,EAAK,MAAO,GACjB,GAAI,CACF,IAAMC,EAAI,IAAI,IAAID,CAAG,EACrB,OAAI,EAAE,QAAU,QAAWC,EAAE,aAAa,IAAI,QAAS,OAAO,EAAE,KAAK,CAAC,EAClE,EAAE,SAAW,QAAWA,EAAE,aAAa,IAAI,SAAU,OAAO,EAAE,MAAM,CAAC,EACrE,EAAE,MAAMA,EAAE,aAAa,IAAI,OAAQ,EAAE,IAAI,EACtCA,EAAE,SAAS,CACpB,MAAQ,CACN,OAAOD,CACT,CACF,CC1BO,SAASE,GAAgBC,EAA6B,CAC3D,GAAI,CAAC,OAAO,SAASA,CAAW,GAAKA,GAAe,EAAG,MAAO,GAC9D,IAAMC,EAAe,KAAK,MAAMD,EAAc,GAAI,EAC5CE,EAAO,KAAK,MAAMD,EAAe,KAAK,EACtCE,EAAQ,KAAK,MAAOF,EAAe,MAAS,IAAI,EAChDG,EAAU,KAAK,MAAOH,EAAe,KAAQ,EAAE,EAC/CI,EAAUJ,EAAe,GACzBK,EAAOC,GAAc,OAAOA,CAAC,EAAE,SAAS,EAAG,GAAG,EACpD,OAAIL,EAAO,EACF,GAAGA,CAAI,KAAKI,EAAIH,CAAK,CAAC,KAAKG,EAAIF,CAAO,CAAC,KAAKE,EAAID,CAAO,CAAC,IAE1D,GAAGC,EAAIH,CAAK,CAAC,KAAKG,EAAIF,CAAO,CAAC,KAAKE,EAAID,CAAO,CAAC,GACxD,CCTO,SAASG,EAAgBC,EAA2C,CACzE,IAAMC,EAASC,GAASF,CAAS,EAGjC,GAFIC,IAAW,MAEXA,GAAU,KAAK,IAAI,EAAG,OAAO,KACjC,IAAME,EAAiBF,EAEjBG,EAAO,SAAS,cAAc,KAAK,EACzCA,EAAK,UAAY,sBACjBA,EAAK,aAAa,iBAAkB,EAAE,EAEtC,IAAMC,EAAY,SAAS,cAAc,KAAK,EAC9CA,EAAU,UAAY,6BACtB,IAAMC,EAAY,SAAS,cAAc,MAAM,EAC/CA,EAAU,YAAc,UACxBD,EAAU,YAAYC,CAAS,EAC/BF,EAAK,YAAYC,CAAS,EAE1B,IAAME,EAAQ,SAAS,cAAc,MAAM,EAC3CA,EAAM,UAAY,6BAClBA,EAAM,aAAa,uBAAwB,EAAE,EAC7CH,EAAK,YAAYG,CAAK,EAEtB,IAAIC,EAAoD,KAExD,SAASC,GAAO,CACd,IAAMC,EAASP,EAAS,KAAK,IAAI,EACjC,GAAIO,GAAU,EAAG,CACfN,EAAK,MAAM,QAAU,OACrBO,EAAK,EACL,MACF,CACAJ,EAAM,YAAcK,GAAgBF,CAAM,CAC5C,CAEA,SAASC,GAAO,CACVH,IAAe,OACjB,cAAcA,CAAU,EACxBA,EAAa,KAEjB,CAEA,OAAAC,EAAK,EACLD,EAAa,YAAYC,EAAM,GAAI,EAE5B,CAAE,GAAIL,EAAM,KAAAO,CAAK,CAC1B,CAEA,SAAST,GAASW,EAA4B,CAC5C,IAAM,EAAI,KAAK,MAAMA,CAAG,EACxB,OAAO,OAAO,SAAS,CAAC,EAAI,EAAI,IAClC,CCvCO,SAASC,EAAeC,EAAkC,CAC/D,IAAMC,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,KAAO,SACdA,EAAO,UAAY,gBACnBA,EAAO,aAAa,kBAAmB,EAAE,EAEzC,IAAMC,EAAY,SAAS,cAAc,MAAM,EAC/CA,EAAU,UAAY,eACtBA,EAAU,aAAa,iBAAkB,EAAE,EAC3CA,EAAU,YAAcF,EACxBC,EAAO,YAAYC,CAAS,EAE5B,IAAMC,EAAc,SAAS,cAAc,MAAM,EACjD,OAAAA,EAAY,UAAY,iBACxBA,EAAY,aAAa,mBAAoB,EAAE,EAC/CA,EAAY,aAAa,cAAe,MAAM,EAE9CA,EAAY,UACV,yKAGFF,EAAO,YAAYE,CAAW,EAEvBF,CACT,CAkBO,SAASG,GAAYH,EAA2BI,EAAoB,CACzE,IAAML,EAAQC,EAAO,cAA2B,kBAAkB,EAC9DD,IACFA,EAAM,YAAcK,EAExB,CCjEO,SAASC,EACdC,EACAC,EACAC,EAAgC,CAAC,EACpB,CACb,IAAMC,EAAO,SAAS,cAAcH,CAAG,EACnCC,IAAWE,EAAK,UAAYF,GAChC,OAAW,CAACG,EAAGC,CAAC,IAAK,OAAO,QAAQH,CAAK,EACvCC,EAAK,aAAaC,EAAGC,CAAC,EAExB,OAAOF,CACT,CCoBA,IAAMG,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA,QAmBvB,SAASC,GACdC,EACAC,EACAC,EACAC,EACA,CACA,IAAMC,EAAKH,EAAO,aAKZI,EAAS,CAACC,EAAmBC,IACjCC,EAAiBP,EAAQK,EAAWC,CAAS,EAGzCE,EAA0B,CAAC,EAC7BC,EAAW,EAkBf,GAjBAT,EAAO,SAAS,QAAQ,CAACU,EAASC,IAAQ,CACxC,IAAMC,EAAMC,GAAcb,EAAQU,EAASC,CAAG,EAI1CC,EAAI,MAAQ,IACZA,EAAI,QACNH,IACIN,EAAG,qBAAuB,SAEhCK,EAAK,KAAKI,CAAG,EACf,CAAC,EAMGJ,EAAK,SAAW,EAAG,OAEvB,IAAMM,EACJd,EAAO,SAAS,CAAC,GAAG,WAAW,gBAAgB,cAAgB,MAE3De,EAAOC,EAAG,MAAO,WAAY,CACjC,qBAAsBhB,EAAO,eAAe,aAC5C,sBAAuB,OAAOA,EAAO,eAAe,aAAa,CACnE,CAAC,EAGKiB,EAAeC,GAAalB,EAAQc,CAAQ,EAIlD,GAHAC,EAAK,YAAYE,EAAa,EAAE,EAG5Bd,EAAG,UAAU,eAAiBH,EAAO,OAAQ,CAC/C,IAAMmB,EAAYC,EAAgBpB,EAAO,MAAM,EAC3CmB,IACFJ,EAAK,YAAYI,EAAU,EAAE,EAC7BjB,IAAYiB,EAAU,IAAI,EAE9B,CAGA,IAAME,EAAOL,EAAG,MAAO,oBAAoB,EACrCM,EAAyD,CAAC,EAChEd,EAAK,QAASe,GAAa,CACzB,IAAMC,EAASC,GAAiBF,EAAUT,EAAUV,EAAQ,IAAM,CAEhEsB,EAAc,CAChB,CAAC,EACDJ,EAAW,KAAKE,CAAM,EACtBH,EAAK,YAAYG,EAAO,EAAE,CAC5B,CAAC,EACDT,EAAK,YAAYM,CAAI,EAErBN,EAAK,YAAYC,EAAG,MAAO,mBAAmB,CAAC,EAG/C,IAAMW,EAAgBC,GAAiB5B,CAAM,EAC7Ce,EAAK,YAAYY,EAAc,EAAE,EACjC,IAAME,EAAmB1B,EAAG,WAAW,QAAU2B,GAAiB,EAAI,KAClED,GAAkBd,EAAK,YAAYc,EAAiB,EAAE,EAG1D,IAAME,EAAMC,GAAUhC,EAAQS,EAAU,IAAM,CAC5C,IAAMwB,EAAyBzB,EAC5B,OAAQ0B,GAAMA,EAAE,QAAQ,EACxB,IAAKA,IAAO,CACX,cAAeA,EAAE,SAAU,GAC3B,SAAUA,EAAE,IACZ,WAAY,CACV,CAAE,IAAK,mBAAoB,MAAOlC,EAAO,EAAG,EAC5C,CAAE,IAAK,oBAAqB,MAAOA,EAAO,UAAW,CACvD,CACF,EAAE,EACAiC,EAAM,SAAW,GACrBhC,EAAYgC,CAAK,CACnB,CAAC,EACDlB,EAAK,YAAYgB,CAAG,EAEpBhB,EAAK,YACHC,EAAG,IAAK,kBAAmB,CAAE,aAAc,GAAI,YAAa,QAAS,CAAC,CACxE,EACAD,EAAK,YACHC,EAAG,OAAQ,qBAAsB,CAC/B,cAAe,GACf,YAAa,QACf,CAAC,CACH,EAEAjB,EAAU,YAAYgB,CAAI,EAE1BW,EAAc,EAEd,SAASA,GAAgB,CACvB,IAAMS,EAAa3B,EAAK,OAAO,CAAC4B,EAAKF,IAAM,CACzC,GAAI,CAACA,EAAE,SAAU,OAAOE,EACxB,IAAMC,EAAOC,EAAWJ,EAAE,SAAS,MAAM,MAAM,EAC/C,OAAOE,EAAMC,EAAOH,EAAE,GACxB,EAAG,CAAC,EACEK,EAAYC,GAAYL,EAAYnC,EAAO,eAAgBQ,CAAI,EAC/DiC,EAAe,KAAK,IAAI,EAAGN,EAAaI,CAAS,EAEvDZ,EAAc,OAAO,CAAE,WAAAQ,EAAY,UAAAI,EAAW,aAAAE,EAAc,SAAA3B,CAAS,CAAC,EAClEe,GACFA,EAAiB,OAAO,CAAE,aAAAY,EAAc,SAAA3B,CAAS,CAAC,EAEpDG,EAAa,QACXyB,GAAkB1C,EAAQmC,EAAYI,EAAWzB,CAAQ,CAC3D,CACF,CACF,CAIA,SAASD,GACPb,EACAU,EACAiC,EACiB,CACjB,IAAMC,EACJ5C,EAAO,qBAAqB2C,CAAY,GAAK,KAGzCE,EAAYnC,EAAQ,SAAS,MAAM,OACtCoC,GAAMA,EAAE,gBACX,EACMC,EACJH,GAAsBA,EAAmB,OAAS,EAC9CC,EAAU,OAAQC,GAAMF,EAAmB,SAASE,EAAE,EAAE,CAAC,EACzDD,EAEAG,EAAQD,EAAiB,SAAW,EACpCE,EAAWF,EAAiB,CAAC,GAAK,KAClCG,EAAMD,EAAW1C,EAAiBP,EAAQU,EAAQ,GAAIuC,EAAS,EAAE,EAAI,EAE3E,MAAO,CAAE,QAAAvC,EAAS,iBAAAqC,EAAkB,SAAAE,EAAU,IAAAC,EAAK,MAAAF,CAAM,CAC3D,CAUA,SAAS9B,GACPlB,EACAc,EACc,CACd,IAAMX,EAAKH,EAAO,aACZmD,EAASnC,EAAG,MAAO,kBAAkB,EACrCoC,EAAUpC,EAAG,MAAO,2BAA2B,EAE/CqC,EAAQrC,EAAG,KAAM,iBAAiB,EAIxC,GAHAqC,EAAM,YAAcrD,EAAO,MAC3BoD,EAAQ,YAAYC,CAAK,EAErBrD,EAAO,YAAa,CACtB,IAAMsD,EAAWtC,EAAG,IAAK,oBAAoB,EAC7CsC,EAAS,YAActD,EAAO,YAC9BoD,EAAQ,YAAYE,CAAQ,CAC9B,CACAH,EAAO,YAAYC,CAAO,EAE1B,IAAMG,EAAUvC,EAAG,OAAQ,0BAA2B,CACpD,oBAAqB,EACvB,CAAC,EACDmC,EAAO,YAAYI,CAAO,EAG1B,IAAMC,EAAiBC,GACrBzD,EACAA,EAAO,kBACPG,EAAG,QAAQ,aACb,EACA,OAAIqD,EAAe,YACjBD,EAAQ,YAAcC,EAAe,YAErCD,EAAQ,MAAM,QAAU,OAInB,CACL,GAAIJ,EACJ,QAAQO,EAAW,CACjB,GAAI,CAACvD,EAAG,QAAQ,cAAe,CAC7BoD,EAAQ,MAAM,QAAU,OACxB,MACF,CACIG,GACFH,EAAQ,YAAcG,EACtBH,EAAQ,MAAM,QAAU,IAExBA,EAAQ,MAAM,QAAU,MAE5B,CACF,CACF,CAEA,SAASb,GACP1C,EACAmC,EACAI,EACAzB,EACQ,CACR,GAAI,CAACd,EAAO,aAAa,QAAQ,cAAe,MAAO,GACvD,IAAM2D,EAAUxB,EAAaI,EAC7B,GAAIoB,GAAW,EAAG,MAAO,GACzB,IAAMC,EAAK5D,EAAO,eAClB,OAAI4D,EAAG,eAAiB,cAAgBA,EAAG,cAAgB,EAClD,IAAI,KAAK,MAAMA,EAAG,aAAa,CAAC,IAErCA,EAAG,eAAiB,gBAAkBA,EAAG,cAAgB,EACpD,IAAIC,EAAY,KAAK,MAAMD,EAAG,cAAgB,GAAG,EAAG9C,CAAQ,CAAC,GAE/D,IAAI+C,EAAYF,EAAS7C,CAAQ,CAAC,EAC3C,CAOA,SAASW,GACPqC,EACAhD,EACAV,EACA2D,EACkB,CAClB,IAAMC,EAAQhD,EACZ,MACA8C,EAAM,MACF,mDACA,wBACJ,CACE,kBAAmBA,EAAM,QAAQ,GAAG,QAAQ,QAAS,EAAE,EACvD,GAAIA,EAAM,MAAQ,CAAE,gBAAiB,MAAO,EAAI,CAAC,CACnD,CACF,EAGMG,EAAQjD,EAAG,MAAO,sBAAuB,CAAE,iBAAkB,EAAG,CAAC,EACvE,GAAI8C,EAAM,QAAQ,cAAe,CAC/B,IAAMI,EAAM,SAAS,cAAc,KAAK,EACxCA,EAAI,IAAMC,EAAkBL,EAAM,QAAQ,cAAc,IAAK,CAC3D,MAAOM,EACP,OAAQA,EACR,KAAM,QACR,CAAC,EACDF,EAAI,IAAMJ,EAAM,QAAQ,cAAc,SAAWA,EAAM,QAAQ,MAC/DI,EAAI,MAAQE,EACZF,EAAI,OAASE,EACbF,EAAI,QAAU,OACdD,EAAM,YAAYC,CAAG,CACvB,MACED,EAAM,mBAAmB,YAAapE,EAAqB,EAI7D,IAAIwE,EAAkC,KACjCP,EAAM,QACTO,EAAcrD,EAAG,OAAQ,sBAAuB,CAC9C,iBAAkB,EACpB,CAAC,EACDqD,EAAY,YAAc,OAAOP,EAAM,GAAG,EAC1CG,EAAM,YAAYI,CAAW,GAE/BL,EAAM,YAAYC,CAAK,EAGvB,IAAMK,EAAOtD,EAAG,MAAO,wBAAwB,EACzCuD,EAAO,SAAS,cAAc,GAAG,EAMvC,GALAA,EAAK,UAAY,yBACjBA,EAAK,KAAO,aAAaT,EAAM,QAAQ,MAAM,GAC7CS,EAAK,YAAcT,EAAM,QAAQ,MACjCQ,EAAK,YAAYC,CAAI,EAEjBT,EAAM,MAAO,CACf,IAAMU,EAAWxD,EAAG,OAAQ,qBAAqB,EACjDwD,EAAS,YAAc,eACvBF,EAAK,YAAYE,CAAQ,CAC3B,SAAWV,EAAM,SAAU,CAEzB,IAAMW,EAASzD,EAAG,OAAQ,0BAA0B,EAC9C0D,EAAU1D,EAAG,OAAQ,kCAAmC,CAC5D,6BAA8B,EAChC,CAAC,EACK2D,EAAU3D,EAAG,OAAQ,0BAA2B,CACpD,qBAAsB,EACxB,CAAC,EACDyD,EAAO,YAAYC,CAAO,EAC1BD,EAAO,YAAYE,CAAO,EAC1BL,EAAK,YAAYG,CAAM,EAEvB,IAAMG,EAAqBC,GAA4B,CACrD,IAAMxC,EAAOC,EAAWuC,EAAQ,MAAM,MAAM,EAE5C,GADAF,EAAQ,YAAcd,EAAYxB,EAAMvB,CAAQ,EAC5C+D,EAAQ,eAAgB,CAC1B,IAAMC,EAAMxC,EAAWuC,EAAQ,eAAe,MAAM,EAChDC,EAAMzC,GACRqC,EAAQ,YAAcb,EAAYiB,EAAKhE,CAAQ,EAC/C4D,EAAQ,gBAAgB,QAAQ,GAEhCA,EAAQ,aAAa,SAAU,EAAE,CAErC,MACEA,EAAQ,aAAa,SAAU,EAAE,CAErC,EAKA,GAHAE,EAAkBd,EAAM,QAAQ,EAG5BA,EAAM,iBAAiB,OAAS,EAAG,CACrC,IAAMiB,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,UAAY,2BACnBA,EAAO,aAAa,sBAAuB,EAAE,EAC7CA,EAAO,aACL,aACA,sBAAsBjB,EAAM,QAAQ,KAAK,EAC3C,EACAA,EAAM,iBAAiB,QAASe,GAAY,CAC1C,IAAMG,EAAM,SAAS,cAAc,QAAQ,EAC3CA,EAAI,MAAQH,EAAQ,GACpBG,EAAI,YAAcH,EAAQ,MACtBA,EAAQ,KAAOf,EAAM,UAAU,KAAIkB,EAAI,SAAW,IACtDD,EAAO,YAAYC,CAAG,CACxB,CAAC,EACDD,EAAO,iBAAiB,SAAU,IAAM,CACtC,IAAMF,EAAUf,EAAM,iBAAiB,KACpC,GAAM,EAAE,KAAOiB,EAAO,KACzB,EACKF,IACLf,EAAM,SAAWe,EAGjBf,EAAM,IAAM1D,EAAO0D,EAAM,QAAQ,GAAIe,EAAQ,EAAE,EAC3CR,IAAaA,EAAY,YAAc,OAAOP,EAAM,GAAG,GAC3Dc,EAAkBC,CAAO,EACzBd,EAAgB,EAClB,CAAC,EACDO,EAAK,YAAYS,CAAM,CACzB,SACEjB,EAAM,iBAAiB,SAAW,GAClCA,EAAM,QAAQ,SAAS,MAAM,OAAS,EACtC,CAGA,IAAMmB,EAAQjE,EAAG,OAAQ,yBAAyB,EAClDiE,EAAM,YAAcnB,EAAM,iBAAiB,CAAC,EAAE,MAC9CQ,EAAK,YAAYW,CAAK,CACxB,CACF,CAEA,OAAAjB,EAAM,YAAYM,CAAI,EACf,CAAE,GAAIN,EAAO,MAAAF,CAAM,CAC5B,CAYA,SAASlC,GAAiB5B,EAAwC,CAChE,IAAMY,EAAMI,EAAG,MAAO,mBAAmB,EACnCkE,EAAQlE,EAAG,OAAQ,0BAA0B,EACnDkE,EAAM,YAAc,eACpBtE,EAAI,YAAYsE,CAAK,EAErB,IAAMT,EAASzD,EAAG,OAAQ,2BAA2B,EAC/C0D,EAAU1D,EAAG,OAAQ,0BAA2B,CACpD,qBAAsB,EACxB,CAAC,EACD0D,EAAQ,MAAM,QAAU,OACxBD,EAAO,YAAYC,CAAO,EAC1B,IAAMS,EAAOnE,EAAG,OAAQ,uBAAwB,CAC9C,kBAAmB,EACrB,CAAC,EACD,OAAAyD,EAAO,YAAYU,CAAI,EACvBvE,EAAI,YAAY6D,CAAM,EAEf,CACL,GAAI7D,EACJ,OAAO,CAAE,WAAAuB,EAAY,UAAAI,EAAW,aAAAE,EAAc,SAAA3B,CAAS,EAAG,CACxDqE,EAAK,YAActB,EAAYtB,EAAWzB,CAAQ,EAC9Cd,EAAO,aAAa,QAAQ,oBAAsByC,EAAe,GACnEiC,EAAQ,YAAcb,EAAY1B,EAAYrB,CAAQ,EACtD4D,EAAQ,MAAM,QAAU,IAExBA,EAAQ,MAAM,QAAU,MAE5B,CACF,CACF,CAOA,SAAS5C,IAAqC,CAC5C,IAAMsD,EAAMpE,EAAG,MAAO,wBAAyB,CAC7C,mBAAoB,EACtB,CAAC,EACKkE,EAAQ,SAAS,cAAc,MAAM,EAC3CA,EAAM,YAAc,WACpBE,EAAI,YAAYF,CAAK,EACrB,IAAMG,EAASrE,EAAG,OAAQ,GAAI,CAAE,sBAAuB,EAAG,CAAC,EAC3D,OAAAoE,EAAI,YAAYC,CAAM,EACf,CACL,GAAID,EACJ,OAAO,CAAE,aAAA3C,EAAc,SAAA3B,CAAS,EAAG,CACjC,GAAI2B,GAAgB,EAAG,CACrB2C,EAAI,MAAM,QAAU,OACpB,MACF,CACAA,EAAI,MAAM,QAAU,GACpBC,EAAO,YAAcxB,EAAYpB,EAAc3B,CAAQ,CACzD,CACF,CACF,CAEA,SAASkB,GACPhC,EACAS,EACA6E,EACa,CACb,IAAMJ,EACJzE,EAAW,EACP,GAAGA,CAAQ,QAAQA,IAAa,EAAI,GAAK,GAAG,gBAC5CT,EAAO,aAAa,IAAI,SAAW,cACnCuF,EAASC,EAAeN,CAAK,EACnC,OAAIzE,EAAW,EACb8E,EAAO,SAAW,GAElBA,EAAO,iBAAiB,QAAS,IAAM,CACjCA,EAAO,UACXD,EAAQ,CACV,CAAC,EAEIC,CACT,CAIA,SAAS/C,GACPL,EACAsD,EACAjF,EACQ,CACR,GAAIiF,EAAS,eAAiB,aAAc,CAE1C,IAAIlD,EAAY,EAChB,QAAW,KAAK/B,EAAM,CACpB,GAAI,CAAC,EAAE,SAAU,SACjB,IAAM6B,EAAOC,EAAW,EAAE,SAAS,MAAM,MAAM,EACzCoD,EAAM,KAAK,MAAOrD,EAAOoD,EAAS,cAAiB,GAAG,EACtDE,EAAU,KAAK,IAAI,EAAGtD,EAAOqD,CAAG,EACtCnD,GAAaoD,EAAU,EAAE,GAC3B,CACA,OAAOpD,CACT,CAEA,OAAO,KAAK,IAAI,EAAGJ,EAAa,KAAK,MAAMsD,EAAS,cAAgB,GAAG,CAAC,CAC1E,CC/eA,IAAMG,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA,QAOxBC,GAAgB;AAAA;AAAA;AAAA;AAAA,QAMhBC,GAAiB;AAAA;AAAA;AAAA;AAAA,QAMjBC,GAAwB;AAAA;AAAA;AAAA,QAKvB,SAASC,GACdC,EACAC,EACAC,EACAC,EACA,CACA,IAAMC,EAAKH,EAAO,aACZI,EACJJ,EAAO,SAAS,CAAC,GAAG,WAAW,gBAAgB,cAAgB,MAC3DK,EAAcL,EAAO,aAAe,EACpCM,EAASN,EAAO,aAAeK,EAG/BE,EAAWC,GAAsBR,EAAQG,EAAG,kBAAkB,EAKpE,GAJqBI,EAAS,OAAQE,GAAM,CAACA,EAAE,KAAK,EAAE,OAInCJ,EAAa,OAEhC,IAAMK,EAA0B,CAAC,EAC3BC,EAAOC,EAAG,MAAO,eAAgB,CACrC,yBAA0B,OAAOP,CAAW,EAC5C,oBAAqB,OAAOC,CAAM,CACpC,CAAC,EAGKO,EAASC,GAAad,CAAM,EAIlC,GAHAW,EAAK,YAAYE,CAAM,EAGnBV,EAAG,UAAU,eAAiBH,EAAO,OAAQ,CAC/C,IAAMe,EAAYC,EAAgBhB,EAAO,MAAM,EAC3Ce,IACFJ,EAAK,YAAYI,EAAU,EAAE,EAC7Bb,IAAYa,EAAU,IAAI,EAE9B,CAGA,IAAME,EAAWC,GAAeb,CAAW,EAC3CM,EAAK,YAAYM,EAAS,EAAE,EAG5B,IAAME,EAAiBP,EAAG,MAAO,sBAAuB,CACtD,uBAAwB,EAC1B,CAAC,EACDD,EAAK,YAAYQ,CAAc,EAE/BR,EAAK,YAAYC,EAAG,MAAO,mBAAmB,CAAC,EAG/C,IAAMQ,EAAiBC,GAAqBlB,EAAG,QAAQ,kBAAkB,EACzEQ,EAAK,YAAYS,EAAe,EAAE,EAElC,IAAME,EAAanB,EAAG,WAAW,QAC7BoB,GAAiB,EACjB,KACAD,GAAYX,EAAK,YAAYW,EAAW,EAAE,EAG9C,IAAME,EAAcZ,EAAG,MAAO,kCAAmC,CAC/D,yBAA0B,EAC5B,CAAC,EACKa,EAAkBb,EACtB,OACA,sCACF,EACAa,EAAgB,YAAc,UAAUpB,CAAW,sBACnDmB,EAAY,YAAYC,CAAe,EACvCd,EAAK,YAAYa,CAAW,EAG5B,IAAME,EAAQC,GAAY3B,EAAQO,EAAUH,EAAU,CACpD,WAAYD,EAAG,WACf,MAAO,CAACyB,EAASC,IAAYC,EAAaF,EAASC,CAAO,EAC1D,SAAU,CAACE,EAAWC,IAAcC,EAAgBF,EAAWC,CAAS,EACxE,SAAU,CAACD,EAAWC,IACpBtB,EAAW,OACRwB,GAAMA,EAAE,YAAcH,GAAaG,EAAE,YAAcF,CACtD,EAAE,OACJ,UAAW,IAAMtB,EAAW,QAAUJ,CACxC,CAAC,EACDK,EAAK,YAAYe,EAAM,EAAE,EAGzB,IAAMS,EAAMC,EAAe,UAAU/B,CAAW,kBAAkB,EAClE8B,EAAI,SAAW,GACfA,EAAI,iBAAiB,QAAS,IAAM,CAClC,GAAIA,EAAI,SAAU,OAClB,IAAME,EAAyB3B,EAAW,IAAKwB,IAAO,CACpD,cAAeA,EAAE,UACjB,SAAU,EACV,WAAY,CACV,CAAE,IAAK,mBAAoB,MAAOlC,EAAO,EAAG,EAC5C,CAAE,IAAK,oBAAqB,MAAOA,EAAO,UAAW,CACvD,CACF,EAAE,EACFC,EAAYoC,CAAK,CACnB,CAAC,EACD1B,EAAK,YAAYwB,CAAG,EAEpBxB,EAAK,YACHC,EAAG,IAAK,kBAAmB,CAAE,aAAc,GAAI,YAAa,QAAS,CAAC,CACxE,EACAD,EAAK,YACHC,EAAG,OAAQ,qBAAsB,CAC/B,cAAe,GACf,YAAa,QACf,CAAC,CACH,EAEAb,EAAU,YAAYY,CAAI,EAK1B,IAAM2B,EAAgB/B,EAAS,KAAMgC,GAAO,CAACA,EAAG,KAAK,EAC/CC,EACJF,GAAe,uBAAyBA,GAAe,SAAS,CAAC,EAC/DA,GAAiBE,GACnB9B,EAAW,KAAK,CACd,UAAW4B,EAAc,QAAQ,GACjC,aAAcA,EAAc,QAAQ,MACpC,UAAWE,EAAa,GACxB,aAAcA,EAAa,MAC3B,SAAUF,EAAc,QAAQ,eAAe,KAAO,KACtD,WAAYG,EAAWD,EAAa,MAAM,MAAM,EAChD,aAAcA,EAAa,eACvBC,EAAWD,EAAa,eAAe,MAAM,EAC7C,IACN,CAAC,EAOHE,EAAc,EAId,SAASZ,EAAaF,EAAkBC,EAAyB,CAC3DnB,EAAW,QAAUJ,IACzBI,EAAW,KAAK,CACd,UAAWkB,EAAQ,GACnB,aAAcA,EAAQ,MACtB,UAAWC,EAAQ,GACnB,aAAcA,EAAQ,MACtB,SAAUD,EAAQ,eAAe,KAAO,KACxC,WAAYa,EAAWZ,EAAQ,MAAM,MAAM,EAC3C,aAAcA,EAAQ,eAClBY,EAAWZ,EAAQ,eAAe,MAAM,EACxC,IACN,CAAC,EACDa,EAAc,EAChB,CAEA,SAAST,EAAgBF,EAAmBC,EAAmB,CAC7D,IAAMW,EAAMjC,EAAW,UACpBwB,GAAMA,EAAE,YAAcH,GAAaG,EAAE,YAAcF,CACtD,EACIW,IAAQ,KACZjC,EAAW,OAAOiC,EAAK,CAAC,EACxBD,EAAc,EAChB,CAEA,SAASE,EAAaC,EAAe,CAC/BA,EAAQ,GAAKA,GAASnC,EAAW,SACrCA,EAAW,OAAOmC,EAAO,CAAC,EAC1BH,EAAc,EAChB,CAEA,SAASA,GAAgB,CACvBI,EAAY,EACZ7B,EAAS,OAAOP,EAAW,MAAM,EACjCU,EAAe,OAAOV,EAAYV,EAAQI,CAAQ,EAC9CkB,GAAYA,EAAW,OAAOZ,EAAYV,EAAQI,CAAQ,EAC9DoB,EAAY,MAAM,QAAUd,EAAW,SAAW,EAAI,GAAK,OAC3DgB,EAAM,cAAc,EACpBqB,EAAU,CACZ,CAEA,SAASD,GAAc,CACrB3B,EAAe,UAAY,GAC3B,IAAM6B,EAAa,KAAK,IAAI3C,EAAaK,EAAW,MAAM,EAC1D,QAASuC,EAAI,EAAGA,EAAID,EAAYC,IAAK,CACnC,IAAMC,EAAYxC,EAAWuC,CAAC,EAC1BC,EACF/B,EAAe,YACbgC,GAAiBD,EAAWD,EAAG7C,EAAU,IAAMwC,EAAaK,CAAC,CAAC,CAChE,EAEA9B,EAAe,YACbiC,GAAgBH,EAAG,IAAMvB,EAAM,KAAK,CAAC,CACvC,CAEJ,CACF,CAEA,SAASqB,GAAY,CAInB,IAAMM,EAAQ3C,EAAW,OACrB2C,EAAQhD,GACV8B,EAAI,SAAW,GACfmB,GAAYnB,EAAK,UAAU9B,EAAcgD,CAAK,iBAAiB,IAE/DlB,EAAI,SAAW,GACfmB,GAAYnB,EAAKhC,EAAG,IAAI,SAAW,aAAa,EAEpD,CACF,CAIA,SAASW,GAAad,EAAyC,CAC7D,IAAMG,EAAKH,EAAO,aACZa,EAASD,EAAG,MAAO,kBAAkB,EACrC2C,EAAU3C,EAAG,MAAO,2BAA2B,EAC/C4C,EAAQ5C,EAAG,KAAM,iBAAiB,EAKxC,GAJA4C,EAAM,YAAcxD,EAAO,MAC3BuD,EAAQ,YAAYC,CAAK,EACzB3C,EAAO,YAAY0C,CAAO,EAEtBpD,EAAG,QAAQ,cAAe,CAC5B,GAAM,CAAE,aAAAsD,EAAc,cAAAC,CAAc,EAAI1D,EAAO,eAC3C2D,EAAuB,KAS3B,GARIF,IAAiB,cAAgBC,EAAgB,EACnDC,EAAQ,IAAI,KAAK,MAAMD,CAAa,CAAC,IAC5BD,IAAiB,gBAAkBC,EAAgB,IAC5DC,EAAQ,IAAIC,EACV,KAAK,MAAMF,EAAgB,GAAG,EAC9B1D,EAAO,SAAS,CAAC,GAAG,WAAW,gBAAgB,cAAgB,KACjE,CAAC,IAEC2D,EAAO,CACT,IAAME,EAAQjD,EAAG,OAAQ,yBAAyB,EAClDiD,EAAM,YAAcF,EACpB9C,EAAO,YAAYgD,CAAK,CAC1B,CACF,CACA,OAAOhD,CACT,CAEA,SAASK,GAAeb,EAAqB,CAC3C,IAAMyD,EAAOlD,EAAG,MAAO,wBAAwB,EACzCmD,EAASnD,EAAG,MAAO,+BAA+B,EAClDyC,EAAQzC,EAAG,OAAQ,+BAAgC,CACvD,sBAAuB,EACzB,CAAC,EACDyC,EAAM,YAAc,QAAQhD,CAAW,YACvC0D,EAAO,YAAYV,CAAK,EACxB,IAAMW,EAAYpD,EAAG,OAAQ,mCAAoC,CAC/D,0BAA2B,EAC7B,CAAC,EACDoD,EAAU,YAAc,GAAG3D,CAAW,cACtC0D,EAAO,YAAYC,CAAS,EAC5BF,EAAK,YAAYC,CAAM,EAEvB,IAAME,EAAQrD,EAAG,MAAO,+BAAgC,CACtD,KAAM,cACN,gBAAiB,IACjB,gBAAiB,IACjB,gBAAiB,OAAOP,CAAW,CACrC,CAAC,EACK6D,EAAOtD,EAAG,MAAO,8BAA+B,CACpD,qBAAsB,EACxB,CAAC,EACDsD,EAAK,MAAM,MAAQ,KACnBD,EAAM,YAAYC,CAAI,EACtBJ,EAAK,YAAYG,CAAK,EAEtB,SAASE,EAAOC,EAAkB,CAChC,IAAMC,EAAM,KAAK,IAAI,IAAMD,EAAW/D,EAAe,GAAG,EACxDgD,EAAM,YAAc,GAAGe,CAAQ,OAAO/D,CAAW,YAC7C+D,GAAY/D,EACd2D,EAAU,YAAc,WAExBA,EAAU,YAAc,GAAG3D,EAAc+D,CAAQ,cAEnDF,EAAK,MAAM,MAAQ,GAAGG,CAAG,IACzBJ,EAAM,aAAa,gBAAiB,OAAO,KAAK,IAAIG,EAAU/D,CAAW,CAAC,CAAC,CAC7E,CAEA,MAAO,CAAE,GAAIyD,EAAM,OAAAK,CAAO,CAC5B,CAEA,SAASf,GAAgBP,EAAeyB,EAAkC,CACxE,IAAMC,EAAO3D,EACX,MACA,qEACA,CACE,YAAa,OAAOiC,EAAQ,CAAC,EAC7B,SAAU,IACV,KAAM,SACN,aAAc,6BAChB,CACF,EACM2B,EAAQ5D,EAAG,MAAO,2BAA2B,EACnD4D,EAAM,UAAY7E,GAClB4E,EAAK,YAAYC,CAAK,EACtB,IAAMC,EAAO7D,EAAG,OAAQ,0BAA0B,EAClD,OAAA6D,EAAK,YAAc,iBACnBF,EAAK,YAAYE,CAAI,EACrBF,EAAK,iBAAiB,QAASD,CAAO,EACtCC,EAAK,iBAAiB,UAAY9D,GAAM,EAClCA,EAAE,MAAQ,SAAWA,EAAE,MAAQ,OACjCA,EAAE,eAAe,EACjB6D,EAAQ,EAEZ,CAAC,EACMC,CACT,CAEA,SAASpB,GACPD,EACAL,EACAzC,EACAsE,EACa,CACb,IAAMH,EAAO3D,EACX,MACA,sEACA,CAAE,YAAa,OAAOiC,EAAQ,CAAC,CAAE,CACnC,EACM2B,EAAQ5D,EAAG,MAAO,sBAAuB,CAAE,iBAAkB,EAAG,CAAC,EACvE,GAAIsC,EAAU,SAAU,CACtB,IAAMyB,EAAM,SAAS,cAAc,KAAK,EACxCA,EAAI,IAAMC,EAAkB1B,EAAU,SAAU,CAC9C,MAAO2B,EACP,OAAQA,EACR,KAAM,QACR,CAAC,EACDF,EAAI,IAAMzB,EAAU,aACpByB,EAAI,MAAQE,EACZF,EAAI,OAASE,EACbF,EAAI,QAAU,OACdH,EAAM,YAAYG,CAAG,CACvB,MACEH,EAAM,mBAAmB,YAAa9E,EAAqB,EAE7D6E,EAAK,YAAYC,CAAK,EAEtB,IAAMM,EAAOlE,EAAG,MAAO,2BAA2B,EAC5C4C,EAAQ5C,EAAG,OAAQ,4BAA4B,EAGrD,GAFA4C,EAAM,YAAcN,EAAU,aAC9B4B,EAAK,YAAYtB,CAAK,EAClBN,EAAU,cAAgBA,EAAU,eAAiB,gBAAiB,CACxE,IAAMrB,EAAUjB,EAAG,OAAQ,8BAA8B,EACzDiB,EAAQ,YAAcqB,EAAU,aAChC4B,EAAK,YAAYjD,CAAO,CAC1B,CACA,IAAMkD,EAAYnE,EAAG,OAAQ,4BAA4B,EACzD,GACEsC,EAAU,cACVA,EAAU,aAAeA,EAAU,WACnC,CACA,IAAM8B,EAAUpE,EAAG,OAAQ,8BAA8B,EACzDoE,EAAQ,YAAcpB,EAAYV,EAAU,aAAc9C,CAAQ,EAClE2E,EAAU,YAAYC,CAAO,CAC/B,CACA,IAAMC,EAAU,SAAS,cAAc,MAAM,EAC7CA,EAAQ,YAAcrB,EAAYV,EAAU,WAAY9C,CAAQ,EAChE2E,EAAU,YAAYE,CAAO,EAC7BH,EAAK,YAAYC,CAAS,EAC1BR,EAAK,YAAYO,CAAI,EAErB,IAAMI,EAAS,SAAS,cAAc,QAAQ,EAC9C,OAAAA,EAAO,KAAO,SACdA,EAAO,UAAY,4BACnBA,EAAO,aAAa,aAAc,UAAUhC,EAAU,YAAY,EAAE,EACpEgC,EAAO,UAAYtF,GACnBsF,EAAO,iBAAiB,QAAUzE,GAAM,CACtCA,EAAE,gBAAgB,EAClBiE,EAAS,CACX,CAAC,EACDH,EAAK,YAAYW,CAAM,EAChBX,CACT,CAEA,SAASlD,GAAqB8D,EAA6B,CACzD,IAAMrB,EAAOlD,EAAG,MAAO,oBAAqB,CAAE,uBAAwB,EAAG,CAAC,EAC1EkD,EAAK,MAAM,QAAU,OACrB,IAAMH,EAAQ/C,EAAG,OAAQ,0BAA0B,EACnD+C,EAAM,YAAc,eACpBG,EAAK,YAAYH,CAAK,EACtB,IAAMyB,EAASxE,EAAG,OAAQ,2BAA2B,EAC/CoE,EAAUpE,EAAG,OAAQ,0BAA2B,CACpD,qBAAsB,EACxB,CAAC,EACGuE,GAAoBC,EAAO,YAAYJ,CAAO,EAClD,IAAMK,EAAOzE,EAAG,OAAQ,uBAAwB,CAAE,kBAAmB,EAAG,CAAC,EACzEwE,EAAO,YAAYC,CAAI,EACvBvB,EAAK,YAAYsB,CAAM,EAEvB,SAASjB,EACPzD,EACAV,EACAI,EACA,CACA,GAAIM,EAAW,SAAW,EAAG,CAC3BoD,EAAK,MAAM,QAAU,OACrB,MACF,CACAA,EAAK,MAAM,QAAU,GACrB,IAAMwB,EAAa5E,EAAW,OAAO,CAACwB,EAAGqD,IAAQrD,EAAIqD,EAAI,WAAY,CAAC,EAChEC,EAAYC,EAAuBH,EAAYtF,EAAO,cAAc,EACtEmF,GAAsBG,EAAaE,GACrCR,EAAQ,YAAcpB,EAAY0B,EAAYlF,CAAQ,EACtD4E,EAAQ,MAAM,QAAU,IAExBA,EAAQ,MAAM,QAAU,OAE1BK,EAAK,YAAczB,EAAY4B,EAAWpF,CAAQ,CACpD,CAEA,MAAO,CAAE,GAAI0D,EAAM,OAAAK,CAAO,CAC5B,CAEA,SAAS5C,IAAmB,CAC1B,IAAMuC,EAAOlD,EAAG,MAAO,wBAAyB,CAC9C,mBAAoB,EACtB,CAAC,EACDkD,EAAK,MAAM,QAAU,OACrB,IAAM4B,EAAU,SAAS,cAAc,MAAM,EAC7CA,EAAQ,YAAc,WACtB5B,EAAK,YAAY4B,CAAO,EACxB,IAAMC,EAAS/E,EAAG,OAAQ,GAAI,CAAE,sBAAuB,EAAG,CAAC,EAC3DkD,EAAK,YAAY6B,CAAM,EAEvB,SAASxB,EACPzD,EACAV,EACAI,EACA,CACA,GAAIM,EAAW,SAAW,EAAG,CAC3BoD,EAAK,MAAM,QAAU,OACrB,MACF,CACA,IAAMwB,EAAa5E,EAAW,OAAO,CAACwB,EAAGqD,IAAQrD,EAAIqD,EAAI,WAAY,CAAC,EAChEC,EAAYC,EAAuBH,EAAYtF,EAAO,cAAc,EACpE4F,EAAU,KAAK,IAAI,EAAGN,EAAaE,CAAS,EAClD,GAAII,GAAW,EAAG,CAChB9B,EAAK,MAAM,QAAU,OACrB,MACF,CACAA,EAAK,MAAM,QAAU,GACrB6B,EAAO,YAAc/B,EAAYgC,EAASxF,CAAQ,CACpD,CAEA,MAAO,CAAE,GAAI0D,EAAM,OAAAK,CAAO,CAC5B,CAYA,SAASxC,GACP3B,EACAO,EACAH,EACAyF,EACA,CACA,IAAMC,EAAUlF,EAAG,MAAO,8BAA+B,CACvD,qBAAsB,GACtB,kBAAmBZ,EAAO,EAC5B,CAAC,EACD8F,EAAQ,MAAM,QAAU,OAExB,IAAMpE,EAAQd,EAAG,MAAO,sBAAuB,CAC7C,KAAM,SACN,aAAc,OACd,kBAAmB,kBAAkBmF,GAAW/F,EAAO,EAAE,CAAC,GAC1D,SAAU,IACZ,CAAC,EAGKgG,EAAcpF,EAAG,MAAO,4BAA4B,EACpDqF,EAAarF,EAAG,KAAM,4BAA6B,CACvD,GAAI,kBAAkBmF,GAAW/F,EAAO,EAAE,CAAC,EAC7C,CAAC,EACDiG,EAAW,YAAc,eACzBD,EAAY,YAAYC,CAAU,EAClC,IAAMC,EAAW,SAAS,cAAc,QAAQ,EAChDA,EAAS,KAAO,SAChBA,EAAS,UAAY,4BACrBA,EAAS,aAAa,mBAAoB,EAAE,EAC5CA,EAAS,aAAa,aAAc,OAAO,EAC3CA,EAAS,UAAYtG,GACrBsG,EAAS,iBAAiB,QAASC,CAAK,EACxCH,EAAY,YAAYE,CAAQ,EAChCxE,EAAM,YAAYsE,CAAW,EAG7B,IAAII,EAAuC,KACvCC,EAA2C,KAC/C,GAAIR,EAAS,WAAY,CACvB,IAAMS,EAAa1F,EAAG,MAAO,4BAA4B,EACzDwF,EAAc,SAAS,cAAc,OAAO,EAC5CA,EAAY,KAAO,OACnBA,EAAY,UAAY,mCACxBA,EAAY,aAAa,oBAAqB,EAAE,EAChDA,EAAY,aAAa,OAAQ,WAAW,EAC5CA,EAAY,aAAa,aAAc,iBAAiB,EACxDA,EAAY,aAAa,cAAe,iBAAiB,EACzDA,EAAY,aAAe,MAC3BA,EAAY,iBAAiB,QAAS,IAAMG,EAAY,CAAC,EACzDD,EAAW,YAAYF,CAAW,EAElCC,EAAiB,SAAS,cAAc,QAAQ,EAChDA,EAAe,KAAO,SACtBA,EAAe,UAAY,mCAC3BA,EAAe,aAAa,0BAA2B,EAAE,EACzDA,EAAe,aAAa,aAAc,cAAc,EACxDA,EAAe,MAAM,QAAU,OAC/BA,EAAe,UAAYxG,GAC3BwG,EAAe,iBAAiB,QAAS,IAAM,CACxCD,IACLA,EAAY,MAAQ,GACpBG,EAAY,EACZH,EAAY,MAAM,EACpB,CAAC,EACDE,EAAW,YAAYD,CAAc,EACrC3E,EAAM,YAAY4E,CAAU,CAC9B,CAGA,IAAME,EAAO5F,EAAG,MAAO,2BAA4B,CACjD,kBAAmB,EACrB,CAAC,EACDc,EAAM,YAAY8E,CAAI,EAEtB,IAAMC,EAAQ7F,EAAG,MAAO,4BAA6B,CACnD,mBAAoB,EACtB,CAAC,EACD6F,EAAM,MAAM,QAAU,OACtB,IAAMC,EAAY,SAAS,cAAc,GAAG,EAC5CA,EAAU,YAAc,iCACxBD,EAAM,YAAYC,CAAS,EAC3BhF,EAAM,YAAY+E,CAAK,EAEvB,IAAME,EAAO/F,EAAG,OAAQ,qBAAsB,CAC5C,kBAAmB,GACnB,YAAa,QACf,CAAC,EACDc,EAAM,YAAYiF,CAAI,EAEtBb,EAAQ,YAAYpE,CAAK,EAGzB,IAAIkF,EAAY,GACVC,EAKD,CAAC,EAEN,SAASC,GAAY,CACfF,IACJA,EAAY,GACZJ,EAAK,UAAY,GAEjBjG,EAAS,QAASgC,GAAO,CACvB,IAAMV,EAAUU,EAAG,uBAAyBA,EAAG,SAAS,CAAC,EACzD,GAAI,CAACV,EAAS,OAEd,IAAMkF,EAAYnG,EAChB,MACA2B,EAAG,MACC,oEACA,8BACJ,CAAE,kBAAmBA,EAAG,QAAQ,GAAG,QAAQ,QAAS,EAAE,CAAE,CAC1D,EAEMiC,EAAQ5D,EAAG,MAAO,mCAAmC,EAC3D,GAAI2B,EAAG,QAAQ,cAAe,CAC5B,IAAMoC,EAAM,SAAS,cAAc,KAAK,EACxCA,EAAI,IAAMC,EAAkBrC,EAAG,QAAQ,cAAc,IAAK,CACxD,MAAOsC,EACP,OAAQA,EACR,KAAM,QACR,CAAC,EACDF,EAAI,IAAMpC,EAAG,QAAQ,cAAc,SAAWA,EAAG,QAAQ,MACzDoC,EAAI,MAAQE,EACZF,EAAI,OAASE,EACbF,EAAI,QAAU,OACdH,EAAM,YAAYG,CAAG,CACvB,MACEH,EAAM,mBAAmB,YAAa9E,EAAqB,EAE7DqH,EAAU,YAAYvC,CAAK,EAE3B,IAAMM,EAAOlE,EAAG,MAAO,kCAAkC,EACnD4C,EAAQ5C,EAAG,OAAQ,mCAAmC,EAC5D4C,EAAM,YAAcjB,EAAG,QAAQ,MAC/BuC,EAAK,YAAYtB,CAAK,EACtB,IAAMwD,EAAQpG,EAAG,OAAQ,mCAAmC,EAG5D,GAFAoG,EAAM,YAAcpD,EAAYnB,EAAWZ,EAAQ,MAAM,MAAM,EAAGzB,CAAQ,EAC1E0E,EAAK,YAAYkC,CAAK,EAClBzE,EAAG,MAAO,CACZ,IAAM0E,EAAUrG,EAAG,OAAQ,oCAAoC,EAC/DqG,EAAQ,YAAc,WACtBnC,EAAK,YAAYmC,CAAO,CAC1B,CAGA,GAFAF,EAAU,YAAYjC,CAAI,EAErBvC,EAAG,MAgDNsE,EAAY,KAAK,CACf,GAAIE,EACJ,QAASxE,EAAG,QACZ,QAAAV,EACA,YAAa,IAAM,CAAC,CACtB,CAAC,MArDY,CACb,IAAMqF,EAAS,SAAS,cAAc,QAAQ,EAC9CA,EAAO,KAAO,SACdA,EAAO,UAAY,0BACnBA,EAAO,aAAa,aAAc,OAAO3E,EAAG,QAAQ,KAAK,EAAE,EAC3D2E,EAAO,UAAYvH,GACnB,IAAMwH,EAAavG,EAAG,OAAQ,qBAAqB,EACnDuG,EAAW,MAAM,QAAU,OAC3BD,EAAO,YAAYC,CAAU,EAC7BD,EAAO,iBAAiB,QAAS,IAAM,CACjCrB,EAAS,UAAU,GACvBA,EAAS,MAAMtD,EAAG,QAASV,CAAO,CACpC,CAAC,EACDkF,EAAU,YAAYG,CAAM,EAG5B,IAAME,EAAY,SAAS,cAAc,QAAQ,EACjDA,EAAU,KAAO,SACjBA,EAAU,UAAY,4BACtBA,EAAU,aAAa,aAAc,cAAc7E,EAAG,QAAQ,KAAK,EAAE,EACrE6E,EAAU,MAAM,QAAU,OAC1BA,EAAU,UAAYxH,GACtBwH,EAAU,iBAAiB,QAAU3G,GAAM,CACzCA,EAAE,gBAAgB,EAClBoF,EAAS,SAAStD,EAAG,QAAQ,GAAIV,EAAQ,EAAE,CAC7C,CAAC,EACDkF,EAAU,YAAYK,CAAS,EAE/BP,EAAY,KAAK,CACf,GAAIE,EACJ,QAASxE,EAAG,QACZ,QAAAV,EACA,YAAa,IAAM,CACjB,IAAMwB,EAAQwC,EAAS,SAAStD,EAAG,QAAQ,GAAIV,EAAQ,EAAE,EACrDwB,EAAQ,GACV8D,EAAW,YAAc,OAAO9D,CAAK,EACrC8D,EAAW,MAAM,QAAU,GAC3BC,EAAU,MAAM,QAAU,KAE1BD,EAAW,MAAM,QAAU,OAC3BC,EAAU,MAAM,QAAU,OAE9B,CACF,CAAC,CACH,CAYAZ,EAAK,YAAYO,CAAS,CAC5B,CAAC,EAEDM,EAAc,EAChB,CAEA,SAASd,GAAc,CACrB,GAAI,CAACH,EAAa,OAClB,IAAMkB,EAAQlB,EAAY,MAAM,KAAK,EAAE,YAAY,EAC/CC,IACFA,EAAe,MAAM,QAAUiB,EAAQ,GAAK,QAE9C,IAAIC,EAAe,EACnBV,EAAY,QAASW,GAAQ,CAC3B,IAAMC,EAAQ,CAACH,GAASE,EAAI,QAAQ,MAAM,YAAY,EAAE,SAASF,CAAK,EACtEE,EAAI,GAAG,MAAM,QAAUC,EAAQ,GAAK,OAChCA,GAAOF,GACb,CAAC,EACDd,EAAM,MAAM,QAAUc,IAAiB,GAAKD,EAAQ,GAAK,MAC3D,CAGA,IAAII,EAA8B,KAClC,SAASC,EAAUlH,EAAkB,CACnC,GAAIA,EAAE,MAAQ,SAAU,CACtBA,EAAE,eAAe,EACjB0F,EAAM,EACN,MACF,CACI1F,EAAE,MAAQ,OACZmH,GAAUnH,EAAGiB,CAAK,CAEtB,CAEA,IAAImG,EAAS,GAEb,SAASC,GAAO,CACVD,GACAhC,EAAS,UAAU,IACvBgC,EAAS,GACTf,EAAU,EACVY,EAAe5B,EAAQ,YAAY,EAChC,cACHA,EAAQ,MAAM,QAAU,GACxBA,EAAQ,UAAU,IAAI,mCAAmC,EACzDpE,EAAM,MAAM,EACZ,SAAS,iBAAiB,UAAWiG,CAAS,EAC9C7B,EAAQ,iBAAiB,QAASiC,CAAc,EAClD,CAEA,SAAS5B,GAAQ,CACV0B,IACLA,EAAS,GACT/B,EAAQ,UAAU,OAAO,mCAAmC,EAC5DA,EAAQ,MAAM,QAAU,OACxB,SAAS,oBAAoB,UAAW6B,CAAS,EACjD7B,EAAQ,oBAAoB,QAASiC,CAAc,EAC/CL,aAAuB,aACzBA,EAAY,MAAM,EAEtB,CAEA,SAASK,EAAetH,EAAe,CACjCA,EAAE,SAAWqF,GAASK,EAAM,CAClC,CAEA,SAASkB,GAAgB,CACvBR,EAAY,QAASmB,GAAMA,EAAE,YAAY,CAAC,CAC5C,CAEA,MAAO,CAAE,GAAIlC,EAAS,KAAAgC,EAAM,MAAA3B,EAAO,cAAAkB,CAAc,CACnD,CAIA,SAAS7G,GACPR,EACAiI,EACmB,CACnB,IAAMC,EAA4B,CAAC,EAC7BC,EAAO,IAAI,IACjB,QAAWvG,KAAW5B,EAAO,SAAU,CACrC,GAAImI,EAAK,IAAIvG,EAAQ,EAAE,EAAG,SAC1BuG,EAAK,IAAIvG,EAAQ,EAAE,EACnB,IAAMwG,EAAYxG,EAAQ,SAAS,MAAM,OAAQyG,GAAMA,EAAE,gBAAgB,EACnEC,EAAQF,EAAU,SAAW,EAC/BE,GAASL,IAAgB,QAC7BC,EAAO,KAAK,CACV,QAAAtG,EACA,SAAUA,EAAQ,SAAS,MAC3B,sBAAuBwG,EAAU,CAAC,GAAK,KACvC,MAAAE,CACF,CAAC,CACH,CACA,OAAOJ,CACT,CAEA,SAASN,GAAU,EAAkB7H,EAAwB,CAC3D,IAAMwI,EAAaxI,EAAU,iBAC3B,0EACF,EACA,GAAIwI,EAAW,SAAW,EAAG,OAC7B,IAAMC,EAAQD,EAAW,CAAC,EACpBE,EAAOF,EAAWA,EAAW,OAAS,CAAC,EACvCG,EAAU3I,EAAU,YAAY,EACnC,cACC,EAAE,UAAY2I,IAAWF,GAC3B,EAAE,eAAe,EACjBC,EAAK,MAAM,GACF,CAAC,EAAE,UAAYC,IAAWD,IACnC,EAAE,eAAe,EACjBD,EAAM,MAAM,EAEhB,CAEA,SAASzC,GAAW4C,EAAqB,CACvC,OAAOA,EAAI,QAAQ,kBAAmB,GAAG,CAC3C,CC7zBO,SAASC,GACdC,EACAC,EACAC,EACAC,EACA,CACA,IAAMC,EAAKH,EAAO,aAEZI,EADUJ,EAAO,SAAS,CAAC,GACR,SAAS,MAAM,KAAMK,GAAMA,EAAE,gBAAgB,EAItE,GAAI,CAACD,GAAWD,EAAG,qBAAuB,OAAQ,OAElD,IAAMG,EAAiBF,EAAUG,EAAWH,EAAQ,MAAM,MAAM,EAAI,EAC9DI,EAAWJ,GAAS,MAAM,cAAgB,MAO1CK,EAAeT,EAAO,eAAe,aAErCU,EAAWV,EAAO,YAAY,IAAkB,CAACW,EAAMC,IAAU,CACrE,IAAIC,EACJ,GAAIJ,IAAiB,eAAgB,CACnC,IAAMK,EAAM,KAAK,OAAOH,EAAK,QAAU,GAAK,GAAG,EAC/CE,EAAU,KAAK,IAAI,EAAGP,EAAiBQ,CAAG,CAC5C,KAAO,CACL,IAAMC,EAAMJ,EAAK,YAAc,EACzBK,EAAW,KAAK,MAAOV,EAAiBS,EAAO,GAAG,EACxDF,EAAU,KAAK,IAAI,EAAGP,EAAiBU,CAAQ,CACjD,CACA,MAAO,CACL,KAAAL,EACA,MAAAC,EACA,IAAKD,EAAK,YACV,kBAAmBE,EACnB,sBAAuBP,CACzB,CACF,CAAC,EAEKW,EAAgBC,GAAkBR,CAAQ,EAC5CS,EAAgBhB,EAAG,cAAgB,aAAec,EAAgB,EAClE,OAAOd,EAAG,aAAgB,WAC5BgB,EAAgBC,GAAMjB,EAAG,YAAa,EAAGO,EAAS,OAAS,CAAC,GAG9D,IAAMW,EACJlB,EAAG,aAAa,YAAc,OAC1BiB,GAAMjB,EAAG,aAAa,UAAW,EAAGO,EAAS,OAAS,CAAC,EACvDO,EAEAK,EAAOC,EAAG,MAAO,WAAW,EAKlC,GAJAD,EAAK,YACHE,GAAaxB,EAAQU,EAAUS,EAAeX,EAAUC,CAAY,CACtE,EAEIN,EAAG,UAAU,eAAiBH,EAAO,OAAQ,CAC/C,IAAMyB,EAAYC,EAAgB1B,EAAO,MAAM,EAC3CyB,IACFH,EAAK,YAAYG,EAAU,EAAE,EAC7BvB,IAAYuB,EAAU,IAAI,EAE9B,CAEA,IAAME,EAAYJ,EAAG,MAAO,mBAAoB,CAC9C,KAAM,aACN,aAAc,iBACd,kBAAmB,EACrB,CAAC,EAEDb,EAAS,QAASkB,GAAM,CACtB,IAAMC,EAASC,GACbF,EACAA,EAAE,QAAUT,EACZX,EACAL,EAAG,aAAa,SAAWyB,EAAE,QAAUP,EACnClB,EAAG,aAAa,KAChB,KACJA,EAAG,QAAQ,iBACXA,EAAG,QAAQ,gBACb,EACA0B,EAAO,iBAAiB,QAAS,IAAME,EAAWH,EAAE,KAAK,CAAC,EAG1DC,EAAO,iBAAiB,UAAYG,GAAM,CACxC,GAAIA,EAAE,MAAQ,SAAWA,EAAE,MAAQ,IAAK,CACtCA,EAAE,eAAe,EACjBD,EAAWH,EAAE,KAAK,EAClB,MACF,CACA,GACEI,EAAE,MAAQ,aACVA,EAAE,MAAQ,cACVA,EAAE,MAAQ,WACVA,EAAE,MAAQ,YACV,CACAA,EAAE,eAAe,EACjB,IAAMC,EACJD,EAAE,MAAQ,aAAeA,EAAE,MAAQ,aAAe,EAAI,GAClDE,GAAQN,EAAE,MAAQK,EAAQvB,EAAS,QAAUA,EAAS,OAC5DqB,EAAWG,CAAI,EACAP,EAAU,SAASO,CAAI,GAC9B,MAAM,CAChB,CACF,CAAC,EACDP,EAAU,YAAYE,CAAM,CAC9B,CAAC,EAEDP,EAAK,YAAYK,CAAS,EAC1BL,EAAK,YAAYC,EAAG,MAAO,mBAAmB,CAAC,EAE/C,IAAIY,EAAYC,GACd1B,EACAS,EACAX,EACAL,EAAG,QAAQ,cACXA,EAAG,QAAQ,kBACb,EACAmB,EAAK,YAAYa,CAAS,EAE1B,IAAIE,EAAmClC,EAAG,WAAW,QACjDmC,GAAiB5B,EAAUS,EAAeX,CAAQ,EAClD,KACA6B,GAAcf,EAAK,YAAYe,CAAY,EAE/C,IAAME,EAAMC,GAAUxC,EAAQ,IAAM,CAClC,GAAI,CAACI,EAAS,OACd,IAAMwB,EAAIlB,EAASS,CAAa,EAC3BS,GACL3B,EAAY,CACV,CACE,cAAeG,EAAQ,GACvB,SAAUwB,EAAE,IACZ,WAAY,CACV,CAAE,IAAK,mBAAoB,MAAO5B,EAAO,EAAG,EAC5C,CAAE,IAAK,oBAAqB,MAAOA,EAAO,UAAW,CACvD,CACF,CACF,CAAC,CACH,CAAC,EACDsB,EAAK,YAAYiB,CAAG,EAEpBjB,EAAK,YACHC,EAAG,IAAK,kBAAmB,CAAE,aAAc,GAAI,YAAa,QAAS,CAAC,CACxE,EACAD,EAAK,YACHC,EAAG,OAAQ,qBAAsB,CAC/B,cAAe,GACf,YAAa,QACf,CAAC,CACH,EAEAxB,EAAU,YAAYuB,CAAI,EAE1B,SAASS,EAAWU,EAAa,CAC/B,GAAIA,IAAQtB,GAAiBsB,EAAM,GAAKA,GAAO/B,EAAS,OAAQ,OAChES,EAAgBsB,EAChB,MAAM,KAAKd,EAAU,QAAQ,EAAE,QAAQ,CAACe,EAAMC,IAAM,CAClDD,EAAK,aAAa,eAAgB,OAAOC,IAAMF,CAAG,CAAC,EAClDC,EAAqB,SAAWC,IAAMF,EAAM,EAAI,EACnD,CAAC,EACD,IAAMG,EAAaR,GACjB1B,EACA+B,EACAjC,EACAL,EAAG,QAAQ,cACXA,EAAG,QAAQ,kBACb,EAGA,GAFAgC,EAAU,YAAYS,CAAU,EAChCT,EAAYS,EACRP,EAAc,CAChB,IAAMQ,EAASP,GAAiB5B,EAAU+B,EAAKjC,CAAQ,EACvD6B,EAAa,YAAYQ,CAAM,EAC/BR,EAAeQ,CACjB,CAIA,IAAMC,EAAUxB,EAAK,cAA2B,qBAAqB,EACrE,GAAIwB,EAAS,CACX,IAAMC,EAAQC,GAAStC,EAAS+B,CAAG,EAAGjC,EAAUC,CAAY,EACxDsC,GACFD,EAAQ,YAAcC,EACtBD,EAAQ,MAAM,QAAU,IAExBA,EAAQ,MAAM,QAAU,MAE5B,CACF,CACF,CAIA,SAAStB,GACPxB,EACAU,EACAS,EACAX,EACAC,EACa,CACb,IAAMN,EAAKH,EAAO,aACZiD,EAAS1B,EAAG,MAAO,kBAAkB,EACrC2B,EAAU3B,EAAG,MAAO,2BAA2B,EAC/C4B,EAAQ5B,EAAG,KAAM,iBAAiB,EAKxC,GAJA4B,EAAM,YAAcnD,EAAO,MAC3BkD,EAAQ,YAAYC,CAAK,EACzBF,EAAO,YAAYC,CAAO,EAEtB/C,EAAG,QAAQ,cAAe,CAC5B,IAAMiD,EAAQJ,GAAStC,EAASS,CAAa,EAAGX,EAAUC,CAAY,EACtE,GAAI2C,EAAO,CACT,IAAMN,EAAUvB,EAAG,OAAQ,0BAA2B,CACpD,oBAAqB,EACvB,CAAC,EACDuB,EAAQ,YAAcM,EACtBH,EAAO,YAAYH,CAAO,CAC5B,CACF,CACA,OAAOG,CACT,CAEA,SAASnB,GACPF,EACAyB,EACA7C,EACA8C,EACAC,EACAC,EACa,CACb,IAAM7C,EAAOY,EAAG,MAAO,kBAAmB,CACxC,KAAM,QACN,eAAgB,OAAO8B,CAAU,EACjC,SAAUA,EAAa,IAAM,KAC7B,kBAAmB,OAAOzB,EAAE,KAAK,EACjC,gBAAiB,OAAOA,EAAE,GAAG,CAC/B,CAAC,EAEK6B,EAAQlC,EAAG,OAAQ,kBAAkB,EAC3CkC,EAAM,YAAYlC,EAAG,OAAQ,sBAAsB,CAAC,EACpDZ,EAAK,YAAY8C,CAAK,EAEtB,IAAMC,EAAOnC,EAAG,OAAQ,sBAAsB,EACxCwB,EAAQxB,EAAG,OAAQ,uBAAuB,EAChDwB,EAAM,YAAc,OAAOnB,EAAE,GAAG,GAChC8B,EAAK,YAAYX,CAAK,EAEtB,IAAMY,EAAQpC,EAAG,OAAQ,uBAAuB,EAChD,GAAIgC,GAAoB3B,EAAE,kBAAoBA,EAAE,sBAAuB,CACrE,IAAMgC,EAAUrC,EAAG,OAAQ,yBAAyB,EACpDqC,EAAQ,YAAcC,EAAYjC,EAAE,sBAAuBpB,CAAQ,EACnEmD,EAAM,YAAYC,CAAO,CAC3B,CACA,GAAIJ,EAAkB,CACpB,IAAMM,EAAO,SAAS,cAAc,MAAM,EAC1CA,EAAK,aAAa,uBAAwB,EAAE,EAC5CA,EAAK,YAAcD,EAAYjC,EAAE,kBAAmBpB,CAAQ,EAC5DmD,EAAM,YAAYG,CAAI,EACtB,IAAMC,EAAOxC,EAAG,OAAQ,sBAAsB,EAC9CwC,EAAK,YAAc,QACnBJ,EAAM,YAAYI,CAAI,CACxB,CACAL,EAAK,YAAYC,CAAK,EACtBhD,EAAK,YAAY+C,CAAI,EAErB,IAAMN,EAAQ7B,EAAG,OAAQ,uBAAuB,EAChD,OAAI+B,EACFF,EAAM,YAAcE,EAEpBF,EAAM,MAAM,QAAU,OAExBzC,EAAK,YAAYyC,CAAK,EAEfzC,CACT,CAEA,SAASyB,GACP1B,EACAS,EACAX,EACAwD,EACAC,EACa,CACb,IAAMrC,EAAIlB,EAASS,CAAa,EAC1B+C,EAAatC,EAAIA,EAAE,kBAAoBA,EAAE,IAAM,EAC/CuC,EAAoBvC,EAAIA,EAAE,sBAAwBA,EAAE,IAAM,EAC1DwC,EAAU,KAAK,IAAI,EAAGD,EAAoBD,CAAU,EAEpDG,EAAM9C,EAAG,MAAO,mBAAmB,EACnCwB,EAAQxB,EAAG,OAAQ,2BAA4B,CACnD,mBAAoB,EACtB,CAAC,EAED,GADAwB,EAAM,YAAc,QAChBiB,GAAiBpC,EAAG,CACtB,IAAM0C,EAAQ,SAAS,cAAc,MAAM,EAC3CA,EAAM,aAAa,kBAAmB,EAAE,EACxCA,EAAM,YAAc,KAAK1C,EAAE,GAAG,QAAQA,EAAE,MAAQ,EAAI,GAAK,GAAG,IAC5DmB,EAAM,YAAYuB,CAAK,CACzB,CACAD,EAAI,YAAYtB,CAAK,EAErB,IAAMwB,EAAShD,EAAG,OAAQ,2BAA2B,EACrD,GAAI0C,GAAsBG,EAAU,EAAG,CACrC,IAAMR,EAAUrC,EAAG,OAAQ,0BAA2B,CACpD,qBAAsB,EACxB,CAAC,EACDqC,EAAQ,YAAcC,EAAYM,EAAmB3D,CAAQ,EAC7D+D,EAAO,YAAYX,CAAO,CAC5B,CACA,IAAMY,EAAOjD,EAAG,OAAQ,uBAAwB,CAAE,mBAAoB,EAAG,CAAC,EAC1E,OAAAiD,EAAK,YAAcX,EAAYK,EAAY1D,CAAQ,EACnD+D,EAAO,YAAYC,CAAI,EACvBH,EAAI,YAAYE,CAAM,EACfF,CACT,CAEA,SAAS/B,GACP5B,EACAS,EACAX,EACa,CACb,IAAMoB,EAAIlB,EAASS,CAAa,EAC1B+C,EAAatC,EAAIA,EAAE,kBAAoBA,EAAE,IAAM,EAC/CuC,EAAoBvC,EAAIA,EAAE,sBAAwBA,EAAE,IAAM,EAC1DwC,EAAU,KAAK,IAAI,EAAGD,EAAoBD,CAAU,EAEpDO,EAAMlD,EAAG,MAAO,wBAAyB,CAAE,mBAAoB,EAAG,CAAC,EACrE6C,GAAW,IAAGK,EAAI,MAAM,QAAU,QACtC,IAAMC,EAAU,SAAS,cAAc,MAAM,EAC7CA,EAAQ,YAAc,WACtBD,EAAI,YAAYC,CAAO,EACvB,IAAMC,EAASpD,EAAG,OAAQ,GAAI,CAAE,sBAAuB,EAAG,CAAC,EAC3D,OAAAoD,EAAO,YAAcd,EAAYO,EAAS5D,CAAQ,EAClDiE,EAAI,YAAYE,CAAM,EACfF,CACT,CAEA,SAASjC,GACPxC,EACA4E,EACa,CAEb,IAAMC,EADU7E,EAAO,SAAS,CAAC,GACJ,SAAS,MAAM,KAAMK,GAAMA,EAAE,gBAAgB,EACpE0C,EAAQ8B,EACV7E,EAAO,aAAa,IAAI,SAAW,cACnC,WACE8E,EAASC,EAAehC,CAAK,EACnC,OAAK8B,IAAaC,EAAO,SAAW,IACpCA,EAAO,iBAAiB,QAAS,IAAM,CACjCA,EAAO,UACXF,EAAQ,CACV,CAAC,EACME,CACT,CAIA,SAAS9B,GACPtC,EACAF,EACAC,EACe,CACf,GAAI,CAACC,EAAU,OAAO,KACtB,GAAM,CAAE,KAAAC,CAAK,EAAID,EACjB,GAAID,IAAiB,eAAgB,CACnC,IAAMkE,EAAShE,EAAK,QAAU,EAC9B,OAAIgE,EAAS,EAAU,IAAId,EAAY,KAAK,MAAMc,EAAS,GAAG,EAAGnE,CAAQ,CAAC,GACnE,IACT,CACA,GAAIC,IAAiB,aAAc,CACjC,IAAMM,EAAMJ,EAAK,YAAc,EAC/B,OAAII,EAAM,EAAU,IAAI,KAAK,MAAMA,CAAG,CAAC,IAChC,IACT,CACA,OAAO,IACT,CAEA,SAASG,GAAkBR,EAAkC,CAC3D,IAAIsE,EAAc,EACdC,EAAY,EAChB,OAAAvE,EAAS,QAAQ,CAACkB,EAAGe,IAAM,CACzB,IAAMyB,EAAUxC,EAAE,sBAAwBA,EAAE,kBACxCwC,EAAUY,IACZA,EAAcZ,EACda,EAAYtC,EAEhB,CAAC,EACMsC,CACT,CAEA,SAAS7D,GAAM8D,EAAWC,EAAaC,EAAqB,CAC1D,OAAO,KAAK,IAAID,EAAK,KAAK,IAAIC,EAAKF,CAAC,CAAC,CACvC,CC3ZO,IAAMG,GAAkB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyelBC,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,EA0DnBC,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,EAuevBC,GAAoB;AAAA;AAAA;AAAA;AAAA;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","names":["src_exports","__export","LimeBundleElement","StorefrontApiError","errors","e","DEFAULT_API_VERSION","createStorefrontClient","config","version","endpoint","query","variables","options","headers","response","json","BUNDLE_METAOBJECT_QUERY","SHOP_CUSTOM_CSS_QUERY","BUNDLES_FOR_PRODUCT_QUERY","CART_CREATE_MUTATION","CART_LINES_ADD_MUTATION","BundleParseError","message","reason","WIDGET_CONFIG_DEFAULTS","mergeWidgetConfig","raw","input","sanitizeDefaultTier","flattenWidgetConfig","CSS_VAR_MAP","PX_KEYS","applyWidgetConfigVars","el","flat","flatKey","cssVar","value","serialized","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","parseVolumeTiers","parseIntField","productsField","node","collectionField","parseJsonField","result","entry","x","key","record","k","v","num","overrides","description","discountType","discountValueRaw","abTiers","t","parseOneVolumeTier","minQty","tier","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","hasConsent","consentGranted","shopifyGlobal","SESSION_COOKIE_NAME","SESSION_COOKIE_MAX_AGE","AB_COOKIE_MAX_AGE","abCookieName","bundleId","getABTestAssignment","appUrl","shopDomain","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","rawCss","sanitized","simpleHash","style","parseCents","amount","num","formatCents","cents","currencyCode","percentageDiscountUnit","unitCents","percent","computeFixedPricing","bundle","productQuantities","showSaveBadge","discountType","discountValue","rows","totalCents","saleCents","product","variant","v","qty","lineCents","compareCents","perUnit","savingsCents","currency","headerBadge","computeBundleSaleCents","discount","off","THUMB_PX","transformImageUrl","url","u","formatCountdown","msRemaining","totalSeconds","days","hours","minutes","seconds","pad","n","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","totalCents","sum","unit","parseCents","saleCents","computeSale","savingsCents","deriveHeaderBadge","productIndex","selectedVariantIds","available","v","eligibleVariants","isOos","selected","qty","header","content","title","subtitle","badgeEl","initialPricing","computeFixedPricing","badgeText","savings","dc","formatCents","state","onVariantChange","rowEl","thumb","img","transformImageUrl","THUMB_PX","qtyBadgeRef","info","name","oosLabel","prices","compare","priceEl","applyVariantToRow","variant","cmp","select","opt","badge","label","sale","bar","amount","onClick","button","buildCtaButton","discount","off","perUnit","PLACEHOLDER_THUMB_SVG","PLUS_ICON_SVG","CLOSE_ICON_SVG","SEARCH_CLEAR_ICON_SVG","renderMixMatchBundle","container","bundle","onAddToCart","onCleanup","wc","currency","requiredQty","maxQty","eligible","buildEligibleProducts","e","selections","root","el","header","renderHeader","countdown","renderCountdown","progress","renderProgress","slotsContainer","pricingSection","renderPricingSection","savingsBar","renderSavingsBar","placeholder","placeholderText","modal","renderModal","product","variant","addSelection","productId","variantId","removeSelection","s","cta","buildCtaButton","lines","firstEligible","ep","firstVariant","parseCents","afterMutation","idx","removeSlotAt","index","renderSlots","updateCta","totalSlots","i","selection","renderFilledSlot","renderEmptySlot","count","setCtaLabel","content","title","discountType","discountValue","label","formatCents","badge","wrap","labels","remaining","track","fill","update","selected","pct","onClick","slot","thumb","text","onRemove","img","transformImageUrl","THUMB_PX","info","priceWrap","compare","priceEl","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","productEl","price","soldOut","addBtn","countBadge","removeBtn","refreshCounts","query","visibleCount","row","match","lastFocused","onKeydown","trapFocus","isOpen","open","onOverlayClick","r","oosBehavior","result","seen","available","v","isOos","focusables","first","last","active","gid","renderVolumeBundle","container","bundle","onAddToCart","onCleanup","wc","variant","v","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","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","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","BUNDLE_SKELETON_CSS","cartStorageKey","shopDomain","resolveProductHandle","explicit","meta","match","LimeBundleElement","cleanup","name","oldValue","newValue","controller","client","createStorefrontClient","bundlePromise","singleBundleMode","handle","cssPromise","SHOP_CUSTOM_CSS_QUERY","css","injectCustomCss","sanitized","sanitizeCustomCss","err","results","bundle","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","p","v","price","reportAddToCart","style","BUNDLE_BASE_CSS","BUNDLE_FIXED_CSS","BUNDLE_MIX_MATCH_CSS","BUNDLE_VOLUME_CSS","customStyle","container","applyWidgetConfigVars","dispatch","registerCleanup","fn","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","first","b","element","observeImpression","reportImpression","BUNDLE_SKELETON_CSS","message","LimeBundleElement"]}
|