@lime-bundles/widget 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +125 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -32
- package/dist/index.d.ts +7 -32
- package/dist/index.js +138 -180
- package/dist/index.js.map +1 -1
- package/dist/lime-bundle.global.js +23 -39
- package/dist/lime-bundle.global.js.map +1 -1
- package/package.json +2 -6
- package/dist/lime-thankyou.cjs +0 -2
- package/dist/lime-thankyou.cjs.map +0 -1
- package/dist/lime-thankyou.global.js +0 -2
- package/dist/lime-thankyou.global.js.map +0 -1
- package/dist/lime-thankyou.js +0 -2
- package/dist/lime-thankyou.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../../core/src/storefront-api/client.ts","../../core/src/storefront-api/queries.ts","../../core/src/bundle/parser.ts","../../core/src/bundle/tier-calculator.ts","../../core/src/bundle/validator.ts","../../core/src/cart/detector.ts","../../core/src/cart/ajax-cart.ts","../../core/src/cart/storefront-cart.ts","../../core/src/analytics/reporter.ts","../../core/src/ab-test/assigner.ts","../../core/src/utils/money.ts","../src/renderers/fixed.ts","../src/renderers/mix-match.ts","../src/renderers/volume.ts","../src/styles/widget-styles.ts","../src/lime-bundle.ts","../src/thankyou/index.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 (typeof customElements !== \"undefined\" && !customElements.get(\"lime-bundle\")) {\n customElements.define(\"lime-bundle\", LimeBundleElement);\n}\n\nexport { LimeBundleElement };\nexport { trackPurchase } from \"./thankyou/index\";\n","/**\n * Lightweight Storefront API client.\n * No framework dependencies — just fetch.\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 ): Promise<T>;\n}\n\nexport interface StorefrontClientConfig {\n shopDomain: string;\n accessToken: string;\n apiVersion?: string;\n}\n\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 ): Promise<T> {\n const response = await fetch(endpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Shopify-Storefront-Access-Token\": config.accessToken,\n },\n body: JSON.stringify({ query, variables }),\n });\n\n if (!response.ok) {\n throw new StorefrontApiError([\n { message: `Storefront API error: ${response.status} ${response.statusText}` },\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 for bundle metaobjects and products.\n */\n\nexport const BUNDLE_METAOBJECT_QUERY = `#graphql\n query BundleMetaobject($id: ID!) {\n metaobject(id: $id) {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n 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 * Parse metaobject fields into a typed ParsedBundle.\n * Maps Storefront API metaobject field keys to structured data.\n */\nimport type { MetaobjectField, Product } from \"../storefront-api/types\";\nimport type {\n ParsedBundle,\n BundleType,\n BundleStatus,\n DiscountConfig,\n WidgetConfig,\n VolumeTier,\n} from \"./types\";\n\nconst VALID_BUNDLE_TYPES = new Set([\"fixed\", \"mix_match\", \"volume\"]);\nconst ACTIVE_STATUSES = new Set([\"active\"]);\n\n/**\n * Parse a metaobject's fields array into a structured ParsedBundle.\n * Returns null if the bundle is not renderable (invalid type, inactive, expired).\n */\nexport function parseMetaobjectBundle(\n metaobjectId: string,\n fields: MetaobjectField[],\n): ParsedBundle | null {\n const fieldMap = new Map(fields.map((f) => [f.key, f]));\n\n const title = fieldMap.get(\"title\")?.value ?? \"Bundle\";\n const bundleType = fieldMap.get(\"bundle_type\")?.value as BundleType | null;\n const status = fieldMap.get(\"status\")?.value as BundleStatus | null;\n\n // Validate bundle type\n if (!bundleType || !VALID_BUNDLE_TYPES.has(bundleType)) return null;\n\n // Only render active bundles\n if (!status || !ACTIVE_STATUSES.has(status)) return null;\n\n // Check schedule\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 && new Date(startsAt) > now) return null;\n if (endsAt && new Date(endsAt) < now) return null;\n\n // Resolve products from references\n const products = resolveProducts(fieldMap);\n\n // Parse discount config\n const discountConfig = parseDiscountConfig(fieldMap);\n\n // Parse widget config\n const widgetConfig = parseWidgetConfig(fieldMap);\n\n // Parse volume tiers\n const volumeTiers = parseVolumeTiers(fieldMap);\n\n return {\n id: metaobjectId,\n title,\n bundleType,\n status,\n products,\n discountConfig,\n widgetConfig,\n volumeTiers,\n minQuantity: parseIntField(fieldMap, \"min_quantity\"),\n maxQuantity: parseIntField(fieldMap, \"max_quantity\"),\n startsAt,\n endsAt,\n discountLabel: fieldMap.get(\"discount_label\")?.value ?? null,\n abTestId: fieldMap.get(\"ab_test_id\")?.value ?? null,\n abTestConfig: parseJsonField(fieldMap, \"ab_test_config\"),\n };\n}\n\nfunction resolveProducts(fieldMap: Map<string, MetaobjectField>): Product[] {\n const products: Product[] = [];\n\n // Single product reference\n const productsField = fieldMap.get(\"products\");\n if (productsField?.reference && \"variants\" in productsField.reference) {\n products.push(productsField.reference as Product);\n }\n\n // Multiple product/collection references\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 // Collection reference — flatten products\n products.push(...node.products.nodes);\n }\n }\n }\n\n // Also check collection field\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\nfunction parseWidgetConfig(\n fieldMap: Map<string, MetaobjectField>,\n): WidgetConfig {\n const raw = parseJsonField(fieldMap, \"widget_config\");\n if (raw && typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>;\n return {\n primaryColor: (obj.primaryColor as string) ?? null,\n ctaText: (obj.ctaText as string) ?? null,\n outOfStockBehavior:\n (obj.outOfStockBehavior as \"hide\" | \"disable\" | \"show\") ?? \"hide\",\n };\n }\n return { primaryColor: null, ctaText: null, outOfStockBehavior: \"hide\" };\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((t) => ({\n minQuantity: Number(t.minQuantity ?? 0),\n discountType: (t.discountType as \"percentage\" | \"fixed_amount\") ?? \"percentage\",\n discountValue: Number(t.discountValue ?? 0),\n label: (t.label as string) ?? null,\n }));\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): number | null {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n const num = parseInt(value, 10);\n return isNaN(num) ? null : num;\n}\n","/**\n * Volume tier savings calculation.\n */\nimport type { VolumeTier } 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): 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 tier.discountType === \"percentage\"\n ? basePrice * (tier.discountValue / 100)\n : tier.discountValue;\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 * Cart API auto-detection.\n * Checks for window.Shopify to determine if AJAX Cart API is available.\n */\n\nexport type CartApiType = \"ajax\" | \"storefront\";\n\nexport function detectCartApi(): CartApiType {\n if (\n typeof window !== \"undefined\" &&\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n typeof (window as any).Shopify === \"object\"\n ) {\n return \"ajax\";\n }\n return \"storefront\";\n}\n","/**\n * Shopify AJAX Cart API (/cart/add.js) wrapper.\n * Used when window.Shopify is detected (Shopify-hosted storefronts).\n *\n * Sets _lime_bundle_gid and _lime_bundle_type as line item properties\n * so the thank-you page snippet can identify bundles in the completed order.\n */\nimport type { CartApi, CartLineItem, AddToCartResult } from \"./types\";\n\n// Promise chain serialization to prevent concurrent read-modify-write races\n// (per institutional learning: shopify-cart-attribute-read-modify-write-patterns.md)\nlet cartWriteChain = Promise.resolve();\n\nexport function createAjaxCartApi(\n bundleGid: string,\n bundleType: string,\n): CartApi {\n return {\n async addLines(items: CartLineItem[]): Promise<AddToCartResult> {\n return new Promise((resolve) => {\n cartWriteChain = cartWriteChain\n .catch(() => {}) // Swallow prior failure so chain continues\n .then(async () => {\n try {\n const cartItems = items.map((item) => ({\n id: extractNumericId(item.variantId),\n quantity: item.quantity,\n properties: {\n ...item.properties,\n _lime_bundle_gid: bundleGid,\n _lime_bundle_type: bundleType,\n },\n }));\n\n const response = await fetch(\"/cart/add.js\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ items: cartItems }),\n });\n\n if (!response.ok) {\n const body = await response.json().catch(() => ({}));\n resolve({\n success: false,\n error:\n (body as Record<string, unknown>)?.description?.toString() ??\n `Cart error: ${response.status}`,\n });\n return;\n }\n\n resolve({ success: true });\n } catch (err) {\n resolve({\n success: false,\n error: err instanceof Error ? err.message : \"Cart add failed\",\n });\n }\n });\n });\n },\n };\n}\n\n/**\n * Extract numeric ID from a Shopify GID.\n * \"gid://shopify/ProductVariant/12345\" → 12345\n */\nfunction extractNumericId(gid: string): number {\n const match = gid.match(/\\/(\\d+)$/);\n return match ? parseInt(match[1], 10) : 0;\n}\n","/**\n * Shopify Storefront API Cart mutations wrapper.\n * Used on headless storefronts where window.Shopify is not available.\n */\nimport type { StorefrontClient } from \"../storefront-api/client\";\nimport type { CartApi, CartLineItem, AddToCartResult } from \"./types\";\n\nconst CART_LINES_ADD_MUTATION = `#graphql\n mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $lines) {\n cart {\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n`;\n\nconst CART_CREATE_MUTATION = `#graphql\n mutation CartCreate($input: CartInput!) {\n cartCreate(input: $input) {\n cart {\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n`;\n\nexport function createStorefrontCartApi(\n client: StorefrontClient,\n bundleGid: string,\n bundleType: string,\n cartId?: string,\n): CartApi {\n return {\n async addLines(items: CartLineItem[]): Promise<AddToCartResult> {\n try {\n const lines = items.map((item) => ({\n merchandiseId: item.variantId,\n quantity: item.quantity,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundleGid },\n { key: \"_lime_bundle_type\", value: bundleType },\n ...Object.entries(item.properties ?? {}).map(([key, value]) => ({\n key,\n value,\n })),\n ],\n }));\n\n if (cartId) {\n // Add to existing cart\n const data = await client.query<{\n cartLinesAdd: {\n cart: { id: string } | null;\n userErrors: Array<{ message: string }>;\n };\n }>(CART_LINES_ADD_MUTATION, { cartId, lines });\n\n if (data.cartLinesAdd.userErrors.length > 0) {\n return {\n success: false,\n error: data.cartLinesAdd.userErrors[0].message,\n };\n }\n return { success: true, cartId };\n } else {\n // Create new cart\n const data = await client.query<{\n cartCreate: {\n cart: { id: string } | null;\n userErrors: Array<{ message: string }>;\n };\n }>(CART_CREATE_MUTATION, { input: { lines } });\n\n if (data.cartCreate.userErrors.length > 0) {\n return {\n success: false,\n error: data.cartCreate.userErrors[0].message,\n };\n }\n return {\n success: true,\n cartId: data.cartCreate.cart?.id,\n };\n }\n } catch (err) {\n return {\n success: false,\n error: err instanceof Error ? err.message : \"Cart add failed\",\n };\n }\n },\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 * A/B test variant assignment for headless widgets.\n * Calls /api/ab-assign and manages the lb_session cookie.\n */\n\nconst SESSION_COOKIE_NAME = \"lb_session\";\nconst SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\n\nexport interface ABTestAssignment {\n variant: \"A\" | \"B\";\n testId: string;\n}\n\n/**\n * Get or assign an A/B test variant.\n * Reads/creates the lb_session cookie and calls /api/ab-assign.\n */\nexport async function getABTestAssignment(\n appUrl: string,\n shopDomain: string,\n testId: string,\n): Promise<ABTestAssignment | null> {\n if (typeof document === \"undefined\") return null; // SSR guard\n\n const sessionId = getOrCreateSessionId();\n\n // Deterministic assignment: fnv1a(sessionId + testId) % 2\n const variant = fnv1aVariant(sessionId, testId);\n\n // Fire-and-forget 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(() => {}); // Swallow errors — assignment is best-effort\n } catch {\n // Silently ignore\n }\n\n return { variant, testId };\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 document.cookie = `${SESSION_COOKIE_NAME}=${id}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax`;\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 * 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","/**\n * DOM renderer for fixed bundles.\n */\nimport { formatMoney, type ParsedBundle, type CartLineItem, type AddToCartResult } from \"@lime-bundles/core\";\n\nexport function renderFixedBundle(\n container: HTMLElement,\n bundle: ParsedBundle,\n addToCart: (items: CartLineItem[]) => Promise<AddToCartResult>,\n) {\n const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n // Title\n const title = document.createElement(\"h3\");\n title.className = \"lb-bundle__title\";\n title.textContent = bundle.title;\n title.setAttribute(\"part\", \"title\");\n container.appendChild(title);\n\n // Discount badge\n if (bundle.discountLabel) {\n const badge = document.createElement(\"span\");\n badge.className = \"lb-bundle__discount-badge\";\n badge.textContent = bundle.discountLabel;\n container.appendChild(badge);\n }\n\n // Products\n const productsDiv = document.createElement(\"div\");\n productsDiv.className = \"lb-bundle__products\";\n\n for (const product of bundle.products) {\n const productEl = document.createElement(\"div\");\n productEl.className = \"lb-bundle__product\";\n productEl.setAttribute(\"part\", \"product\");\n\n if (product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = product.featuredImage.url;\n img.alt = product.featuredImage.altText ?? product.title;\n img.className = \"lb-bundle__product-image\";\n img.loading = \"lazy\";\n productEl.appendChild(img);\n }\n\n const info = document.createElement(\"div\");\n info.className = \"lb-bundle__product-info\";\n info.innerHTML = `\n <p class=\"lb-bundle__product-title\">${escapeHtml(product.title)}</p>\n <p class=\"lb-bundle__product-price\">${escapeHtml(formatMoney(product.priceRange.minVariantPrice.amount, currency))}</p>\n `;\n productEl.appendChild(info);\n productsDiv.appendChild(productEl);\n }\n container.appendChild(productsDiv);\n\n // CTA button\n const button = document.createElement(\"button\");\n button.className = \"lb-bundle__cta\";\n button.textContent = bundle.widgetConfig.ctaText ?? \"Add Bundle to Cart\";\n button.setAttribute(\"part\", \"button\");\n\n button.addEventListener(\"click\", async () => {\n button.disabled = true;\n button.textContent = \"Adding...\";\n\n const items: CartLineItem[] = bundle.products\n .filter((p) => p.variants.nodes.some((v) => v.availableForSale))\n .map((p) => {\n const variant = p.variants.nodes.find((v) => v.availableForSale)!;\n return { variantId: variant.id, quantity: 1 };\n });\n\n const result = await addToCart(items);\n\n button.disabled = false;\n button.textContent = bundle.widgetConfig.ctaText ?? \"Add Bundle to Cart\";\n\n if (!result.success) {\n const error = document.createElement(\"p\");\n error.className = \"lb-bundle__error\";\n error.textContent = result.error ?? \"Failed to add to cart\";\n container.appendChild(error);\n setTimeout(() => error.remove(), 5000);\n }\n });\n\n container.appendChild(button);\n}\n\nfunction escapeHtml(str: string): string {\n const div = document.createElement(\"div\");\n div.textContent = str;\n return div.innerHTML;\n}\n","/**\n * DOM renderer for mix-and-match bundles.\n */\nimport { formatMoney, validateQuantity, type ParsedBundle, type CartLineItem, type AddToCartResult } from \"@lime-bundles/core\";\n\nexport function renderMixMatchBundle(\n container: HTMLElement,\n bundle: ParsedBundle,\n addToCart: (items: CartLineItem[]) => Promise<AddToCartResult>,\n) {\n const currency = bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n const selections = new Map<string, { variantId: string; quantity: number }>();\n\n // Title\n const title = document.createElement(\"h3\");\n title.className = \"lb-bundle__title\";\n title.textContent = bundle.title;\n title.setAttribute(\"part\", \"title\");\n container.appendChild(title);\n\n // Instructions\n const instructions = document.createElement(\"p\");\n instructions.className = \"lb-bundle__instructions\";\n instructions.textContent = bundle.minQuantity && bundle.maxQuantity\n ? `Select ${bundle.minQuantity}–${bundle.maxQuantity} items`\n : bundle.minQuantity\n ? `Select at least ${bundle.minQuantity} items`\n : \"Select your items\";\n container.appendChild(instructions);\n\n // Products\n const productsDiv = document.createElement(\"div\");\n productsDiv.className = \"lb-bundle__products lb-bundle__products--selectable\";\n\n for (const product of bundle.products) {\n const variant = product.variants.nodes.find((v) => v.availableForSale) ?? product.variants.nodes[0];\n if (!variant) continue;\n\n const productEl = document.createElement(\"div\");\n productEl.className = \"lb-bundle__product lb-bundle__product--selectable\";\n\n if (product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = product.featuredImage.url;\n img.alt = product.featuredImage.altText ?? product.title;\n img.className = \"lb-bundle__product-image\";\n img.loading = \"lazy\";\n productEl.appendChild(img);\n }\n\n const info = document.createElement(\"div\");\n info.className = \"lb-bundle__product-info\";\n info.innerHTML = `\n <p class=\"lb-bundle__product-title\">${escapeHtml(product.title)}</p>\n <p class=\"lb-bundle__product-price\">${escapeHtml(formatMoney(variant.price.amount, currency))}</p>\n `;\n productEl.appendChild(info);\n\n const selectBtn = document.createElement(\"button\");\n selectBtn.className = \"lb-bundle__select-btn\";\n selectBtn.textContent = variant.availableForSale ? \"Select\" : \"Sold out\";\n selectBtn.disabled = !variant.availableForSale;\n\n selectBtn.addEventListener(\"click\", () => {\n const key = product.id;\n if (selections.has(key)) {\n selections.delete(key);\n productEl.classList.remove(\"lb-bundle__product--selected\");\n selectBtn.textContent = \"Select\";\n } else {\n selections.set(key, { variantId: variant.id, quantity: 1 });\n productEl.classList.add(\"lb-bundle__product--selected\");\n selectBtn.textContent = \"Selected\";\n }\n updateCta();\n });\n\n productEl.appendChild(selectBtn);\n productsDiv.appendChild(productEl);\n }\n container.appendChild(productsDiv);\n\n // Validation message\n const validationEl = document.createElement(\"p\");\n validationEl.className = \"lb-bundle__validation\";\n container.appendChild(validationEl);\n\n // CTA\n const button = document.createElement(\"button\");\n button.className = \"lb-bundle__cta\";\n button.setAttribute(\"part\", \"button\");\n button.disabled = true;\n container.appendChild(button);\n\n function updateCta() {\n const total = Array.from(selections.values()).reduce((s, v) => s + v.quantity, 0);\n const validation = validateQuantity(total, bundle.minQuantity, bundle.maxQuantity);\n button.disabled = !validation.valid;\n button.textContent = bundle.widgetConfig.ctaText ?? `Add ${total} Items to Cart`;\n validationEl.textContent = validation.message ?? \"\";\n }\n\n updateCta();\n\n button.addEventListener(\"click\", async () => {\n button.disabled = true;\n button.textContent = \"Adding...\";\n\n const items: CartLineItem[] = Array.from(selections.entries()).map(([, s]) => ({\n variantId: s.variantId,\n quantity: s.quantity,\n }));\n\n const result = await addToCart(items);\n updateCta();\n\n if (!result.success) {\n const error = document.createElement(\"p\");\n error.className = \"lb-bundle__error\";\n error.textContent = result.error ?? \"Failed to add to cart\";\n container.appendChild(error);\n setTimeout(() => error.remove(), 5000);\n }\n });\n}\n\nfunction escapeHtml(str: string): string {\n const div = document.createElement(\"div\");\n div.textContent = str;\n return div.innerHTML;\n}\n","/**\n * DOM renderer for volume bundles.\n */\nimport { formatMoney, calculateTierSavings, type ParsedBundle, type CartLineItem, type AddToCartResult } from \"@lime-bundles/core\";\n\nexport function renderVolumeBundle(\n container: HTMLElement,\n bundle: ParsedBundle,\n addToCart: (items: CartLineItem[]) => Promise<AddToCartResult>,\n) {\n const product = bundle.products[0];\n if (!product) return;\n\n const basePrice = parseFloat(product.priceRange.minVariantPrice.amount);\n const currency = product.priceRange.minVariantPrice.currencyCode;\n let quantity = 1;\n\n // Title\n const title = document.createElement(\"h3\");\n title.className = \"lb-bundle__title\";\n title.textContent = bundle.title;\n title.setAttribute(\"part\", \"title\");\n container.appendChild(title);\n\n // Product\n const productEl = document.createElement(\"div\");\n productEl.className = \"lb-bundle__product lb-bundle__product--volume\";\n if (product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = product.featuredImage.url;\n img.alt = product.featuredImage.altText ?? product.title;\n img.className = \"lb-bundle__product-image\";\n img.loading = \"lazy\";\n productEl.appendChild(img);\n }\n const info = document.createElement(\"div\");\n info.className = \"lb-bundle__product-info\";\n info.innerHTML = `\n <p class=\"lb-bundle__product-title\">${escapeHtml(product.title)}</p>\n <p class=\"lb-bundle__product-price\">${escapeHtml(formatMoney(basePrice, currency))} each</p>\n `;\n productEl.appendChild(info);\n container.appendChild(productEl);\n\n // Tier table\n const tiersDiv = document.createElement(\"div\");\n tiersDiv.className = \"lb-bundle__tiers\";\n tiersDiv.setAttribute(\"role\", \"table\");\n tiersDiv.setAttribute(\"aria-label\", \"Volume discounts\");\n container.appendChild(tiersDiv);\n\n // Quantity selector\n const qtyWrapper = document.createElement(\"div\");\n qtyWrapper.className = \"lb-bundle__quantity-selector\";\n const label = document.createElement(\"label\");\n label.textContent = \"Quantity\";\n qtyWrapper.appendChild(label);\n\n const qtyControl = document.createElement(\"div\");\n qtyControl.className = \"lb-bundle__quantity-control\";\n\n const minusBtn = document.createElement(\"button\");\n minusBtn.textContent = \"−\";\n minusBtn.setAttribute(\"aria-label\", \"Decrease quantity\");\n\n const qtyInput = document.createElement(\"input\");\n qtyInput.type = \"number\";\n qtyInput.min = \"1\";\n qtyInput.value = \"1\";\n qtyInput.className = \"lb-bundle__quantity-input\";\n\n const plusBtn = document.createElement(\"button\");\n plusBtn.textContent = \"+\";\n plusBtn.setAttribute(\"aria-label\", \"Increase quantity\");\n\n qtyControl.append(minusBtn, qtyInput, plusBtn);\n qtyWrapper.appendChild(qtyControl);\n container.appendChild(qtyWrapper);\n\n // CTA\n const button = document.createElement(\"button\");\n button.className = \"lb-bundle__cta\";\n button.setAttribute(\"part\", \"button\");\n container.appendChild(button);\n\n function updateTiers() {\n const savings = calculateTierSavings(bundle.volumeTiers, basePrice, quantity);\n tiersDiv.innerHTML = \"\";\n for (const ts of savings) {\n const row = document.createElement(\"div\");\n row.className = `lb-bundle__tier${ts.isActive ? \" lb-bundle__tier--active\" : \"\"}`;\n row.setAttribute(\"role\", \"row\");\n row.innerHTML = `\n <span class=\"lb-bundle__tier-quantity\" role=\"cell\">${ts.tier.minQuantity}+ items</span>\n <span class=\"lb-bundle__tier-price\" role=\"cell\">${escapeHtml(formatMoney(ts.unitPrice, currency))} each</span>\n <span class=\"lb-bundle__tier-savings\" role=\"cell\">Save ${ts.savingsPercent.toFixed(0)}%</span>\n ${ts.tier.label ? `<span class=\"lb-bundle__tier-label\" role=\"cell\">${escapeHtml(ts.tier.label)}</span>` : \"\"}\n `;\n tiersDiv.appendChild(row);\n }\n button.textContent = bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`;\n }\n\n updateTiers();\n\n minusBtn.addEventListener(\"click\", () => {\n if (quantity > 1) { quantity--; qtyInput.value = String(quantity); updateTiers(); }\n });\n plusBtn.addEventListener(\"click\", () => {\n quantity++; qtyInput.value = String(quantity); updateTiers();\n });\n qtyInput.addEventListener(\"change\", () => {\n const val = parseInt(qtyInput.value, 10);\n if (!isNaN(val) && val > 0) { quantity = val; updateTiers(); }\n });\n\n button.addEventListener(\"click\", async () => {\n const variant = product.variants.nodes.find((v) => v.availableForSale);\n if (!variant) return;\n\n button.disabled = true;\n button.textContent = \"Adding...\";\n\n const items: CartLineItem[] = [{ variantId: variant.id, quantity }];\n const result = await addToCart(items);\n\n button.disabled = false;\n updateTiers();\n\n if (!result.success) {\n const error = document.createElement(\"p\");\n error.className = \"lb-bundle__error\";\n error.textContent = result.error ?? \"Failed to add to cart\";\n container.appendChild(error);\n setTimeout(() => error.remove(), 5000);\n }\n });\n}\n\nfunction escapeHtml(str: string): string {\n const div = document.createElement(\"div\");\n div.textContent = str;\n return div.innerHTML;\n}\n","/**\n * CSS styles inlined into Shadow DOM.\n * Uses CSS custom properties that pierce the shadow boundary for theming.\n */\nexport const WIDGET_STYLES = `\n:host {\n display: block;\n --lb-primary-color: #000;\n --lb-secondary-color: #666;\n --lb-accent-color: #2563eb;\n --lb-background: #fff;\n --lb-border-color: #e5e7eb;\n --lb-border-radius: 8px;\n --lb-font-family: inherit;\n --lb-font-size: 14px;\n --lb-spacing-sm: 8px;\n --lb-spacing-md: 16px;\n --lb-spacing-lg: 24px;\n --lb-button-bg: var(--lb-accent-color);\n --lb-button-text: #fff;\n --lb-button-radius: var(--lb-border-radius);\n --lb-savings-color: #16a34a;\n --lb-error-color: #dc2626;\n}\n\n.lb-bundle {\n font-family: var(--lb-font-family);\n font-size: var(--lb-font-size);\n color: var(--lb-primary-color);\n background: var(--lb-background);\n border: 1px solid var(--lb-border-color);\n border-radius: var(--lb-border-radius);\n padding: var(--lb-spacing-lg);\n}\n\n.lb-bundle__title { margin: 0 0 var(--lb-spacing-md); font-size: 1.25em; font-weight: 600; }\n.lb-bundle__discount-badge { display: inline-block; background: var(--lb-savings-color); color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 600; margin-bottom: var(--lb-spacing-md); }\n.lb-bundle__products { display: grid; gap: var(--lb-spacing-md); margin-bottom: var(--lb-spacing-lg); }\n.lb-bundle__product { display: flex; gap: var(--lb-spacing-md); align-items: center; padding: var(--lb-spacing-sm); border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); }\n.lb-bundle__product--selected { border-color: var(--lb-accent-color); }\n.lb-bundle__product-image { width: 64px; height: 64px; object-fit: cover; border-radius: calc(var(--lb-border-radius) - 2px); flex-shrink: 0; }\n.lb-bundle__product-info { flex: 1; min-width: 0; }\n.lb-bundle__product-title { margin: 0; font-weight: 500; }\n.lb-bundle__product-price { margin: 4px 0 0; color: var(--lb-secondary-color); }\n.lb-bundle__instructions { color: var(--lb-secondary-color); margin: 0 0 var(--lb-spacing-md); }\n.lb-bundle__tiers { margin-bottom: var(--lb-spacing-lg); }\n.lb-bundle__tier { display: flex; align-items: center; gap: var(--lb-spacing-md); padding: var(--lb-spacing-sm) var(--lb-spacing-md); border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); margin-bottom: var(--lb-spacing-sm); }\n.lb-bundle__tier--active { border-color: var(--lb-savings-color); }\n.lb-bundle__tier-savings { color: var(--lb-savings-color); font-weight: 600; }\n.lb-bundle__quantity-selector { margin-bottom: var(--lb-spacing-lg); }\n.lb-bundle__quantity-selector label { display: block; margin-bottom: var(--lb-spacing-sm); font-weight: 500; }\n.lb-bundle__quantity-control { display: inline-flex; align-items: center; border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); }\n.lb-bundle__quantity-control button { width: 32px; height: 32px; border: none; background: transparent; cursor: pointer; font-size: 1.1em; display: flex; align-items: center; justify-content: center; }\n.lb-bundle__quantity-input { width: 40px; text-align: center; border: none; border-left: 1px solid var(--lb-border-color); border-right: 1px solid var(--lb-border-color); height: 32px; font-size: var(--lb-font-size); -moz-appearance: textfield; }\n.lb-bundle__quantity-input::-webkit-outer-spin-button, .lb-bundle__quantity-input::-webkit-inner-spin-button { -webkit-appearance: none; }\n.lb-bundle__select-btn { padding: 6px 12px; border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); background: transparent; cursor: pointer; }\n.lb-bundle__select-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n.lb-bundle__cta { width: 100%; padding: 12px 24px; border: none; border-radius: var(--lb-button-radius); background: var(--lb-button-bg); color: var(--lb-button-text); font-size: 1em; font-weight: 600; cursor: pointer; transition: opacity 0.15s; }\n.lb-bundle__cta:hover:not(:disabled) { opacity: 0.9; }\n.lb-bundle__cta:disabled { opacity: 0.5; cursor: not-allowed; }\n.lb-bundle__error { color: var(--lb-error-color); margin: var(--lb-spacing-sm) 0; font-size: 0.9em; }\n.lb-bundle__validation { color: var(--lb-secondary-color); margin: var(--lb-spacing-sm) 0; font-size: 0.9em; }\n\n.lb-skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: lb-shimmer 1.5s infinite; border-radius: var(--lb-border-radius); }\n.lb-skeleton--title { height: 24px; width: 60%; margin-bottom: var(--lb-spacing-md); }\n.lb-skeleton--products { height: 200px; }\n@keyframes lb-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }\n`;\n","/**\n * <lime-bundle> Web Component — renders Lime Bundles on headless storefronts.\n *\n * Usage:\n * <lime-bundle\n * shop-domain=\"my-store.myshopify.com\"\n * storefront-token=\"abc123\"\n * bundle-gid=\"gid://shopify/Metaobject/12345\"\n * ></lime-bundle>\n */\nimport {\n createStorefrontClient,\n BUNDLE_METAOBJECT_QUERY,\n parseMetaobjectBundle,\n detectCartApi,\n createAjaxCartApi,\n createStorefrontCartApi,\n observeImpression,\n reportImpression,\n reportAddToCart,\n type ParsedBundle,\n type BundleMetaobjectResponse,\n type CartLineItem,\n} from \"@lime-bundles/core\";\nimport { renderFixedBundle } from \"./renderers/fixed\";\nimport { renderMixMatchBundle } from \"./renderers/mix-match\";\nimport { renderVolumeBundle } from \"./renderers/volume\";\nimport { WIDGET_STYLES } from \"./styles/widget-styles\";\n\nexport class LimeBundleElement extends HTMLElement {\n static observedAttributes = [\n \"shop-domain\",\n \"storefront-token\",\n \"bundle-gid\",\n \"cart-id\",\n \"app-url\",\n \"analytics\",\n \"locale\",\n ];\n\n private shadow: ShadowRoot;\n private bundle: ParsedBundle | null = null;\n private abortController: AbortController | null = null;\n private impressionCleanup: (() => void) | null = null;\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n this.render();\n this.fetchBundle();\n }\n\n disconnectedCallback() {\n this.abortController?.abort();\n this.teardownImpression();\n }\n\n /** Clean up stale bundle state and active impression observer. */\n private teardownImpression() {\n this.impressionCleanup?.();\n this.impressionCleanup = null;\n }\n\n attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {\n // Ignore no-op writes (same value re-set) and pre-connect attribute parsing\n if (oldValue === newValue || !this.isConnected) return;\n\n if (name === \"bundle-gid\" || name === \"shop-domain\" || name === \"storefront-token\") {\n // Only refetch when all required attributes are present — prevents\n // partial-attribute races during element initialization or programmatic updates\n if (this.shopDomain && this.storefrontToken && this.bundleGid) {\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 cartId(): string | undefined {\n return this.getAttribute(\"cart-id\") ?? undefined;\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 || !this.bundleGid) {\n this.renderError(\"Missing required attributes: shop-domain, storefront-token, bundle-gid\");\n return;\n }\n\n // Abort any previous fetch and create a fresh controller.\n // Capture in a local const so post-await checks reference the controller\n // for THIS invocation, not a later one created by a concurrent attributeChangedCallback.\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n this.renderLoading();\n\n try {\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n\n const data = await client.query<BundleMetaobjectResponse>(\n BUNDLE_METAOBJECT_QUERY,\n { id: this.bundleGid },\n );\n\n // Check the LOCAL controller — if a newer fetch replaced this.abortController,\n // this invocation's controller will be aborted.\n if (controller.signal.aborted) return;\n\n if (!data.metaobject) {\n this.bundle = null;\n this.teardownImpression();\n this.renderError(\"Bundle not found\");\n return;\n }\n\n this.bundle = parseMetaobjectBundle(\n data.metaobject.id,\n data.metaobject.fields,\n );\n\n if (!this.bundle) {\n this.teardownImpression();\n this.renderError(\"Bundle is not active or has expired\");\n return;\n }\n\n this.renderBundle();\n this.setupImpression();\n } catch (err) {\n if (controller.signal.aborted) return;\n this.bundle = null;\n this.teardownImpression();\n this.renderError(\n err instanceof Error ? err.message : \"Failed to load bundle\",\n );\n }\n }\n\n private renderBundle() {\n if (!this.bundle) return;\n\n const container = document.createElement(\"div\");\n container.className = \"lb-bundle\";\n container.setAttribute(\"role\", \"region\");\n container.setAttribute(\"aria-label\", this.bundle.title);\n\n const addToCart = async (items: CartLineItem[]) => {\n const apiType = detectCartApi();\n const cart =\n apiType === \"ajax\"\n ? createAjaxCartApi(this.bundleGid, this.bundle!.bundleType)\n : createStorefrontCartApi(\n createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n }),\n this.bundleGid,\n this.bundle!.bundleType,\n this.cartId,\n );\n\n let result: Awaited<ReturnType<typeof cart.addLines>>;\n try {\n result = await cart.addLines(items);\n } catch (err) {\n result = {\n success: false,\n error: err instanceof Error ? err.message : \"Cart add failed\",\n };\n }\n\n if (result.success) {\n // Always dispatch the CustomEvent so consumers can react to add-to-cart\n // regardless of analytics configuration\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:add-to-cart\", {\n detail: { items, cartId: result.cartId },\n bubbles: true,\n composed: true,\n }),\n );\n\n // Analytics reporting is separate — only fires when enabled and appUrl is set\n if (this.analyticsEnabled && this.appUrl) {\n const quantity = items.reduce((sum, i) => sum + i.quantity, 0);\n const totalPrice = items.reduce((sum, item) => {\n const product = this.bundle!.products.find((p) =>\n p.variants.nodes.some((v) => v.id === item.variantId),\n );\n const variant = product?.variants.nodes.find((v) => v.id === item.variantId);\n const price = variant ? parseFloat(variant.price.amount) : 0;\n return sum + price * item.quantity;\n }, 0);\n\n reportAddToCart(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: this.bundleGid,\n bundleType: this.bundle!.bundleType,\n productId: this.bundle!.products[0]?.id ?? \"\",\n quantity,\n totalPrice: Math.round(totalPrice * 100) / 100,\n },\n );\n }\n }\n\n return result;\n };\n\n switch (this.bundle.bundleType) {\n case \"fixed\":\n renderFixedBundle(container, this.bundle, addToCart);\n break;\n case \"mix_match\":\n renderMixMatchBundle(container, this.bundle, addToCart);\n break;\n case \"volume\":\n renderVolumeBundle(container, this.bundle, addToCart);\n break;\n }\n\n this.shadow.innerHTML = \"\";\n const style = document.createElement(\"style\");\n style.textContent = WIDGET_STYLES;\n this.shadow.appendChild(style);\n this.shadow.appendChild(container);\n\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:loaded\", {\n detail: {\n bundleType: this.bundle.bundleType,\n title: this.bundle.title,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private setupImpression() {\n if (!this.analyticsEnabled || !this.bundle || !this.appUrl) return;\n\n this.impressionCleanup?.();\n this.impressionCleanup = observeImpression(this, () => {\n reportImpression(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: this.bundleGid,\n bundleType: this.bundle!.bundleType,\n },\n );\n });\n }\n\n private renderLoading() {\n this.shadow.innerHTML = `\n <style>${WIDGET_STYLES}</style>\n <div class=\"lb-bundle lb-bundle--loading\">\n <div class=\"lb-skeleton lb-skeleton--title\"></div>\n <div class=\"lb-skeleton lb-skeleton--products\"></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 private render() {\n // Initial empty state\n this.shadow.innerHTML = `<style>${WIDGET_STYLES}</style>`;\n }\n}\n","/**\n * Thank-you page snippet — fires purchase events for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-thankyou.js\"></script>\n * <script>\n * LimeBundles.trackPurchase({\n * shopDomain: 'my-store.myshopify.com',\n * orderId: 'gid://shopify/Order/12345',\n * lineItems: [\n * { bundleGid: '...', bundleType: 'fixed', price: 29.99, quantity: 1 }\n * ]\n * });\n * </script>\n */\n\ninterface PurchaseLineItem {\n bundleGid: string;\n bundleType: string;\n price: number;\n quantity: number;\n}\n\ninterface TrackPurchaseInput {\n shopDomain: string;\n orderId: string;\n lineItems: PurchaseLineItem[];\n appUrl?: string;\n}\n\n// Deduplication: track order+bundle combos we've already reported\nconst reportedPurchases = new Set<string>();\n\nexport function trackPurchase(input: TrackPurchaseInput): void {\n const appUrl = input.appUrl ?? `https://${input.shopDomain}`;\n\n // Group line items by bundleGid (merge quantities and prices)\n const bundleMap = new Map<string, { bundleType: string; revenue: number; lineItemCount: number }>();\n\n for (const item of input.lineItems) {\n if (!item.bundleGid) continue;\n\n const existing = bundleMap.get(item.bundleGid);\n if (existing) {\n existing.revenue += item.price * item.quantity;\n existing.lineItemCount += 1;\n } else {\n bundleMap.set(item.bundleGid, {\n bundleType: item.bundleType,\n revenue: item.price * item.quantity,\n lineItemCount: 1,\n });\n }\n }\n\n // Fire purchase events for each unique bundle\n for (const [bundleGid, data] of bundleMap) {\n const dedupKey = `${input.orderId}:${bundleGid}`;\n if (reportedPurchases.has(dedupKey)) continue;\n reportedPurchases.add(dedupKey);\n\n const payload = {\n shopDomain: input.shopDomain,\n eventType: \"bundle_purchased\",\n bundleGid,\n bundleType: data.bundleType,\n orderId: input.orderId,\n revenue: Math.round(data.revenue * 100) / 100,\n lineItemCount: data.lineItemCount,\n occurredAt: new Date().toISOString(),\n };\n\n // Fire-and-forget with sendBeacon fallback\n const url = `${appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n }).catch(() => {\n // Fallback to sendBeacon\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n });\n } catch {\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n }\n }\n}\n"],"mappings":"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","names":["src_exports","__export","LimeBundleElement","trackPurchase","StorefrontApiError","errors","DEFAULT_API_VERSION","createStorefrontClient","config","version","endpoint","query","variables","response","json","BUNDLE_METAOBJECT_QUERY","VALID_BUNDLE_TYPES","ACTIVE_STATUSES","parseMetaobjectBundle","metaobjectId","fields","fieldMap","f","title","bundleType","status","startsAt","endsAt","now","products","resolveProducts","discountConfig","parseDiscountConfig","widgetConfig","parseWidgetConfig","volumeTiers","parseVolumeTiers","parseIntField","parseJsonField","productsField","node","collectionField","raw","obj","t","key","value","num","calculateTierSavings","tiers","basePrice","currentQuantity","a","b","tier","discount","unitPrice","savings","savingsPercent","isActive","validateQuantity","totalQuantity","minQuantity","maxQuantity","detectCartApi","cartWriteChain","createAjaxCartApi","bundleGid","bundleType","items","resolve","cartItems","item","extractNumericId","response","body","err","gid","match","CART_LINES_ADD_MUTATION","CART_CREATE_MUTATION","createStorefrontCartApi","client","cartId","lines","key","value","data","reportImpression","config","event","sendEvent","reportAddToCart","observeImpression","element","callback","observer","entries","entry","payload","url","r","SESSION_COOKIE_MAX_AGE","formatMoney","amount","currencyCode","num","renderFixedBundle","container","bundle","addToCart","currency","title","badge","productsDiv","product","productEl","img","info","escapeHtml","formatMoney","button","items","p","v","result","error","str","div","renderMixMatchBundle","container","bundle","addToCart","currency","selections","title","instructions","productsDiv","product","variant","v","productEl","img","info","escapeHtml","formatMoney","selectBtn","key","updateCta","validationEl","button","total","s","validation","validateQuantity","items","result","error","str","div","renderVolumeBundle","container","bundle","addToCart","product","basePrice","currency","quantity","title","productEl","img","info","escapeHtml","formatMoney","tiersDiv","qtyWrapper","label","qtyControl","minusBtn","qtyInput","plusBtn","button","updateTiers","savings","calculateTierSavings","ts","row","val","variant","v","items","result","error","str","div","WIDGET_STYLES","LimeBundleElement","name","oldValue","newValue","controller","data","createStorefrontClient","BUNDLE_METAOBJECT_QUERY","parseMetaobjectBundle","err","container","addToCart","items","cart","detectCartApi","createAjaxCartApi","createStorefrontCartApi","result","quantity","sum","i","totalPrice","item","variant","p","v","price","reportAddToCart","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","style","WIDGET_STYLES","observeImpression","reportImpression","message","reportedPurchases","trackPurchase","input","appUrl","bundleMap","item","existing","bundleGid","data","dedupKey","payload","url","body","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/parser.ts","../../core/src/bundle/tier-calculator.ts","../../core/src/bundle/validator.ts","../../core/src/cart/detector.ts","../../core/src/analytics/reporter.ts","../../core/src/ab-test/assigner.ts","../../core/src/styles/sanitize.ts","../../core/src/styles/custom-css-injector.ts","../../core/src/utils/money.ts","../src/renderers/fixed.ts","../src/renderers/mix-match.ts","../src/renderers/volume.ts","../src/styles/widget-styles.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 */\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 * 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 v1.0.0 — widening or narrowing these discriminants is a\n * breaking change.\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\nexport interface WidgetConfig {\n primaryColor: string | null;\n ctaText: string | null;\n outOfStockBehavior: \"hide\" | \"disable\" | \"show\";\n}\n\nexport interface VolumeTier {\n minQuantity: number;\n discountType: \"percentage\" | \"fixed_amount\";\n discountValue: number;\n label: string | null;\n}\n\n/** Fields present on every bundle regardless of type. */\ninterface BundleBase {\n id: string;\n title: string;\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 abTestId: string | null;\n abTestConfig: unknown | 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 * 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 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\";\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 status,\n products,\n discountConfig,\n widgetConfig,\n startsAt,\n endsAt,\n discountLabel: fieldMap.get(\"discount_label\")?.value ?? null,\n abTestId: fieldMap.get(\"ab_test_id\")?.value ?? null,\n abTestConfig: parseJsonField(fieldMap, \"ab_test_config\"),\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\"),\n maxQuantity: parseIntField(fieldMap, \"max_quantity\"),\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\nfunction parseWidgetConfig(\n fieldMap: Map<string, MetaobjectField>,\n): WidgetConfig {\n const raw = parseJsonField(fieldMap, \"widget_config\");\n if (raw && typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>;\n return {\n primaryColor: (obj.primaryColor as string) ?? null,\n ctaText: (obj.ctaText as string) ?? null,\n outOfStockBehavior:\n (obj.outOfStockBehavior as \"hide\" | \"disable\" | \"show\") ?? \"hide\",\n };\n }\n return { primaryColor: null, ctaText: null, outOfStockBehavior: \"hide\" };\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((t) => ({\n minQuantity: Number(t.minQuantity ?? 0),\n discountType: (t.discountType as \"percentage\" | \"fixed_amount\") ?? \"percentage\",\n discountValue: Number(t.discountValue ?? 0),\n label: (t.label as string) ?? null,\n }));\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): number | null {\n const value = fieldMap.get(key)?.value;\n if (!value) return null;\n const num = parseInt(value, 10);\n return isNaN(num) ? null : num;\n}\n","/**\n * Volume tier savings calculation.\n */\nimport type { VolumeTier } 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): 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 tier.discountType === \"percentage\"\n ? basePrice * (tier.discountValue / 100)\n : tier.discountValue;\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 * Legacy cart API auto-detection.\n *\n * @deprecated Prefer merchant-supplied cart handling via the `onAddToCart`\n * callback on `<FixedBundle />` / `<VolumeBundle />` / `<MixMatchBundle />`,\n * or the `lime-bundle:add-to-cart` CustomEvent fired by the\n * `<lime-bundle>` web component. Auto-detection is fragile across headless\n * setups (Hydrogen sometimes exposes `window.Shopify` even though Ajax\n * Cart API is unavailable).\n */\n\nexport type CartApiType = \"ajax\" | \"storefront\";\n\nexport function detectCartApi(): CartApiType {\n if (\n typeof window !== \"undefined\" &&\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n typeof (window as any).Shopify === \"object\"\n ) {\n return \"ajax\";\n }\n return \"storefront\";\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 * A/B test variant assignment for headless widgets.\n * Calls /api/ab-assign and manages the lb_session cookie.\n */\n\nconst SESSION_COOKIE_NAME = \"lb_session\";\nconst SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\n\nexport interface ABTestAssignment {\n variant: \"A\" | \"B\";\n testId: string;\n}\n\n/**\n * Get or assign an A/B test variant.\n * Reads/creates the lb_session cookie and calls /api/ab-assign.\n */\nexport async function getABTestAssignment(\n appUrl: string,\n shopDomain: string,\n testId: string,\n): Promise<ABTestAssignment | null> {\n if (typeof document === \"undefined\") return null; // SSR guard\n\n const sessionId = getOrCreateSessionId();\n\n // Deterministic assignment: fnv1a(sessionId + testId) % 2\n const variant = fnv1aVariant(sessionId, testId);\n\n // Fire-and-forget 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(() => {}); // Swallow errors — assignment is best-effort\n } catch {\n // Silently ignore\n }\n\n return { variant, testId };\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 document.cookie = `${SESSION_COOKIE_NAME}=${id}; path=/; max-age=${SESSION_COOKIE_MAX_AGE}; SameSite=Lax`;\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","/**\n * DOM renderer for fixed bundles.\n *\n * Takes the narrowed `FixedBundleData` variant so we get TS errors if any\n * caller passes a non-fixed bundle.\n *\n * `onAddToCart` is a BYO-cart dispatch: the widget owner listens for\n * `lime-bundle:add-to-cart` (CustomEvent) and performs the actual cart\n * mutation. This renderer only builds the DOM and invokes the dispatch\n * — it does not know how or whether the add succeeds.\n */\nimport {\n formatMoney,\n type FixedBundleData,\n type CartLineInput,\n} from \"@lime-bundles/core\";\n\nexport function renderFixedBundle(\n container: HTMLElement,\n bundle: FixedBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n) {\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n\n const title = document.createElement(\"h3\");\n title.className = \"lb-bundle__title\";\n title.textContent = bundle.title;\n title.setAttribute(\"part\", \"title\");\n container.appendChild(title);\n\n if (bundle.discountLabel) {\n const badge = document.createElement(\"span\");\n badge.className = \"lb-bundle__discount-badge\";\n badge.textContent = bundle.discountLabel;\n container.appendChild(badge);\n }\n\n const productsDiv = document.createElement(\"div\");\n productsDiv.className = \"lb-bundle__products\";\n\n for (const product of bundle.products) {\n const productEl = document.createElement(\"div\");\n productEl.className = \"lb-bundle__product\";\n productEl.setAttribute(\"part\", \"product\");\n\n if (product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = product.featuredImage.url;\n img.alt = product.featuredImage.altText ?? product.title;\n img.className = \"lb-bundle__product-image\";\n img.loading = \"lazy\";\n productEl.appendChild(img);\n }\n\n const info = document.createElement(\"div\");\n info.className = \"lb-bundle__product-info\";\n info.innerHTML = `\n <p class=\"lb-bundle__product-title\">${escapeHtml(product.title)}</p>\n <p class=\"lb-bundle__product-price\">${escapeHtml(formatMoney(product.priceRange.minVariantPrice.amount, currency))}</p>\n `;\n productEl.appendChild(info);\n productsDiv.appendChild(productEl);\n }\n container.appendChild(productsDiv);\n\n const button = document.createElement(\"button\");\n button.className = \"lb-bundle__cta\";\n button.textContent = bundle.widgetConfig.ctaText ?? \"Add Bundle to Cart\";\n button.setAttribute(\"part\", \"button\");\n\n button.addEventListener(\"click\", () => {\n const lines: CartLineInput[] = bundle.products\n .filter((p) => p.variants.nodes.some((v) => v.availableForSale))\n .map((p) => {\n const variant = p.variants.nodes.find((v) => v.availableForSale)!;\n return {\n merchandiseId: variant.id,\n quantity: 1,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n };\n });\n\n if (lines.length === 0) return;\n onAddToCart(lines);\n });\n\n container.appendChild(button);\n}\n\nfunction escapeHtml(str: string): string {\n const div = document.createElement(\"div\");\n div.textContent = str;\n return div.innerHTML;\n}\n","/**\n * DOM renderer for mix-and-match bundles. See fixed.ts for the BYO-cart\n * contract.\n */\nimport {\n formatMoney,\n validateQuantity,\n type MixMatchBundleData,\n type CartLineInput,\n} from \"@lime-bundles/core\";\n\nexport function renderMixMatchBundle(\n container: HTMLElement,\n bundle: MixMatchBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n) {\n const currency =\n bundle.products[0]?.priceRange.minVariantPrice.currencyCode ?? \"USD\";\n const selections = new Map<string, { variantId: string; quantity: number }>();\n\n const title = document.createElement(\"h3\");\n title.className = \"lb-bundle__title\";\n title.textContent = bundle.title;\n title.setAttribute(\"part\", \"title\");\n container.appendChild(title);\n\n const instructions = document.createElement(\"p\");\n instructions.className = \"lb-bundle__instructions\";\n instructions.textContent =\n bundle.minQuantity && bundle.maxQuantity\n ? `Select ${bundle.minQuantity}–${bundle.maxQuantity} items`\n : bundle.minQuantity\n ? `Select at least ${bundle.minQuantity} items`\n : \"Select your items\";\n container.appendChild(instructions);\n\n const productsDiv = document.createElement(\"div\");\n productsDiv.className = \"lb-bundle__products lb-bundle__products--selectable\";\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 productEl = document.createElement(\"div\");\n productEl.className = \"lb-bundle__product lb-bundle__product--selectable\";\n\n if (product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = product.featuredImage.url;\n img.alt = product.featuredImage.altText ?? product.title;\n img.className = \"lb-bundle__product-image\";\n img.loading = \"lazy\";\n productEl.appendChild(img);\n }\n\n const info = document.createElement(\"div\");\n info.className = \"lb-bundle__product-info\";\n info.innerHTML = `\n <p class=\"lb-bundle__product-title\">${escapeHtml(product.title)}</p>\n <p class=\"lb-bundle__product-price\">${escapeHtml(formatMoney(variant.price.amount, currency))}</p>\n `;\n productEl.appendChild(info);\n\n const selectBtn = document.createElement(\"button\");\n selectBtn.className = \"lb-bundle__select-btn\";\n selectBtn.textContent = variant.availableForSale ? \"Select\" : \"Sold out\";\n selectBtn.disabled = !variant.availableForSale;\n\n selectBtn.addEventListener(\"click\", () => {\n const key = product.id;\n if (selections.has(key)) {\n selections.delete(key);\n productEl.classList.remove(\"lb-bundle__product--selected\");\n selectBtn.textContent = \"Select\";\n } else {\n selections.set(key, { variantId: variant.id, quantity: 1 });\n productEl.classList.add(\"lb-bundle__product--selected\");\n selectBtn.textContent = \"Selected\";\n }\n updateCta();\n });\n\n productEl.appendChild(selectBtn);\n productsDiv.appendChild(productEl);\n }\n container.appendChild(productsDiv);\n\n const validationEl = document.createElement(\"p\");\n validationEl.className = \"lb-bundle__validation\";\n container.appendChild(validationEl);\n\n const button = document.createElement(\"button\");\n button.className = \"lb-bundle__cta\";\n button.setAttribute(\"part\", \"button\");\n button.disabled = true;\n container.appendChild(button);\n\n function updateCta() {\n const total = Array.from(selections.values()).reduce(\n (s, v) => s + v.quantity,\n 0,\n );\n const validation = validateQuantity(\n total,\n bundle.minQuantity,\n bundle.maxQuantity,\n );\n button.disabled = !validation.valid;\n button.textContent =\n bundle.widgetConfig.ctaText ?? `Add ${total} Items to Cart`;\n validationEl.textContent = validation.message ?? \"\";\n }\n\n updateCta();\n\n button.addEventListener(\"click\", () => {\n const lines: CartLineInput[] = Array.from(selections.values()).map((s) => ({\n merchandiseId: s.variantId,\n quantity: s.quantity,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n }));\n\n if (lines.length === 0) return;\n onAddToCart(lines);\n });\n}\n\nfunction escapeHtml(str: string): string {\n const div = document.createElement(\"div\");\n div.textContent = str;\n return div.innerHTML;\n}\n","/**\n * DOM renderer for volume bundles. See fixed.ts for the BYO-cart contract.\n */\nimport {\n formatMoney,\n calculateTierSavings,\n type VolumeBundleData,\n type CartLineInput,\n} from \"@lime-bundles/core\";\n\nexport function renderVolumeBundle(\n container: HTMLElement,\n bundle: VolumeBundleData,\n onAddToCart: (lines: CartLineInput[]) => void,\n) {\n const product = bundle.products[0];\n if (!product) return;\n\n const basePrice = parseFloat(product.priceRange.minVariantPrice.amount);\n const currency = product.priceRange.minVariantPrice.currencyCode;\n let quantity = 1;\n\n const title = document.createElement(\"h3\");\n title.className = \"lb-bundle__title\";\n title.textContent = bundle.title;\n title.setAttribute(\"part\", \"title\");\n container.appendChild(title);\n\n const productEl = document.createElement(\"div\");\n productEl.className = \"lb-bundle__product lb-bundle__product--volume\";\n if (product.featuredImage) {\n const img = document.createElement(\"img\");\n img.src = product.featuredImage.url;\n img.alt = product.featuredImage.altText ?? product.title;\n img.className = \"lb-bundle__product-image\";\n img.loading = \"lazy\";\n productEl.appendChild(img);\n }\n const info = document.createElement(\"div\");\n info.className = \"lb-bundle__product-info\";\n info.innerHTML = `\n <p class=\"lb-bundle__product-title\">${escapeHtml(product.title)}</p>\n <p class=\"lb-bundle__product-price\">${escapeHtml(formatMoney(basePrice, currency))} each</p>\n `;\n productEl.appendChild(info);\n container.appendChild(productEl);\n\n const tiersDiv = document.createElement(\"div\");\n tiersDiv.className = \"lb-bundle__tiers\";\n tiersDiv.setAttribute(\"role\", \"table\");\n tiersDiv.setAttribute(\"aria-label\", \"Volume discounts\");\n container.appendChild(tiersDiv);\n\n const qtyWrapper = document.createElement(\"div\");\n qtyWrapper.className = \"lb-bundle__quantity-selector\";\n const label = document.createElement(\"label\");\n label.textContent = \"Quantity\";\n qtyWrapper.appendChild(label);\n\n const qtyControl = document.createElement(\"div\");\n qtyControl.className = \"lb-bundle__quantity-control\";\n\n const minusBtn = document.createElement(\"button\");\n minusBtn.textContent = \"−\";\n minusBtn.setAttribute(\"aria-label\", \"Decrease quantity\");\n\n const qtyInput = document.createElement(\"input\");\n qtyInput.type = \"number\";\n qtyInput.min = \"1\";\n qtyInput.value = \"1\";\n qtyInput.className = \"lb-bundle__quantity-input\";\n\n const plusBtn = document.createElement(\"button\");\n plusBtn.textContent = \"+\";\n plusBtn.setAttribute(\"aria-label\", \"Increase quantity\");\n\n qtyControl.append(minusBtn, qtyInput, plusBtn);\n qtyWrapper.appendChild(qtyControl);\n container.appendChild(qtyWrapper);\n\n const button = document.createElement(\"button\");\n button.className = \"lb-bundle__cta\";\n button.setAttribute(\"part\", \"button\");\n container.appendChild(button);\n\n function updateTiers() {\n const savings = calculateTierSavings(\n bundle.volumeTiers,\n basePrice,\n quantity,\n );\n tiersDiv.innerHTML = \"\";\n for (const ts of savings) {\n const row = document.createElement(\"div\");\n row.className = `lb-bundle__tier${ts.isActive ? \" lb-bundle__tier--active\" : \"\"}`;\n row.setAttribute(\"role\", \"row\");\n row.innerHTML = `\n <span class=\"lb-bundle__tier-quantity\" role=\"cell\">${ts.tier.minQuantity}+ items</span>\n <span class=\"lb-bundle__tier-price\" role=\"cell\">${escapeHtml(formatMoney(ts.unitPrice, currency))} each</span>\n <span class=\"lb-bundle__tier-savings\" role=\"cell\">Save ${ts.savingsPercent.toFixed(0)}%</span>\n ${ts.tier.label ? `<span class=\"lb-bundle__tier-label\" role=\"cell\">${escapeHtml(ts.tier.label)}</span>` : \"\"}\n `;\n tiersDiv.appendChild(row);\n }\n button.textContent = bundle.widgetConfig.ctaText ?? `Add ${quantity} to Cart`;\n }\n\n updateTiers();\n\n minusBtn.addEventListener(\"click\", () => {\n if (quantity > 1) {\n quantity--;\n qtyInput.value = String(quantity);\n updateTiers();\n }\n });\n plusBtn.addEventListener(\"click\", () => {\n quantity++;\n qtyInput.value = String(quantity);\n updateTiers();\n });\n qtyInput.addEventListener(\"change\", () => {\n const val = parseInt(qtyInput.value, 10);\n if (!isNaN(val) && val > 0) {\n quantity = val;\n updateTiers();\n }\n });\n\n button.addEventListener(\"click\", () => {\n const variant = product.variants.nodes.find((v) => v.availableForSale);\n if (!variant) return;\n\n onAddToCart([\n {\n merchandiseId: variant.id,\n quantity,\n attributes: [\n { key: \"_lime_bundle_gid\", value: bundle.id },\n { key: \"_lime_bundle_type\", value: bundle.bundleType },\n ],\n },\n ]);\n });\n}\n\nfunction escapeHtml(str: string): string {\n const div = document.createElement(\"div\");\n div.textContent = str;\n return div.innerHTML;\n}\n","/**\n * CSS styles inlined into Shadow DOM.\n * Uses CSS custom properties that pierce the shadow boundary for theming.\n */\nexport const WIDGET_STYLES = `\n:host {\n display: block;\n --lb-primary-color: #000;\n --lb-secondary-color: #666;\n --lb-accent-color: #2563eb;\n --lb-background: #fff;\n --lb-border-color: #e5e7eb;\n --lb-border-radius: 8px;\n --lb-font-family: inherit;\n --lb-font-size: 14px;\n --lb-spacing-sm: 8px;\n --lb-spacing-md: 16px;\n --lb-spacing-lg: 24px;\n --lb-button-bg: var(--lb-accent-color);\n --lb-button-text: #fff;\n --lb-button-radius: var(--lb-border-radius);\n --lb-savings-color: #16a34a;\n --lb-error-color: #dc2626;\n}\n\n.lb-bundle {\n font-family: var(--lb-font-family);\n font-size: var(--lb-font-size);\n color: var(--lb-primary-color);\n background: var(--lb-background);\n border: 1px solid var(--lb-border-color);\n border-radius: var(--lb-border-radius);\n padding: var(--lb-spacing-lg);\n}\n\n.lb-bundle__title { margin: 0 0 var(--lb-spacing-md); font-size: 1.25em; font-weight: 600; }\n.lb-bundle__discount-badge { display: inline-block; background: var(--lb-savings-color); color: #fff; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 600; margin-bottom: var(--lb-spacing-md); }\n.lb-bundle__products { display: grid; gap: var(--lb-spacing-md); margin-bottom: var(--lb-spacing-lg); }\n.lb-bundle__product { display: flex; gap: var(--lb-spacing-md); align-items: center; padding: var(--lb-spacing-sm); border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); }\n.lb-bundle__product--selected { border-color: var(--lb-accent-color); }\n.lb-bundle__product-image { width: 64px; height: 64px; object-fit: cover; border-radius: calc(var(--lb-border-radius) - 2px); flex-shrink: 0; }\n.lb-bundle__product-info { flex: 1; min-width: 0; }\n.lb-bundle__product-title { margin: 0; font-weight: 500; }\n.lb-bundle__product-price { margin: 4px 0 0; color: var(--lb-secondary-color); }\n.lb-bundle__instructions { color: var(--lb-secondary-color); margin: 0 0 var(--lb-spacing-md); }\n.lb-bundle__tiers { margin-bottom: var(--lb-spacing-lg); }\n.lb-bundle__tier { display: flex; align-items: center; gap: var(--lb-spacing-md); padding: var(--lb-spacing-sm) var(--lb-spacing-md); border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); margin-bottom: var(--lb-spacing-sm); }\n.lb-bundle__tier--active { border-color: var(--lb-savings-color); }\n.lb-bundle__tier-savings { color: var(--lb-savings-color); font-weight: 600; }\n.lb-bundle__quantity-selector { margin-bottom: var(--lb-spacing-lg); }\n.lb-bundle__quantity-selector label { display: block; margin-bottom: var(--lb-spacing-sm); font-weight: 500; }\n.lb-bundle__quantity-control { display: inline-flex; align-items: center; border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); }\n.lb-bundle__quantity-control button { width: 32px; height: 32px; border: none; background: transparent; cursor: pointer; font-size: 1.1em; display: flex; align-items: center; justify-content: center; }\n.lb-bundle__quantity-input { width: 40px; text-align: center; border: none; border-left: 1px solid var(--lb-border-color); border-right: 1px solid var(--lb-border-color); height: 32px; font-size: var(--lb-font-size); -moz-appearance: textfield; }\n.lb-bundle__quantity-input::-webkit-outer-spin-button, .lb-bundle__quantity-input::-webkit-inner-spin-button { -webkit-appearance: none; }\n.lb-bundle__select-btn { padding: 6px 12px; border: 1px solid var(--lb-border-color); border-radius: var(--lb-border-radius); background: transparent; cursor: pointer; }\n.lb-bundle__select-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n.lb-bundle__cta { width: 100%; padding: 12px 24px; border: none; border-radius: var(--lb-button-radius); background: var(--lb-button-bg); color: var(--lb-button-text); font-size: 1em; font-weight: 600; cursor: pointer; transition: opacity 0.15s; }\n.lb-bundle__cta:hover:not(:disabled) { opacity: 0.9; }\n.lb-bundle__cta:disabled { opacity: 0.5; cursor: not-allowed; }\n.lb-bundle__error { color: var(--lb-error-color); margin: var(--lb-spacing-sm) 0; font-size: 0.9em; }\n.lb-bundle__validation { color: var(--lb-secondary-color); margin: var(--lb-spacing-sm) 0; font-size: 0.9em; }\n\n.lb-skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: lb-shimmer 1.5s infinite; border-radius: var(--lb-border-radius); }\n.lb-skeleton--title { height: 24px; width: 60%; margin-bottom: var(--lb-spacing-md); }\n.lb-skeleton--products { height: 200px; }\n@keyframes lb-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }\n`;\n","/**\n * <lime-bundle> Web Component — renders Lime Bundles on any storefront.\n *\n * Usage:\n * <lime-bundle\n * shop-domain=\"my-store.myshopify.com\"\n * storefront-token=\"abc123\"\n * bundle-gid=\"gid://shopify/Metaobject/12345\"\n * app-url=\"https://bundles.example.com\"\n * ></lime-bundle>\n *\n * document.querySelector(\"lime-bundle\").addEventListener(\n * \"lime-bundle:add-to-cart\",\n * (ev) => { cart.linesAdd(ev.detail.lines); }\n * );\n *\n * BYO-cart model: the widget fires `lime-bundle:add-to-cart` with a\n * `CartLineInput[]` payload in the `detail.lines` field. Merchants wire\n * this to their cart system (Hydrogen's useCart, Storefront Cart API,\n * ajax cart — whatever). The widget does not perform the cart add\n * itself; it optimistically reports success to the UI after dispatch.\n *\n * Analytics: the widget calls `reportImpression` / `reportAddToCart`\n * against the app URL if the `analytics` attribute is not \"false\" and\n * `app-url` is set. These fire-and-forget.\n */\nimport {\n createStorefrontClient,\n BUNDLE_METAOBJECT_QUERY,\n SHOP_CUSTOM_CSS_QUERY,\n parseMetaobjectBundle,\n observeImpression,\n reportImpression,\n reportAddToCart,\n injectCustomCss,\n type ParsedBundle,\n type FixedBundleData,\n type VolumeBundleData,\n type MixMatchBundleData,\n type BundleMetaobjectResponse,\n type ShopCustomCssResponse,\n type CartLineInput,\n} from \"@lime-bundles/core\";\nimport { renderFixedBundle } from \"./renderers/fixed\";\nimport { renderMixMatchBundle } from \"./renderers/mix-match\";\nimport { renderVolumeBundle } from \"./renderers/volume\";\nimport { WIDGET_STYLES } from \"./styles/widget-styles\";\n\nexport class LimeBundleElement extends HTMLElement {\n static observedAttributes = [\n \"shop-domain\",\n \"storefront-token\",\n \"bundle-gid\",\n \"app-url\",\n \"analytics\",\n \"locale\",\n ];\n\n private shadow: ShadowRoot;\n private bundle: ParsedBundle | null = null;\n private abortController: AbortController | null = null;\n private impressionCleanup: (() => void) | null = null;\n\n constructor() {\n super();\n this.shadow = this.attachShadow({ mode: \"open\" });\n }\n\n connectedCallback() {\n this.render();\n this.fetchBundle();\n }\n\n disconnectedCallback() {\n this.abortController?.abort();\n this.teardownImpression();\n }\n\n private teardownImpression() {\n this.impressionCleanup?.();\n this.impressionCleanup = null;\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue || !this.isConnected) return;\n\n if (\n name === \"bundle-gid\" ||\n name === \"shop-domain\" ||\n name === \"storefront-token\"\n ) {\n if (this.shopDomain && this.storefrontToken && this.bundleGid) {\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 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 || !this.bundleGid) {\n this.renderError(\n \"Missing required attributes: shop-domain, storefront-token, bundle-gid\",\n );\n return;\n }\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n this.renderLoading();\n\n try {\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n\n // Fetch bundle data and custom CSS in parallel. Custom CSS is best-effort;\n // if it fails we still render the bundle without merchant styling.\n const [bundleData, cssData] = await Promise.all([\n client.query<BundleMetaobjectResponse>(\n BUNDLE_METAOBJECT_QUERY,\n { id: this.bundleGid },\n { signal: controller.signal },\n ),\n client\n .query<ShopCustomCssResponse>(\n SHOP_CUSTOM_CSS_QUERY,\n undefined,\n { signal: controller.signal },\n )\n .catch(() => null),\n ]);\n\n if (controller.signal.aborted) return;\n\n if (!bundleData.metaobject) {\n this.bundle = null;\n this.teardownImpression();\n this.renderError(\"Bundle not found\");\n return;\n }\n\n this.bundle = parseMetaobjectBundle(\n bundleData.metaobject.id,\n bundleData.metaobject.fields,\n );\n\n if (!this.bundle) {\n this.teardownImpression();\n this.renderError(\"Bundle is not active or has expired\");\n return;\n }\n\n // Best-effort custom CSS injection. No-op in jsdom/SSR contexts.\n if (cssData?.shop?.metafield?.value) {\n injectCustomCss(this.shopDomain, cssData.shop.metafield.value);\n }\n\n this.renderBundle();\n this.setupImpression();\n } catch (err) {\n if (controller.signal.aborted) return;\n this.bundle = null;\n this.teardownImpression();\n this.renderError(\n err instanceof Error ? err.message : \"Failed to load bundle\",\n );\n }\n }\n\n /**\n * Dispatch add-to-cart for merchant handling. Returns true — the widget\n * reports success optimistically. If the merchant's cart mutation fails,\n * they're responsible for surfacing that error in their own UI.\n */\n private dispatchAddToCart = (lines: CartLineInput[]): void => {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:add-to-cart\", {\n detail: { lines },\n bubbles: true,\n composed: true,\n }),\n );\n\n if (this.analyticsEnabled && this.appUrl && this.bundle) {\n const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);\n const totalPrice = lines.reduce((sum, line) => {\n const product = this.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: this.bundleGid,\n bundleType: this.bundle.bundleType,\n productId: this.bundle.products[0]?.id ?? \"\",\n quantity,\n totalPrice: Math.round(totalPrice * 100) / 100,\n },\n );\n }\n };\n\n private renderBundle() {\n if (!this.bundle) return;\n\n const container = document.createElement(\"div\");\n container.className = \"lb-bundle\";\n container.setAttribute(\"role\", \"region\");\n container.setAttribute(\"aria-label\", this.bundle.title);\n\n switch (this.bundle.bundleType) {\n case \"fixed\":\n renderFixedBundle(\n container,\n this.bundle as FixedBundleData,\n this.dispatchAddToCart,\n );\n break;\n case \"mix_match\":\n renderMixMatchBundle(\n container,\n this.bundle as MixMatchBundleData,\n this.dispatchAddToCart,\n );\n break;\n case \"volume\":\n renderVolumeBundle(\n container,\n this.bundle as VolumeBundleData,\n this.dispatchAddToCart,\n );\n break;\n }\n\n this.shadow.innerHTML = \"\";\n const style = document.createElement(\"style\");\n style.textContent = WIDGET_STYLES;\n this.shadow.appendChild(style);\n this.shadow.appendChild(container);\n\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:loaded\", {\n detail: {\n bundleType: this.bundle.bundleType,\n title: this.bundle.title,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private setupImpression() {\n if (!this.analyticsEnabled || !this.bundle || !this.appUrl) return;\n\n this.impressionCleanup?.();\n this.impressionCleanup = observeImpression(this, () => {\n reportImpression(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: this.bundleGid,\n bundleType: this.bundle!.bundleType,\n },\n );\n });\n }\n\n private renderLoading() {\n this.shadow.innerHTML = `\n <style>${WIDGET_STYLES}</style>\n <div class=\"lb-bundle lb-bundle--loading\">\n <div class=\"lb-skeleton lb-skeleton--title\"></div>\n <div class=\"lb-skeleton lb-skeleton--products\"></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 private render() {\n this.shadow.innerHTML = `<style>${WIDGET_STYLES}</style>`;\n }\n}\n"],"mappings":"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","names":["src_exports","__export","LimeBundleElement","StorefrontApiError","errors","DEFAULT_API_VERSION","createStorefrontClient","config","version","endpoint","query","variables","options","headers","response","json","BUNDLE_METAOBJECT_QUERY","SHOP_CUSTOM_CSS_QUERY","BundleParseError","message","reason","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","parseJsonField","parseVolumeTiers","parseIntField","productsField","node","collectionField","raw","obj","key","value","num","calculateTierSavings","tiers","basePrice","currentQuantity","a","b","tier","discount","unitPrice","savings","savingsPercent","isActive","validateQuantity","totalQuantity","minQuantity","maxQuantity","reportImpression","config","event","sendEvent","reportAddToCart","observeImpression","element","callback","observer","entries","entry","payload","url","body","r","SESSION_COOKIE_MAX_AGE","MAX_CSS_LENGTH","BLOCKED_PATTERNS","SAFE_URL_VALUE","sanitizeCustomCss","raw","css","_","hex","codePoint","pattern","label","urlPattern","urlMatch","urlValue","STYLE_ID_PREFIX","injectCustomCss","shopDomain","rawCss","sanitized","id","simpleHash","style","input","hash","i","formatMoney","amount","currencyCode","num","renderFixedBundle","container","bundle","onAddToCart","currency","title","badge","productsDiv","product","productEl","img","info","escapeHtml","formatMoney","button","lines","p","v","str","div","renderMixMatchBundle","container","bundle","onAddToCart","currency","selections","title","instructions","productsDiv","product","variant","v","productEl","img","info","escapeHtml","formatMoney","selectBtn","key","updateCta","validationEl","button","total","s","validation","validateQuantity","lines","str","div","renderVolumeBundle","container","bundle","onAddToCart","product","basePrice","currency","quantity","title","productEl","img","info","escapeHtml","formatMoney","tiersDiv","qtyWrapper","label","qtyControl","minusBtn","qtyInput","plusBtn","button","updateTiers","savings","calculateTierSavings","ts","row","val","variant","str","div","WIDGET_STYLES","LimeBundleElement","name","oldValue","newValue","controller","client","createStorefrontClient","bundleData","cssData","BUNDLE_METAOBJECT_QUERY","SHOP_CUSTOM_CSS_QUERY","parseMetaobjectBundle","injectCustomCss","err","lines","quantity","sum","l","totalPrice","line","variant","p","v","price","reportAddToCart","container","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","style","WIDGET_STYLES","observeImpression","reportImpression","message","LimeBundleElement"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lime-bundles/widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Framework-agnostic <lime-bundle> web component for Lime Bundles headless storefronts — Astro, Vue, Svelte, plain HTML, and mobile webviews.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
14
|
},
|
|
15
|
-
"./thankyou": {
|
|
16
|
-
"import": "./dist/lime-thankyou.js",
|
|
17
|
-
"require": "./dist/lime-thankyou.cjs"
|
|
18
|
-
},
|
|
19
15
|
"./package.json": "./package.json"
|
|
20
16
|
},
|
|
21
17
|
"files": [
|
|
@@ -49,7 +45,7 @@
|
|
|
49
45
|
"typecheck": "tsc --noEmit"
|
|
50
46
|
},
|
|
51
47
|
"dependencies": {
|
|
52
|
-
"@lime-bundles/core": "^0.
|
|
48
|
+
"@lime-bundles/core": "^0.2.0"
|
|
53
49
|
},
|
|
54
50
|
"devDependencies": {
|
|
55
51
|
"tsup": "^8.0.0",
|
package/dist/lime-thankyou.cjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use strict";var d=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var h=Object.prototype.hasOwnProperty;var m=(e,r)=>{for(var o in r)d(e,o,{get:r[o],enumerable:!0})},y=(e,r,o,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of l(r))!h.call(e,t)&&t!==o&&d(e,t,{get:()=>r[t],enumerable:!(n=p(r,t))||n.enumerable});return e};var b=e=>y(d({},"__esModule",{value:!0}),e);var g={};m(g,{trackPurchase:()=>f});module.exports=b(g);var u=new Set;function f(e){let r=e.appUrl??`https://${e.shopDomain}`,o=new Map;for(let n of e.lineItems){if(!n.bundleGid)continue;let t=o.get(n.bundleGid);t?(t.revenue+=n.price*n.quantity,t.lineItemCount+=1):o.set(n.bundleGid,{bundleType:n.bundleType,revenue:n.price*n.quantity,lineItemCount:1})}for(let[n,t]of o){let s=`${e.orderId}:${n}`;if(u.has(s))continue;u.add(s);let c={shopDomain:e.shopDomain,eventType:"bundle_purchased",bundleGid:n,bundleType:t.bundleType,orderId:e.orderId,revenue:Math.round(t.revenue*100)/100,lineItemCount:t.lineItemCount,occurredAt:new Date().toISOString()},a=`${r}/api/analytics`,i=JSON.stringify(c);try{fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:i}).catch(()=>{typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(a,i)})}catch{typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(a,i)}}}0&&(module.exports={trackPurchase});
|
|
2
|
-
//# sourceMappingURL=lime-thankyou.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/thankyou/index.ts"],"sourcesContent":["/**\n * Thank-you page snippet — fires purchase events for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-thankyou.js\"></script>\n * <script>\n * LimeBundles.trackPurchase({\n * shopDomain: 'my-store.myshopify.com',\n * orderId: 'gid://shopify/Order/12345',\n * lineItems: [\n * { bundleGid: '...', bundleType: 'fixed', price: 29.99, quantity: 1 }\n * ]\n * });\n * </script>\n */\n\ninterface PurchaseLineItem {\n bundleGid: string;\n bundleType: string;\n price: number;\n quantity: number;\n}\n\ninterface TrackPurchaseInput {\n shopDomain: string;\n orderId: string;\n lineItems: PurchaseLineItem[];\n appUrl?: string;\n}\n\n// Deduplication: track order+bundle combos we've already reported\nconst reportedPurchases = new Set<string>();\n\nexport function trackPurchase(input: TrackPurchaseInput): void {\n const appUrl = input.appUrl ?? `https://${input.shopDomain}`;\n\n // Group line items by bundleGid (merge quantities and prices)\n const bundleMap = new Map<string, { bundleType: string; revenue: number; lineItemCount: number }>();\n\n for (const item of input.lineItems) {\n if (!item.bundleGid) continue;\n\n const existing = bundleMap.get(item.bundleGid);\n if (existing) {\n existing.revenue += item.price * item.quantity;\n existing.lineItemCount += 1;\n } else {\n bundleMap.set(item.bundleGid, {\n bundleType: item.bundleType,\n revenue: item.price * item.quantity,\n lineItemCount: 1,\n });\n }\n }\n\n // Fire purchase events for each unique bundle\n for (const [bundleGid, data] of bundleMap) {\n const dedupKey = `${input.orderId}:${bundleGid}`;\n if (reportedPurchases.has(dedupKey)) continue;\n reportedPurchases.add(dedupKey);\n\n const payload = {\n shopDomain: input.shopDomain,\n eventType: \"bundle_purchased\",\n bundleGid,\n bundleType: data.bundleType,\n orderId: input.orderId,\n revenue: Math.round(data.revenue * 100) / 100,\n lineItemCount: data.lineItemCount,\n occurredAt: new Date().toISOString(),\n };\n\n // Fire-and-forget with sendBeacon fallback\n const url = `${appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n }).catch(() => {\n // Fallback to sendBeacon\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n });\n } catch {\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n }\n }\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,IAAA,eAAAC,EAAAH,GA+BA,IAAMI,EAAoB,IAAI,IAEvB,SAASF,EAAcG,EAAiC,CAC7D,IAAMC,EAASD,EAAM,QAAU,WAAWA,EAAM,UAAU,GAGpDE,EAAY,IAAI,IAEtB,QAAWC,KAAQH,EAAM,UAAW,CAClC,GAAI,CAACG,EAAK,UAAW,SAErB,IAAMC,EAAWF,EAAU,IAAIC,EAAK,SAAS,EACzCC,GACFA,EAAS,SAAWD,EAAK,MAAQA,EAAK,SACtCC,EAAS,eAAiB,GAE1BF,EAAU,IAAIC,EAAK,UAAW,CAC5B,WAAYA,EAAK,WACjB,QAASA,EAAK,MAAQA,EAAK,SAC3B,cAAe,CACjB,CAAC,CAEL,CAGA,OAAW,CAACE,EAAWC,CAAI,IAAKJ,EAAW,CACzC,IAAMK,EAAW,GAAGP,EAAM,OAAO,IAAIK,CAAS,GAC9C,GAAIN,EAAkB,IAAIQ,CAAQ,EAAG,SACrCR,EAAkB,IAAIQ,CAAQ,EAE9B,IAAMC,EAAU,CACd,WAAYR,EAAM,WAClB,UAAW,mBACX,UAAAK,EACA,WAAYC,EAAK,WACjB,QAASN,EAAM,QACf,QAAS,KAAK,MAAMM,EAAK,QAAU,GAAG,EAAI,IAC1C,cAAeA,EAAK,cACpB,WAAY,IAAI,KAAK,EAAE,YAAY,CACrC,EAGMG,EAAM,GAAGR,CAAM,iBACfS,EAAO,KAAK,UAAUF,CAAO,EAEnC,GAAI,CACF,MAAMC,EAAK,CACT,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAC,CACF,CAAC,EAAE,MAAM,IAAM,CAET,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,CAAC,CACH,MAAQ,CACF,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,CACF,CACF","names":["thankyou_exports","__export","trackPurchase","__toCommonJS","reportedPurchases","input","appUrl","bundleMap","item","existing","bundleGid","data","dedupKey","payload","url","body"]}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
"use strict";var LimeBundles=(()=>{var d=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var h=Object.prototype.hasOwnProperty;var m=(e,r)=>{for(var o in r)d(e,o,{get:r[o],enumerable:!0})},y=(e,r,o,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let t of l(r))!h.call(e,t)&&t!==o&&d(e,t,{get:()=>r[t],enumerable:!(n=p(r,t))||n.enumerable});return e};var b=e=>y(d({},"__esModule",{value:!0}),e);var g={};m(g,{trackPurchase:()=>f});var u=new Set;function f(e){let r=e.appUrl??`https://${e.shopDomain}`,o=new Map;for(let n of e.lineItems){if(!n.bundleGid)continue;let t=o.get(n.bundleGid);t?(t.revenue+=n.price*n.quantity,t.lineItemCount+=1):o.set(n.bundleGid,{bundleType:n.bundleType,revenue:n.price*n.quantity,lineItemCount:1})}for(let[n,t]of o){let s=`${e.orderId}:${n}`;if(u.has(s))continue;u.add(s);let c={shopDomain:e.shopDomain,eventType:"bundle_purchased",bundleGid:n,bundleType:t.bundleType,orderId:e.orderId,revenue:Math.round(t.revenue*100)/100,lineItemCount:t.lineItemCount,occurredAt:new Date().toISOString()},a=`${r}/api/analytics`,i=JSON.stringify(c);try{fetch(a,{method:"POST",headers:{"Content-Type":"application/json"},body:i}).catch(()=>{typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(a,i)})}catch{typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(a,i)}}}return b(g);})();
|
|
2
|
-
//# sourceMappingURL=lime-thankyou.global.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/thankyou/index.ts"],"sourcesContent":["/**\n * Thank-you page snippet — fires purchase events for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-thankyou.js\"></script>\n * <script>\n * LimeBundles.trackPurchase({\n * shopDomain: 'my-store.myshopify.com',\n * orderId: 'gid://shopify/Order/12345',\n * lineItems: [\n * { bundleGid: '...', bundleType: 'fixed', price: 29.99, quantity: 1 }\n * ]\n * });\n * </script>\n */\n\ninterface PurchaseLineItem {\n bundleGid: string;\n bundleType: string;\n price: number;\n quantity: number;\n}\n\ninterface TrackPurchaseInput {\n shopDomain: string;\n orderId: string;\n lineItems: PurchaseLineItem[];\n appUrl?: string;\n}\n\n// Deduplication: track order+bundle combos we've already reported\nconst reportedPurchases = new Set<string>();\n\nexport function trackPurchase(input: TrackPurchaseInput): void {\n const appUrl = input.appUrl ?? `https://${input.shopDomain}`;\n\n // Group line items by bundleGid (merge quantities and prices)\n const bundleMap = new Map<string, { bundleType: string; revenue: number; lineItemCount: number }>();\n\n for (const item of input.lineItems) {\n if (!item.bundleGid) continue;\n\n const existing = bundleMap.get(item.bundleGid);\n if (existing) {\n existing.revenue += item.price * item.quantity;\n existing.lineItemCount += 1;\n } else {\n bundleMap.set(item.bundleGid, {\n bundleType: item.bundleType,\n revenue: item.price * item.quantity,\n lineItemCount: 1,\n });\n }\n }\n\n // Fire purchase events for each unique bundle\n for (const [bundleGid, data] of bundleMap) {\n const dedupKey = `${input.orderId}:${bundleGid}`;\n if (reportedPurchases.has(dedupKey)) continue;\n reportedPurchases.add(dedupKey);\n\n const payload = {\n shopDomain: input.shopDomain,\n eventType: \"bundle_purchased\",\n bundleGid,\n bundleType: data.bundleType,\n orderId: input.orderId,\n revenue: Math.round(data.revenue * 100) / 100,\n lineItemCount: data.lineItemCount,\n occurredAt: new Date().toISOString(),\n };\n\n // Fire-and-forget with sendBeacon fallback\n const url = `${appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n }).catch(() => {\n // Fallback to sendBeacon\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n });\n } catch {\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n }\n }\n}\n"],"mappings":"+bAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,IA+BA,IAAMC,EAAoB,IAAI,IAEvB,SAASD,EAAcE,EAAiC,CAC7D,IAAMC,EAASD,EAAM,QAAU,WAAWA,EAAM,UAAU,GAGpDE,EAAY,IAAI,IAEtB,QAAWC,KAAQH,EAAM,UAAW,CAClC,GAAI,CAACG,EAAK,UAAW,SAErB,IAAMC,EAAWF,EAAU,IAAIC,EAAK,SAAS,EACzCC,GACFA,EAAS,SAAWD,EAAK,MAAQA,EAAK,SACtCC,EAAS,eAAiB,GAE1BF,EAAU,IAAIC,EAAK,UAAW,CAC5B,WAAYA,EAAK,WACjB,QAASA,EAAK,MAAQA,EAAK,SAC3B,cAAe,CACjB,CAAC,CAEL,CAGA,OAAW,CAACE,EAAWC,CAAI,IAAKJ,EAAW,CACzC,IAAMK,EAAW,GAAGP,EAAM,OAAO,IAAIK,CAAS,GAC9C,GAAIN,EAAkB,IAAIQ,CAAQ,EAAG,SACrCR,EAAkB,IAAIQ,CAAQ,EAE9B,IAAMC,EAAU,CACd,WAAYR,EAAM,WAClB,UAAW,mBACX,UAAAK,EACA,WAAYC,EAAK,WACjB,QAASN,EAAM,QACf,QAAS,KAAK,MAAMM,EAAK,QAAU,GAAG,EAAI,IAC1C,cAAeA,EAAK,cACpB,WAAY,IAAI,KAAK,EAAE,YAAY,CACrC,EAGMG,EAAM,GAAGR,CAAM,iBACfS,EAAO,KAAK,UAAUF,CAAO,EAEnC,GAAI,CACF,MAAMC,EAAK,CACT,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAC,CACF,CAAC,EAAE,MAAM,IAAM,CAET,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,CAAC,CACH,MAAQ,CACF,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,CACF,CACF","names":["thankyou_exports","__export","trackPurchase","reportedPurchases","input","appUrl","bundleMap","item","existing","bundleGid","data","dedupKey","payload","url","body"]}
|
package/dist/lime-thankyou.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
var d=new Set;function c(t){let s=t.appUrl??`https://${t.shopDomain}`,r=new Map;for(let e of t.lineItems){if(!e.bundleGid)continue;let n=r.get(e.bundleGid);n?(n.revenue+=e.price*e.quantity,n.lineItemCount+=1):r.set(e.bundleGid,{bundleType:e.bundleType,revenue:e.price*e.quantity,lineItemCount:1})}for(let[e,n]of r){let i=`${t.orderId}:${e}`;if(d.has(i))continue;d.add(i);let u={shopDomain:t.shopDomain,eventType:"bundle_purchased",bundleGid:e,bundleType:n.bundleType,orderId:t.orderId,revenue:Math.round(n.revenue*100)/100,lineItemCount:n.lineItemCount,occurredAt:new Date().toISOString()},o=`${s}/api/analytics`,a=JSON.stringify(u);try{fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:a}).catch(()=>{typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(o,a)})}catch{typeof navigator<"u"&&navigator.sendBeacon&&navigator.sendBeacon(o,a)}}}export{c as trackPurchase};
|
|
2
|
-
//# sourceMappingURL=lime-thankyou.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/thankyou/index.ts"],"sourcesContent":["/**\n * Thank-you page snippet — fires purchase events for headless storefronts.\n *\n * Usage:\n * <script src=\"https://unpkg.com/@lime-bundles/widget/dist/lime-thankyou.js\"></script>\n * <script>\n * LimeBundles.trackPurchase({\n * shopDomain: 'my-store.myshopify.com',\n * orderId: 'gid://shopify/Order/12345',\n * lineItems: [\n * { bundleGid: '...', bundleType: 'fixed', price: 29.99, quantity: 1 }\n * ]\n * });\n * </script>\n */\n\ninterface PurchaseLineItem {\n bundleGid: string;\n bundleType: string;\n price: number;\n quantity: number;\n}\n\ninterface TrackPurchaseInput {\n shopDomain: string;\n orderId: string;\n lineItems: PurchaseLineItem[];\n appUrl?: string;\n}\n\n// Deduplication: track order+bundle combos we've already reported\nconst reportedPurchases = new Set<string>();\n\nexport function trackPurchase(input: TrackPurchaseInput): void {\n const appUrl = input.appUrl ?? `https://${input.shopDomain}`;\n\n // Group line items by bundleGid (merge quantities and prices)\n const bundleMap = new Map<string, { bundleType: string; revenue: number; lineItemCount: number }>();\n\n for (const item of input.lineItems) {\n if (!item.bundleGid) continue;\n\n const existing = bundleMap.get(item.bundleGid);\n if (existing) {\n existing.revenue += item.price * item.quantity;\n existing.lineItemCount += 1;\n } else {\n bundleMap.set(item.bundleGid, {\n bundleType: item.bundleType,\n revenue: item.price * item.quantity,\n lineItemCount: 1,\n });\n }\n }\n\n // Fire purchase events for each unique bundle\n for (const [bundleGid, data] of bundleMap) {\n const dedupKey = `${input.orderId}:${bundleGid}`;\n if (reportedPurchases.has(dedupKey)) continue;\n reportedPurchases.add(dedupKey);\n\n const payload = {\n shopDomain: input.shopDomain,\n eventType: \"bundle_purchased\",\n bundleGid,\n bundleType: data.bundleType,\n orderId: input.orderId,\n revenue: Math.round(data.revenue * 100) / 100,\n lineItemCount: data.lineItemCount,\n occurredAt: new Date().toISOString(),\n };\n\n // Fire-and-forget with sendBeacon fallback\n const url = `${appUrl}/api/analytics`;\n const body = JSON.stringify(payload);\n\n try {\n fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n }).catch(() => {\n // Fallback to sendBeacon\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n });\n } catch {\n if (typeof navigator !== \"undefined\" && navigator.sendBeacon) {\n navigator.sendBeacon(url, body);\n }\n }\n }\n}\n"],"mappings":"AA+BA,IAAMA,EAAoB,IAAI,IAEvB,SAASC,EAAcC,EAAiC,CAC7D,IAAMC,EAASD,EAAM,QAAU,WAAWA,EAAM,UAAU,GAGpDE,EAAY,IAAI,IAEtB,QAAWC,KAAQH,EAAM,UAAW,CAClC,GAAI,CAACG,EAAK,UAAW,SAErB,IAAMC,EAAWF,EAAU,IAAIC,EAAK,SAAS,EACzCC,GACFA,EAAS,SAAWD,EAAK,MAAQA,EAAK,SACtCC,EAAS,eAAiB,GAE1BF,EAAU,IAAIC,EAAK,UAAW,CAC5B,WAAYA,EAAK,WACjB,QAASA,EAAK,MAAQA,EAAK,SAC3B,cAAe,CACjB,CAAC,CAEL,CAGA,OAAW,CAACE,EAAWC,CAAI,IAAKJ,EAAW,CACzC,IAAMK,EAAW,GAAGP,EAAM,OAAO,IAAIK,CAAS,GAC9C,GAAIP,EAAkB,IAAIS,CAAQ,EAAG,SACrCT,EAAkB,IAAIS,CAAQ,EAE9B,IAAMC,EAAU,CACd,WAAYR,EAAM,WAClB,UAAW,mBACX,UAAAK,EACA,WAAYC,EAAK,WACjB,QAASN,EAAM,QACf,QAAS,KAAK,MAAMM,EAAK,QAAU,GAAG,EAAI,IAC1C,cAAeA,EAAK,cACpB,WAAY,IAAI,KAAK,EAAE,YAAY,CACrC,EAGMG,EAAM,GAAGR,CAAM,iBACfS,EAAO,KAAK,UAAUF,CAAO,EAEnC,GAAI,CACF,MAAMC,EAAK,CACT,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAAC,CACF,CAAC,EAAE,MAAM,IAAM,CAET,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,CAAC,CACH,MAAQ,CACF,OAAO,UAAc,KAAe,UAAU,YAChD,UAAU,WAAWD,EAAKC,CAAI,CAElC,CACF,CACF","names":["reportedPurchases","trackPurchase","input","appUrl","bundleMap","item","existing","bundleGid","data","dedupKey","payload","url","body"]}
|