@kotao/storefront 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -437,6 +437,12 @@ function useGlobalThemeTokens() {
437
437
 
438
438
  // ../smart-cart/src/format.ts
439
439
  var formatMoney2 = (m) => {
440
+ if (m.currency === "EUR" && Number.isFinite(m.amount)) {
441
+ const negative = m.amount < 0;
442
+ const abs = Math.round(Math.abs(m.amount));
443
+ const units = Math.floor(abs / 100).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
444
+ return `${negative ? "-" : ""}\u20AC${units},${String(abs % 100).padStart(2, "0")}`;
445
+ }
440
446
  if (m.currency && /^[A-Za-z]{3}$/.test(m.currency)) {
441
447
  try {
442
448
  return new Intl.NumberFormat(void 0, {
@@ -448,6 +454,10 @@ var formatMoney2 = (m) => {
448
454
  }
449
455
  return new Intl.NumberFormat(void 0, { minimumFractionDigits: 2 }).format(m.amount / 100);
450
456
  };
457
+ var formatMoneyCompact = (m) => {
458
+ const formatted = formatMoney2(m);
459
+ return Number.isFinite(m.amount) && Math.round(Math.abs(m.amount)) % 100 === 0 ? formatted.replace(/[.,]00(?=\D*$)/, "") : formatted;
460
+ };
451
461
  var escapeHtml = (s) => String(s ?? "").replace(
452
462
  /[&<>"']/g,
453
463
  (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
@@ -456,7 +466,7 @@ var formatCountdown = (ms) => {
456
466
  const total = Number.isFinite(ms) && ms > 0 ? Math.floor(ms / 1e3) : 0;
457
467
  const minutes = Math.floor(total / 60);
458
468
  const seconds = total % 60;
459
- return `${minutes}:${String(seconds).padStart(2, "0")}`;
469
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
460
470
  };
461
471
 
462
472
  // ../smart-cart/src/widgets/add-on.ts
@@ -478,10 +488,10 @@ function findAddOnLine(state, variantId) {
478
488
  var css = `
479
489
  .ksc-addon { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center;
480
490
  gap: 2px 12px; padding: 14px 18px; border-top: 1px solid var(--ksc-border); font: inherit; color: var(--ksc-text); }
481
- .ksc-addon__icon { grid-row: 1 / span 2; width: 34px; height: 34px; display: flex; align-items: center;
491
+ .ksc-addon__icon { grid-row: 1 / span 2; width: 44px; height: 44px; display: flex; align-items: center;
482
492
  justify-content: center; color: var(--ksc-accent); }
483
- .ksc-addon__icon svg { width: 28px; height: 28px; }
484
- .ksc-addon__img { grid-row: 1 / span 2; width: 34px; height: 34px; object-fit: cover; border-radius: 8px;
493
+ .ksc-addon__icon svg { width: 34px; height: 34px; }
494
+ .ksc-addon__img { grid-row: 1 / span 2; width: 44px; height: 44px; object-fit: cover; border-radius: 8px;
485
495
  background: var(--ksc-border); }
486
496
  .ksc-addon__text { grid-column: 2; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
487
497
  .ksc-addon__title { font-size: .875rem; font-weight: 600; }
@@ -499,7 +509,11 @@ var css = `
499
509
  var SHIELD_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l7 3v5c0 4.6-3 7.8-7 9-4-1.2-7-4.4-7-9V6l7-3z"/><path d="M9 12l2 2 4-4"/></svg>`;
500
510
  var addOn = {
501
511
  key: ADDON_KEY,
502
- zone: "after-items",
512
+ // Footer, not 'after-items': the add-on is a checkout-adjacent offer that must stay pinned above
513
+ // the discount code, NOT scroll away with the upsell carousel. It renders before `discountCode`
514
+ // (WIDGETS registry order) so it sits directly above the code input (#1548 moved the whole
515
+ // 'after-items' zone into the scroll region — only the upsell belongs there).
516
+ zone: "footer",
503
517
  css,
504
518
  render({ config, state, variants }) {
505
519
  const variantId = readVariantId(config);
@@ -545,12 +559,11 @@ function readMessage(config) {
545
559
  return typeof raw === "string" && raw.trim() !== "" ? raw : null;
546
560
  }
547
561
  var css2 = `
548
- .ksc-rt { display: flex; align-items: center; justify-content: center; gap: 6px;
549
- padding: 10px 16px; font: inherit; font-size: 0.85rem; font-weight: 500;
550
- color: color-mix(in oklab, var(--ksc-positive, #0e9f6e) 72%, var(--ksc-text, #111));
551
- background: color-mix(in oklab, var(--ksc-positive, #0e9f6e) 13%, var(--ksc-surface, #fff)); }
552
- .ksc-rt__count { font-variant-numeric: tabular-nums; font-weight: 700;
553
- color: color-mix(in oklab, var(--ksc-positive, #0e9f6e) 85%, var(--ksc-text, #111)); }
562
+ .ksc-rt { display: flex; align-items: center; justify-content: center; gap: 4px;
563
+ padding: 9px 16px; font: inherit; font-size: 0.8125rem; font-weight: 400;
564
+ color: var(--ksc-text, #111);
565
+ background: rgb(215, 254, 218); }
566
+ .ksc-rt__count { font-variant-numeric: tabular-nums; font-weight: 700; }
554
567
  .ksc-rt__icon { font-size: 0.95em; line-height: 1; }
555
568
  @media (prefers-reduced-motion: reduce) { .ksc-rt { transition: none; } }
556
569
  `;
@@ -563,10 +576,10 @@ var reservationTimer = {
563
576
  if (minutes === null) return "";
564
577
  void readMode(config);
565
578
  const seed = formatCountdown(minutes * MS_PER_MINUTE);
566
- const message = readMessage(config) ?? "Your cart is reserved for";
579
+ const message = readMessage(config) ?? "Your cart will expire in";
567
580
  return `<div class="ksc-rt">
568
581
  <span>${escapeHtml(message)}</span>
569
- <span class="ksc-rt__count" data-ksc-countdown data-ksc-end-text="0:00">${seed}</span>
582
+ <span class="ksc-rt__count" data-ksc-countdown data-ksc-end-text="00:00">${seed}</span>
570
583
  <span class="ksc-rt__icon" aria-hidden="true">\u23F0</span>
571
584
  </div>`;
572
585
  }
@@ -587,78 +600,121 @@ function readVariantIds(config) {
587
600
  }
588
601
  return [...new Set(ids)];
589
602
  }
603
+ function readTitle(config) {
604
+ const raw = config.title;
605
+ return typeof raw === "string" && raw.trim() !== "" ? raw : "You might also like";
606
+ }
590
607
  function cartVariantIds(state) {
591
608
  if (state === null) return /* @__PURE__ */ new Set();
592
609
  const ids = /* @__PURE__ */ new Set();
593
610
  for (const line of state.lines) {
594
611
  const tag = line.attributes?.[UPSELL_VARIANT_ATTR];
595
612
  if (typeof tag === "string") ids.add(tag);
613
+ if (line.variantId !== null && line.variantId !== void 0) ids.add(String(line.variantId));
596
614
  }
597
615
  return ids;
598
616
  }
617
+ var ARROW_LEFT_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6"/></svg>`;
618
+ var ARROW_RIGHT_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>`;
599
619
  var css3 = `
600
- .ksc-up { padding: 12px 16px; font: inherit; color: var(--ksc-text); }
601
- .ksc-up__title { margin: 0 0 8px; font-size: 0.8125rem; font-weight: 600;
602
- text-transform: uppercase; letter-spacing: 0.04em; color: color-mix(in oklab, var(--ksc-text) 65%, transparent); }
620
+ .ksc-up { padding: 14px 6px 16px; font: inherit; color: var(--ksc-text); }
621
+ .ksc-up__title { margin: 0 0 10px; font-size: 0.9375rem; font-weight: 700; text-align: center; }
622
+ .ksc-up__row { display: grid; grid-template-columns: 28px minmax(0, 1fr) 28px; align-items: center; gap: 2px; }
623
+ .ksc-up__arrow { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px;
624
+ padding: 0; border: none; background: none; cursor: pointer; color: rgb(52, 64, 84); border-radius: 8px; }
625
+ .ksc-up__arrow:hover { background: var(--ksc-border); }
626
+ .ksc-up__arrow svg { width: 18px; height: 18px; }
603
627
  .ksc-up__track { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory;
604
- -webkit-overflow-scrolling: touch; padding-bottom: 4px; scrollbar-width: thin; }
605
- .ksc-up__card { scroll-snap-align: start; flex: 0 0 132px; display: flex; flex-direction: column;
606
- gap: 6px; border: 1px solid color-mix(in oklab, var(--ksc-text) 12%, transparent);
607
- border-radius: var(--ksc-radius); padding: 8px; background: var(--ksc-surface); }
608
- .ksc-up__img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; border-radius: calc(var(--ksc-radius) - 4px);
609
- background: color-mix(in oklab, var(--ksc-text) 6%, transparent); }
610
- .ksc-up__name { font-size: 0.8125rem; line-height: 1.2; display: -webkit-box; -webkit-line-clamp: 2;
611
- -webkit-box-orient: vertical; overflow: hidden; }
612
- .ksc-up__variant { font-size: 0.75rem; color: color-mix(in oklab, var(--ksc-text) 55%, transparent); }
613
- .ksc-up__price { font-size: 0.8125rem; color: color-mix(in oklab, var(--ksc-text) 70%, transparent); }
614
- .ksc-up__compare { color: color-mix(in oklab, var(--ksc-text) 45%, transparent); margin-right: 4px; }
615
- .ksc-up__add { margin-top: auto; padding: 6px 8px; font: inherit; font-size: 0.8125rem; cursor: pointer;
616
- border: none; border-radius: calc(var(--ksc-radius) - 4px); background: var(--ksc-accent);
617
- color: var(--ksc-accent-text); transition: opacity .15s ease; }
628
+ scroll-behavior: smooth; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
629
+ .ksc-up__track::-webkit-scrollbar { display: none; }
630
+ .ksc-up__card { scroll-snap-align: start; flex: 0 0 100%; display: grid;
631
+ grid-template-columns: 64px minmax(0, 1fr); gap: 4px 12px; align-items: start;
632
+ border-radius: 12px; padding: 12px; background: rgb(242, 244, 247); box-sizing: border-box; }
633
+ .ksc-up__img { grid-row: 1 / span 4; width: 64px; height: 64px; object-fit: cover; border-radius: 8px;
634
+ background: var(--ksc-surface); }
635
+ .ksc-up__name { font-size: 0.9375rem; font-weight: 700; line-height: 1.25; display: -webkit-box;
636
+ -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
637
+ .ksc-up__prices { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; font-size: 0.875rem; }
638
+ .ksc-up__price { font-weight: 700; }
639
+ .ksc-up__compare { color: rgb(152, 162, 179); text-decoration: line-through; }
640
+ .ksc-up__save { font-size: 0.75rem; font-weight: 500; color: rgb(7, 148, 85);
641
+ background: rgb(220, 250, 230); padding: 2px 8px; border-radius: 6px; white-space: nowrap; }
642
+ .ksc-up__variant { display: flex; flex-direction: column; gap: 3px; align-items: flex-start;
643
+ font-size: 0.78rem; color: rgb(152, 162, 179); }
644
+ .ksc-up__select { font: inherit; font-size: 0.8125rem; color: var(--ksc-text); padding: 4px 24px 4px 10px;
645
+ border: 1px solid var(--ksc-border); border-radius: 8px; background-color: var(--ksc-surface); cursor: pointer;
646
+ appearance: none; -webkit-appearance: none;
647
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none' stroke='%23999' stroke-width='1.5'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
648
+ background-repeat: no-repeat; background-position: right 8px center; }
649
+ .ksc-up__add { justify-self: start; margin-top: 6px; padding: 8px 14px; font: inherit; font-size: 0.8125rem;
650
+ font-weight: 600; cursor: pointer; border: none; border-radius: 8px; background: rgb(7, 148, 85);
651
+ color: #fff; transition: opacity .15s ease; }
618
652
  .ksc-up__add:hover { opacity: 0.9; }
619
- .ksc-up__add:focus-visible { outline: 2px solid var(--ksc-accent); outline-offset: 2px; }
653
+ .ksc-up__add:focus-visible { outline: 2px solid rgb(7, 148, 85); outline-offset: 2px; }
620
654
  @media (prefers-reduced-motion: reduce) { .ksc-up__track { scroll-behavior: auto; } .ksc-up__add { transition: none; } }
621
655
  `;
622
- function card(variant, currency) {
656
+ function card(variant, currency, siblings) {
623
657
  const name = escapeHtml(variant.productTitle);
624
658
  const img = variant.imageUrl !== null ? `<img class="ksc-up__img" src="${escapeHtml(variant.imageUrl)}" alt="${escapeHtml(variant.imageAltText ?? "")}" loading="lazy" />` : `<div class="ksc-up__img" role="presentation"></div>`;
625
- const variantTitle = variant.title !== null && variant.title.trim() !== "" && variant.title !== DEFAULT_VARIANT_TITLE ? `<span class="ksc-up__variant">${escapeHtml(variant.title)}</span>` : "";
626
659
  const price = escapeHtml(formatMoney2({ amount: variant.price.amount, currency }));
627
660
  const showCompare = variant.compareAtPrice !== null && variant.compareAtPrice.amount > variant.price.amount;
628
- const priceHtml = showCompare ? `<span class="ksc-up__price"><s class="ksc-up__compare">${escapeHtml(formatMoney2({ amount: variant.compareAtPrice.amount, currency }))}</s>${price}</span>` : `<span class="ksc-up__price">${price}</span>`;
661
+ const compare = showCompare ? `<s class="ksc-up__compare">${escapeHtml(formatMoney2({ amount: variant.compareAtPrice.amount, currency }))}</s>` : "";
662
+ const savePct = showCompare ? Math.round(
663
+ (variant.compareAtPrice.amount - variant.price.amount) / variant.compareAtPrice.amount * 100
664
+ ) : 0;
665
+ const save = savePct > 0 ? `<span class="ksc-up__save">You save ${savePct}%</span>` : "";
666
+ const hasMeaningfulTitle = variant.title !== null && variant.title.trim() !== "" && variant.title !== DEFAULT_VARIANT_TITLE;
667
+ const variantHtml = siblings !== void 0 && siblings.length > 1 ? `<span class="ksc-up__variant"><span>Size:</span><select class="ksc-up__select" data-smart-cart-upsell-variant aria-label="Size">${siblings.map(
668
+ (v) => `<option value="${escapeHtml(v.id)}"${v.id === variant.id ? " selected" : ""}>${escapeHtml(v.title ?? "")}</option>`
669
+ ).join("")}</select></span>` : hasMeaningfulTitle ? `<span class="ksc-up__variant"><span>Size:</span><span>${escapeHtml(variant.title)}</span></span>` : "";
629
670
  return `<div class="ksc-up__card" role="listitem">
630
671
  ${img}
631
672
  <span class="ksc-up__name">${name}</span>
632
- ${variantTitle}
633
- ${priceHtml}
634
- <button type="button" class="ksc-up__add" data-smart-cart-upsell-add data-variant-id="${escapeHtml(variant.id)}" aria-label="Add ${name} to cart">Add</button>
673
+ <span class="ksc-up__prices"><span class="ksc-up__price">${price}</span>${compare}${save}</span>
674
+ ${variantHtml}
675
+ <button type="button" class="ksc-up__add" data-smart-cart-upsell-add data-variant-id="${escapeHtml(variant.id)}" aria-label="Add ${name} to cart">Add to cart</button>
635
676
  </div>`;
636
677
  }
637
678
  var upsellCarousel = {
638
679
  key: UPSELL_KEY,
639
680
  zone: "after-items",
640
681
  css: css3,
641
- render({ config, state, variants }) {
682
+ render({ config, state, variants, productVariants }) {
642
683
  if (variants === void 0 || variants.size === 0) return "";
643
684
  const inCart = cartVariantIds(state);
644
- const cards = readVariantIds(config).filter((id) => !inCart.has(id)).map((id) => variants.get(id)).filter((variant) => variant !== void 0).map((variant) => card(variant, state?.currency ?? variant.price.currency)).join("");
685
+ const cards = readVariantIds(config).filter((id) => !inCart.has(id)).map((id) => variants.get(id)).filter((variant) => variant !== void 0).map(
686
+ (variant) => card(
687
+ variant,
688
+ state?.currency ?? variant.price.currency,
689
+ variant.productHandle !== null ? productVariants?.get(variant.productHandle) : void 0
690
+ )
691
+ ).join("");
645
692
  if (cards === "") return "";
646
693
  return `<section class="ksc-up" aria-label="Recommended add-ons">
647
- <p class="ksc-up__title">You might also like</p>
648
- <div class="ksc-up__track" role="list">${cards}</div>
694
+ <p class="ksc-up__title">${escapeHtml(readTitle(config))}</p>
695
+ <div class="ksc-up__row">
696
+ <button type="button" class="ksc-up__arrow" data-smart-cart-upsell-prev aria-label="Previous item">${ARROW_LEFT_SVG}</button>
697
+ <div class="ksc-up__track" role="list">${cards}</div>
698
+ <button type="button" class="ksc-up__arrow" data-smart-cart-upsell-next aria-label="Next item">${ARROW_RIGHT_SVG}</button>
699
+ </div>
649
700
  </section>`;
650
701
  }
651
702
  };
652
703
 
653
704
  // ../smart-cart/src/widgets/discount-code.ts
654
705
  var css4 = `
655
- .ksc-discount { padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; }
656
- .ksc-discount__row { display: flex; gap: 8px; }
657
- .ksc-discount__input { flex: 1; padding: 8px 10px; border: 1.5px solid color-mix(in oklab, var(--ksc-text) 25%, transparent); border-radius: calc(var(--ksc-radius) / 2); background: none; color: var(--ksc-text); font: inherit; font-size: 0.875rem; outline: none; }
658
- .ksc-discount__input:focus { border-color: var(--ksc-accent); }
659
- .ksc-discount__apply { padding: 8px 14px; border: none; border-radius: calc(var(--ksc-radius) / 2); background: var(--ksc-accent); color: var(--ksc-accent-text); font: inherit; font-size: 0.875rem; cursor: pointer; white-space: nowrap; }
706
+ .ksc-discount { padding: 10px 20px; display: flex; flex-direction: column; gap: 8px; }
707
+ .ksc-discount__row { display: flex; align-items: center; gap: 6px; padding: 5px 5px 5px 14px;
708
+ border: 1px solid rgb(190, 190, 190); border-radius: 10px; background: var(--ksc-surface); }
709
+ .ksc-discount__row:focus-within { border-color: var(--ksc-text); }
710
+ .ksc-discount__input { flex: 1; min-width: 0; padding: 6px 0; border: none; background: none;
711
+ color: var(--ksc-text); font: inherit; font-size: 0.875rem; outline: none; }
712
+ .ksc-discount__apply { padding: 8px 16px; border: none; border-radius: 8px;
713
+ background: rgb(220, 250, 230); color: var(--ksc-text); font: inherit; font-size: 0.875rem;
714
+ font-weight: 500; cursor: pointer; white-space: nowrap; }
715
+ .ksc-discount__apply:hover { filter: brightness(0.97); }
660
716
  .ksc-discount__chips { display: flex; flex-wrap: wrap; gap: 6px; }
661
- .ksc-discount__chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px 3px 10px; background: color-mix(in oklab, var(--ksc-accent) 12%, transparent); border-radius: 999px; font-size: 0.8125rem; color: var(--ksc-text); }
717
+ .ksc-discount__chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px 3px 10px; background: rgb(242, 244, 247); border-radius: 999px; font-size: 0.8125rem; color: var(--ksc-text); }
662
718
  .ksc-discount__chip-remove { display: inline-flex; align-items: center; justify-content: center; background: none; border: none; cursor: pointer; padding: 2px; color: color-mix(in oklab, var(--ksc-text) 60%, transparent); line-height: 1; font-size: 1rem; }
663
719
  .ksc-discount__chip-remove:hover { color: var(--ksc-text); }
664
720
  `;
@@ -691,11 +747,26 @@ function readMessage2(config, key) {
691
747
  const raw = config[key];
692
748
  return typeof raw === "string" && raw.trim() !== "" ? raw : null;
693
749
  }
750
+ function renderTemplate(template, amount) {
751
+ return template.split(/\{amount\}/g).map((part) => escapeHtml(part)).join(`<b>${escapeHtml(amount)}</b>`);
752
+ }
753
+ var TRUCK_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 5h14v11H1zM15 9h4l3 3v4h-7z"/><circle cx="6" cy="18.5" r="1.8"/><circle cx="18" cy="18.5" r="1.8"/></svg>`;
694
754
  var css5 = `
695
- .ksc-fsb { padding: 12px 16px; font: inherit; color: var(--ksc-text); }
696
- .ksc-fsb__msg { margin: 0 0 8px; font-size: 0.875rem; }
697
- .ksc-fsb__track { height: 6px; border-radius: var(--ksc-radius); background: color-mix(in oklab, var(--ksc-text) 12%, transparent); overflow: hidden; }
698
- .ksc-fsb__fill { height: 100%; background: var(--ksc-accent); transition: width .3s ease; }
755
+ .ksc-fsb { padding: 14px 20px 10px; font: inherit; color: var(--ksc-text); }
756
+ .ksc-fsb__msg { margin: 0 0 10px; font-size: 0.8125rem; text-align: center; }
757
+ .ksc-fsb__msg b { font-weight: 700; }
758
+ .ksc-fsb__track { position: relative; height: 8px; border-radius: 999px;
759
+ background: rgb(234, 236, 240); }
760
+ .ksc-fsb__fill { height: 100%; border-radius: 999px; background: rgb(29, 41, 57);
761
+ transition: width .3s ease; }
762
+ .ksc-fsb__step { position: absolute; right: -2px; top: 50%; transform: translateY(-50%);
763
+ width: 14px; height: 14px; border-radius: 4px; border: 2px solid rgb(29, 41, 57);
764
+ background: var(--ksc-surface); box-sizing: border-box; }
765
+ .ksc-fsb--unlocked .ksc-fsb__step { background: rgb(29, 41, 57); }
766
+ .ksc-fsb__tier { display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
767
+ margin-top: 6px; font-size: 0.78rem; color: rgb(152, 162, 179); }
768
+ .ksc-fsb__tier svg { width: 20px; height: 20px; }
769
+ .ksc-fsb--unlocked .ksc-fsb__tier { color: rgb(29, 41, 57); }
699
770
  @media (prefers-reduced-motion: reduce) { .ksc-fsb__fill { transition: none; } }
700
771
  `;
701
772
  var freeShippingBar = {
@@ -705,20 +776,22 @@ var freeShippingBar = {
705
776
  render({ config, state }) {
706
777
  const threshold = readThreshold(config);
707
778
  if (threshold === null || state === null) return "";
708
- const subtotal = state.subtotal.amount;
779
+ const discountTotal = state.discountTotal?.amount ?? 0;
780
+ const subtotal = Math.max(0, state.subtotal.amount - discountTotal);
709
781
  const remaining = Math.max(0, threshold - subtotal);
710
782
  const unlocked = remaining === 0;
711
783
  const pct = Math.min(100, Math.round(subtotal / threshold * 100));
712
- const remainingMoney = formatMoney2({ amount: remaining, currency: state.subtotal.currency });
784
+ const remainingMoney = formatMoneyCompact({
785
+ amount: remaining,
786
+ currency: state.subtotal.currency
787
+ });
713
788
  const beforeTemplate = readMessage2(config, "messageBefore");
714
789
  const afterTemplate = readMessage2(config, "messageAfter");
715
- const message = unlocked ? afterTemplate ?? "You've unlocked free shipping!" : (beforeTemplate ?? "You're {amount} away from free shipping").replace(
716
- /\{amount\}/g,
717
- remainingMoney
718
- );
719
- return `<div class="ksc-fsb">
720
- <p class="ksc-fsb__msg">${escapeHtml(message)}</p>
721
- <div class="ksc-fsb__track"><div class="ksc-fsb__fill" style="width: ${pct}%"></div></div>
790
+ const message = unlocked ? afterTemplate !== null ? renderTemplate(afterTemplate, remainingMoney) : `<b>Free shipping</b> unlocked!` : beforeTemplate !== null ? renderTemplate(beforeTemplate, remainingMoney) : `Add <b>${escapeHtml(remainingMoney)}</b> to unlock <b>Free shipping</b>!`;
791
+ return `<div class="ksc-fsb${unlocked ? " ksc-fsb--unlocked" : ""}">
792
+ <p class="ksc-fsb__msg">${message}</p>
793
+ <div class="ksc-fsb__track"><div class="ksc-fsb__fill" style="width: ${pct}%"></div><span class="ksc-fsb__step" aria-hidden="true"></span></div>
794
+ <div class="ksc-fsb__tier">${TRUCK_SVG}<span>${unlocked ? "" : "\u{1F512} "}Free shipping</span></div>
722
795
  </div>`;
723
796
  }
724
797
  };
@@ -885,6 +958,23 @@ var PAYMENT_ICON_SVGS = {
885
958
  <path d="M228.897012,82.7490197 C216.569147,146.069792 174.366398,167.970272 120.46841,167.970272 L93.0241367,167.970272 C86.4398419,167.970272 80.8794007,172.764903 79.8555471,179.265958 L61.8174095,293.621258 C61.1431644,297.883153 64.4394738,301.745495 68.7513129,301.745495 L117.421821,301.745495 C123.182038,301.745495 128.084882,297.550192 128.983876,291.864891 L129.458344,289.384335 L138.631407,231.249423 L139.222412,228.036354 C140.121406,222.351053 145.02425,218.15575 150.784467,218.15575 L158.067979,218.15575 C205.215193,218.15575 242.132193,199.002194 252.920115,143.605884 C257.423406,120.456802 255.092683,101.128442 243.181019,87.5519756 C239.568397,83.4399129 235.081754,80.0437153 229.854273,77.2718188 C229.571257,79.0614817 229.263268,80.8761167 228.897012,82.7490197 L228.897012,82.7490197 Z" fill="#2790C3"></path>
886
959
  <path d="M216.952052,72.1275783 C215.070825,71.5781934 213.13133,71.0787526 211.133566,70.6375799 C209.135803,70.1964071 207.071448,69.8051785 204.957148,69.4638939 C197.548776,68.265236 189.457835,67.699203 180.767565,67.699203 L107.457976,67.699203 C105.651665,67.699203 103.936919,68.1070797 102.4053,68.8479169 C99.0174265,70.4710996 96.5118984,73.6675208 95.8959214,77.5881313 L80.3133678,176.385849 L79.8638711,179.265958 C80.8877248,172.764903 86.4481659,167.970272 93.0324607,167.970272 L120.476734,167.970272 C174.374722,167.970272 216.577471,146.078116 228.905336,82.7490197 C229.271592,80.8761167 229.579581,79.0614817 229.862597,77.2718188 C226.741092,75.623664 223.361542,74.2002577 219.723948,72.9932757 C218.816631,72.6936112 217.892665,72.4022708 216.952052,72.1275783" fill="#1F264F"></path>
887
960
  </g>
961
+ </svg>`,
962
+ "apple-pay": `<svg width="512px" height="211px" viewBox="0 0 512 211" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
963
+ <g>
964
+ <path d="M93.5520633,27.1031049 C87.5513758,34.2039183 77.9502759,39.8045599 68.349176,39.0044683 C67.1490386,29.4033684 71.849577,19.2021998 77.3502072,12.901478 C83.3508946,5.6006416 93.8520976,0.400045829 102.353071,0 C103.353186,10.0011457 99.4527392,19.8022685 93.5520633,27.1031049 Z M102.25306,40.904686 C88.3514675,40.1045943 76.4501041,48.8055911 69.8493479,48.8055911 C63.1485803,48.8055911 53.0474231,41.3047318 42.0461628,41.5047547 C27.7445244,41.7047776 14.4430006,49.8057057 7.14216427,62.7071836 C-7.8595543,88.5101396 3.24171744,126.714516 17.7433787,147.716922 C24.8441922,158.118114 33.345166,169.51942 44.5464492,169.119374 C55.1476637,168.719328 59.3481449,162.218584 72.1496114,162.218584 C85.0510894,162.218584 88.7515133,169.119374 99.9527965,168.919351 C111.554126,168.719328 118.854962,158.51816 125.955775,148.116968 C134.056703,136.315616 137.357081,124.814299 137.557104,124.21423 C137.357081,124.014207 115.154538,115.513233 114.954515,89.9103 C114.754492,68.5078482 132.45652,58.3066795 133.256612,57.7066108 C123.255466,42.9049151 107.653679,41.3047318 102.25306,40.904686 Z M182.56226,11.9013634 L182.56226,167.819225 L206.765033,167.819225 L206.765033,114.513118 L240.268871,114.513118 C270.872377,114.513118 292.37484,93.5107124 292.37484,63.1072295 C292.37484,32.7037465 271.272423,11.9013634 241.068963,11.9013634 L182.56226,11.9013634 Z M206.765033,32.3037007 L234.668229,32.3037007 C255.670635,32.3037007 267.67201,43.5049839 267.67201,63.2072409 C267.67201,82.909498 255.670635,94.2107926 234.568218,94.2107926 L206.765033,94.2107926 L206.765033,32.3037007 Z M336.579904,169.019363 C351.781646,169.019363 365.883261,161.31848 372.283994,149.117083 L372.784052,149.117083 L372.784052,167.819225 L395.186618,167.819225 L395.186618,90.2103344 C395.186618,67.7077565 377.184556,53.2060952 349.481382,53.2060952 C323.778438,53.2060952 304.776261,67.9077794 304.076181,88.1100938 L325.878678,88.1100938 C327.678884,78.5089939 336.579904,72.2082721 348.781302,72.2082721 C363.582998,72.2082721 371.883949,79.1090626 371.883949,91.8105177 L371.883949,100.411503 L341.680488,102.211709 C313.577269,103.911904 298.375528,115.413222 298.375528,135.415513 C298.375528,155.617827 314.077326,169.019363 336.579904,169.019363 Z M343.080649,150.517243 C330.179171,150.517243 321.978231,144.316533 321.978231,134.815444 C321.978231,125.014321 329.879137,119.313668 344.980867,118.413565 L371.883949,116.713371 L371.883949,125.514379 C371.883949,140.116051 359.482528,150.517243 343.080649,150.517243 Z M425.090044,210.224083 C448.692748,210.224083 459.794019,201.223052 469.495131,173.919924 L512,54.7062671 L487.397182,54.7062671 L458.893916,146.816819 L458.393859,146.816819 L429.890594,54.7062671 L404.587695,54.7062671 L445.592392,168.219271 L443.39214,175.120061 C439.691716,186.821402 433.691029,191.321918 422.989803,191.321918 C421.089585,191.321918 417.389162,191.121895 415.88899,190.921872 L415.88899,209.624014 C417.28915,210.02406 423.289838,210.224083 425.090044,210.224083 Z" fill="#000000"></path>
965
+ </g>
966
+ </svg>`,
967
+ "google-pay": `<svg width="512px" height="204px" viewBox="0 0 512 204" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
968
+ <title>Google Pay</title>
969
+ <g>
970
+ <path d="M362.926585,55.0572275 C377.002187,55.0572275 387.878788,58.8960279 396.196189,66.5736286 C404.51359,74.2512294 408.35239,84.4880304 408.35239,97.2840316 L408.35239,158.704838 L390.437988,158.704838 L390.437988,144.629236 L389.798188,144.629236 C382.120587,156.145637 371.243986,161.903838 358.447985,161.903838 C347.571384,161.903838 337.974383,158.704838 330.296782,152.306837 C323.258982,145.908836 318.780381,136.951636 318.780381,127.354635 C318.780381,117.117834 322.619182,108.800433 330.296782,102.402432 C337.974383,96.0044315 348.850984,93.4452313 361.646985,93.4452313 C373.163386,93.4452313 382.120587,95.3646315 389.158388,99.8432319 L389.158388,95.3646315 C389.158388,89.3931642 386.92904,83.421697 382.470345,79.5309543 L381.480787,78.7298298 C376.362387,74.2512294 369.964386,71.6920291 362.926585,71.6920291 C352.049984,71.6920291 343.732584,76.1706296 337.974383,85.1278305 L321.339581,74.8910295 C331.576382,61.4552281 345.012184,55.0572275 362.926585,55.0572275 Z M272.714777,11.5508233 C284.195633,11.5508233 295.105804,15.5456243 303.828346,22.9962452 L305.34458,24.3468245 C314.301781,32.0244253 318.780381,43.5408264 318.780381,55.6970276 C318.780381,67.8532288 314.301781,78.7298298 305.34458,87.0472306 C296.387379,95.3646315 285.510778,99.8432319 272.714777,99.8432319 L242.004374,99.2034318 L242.004374,158.704838 L222.810372,158.704838 L222.810372,11.5508233 L272.714777,11.5508233 Z M365.485786,108.800433 C357.808185,108.800433 351.410184,110.719833 346.291784,114.558633 C341.173383,117.757634 338.614183,122.236234 338.614183,127.994435 C338.614183,133.112835 341.173383,137.591436 345.012184,140.150636 C349.490784,143.349636 354.609185,145.269036 359.727585,145.269036 C366.893346,145.269036 374.059107,142.482352 379.664324,137.429163 L380.840987,136.311835 C387.238988,130.553635 390.437988,123.515834 390.437988,115.838233 C384.679788,111.359633 376.362387,108.800433 365.485786,108.800433 Z M273.994377,29.465225 L242.004374,29.465225 L242.004374,80.64923 L273.994377,80.64923 C281.032177,80.64923 288.069978,78.0900298 292.548579,72.9716293 C302.78538,63.3746283 302.78538,47.3796268 293.188379,37.7826258 L292.548579,37.1428258 C287.430178,32.0244253 281.032177,28.825425 273.994377,29.465225 Z M512,58.2562278 L448.659794,203.490842 L429.465792,203.490842 L453.138394,152.946637 L411.55139,58.8960279 L432.024992,58.8960279 L462.095595,131.193435 L462.735395,131.193435 L492.166198,58.8960279 L512,58.8960279 L512,58.2562278 Z" fill="#5F6368"></path>
971
+ <g>
972
+ <path d="M165.868166,86.4074306 C165.868166,80.64923 165.228366,74.8910295 164.588566,69.1328289 L84.6135583,69.1328289 L84.6135583,101.762632 L130.039363,101.762632 C128.119963,111.999433 122.361762,121.596434 113.404561,127.354635 L113.404561,148.468037 L140.915964,148.468037 C156.910965,133.752635 165.868166,111.999433 165.868166,86.4074306 Z" fill="#4285F4"></path>
973
+ <path d="M84.6135583,168.941639 C107.646361,168.941639 126.840362,161.264038 140.915964,148.468037 L113.404561,127.354635 C105.72696,132.473035 96.1299594,135.672035 84.6135583,135.672035 C62.8603561,135.672035 43.6663543,120.956634 37.2683536,100.483032 L9.11715089,100.483032 L9.11715089,122.236234 C23.8325523,151.027237 52.6235551,168.941639 84.6135583,168.941639 Z" fill="#34A853"></path>
974
+ <path d="M37.2683536,100.483032 C33.4295533,90.246231 33.4295533,78.7298298 37.2683536,67.8532288 L37.2683536,46.1000266 L9.11715089,46.1000266 C-3.0390503,69.772629 -3.0390503,97.9238317 9.11715089,122.236234 L37.2683536,100.483032 Z" fill="#FBBC04"></path>
975
+ <path d="M84.6135583,33.3040254 C96.7697595,33.3040254 108.286161,37.7826258 117.243361,46.1000266 L117.243361,46.1000266 L141.555764,21.7876243 C126.200562,7.71202289 105.72696,-0.60537792 85.2533583,0.0344221425 C53.2633552,0.0344221425 23.8325523,17.9488239 9.75695095,46.7398267 L37.9081537,68.4930288 C43.6663543,48.0194268 62.8603561,33.3040254 84.6135583,33.3040254 Z" fill="#EA4335"></path>
976
+ </g>
977
+ </g>
888
978
  </svg>`
889
979
  };
890
980
 
@@ -905,10 +995,11 @@ function readBadges(config) {
905
995
  return valid.length > 0 ? valid : null;
906
996
  }
907
997
  var css7 = `
908
- .ksc-tb { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: center;
998
+ .ksc-tb { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; justify-content: center;
909
999
  padding: 10px 16px 4px; }
910
- .ksc-tb__icon { display: flex; align-items: center; height: 22px; width: auto; }
911
- .ksc-tb__icon svg { height: 22px; width: auto; display: block; }
1000
+ .ksc-tb__icon { display: flex; align-items: center; justify-content: center; width: 38px; height: 26px;
1001
+ border: 1px solid rgb(234, 236, 240); border-radius: 5px; background: #fff; box-sizing: border-box; }
1002
+ .ksc-tb__icon svg { max-height: 15px; max-width: 28px; width: auto; height: auto; display: block; }
912
1003
  `;
913
1004
  var trustBadges = {
914
1005
  key: "trustBadges",
@@ -956,7 +1047,7 @@ var STYLES2 = `
956
1047
  --ksc-border: color-mix(in oklab, var(--ksc-text) 12%, transparent); }
957
1048
  .overlay { position: absolute; inset: 0; background: rgba(0,0,0,.45); opacity: 0;
958
1049
  transition: opacity .25s ease; }
959
- .panel { position: absolute; top: 0; right: 0; height: 100%; width: min(480px, 100vw);
1050
+ .panel { position: absolute; top: 0; right: 0; height: 100%; width: min(520px, 100vw);
960
1051
  background: var(--ksc-surface); color: var(--ksc-text); font-family: var(--ksc-font);
961
1052
  transform: translateX(100%); transition: transform .25s ease; display: flex; flex-direction: column;
962
1053
  box-shadow: -8px 0 40px rgba(0,0,0,.14); }
@@ -965,12 +1056,13 @@ var STYLES2 = `
965
1056
  :host(:not([open])) { pointer-events: none; }
966
1057
  :host([open]) { pointer-events: auto; }
967
1058
 
968
- /* Header */
969
- .ksc-head { display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
970
- padding: 16px 18px; border-bottom: 1px solid var(--ksc-border); }
971
- .ksc-head__title { font-size: 1rem; font-weight: 600; letter-spacing: .01em; }
972
- .ksc-head__count { color: var(--ksc-muted); font-weight: 500; font-size: .95rem; }
973
- .ksc-head__close { margin-left: auto; display: flex; align-items: center; justify-content: center;
1059
+ /* Header \u2014 centered "My cart \xB7 N" with the close control pinned right (original cart layout). */
1060
+ .ksc-head { position: relative; display: flex; align-items: center; justify-content: center;
1061
+ gap: 6px; flex: 0 0 auto; padding: 16px 52px; }
1062
+ .ksc-head__title { font-size: 1.125rem; font-weight: 600; letter-spacing: .01em; }
1063
+ .ksc-head__count { font-weight: 600; font-size: 1.125rem; }
1064
+ .ksc-head__close { position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
1065
+ display: flex; align-items: center; justify-content: center;
974
1066
  width: 32px; height: 32px; padding: 0; border: none; background: none; cursor: pointer;
975
1067
  color: var(--ksc-text); border-radius: 8px; }
976
1068
  .ksc-head__close:hover { background: var(--ksc-border); }
@@ -982,61 +1074,66 @@ var STYLES2 = `
982
1074
  gap: 14px; padding: 56px 24px; text-align: center; color: var(--ksc-muted); }
983
1075
  .ksc-empty svg { width: 44px; height: 44px; opacity: .5; }
984
1076
  .ksc-empty p { margin: 0; font-size: .95rem; }
985
- .ksc-line { display: grid; grid-template-columns: 64px 1fr; gap: 14px;
986
- padding: 16px 18px; border-bottom: 1px solid var(--ksc-border); }
987
- .ksc-line__img { width: 64px; height: 64px; object-fit: cover; border-radius: 10px;
1077
+ .ksc-line { display: grid; grid-template-columns: 80px 1fr; gap: 14px;
1078
+ padding: 14px 20px; }
1079
+ .ksc-line + .ksc-line { border-top: 1px solid var(--ksc-border); }
1080
+ .ksc-line__img { width: 80px; height: 80px; object-fit: cover; border-radius: 8px;
988
1081
  background: var(--ksc-border); }
989
1082
  .ksc-line__img--ph { display: block; }
990
- .ksc-line__body { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
991
- .ksc-line__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
992
- .ksc-line__title { font-size: .875rem; font-weight: 600; line-height: 1.3; }
993
- .ksc-line__compare { color: var(--ksc-muted); text-decoration: line-through;
994
- font-size: .8125rem; white-space: nowrap; padding-top: 1px; }
995
- .ksc-line__meta { display: flex; flex-direction: column; gap: 1px; }
996
- .ksc-line__opt { font-size: .78rem; color: var(--ksc-muted); line-height: 1.35;
1083
+ .ksc-line__body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
1084
+ .ksc-line__top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
1085
+ .ksc-line__main { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
1086
+ .ksc-line__title { font-size: .9375rem; font-weight: 700; line-height: 1.3; }
1087
+ .ksc-line__compare { color: rgb(152, 162, 179); text-decoration: line-through;
1088
+ font-size: .875rem; white-space: nowrap; }
1089
+ .ksc-line__meta { display: flex; flex-direction: column; gap: 3px; }
1090
+ .ksc-line__opt { font-size: .8125rem; color: rgb(152, 162, 179); line-height: 1.35;
997
1091
  overflow: hidden; text-overflow: ellipsis; }
998
- .ksc-line__opt b { font-weight: 500; color: color-mix(in oklab, var(--ksc-text) 78%, transparent); }
999
- .ksc-line__variant { display: flex; align-items: center; gap: 6px; font-size: .78rem; color: var(--ksc-muted); }
1000
- .ksc-line__select { font: inherit; font-size: .78rem; color: var(--ksc-text); padding: 3px 22px 3px 8px;
1092
+ .ksc-line__opt b { font-weight: 700; color: var(--ksc-text); }
1093
+ .ksc-line__variant { display: flex; flex-direction: column; gap: 3px; align-items: flex-start;
1094
+ font-size: .8125rem; color: rgb(152, 162, 179); }
1095
+ .ksc-line__discount { font-size: .8125rem; font-weight: 500; color: var(--ksc-positive); line-height: 1.35; }
1096
+ .ksc-line__discount s { color: rgb(152, 162, 179); text-decoration: line-through; }
1097
+ .ksc-line__select { font: inherit; font-size: .8125rem; color: var(--ksc-text); padding: 4px 24px 4px 10px;
1001
1098
  border: 1px solid var(--ksc-border); border-radius: 8px; background: var(--ksc-surface); cursor: pointer;
1002
1099
  appearance: none; -webkit-appearance: none;
1003
1100
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none' stroke='%23999' stroke-width='1.5'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
1004
1101
  background-repeat: no-repeat; background-position: right 8px center; }
1005
1102
  .ksc-line__select:focus { outline: none; border-color: var(--ksc-accent); }
1006
1103
  .ksc-line__bottom { display: flex; align-items: center; justify-content: space-between;
1007
- gap: 10px; margin-top: 5px; }
1104
+ gap: 10px; margin-top: 4px; }
1008
1105
  .ksc-qty { display: inline-flex; align-items: center; border: 1px solid var(--ksc-border);
1009
- border-radius: 999px; }
1010
- .ksc-qty button { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
1106
+ border-radius: 8px; }
1107
+ .ksc-qty button { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
1011
1108
  padding: 0; border: none; background: none; cursor: pointer; color: var(--ksc-text);
1012
- font-size: 16px; line-height: 1; border-radius: 999px; }
1109
+ font-size: 16px; line-height: 1; border-radius: 8px; }
1013
1110
  .ksc-qty button:hover { background: var(--ksc-border); }
1014
- .ksc-qty__n { min-width: 22px; text-align: center; font-size: .8125rem; font-variant-numeric: tabular-nums; }
1015
- .ksc-line__end { display: flex; align-items: center; gap: 8px; }
1016
- .ksc-line__prices { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
1017
- .ksc-line__price { font-size: .875rem; font-weight: 600; }
1018
- .ksc-line__save { font-size: .68rem; font-weight: 600; color: var(--ksc-positive); white-space: nowrap;
1019
- background: color-mix(in oklab, var(--ksc-positive) 14%, transparent); padding: 1px 6px; border-radius: 999px; }
1020
- .ksc-line__remove { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px;
1111
+ .ksc-qty__n { min-width: 26px; text-align: center; font-size: .875rem; font-variant-numeric: tabular-nums; }
1112
+ .ksc-line__prices { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex: 0 0 auto; }
1113
+ .ksc-line__price { font-size: .9375rem; font-weight: 700; }
1114
+ .ksc-line__save { display: inline-flex; align-items: center; gap: 4px; font-size: .75rem;
1115
+ font-weight: 500; color: rgb(7, 148, 85); white-space: nowrap;
1116
+ background: rgb(220, 250, 230); padding: 3px 8px; border-radius: 6px; }
1117
+ .ksc-line__save svg { width: 12px; height: 12px; }
1118
+ .ksc-line__remove { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px;
1021
1119
  padding: 0; border: none; background: none; cursor: pointer; color: var(--ksc-muted); border-radius: 8px; }
1022
1120
  .ksc-line__remove:hover { color: var(--ksc-text); background: var(--ksc-border); }
1023
- .ksc-line__remove svg { width: 15px; height: 15px; }
1121
+ .ksc-line__remove svg { width: 17px; height: 17px; }
1024
1122
 
1025
1123
  /* Footer / summary / checkout */
1026
1124
  .footer { flex: 0 0 auto; border-top: 1px solid var(--ksc-border); }
1027
- .ksc-summary { display: flex; flex-direction: column; gap: 3px; padding: 12px 18px 2px; }
1125
+ .ksc-summary { display: flex; flex-direction: column; gap: 6px; padding: 12px 20px 2px; }
1028
1126
  .ksc-summary__row { display: flex; align-items: center; justify-content: space-between;
1029
- font-size: .8125rem; color: var(--ksc-muted); }
1030
- .ksc-summary__row--discount { color: var(--ksc-positive); font-weight: 500; }
1031
- .ksc-summary__row--total { font-size: .95rem; font-weight: 600; color: var(--ksc-text); padding-top: 3px; }
1032
- .checkout { display: block; text-align: center; margin: 8px 18px 0; padding: 15px;
1127
+ font-size: .875rem; color: rgb(71, 84, 103); }
1128
+ .ksc-summary__row--total { font-size: 1rem; font-weight: 700; color: var(--ksc-text); }
1129
+ .checkout { display: block; text-align: center; margin: 10px 20px 0; padding: 16px;
1033
1130
  background: var(--ksc-accent); color: var(--ksc-accent-text); text-decoration: none;
1034
- border-radius: var(--ksc-radius); font-weight: 600; font-size: .95rem; }
1131
+ border-radius: 10px; font-weight: 700; font-size: 1rem; }
1035
1132
  .checkout:hover { opacity: .92; }
1036
1133
  .ksc-continue { display: block; width: 100%; padding: 8px 18px 16px; text-align: center;
1037
- border: none; background: none; cursor: pointer; color: var(--ksc-muted); font: inherit;
1038
- font-size: .8125rem; text-decoration: underline; text-underline-offset: 2px; }
1039
- .ksc-continue:hover { color: var(--ksc-text); }
1134
+ border: none; background: none; cursor: pointer; color: var(--ksc-text); font: inherit;
1135
+ font-size: .875rem; font-weight: 700; }
1136
+ .ksc-continue:hover { opacity: .7; }
1040
1137
 
1041
1138
  @media (prefers-reduced-motion: reduce) { .overlay, .panel { transition: none; } }
1042
1139
  .chrome { position: absolute; inset: 0; pointer-events: none; }
@@ -1046,6 +1143,7 @@ var STYLES2 = `
1046
1143
  var CLOSE_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>`;
1047
1144
  var TRASH_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M7 7l1 13h8l1-13"/></svg>`;
1048
1145
  var BAG_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8h12l1 12H5L6 8zM9 8a3 3 0 0 1 6 0"/></svg>`;
1146
+ var TAG_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 13l-7 7-9-9V4h7l9 9z"/><circle cx="7.5" cy="7.5" r="1" fill="currentColor"/></svg>`;
1049
1147
  var Base = typeof HTMLElement !== "undefined" ? HTMLElement : class {
1050
1148
  };
1051
1149
  var SmartCartElement = class _SmartCartElement extends Base {
@@ -1139,6 +1237,13 @@ var SmartCartElement = class _SmartCartElement extends Base {
1139
1237
  }
1140
1238
  const upsellBtn = target?.closest("[data-smart-cart-upsell-add]");
1141
1239
  if (upsellBtn) void this.#onUpsellAdd(upsellBtn);
1240
+ const upsellNav = target?.closest(
1241
+ "[data-smart-cart-upsell-prev], [data-smart-cart-upsell-next]"
1242
+ );
1243
+ if (upsellNav) {
1244
+ this.#onUpsellNav(upsellNav);
1245
+ return;
1246
+ }
1142
1247
  const discountApply = target?.closest("[data-smart-cart-discount-apply]");
1143
1248
  if (discountApply) {
1144
1249
  void this.#onDiscountApply(discountApply);
@@ -1149,7 +1254,12 @@ var SmartCartElement = class _SmartCartElement extends Base {
1149
1254
  });
1150
1255
  this.#root.querySelector(".panel")?.addEventListener("change", (e) => {
1151
1256
  const select = e.target?.closest("[data-smart-cart-variant]");
1152
- if (select) void this.#onVariantSelect(select);
1257
+ if (select) {
1258
+ void this.#onVariantSelect(select);
1259
+ return;
1260
+ }
1261
+ const upsellSelect = e.target?.closest("[data-smart-cart-upsell-variant]");
1262
+ if (upsellSelect) this.#onUpsellVariantSelect(upsellSelect);
1153
1263
  });
1154
1264
  }
1155
1265
  connectedCallback() {
@@ -1194,8 +1304,7 @@ var SmartCartElement = class _SmartCartElement extends Base {
1194
1304
  this.#renderCart(state);
1195
1305
  this.#renderChrome(state);
1196
1306
  }
1197
- void this.#resolveUpsellVariants();
1198
- void this.#resolveProductVariants();
1307
+ void this.#resolveUpsellVariants().then(() => this.#resolveProductVariants());
1199
1308
  }
1200
1309
  /** Add a line (from a theme add-to-cart submit intercepted by mountSmartCart) and open the drawer on
1201
1310
  * the result. Opens first for instant feedback, then adds and reconciles against the server cart.
@@ -1217,8 +1326,9 @@ var SmartCartElement = class _SmartCartElement extends Base {
1217
1326
  }
1218
1327
  }
1219
1328
  /** Lazily fetch sibling variants for every cart line that has a product handle and a real variant
1220
- * title (single-variant lines have none → no switcher). Caches per handle, never re-fetches, and
1221
- * re-renders the cart once new variants land so the `<select>`s appear. Fail-closed per the lookup. */
1329
+ * title (single-variant lines have none → no switcher), plus every resolved upsell/add-on
1330
+ * product (the upsell card's size select). Caches per handle, never re-fetches, and re-renders
1331
+ * the cart once new variants land so the `<select>`s appear. Fail-closed per the lookup. */
1222
1332
  async #resolveProductVariants() {
1223
1333
  const lookup = this.#variantLookup;
1224
1334
  if (!lookup?.fetchProductVariants || this.#lastState === null) return;
@@ -1229,6 +1339,12 @@ var SmartCartElement = class _SmartCartElement extends Base {
1229
1339
  handles.add(handle);
1230
1340
  }
1231
1341
  }
1342
+ for (const variant of this.#upsellVariants?.values() ?? []) {
1343
+ const handle = variant.productHandle;
1344
+ if (handle !== null && handle.length > 0 && !this.#productVariants.has(handle) && !this.#productVariantsFetching.has(handle)) {
1345
+ handles.add(handle);
1346
+ }
1347
+ }
1232
1348
  if (handles.size === 0) return;
1233
1349
  await Promise.all(
1234
1350
  [...handles].map(async (handle) => {
@@ -1373,6 +1489,20 @@ var SmartCartElement = class _SmartCartElement extends Base {
1373
1489
  this.#addOnPending = false;
1374
1490
  }
1375
1491
  }
1492
+ /** Scroll the upsell track one card left/right (the arrows sit outside the track). */
1493
+ #onUpsellNav(btn) {
1494
+ const track = btn.closest(".ksc-up")?.querySelector(".ksc-up__track");
1495
+ if (!track) return;
1496
+ const direction = btn.hasAttribute("data-smart-cart-upsell-prev") ? -1 : 1;
1497
+ track.scrollBy({ left: direction * track.clientWidth, behavior: "smooth" });
1498
+ }
1499
+ /** Retarget an upsell card's add button when the shopper picks a different size — the select
1500
+ * carries no other intent (no re-render, no network). */
1501
+ #onUpsellVariantSelect(select) {
1502
+ if (!(select instanceof HTMLSelectElement)) return;
1503
+ const addBtn = select.closest(".ksc-up__card")?.querySelector("[data-smart-cart-upsell-add]");
1504
+ if (addBtn && select.value) addBtn.setAttribute("data-variant-id", select.value);
1505
+ }
1376
1506
  async #onUpsellAdd(btn) {
1377
1507
  if (!this.#adapter || this.#upsellPending) return;
1378
1508
  const variantId = btn.getAttribute("data-variant-id");
@@ -1548,7 +1678,8 @@ ${css8}`);
1548
1678
  config: cfg,
1549
1679
  state,
1550
1680
  formatMoney: formatMoney2,
1551
- ...this.#upsellVariants !== null && { variants: this.#upsellVariants }
1681
+ ...this.#upsellVariants !== null && { variants: this.#upsellVariants },
1682
+ ...this.#productVariants.size > 0 && { productVariants: this.#productVariants }
1552
1683
  });
1553
1684
  } catch (err) {
1554
1685
  console.error(`[kotao-smart-cart] widget render failed`, widget.key, err);
@@ -1606,44 +1737,59 @@ ${css8}`);
1606
1737
  const hasSwitcher = variants !== void 0 && variants.length > 1;
1607
1738
  const optName = l.options?.[0]?.name ?? "";
1608
1739
  const propLines = (l.properties ?? []).map(
1609
- (p) => `<span class="ksc-line__opt"><b>${escapeHtml(p.name)}:</b> ${escapeHtml(p.value)}</span>`
1740
+ (p) => `<span class="ksc-line__opt">${escapeHtml(p.name)}: <b>${escapeHtml(p.value)}</b></span>`
1610
1741
  );
1611
1742
  const optLines = hasSwitcher ? [] : opts.map(
1612
- (o) => o.name ? `<span class="ksc-line__opt"><b>${escapeHtml(o.name)}:</b> ${escapeHtml(o.value)}</span>` : `<span class="ksc-line__opt">${escapeHtml(o.value)}</span>`
1743
+ (o) => o.name ? `<span class="ksc-line__opt">${escapeHtml(o.name)}: <b>${escapeHtml(o.value)}</b></span>` : `<span class="ksc-line__opt">${escapeHtml(o.value)}</span>`
1613
1744
  );
1614
- const switcher = hasSwitcher ? `<div class="ksc-line__variant">${optName ? `<span><b>${escapeHtml(optName)}:</b></span>` : ""}<select class="ksc-line__select" data-smart-cart-variant data-line-id="${id}" data-qty="${l.quantity}" aria-label="${escapeHtml(optName || "Variant")}">${variants.map(
1745
+ const switcher = hasSwitcher ? `<div class="ksc-line__variant">${optName ? `<span>${escapeHtml(optName)}:</span>` : ""}<select class="ksc-line__select" data-smart-cart-variant data-line-id="${id}" data-qty="${l.quantity}" aria-label="${escapeHtml(optName || "Variant")}">${variants.map(
1615
1746
  (v) => `<option value="${escapeHtml(v.id)}"${v.title === l.variantTitle ? " selected" : ""}>${escapeHtml(v.title ?? "")}</option>`
1616
1747
  ).join("")}</select></div>` : "";
1617
1748
  const metaParts = [...propLines, ...optLines, ...switcher ? [switcher] : []];
1618
1749
  const meta = metaParts.length ? `<div class="ksc-line__meta">${metaParts.join("")}</div>` : "";
1619
- const compare = l.compareAtLinePrice && l.compareAtLinePrice.amount > l.linePrice.amount ? `<span class="ksc-line__compare">${escapeHtml(formatMoney2(l.compareAtLinePrice))}</span>` : "";
1620
- const save = l.compareAtLinePrice && l.compareAtLinePrice.amount > l.linePrice.amount ? `<span class="ksc-line__save">You save ${escapeHtml(
1621
- formatMoney2({
1622
- amount: l.compareAtLinePrice.amount - l.linePrice.amount,
1750
+ const hasLineDiscount = (l.discountAllocations?.length ?? 0) > 0;
1751
+ const struck = l.compareAtLinePrice && l.compareAtLinePrice.amount > l.linePrice.amount ? l.compareAtLinePrice : hasLineDiscount && l.originalLinePrice && l.originalLinePrice.amount > l.linePrice.amount ? l.originalLinePrice : void 0;
1752
+ const compare = struck ? `<span class="ksc-line__compare">${escapeHtml(formatMoney2(struck))}</span>` : "";
1753
+ const save = struck && struck.amount - l.linePrice.amount > 0 ? `<span class="ksc-line__save">${TAG_SVG}You save ${escapeHtml(
1754
+ formatMoneyCompact({
1755
+ amount: struck.amount - l.linePrice.amount,
1623
1756
  currency: l.linePrice.currency
1624
1757
  })
1625
1758
  )}</span>` : "";
1759
+ const displayPrice = l.linePrice;
1760
+ const discountRows = hasLineDiscount ? l.discountAllocations.map((a) => {
1761
+ const code = escapeHtml(a.code);
1762
+ if (typeof a.quantity === "number" && a.quantity > 0) {
1763
+ const unit = Math.round((l.originalLinePrice ?? l.linePrice).amount / l.quantity);
1764
+ const struck2 = escapeHtml(
1765
+ formatMoney2({ amount: unit * a.quantity, currency: l.linePrice.currency })
1766
+ );
1767
+ return `<div class="ksc-line__discount"><span>\u{1F381} ${a.quantity}\xD7 ${code} \u2014 <s>${struck2}</s> FREE \xB7 You save 100%</span></div>`;
1768
+ }
1769
+ return `<div class="ksc-line__discount"><span>\u{1F3F7} ${code} \u2212${escapeHtml(formatMoney2(a.amount))}</span></div>`;
1770
+ }).join("") : "";
1626
1771
  return `<div class="ksc-line" data-line-id="${id}">
1627
1772
  ${image}
1628
1773
  <div class="ksc-line__body">
1629
1774
  <div class="ksc-line__top">
1630
- <span class="ksc-line__title">${escapeHtml(l.title)}</span>
1631
- ${compare}
1775
+ <div class="ksc-line__main">
1776
+ <span class="ksc-line__title">${escapeHtml(l.title)}</span>
1777
+ ${meta}
1778
+ ${discountRows}
1779
+ </div>
1780
+ <div class="ksc-line__prices">
1781
+ ${compare}
1782
+ <span class="ksc-line__price">${escapeHtml(formatMoney2(displayPrice))}</span>
1783
+ ${save}
1784
+ </div>
1632
1785
  </div>
1633
- ${meta}
1634
1786
  <div class="ksc-line__bottom">
1635
1787
  <div class="ksc-qty">
1636
1788
  <button type="button" data-smart-cart-qty-dec data-qty="${l.quantity}" aria-label="Decrease quantity">\u2212</button>
1637
1789
  <span class="ksc-qty__n">${l.quantity}</span>
1638
1790
  <button type="button" data-smart-cart-qty-inc data-qty="${l.quantity}" aria-label="Increase quantity">+</button>
1639
1791
  </div>
1640
- <div class="ksc-line__end">
1641
- <div class="ksc-line__prices">
1642
- <span class="ksc-line__price">${escapeHtml(formatMoney2(l.linePrice))}</span>
1643
- ${save}
1644
- </div>
1645
- <button type="button" class="ksc-line__remove" data-smart-cart-line-remove aria-label="Remove item">${TRASH_SVG}</button>
1646
- </div>
1792
+ <button type="button" class="ksc-line__remove" data-smart-cart-line-remove aria-label="Remove item">${TRASH_SVG}</button>
1647
1793
  </div>
1648
1794
  </div>
1649
1795
  </div>`;
@@ -1663,7 +1809,7 @@ ${css8}`);
1663
1809
  }
1664
1810
  try {
1665
1811
  if (displayLines.length === 0) {
1666
- items.innerHTML = `<div class="ksc-empty">${BAG_SVG}<p data-empty>Your cart is empty.</p></div>`;
1812
+ items.innerHTML = `<div class="ksc-empty">${BAG_SVG}<p data-empty>Your cart is empty</p></div>`;
1667
1813
  checkoutWrap.innerHTML = "";
1668
1814
  if (continueWrap)
1669
1815
  continueWrap.innerHTML = `<button type="button" class="ksc-continue" data-smart-cart-close>Continue shopping</button>`;
@@ -1673,28 +1819,21 @@ ${css8}`);
1673
1819
  return;
1674
1820
  }
1675
1821
  items.innerHTML = displayLines.map((l) => this.#renderLine(l)).join("");
1676
- const compareAtSavings = state.lines.reduce(
1677
- (sum, l) => sum + (l.compareAtLinePrice && l.compareAtLinePrice.amount > l.linePrice.amount ? l.compareAtLinePrice.amount - l.linePrice.amount : 0),
1678
- 0
1679
- );
1680
- const cartDiscount = state.discountTotal?.amount ?? 0;
1681
- const savings = compareAtSavings + cartDiscount;
1682
- const displayTotal = {
1683
- amount: Math.max(0, state.subtotal.amount - cartDiscount),
1822
+ const finalSubtotal = {
1823
+ amount: state.lines.reduce((sum, l) => sum + l.linePrice.amount, 0),
1684
1824
  currency: state.subtotal.currency
1685
1825
  };
1686
- const rows = [];
1687
- if (savings > 0) {
1688
- rows.push(
1689
- `<div class="ksc-summary__row ksc-summary__row--discount" data-savings><span>Discounts</span><span>\u2212${escapeHtml(formatMoney2({ amount: savings, currency: state.subtotal.currency }))}</span></div>`
1690
- );
1691
- }
1692
- rows.push(
1693
- `<div class="ksc-summary__row ksc-summary__row--total"><span>Subtotal</span><span>${escapeHtml(formatMoney2(state.subtotal))}</span></div>`
1694
- );
1826
+ const discountsTotal = state.discountTotal?.amount ?? state.discountAllocations?.reduce((sum, a) => sum + a.amount.amount, 0) ?? 0;
1827
+ const displayTotal = finalSubtotal;
1828
+ const rows = [
1829
+ ...discountsTotal > 0 ? [
1830
+ `<div class="ksc-summary__row ksc-summary__row--discount"><span>Discounts</span><span>-${escapeHtml(formatMoney2({ amount: discountsTotal, currency: state.subtotal.currency }))}</span></div>`
1831
+ ] : [],
1832
+ `<div class="ksc-summary__row ksc-summary__row--total"><span>Subtotal</span><span>${escapeHtml(formatMoney2(finalSubtotal))}</span></div>`
1833
+ ];
1695
1834
  const totalLabel = escapeHtml(formatMoney2(displayTotal));
1696
1835
  checkoutWrap.innerHTML = `<div class="ksc-summary">${rows.join("")}</div>
1697
- <a class="checkout" data-checkout href="${escapeHtml(state.checkoutUrl)}">Checkout${totalLabel ? ` \xB7 ${totalLabel}` : ""}</a>`;
1836
+ <a class="checkout" data-checkout href="${escapeHtml(state.checkoutUrl)}">Checkout${totalLabel ? ` \u2022 ${totalLabel}` : ""}</a>`;
1698
1837
  if (continueWrap)
1699
1838
  continueWrap.innerHTML = `<button type="button" class="ksc-continue" data-smart-cart-close>or continue shopping</button>`;
1700
1839
  } catch (err) {