@lime-bundles/widget 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../../core/src/storefront-api/client.ts","../../core/src/storefront-api/queries.ts","../../core/src/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"]}
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/fetch-by-product.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/consent.ts","../../core/src/ab-test/assigner.ts","../../core/src/styles/sanitize.ts","../../core/src/styles/custom-css-injector.ts","../../core/src/utils/money.ts","../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 */\nimport type { MetaobjectField } from \"./types\";\n\nexport const BUNDLE_METAOBJECT_QUERY = `#graphql\n query BundleMetaobject($id: ID!) {\n metaobject(id: $id) {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Shop-level custom CSS metafield query. The SDK fetches this alongside\n * bundle data and auto-injects the result as a scoped <style> tag so\n * headless storefronts get CSS parity with classic-theme merchants.\n */\nexport const SHOP_CUSTOM_CSS_QUERY = `#graphql\n query ShopCustomCss {\n shop {\n metafield(namespace: \"$app\", key: \"custom_css\") {\n value\n }\n }\n }\n`;\n\nexport interface ShopCustomCssResponse {\n shop: {\n metafield: { value: string | null } | null;\n } | null;\n}\n\n/**\n * Product-aware bundle lookup. Mirrors the pattern the classic Liquid theme\n * block uses (`product.metafields[\"$app\"].active_bundle_ids`): one query\n * returns every bundle that applies to the current product, fully resolved\n * (nested product/collection references included) so the caller gets the\n * same shape as `parseMetaobjectBundleStrict` expects.\n *\n * The product handle is the natural input on a headless storefront — every\n * canonical Shopify product URL is `/products/<handle>`, so the widget can\n * often derive it from `window.location` without merchant input.\n *\n * Complexity: the nested references (bundle → products → variants) are deep\n * but stay within the Storefront API budget for token-based queries at\n * realistic bundle sizes (≤10 bundles per product, ≤50 products per bundle).\n */\nexport const BUNDLES_FOR_PRODUCT_QUERY = `#graphql\n query BundlesForProduct($handle: String!) {\n product(handle: $handle) {\n id\n handle\n title\n metafield(namespace: \"$app\", key: \"active_bundle_ids\") {\n references(first: 10) {\n nodes {\n ... on Metaobject {\n id\n type\n fields {\n key\n value\n reference {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\n }\n }\n }\n }\n references(first: 50) {\n nodes {\n ... on Product {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\n }\n }\n }\n ... on Collection {\n id\n title\n handle\n products(first: 50) {\n nodes {\n id\n title\n handle\n featuredImage { url altText }\n priceRange {\n minVariantPrice { amount currencyCode }\n maxVariantPrice { amount currencyCode }\n }\n variants(first: 100) {\n nodes {\n id\n title\n availableForSale\n price { amount currencyCode }\n compareAtPrice { amount currencyCode }\n selectedOptions { name value }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n`;\n\n/**\n * Return shape for BUNDLES_FOR_PRODUCT_QUERY. The inner `fields` array\n * reuses `MetaobjectField` verbatim so callers can pass it straight to\n * `parseMetaobjectBundleStrict` without a type cast — the same shape the\n * BUNDLE_METAOBJECT_QUERY response produces for a single bundle.\n */\nexport interface BundlesForProductResponse {\n product: {\n id: string;\n handle: string;\n title: string;\n metafield: {\n references: {\n nodes: Array<{\n id: string;\n type: string;\n fields: MetaobjectField[];\n }>;\n };\n } | null;\n } | null;\n}\n\n/**\n * Tokenless cart mutations. Storefront API docs: \"Cart (read/write)\" is\n * available on the Tokenless Access tier. The widget's default-cart path\n * uses these so merchants don't need a cart implementation.\n */\nexport const CART_CREATE_MUTATION = `#graphql\n mutation CartCreate($input: CartInput!) {\n cartCreate(input: $input) {\n cart { id checkoutUrl }\n userErrors { field message }\n }\n }\n`;\n\nexport const CART_LINES_ADD_MUTATION = `#graphql\n mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $lines) {\n cart { id checkoutUrl }\n userErrors { field message }\n }\n }\n`;\n\n/**\n * Inner payload shape shared by both cart mutations. Not a GraphQL\n * response on its own — see `CartCreateResponse` and `CartLinesAddResponse`\n * for the correctly-wrapped top-level response shapes.\n */\nexport interface CartMutationPayload {\n cart: { id: string; checkoutUrl: string } | null;\n userErrors: Array<{ field: string[] | null; message: string }>;\n}\n\n/** Top-level response shape for `CART_CREATE_MUTATION`. */\nexport interface CartCreateResponse {\n cartCreate: CartMutationPayload;\n}\n\n/** Top-level response shape for `CART_LINES_ADD_MUTATION`. */\nexport interface CartLinesAddResponse {\n cartLinesAdd: CartMutationPayload;\n}\n","/**\n * Bundle types for headless rendering.\n *\n * `ParsedBundle` is a discriminated union keyed on `bundleType` so consumers\n * can narrow the type via a switch statement or conditional check:\n *\n * function render(bundle: ParsedBundle) {\n * switch (bundle.bundleType) {\n * case \"fixed\": // bundle.products is load-bearing\n * case \"volume\": // bundle.volumeTiers is load-bearing\n * case \"mix_match\": // bundle.minQuantity / maxQuantity are load-bearing\n * }\n * }\n *\n * Each variant carries ONLY the fields it actually uses. Fields that are\n * always present (id, title, status, schedule, widget config, A/B test\n * metadata) live on the shared `BundleBase` type.\n *\n * Locked for 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 * Product-aware bundle fetch — the headless analogue of the classic Liquid\n * theme block.\n *\n * Classic-theme widget reads `product.metafields[\"$app\"].active_bundle_ids`\n * inside Liquid and renders every active bundle that applies. Headless\n * storefronts get the same behaviour via this helper: pass a product\n * handle, receive `ParsedBundle[]` for every active bundle configured\n * against that product.\n *\n * The caller is responsible for knowing the handle — either because their\n * storefront template already knows it (Hydrogen loader, Next.js param),\n * or because the widget auto-detects from `/products/<handle>` URLs.\n *\n * Non-active bundles (draft / inactive / scheduled / expired) are silently\n * skipped rather than throwing — the calling surface is a list, not a\n * single lookup. Surface errors only for genuine failures (network /\n * GraphQL / invalid schema).\n */\nimport { createStorefrontClient } from \"../storefront-api/client\";\nimport {\n BUNDLES_FOR_PRODUCT_QUERY,\n type BundlesForProductResponse,\n} from \"../storefront-api/queries\";\nimport { parseMetaobjectBundleStrict } from \"./parser\";\nimport { BundleParseError, type ParsedBundle } from \"./types\";\nimport type { MetaobjectField } from \"../storefront-api/types\";\n\nexport interface FetchBundlesForProductOptions {\n shopDomain: string;\n storefrontAccessToken: string;\n productHandle: string;\n /** Forwarded as `Shopify-Storefront-Buyer-IP` — required for SSR. */\n buyerIp?: string;\n signal?: AbortSignal;\n apiVersion?: string;\n}\n\n/**\n * Reasons a bundle is expected-not-renderable (skipped silently, not\n * surfaced as an error). Matches `BundleParseFailReason` minus\n * `not_found` / `invalid_type`, which indicate genuine data problems\n * worth propagating.\n */\nconst SKIPPABLE_REASONS = new Set([\"inactive\", \"not_started\", \"expired\"]);\n\nexport async function fetchBundlesForProduct(\n options: FetchBundlesForProductOptions,\n): Promise<ParsedBundle[]> {\n const client = createStorefrontClient({\n shopDomain: options.shopDomain,\n accessToken: options.storefrontAccessToken,\n buyerIp: options.buyerIp,\n apiVersion: options.apiVersion,\n });\n\n const data = await client.query<BundlesForProductResponse>(\n BUNDLES_FOR_PRODUCT_QUERY,\n { handle: options.productHandle },\n { signal: options.signal },\n );\n\n if (!data.product) {\n // Product not found for this handle. The widget's caller (merchant's\n // storefront) is presumably on a valid product page, so this likely\n // means the handle is wrong or the product is unpublished. Return\n // empty rather than throwing — the widget renders nothing in that\n // case, which is the right UX (bundle section invisible).\n return [];\n }\n\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n\n for (const ref of refs) {\n try {\n // The nested references query resolves products + collections to the\n // same shape BUNDLE_METAOBJECT_QUERY returns, so parser.ts doesn't\n // need a new code path.\n const bundle = parseMetaobjectBundleStrict(\n ref.id,\n ref.fields as MetaobjectField[],\n );\n bundles.push(bundle);\n } catch (err) {\n if (err instanceof BundleParseError && SKIPPABLE_REASONS.has(err.reason)) {\n continue;\n }\n throw err;\n }\n }\n\n return bundles;\n}\n","/**\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 * Merchant-facing consent gate for A/B bucket persistence.\n *\n * Why this exists:\n *\n * On classic-theme Shopify storefronts, `window.Shopify.customerPrivacy`\n * exposes GDPR consent state. We can gate A/B cookie writes on that.\n *\n * On headless storefronts (Hydrogen, custom Next.js, etc.) there is no\n * universal consent primitive — the merchant is the privacy controller,\n * not Shopify. GDPR Art. 6(1)(a) requires opt-in consent for analytics-\n * related cookies, so the SDK must default to deny and accept explicit\n * opt-in from the merchant.\n *\n * Usage:\n *\n * import { setConsent } from \"@lime-bundles/core\";\n *\n * // In the merchant's consent-banner accept handler:\n * setConsent(true);\n *\n * // To revoke (e.g. on consent-banner decline):\n * setConsent(false);\n *\n * The A/B assigner calls `hasConsent()` before writing the bucket cookie.\n * Without consent, the assigner returns a stable \"control\" bucket and\n * never calls `/api/ab-assign` — no cookie, no network fetch, no tracking.\n */\n\nlet consentGranted = false;\n\n/**\n * Merchant signals explicit opt-in (true) or revocation (false) for\n * A/B-related cookie and analytics writes.\n */\nexport function setConsent(allowed: boolean): void {\n consentGranted = allowed;\n}\n\n/**\n * Check if writes are permitted. Returns true when:\n * (a) the merchant has called `setConsent(true)`, OR\n * (b) Shopify's `customerPrivacy.analyticsProcessingAllowed()` returns true\n * (classic-theme / hosted checkout context).\n *\n * Returns false by default (GDPR-safe).\n */\nexport function hasConsent(): boolean {\n if (consentGranted) return true;\n if (typeof window === \"undefined\") return false;\n\n // Type cast — Shopify global isn't typed in headless TS environments.\n const shopifyGlobal = (\n window as unknown as {\n Shopify?: {\n customerPrivacy?: {\n analyticsProcessingAllowed?: () => boolean;\n };\n };\n }\n ).Shopify;\n\n try {\n return shopifyGlobal?.customerPrivacy?.analyticsProcessingAllowed?.() === true;\n } catch {\n return false;\n }\n}\n\n/** @internal Test-only: reset the consent flag between tests. */\nexport function __resetConsent(): void {\n consentGranted = false;\n}\n","/**\n * A/B test variant assignment for headless widgets.\n *\n * Writes a first-party `__Host-_lb_ab_${bundleId}` cookie carrying the\n * deterministic bucket. The `__Host-` prefix is a browser-enforced\n * hardening: requires `Secure`, `Path=/`, no `Domain=` — the cookie is\n * locked to the exact origin that set it.\n *\n * GDPR default: DENY. The assigner returns a stable \"control\" bucket and\n * writes no cookie when consent is absent. Merchants on classic-theme\n * Shopify get consent automatically via `window.Shopify.customerPrivacy`;\n * headless merchants must call `setConsent(true)` from\n * `@lime-bundles/core` in their consent-banner accept handler.\n *\n * Bucket assignment is deterministic (fnv1a hash) so repeated calls with\n * the same `sessionId + testId` always produce the same bucket — a client\n * that revokes consent and re-grants it later still lands on the same\n * variant. Same with the server-side `expectedVariant()` check on\n * `/api/ab-assign`.\n */\nimport { hasConsent } from \"./consent\";\n\nconst SESSION_COOKIE_NAME = \"lb_session\";\nconst SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\nconst AB_COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 days\n\nexport interface ABTestAssignment {\n variant: \"A\" | \"B\";\n testId: string;\n /**\n * Whether this assignment was persisted (cookie + server write) or\n * returned in-memory as a consent-denied control bucket.\n */\n persisted: boolean;\n}\n\nfunction abCookieName(bundleId: string): string {\n // __Host- prefix requires Secure + Path=/ + no Domain attribute.\n // Browser rejects the cookie on write if any of those aren't set, so it\n // doubles as a defense-in-depth check against misconfigured deployments.\n return `__Host-_lb_ab_${bundleId}`;\n}\n\n/**\n * Get or assign an A/B test variant.\n *\n * Reads/creates the `lb_session` cookie, computes the deterministic\n * bucket, and (if consent is granted) writes the `__Host-_lb_ab_${bundleId}`\n * cookie and fires `/api/ab-assign` for server-side persistence.\n */\nexport async function getABTestAssignment(\n appUrl: string,\n shopDomain: string,\n testId: string,\n bundleId: string,\n): Promise<ABTestAssignment | null> {\n if (typeof document === \"undefined\") return null; // SSR guard\n\n // If we already persisted a bucket for this bundle, trust the cookie —\n // switching buckets mid-session skews test results. (Reading the cookie\n // doesn't require consent; a cookie's existence implies prior consent.)\n const existing = readABCookie(bundleId);\n if (existing && existing.testId === testId) {\n return { variant: existing.variant, testId, persisted: true };\n }\n\n // Consent gate FIRST — must not write lb_session or any tracking cookie\n // before consent is granted. All non-consenting users see control\n // (\"A\") without any cookie or network write.\n if (!hasConsent()) {\n return { variant: \"A\", testId, persisted: false };\n }\n\n // Consent granted — safe to create/read the session cookie and compute\n // the deterministic bucket.\n const sessionId = getOrCreateSessionId();\n const variant = fnv1aVariant(sessionId, testId);\n\n writeABCookie(bundleId, testId, variant);\n\n // Fire-and-forget server-side assignment persistence.\n try {\n fetch(`${appUrl}/api/ab-assign`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n shopDomain,\n testId,\n sessionId,\n variant,\n }),\n }).catch(() => {});\n } catch {\n // Silently ignore — persistence is best-effort.\n }\n\n return { variant, testId, persisted: true };\n}\n\nfunction readABCookie(\n bundleId: string,\n): { testId: string; variant: \"A\" | \"B\" } | null {\n if (typeof document === \"undefined\") return null;\n const name = abCookieName(bundleId);\n const prefix = `${name}=`;\n const cookies = document.cookie.split(\";\").map((c) => c.trim());\n // Use substring(prefix.length) rather than split(\"=\")[1] — robust against\n // values that contain \"=\" (e.g. base64 padding if the cookie shape ever\n // changes). CUIDs don't contain \"=\" today but this is defensive.\n const raw = cookies.find((c) => c.startsWith(prefix));\n if (!raw) return null;\n const value = raw.substring(prefix.length);\n\n // Format: `${bucket}|${testId}|${ts}` — see writeABCookie.\n const [variant, testId] = value.split(\"|\");\n if (variant !== \"A\" && variant !== \"B\") return null;\n if (!testId) return null;\n return { testId, variant };\n}\n\nfunction writeABCookie(\n bundleId: string,\n testId: string,\n variant: \"A\" | \"B\",\n): void {\n if (typeof document === \"undefined\") return;\n const name = abCookieName(bundleId);\n const value = `${variant}|${testId}|${Date.now()}`;\n // __Host- prefix requires Secure + Path=/ + no Domain.\n document.cookie =\n `${name}=${value}; Path=/; Max-Age=${AB_COOKIE_MAX_AGE}; ` +\n `SameSite=Lax; Secure`;\n}\n\nfunction getOrCreateSessionId(): string {\n if (typeof document === \"undefined\") return generateUUID();\n\n const cookies = document.cookie.split(\";\").map((c) => c.trim());\n const existing = cookies\n .find((c) => c.startsWith(`${SESSION_COOKIE_NAME}=`))\n ?.split(\"=\")[1];\n\n if (existing) return existing;\n\n const id = generateUUID();\n 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 * ## The two modes\n *\n * Single-bundle (pinned):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * bundle-gid=\"gid://shopify/Metaobject/42\"\n * ></lime-bundle>\n *\n * Product-aware (matches classic Liquid theme block behaviour — one snippet\n * on the product page renders every active bundle configured against that\n * product):\n * <lime-bundle\n * shop-domain=\"my-shop.myshopify.com\"\n * storefront-token=\"shpat_...\"\n * ></lime-bundle>\n *\n * Product resolution cascade (when no `bundle-gid` is set):\n * 1. explicit `product-handle` attribute\n * 2. <meta name=\"shopify:product-handle\" content=\"...\"> on the page\n * 3. /products/<handle> segment of window.location.pathname\n * 4. fallthrough: renders nothing, fires `lime-bundle:error`\n *\n * ## Add-to-cart behaviour\n *\n * Merchants who do nothing get a default: the widget calls Shopify's\n * Storefront Cart API (tokenless — no extra scopes required) and redirects\n * the browser to the returned checkoutUrl. One-click-to-checkout is the\n * right UX for most merchants pasting the widget into Webflow / Wix /\n * Squarespace / static HTML.\n *\n * Merchants with their own cart state (Hydrogen's useCart, a custom cart\n * drawer, etc.) opt out by attaching a listener that calls\n * `event.preventDefault()`:\n *\n * document.querySelector(\"lime-bundle\").addEventListener(\n * \"lime-bundle:add-to-cart\",\n * (ev) => {\n * ev.preventDefault(); // suppress the default redirect\n * myCart.linesAdd(ev.detail.lines);\n * },\n * );\n *\n * The event is always dispatched; only the default action is conditional.\n */\nimport {\n createStorefrontClient,\n BUNDLE_METAOBJECT_QUERY,\n BUNDLES_FOR_PRODUCT_QUERY,\n CART_CREATE_MUTATION,\n CART_LINES_ADD_MUTATION,\n SHOP_CUSTOM_CSS_QUERY,\n parseMetaobjectBundle,\n observeImpression,\n reportImpression,\n reportAddToCart,\n injectCustomCss,\n fetchBundlesForProduct,\n type ParsedBundle,\n type FixedBundleData,\n type VolumeBundleData,\n type MixMatchBundleData,\n type BundleMetaobjectResponse,\n type BundlesForProductResponse,\n type CartCreateResponse,\n type CartLinesAddResponse,\n type ShopCustomCssResponse,\n type CartLineInput,\n type StorefrontClient,\n} from \"@lime-bundles/core\";\nimport { renderFixedBundle } from \"./renderers/fixed\";\nimport { renderMixMatchBundle } from \"./renderers/mix-match\";\nimport { renderVolumeBundle } from \"./renderers/volume\";\nimport { WIDGET_STYLES } from \"./styles/widget-styles\";\n\n/**\n * Per-shop localStorage key for the active cart id. Scoping by shop domain\n * keeps the cart isolated when a single browser visits multiple Lime-\n * Bundles-powered storefronts (rare, but correct).\n */\nconst cartStorageKey = (shopDomain: string) => `lb_cart_id:${shopDomain}`;\n\n/**\n * Resolve the current product handle from the page. Runs the cascade\n * documented on LimeBundleElement and returns null if no source matches.\n */\nfunction resolveProductHandle(explicit: string | null): string | null {\n if (explicit) return explicit.trim() || null;\n\n if (typeof document !== \"undefined\") {\n const meta = document.querySelector<HTMLMetaElement>(\n 'meta[name=\"shopify:product-handle\"]',\n );\n if (meta?.content) return meta.content.trim() || null;\n }\n\n if (typeof window !== \"undefined\") {\n const match = window.location.pathname.match(/\\/products\\/([^/?#]+)/);\n if (match?.[1]) return decodeURIComponent(match[1]);\n }\n\n return null;\n}\n\nexport class LimeBundleElement extends HTMLElement {\n static observedAttributes = [\n \"shop-domain\",\n \"storefront-token\",\n \"bundle-gid\",\n \"product-handle\",\n \"app-url\",\n \"analytics\",\n \"locale\",\n ];\n\n private shadow: ShadowRoot;\n private bundles: ParsedBundle[] = [];\n private abortController: AbortController | null = null;\n private impressionCleanups: Array<() => void> = [];\n\n 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.teardownImpressions();\n }\n\n private teardownImpressions() {\n for (const cleanup of this.impressionCleanups) cleanup();\n this.impressionCleanups = [];\n }\n\n attributeChangedCallback(\n name: string,\n oldValue: string | null,\n newValue: string | null,\n ) {\n if (oldValue === newValue || !this.isConnected) return;\n if (\n name === \"bundle-gid\" ||\n name === \"product-handle\" ||\n name === \"shop-domain\" ||\n name === \"storefront-token\"\n ) {\n if (this.shopDomain && this.storefrontToken) {\n this.fetchBundle();\n }\n }\n }\n\n private get shopDomain(): string {\n return this.getAttribute(\"shop-domain\") ?? \"\";\n }\n\n private get storefrontToken(): string {\n return this.getAttribute(\"storefront-token\") ?? \"\";\n }\n\n private get bundleGid(): string {\n return this.getAttribute(\"bundle-gid\") ?? \"\";\n }\n\n private get productHandleAttr(): string {\n return this.getAttribute(\"product-handle\") ?? \"\";\n }\n\n private get appUrl(): string {\n return this.getAttribute(\"app-url\") ?? \"\";\n }\n\n private get analyticsEnabled(): boolean {\n return this.getAttribute(\"analytics\") !== \"false\";\n }\n\n private async fetchBundle() {\n if (!this.shopDomain || !this.storefrontToken) {\n this.renderError(\n \"Missing required attributes: shop-domain, storefront-token\",\n );\n return;\n }\n\n this.abortController?.abort();\n const controller = new AbortController();\n this.abortController = controller;\n\n this.renderLoading();\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n\n try {\n // Fire bundle query BEFORE CSS so order-sensitive call logs (and any\n // test harness that mocks fetch with sequential mockResolvedValueOnce)\n // see the bundle as call #1, CSS as call #2. Parallelism preserved\n // via Promise.all at the await site below.\n let bundlePromise: Promise<void>;\n let singleBundleMode = false;\n if (this.bundleGid) {\n singleBundleMode = true;\n bundlePromise = this.fetchSingleBundle(client, controller.signal);\n } else {\n const handle = resolveProductHandle(this.productHandleAttr);\n if (!handle) {\n this.teardownImpressions();\n this.renderError(\n \"No bundle-gid or product-handle provided, and the current URL doesn't match /products/<handle>.\",\n );\n return;\n }\n bundlePromise = this.fetchProductBundles(\n client,\n controller.signal,\n handle,\n );\n }\n\n // CSS fetch kicks off AFTER bundle fetch for deterministic call order.\n // Best-effort — widget renders without merchant styling if it fails.\n const cssPromise = client\n .query<ShopCustomCssResponse>(SHOP_CUSTOM_CSS_QUERY, undefined, {\n signal: controller.signal,\n })\n .catch(() => null);\n\n await bundlePromise;\n if (controller.signal.aborted) return;\n\n // Single-bundle mode with an explicit GID that didn't resolve is a\n // genuine error — the merchant pinned a specific bundle and it's\n // missing. Product-handle mode with zero bundles is NOT an error:\n // the product legitimately has no bundles configured; widget stays\n // invisible (mirrors classic-theme Liquid block behaviour).\n if (singleBundleMode && this.bundles.length === 0) {\n this.teardownImpressions();\n this.renderError(\"Bundle not found\");\n return;\n }\n\n const css = await cssPromise;\n if (css?.shop?.metafield?.value) {\n injectCustomCss(this.shopDomain, css.shop.metafield.value);\n }\n\n this.renderBundles();\n } catch (err) {\n if (controller.signal.aborted) return;\n this.bundles = [];\n this.teardownImpressions();\n this.renderError(\n err instanceof Error ? err.message : \"Failed to load bundle\",\n );\n }\n }\n\n private async fetchSingleBundle(\n client: StorefrontClient,\n signal: AbortSignal,\n ): Promise<void> {\n const data = await client.query<BundleMetaobjectResponse>(\n BUNDLE_METAOBJECT_QUERY,\n { id: this.bundleGid },\n { signal },\n );\n if (!data.metaobject) {\n this.bundles = [];\n return;\n }\n const parsed = parseMetaobjectBundle(\n data.metaobject.id,\n data.metaobject.fields,\n );\n this.bundles = parsed ? [parsed] : [];\n }\n\n private async fetchProductBundles(\n client: StorefrontClient,\n signal: AbortSignal,\n productHandle: string,\n ): Promise<void> {\n // fetchBundlesForProduct re-creates its own client; skip that indirection\n // and reuse the one we already built so the request shares the same\n // AbortSignal and header config.\n const data = await client.query<BundlesForProductResponse>(\n BUNDLES_FOR_PRODUCT_QUERY,\n { handle: productHandle },\n { signal },\n );\n if (!data.product) {\n this.bundles = [];\n return;\n }\n const refs = data.product.metafield?.references?.nodes ?? [];\n const bundles: ParsedBundle[] = [];\n for (const ref of refs) {\n const parsed = parseMetaobjectBundle(ref.id, ref.fields);\n if (parsed) bundles.push(parsed);\n }\n this.bundles = bundles;\n }\n\n /**\n * Dispatch add-to-cart with a cancelable event, then — unless a listener\n * called preventDefault — execute the default cart-and-checkout flow.\n *\n * `fire-and-forget` against `reportAddToCart` runs regardless so merchants\n * with BYO cart still get analytics.\n */\n private handleAddToCart = async (\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): Promise<void> => {\n const ev = new CustomEvent(\"lime-bundle:add-to-cart\", {\n detail: { lines },\n bubbles: true,\n composed: true,\n cancelable: true,\n });\n // dispatchEvent returns false if preventDefault() was called on a\n // cancelable event. That's how merchants opt out of the default flow.\n const allowDefault = this.dispatchEvent(ev);\n\n // Analytics fire regardless of which cart path runs.\n this.reportAddToCartEvent(bundle, lines);\n\n if (allowDefault) {\n await this.defaultAddToCart(lines);\n }\n };\n\n /**\n * Default cart flow: Shopify's Storefront Cart API is tokenless, so we\n * don't need any additional scopes. Persist the cart ID in localStorage\n * so subsequent adds on the same browser session join the existing cart\n * instead of creating a new one every click.\n */\n private async defaultAddToCart(lines: CartLineInput[]): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const client = createStorefrontClient({\n shopDomain: this.shopDomain,\n accessToken: this.storefrontToken,\n });\n const storage = window.localStorage;\n const key = cartStorageKey(this.shopDomain);\n const existingCartId = storage?.getItem(key) ?? null;\n\n try {\n let checkoutUrl: string | null = null;\n\n if (existingCartId) {\n const res = await client.query<CartLinesAddResponse>(\n CART_LINES_ADD_MUTATION,\n { cartId: existingCartId, lines },\n );\n const payload = res.cartLinesAdd;\n if (payload?.userErrors?.length) {\n // Cart GID expired or was merged on Shopify's side — fall back to\n // cartCreate below. This happens after ~10 days of inactivity.\n storage?.removeItem(key);\n } else if (payload?.cart) {\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (!checkoutUrl) {\n const res = await client.query<CartCreateResponse>(\n CART_CREATE_MUTATION,\n { input: { lines } },\n );\n const payload = res.cartCreate;\n if (payload?.cart) {\n storage?.setItem(key, payload.cart.id);\n checkoutUrl = payload.cart.checkoutUrl;\n }\n }\n\n if (checkoutUrl) {\n window.location.assign(checkoutUrl);\n } else {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: { message: \"Cart creation failed\", code: \"CART_ERROR\" },\n bubbles: true,\n composed: true,\n }),\n );\n }\n } catch (err) {\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:error\", {\n detail: {\n message:\n err instanceof Error ? err.message : \"Cart mutation failed\",\n code: \"CART_ERROR\",\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n }\n\n private reportAddToCartEvent(\n bundle: ParsedBundle,\n lines: CartLineInput[],\n ): void {\n if (!this.analyticsEnabled || !this.appUrl) return;\n\n const quantity = lines.reduce((sum, l) => sum + l.quantity, 0);\n const totalPrice = lines.reduce((sum, line) => {\n const product = bundle.products.find((p) =>\n p.variants.nodes.some((v) => v.id === line.merchandiseId),\n );\n const variant = product?.variants.nodes.find(\n (v) => v.id === line.merchandiseId,\n );\n const price = variant ? parseFloat(variant.price.amount) : 0;\n return sum + price * line.quantity;\n }, 0);\n\n reportAddToCart(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n productId: bundle.products[0]?.id ?? \"\",\n quantity,\n totalPrice: Math.round(totalPrice * 100) / 100,\n },\n );\n }\n\n private renderBundles() {\n this.teardownImpressions();\n this.shadow.innerHTML = \"\";\n\n const style = document.createElement(\"style\");\n style.textContent = WIDGET_STYLES;\n this.shadow.appendChild(style);\n\n // Empty state: the shadow root holds only the <style> tag — nothing\n // visible. Matches Liquid theme UX where a product with no bundles\n // simply shows no block. We still fire `lime-bundle:loaded` below so\n // consumers know the async work completed (important for test\n // synchronisation and for merchants who want to hide a parent\n // placeholder once the widget has decided whether to render).\n\n for (const bundle of this.bundles) {\n const container = document.createElement(\"div\");\n container.className = \"lb-bundle\";\n container.setAttribute(\"role\", \"region\");\n container.setAttribute(\"aria-label\", bundle.title);\n\n const dispatch = (lines: CartLineInput[]) =>\n this.handleAddToCart(bundle, lines);\n\n switch (bundle.bundleType) {\n case \"fixed\":\n renderFixedBundle(container, bundle as FixedBundleData, dispatch);\n break;\n case \"mix_match\":\n renderMixMatchBundle(\n container,\n bundle as MixMatchBundleData,\n dispatch,\n );\n break;\n case \"volume\":\n renderVolumeBundle(container, bundle as VolumeBundleData, dispatch);\n break;\n }\n\n this.shadow.appendChild(container);\n this.setupImpressionFor(bundle, container);\n }\n\n const first = this.bundles[0];\n this.dispatchEvent(\n new CustomEvent(\"lime-bundle:loaded\", {\n detail: {\n bundleCount: this.bundles.length,\n bundleTypes: this.bundles.map((b) => b.bundleType),\n // Legacy fields — meaningful only in single-bundle mode. Preserved\n // for merchants who attached listeners against the pre-1.0 shape.\n bundleType: first?.bundleType,\n title: first?.title,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private setupImpressionFor(bundle: ParsedBundle, element: Element) {\n if (!this.analyticsEnabled || !this.appUrl) return;\n const cleanup = observeImpression(element, () => {\n reportImpression(\n { shopDomain: this.shopDomain, appUrl: this.appUrl },\n {\n bundleGid: bundle.id,\n bundleType: bundle.bundleType,\n },\n );\n });\n this.impressionCleanups.push(cleanup);\n }\n\n private renderLoading() {\n 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\n// Re-export the helper so consumers of the widget package can import it\n// when they want to share the URL-detection logic with their own code.\nexport { resolveProductHandle, fetchBundlesForProduct };\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","BUNDLES_FOR_PRODUCT_QUERY","CART_CREATE_MUTATION","CART_LINES_ADD_MUTATION","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","AB_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","cartStorageKey","shopDomain","resolveProductHandle","explicit","meta","match","LimeBundleElement","cleanup","name","oldValue","newValue","controller","client","createStorefrontClient","bundlePromise","singleBundleMode","handle","cssPromise","SHOP_CUSTOM_CSS_QUERY","css","injectCustomCss","err","signal","data","BUNDLE_METAOBJECT_QUERY","parsed","parseMetaobjectBundle","productHandle","BUNDLES_FOR_PRODUCT_QUERY","refs","bundles","ref","bundle","lines","ev","allowDefault","storage","key","existingCartId","checkoutUrl","payload","CART_LINES_ADD_MUTATION","CART_CREATE_MUTATION","quantity","sum","l","totalPrice","line","variant","p","v","price","reportAddToCart","style","WIDGET_STYLES","container","dispatch","renderFixedBundle","renderMixMatchBundle","renderVolumeBundle","first","b","element","observeImpression","reportImpression","message","LimeBundleElement"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lime-bundles/widget",
3
- "version": "0.2.0",
3
+ "version": "1.0.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",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@lime-bundles/core": "^0.2.0"
48
+ "@lime-bundles/core": "^1.0.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "tsup": "^8.0.0",