@neowhale/storefront 0.2.34 → 0.2.36

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,4 +1,4 @@
1
- var WhaleStorefront = (function (exports, react, jsxRuntime) {
1
+ var WhaleStorefront = (function (exports) {
2
2
  'use strict';
3
3
 
4
4
  var __defProp = Object.defineProperty;
@@ -345,14 +345,65 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
345
345
  };
346
346
  }
347
347
  });
348
- var NUM_PATTERN = /(\$?[\d,]+\.?\d*[+★%]?)/g;
348
+
349
+ // src/shims/react-global.ts
350
+ var R = globalThis.React;
351
+ var {
352
+ createElement,
353
+ createContext,
354
+ forwardRef,
355
+ memo,
356
+ lazy,
357
+ useCallback,
358
+ useContext,
359
+ useEffect,
360
+ useId,
361
+ useImperativeHandle,
362
+ useLayoutEffect,
363
+ useMemo,
364
+ useReducer,
365
+ useRef,
366
+ useState,
367
+ Fragment,
368
+ Children,
369
+ cloneElement,
370
+ isValidElement,
371
+ Suspense,
372
+ startTransition,
373
+ useTransition,
374
+ useDeferredValue,
375
+ useSyncExternalStore,
376
+ useInsertionEffect,
377
+ useDebugValue
378
+ } = R;
379
+
380
+ // src/shims/jsx-runtime-global.ts
381
+ var R2 = globalThis.React;
382
+ var jsx = R2.createElement;
383
+ var jsxs = R2.createElement;
384
+ R2.createElement;
385
+ var Fragment2 = R2.Fragment;
386
+
387
+ // src/react/components/sections/shared.tsx
388
+ function trackClick(tracking, label, url, position) {
389
+ if (!tracking?.gatewayUrl || !tracking?.code) return;
390
+ const body = JSON.stringify({ label, url, position });
391
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
392
+ navigator.sendBeacon(
393
+ `${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
394
+ new Blob([body], { type: "application/json" })
395
+ );
396
+ }
397
+ }
398
+ var NUM_SPLIT = /(\$?[\d,]+\.?\d*[+★%]?)/g;
399
+ var NUM_TEST = /^\$?[\d,]+\.?\d*[+★%]?$/;
349
400
  function easeOutQuart(t) {
350
401
  return 1 - Math.pow(1 - t, 4);
351
402
  }
352
403
  function useCountUp(target, duration, start) {
353
- const [value, setValue] = react.useState(0);
354
- const raf = react.useRef(0);
355
- react.useEffect(() => {
404
+ const [value, setValue] = useState(0);
405
+ const raf = useRef(0);
406
+ useEffect(() => {
356
407
  if (!start) return;
357
408
  const t0 = performance.now();
358
409
  function tick(now) {
@@ -367,9 +418,9 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
367
418
  return value;
368
419
  }
369
420
  function AnimatedNumber({ raw }) {
370
- const ref = react.useRef(null);
371
- const [visible, setVisible] = react.useState(false);
372
- react.useEffect(() => {
421
+ const ref = useRef(null);
422
+ const [visible, setVisible] = useState(false);
423
+ useEffect(() => {
373
424
  const el = ref.current;
374
425
  if (!el || typeof IntersectionObserver === "undefined") {
375
426
  setVisible(true);
@@ -395,160 +446,84 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
395
446
  visible
396
447
  );
397
448
  const display = decimals > 0 ? (count / Math.pow(10, decimals)).toFixed(decimals) : hasCommas ? count.toLocaleString() : String(count);
398
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { ref, children: [
449
+ return /* @__PURE__ */ jsxs("span", { ref, children: [
399
450
  prefix,
400
451
  display,
401
452
  suffix
402
453
  ] });
403
454
  }
404
455
  function AnimatedText({ text }) {
405
- const parts = text.split(NUM_PATTERN);
406
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts.map(
407
- (part, i) => NUM_PATTERN.test(part) ? /* @__PURE__ */ jsxRuntime.jsx(AnimatedNumber, { raw: part }, i) : part
456
+ const parts = text.split(NUM_SPLIT);
457
+ return /* @__PURE__ */ jsx(Fragment2, { children: parts.map(
458
+ (part, i) => NUM_TEST.test(part) ? /* @__PURE__ */ jsx(AnimatedNumber, { raw: part }, i) : part
408
459
  ) });
409
460
  }
410
- function trackClick(tracking, label, url, position) {
411
- if (!tracking?.gatewayUrl || !tracking?.code) return;
412
- const body = JSON.stringify({ label, url, position });
413
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
414
- navigator.sendBeacon(
415
- `${tracking.gatewayUrl}/q/${encodeURIComponent(tracking.code)}/click`,
416
- new Blob([body], { type: "application/json" })
417
- );
418
- }
419
- }
420
- function SectionRenderer({
421
- section,
422
- data,
423
- theme,
424
- tracking,
425
- onEvent
426
- }) {
427
- const [showCOA, setShowCOA] = react.useState(false);
428
- const el = (() => {
429
- switch (section.type) {
430
- case "hero":
431
- return /* @__PURE__ */ jsxRuntime.jsx(HeroSection, { section, theme, tracking, onEvent });
432
- case "text":
433
- return /* @__PURE__ */ jsxRuntime.jsx(TextSection, { section, theme });
434
- case "image":
435
- return /* @__PURE__ */ jsxRuntime.jsx(ImageSection, { section, theme });
436
- case "video":
437
- return /* @__PURE__ */ jsxRuntime.jsx(VideoSection, { section, theme });
438
- case "gallery":
439
- return /* @__PURE__ */ jsxRuntime.jsx(GallerySection, { section, theme });
440
- case "cta":
441
- return /* @__PURE__ */ jsxRuntime.jsx(CTASection, { section, theme, tracking, onEvent });
442
- case "stats":
443
- return /* @__PURE__ */ jsxRuntime.jsx(StatsSection, { section, theme });
444
- case "product_card":
445
- return /* @__PURE__ */ jsxRuntime.jsx(ProductCardSection, { section, data, theme, tracking });
446
- case "coa_viewer":
447
- return /* @__PURE__ */ jsxRuntime.jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
448
- case "social_links":
449
- return /* @__PURE__ */ jsxRuntime.jsx(SocialLinksSection, { section, theme });
450
- case "lead_capture":
451
- return /* @__PURE__ */ jsxRuntime.jsx(LeadCaptureSection, { section, data, theme, onEvent });
452
- case "divider":
453
- return /* @__PURE__ */ jsxRuntime.jsx(DividerSection, { theme });
454
- default:
455
- return null;
456
- }
457
- })();
458
- const sectionRef = react.useRef(null);
459
- react.useEffect(() => {
460
- const el2 = sectionRef.current;
461
- if (!el2 || typeof IntersectionObserver === "undefined") return;
462
- const obs = new IntersectionObserver(
463
- ([entry]) => {
464
- if (entry.isIntersecting) {
465
- onEvent?.("section_view", {
466
- section_id: section.id,
467
- section_type: section.type
468
- });
469
- obs.disconnect();
470
- }
471
- },
472
- { threshold: 0.5 }
473
- );
474
- obs.observe(el2);
475
- return () => obs.disconnect();
476
- }, [section.id, section.type, onEvent]);
477
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: sectionRef, "data-section-id": section.id, "data-section-type": section.type, children: [
478
- el,
479
- showCOA && data?.coa && /* @__PURE__ */ jsxRuntime.jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
480
- ] });
481
- }
461
+
462
+ // src/react/components/sections/content-sections.tsx
482
463
  function HeroSection({ section, theme, tracking, onEvent }) {
483
464
  const { title, subtitle, background_image, cta_text, cta_url } = section.content;
484
- return /* @__PURE__ */ jsxRuntime.jsxs(
485
- "div",
486
- {
487
- style: {
488
- position: "relative",
489
- minHeight: "60vh",
490
- display: "flex",
491
- flexDirection: "column",
492
- justifyContent: "center",
493
- alignItems: "center",
494
- textAlign: "center",
495
- padding: "3rem 1.5rem",
496
- backgroundImage: background_image ? `url(${background_image})` : void 0,
497
- backgroundSize: "cover",
498
- backgroundPosition: "center"
499
- },
500
- children: [
501
- background_image && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)" } }),
502
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", zIndex: 1, maxWidth: 640 }, children: [
503
- title && /* @__PURE__ */ jsxRuntime.jsx("h1", { style: {
504
- fontSize: "clamp(2rem, 8vw, 3rem)",
505
- fontWeight: 300,
506
- fontFamily: theme.fontDisplay || "inherit",
507
- margin: "0 0 1rem",
508
- lineHeight: 1.15,
509
- letterSpacing: "-0.02em",
510
- color: theme.fg
511
- }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: title }) }),
512
- subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
465
+ return /* @__PURE__ */ jsxs("div", { style: {
466
+ position: "relative",
467
+ minHeight: "60vh",
468
+ display: "flex",
469
+ flexDirection: "column",
470
+ justifyContent: "center",
471
+ alignItems: "center",
472
+ textAlign: "center",
473
+ padding: "3rem 1.5rem",
474
+ backgroundImage: background_image ? `url(${background_image})` : void 0,
475
+ backgroundSize: "cover",
476
+ backgroundPosition: "center"
477
+ }, children: [
478
+ background_image && /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)" } }),
479
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", zIndex: 1, maxWidth: 640 }, children: [
480
+ title && /* @__PURE__ */ jsx("h1", { style: {
481
+ fontSize: "clamp(2rem, 8vw, 3rem)",
482
+ fontWeight: 300,
483
+ fontFamily: theme.fontDisplay || "inherit",
484
+ margin: "0 0 1rem",
485
+ lineHeight: 1.15,
486
+ letterSpacing: "-0.02em",
487
+ color: theme.fg
488
+ }, children: /* @__PURE__ */ jsx(AnimatedText, { text: title }) }),
489
+ subtitle && /* @__PURE__ */ jsx("p", { style: {
490
+ fontSize: "0.85rem",
491
+ color: theme.accent,
492
+ margin: "0 0 2rem",
493
+ lineHeight: 1.6,
494
+ textTransform: "uppercase",
495
+ letterSpacing: "0.15em"
496
+ }, children: subtitle }),
497
+ cta_text && cta_url && /* @__PURE__ */ jsx(
498
+ "a",
499
+ {
500
+ href: cta_url,
501
+ onClick: () => {
502
+ trackClick(tracking, cta_text, cta_url);
503
+ onEvent?.("cta_click", { label: cta_text, url: cta_url });
504
+ },
505
+ style: {
506
+ display: "inline-block",
507
+ padding: "0.875rem 2rem",
508
+ background: theme.fg,
509
+ color: theme.bg,
510
+ textDecoration: "none",
513
511
  fontSize: "0.85rem",
514
- color: theme.accent,
515
- margin: "0 0 2rem",
516
- lineHeight: 1.6,
517
- textTransform: "uppercase",
518
- letterSpacing: "0.15em"
519
- }, children: subtitle }),
520
- cta_text && cta_url && /* @__PURE__ */ jsxRuntime.jsx(
521
- "a",
522
- {
523
- href: cta_url,
524
- onClick: () => {
525
- trackClick(tracking, cta_text, cta_url);
526
- onEvent?.("cta_click", { label: cta_text, url: cta_url });
527
- },
528
- style: {
529
- display: "inline-block",
530
- padding: "0.875rem 2rem",
531
- background: theme.fg,
532
- color: theme.bg,
533
- textDecoration: "none",
534
- fontSize: "0.85rem",
535
- fontWeight: 500,
536
- letterSpacing: "0.08em",
537
- textTransform: "uppercase"
538
- },
539
- children: cta_text
540
- }
541
- )
542
- ] })
543
- ]
544
- }
545
- );
512
+ fontWeight: 500,
513
+ letterSpacing: "0.08em",
514
+ textTransform: "uppercase"
515
+ },
516
+ children: cta_text
517
+ }
518
+ )
519
+ ] })
520
+ ] });
546
521
  }
547
522
  function TextSection({ section, theme }) {
548
523
  const { heading, body } = section.content;
549
524
  const align = section.config?.align || "left";
550
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 640, margin: "0 auto", textAlign: align }, children: [
551
- heading && /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
525
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 640, margin: "0 auto", textAlign: align }, children: [
526
+ heading && /* @__PURE__ */ jsx("h2", { style: {
552
527
  fontSize: 11,
553
528
  fontWeight: 500,
554
529
  textTransform: "uppercase",
@@ -556,65 +531,62 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
556
531
  color: `${theme.fg}40`,
557
532
  margin: "0 0 0.75rem"
558
533
  }, children: heading }),
559
- body && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { color: `${theme.fg}99`, lineHeight: 1.7, fontSize: "0.9rem", fontWeight: 300, whiteSpace: "pre-wrap" }, children: body })
534
+ body && /* @__PURE__ */ jsx("div", { style: { color: `${theme.fg}99`, lineHeight: 1.7, fontSize: "0.9rem", fontWeight: 300, whiteSpace: "pre-wrap" }, children: body })
560
535
  ] });
561
536
  }
562
537
  function ImageSection({ section, theme }) {
563
538
  const { url, alt, caption } = section.content;
564
539
  const contained = section.config?.contained !== false;
565
540
  if (!url) return null;
566
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: contained ? "1.5rem" : 0, maxWidth: contained ? 640 : void 0, margin: contained ? "0 auto" : void 0 }, children: [
567
- /* @__PURE__ */ jsxRuntime.jsx(
568
- "img",
569
- {
570
- src: url,
571
- alt: alt || "",
572
- style: { width: "100%", display: "block", objectFit: "cover" }
573
- }
574
- ),
575
- caption && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: theme.muted, textAlign: "center", marginTop: "0.75rem" }, children: caption })
541
+ return /* @__PURE__ */ jsxs("div", { style: { padding: contained ? "1.5rem" : 0, maxWidth: contained ? 640 : void 0, margin: contained ? "0 auto" : void 0 }, children: [
542
+ /* @__PURE__ */ jsx("img", { src: url, alt: alt || "", style: { width: "100%", display: "block", objectFit: "cover" } }),
543
+ caption && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8rem", color: theme.muted, textAlign: "center", marginTop: "0.75rem" }, children: caption })
576
544
  ] });
577
545
  }
578
546
  function VideoSection({ section, theme }) {
579
547
  const { url, poster } = section.content;
580
548
  if (!url) return null;
581
549
  const isEmbed = url.includes("youtube") || url.includes("youtu.be") || url.includes("vimeo");
582
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: isEmbed ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative", paddingBottom: "56.25%", height: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(
583
- "iframe",
584
- {
585
- src: toEmbedUrl(url),
586
- style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", border: "none" },
587
- allow: "autoplay; fullscreen",
588
- title: "Video"
589
- }
590
- ) }) : /* @__PURE__ */ jsxRuntime.jsx(
591
- "video",
592
- {
593
- src: url,
594
- poster,
595
- controls: true,
596
- style: { width: "100%", display: "block", background: theme.surface }
597
- }
598
- ) });
550
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: isEmbed ? /* @__PURE__ */ jsx("div", { style: { position: "relative", paddingBottom: "56.25%", height: 0 }, children: /* @__PURE__ */ jsx("iframe", { src: toEmbedUrl(url), style: { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", border: "none" }, allow: "autoplay; fullscreen", title: "Video" }) }) : /* @__PURE__ */ jsx("video", { src: url, poster, controls: true, style: { width: "100%", display: "block", background: theme.surface } }) });
599
551
  }
600
552
  function GallerySection({ section, theme }) {
601
553
  const { images } = section.content;
602
554
  const columns = section.config?.columns || 3;
603
555
  if (!images || images.length === 0) return null;
604
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 800, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: "0.5rem" }, children: images.map((img, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { style: { aspectRatio: "1", overflow: "hidden", background: theme.surface }, children: /* @__PURE__ */ jsxRuntime.jsx(
605
- "img",
556
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 800, margin: "0 auto" }, children: /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: "0.5rem" }, children: images.map((img, i) => /* @__PURE__ */ jsx("div", { style: { aspectRatio: "1", overflow: "hidden", background: theme.surface }, children: /* @__PURE__ */ jsx("img", { src: img.url, alt: img.alt || "", style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }, i)) }) });
557
+ }
558
+ function SocialLinksSection({ section, theme }) {
559
+ const { links } = section.content;
560
+ if (!links || links.length === 0) return null;
561
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center", gap: "1.5rem", flexWrap: "wrap" }, children: links.map((link, i) => /* @__PURE__ */ jsx(
562
+ "a",
606
563
  {
607
- src: img.url,
608
- alt: img.alt || "",
609
- style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
610
- }
611
- ) }, i)) }) });
564
+ href: link.url,
565
+ target: "_blank",
566
+ rel: "noopener noreferrer",
567
+ style: { color: theme.muted, textDecoration: "none", fontSize: "0.85rem", fontWeight: 500, textTransform: "capitalize", letterSpacing: "0.03em" },
568
+ children: link.platform
569
+ },
570
+ i
571
+ )) });
572
+ }
573
+ function DividerSection({ theme }) {
574
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1rem 1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } }) });
612
575
  }
576
+ function toEmbedUrl(url) {
577
+ const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
578
+ if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
579
+ const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
580
+ if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
581
+ return url;
582
+ }
583
+
584
+ // src/react/components/sections/interactive-sections.tsx
613
585
  function CTASection({ section, theme, tracking, onEvent }) {
614
586
  const { title, subtitle, buttons } = section.content;
615
587
  if (!buttons || buttons.length === 0) return null;
616
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
617
- title && /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
588
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "2rem 1.5rem", maxWidth: 480, margin: "0 auto", display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
589
+ title && /* @__PURE__ */ jsx("h2", { style: {
618
590
  fontSize: "clamp(1.25rem, 4vw, 1.5rem)",
619
591
  fontWeight: 300,
620
592
  fontFamily: theme.fontDisplay || "inherit",
@@ -624,7 +596,7 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
624
596
  color: theme.fg,
625
597
  textAlign: "center"
626
598
  }, children: title }),
627
- subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
599
+ subtitle && /* @__PURE__ */ jsx("p", { style: {
628
600
  fontSize: "0.8rem",
629
601
  color: theme.accent,
630
602
  margin: "0 0 0.75rem",
@@ -635,7 +607,7 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
635
607
  }, children: subtitle }),
636
608
  buttons.map((btn, i) => {
637
609
  const isPrimary = btn.style !== "outline";
638
- return /* @__PURE__ */ jsxRuntime.jsx(
610
+ return /* @__PURE__ */ jsx(
639
611
  "a",
640
612
  {
641
613
  href: btn.url,
@@ -670,42 +642,28 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
670
642
  const layout = section.config?.layout;
671
643
  if (!stats || stats.length === 0) return null;
672
644
  if (layout === "list") {
673
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: stats.map((stat, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
674
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
675
- display: "flex",
676
- justifyContent: "space-between",
677
- alignItems: "baseline",
678
- padding: "0.625rem 0"
679
- }, children: [
680
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
681
- fontSize: 12,
682
- textTransform: "uppercase",
683
- letterSpacing: "0.15em",
684
- color: `${theme.fg}66`
685
- }, children: stat.label }),
686
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 14, fontWeight: 300, color: `${theme.fg}CC` }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: stat.value }) })
645
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: stats.map((stat, i) => /* @__PURE__ */ jsxs("div", { children: [
646
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "0.625rem 0" }, children: [
647
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 12, textTransform: "uppercase", letterSpacing: "0.15em", color: `${theme.fg}66` }, children: stat.label }),
648
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 14, fontWeight: 300, color: `${theme.fg}CC` }, children: /* @__PURE__ */ jsx(AnimatedText, { text: stat.value }) })
687
649
  ] }),
688
- i < stats.length - 1 && /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } })
650
+ i < stats.length - 1 && /* @__PURE__ */ jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } })
689
651
  ] }, i)) });
690
652
  }
691
653
  const columns = Math.min(stats.length, 4);
692
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
693
- display: "grid",
694
- gridTemplateColumns: `repeat(${columns}, 1fr)`,
695
- border: `1px solid ${theme.fg}0F`
696
- }, children: stats.map((stat, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
654
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsx("div", { style: { display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, border: `1px solid ${theme.fg}0F` }, children: stats.map((stat, i) => /* @__PURE__ */ jsxs("div", { style: {
697
655
  padding: "1.25rem 0.5rem",
698
656
  textAlign: "center",
699
657
  borderRight: i < stats.length - 1 ? `1px solid ${theme.fg}0F` : void 0
700
658
  }, children: [
701
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
659
+ /* @__PURE__ */ jsx("div", { style: {
702
660
  fontFamily: theme.fontDisplay || "inherit",
703
661
  fontSize: "clamp(1.5rem, 5vw, 2rem)",
704
662
  fontWeight: 300,
705
663
  lineHeight: 1,
706
664
  color: theme.fg
707
- }, children: /* @__PURE__ */ jsxRuntime.jsx(AnimatedText, { text: stat.value }) }),
708
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
665
+ }, children: /* @__PURE__ */ jsx(AnimatedText, { text: stat.value }) }),
666
+ /* @__PURE__ */ jsx("div", { style: {
709
667
  fontSize: 11,
710
668
  fontWeight: 500,
711
669
  textTransform: "uppercase",
@@ -722,12 +680,12 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
722
680
  const description = c.description || product?.description || "";
723
681
  const imageUrl = c.image_url || product?.featured_image || null;
724
682
  const url = c.url || null;
725
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { background: theme.surface, overflow: "hidden" }, children: [
726
- imageUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: "100%", aspectRatio: "1", overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: imageUrl, alt: name, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }),
727
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "1.25rem" }, children: [
728
- /* @__PURE__ */ jsxRuntime.jsx("h3", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 0.5rem", color: theme.fg }, children: name }),
729
- description && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.9rem", color: theme.muted, margin: "0 0 1rem", lineHeight: 1.5 }, children: description }),
730
- url && /* @__PURE__ */ jsxRuntime.jsx(
683
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxs("div", { style: { background: theme.surface, overflow: "hidden" }, children: [
684
+ imageUrl && /* @__PURE__ */ jsx("div", { style: { width: "100%", aspectRatio: "1", overflow: "hidden" }, children: /* @__PURE__ */ jsx("img", { src: imageUrl, alt: name, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }),
685
+ /* @__PURE__ */ jsxs("div", { style: { padding: "1.25rem" }, children: [
686
+ /* @__PURE__ */ jsx("h3", { style: { fontSize: "1.25rem", fontWeight: 600, margin: "0 0 0.5rem", color: theme.fg }, children: name }),
687
+ description && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.9rem", color: theme.muted, margin: "0 0 1rem", lineHeight: 1.5 }, children: description }),
688
+ url && /* @__PURE__ */ jsx(
731
689
  "a",
732
690
  {
733
691
  href: url,
@@ -752,13 +710,7 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
752
710
  ] })
753
711
  ] }) });
754
712
  }
755
- function COAViewerSection({
756
- section,
757
- data,
758
- theme,
759
- onShowCOA,
760
- tracking
761
- }) {
713
+ function COAViewerSection({ section, data, theme, onShowCOA, tracking }) {
762
714
  const coa = data?.coa;
763
715
  const c = section.content;
764
716
  if (!coa) return null;
@@ -780,7 +732,7 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
780
732
  };
781
733
  const buttonLabel = c.button_text || "View Lab Results";
782
734
  if (coa.viewer_url) {
783
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx(
735
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx(
784
736
  "a",
785
737
  {
786
738
  href: coa.viewer_url,
@@ -792,28 +744,56 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
792
744
  }
793
745
  ) });
794
746
  }
795
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => {
747
+ return /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", maxWidth: 480, margin: "0 auto" }, children: /* @__PURE__ */ jsx("button", { onClick: () => {
796
748
  trackClick(tracking, buttonLabel, coa.url);
797
749
  onShowCOA();
798
750
  }, style: buttonStyle, children: buttonLabel }) });
799
751
  }
752
+ function COAModal({ coa, theme, onClose }) {
753
+ return /* @__PURE__ */ jsxs("div", { style: { position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,0.95)", display: "flex", flexDirection: "column" }, children: [
754
+ /* @__PURE__ */ jsxs("div", { style: {
755
+ display: "flex",
756
+ justifyContent: "space-between",
757
+ alignItems: "center",
758
+ padding: "0.75rem 1rem",
759
+ borderBottom: `1px solid ${theme.fg}10`
760
+ }, children: [
761
+ /* @__PURE__ */ jsx("span", { style: { color: "#fff", fontWeight: 500, fontSize: "0.85rem" }, children: coa.document_name || "Lab Results" }),
762
+ /* @__PURE__ */ jsx("button", { onClick: onClose, style: {
763
+ background: `${theme.fg}10`,
764
+ border: "none",
765
+ color: "#fff",
766
+ fontSize: "0.85rem",
767
+ cursor: "pointer",
768
+ padding: "0.375rem 0.75rem"
769
+ }, children: "Close" })
770
+ ] }),
771
+ /* @__PURE__ */ jsx("iframe", { src: coa.url, style: { flex: 1, border: "none", background: "#fff" }, title: "Lab Results" })
772
+ ] });
773
+ }
774
+
775
+ // src/react/components/sections/lead-capture-section.tsx
800
776
  function LeadCaptureSection({ section, data, theme, onEvent }) {
801
777
  const c = section.content;
802
- const [firstName, setFirstName] = react.useState("");
803
- const [email, setEmail] = react.useState("");
804
- const [newsletterOptIn, setNewsletterOptIn] = react.useState(false);
805
- const [status, setStatus] = react.useState("idle");
806
- const [errorMsg, setErrorMsg] = react.useState("");
778
+ const [firstName, setFirstName] = useState("");
779
+ const [email, setEmail] = useState("");
780
+ const [newsletterOptIn, setNewsletterOptIn] = useState(false);
781
+ const [status, setStatus] = useState("idle");
782
+ const [errorMsg, setErrorMsg] = useState("");
807
783
  const gatewayUrl = c.gateway_url || data.gatewayUrl || "https://whale-gateway.fly.dev";
808
784
  const storeId = c.store_id || data.store?.id;
809
785
  const slug = c.landing_page_slug || data.landing_page?.slug;
786
+ const heading = c.heading || "get 10% off your first visit.";
787
+ const subtitle = c.subtitle || "drop your email and we will send you the code.";
788
+ const buttonText = c.button_text || "Claim My Discount";
789
+ const successHeading = c.success_heading || "You\u2019re in!";
790
+ const successMessage = c.success_message || "Check your inbox for the discount code.";
810
791
  async function handleSubmit(e) {
811
792
  e.preventDefault();
812
793
  if (!email || !storeId) return;
813
794
  setStatus("loading");
814
795
  setErrorMsg("");
815
796
  const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
816
- const analyticsData = data.analyticsContext;
817
797
  try {
818
798
  const res = await fetch(`${gatewayUrl}/v1/stores/${storeId}/storefront/leads`, {
819
799
  method: "POST",
@@ -829,8 +809,8 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
829
809
  if (newsletterOptIn) t.push(c.newsletter_tag || "newsletter-subscriber");
830
810
  return t.length > 0 ? t : void 0;
831
811
  })(),
832
- visitor_id: analyticsData?.visitorId || void 0,
833
- session_id: analyticsData?.sessionId || void 0,
812
+ visitor_id: data.analyticsContext?.visitorId || void 0,
813
+ session_id: data.analyticsContext?.sessionId || void 0,
834
814
  utm_source: urlParams?.get("utm_source") || void 0,
835
815
  utm_medium: urlParams?.get("utm_medium") || void 0,
836
816
  utm_campaign: urlParams?.get("utm_campaign") || void 0,
@@ -848,11 +828,6 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
848
828
  setStatus("error");
849
829
  }
850
830
  }
851
- const heading = c.heading || "get 10% off your first visit.";
852
- const subtitle = c.subtitle || "drop your email and we will send you the code.";
853
- const buttonText = c.button_text || "Claim My Discount";
854
- const successHeading = c.success_heading || "You\u2019re in!";
855
- const successMessage = c.success_message || "Check your inbox for the discount code.";
856
831
  const inputStyle = {
857
832
  flex: 1,
858
833
  minWidth: 0,
@@ -867,43 +842,12 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
867
842
  fontFamily: "inherit",
868
843
  transition: "border-color 0.2s"
869
844
  };
870
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
871
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
872
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
873
- background: theme.surface,
874
- border: `1px solid ${theme.fg}12`,
875
- padding: "clamp(2rem, 6vw, 3rem)"
876
- }, children: status === "success" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center" }, children: [
877
- /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
878
- fontSize: "clamp(1.5rem, 5vw, 2rem)",
879
- fontWeight: 300,
880
- fontFamily: theme.fontDisplay || "inherit",
881
- margin: "0 0 0.75rem",
882
- lineHeight: 1.2,
883
- letterSpacing: "-0.02em",
884
- color: theme.fg
885
- }, children: successHeading }),
886
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
887
- fontSize: "0.9rem",
888
- color: `${theme.fg}99`,
889
- margin: "0 0 1.5rem",
890
- lineHeight: 1.6,
891
- fontWeight: 300
892
- }, children: successMessage }),
893
- c.coupon_code && /* @__PURE__ */ jsxRuntime.jsx("div", { style: {
894
- display: "inline-block",
895
- padding: "0.75rem 2rem",
896
- background: `${theme.fg}08`,
897
- border: `1px dashed ${theme.fg}30`,
898
- fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
899
- fontWeight: 500,
900
- fontFamily: "monospace",
901
- letterSpacing: "0.12em",
902
- color: theme.accent
903
- }, children: c.coupon_code })
904
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
905
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
906
- /* @__PURE__ */ jsxRuntime.jsx("h2", { style: {
845
+ if (status === "success") return /* @__PURE__ */ jsx(SuccessState, { theme, heading: successHeading, message: successMessage, couponCode: c.coupon_code });
846
+ return /* @__PURE__ */ jsxs("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: [
847
+ /* @__PURE__ */ jsx("style", { children: `@keyframes lc-spin { to { transform: rotate(360deg) } }` }),
848
+ /* @__PURE__ */ jsxs("div", { style: { background: theme.surface, border: `1px solid ${theme.fg}12`, padding: "clamp(2rem, 6vw, 3rem)" }, children: [
849
+ /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", marginBottom: "clamp(1.5rem, 4vw, 2rem)" }, children: [
850
+ /* @__PURE__ */ jsx("h2", { style: {
907
851
  fontSize: "clamp(1.5rem, 5vw, 2.25rem)",
908
852
  fontWeight: 300,
909
853
  fontFamily: theme.fontDisplay || "inherit",
@@ -912,7 +856,7 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
912
856
  letterSpacing: "-0.02em",
913
857
  color: theme.fg
914
858
  }, children: heading }),
915
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: {
859
+ /* @__PURE__ */ jsx("p", { style: {
916
860
  fontSize: "0.85rem",
917
861
  color: theme.accent,
918
862
  margin: 0,
@@ -921,31 +865,12 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
921
865
  letterSpacing: "0.15em"
922
866
  }, children: subtitle })
923
867
  ] }),
924
- /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
925
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
926
- /* @__PURE__ */ jsxRuntime.jsx(
927
- "input",
928
- {
929
- type: "text",
930
- placeholder: "First name",
931
- value: firstName,
932
- onChange: (e) => setFirstName(e.target.value),
933
- style: inputStyle
934
- }
935
- ),
936
- /* @__PURE__ */ jsxRuntime.jsx(
937
- "input",
938
- {
939
- type: "email",
940
- placeholder: "Email address",
941
- value: email,
942
- onChange: (e) => setEmail(e.target.value),
943
- required: true,
944
- style: inputStyle
945
- }
946
- )
868
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
869
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "0.75rem", flexWrap: "wrap" }, children: [
870
+ /* @__PURE__ */ jsx("input", { type: "text", placeholder: "First name", value: firstName, onChange: (e) => setFirstName(e.target.value), style: inputStyle }),
871
+ /* @__PURE__ */ jsx("input", { type: "email", placeholder: "Email address", value: email, onChange: (e) => setEmail(e.target.value), required: true, style: inputStyle })
947
872
  ] }),
948
- c.show_newsletter_opt_in !== false && /* @__PURE__ */ jsxRuntime.jsxs("label", { style: {
873
+ c.show_newsletter_opt_in !== false && /* @__PURE__ */ jsxs("label", { style: {
949
874
  display: "flex",
950
875
  alignItems: "center",
951
876
  gap: "0.5rem",
@@ -955,126 +880,145 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
955
880
  fontWeight: 300,
956
881
  lineHeight: 1.4
957
882
  }, children: [
958
- /* @__PURE__ */ jsxRuntime.jsx(
883
+ /* @__PURE__ */ jsx(
959
884
  "input",
960
885
  {
961
886
  type: "checkbox",
962
887
  checked: newsletterOptIn,
963
888
  onChange: (e) => setNewsletterOptIn(e.target.checked),
964
- style: {
965
- width: 16,
966
- height: 16,
967
- accentColor: theme.accent,
968
- cursor: "pointer",
969
- flexShrink: 0
970
- }
889
+ style: { width: 16, height: 16, accentColor: theme.accent, cursor: "pointer", flexShrink: 0 }
971
890
  }
972
891
  ),
973
892
  c.newsletter_label || "Also sign me up for the newsletter \u2014 new drops, deals, and company news."
974
893
  ] }),
975
- status === "error" && errorMsg && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
976
- /* @__PURE__ */ jsxRuntime.jsxs(
977
- "button",
978
- {
979
- type: "submit",
980
- disabled: status === "loading",
981
- style: {
982
- width: "100%",
983
- padding: "0.875rem",
984
- background: theme.fg,
985
- color: theme.bg,
986
- border: "none",
987
- fontSize: "0.85rem",
988
- fontWeight: 500,
989
- cursor: status === "loading" ? "wait" : "pointer",
990
- letterSpacing: "0.08em",
991
- textTransform: "uppercase",
992
- fontFamily: "inherit",
993
- display: "flex",
994
- alignItems: "center",
995
- justifyContent: "center",
996
- gap: "0.5rem",
997
- opacity: status === "loading" ? 0.7 : 1,
998
- transition: "opacity 0.2s"
999
- },
1000
- children: [
1001
- status === "loading" && /* @__PURE__ */ jsxRuntime.jsx("span", { style: {
1002
- display: "inline-block",
1003
- width: 16,
1004
- height: 16,
1005
- border: `2px solid ${theme.bg}40`,
1006
- borderTopColor: theme.bg,
1007
- borderRadius: "50%",
1008
- animation: "lc-spin 0.8s linear infinite"
1009
- } }),
1010
- buttonText
1011
- ]
1012
- }
1013
- )
894
+ status === "error" && errorMsg && /* @__PURE__ */ jsx("p", { style: { fontSize: "0.8rem", color: "#e55", margin: 0, fontWeight: 400 }, children: errorMsg }),
895
+ /* @__PURE__ */ jsxs("button", { type: "submit", disabled: status === "loading", style: {
896
+ width: "100%",
897
+ padding: "0.875rem",
898
+ background: theme.fg,
899
+ color: theme.bg,
900
+ border: "none",
901
+ fontSize: "0.85rem",
902
+ fontWeight: 500,
903
+ cursor: status === "loading" ? "wait" : "pointer",
904
+ letterSpacing: "0.08em",
905
+ textTransform: "uppercase",
906
+ fontFamily: "inherit",
907
+ display: "flex",
908
+ alignItems: "center",
909
+ justifyContent: "center",
910
+ gap: "0.5rem",
911
+ opacity: status === "loading" ? 0.7 : 1,
912
+ transition: "opacity 0.2s"
913
+ }, children: [
914
+ status === "loading" && /* @__PURE__ */ jsx("span", { style: {
915
+ display: "inline-block",
916
+ width: 16,
917
+ height: 16,
918
+ border: `2px solid ${theme.bg}40`,
919
+ borderTopColor: theme.bg,
920
+ borderRadius: "50%",
921
+ animation: "lc-spin 0.8s linear infinite"
922
+ } }),
923
+ buttonText
924
+ ] })
1014
925
  ] })
1015
- ] }) })
926
+ ] })
1016
927
  ] });
1017
928
  }
1018
- function SocialLinksSection({ section, theme }) {
1019
- const { links } = section.content;
1020
- if (!links || links.length === 0) return null;
1021
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center", gap: "1.5rem", flexWrap: "wrap" }, children: links.map((link, i) => /* @__PURE__ */ jsxRuntime.jsx(
1022
- "a",
1023
- {
1024
- href: link.url,
1025
- target: "_blank",
1026
- rel: "noopener noreferrer",
1027
- style: {
1028
- color: theme.muted,
1029
- textDecoration: "none",
1030
- fontSize: "0.85rem",
1031
- fontWeight: 500,
1032
- textTransform: "capitalize",
1033
- letterSpacing: "0.03em"
1034
- },
1035
- children: link.platform
1036
- },
1037
- i
1038
- )) });
1039
- }
1040
- function DividerSection({ theme }) {
1041
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1rem 1.5rem", maxWidth: 640, margin: "0 auto" }, children: /* @__PURE__ */ jsxRuntime.jsx("hr", { style: { border: "none", borderTop: `1px solid ${theme.fg}0A`, margin: 0 } }) });
929
+ function SuccessState({ theme, heading, message, couponCode }) {
930
+ return /* @__PURE__ */ jsx("div", { style: { padding: "3.5rem 1.5rem", maxWidth: 560, margin: "0 auto" }, children: /* @__PURE__ */ jsxs("div", { style: { background: theme.surface, border: `1px solid ${theme.fg}12`, padding: "clamp(2rem, 6vw, 3rem)", textAlign: "center" }, children: [
931
+ /* @__PURE__ */ jsx("h2", { style: {
932
+ fontSize: "clamp(1.5rem, 5vw, 2rem)",
933
+ fontWeight: 300,
934
+ fontFamily: theme.fontDisplay || "inherit",
935
+ margin: "0 0 0.75rem",
936
+ lineHeight: 1.2,
937
+ letterSpacing: "-0.02em",
938
+ color: theme.fg
939
+ }, children: heading }),
940
+ /* @__PURE__ */ jsx("p", { style: { fontSize: "0.9rem", color: `${theme.fg}99`, margin: "0 0 1.5rem", lineHeight: 1.6, fontWeight: 300 }, children: message }),
941
+ couponCode && /* @__PURE__ */ jsx("div", { style: {
942
+ display: "inline-block",
943
+ padding: "0.75rem 2rem",
944
+ background: `${theme.fg}08`,
945
+ border: `1px dashed ${theme.fg}30`,
946
+ fontSize: "clamp(1.25rem, 4vw, 1.75rem)",
947
+ fontWeight: 500,
948
+ fontFamily: "monospace",
949
+ letterSpacing: "0.12em",
950
+ color: theme.accent
951
+ }, children: couponCode })
952
+ ] }) });
1042
953
  }
1043
- function COAModal({ coa, theme, onClose }) {
1044
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "fixed", inset: 0, zIndex: 9999, background: "rgba(0,0,0,0.95)", display: "flex", flexDirection: "column" }, children: [
1045
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: {
1046
- display: "flex",
1047
- justifyContent: "space-between",
1048
- alignItems: "center",
1049
- padding: "0.75rem 1rem",
1050
- borderBottom: `1px solid ${theme.fg}10`
1051
- }, children: [
1052
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: "#fff", fontWeight: 500, fontSize: "0.85rem" }, children: coa.document_name || "Lab Results" }),
1053
- /* @__PURE__ */ jsxRuntime.jsx(
1054
- "button",
1055
- {
1056
- onClick: onClose,
1057
- style: {
1058
- background: `${theme.fg}10`,
1059
- border: "none",
1060
- color: "#fff",
1061
- fontSize: "0.85rem",
1062
- cursor: "pointer",
1063
- padding: "0.375rem 0.75rem"
1064
- },
1065
- children: "Close"
954
+
955
+ // src/react/components/section-renderer.tsx
956
+ function SectionRenderer({
957
+ section,
958
+ data,
959
+ theme,
960
+ tracking,
961
+ onEvent
962
+ }) {
963
+ const [showCOA, setShowCOA] = useState(false);
964
+ const el = (() => {
965
+ switch (section.type) {
966
+ case "hero":
967
+ return /* @__PURE__ */ jsx(HeroSection, { section, theme, tracking, onEvent });
968
+ case "text":
969
+ return /* @__PURE__ */ jsx(TextSection, { section, theme });
970
+ case "image":
971
+ return /* @__PURE__ */ jsx(ImageSection, { section, theme });
972
+ case "video":
973
+ return /* @__PURE__ */ jsx(VideoSection, { section, theme });
974
+ case "gallery":
975
+ return /* @__PURE__ */ jsx(GallerySection, { section, theme });
976
+ case "cta":
977
+ return /* @__PURE__ */ jsx(CTASection, { section, theme, tracking, onEvent });
978
+ case "stats":
979
+ return /* @__PURE__ */ jsx(StatsSection, { section, theme });
980
+ case "product_card":
981
+ return /* @__PURE__ */ jsx(ProductCardSection, { section, data, theme, tracking });
982
+ case "coa_viewer":
983
+ return /* @__PURE__ */ jsx(COAViewerSection, { section, data, theme, onShowCOA: () => setShowCOA(true), tracking });
984
+ case "social_links":
985
+ return /* @__PURE__ */ jsx(SocialLinksSection, { section, theme });
986
+ case "lead_capture":
987
+ return /* @__PURE__ */ jsx(LeadCaptureSection, { section, data, theme, onEvent });
988
+ case "divider":
989
+ return /* @__PURE__ */ jsx(DividerSection, { theme });
990
+ default:
991
+ return null;
992
+ }
993
+ })();
994
+ const sectionRef = useRef(null);
995
+ useEffect(() => {
996
+ const el2 = sectionRef.current;
997
+ if (!el2 || typeof IntersectionObserver === "undefined") return;
998
+ const obs = new IntersectionObserver(
999
+ ([entry]) => {
1000
+ if (entry.isIntersecting) {
1001
+ onEvent?.("section_view", { section_id: section.id, section_type: section.type });
1002
+ obs.disconnect();
1066
1003
  }
1067
- )
1068
- ] }),
1069
- /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: coa.url, style: { flex: 1, border: "none", background: "#fff" }, title: "Lab Results" })
1004
+ },
1005
+ { threshold: 0.5 }
1006
+ );
1007
+ obs.observe(el2);
1008
+ return () => obs.disconnect();
1009
+ }, [section.id, section.type, onEvent]);
1010
+ return /* @__PURE__ */ jsxs("div", { ref: sectionRef, "data-section-id": section.id, "data-section-type": section.type, children: [
1011
+ el,
1012
+ showCOA && data?.coa && /* @__PURE__ */ jsx(COAModal, { coa: data.coa, theme, onClose: () => setShowCOA(false) })
1070
1013
  ] });
1071
1014
  }
1072
- function toEmbedUrl(url) {
1073
- const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
1074
- if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
1075
- const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
1076
- if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
1077
- return url;
1015
+
1016
+ // src/react/components/landing-page.tsx
1017
+ function getInlinedData() {
1018
+ if (typeof window !== "undefined" && window.__LANDING_DATA__) {
1019
+ return window.__LANDING_DATA__;
1020
+ }
1021
+ return null;
1078
1022
  }
1079
1023
  function LandingPage({
1080
1024
  slug,
@@ -1086,47 +1030,43 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1086
1030
  analyticsContext,
1087
1031
  enableAnalytics = true
1088
1032
  }) {
1089
- const [state, setState] = react.useState("loading");
1090
- const [data, setData] = react.useState(null);
1091
- const [errorMsg, setErrorMsg] = react.useState("");
1092
- react.useEffect(() => {
1033
+ const inlined = useRef(getInlinedData()).current;
1034
+ const [state, setState] = useState(inlined ? "ready" : "loading");
1035
+ const [data, setData] = useState(inlined);
1036
+ const [errorMsg, setErrorMsg] = useState("");
1037
+ useEffect(() => {
1093
1038
  if (!slug) return;
1094
- let cancelled = false;
1095
- if (typeof window !== "undefined" && window.__LANDING_DATA__) {
1096
- const json = window.__LANDING_DATA__;
1097
- setData(json);
1098
- setState("ready");
1099
- onDataLoaded?.(json);
1039
+ if (data) {
1040
+ onDataLoaded?.(data);
1100
1041
  return;
1101
1042
  }
1043
+ let cancelled = false;
1102
1044
  async function load() {
1103
1045
  try {
1104
1046
  const res = await fetch(`${gatewayUrl}/l/${encodeURIComponent(slug)}`);
1105
- if (!cancelled) {
1106
- if (res.status === 404) {
1107
- setState("not_found");
1108
- return;
1109
- }
1110
- if (res.status === 410) {
1111
- setState("expired");
1112
- return;
1113
- }
1114
- if (!res.ok) {
1115
- const body = await res.json().catch(() => ({}));
1116
- throw new Error(body?.error?.message ?? `Failed to load: ${res.status}`);
1117
- }
1118
- const json = await res.json();
1119
- setData(json);
1120
- setState("ready");
1121
- onDataLoaded?.(json);
1047
+ if (cancelled) return;
1048
+ if (res.status === 404) {
1049
+ setState("not_found");
1050
+ return;
1122
1051
  }
1123
- } catch (err) {
1124
- if (!cancelled) {
1125
- const e = err instanceof Error ? err : new Error(String(err));
1126
- setErrorMsg(e.message);
1127
- setState("error");
1128
- onError?.(e);
1052
+ if (res.status === 410) {
1053
+ setState("expired");
1054
+ return;
1055
+ }
1056
+ if (!res.ok) {
1057
+ const body = await res.json().catch(() => ({}));
1058
+ throw new Error(body?.error?.message ?? `Failed to load: ${res.status}`);
1129
1059
  }
1060
+ const json = await res.json();
1061
+ setData(json);
1062
+ setState("ready");
1063
+ onDataLoaded?.(json);
1064
+ } catch (err) {
1065
+ if (cancelled) return;
1066
+ const e = err instanceof Error ? err : new Error(String(err));
1067
+ setErrorMsg(e.message);
1068
+ setState("error");
1069
+ onError?.(e);
1130
1070
  }
1131
1071
  }
1132
1072
  load();
@@ -1134,12 +1074,22 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1134
1074
  cancelled = true;
1135
1075
  };
1136
1076
  }, [slug, gatewayUrl]);
1137
- if (state === "loading") return /* @__PURE__ */ jsxRuntime.jsx(DefaultLoading, {});
1138
- if (state === "not_found") return /* @__PURE__ */ jsxRuntime.jsx(DefaultNotFound, {});
1139
- if (state === "expired") return /* @__PURE__ */ jsxRuntime.jsx(DefaultExpired, {});
1140
- if (state === "error") return /* @__PURE__ */ jsxRuntime.jsx(DefaultError, { message: errorMsg });
1077
+ if (state === "loading") return /* @__PURE__ */ jsx(StateScreen, { title: "", loading: true });
1078
+ if (state === "not_found") return /* @__PURE__ */ jsx(StateScreen, { title: "Page Not Found", subtitle: "This page does not exist or has been removed." });
1079
+ if (state === "expired") return /* @__PURE__ */ jsx(StateScreen, { title: "Page Expired", subtitle: "This page is no longer active." });
1080
+ if (state === "error") return /* @__PURE__ */ jsx(StateScreen, { title: "Something Went Wrong", subtitle: errorMsg || "Please try again later." });
1141
1081
  if (!data) return null;
1142
- return /* @__PURE__ */ jsxRuntime.jsx(PageLayout, { data, gatewayUrl, renderSection, onEvent, analyticsContext, enableAnalytics });
1082
+ return /* @__PURE__ */ jsx(
1083
+ PageLayout,
1084
+ {
1085
+ data,
1086
+ gatewayUrl,
1087
+ renderSection,
1088
+ onEvent,
1089
+ analyticsContext,
1090
+ enableAnalytics
1091
+ }
1092
+ );
1143
1093
  }
1144
1094
  function isSectionVisible(section, urlParams) {
1145
1095
  const vis = section.config?.visibility;
@@ -1159,11 +1109,11 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1159
1109
  enableAnalytics
1160
1110
  }) {
1161
1111
  const { landing_page: lp, store } = data;
1162
- const trackerRef = react.useRef(null);
1163
- react.useEffect(() => {
1112
+ const trackerRef = useRef(null);
1113
+ useEffect(() => {
1164
1114
  if (!enableAnalytics || typeof window === "undefined") return;
1165
- const analyticsConfig = window.__LANDING_ANALYTICS__;
1166
- if (!analyticsConfig?.slug) return;
1115
+ const config = window.__LANDING_ANALYTICS__;
1116
+ if (!config?.slug) return;
1167
1117
  let visitorId = localStorage.getItem("wt_vid") || "";
1168
1118
  if (!visitorId) {
1169
1119
  visitorId = crypto.randomUUID();
@@ -1175,8 +1125,8 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1175
1125
  sessionStorage.setItem("wt_sid", sessionId);
1176
1126
  }
1177
1127
  Promise.resolve().then(() => (init_tracker(), tracker_exports)).then(({ BehavioralTracker: BehavioralTracker2 }) => {
1178
- const gwUrl = analyticsConfig.gatewayUrl || gatewayUrl;
1179
- const slug = analyticsConfig.slug;
1128
+ const gwUrl = config.gatewayUrl || gatewayUrl;
1129
+ const slug = config.slug;
1180
1130
  const utmParams = new URLSearchParams(window.location.search);
1181
1131
  const tracker = new BehavioralTracker2({
1182
1132
  sessionId,
@@ -1187,65 +1137,44 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1187
1137
  event_data: e.data,
1188
1138
  session_id: batch.session_id,
1189
1139
  visitor_id: batch.visitor_id,
1190
- campaign_id: analyticsConfig.campaignId || utmParams.get("utm_campaign_id") || void 0,
1140
+ campaign_id: config.campaignId || utmParams.get("utm_campaign_id") || void 0,
1191
1141
  utm_source: utmParams.get("utm_source") || void 0,
1192
1142
  utm_medium: utmParams.get("utm_medium") || void 0,
1193
1143
  utm_campaign: utmParams.get("utm_campaign") || void 0
1194
1144
  }));
1195
- const body = JSON.stringify({ events });
1196
- if (typeof navigator !== "undefined" && navigator.sendBeacon) {
1197
- navigator.sendBeacon(
1198
- `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
1199
- new Blob([body], { type: "application/json" })
1200
- );
1201
- } else {
1202
- await fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
1203
- method: "POST",
1204
- headers: { "Content-Type": "application/json" },
1205
- body,
1206
- keepalive: true
1207
- });
1208
- }
1145
+ sendEvents(gwUrl, slug, events);
1209
1146
  }
1210
1147
  });
1211
1148
  tracker.setPageContext(window.location.href, window.location.pathname);
1212
1149
  tracker.start();
1213
1150
  trackerRef.current = tracker;
1214
- const pageViewBody = JSON.stringify({
1215
- events: [{
1216
- event_type: "page_view",
1217
- event_data: { referrer: document.referrer, url: window.location.href },
1218
- session_id: sessionId,
1219
- visitor_id: visitorId,
1220
- campaign_id: analyticsConfig.campaignId || void 0,
1221
- utm_source: utmParams.get("utm_source") || void 0,
1222
- utm_medium: utmParams.get("utm_medium") || void 0,
1223
- utm_campaign: utmParams.get("utm_campaign") || void 0
1224
- }]
1225
- });
1226
- if (navigator.sendBeacon) {
1227
- navigator.sendBeacon(
1228
- `${gwUrl}/l/${encodeURIComponent(slug)}/events`,
1229
- new Blob([pageViewBody], { type: "application/json" })
1230
- );
1231
- } else {
1232
- fetch(`${gwUrl}/l/${encodeURIComponent(slug)}/events`, {
1233
- method: "POST",
1234
- headers: { "Content-Type": "application/json" },
1235
- body: pageViewBody,
1236
- keepalive: true
1237
- }).catch(() => {
1238
- });
1239
- }
1151
+ sendEvents(gwUrl, slug, [{
1152
+ event_type: "page_view",
1153
+ event_data: { referrer: document.referrer, url: window.location.href },
1154
+ session_id: sessionId,
1155
+ visitor_id: visitorId,
1156
+ campaign_id: config.campaignId || void 0
1157
+ }]);
1240
1158
  }).catch(() => {
1241
1159
  });
1242
1160
  return () => {
1243
- if (trackerRef.current) {
1244
- trackerRef.current.stop();
1245
- trackerRef.current = null;
1246
- }
1161
+ trackerRef.current?.stop();
1162
+ trackerRef.current = null;
1247
1163
  };
1248
1164
  }, [enableAnalytics, gatewayUrl]);
1165
+ const handleEvent = useCallback((event, eventData) => {
1166
+ onEvent?.(event, eventData);
1167
+ if (!enableAnalytics || typeof window === "undefined") return;
1168
+ const config = window.__LANDING_ANALYTICS__;
1169
+ if (!config?.slug) return;
1170
+ sendEvents(config.gatewayUrl || gatewayUrl, config.slug, [{
1171
+ event_type: event,
1172
+ event_data: eventData,
1173
+ session_id: sessionStorage.getItem("wt_sid") || void 0,
1174
+ visitor_id: localStorage.getItem("wt_vid") || void 0,
1175
+ campaign_id: config.campaignId || void 0
1176
+ }]);
1177
+ }, [onEvent, enableAnalytics, gatewayUrl]);
1249
1178
  const theme = {
1250
1179
  bg: lp.background_color || store?.theme?.background || "#050505",
1251
1180
  fg: lp.text_color || store?.theme?.foreground || "#fafafa",
@@ -1260,23 +1189,31 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1260
1189
  const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : new URLSearchParams();
1261
1190
  const sorted = [...lp.sections].sort((a, b) => a.order - b.order).filter((s) => isSectionVisible(s, urlParams));
1262
1191
  const sectionData = { ...data, gatewayUrl, landing_page: { slug: lp.slug }, analyticsContext };
1263
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1264
- lp.custom_css && /* @__PURE__ */ jsxRuntime.jsx("style", { children: lp.custom_css }),
1265
- logoUrl && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),
1192
+ return /* @__PURE__ */ jsxs("div", { style: { minHeight: "100dvh", background: theme.bg, color: theme.fg, fontFamily }, children: [
1193
+ lp.custom_css && /* @__PURE__ */ jsx("style", { children: lp.custom_css }),
1194
+ logoUrl && /* @__PURE__ */ jsx("div", { style: { padding: "1.5rem", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsx("img", { src: logoUrl, alt: store?.name || "Store", style: { height: 40, objectFit: "contain" } }) }),
1266
1195
  sorted.map((section) => {
1267
- const defaultRenderer = () => /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme, onEvent }, section.id);
1268
- if (renderSection) {
1269
- return /* @__PURE__ */ jsxRuntime.jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1270
- }
1271
- return /* @__PURE__ */ jsxRuntime.jsx(SectionRenderer, { section, data: sectionData, theme, onEvent }, section.id);
1196
+ const defaultRenderer = () => /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme, onEvent: handleEvent }, section.id);
1197
+ if (renderSection) return /* @__PURE__ */ jsx("div", { children: renderSection(section, defaultRenderer) }, section.id);
1198
+ return /* @__PURE__ */ jsx(SectionRenderer, { section, data: sectionData, theme, onEvent: handleEvent }, section.id);
1272
1199
  }),
1273
- store?.name && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxRuntime.jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1200
+ store?.name && /* @__PURE__ */ jsx("div", { style: { padding: "2rem 1.5rem", borderTop: `1px solid ${theme.surface}`, textAlign: "center" }, children: /* @__PURE__ */ jsxs("p", { style: { fontSize: "0.75rem", color: theme.muted, margin: 0 }, children: [
1274
1201
  "Powered by ",
1275
1202
  store.name
1276
1203
  ] }) })
1277
1204
  ] });
1278
1205
  }
1279
- var containerStyle = {
1206
+ function sendEvents(gwUrl, slug, events) {
1207
+ const body = JSON.stringify({ events });
1208
+ const url = `${gwUrl}/l/${encodeURIComponent(slug)}/events`;
1209
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
1210
+ navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
1211
+ } else {
1212
+ fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true }).catch(() => {
1213
+ });
1214
+ }
1215
+ }
1216
+ var screenStyle = {
1280
1217
  minHeight: "100dvh",
1281
1218
  display: "flex",
1282
1219
  justifyContent: "center",
@@ -1287,28 +1224,14 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1287
1224
  textAlign: "center",
1288
1225
  padding: "2rem"
1289
1226
  };
1290
- function DefaultLoading() {
1291
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1292
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: 32, height: 32, border: "2px solid #333", borderTopColor: "#fafafa", borderRadius: "50%", animation: "spin 0.8s linear infinite", margin: "0 auto 1rem" } }),
1293
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg) } }` })
1294
- ] }) });
1295
- }
1296
- function DefaultNotFound() {
1297
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1298
- /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Not Found" }),
1299
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This page does not exist or has been removed." })
1300
- ] }) });
1301
- }
1302
- function DefaultExpired() {
1303
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1304
- /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Page Expired" }),
1305
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: "This page is no longer active." })
1306
- ] }) });
1307
- }
1308
- function DefaultError({ message }) {
1309
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1310
- /* @__PURE__ */ jsxRuntime.jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: "Something Went Wrong" }),
1311
- /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: "#888" }, children: message || "Please try again later." })
1227
+ function StateScreen({ title, subtitle, loading }) {
1228
+ return /* @__PURE__ */ jsx("div", { style: screenStyle, children: /* @__PURE__ */ jsxs("div", { children: [
1229
+ loading && /* @__PURE__ */ jsxs(Fragment2, { children: [
1230
+ /* @__PURE__ */ jsx("div", { style: { width: 32, height: 32, border: "2px solid #333", borderTopColor: "#fafafa", borderRadius: "50%", animation: "spin 0.8s linear infinite", margin: "0 auto 1rem" } }),
1231
+ /* @__PURE__ */ jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg) } }` })
1232
+ ] }),
1233
+ title && /* @__PURE__ */ jsx("h1", { style: { fontSize: "1.5rem", marginBottom: "0.5rem" }, children: title }),
1234
+ subtitle && /* @__PURE__ */ jsx("p", { style: { color: "#888" }, children: subtitle })
1312
1235
  ] }) });
1313
1236
  }
1314
1237
 
@@ -1320,4 +1243,4 @@ var WhaleStorefront = (function (exports, react, jsxRuntime) {
1320
1243
 
1321
1244
  return exports;
1322
1245
 
1323
- })({}, react, jsxRuntime);
1246
+ })({});