{"version":3,"sources":["../../src/storefront/index.ts","../../src/islands/format.ts","../../src/storefront/render.ts","../../src/storefront/address.ts"],"sourcesContent":["export {\n  renderProductPage,\n  renderProductCard,\n  type RenderProductOptions,\n  type RenderProductCardOptions,\n} from './render';\nexport {\n  validateAddress,\n  noopAutocomplete,\n  type AddressValidationResult,\n  type AddressAutocomplete,\n} from './address';\nexport { formatMoney } from '../islands/format';\n","/**\n * F136 Phase 1 — Currency formatting helpers.\n *\n * Used by both server-side (storefront page renderer) and client-side\n * (cart island totals). Lives in /islands so islands can import without\n * pulling Stripe-SDK code into the browser bundle.\n */\nimport type { CurrencyCode, MoneyAmount } from '../types';\n\nconst FRACTIONLESS = new Set(['JPY', 'KRW', 'VND', 'IDR']);\n\nexport function formatMoney(\n  amount: MoneyAmount,\n  currency: CurrencyCode,\n  locale = 'da-DK',\n): string {\n  const cur = currency.toUpperCase();\n  const minor = FRACTIONLESS.has(cur) ? amount : amount / 100;\n  try {\n    return new Intl.NumberFormat(locale, {\n      style: 'currency',\n      currency: cur,\n      minimumFractionDigits: FRACTIONLESS.has(cur) ? 0 : 2,\n    }).format(minor);\n  } catch {\n    return `${minor.toFixed(2)} ${cur}`;\n  }\n}\n","/**\n * F136 Phase 1 — Server-side product page renderer.\n *\n * Returns an HTML fragment that includes the product card island mount\n * markup. Sites can plug this into their static page templates (Next.js\n * RSC, Astro, plain HTML build) without bringing in a UI framework.\n *\n * The returned HTML is intentionally minimal — we render the data, not\n * the design. Each block has a stable class prefix (`cms-shop-`) so\n * sites style with their own CSS.\n */\nimport { formatMoney } from '../islands/format';\nimport type { CurrencyCode, ShopProduct } from '../types';\n\nexport interface RenderProductOptions {\n  /** Currency to display the primary price in. Default: first key in priceByCurrency. */\n  currency?: CurrencyCode;\n  /** Locale for currency formatting. Default: 'da-DK'. */\n  locale?: string;\n  /** Optional translation map for UI strings. */\n  labels?: Partial<typeof DEFAULT_LABELS>;\n  /** Whether to include the `<script type=\"module\">` mount snippet. Default: false (host typically does this once). */\n  includeMountScript?: boolean;\n  /** Path to the islands JS bundle if includeMountScript is true. */\n  islandsScriptUrl?: string;\n}\n\nconst DEFAULT_LABELS = {\n  addToCart: 'Add to cart',\n  outOfStock: 'Out of stock',\n  inStock: 'In stock',\n  lowStock: 'Only a few left',\n};\n\nfunction escape(s: string): string {\n  return String(s)\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;');\n}\n\nfunction pickCurrency(\n  product: ShopProduct,\n  preferred?: CurrencyCode,\n): CurrencyCode | undefined {\n  const prices = product.data.priceByCurrency ?? {};\n  if (preferred && prices[preferred.toUpperCase()]) {\n    return preferred.toUpperCase();\n  }\n  const first = Object.keys(prices)[0];\n  return first ? first.toUpperCase() : undefined;\n}\n\nfunction stockBadge(product: ShopProduct, labels: typeof DEFAULT_LABELS): string {\n  const qty = product.data.stockQuantity;\n  const threshold = product.data.lowStockThreshold ?? 5;\n  if (typeof qty !== 'number') return '';\n  if (qty <= 0) {\n    return `<span class=\"cms-shop-stock cms-shop-stock-out\">${escape(labels.outOfStock)}</span>`;\n  }\n  if (qty <= threshold) {\n    return `<span class=\"cms-shop-stock cms-shop-stock-low\">${escape(labels.lowStock)}</span>`;\n  }\n  return `<span class=\"cms-shop-stock cms-shop-stock-ok\">${escape(labels.inStock)}</span>`;\n}\n\nexport function renderProductPage(\n  product: ShopProduct,\n  opts: RenderProductOptions = {},\n): string {\n  const labels = { ...DEFAULT_LABELS, ...(opts.labels ?? {}) };\n  const locale = opts.locale ?? 'da-DK';\n  const currency = pickCurrency(product, opts.currency);\n  const data = product.data;\n  const price =\n    currency !== undefined ? data.priceByCurrency?.[currency] : undefined;\n  const compareAt =\n    currency !== undefined\n      ? data.compareAtPriceByCurrency?.[currency]\n      : undefined;\n\n  const images = data.images ?? [];\n  const gallery = images\n    .map(\n      (src, i) =>\n        `<img class=\"cms-shop-product-img${i === 0 ? ' cms-shop-product-img-primary' : ''}\" src=\"${escape(src)}\" alt=\"${escape(data.title)}\">`,\n    )\n    .join('');\n\n  const priceHtml =\n    typeof price === 'number' && currency\n      ? `\n        <div class=\"cms-shop-product-price\">\n          <span class=\"cms-shop-price-current\">${escape(formatMoney(price, currency, locale))}</span>\n          ${\n            typeof compareAt === 'number' && compareAt > price\n              ? `<span class=\"cms-shop-price-compare\">${escape(formatMoney(compareAt, currency, locale))}</span>`\n              : ''\n          }\n        </div>`\n      : '';\n\n  const sellable =\n    currency &&\n    typeof price === 'number' &&\n    price > 0 &&\n    (data.stockQuantity ?? 1) > 0 &&\n    (data.status ?? 'active') === 'active';\n\n  const cardAttrs = [\n    `data-cms-shop=\"product-card\"`,\n    `data-product-id=\"${escape(product.id)}\"`,\n    currency ? `data-currency=\"${escape(currency)}\"` : '',\n  ]\n    .filter(Boolean)\n    .join(' ');\n\n  const buttonHtml = sellable\n    ? `<button type=\"button\" class=\"cms-shop-add\" data-cms-shop-add>${escape(labels.addToCart)}</button>`\n    : `<button type=\"button\" class=\"cms-shop-add\" disabled>${escape(labels.outOfStock)}</button>`;\n\n  const mountScript = opts.includeMountScript\n    ? `<script type=\"module\">\n  import { mountProductCards } from \"${escape(opts.islandsScriptUrl ?? '/_shop/islands.js')}\";\n  mountProductCards();\n</script>`\n    : '';\n\n  return `\n<article class=\"cms-shop-product\" ${cardAttrs}>\n  <div class=\"cms-shop-product-gallery\">${gallery}</div>\n  <div class=\"cms-shop-product-body\">\n    <h1 class=\"cms-shop-product-title\">${escape(data.title)}</h1>\n    ${data.shortDescription ? `<p class=\"cms-shop-product-short\">${escape(data.shortDescription)}</p>` : ''}\n    ${priceHtml}\n    ${stockBadge(product, labels)}\n    ${data.description ? `<div class=\"cms-shop-product-description\">${escape(data.description)}</div>` : ''}\n    ${buttonHtml}\n    <span class=\"cms-shop-product-status\" data-cms-shop-status></span>\n  </div>\n</article>\n${mountScript}\n`.trim();\n}\n\nexport interface RenderProductCardOptions extends RenderProductOptions {\n  /** URL for the product detail page. */\n  href?: string;\n}\n\n/** Compact card for listing pages. */\nexport function renderProductCard(\n  product: ShopProduct,\n  opts: RenderProductCardOptions = {},\n): string {\n  const labels = { ...DEFAULT_LABELS, ...(opts.labels ?? {}) };\n  const locale = opts.locale ?? 'da-DK';\n  const currency = pickCurrency(product, opts.currency);\n  const data = product.data;\n  const price =\n    currency !== undefined ? data.priceByCurrency?.[currency] : undefined;\n  const image = data.cardImageUrl ?? data.images?.[0];\n\n  const titleHtml = opts.href\n    ? `<a class=\"cms-shop-card-title\" href=\"${escape(opts.href)}\">${escape(data.title)}</a>`\n    : `<span class=\"cms-shop-card-title\">${escape(data.title)}</span>`;\n\n  return `\n<article class=\"cms-shop-card\" data-cms-shop=\"product-card\" data-product-id=\"${escape(product.id)}\" ${currency ? `data-currency=\"${escape(currency)}\"` : ''}>\n  ${image ? `<img class=\"cms-shop-card-img\" src=\"${escape(image)}\" alt=\"${escape(data.title)}\">` : ''}\n  ${titleHtml}\n  ${typeof price === 'number' && currency ? `<span class=\"cms-shop-card-price\">${escape(formatMoney(price, currency, locale))}</span>` : ''}\n  <button type=\"button\" class=\"cms-shop-card-add\" data-cms-shop-add>${escape(labels.addToCart)}</button>\n</article>\n`.trim();\n}\n","/**\n * F136 Phase 1 — Address validation (DK-first).\n *\n * Light validation that catches the obvious mistakes without becoming\n * a full address-resolution dependency. Country-specific patterns can\n * be added by later phases (or replaced with DAWA / PostNord lookup).\n */\nimport type { ShopAddress } from '../types';\n\nexport interface AddressValidationResult {\n  valid: boolean;\n  errors: Record<string, string>;\n}\n\nconst POSTAL_PATTERNS: Record<string, RegExp> = {\n  DK: /^\\d{4}$/,\n  SE: /^\\d{3}\\s?\\d{2}$/,\n  NO: /^\\d{4}$/,\n  DE: /^\\d{5}$/,\n  GB: /^[A-Z]{1,2}\\d[A-Z\\d]?\\s?\\d[A-Z]{2}$/i,\n  US: /^\\d{5}(-\\d{4})?$/,\n};\n\nexport function validateAddress(addr: Partial<ShopAddress>): AddressValidationResult {\n  const errors: Record<string, string> = {};\n  if (!addr.name?.trim()) errors['name'] = 'Name is required';\n  if (!addr.line1?.trim()) errors['line1'] = 'Address is required';\n  if (!addr.postalCode?.trim()) errors['postalCode'] = 'Postal code is required';\n  if (!addr.city?.trim()) errors['city'] = 'City is required';\n  if (!addr.country?.trim()) errors['country'] = 'Country is required';\n\n  if (addr.country && addr.postalCode) {\n    const pattern = POSTAL_PATTERNS[addr.country.toUpperCase()];\n    if (pattern && !pattern.test(addr.postalCode.trim())) {\n      errors['postalCode'] =\n        `Invalid postal code for ${addr.country.toUpperCase()}`;\n    }\n  }\n\n  return { valid: Object.keys(errors).length === 0, errors };\n}\n\n/**\n * Address autocomplete extension point. Phase 1 ships a no-op so the\n * import is stable; sites that need real lookup wire in DAWA (DK),\n * PostNord (SE/NO), or Google Places via this interface.\n */\nexport interface AddressAutocomplete {\n  /** Look up suggestions from a partial address string. */\n  suggest(query: string, country?: string): Promise<ShopAddress[]>;\n}\n\nexport const noopAutocomplete: AddressAutocomplete = {\n  async suggest() {\n    return [];\n  },\n};\n"],"mappings":"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","names":[]}