@asteby/storefront-react 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -502,6 +502,7 @@ function useProductOptions(product, config = {}) {
502
502
  exists: exact ? true : loose.length > 0 && Object.keys(selectedOptions).length < product.options.length,
503
503
  available: exact ? exact.available_for_sale : Object.keys(selectedOptions).length < product.options.length && loose.some((v) => v.available_for_sale),
504
504
  variant: exact,
505
+ availability: exact ? storefront.resolveAvailability(exact) : loose.length ? storefront.productAvailability({ variants: loose, available_for_sale: loose.some((v) => v.available_for_sale) }) : null,
505
506
  search: `?${search.toString()}`
506
507
  };
507
508
  })
@@ -520,21 +521,32 @@ function toMerchandise(product, variant) {
520
521
  function AddToCartButton({
521
522
  variantId,
522
523
  quantity = 1,
524
+ availability,
525
+ availabilitySource,
526
+ lowStockThreshold,
527
+ locale,
523
528
  merchandise,
524
529
  onAdded,
525
530
  onClick,
526
531
  disabled,
527
- children = "Agregar al carrito",
532
+ children,
528
533
  type = "button",
529
534
  ...rest
530
535
  }) {
531
536
  const cart = useCart();
537
+ const sf = useOptionalStorefront();
538
+ const resolved = availability ?? (availabilitySource ? storefront.resolveAvailability(availabilitySource, { lowStockThreshold: lowStockThreshold ?? null }) : null);
539
+ const view = resolved ? storefront.availabilityView(resolved, locale ?? sf?.locale ?? "es-MX") : null;
540
+ const blocked = view ? !view.ctaEnabled : false;
541
+ const qty = view?.maxPerOrder != null ? Math.min(quantity, view.maxPerOrder) : quantity;
542
+ const label = children ?? view?.ctaLabel ?? "Agregar al carrito";
532
543
  const [adding, setAdding] = react.useState(false);
533
544
  return /* @__PURE__ */ jsxRuntime.jsx(
534
545
  "button",
535
546
  {
536
547
  type,
537
- disabled: disabled || !variantId || adding,
548
+ disabled: disabled || !variantId || adding || blocked,
549
+ "data-availability": view?.status,
538
550
  "aria-busy": adding || void 0,
539
551
  "data-adding": adding || void 0,
540
552
  onClick: async (event) => {
@@ -542,17 +554,247 @@ function AddToCartButton({
542
554
  if (event.defaultPrevented || !variantId) return;
543
555
  setAdding(true);
544
556
  try {
545
- const result = await cart.addLines([{ variant_id: variantId, quantity, ...merchandise ? { merchandise } : {} }]);
557
+ const result = await cart.addLines([{ variant_id: variantId, quantity: qty, ...merchandise ? { merchandise } : {} }]);
546
558
  if (result) onAdded?.();
547
559
  } finally {
548
560
  setAdding(false);
549
561
  }
550
562
  },
551
563
  ...rest,
552
- children
564
+ children: label
565
+ }
566
+ );
567
+ }
568
+ var SR_ONLY = {
569
+ position: "absolute",
570
+ width: 1,
571
+ height: 1,
572
+ margin: -1,
573
+ padding: 0,
574
+ overflow: "hidden",
575
+ clip: "rect(0 0 0 0)",
576
+ whiteSpace: "nowrap",
577
+ border: 0
578
+ };
579
+ function SrOnly({ children }) {
580
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { style: SR_ONLY, children });
581
+ }
582
+ function useLocale(locale) {
583
+ const sf = useOptionalStorefront();
584
+ return locale ?? sf?.locale ?? "es-MX";
585
+ }
586
+ function pickAvailability({ availability, source, lowStockThreshold }) {
587
+ const opts = { lowStockThreshold: lowStockThreshold ?? null };
588
+ if (availability) return availability;
589
+ return source ? storefront.resolveAvailability(source, opts) : null;
590
+ }
591
+ function useAvailability(input) {
592
+ const locale = useLocale(input.locale);
593
+ const a = pickAvailability(input);
594
+ return react.useMemo(
595
+ () => a ? storefront.availabilityView(a, locale, { messages: input.messages ?? null }) : null,
596
+ // `a` is rebuilt from props each render; key by its content
597
+ [JSON.stringify(a), locale, input.messages]
598
+ );
599
+ }
600
+ function AvailabilityBadge({
601
+ availability,
602
+ source,
603
+ lowStockThreshold,
604
+ locale,
605
+ messages,
606
+ placement = "detail",
607
+ live,
608
+ ...rest
609
+ }) {
610
+ const view = useAvailability({ availability, source, lowStockThreshold, locale, messages });
611
+ const loc = useLocale(locale);
612
+ if (!view) return null;
613
+ if (placement === "card" && !view.showOnCard) return null;
614
+ const m = storefront.availabilityMessages(loc, messages);
615
+ return /* @__PURE__ */ jsxRuntime.jsxs(
616
+ "span",
617
+ {
618
+ "data-availability": view.status,
619
+ "data-tone": view.tone,
620
+ "data-placement": placement,
621
+ ...live ? { role: "status" } : {},
622
+ ...rest,
623
+ children: [
624
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": view.srText !== view.label ? true : void 0, children: view.label }),
625
+ view.srText !== view.label && /* @__PURE__ */ jsxRuntime.jsx(SrOnly, { children: view.srText }),
626
+ placement === "detail" && view.sublabel && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
627
+ " ",
628
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-availability-note": "eta", children: view.sublabel })
629
+ ] }),
630
+ placement === "detail" && view.maxPerOrder !== null && view.status !== "out_of_stock" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
631
+ " ",
632
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-availability-note": "max", children: storefront.formatAvailabilityMessage(m.max_per_order, { n: view.maxPerOrder }) })
633
+ ] })
634
+ ]
553
635
  }
554
636
  );
555
637
  }
638
+ function VariantOptionState({ children, availability, source, lowStockThreshold, locale, messages, ...rest }) {
639
+ const a = pickAvailability({ availability, source, lowStockThreshold });
640
+ const loc = useLocale(locale);
641
+ if (!a) return /* @__PURE__ */ jsxRuntime.jsx("span", { ...rest, children });
642
+ const m = storefront.availabilityMessages(loc, messages);
643
+ const status = storefront.availabilityView(a, loc, { messages: messages ?? null }).status;
644
+ if (status === "out_of_stock") {
645
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-availability": status, ...rest, children: [
646
+ /* @__PURE__ */ jsxRuntime.jsx("s", { children }),
647
+ /* @__PURE__ */ jsxRuntime.jsxs(SrOnly, { children: [
648
+ " (",
649
+ m["option.sold_out"],
650
+ ")"
651
+ ] })
652
+ ] });
653
+ }
654
+ if (status === "backorder" || status === "preorder") {
655
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-availability": status, ...rest, children: [
656
+ children,
657
+ " ",
658
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-availability-tag": true, children: [
659
+ "(",
660
+ m[status === "backorder" ? "option.backorder" : "option.preorder"],
661
+ ")"
662
+ ] })
663
+ ] });
664
+ }
665
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-availability": status, ...rest, children });
666
+ }
667
+ function RestockNotifyForm({
668
+ sku,
669
+ market,
670
+ allowPhone = false,
671
+ action,
672
+ client: clientProp,
673
+ enhance = true,
674
+ onSubscribed,
675
+ onError,
676
+ availability,
677
+ source,
678
+ lowStockThreshold,
679
+ locale,
680
+ messages,
681
+ ...rest
682
+ }) {
683
+ const sf = useOptionalStorefront();
684
+ const view = useAvailability({ availability, source, lowStockThreshold, locale, messages });
685
+ const loc = useLocale(locale);
686
+ const id = react.useId();
687
+ const [state, setState] = react.useState("idle");
688
+ if (!view?.showNotify) return null;
689
+ const m = storefront.availabilityMessages(loc, messages);
690
+ const client = clientProp ?? sf?.client ?? null;
691
+ if (state === "done") {
692
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", "data-restock": "done", children: m["notify.done"] });
693
+ }
694
+ const submit = enhance && client ? (e) => {
695
+ e.preventDefault();
696
+ const form = new FormData(e.currentTarget);
697
+ const email = String(form.get("email") ?? "").trim();
698
+ const phone = String(form.get("phone") ?? "").trim();
699
+ if (!email && !phone) {
700
+ setState("error");
701
+ return;
702
+ }
703
+ setState("sending");
704
+ client.restock.subscribe({ sku, locale: loc, consent: true, ...email ? { email } : {}, ...phone ? { phone } : {}, ...market ? { market } : {} }).then(() => {
705
+ setState("done");
706
+ onSubscribed?.();
707
+ }).catch((err) => {
708
+ setState("error");
709
+ onError?.(err);
710
+ });
711
+ } : void 0;
712
+ return /* @__PURE__ */ jsxRuntime.jsx("form", { method: "post", ...action !== void 0 ? { action } : {}, ...rest, onSubmit: submit, "data-restock": state, children: /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { children: [
713
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { children: m["notify.title"] }),
714
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: "sku", value: sku }),
715
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: "locale", value: loc }),
716
+ market && /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name: "market", value: market }),
717
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
718
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${id}-email`, children: m["notify.email"] }),
719
+ " ",
720
+ /* @__PURE__ */ jsxRuntime.jsx("input", { id: `${id}-email`, name: "email", type: "email", autoComplete: "email", required: !allowPhone })
721
+ ] }),
722
+ allowPhone && /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
723
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${id}-phone`, children: m["notify.phone"] }),
724
+ " ",
725
+ /* @__PURE__ */ jsxRuntime.jsx("input", { id: `${id}-phone`, name: "phone", type: "tel", autoComplete: "tel" })
726
+ ] }),
727
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
728
+ /* @__PURE__ */ jsxRuntime.jsx("input", { id: `${id}-consent`, name: "consent", type: "checkbox", value: "true", required: true }),
729
+ " ",
730
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: `${id}-consent`, children: m["notify.consent"] })
731
+ ] }),
732
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: state === "sending", "aria-busy": state === "sending" || void 0, children: m["notify.submit"] }),
733
+ state === "error" && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", children: m["notify.error"] })
734
+ ] }) });
735
+ }
736
+ function CartLineProblem({ line, locale, messages, onAdjust, onRemove, ...rest }) {
737
+ const cart = useOptionalCart();
738
+ const loc = useLocale(locale);
739
+ if (!line.problem) return null;
740
+ const view = storefront.cartLineProblemView(line.problem, loc, { messages: messages ?? null, lineQuantity: line.quantity });
741
+ const adjust = onAdjust ?? (cart ? (id, q) => cart.updateLines([{ id, quantity: q }]) : void 0);
742
+ const remove = onRemove ?? (cart ? (id) => cart.removeLines([id]) : void 0);
743
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cart-line-problem": view.code, ...rest, children: [
744
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: view.message }),
745
+ view.adjustTo !== null && adjust && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void adjust(line.id, view.adjustTo), children: view.adjustLabel }),
746
+ remove && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void remove(line.id), children: view.removeLabel })
747
+ ] });
748
+ }
749
+ function StockConflictAlert({
750
+ error: errorProp,
751
+ cart: cartProp,
752
+ locale,
753
+ messages,
754
+ onAdjust,
755
+ onRemove,
756
+ onDismiss,
757
+ autoFocus = true,
758
+ ...rest
759
+ }) {
760
+ const cartCtx = useOptionalCart();
761
+ const loc = useLocale(locale);
762
+ const ref = react.useRef(null);
763
+ const error = errorProp !== void 0 ? errorProp : cartCtx?.error ?? null;
764
+ const cart = cartProp !== void 0 ? cartProp : cartCtx?.cart ?? null;
765
+ const active = storefront.isStorefrontError(error) && error.code === "insufficient_stock";
766
+ react.useEffect(() => {
767
+ if (active && autoFocus) ref.current?.focus({ preventScroll: true });
768
+ }, [active, error, autoFocus]);
769
+ if (!active) return null;
770
+ const m = storefront.availabilityMessages(loc, messages);
771
+ const lines = error.stockLines;
772
+ const adjust = onAdjust ?? (cartCtx ? (id, q) => cartCtx.updateLines([{ id, quantity: q }]) : void 0);
773
+ const remove = onRemove ?? (cartCtx ? (id) => cartCtx.removeLines([id]) : void 0);
774
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, role: "alert", tabIndex: -1, "data-stock-conflict": true, ...rest, children: [
775
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("strong", { children: m["conflict.title"] }) }),
776
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: m["conflict.body"] }),
777
+ lines.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { children: lines.map((l) => {
778
+ const cartLine = cart?.lines.find((x) => l.line_id ? x.id === l.line_id : x.merchandise.sku === l.sku);
779
+ const lineId = l.line_id ?? cartLine?.id ?? null;
780
+ const view = storefront.cartLineProblemView(l, loc, { messages: messages ?? null, ...cartLine ? { lineQuantity: cartLine.quantity } : {} });
781
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { "data-cart-line-problem": view.code, children: [
782
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: cartLine ? `${cartLine.merchandise.product.title}${cartLine.merchandise.title ? ` (${cartLine.merchandise.title})` : ""}` : l.sku }),
783
+ ": ",
784
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: view.message }),
785
+ lineId && view.adjustTo !== null && adjust && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
786
+ " ",
787
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void adjust(lineId, view.adjustTo), children: view.adjustLabel })
788
+ ] }),
789
+ lineId && remove && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
790
+ " ",
791
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => void remove(lineId), children: view.removeLabel })
792
+ ] })
793
+ ] }, `${l.sku}:${l.line_id ?? ""}`);
794
+ }) }),
795
+ onDismiss && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onDismiss, children: m["conflict.dismiss"] })
796
+ ] });
797
+ }
556
798
  var clean = (s, max) => {
557
799
  if (!s) return void 0;
558
800
  const text = s.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
@@ -586,7 +828,8 @@ function seoMeta(input) {
586
828
  function seoOf(seo, fallbackTitle, fallbackDescription) {
587
829
  return { title: seo.title ?? fallbackTitle, description: seo.description ?? fallbackDescription ?? null };
588
830
  }
589
- function productMeta(product, shop, url) {
831
+ var OG_AVAILABILITY = { in_stock: "in stock", low_stock: "in stock", out_of_stock: "out of stock", backorder: "backorder", preorder: "preorder" };
832
+ function productMeta(product, shop, url, options = {}) {
590
833
  const { title, description } = seoOf(product.seo, product.title, product.description);
591
834
  const meta = seoMeta({
592
835
  title,
@@ -601,7 +844,7 @@ function productMeta(product, shop, url) {
601
844
  meta.push(
602
845
  { property: "product:price:amount", content: product.price_range.min.amount },
603
846
  { property: "product:price:currency", content: product.price_range.min.currency_code },
604
- { property: "product:availability", content: product.available_for_sale ? "in stock" : "out of stock" }
847
+ { property: "product:availability", content: OG_AVAILABILITY[storefront.productAvailability(product, options).status] ?? "out of stock" }
605
848
  );
606
849
  if (product.vendor) meta.push({ property: "product:brand", content: product.vendor });
607
850
  return meta;
@@ -638,22 +881,24 @@ function withVariant(url, variantId) {
638
881
  return `${url}${url.includes("?") ? "&" : "?"}variant=${encodeURIComponent(variantId)}`;
639
882
  }
640
883
  }
641
- function productJsonLd(product, shop, url) {
884
+ function productJsonLd(product, shop, url, options = {}) {
642
885
  const images = product.images.length ? product.images.map((i) => i.url) : product.featured_image ? [product.featured_image.url] : [];
643
- const offers = product.variants.map(
644
- (v) => compact({
886
+ const offers = product.variants.map((v) => {
887
+ const availability = storefront.resolveAvailability(v, options);
888
+ return compact({
645
889
  "@type": "Offer",
646
890
  sku: v.sku,
647
891
  gtin: v.barcode ?? void 0,
648
892
  name: product.variants.length > 1 ? `${product.title} - ${v.title}` : void 0,
649
893
  price: v.price.amount,
650
894
  priceCurrency: v.price.currency_code,
651
- availability: v.available_for_sale ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
895
+ availability: storefront.schemaAvailability(availability.status),
896
+ availabilityStarts: availability.status === "preorder" ? availability.preorder_release_at ?? void 0 : void 0,
652
897
  itemCondition: "https://schema.org/NewCondition",
653
898
  url: product.variants.length > 1 ? withVariant(url, v.id) : url,
654
899
  seller: shop ? { "@type": "Organization", name: shop.name } : void 0
655
- })
656
- );
900
+ });
901
+ });
657
902
  const additionalProperty = Object.entries(product.attributes).map(([name, value]) => ({ "@type": "PropertyValue", name, value }));
658
903
  return compact({
659
904
  "@context": CONTEXT,
@@ -765,6 +1010,156 @@ function JsonLd({ data, nonce }) {
765
1010
  if (!data || Array.isArray(data) && data.length === 0) return null;
766
1011
  return /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", nonce, dangerouslySetInnerHTML: { __html: serializeJsonLd(data) } });
767
1012
  }
1013
+ var OPS_BUILTIN_SECTION_TYPES = ["hero", "product_grid", "collection_list", "rich_text", "faq", "image_with_text"];
1014
+ var OPS_PROP_KINDS = ["text", "textarea", "richtext", "image", "link", "number", "select", "boolean", "collection", "collection_list", "product", "product_list", "list"];
1015
+ var SectionTypesValidationError = class extends Error {
1016
+ issues;
1017
+ constructor(issues) {
1018
+ super(`Invalid section types for OPS:
1019
+ ${issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n")}`);
1020
+ this.name = "SectionTypesValidationError";
1021
+ this.issues = issues;
1022
+ }
1023
+ };
1024
+ var SNAKE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
1025
+ var KIND_MAP = {
1026
+ text: "text",
1027
+ textarea: "textarea",
1028
+ richtext: "richtext",
1029
+ url: "text",
1030
+ color: "text",
1031
+ image: "image",
1032
+ number: "number",
1033
+ boolean: "boolean",
1034
+ select: "select",
1035
+ collection: "collection",
1036
+ product: "product",
1037
+ products: "product_list",
1038
+ list: "list"
1039
+ };
1040
+ var LIST_KINDS = /* @__PURE__ */ new Set(["list"]);
1041
+ function convertProps(schema, path, issues) {
1042
+ const out = [];
1043
+ for (const [key, spec] of Object.entries(schema ?? {})) {
1044
+ const p = `${path}.${key}`;
1045
+ const kind = KIND_MAP[spec.kind];
1046
+ if (!kind) {
1047
+ issues.push({ path: `${p}.kind`, message: `unknown kind "${String(spec.kind)}"` });
1048
+ continue;
1049
+ }
1050
+ const prop = { key, kind, label: spec.label };
1051
+ if (spec.required) prop.required = true;
1052
+ if ("default" in spec && spec.default !== void 0) prop.default = spec.default;
1053
+ if (spec.options) prop.options = spec.options.map((o) => ({ value: o.value, label: o.label }));
1054
+ if (spec.min !== void 0) prop.min = spec.min;
1055
+ if (spec.max !== void 0) prop.max = spec.max;
1056
+ if (LIST_KINDS.has(kind)) prop.fields = convertProps(spec.items, `${p}.fields`, issues);
1057
+ out.push(prop);
1058
+ }
1059
+ return out;
1060
+ }
1061
+ function validateOpsSectionTypes(types) {
1062
+ const issues = [];
1063
+ const seen = /* @__PURE__ */ new Set();
1064
+ const walk = (props, path) => {
1065
+ if (!Array.isArray(props)) {
1066
+ issues.push({ path, message: "must be a list" });
1067
+ return;
1068
+ }
1069
+ const keys = /* @__PURE__ */ new Set();
1070
+ props.forEach((prop, i) => {
1071
+ const p = `${path}[${i}]`;
1072
+ if (typeof prop.key !== "string" || !SNAKE.test(prop.key)) issues.push({ path: `${p}.key`, message: `"${String(prop.key)}" must be snake_case (a-z, 0-9, _)` });
1073
+ else if (keys.has(prop.key)) issues.push({ path: `${p}.key`, message: `duplicate key "${prop.key}"` });
1074
+ else keys.add(prop.key);
1075
+ if (typeof prop.label !== "string" || !prop.label.trim()) issues.push({ path: `${p}.label`, message: "label is required" });
1076
+ if (!OPS_PROP_KINDS.includes(prop.kind)) {
1077
+ issues.push({ path: `${p}.kind`, message: `unknown kind "${String(prop.kind)}"` });
1078
+ return;
1079
+ }
1080
+ if (prop.kind === "list") {
1081
+ if (!prop.fields || prop.fields.length === 0) issues.push({ path: `${p}.fields`, message: 'list requires a non-empty "fields"' });
1082
+ else walk(prop.fields, `${p}.fields`);
1083
+ }
1084
+ if (prop.kind === "select" && (!prop.options || prop.options.length === 0)) issues.push({ path: `${p}.options`, message: 'select requires non-empty "options"' });
1085
+ if (prop.min !== void 0 && prop.max !== void 0 && prop.min > prop.max) issues.push({ path: `${p}.max`, message: "max must be >= min" });
1086
+ });
1087
+ };
1088
+ types.forEach((t, i) => {
1089
+ const p = t.type ? t.type : `[${i}]`;
1090
+ if (!t.type || !t.type.trim()) issues.push({ path: `${p}.type`, message: "type is required" });
1091
+ else if (seen.has(t.type)) issues.push({ path: `${p}.type`, message: `duplicate type "${t.type}"` });
1092
+ else seen.add(t.type);
1093
+ if (!t.label || !t.label.trim()) issues.push({ path: `${p}.label`, message: "label is required" });
1094
+ if (t.max_per_page !== void 0 && (!Number.isInteger(t.max_per_page) || t.max_per_page < 1)) issues.push({ path: `${p}.max_per_page`, message: "must be a positive integer" });
1095
+ walk(t.schema?.props, `${p}.schema.props`);
1096
+ });
1097
+ return issues;
1098
+ }
1099
+ function sectionTypesToOps(source, options = {}) {
1100
+ const excludeBuiltin = options.excludeBuiltin ?? true;
1101
+ const defs = Array.isArray(source) ? source : source.definitions();
1102
+ const builtin = new Set(OPS_BUILTIN_SECTION_TYPES);
1103
+ const issues = [];
1104
+ const warnings = [];
1105
+ const types = [];
1106
+ for (const def of defs) {
1107
+ if (excludeBuiltin && builtin.has(def.type)) {
1108
+ const known = options.builtinProps?.[def.type];
1109
+ if (known) {
1110
+ for (const name of Object.keys(def.schema ?? {})) {
1111
+ if (!known.includes(name)) warnings.push({ type: def.type, prop: name, message: `prop "${name}" of built-in type "${def.type}" does not exist in the OPS schema; built-in types cannot be redefined, so it will not be editable in OPS` });
1112
+ }
1113
+ } else {
1114
+ warnings.push({ type: def.type, message: `"${def.type}" is a built-in OPS type: not synced, and its local props cannot be verified against the OPS schema` });
1115
+ }
1116
+ continue;
1117
+ }
1118
+ const entryIssues = [];
1119
+ const props = convertProps(def.schema, `${def.type}.schema.props`, entryIssues);
1120
+ issues.push(...entryIssues);
1121
+ const out = { type: def.type, label: def.label ?? def.type, schema: { props } };
1122
+ if (def.description) out.description = def.description;
1123
+ if (def.category) out.category = def.category;
1124
+ if (def.maxPerPage !== void 0) out.max_per_page = def.maxPerPage;
1125
+ types.push(out);
1126
+ }
1127
+ issues.push(...validateOpsSectionTypes(types));
1128
+ if (issues.length) throw new SectionTypesValidationError(issues);
1129
+ return { types, warnings };
1130
+ }
1131
+ var SYNC_PATH = "/storefront/content/section-types";
1132
+ var count = (v) => Array.isArray(v) ? v.length : typeof v === "number" ? v : 0;
1133
+ async function syncSectionTypes(target, source, options = {}) {
1134
+ const { types, warnings } = sectionTypesToOps(source, options);
1135
+ let raw;
1136
+ if ("request" in target && typeof target.request === "function") {
1137
+ raw = await target.request({ method: "PUT", path: SYNC_PATH, body: { section_types: types } });
1138
+ } else {
1139
+ const t = target;
1140
+ if (!t.token.startsWith("sk_")) throw new TypeError("syncSectionTypes: an sk_ token with content:write is required");
1141
+ const f = t.fetch ?? globalThis.fetch.bind(globalThis);
1142
+ const url = t.baseUrl.replace(/\/+$/, "") + SYNC_PATH;
1143
+ const res = await f(url, {
1144
+ method: "PUT",
1145
+ headers: { Authorization: `Bearer ${t.token}`, "Content-Type": "application/json", Accept: "application/json" },
1146
+ body: JSON.stringify({ section_types: types })
1147
+ });
1148
+ const text = await res.text();
1149
+ let body;
1150
+ try {
1151
+ body = text ? JSON.parse(text) : void 0;
1152
+ } catch {
1153
+ body = void 0;
1154
+ }
1155
+ if (!res.ok) {
1156
+ throw new storefront.StorefrontError({ status: res.status, code: body?.code ?? body?.error?.code ?? "unknown", message: body?.message ?? body?.error?.message ?? `Section types sync failed (${res.status})`, url });
1157
+ }
1158
+ raw = body;
1159
+ }
1160
+ const data = raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
1161
+ return { created: count(data?.created), updated: count(data?.updated), warnings };
1162
+ }
768
1163
  function defineSection(definition) {
769
1164
  return definition;
770
1165
  }
@@ -788,6 +1183,9 @@ function resolveSectionProps(schema, raw, path = "") {
788
1183
  else if (spec.required) errors.push({ prop: key, message: "is required" });
789
1184
  continue;
790
1185
  }
1186
+ if (spec.kind === "image" && typeof value === "object" && value !== null && typeof value.url === "string") {
1187
+ value = value.url;
1188
+ }
791
1189
  if (STRING_KINDS.has(spec.kind)) {
792
1190
  if (typeof value !== "string") {
793
1191
  fail2("must be a string");
@@ -911,6 +1309,14 @@ var SectionRegistry = class {
911
1309
  toJSONSchema() {
912
1310
  return Object.fromEntries(this.definitions().map((d) => [d.type, sectionJsonSchema(d)]));
913
1311
  }
1312
+ /**
1313
+ * Section types in the OPS `storefront_content` format, ready for
1314
+ * `PUT /storefront/content/section-types`. Throws `SectionTypesValidationError`
1315
+ * on anything OPS would reject; `warnings` flags built-in types.
1316
+ */
1317
+ toOpsSectionTypes(options) {
1318
+ return sectionTypesToOps(this, options);
1319
+ }
914
1320
  };
915
1321
  function createSectionRegistry(definitions = []) {
916
1322
  return new SectionRegistry(definitions);
@@ -1339,7 +1745,9 @@ function NearestLocationForm({
1339
1745
  }
1340
1746
 
1341
1747
  exports.AddToCartButton = AddToCartButton;
1748
+ exports.AvailabilityBadge = AvailabilityBadge;
1342
1749
  exports.CART_COOKIE_NAME = CART_COOKIE_NAME;
1750
+ exports.CartLineProblem = CartLineProblem;
1343
1751
  exports.CartProvider = CartProvider;
1344
1752
  exports.DEFAULT_IMAGE_WIDTHS = DEFAULT_IMAGE_WIDTHS;
1345
1753
  exports.DirectionsLink = DirectionsLink;
@@ -1349,15 +1757,22 @@ exports.LocationList = LocationList;
1349
1757
  exports.LocationPicker = LocationPicker;
1350
1758
  exports.Money = Money;
1351
1759
  exports.NearestLocationForm = NearestLocationForm;
1760
+ exports.OPS_BUILTIN_SECTION_TYPES = OPS_BUILTIN_SECTION_TYPES;
1761
+ exports.OPS_PROP_KINDS = OPS_PROP_KINDS;
1352
1762
  exports.OpenStatus = OpenStatus;
1353
1763
  exports.PickupOption = PickupOption;
1354
1764
  exports.ProductPrice = ProductPrice;
1765
+ exports.RestockNotifyForm = RestockNotifyForm;
1355
1766
  exports.SELECTED_LOCATION_PARAM = SELECTED_LOCATION_PARAM;
1356
1767
  exports.SELECTED_LOCATION_STORAGE_KEY = SELECTED_LOCATION_STORAGE_KEY;
1357
1768
  exports.SectionRegistry = SectionRegistry;
1769
+ exports.SectionTypesValidationError = SectionTypesValidationError;
1358
1770
  exports.Sections = Sections;
1771
+ exports.SrOnly = SrOnly;
1772
+ exports.StockConflictAlert = StockConflictAlert;
1359
1773
  exports.StorefrontProvider = StorefrontProvider;
1360
1774
  exports.ThemeStyle = ThemeStyle;
1775
+ exports.VariantOptionState = VariantOptionState;
1361
1776
  exports.VariantSelector = VariantSelector;
1362
1777
  exports.breadcrumbJsonLd = breadcrumbJsonLd;
1363
1778
  exports.collectionJsonLd = collectionJsonLd;
@@ -1378,14 +1793,17 @@ exports.readCartCookie = readCartCookie;
1378
1793
  exports.readSelectedLocationId = readSelectedLocationId;
1379
1794
  exports.resolveSectionProps = resolveSectionProps;
1380
1795
  exports.sectionJsonSchema = sectionJsonSchema;
1796
+ exports.sectionTypesToOps = sectionTypesToOps;
1381
1797
  exports.seoMeta = seoMeta;
1382
1798
  exports.serializeCartCookie = serializeCartCookie;
1383
1799
  exports.serializeJsonLd = serializeJsonLd;
1800
+ exports.syncSectionTypes = syncSectionTypes;
1384
1801
  exports.themeFontsHref = themeFontsHref;
1385
1802
  exports.themeToCss = themeToCss;
1386
1803
  exports.themeToCssVariables = themeToCssVariables;
1387
1804
  exports.toMerchandise = toMerchandise;
1388
1805
  exports.tokenName = tokenName;
1806
+ exports.useAvailability = useAvailability;
1389
1807
  exports.useCart = useCart;
1390
1808
  exports.useGeolocation = useGeolocation;
1391
1809
  exports.useNearestLocations = useNearestLocations;
@@ -1395,6 +1813,7 @@ exports.useProductOptions = useProductOptions;
1395
1813
  exports.useSelectedLocation = useSelectedLocation;
1396
1814
  exports.useStorefront = useStorefront;
1397
1815
  exports.useStorefrontClient = useStorefrontClient;
1816
+ exports.validateOpsSectionTypes = validateOpsSectionTypes;
1398
1817
  exports.writeCartCookie = writeCartCookie;
1399
1818
  //# sourceMappingURL=index.cjs.map
1400
1819
  //# sourceMappingURL=index.cjs.map