@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.js CHANGED
@@ -1,7 +1,9 @@
1
- import { useOptionalStorefront } from './chunk-C6QZNKRE.js';
2
- export { DEFAULT_IMAGE_WIDTHS, Image, Money, ProductPrice, StorefrontProvider, defaultImageLoader, useOptionalStorefront, useStorefront, useStorefrontClient } from './chunk-C6QZNKRE.js';
1
+ export { DEFAULT_IMAGE_WIDTHS, Image, Money, ProductPrice, defaultImageLoader } from './chunk-6CVKW7F3.js';
2
+ export { ThemeStyle, isSafeCssValue, themeFontsHref, themeToCss, themeToCssVariables, tokenName } from './chunk-672OTUM2.js';
3
+ import { useOptionalStorefront } from './chunk-DQBL2DFJ.js';
4
+ export { StorefrontProvider, useOptionalStorefront, useStorefront, useStorefrontClient } from './chunk-DQBL2DFJ.js';
3
5
  import { createContext, useRef, useState, useCallback, useEffect, useMemo, useContext, Fragment as Fragment$1, useId } from 'react';
4
- import { formatMoney, isStorefrontError } from '@asteby/storefront';
6
+ import { resolveAvailability, productAvailability, availabilityView, availabilityMessages, formatAvailabilityMessage, cartLineProblemView, isStorefrontError, schemaAvailability, StorefrontError, formatMoney } from '@asteby/storefront';
5
7
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
6
8
  import { toSchemaOpeningHours, isValidPoint, rankByDistance, openStatus, directionsUrl } from '@asteby/storefront/geo';
7
9
 
@@ -347,6 +349,7 @@ function useProductOptions(product, config = {}) {
347
349
  exists: exact ? true : loose.length > 0 && Object.keys(selectedOptions).length < product.options.length,
348
350
  available: exact ? exact.available_for_sale : Object.keys(selectedOptions).length < product.options.length && loose.some((v) => v.available_for_sale),
349
351
  variant: exact,
352
+ availability: exact ? resolveAvailability(exact) : loose.length ? productAvailability({ variants: loose, available_for_sale: loose.some((v) => v.available_for_sale) }) : null,
350
353
  search: `?${search.toString()}`
351
354
  };
352
355
  })
@@ -365,21 +368,32 @@ function toMerchandise(product, variant) {
365
368
  function AddToCartButton({
366
369
  variantId,
367
370
  quantity = 1,
371
+ availability,
372
+ availabilitySource,
373
+ lowStockThreshold,
374
+ locale,
368
375
  merchandise,
369
376
  onAdded,
370
377
  onClick,
371
378
  disabled,
372
- children = "Agregar al carrito",
379
+ children,
373
380
  type = "button",
374
381
  ...rest
375
382
  }) {
376
383
  const cart = useCart();
384
+ const sf = useOptionalStorefront();
385
+ const resolved = availability ?? (availabilitySource ? resolveAvailability(availabilitySource, { lowStockThreshold: lowStockThreshold ?? null }) : null);
386
+ const view = resolved ? availabilityView(resolved, locale ?? sf?.locale ?? "es-MX") : null;
387
+ const blocked = view ? !view.ctaEnabled : false;
388
+ const qty = view?.maxPerOrder != null ? Math.min(quantity, view.maxPerOrder) : quantity;
389
+ const label = children ?? view?.ctaLabel ?? "Agregar al carrito";
377
390
  const [adding, setAdding] = useState(false);
378
391
  return /* @__PURE__ */ jsx(
379
392
  "button",
380
393
  {
381
394
  type,
382
- disabled: disabled || !variantId || adding,
395
+ disabled: disabled || !variantId || adding || blocked,
396
+ "data-availability": view?.status,
383
397
  "aria-busy": adding || void 0,
384
398
  "data-adding": adding || void 0,
385
399
  onClick: async (event) => {
@@ -387,17 +401,247 @@ function AddToCartButton({
387
401
  if (event.defaultPrevented || !variantId) return;
388
402
  setAdding(true);
389
403
  try {
390
- const result = await cart.addLines([{ variant_id: variantId, quantity, ...merchandise ? { merchandise } : {} }]);
404
+ const result = await cart.addLines([{ variant_id: variantId, quantity: qty, ...merchandise ? { merchandise } : {} }]);
391
405
  if (result) onAdded?.();
392
406
  } finally {
393
407
  setAdding(false);
394
408
  }
395
409
  },
396
410
  ...rest,
397
- children
411
+ children: label
398
412
  }
399
413
  );
400
414
  }
415
+ var SR_ONLY = {
416
+ position: "absolute",
417
+ width: 1,
418
+ height: 1,
419
+ margin: -1,
420
+ padding: 0,
421
+ overflow: "hidden",
422
+ clip: "rect(0 0 0 0)",
423
+ whiteSpace: "nowrap",
424
+ border: 0
425
+ };
426
+ function SrOnly({ children }) {
427
+ return /* @__PURE__ */ jsx("span", { style: SR_ONLY, children });
428
+ }
429
+ function useLocale(locale) {
430
+ const sf = useOptionalStorefront();
431
+ return locale ?? sf?.locale ?? "es-MX";
432
+ }
433
+ function pickAvailability({ availability, source, lowStockThreshold }) {
434
+ const opts = { lowStockThreshold: lowStockThreshold ?? null };
435
+ if (availability) return availability;
436
+ return source ? resolveAvailability(source, opts) : null;
437
+ }
438
+ function useAvailability(input) {
439
+ const locale = useLocale(input.locale);
440
+ const a = pickAvailability(input);
441
+ return useMemo(
442
+ () => a ? availabilityView(a, locale, { messages: input.messages ?? null }) : null,
443
+ // `a` is rebuilt from props each render; key by its content
444
+ [JSON.stringify(a), locale, input.messages]
445
+ );
446
+ }
447
+ function AvailabilityBadge({
448
+ availability,
449
+ source,
450
+ lowStockThreshold,
451
+ locale,
452
+ messages,
453
+ placement = "detail",
454
+ live,
455
+ ...rest
456
+ }) {
457
+ const view = useAvailability({ availability, source, lowStockThreshold, locale, messages });
458
+ const loc = useLocale(locale);
459
+ if (!view) return null;
460
+ if (placement === "card" && !view.showOnCard) return null;
461
+ const m = availabilityMessages(loc, messages);
462
+ return /* @__PURE__ */ jsxs(
463
+ "span",
464
+ {
465
+ "data-availability": view.status,
466
+ "data-tone": view.tone,
467
+ "data-placement": placement,
468
+ ...live ? { role: "status" } : {},
469
+ ...rest,
470
+ children: [
471
+ /* @__PURE__ */ jsx("span", { "aria-hidden": view.srText !== view.label ? true : void 0, children: view.label }),
472
+ view.srText !== view.label && /* @__PURE__ */ jsx(SrOnly, { children: view.srText }),
473
+ placement === "detail" && view.sublabel && /* @__PURE__ */ jsxs(Fragment, { children: [
474
+ " ",
475
+ /* @__PURE__ */ jsx("span", { "data-availability-note": "eta", children: view.sublabel })
476
+ ] }),
477
+ placement === "detail" && view.maxPerOrder !== null && view.status !== "out_of_stock" && /* @__PURE__ */ jsxs(Fragment, { children: [
478
+ " ",
479
+ /* @__PURE__ */ jsx("span", { "data-availability-note": "max", children: formatAvailabilityMessage(m.max_per_order, { n: view.maxPerOrder }) })
480
+ ] })
481
+ ]
482
+ }
483
+ );
484
+ }
485
+ function VariantOptionState({ children, availability, source, lowStockThreshold, locale, messages, ...rest }) {
486
+ const a = pickAvailability({ availability, source, lowStockThreshold });
487
+ const loc = useLocale(locale);
488
+ if (!a) return /* @__PURE__ */ jsx("span", { ...rest, children });
489
+ const m = availabilityMessages(loc, messages);
490
+ const status = availabilityView(a, loc, { messages: messages ?? null }).status;
491
+ if (status === "out_of_stock") {
492
+ return /* @__PURE__ */ jsxs("span", { "data-availability": status, ...rest, children: [
493
+ /* @__PURE__ */ jsx("s", { children }),
494
+ /* @__PURE__ */ jsxs(SrOnly, { children: [
495
+ " (",
496
+ m["option.sold_out"],
497
+ ")"
498
+ ] })
499
+ ] });
500
+ }
501
+ if (status === "backorder" || status === "preorder") {
502
+ return /* @__PURE__ */ jsxs("span", { "data-availability": status, ...rest, children: [
503
+ children,
504
+ " ",
505
+ /* @__PURE__ */ jsxs("span", { "data-availability-tag": true, children: [
506
+ "(",
507
+ m[status === "backorder" ? "option.backorder" : "option.preorder"],
508
+ ")"
509
+ ] })
510
+ ] });
511
+ }
512
+ return /* @__PURE__ */ jsx("span", { "data-availability": status, ...rest, children });
513
+ }
514
+ function RestockNotifyForm({
515
+ sku,
516
+ market,
517
+ allowPhone = false,
518
+ action,
519
+ client: clientProp,
520
+ enhance = true,
521
+ onSubscribed,
522
+ onError,
523
+ availability,
524
+ source,
525
+ lowStockThreshold,
526
+ locale,
527
+ messages,
528
+ ...rest
529
+ }) {
530
+ const sf = useOptionalStorefront();
531
+ const view = useAvailability({ availability, source, lowStockThreshold, locale, messages });
532
+ const loc = useLocale(locale);
533
+ const id = useId();
534
+ const [state, setState] = useState("idle");
535
+ if (!view?.showNotify) return null;
536
+ const m = availabilityMessages(loc, messages);
537
+ const client = clientProp ?? sf?.client ?? null;
538
+ if (state === "done") {
539
+ return /* @__PURE__ */ jsx("p", { role: "status", "data-restock": "done", children: m["notify.done"] });
540
+ }
541
+ const submit = enhance && client ? (e) => {
542
+ e.preventDefault();
543
+ const form = new FormData(e.currentTarget);
544
+ const email = String(form.get("email") ?? "").trim();
545
+ const phone = String(form.get("phone") ?? "").trim();
546
+ if (!email && !phone) {
547
+ setState("error");
548
+ return;
549
+ }
550
+ setState("sending");
551
+ client.restock.subscribe({ sku, locale: loc, consent: true, ...email ? { email } : {}, ...phone ? { phone } : {}, ...market ? { market } : {} }).then(() => {
552
+ setState("done");
553
+ onSubscribed?.();
554
+ }).catch((err) => {
555
+ setState("error");
556
+ onError?.(err);
557
+ });
558
+ } : void 0;
559
+ return /* @__PURE__ */ jsx("form", { method: "post", ...action !== void 0 ? { action } : {}, ...rest, onSubmit: submit, "data-restock": state, children: /* @__PURE__ */ jsxs("fieldset", { children: [
560
+ /* @__PURE__ */ jsx("legend", { children: m["notify.title"] }),
561
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "sku", value: sku }),
562
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "locale", value: loc }),
563
+ market && /* @__PURE__ */ jsx("input", { type: "hidden", name: "market", value: market }),
564
+ /* @__PURE__ */ jsxs("p", { children: [
565
+ /* @__PURE__ */ jsx("label", { htmlFor: `${id}-email`, children: m["notify.email"] }),
566
+ " ",
567
+ /* @__PURE__ */ jsx("input", { id: `${id}-email`, name: "email", type: "email", autoComplete: "email", required: !allowPhone })
568
+ ] }),
569
+ allowPhone && /* @__PURE__ */ jsxs("p", { children: [
570
+ /* @__PURE__ */ jsx("label", { htmlFor: `${id}-phone`, children: m["notify.phone"] }),
571
+ " ",
572
+ /* @__PURE__ */ jsx("input", { id: `${id}-phone`, name: "phone", type: "tel", autoComplete: "tel" })
573
+ ] }),
574
+ /* @__PURE__ */ jsxs("p", { children: [
575
+ /* @__PURE__ */ jsx("input", { id: `${id}-consent`, name: "consent", type: "checkbox", value: "true", required: true }),
576
+ " ",
577
+ /* @__PURE__ */ jsx("label", { htmlFor: `${id}-consent`, children: m["notify.consent"] })
578
+ ] }),
579
+ /* @__PURE__ */ jsx("button", { type: "submit", disabled: state === "sending", "aria-busy": state === "sending" || void 0, children: m["notify.submit"] }),
580
+ state === "error" && /* @__PURE__ */ jsx("p", { role: "alert", children: m["notify.error"] })
581
+ ] }) });
582
+ }
583
+ function CartLineProblem({ line, locale, messages, onAdjust, onRemove, ...rest }) {
584
+ const cart = useOptionalCart();
585
+ const loc = useLocale(locale);
586
+ if (!line.problem) return null;
587
+ const view = cartLineProblemView(line.problem, loc, { messages: messages ?? null, lineQuantity: line.quantity });
588
+ const adjust = onAdjust ?? (cart ? (id, q) => cart.updateLines([{ id, quantity: q }]) : void 0);
589
+ const remove = onRemove ?? (cart ? (id) => cart.removeLines([id]) : void 0);
590
+ return /* @__PURE__ */ jsxs("div", { "data-cart-line-problem": view.code, ...rest, children: [
591
+ /* @__PURE__ */ jsx("p", { children: view.message }),
592
+ view.adjustTo !== null && adjust && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void adjust(line.id, view.adjustTo), children: view.adjustLabel }),
593
+ remove && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(line.id), children: view.removeLabel })
594
+ ] });
595
+ }
596
+ function StockConflictAlert({
597
+ error: errorProp,
598
+ cart: cartProp,
599
+ locale,
600
+ messages,
601
+ onAdjust,
602
+ onRemove,
603
+ onDismiss,
604
+ autoFocus = true,
605
+ ...rest
606
+ }) {
607
+ const cartCtx = useOptionalCart();
608
+ const loc = useLocale(locale);
609
+ const ref = useRef(null);
610
+ const error = errorProp !== void 0 ? errorProp : cartCtx?.error ?? null;
611
+ const cart = cartProp !== void 0 ? cartProp : cartCtx?.cart ?? null;
612
+ const active = isStorefrontError(error) && error.code === "insufficient_stock";
613
+ useEffect(() => {
614
+ if (active && autoFocus) ref.current?.focus({ preventScroll: true });
615
+ }, [active, error, autoFocus]);
616
+ if (!active) return null;
617
+ const m = availabilityMessages(loc, messages);
618
+ const lines = error.stockLines;
619
+ const adjust = onAdjust ?? (cartCtx ? (id, q) => cartCtx.updateLines([{ id, quantity: q }]) : void 0);
620
+ const remove = onRemove ?? (cartCtx ? (id) => cartCtx.removeLines([id]) : void 0);
621
+ return /* @__PURE__ */ jsxs("div", { ref, role: "alert", tabIndex: -1, "data-stock-conflict": true, ...rest, children: [
622
+ /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("strong", { children: m["conflict.title"] }) }),
623
+ /* @__PURE__ */ jsx("p", { children: m["conflict.body"] }),
624
+ lines.length > 0 && /* @__PURE__ */ jsx("ul", { children: lines.map((l) => {
625
+ const cartLine = cart?.lines.find((x) => l.line_id ? x.id === l.line_id : x.merchandise.sku === l.sku);
626
+ const lineId = l.line_id ?? cartLine?.id ?? null;
627
+ const view = cartLineProblemView(l, loc, { messages: messages ?? null, ...cartLine ? { lineQuantity: cartLine.quantity } : {} });
628
+ return /* @__PURE__ */ jsxs("li", { "data-cart-line-problem": view.code, children: [
629
+ /* @__PURE__ */ jsx("span", { children: cartLine ? `${cartLine.merchandise.product.title}${cartLine.merchandise.title ? ` (${cartLine.merchandise.title})` : ""}` : l.sku }),
630
+ ": ",
631
+ /* @__PURE__ */ jsx("span", { children: view.message }),
632
+ lineId && view.adjustTo !== null && adjust && /* @__PURE__ */ jsxs(Fragment, { children: [
633
+ " ",
634
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void adjust(lineId, view.adjustTo), children: view.adjustLabel })
635
+ ] }),
636
+ lineId && remove && /* @__PURE__ */ jsxs(Fragment, { children: [
637
+ " ",
638
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(lineId), children: view.removeLabel })
639
+ ] })
640
+ ] }, `${l.sku}:${l.line_id ?? ""}`);
641
+ }) }),
642
+ onDismiss && /* @__PURE__ */ jsx("button", { type: "button", onClick: onDismiss, children: m["conflict.dismiss"] })
643
+ ] });
644
+ }
401
645
  var clean = (s, max) => {
402
646
  if (!s) return void 0;
403
647
  const text = s.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
@@ -431,7 +675,8 @@ function seoMeta(input) {
431
675
  function seoOf(seo, fallbackTitle, fallbackDescription) {
432
676
  return { title: seo.title ?? fallbackTitle, description: seo.description ?? fallbackDescription ?? null };
433
677
  }
434
- function productMeta(product, shop, url) {
678
+ var OG_AVAILABILITY = { in_stock: "in stock", low_stock: "in stock", out_of_stock: "out of stock", backorder: "backorder", preorder: "preorder" };
679
+ function productMeta(product, shop, url, options = {}) {
435
680
  const { title, description } = seoOf(product.seo, product.title, product.description);
436
681
  const meta = seoMeta({
437
682
  title,
@@ -446,7 +691,7 @@ function productMeta(product, shop, url) {
446
691
  meta.push(
447
692
  { property: "product:price:amount", content: product.price_range.min.amount },
448
693
  { property: "product:price:currency", content: product.price_range.min.currency_code },
449
- { property: "product:availability", content: product.available_for_sale ? "in stock" : "out of stock" }
694
+ { property: "product:availability", content: OG_AVAILABILITY[productAvailability(product, options).status] ?? "out of stock" }
450
695
  );
451
696
  if (product.vendor) meta.push({ property: "product:brand", content: product.vendor });
452
697
  return meta;
@@ -483,22 +728,24 @@ function withVariant(url, variantId) {
483
728
  return `${url}${url.includes("?") ? "&" : "?"}variant=${encodeURIComponent(variantId)}`;
484
729
  }
485
730
  }
486
- function productJsonLd(product, shop, url) {
731
+ function productJsonLd(product, shop, url, options = {}) {
487
732
  const images = product.images.length ? product.images.map((i) => i.url) : product.featured_image ? [product.featured_image.url] : [];
488
- const offers = product.variants.map(
489
- (v) => compact({
733
+ const offers = product.variants.map((v) => {
734
+ const availability = resolveAvailability(v, options);
735
+ return compact({
490
736
  "@type": "Offer",
491
737
  sku: v.sku,
492
738
  gtin: v.barcode ?? void 0,
493
739
  name: product.variants.length > 1 ? `${product.title} - ${v.title}` : void 0,
494
740
  price: v.price.amount,
495
741
  priceCurrency: v.price.currency_code,
496
- availability: v.available_for_sale ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
742
+ availability: schemaAvailability(availability.status),
743
+ availabilityStarts: availability.status === "preorder" ? availability.preorder_release_at ?? void 0 : void 0,
497
744
  itemCondition: "https://schema.org/NewCondition",
498
745
  url: product.variants.length > 1 ? withVariant(url, v.id) : url,
499
746
  seller: shop ? { "@type": "Organization", name: shop.name } : void 0
500
- })
501
- );
747
+ });
748
+ });
502
749
  const additionalProperty = Object.entries(product.attributes).map(([name, value]) => ({ "@type": "PropertyValue", name, value }));
503
750
  return compact({
504
751
  "@context": CONTEXT,
@@ -610,6 +857,156 @@ function JsonLd({ data, nonce }) {
610
857
  if (!data || Array.isArray(data) && data.length === 0) return null;
611
858
  return /* @__PURE__ */ jsx("script", { type: "application/ld+json", nonce, dangerouslySetInnerHTML: { __html: serializeJsonLd(data) } });
612
859
  }
860
+ var OPS_BUILTIN_SECTION_TYPES = ["hero", "product_grid", "collection_list", "rich_text", "faq", "image_with_text"];
861
+ var OPS_PROP_KINDS = ["text", "textarea", "richtext", "image", "link", "number", "select", "boolean", "collection", "collection_list", "product", "product_list", "list"];
862
+ var SectionTypesValidationError = class extends Error {
863
+ issues;
864
+ constructor(issues) {
865
+ super(`Invalid section types for OPS:
866
+ ${issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n")}`);
867
+ this.name = "SectionTypesValidationError";
868
+ this.issues = issues;
869
+ }
870
+ };
871
+ var SNAKE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
872
+ var KIND_MAP = {
873
+ text: "text",
874
+ textarea: "textarea",
875
+ richtext: "richtext",
876
+ url: "text",
877
+ color: "text",
878
+ image: "image",
879
+ number: "number",
880
+ boolean: "boolean",
881
+ select: "select",
882
+ collection: "collection",
883
+ product: "product",
884
+ products: "product_list",
885
+ list: "list"
886
+ };
887
+ var LIST_KINDS = /* @__PURE__ */ new Set(["list"]);
888
+ function convertProps(schema, path, issues) {
889
+ const out = [];
890
+ for (const [key, spec] of Object.entries(schema ?? {})) {
891
+ const p = `${path}.${key}`;
892
+ const kind = KIND_MAP[spec.kind];
893
+ if (!kind) {
894
+ issues.push({ path: `${p}.kind`, message: `unknown kind "${String(spec.kind)}"` });
895
+ continue;
896
+ }
897
+ const prop = { key, kind, label: spec.label };
898
+ if (spec.required) prop.required = true;
899
+ if ("default" in spec && spec.default !== void 0) prop.default = spec.default;
900
+ if (spec.options) prop.options = spec.options.map((o) => ({ value: o.value, label: o.label }));
901
+ if (spec.min !== void 0) prop.min = spec.min;
902
+ if (spec.max !== void 0) prop.max = spec.max;
903
+ if (LIST_KINDS.has(kind)) prop.fields = convertProps(spec.items, `${p}.fields`, issues);
904
+ out.push(prop);
905
+ }
906
+ return out;
907
+ }
908
+ function validateOpsSectionTypes(types) {
909
+ const issues = [];
910
+ const seen = /* @__PURE__ */ new Set();
911
+ const walk = (props, path) => {
912
+ if (!Array.isArray(props)) {
913
+ issues.push({ path, message: "must be a list" });
914
+ return;
915
+ }
916
+ const keys = /* @__PURE__ */ new Set();
917
+ props.forEach((prop, i) => {
918
+ const p = `${path}[${i}]`;
919
+ 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, _)` });
920
+ else if (keys.has(prop.key)) issues.push({ path: `${p}.key`, message: `duplicate key "${prop.key}"` });
921
+ else keys.add(prop.key);
922
+ if (typeof prop.label !== "string" || !prop.label.trim()) issues.push({ path: `${p}.label`, message: "label is required" });
923
+ if (!OPS_PROP_KINDS.includes(prop.kind)) {
924
+ issues.push({ path: `${p}.kind`, message: `unknown kind "${String(prop.kind)}"` });
925
+ return;
926
+ }
927
+ if (prop.kind === "list") {
928
+ if (!prop.fields || prop.fields.length === 0) issues.push({ path: `${p}.fields`, message: 'list requires a non-empty "fields"' });
929
+ else walk(prop.fields, `${p}.fields`);
930
+ }
931
+ if (prop.kind === "select" && (!prop.options || prop.options.length === 0)) issues.push({ path: `${p}.options`, message: 'select requires non-empty "options"' });
932
+ if (prop.min !== void 0 && prop.max !== void 0 && prop.min > prop.max) issues.push({ path: `${p}.max`, message: "max must be >= min" });
933
+ });
934
+ };
935
+ types.forEach((t, i) => {
936
+ const p = t.type ? t.type : `[${i}]`;
937
+ if (!t.type || !t.type.trim()) issues.push({ path: `${p}.type`, message: "type is required" });
938
+ else if (seen.has(t.type)) issues.push({ path: `${p}.type`, message: `duplicate type "${t.type}"` });
939
+ else seen.add(t.type);
940
+ if (!t.label || !t.label.trim()) issues.push({ path: `${p}.label`, message: "label is required" });
941
+ 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" });
942
+ walk(t.schema?.props, `${p}.schema.props`);
943
+ });
944
+ return issues;
945
+ }
946
+ function sectionTypesToOps(source, options = {}) {
947
+ const excludeBuiltin = options.excludeBuiltin ?? true;
948
+ const defs = Array.isArray(source) ? source : source.definitions();
949
+ const builtin = new Set(OPS_BUILTIN_SECTION_TYPES);
950
+ const issues = [];
951
+ const warnings = [];
952
+ const types = [];
953
+ for (const def of defs) {
954
+ if (excludeBuiltin && builtin.has(def.type)) {
955
+ const known = options.builtinProps?.[def.type];
956
+ if (known) {
957
+ for (const name of Object.keys(def.schema ?? {})) {
958
+ 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` });
959
+ }
960
+ } else {
961
+ 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` });
962
+ }
963
+ continue;
964
+ }
965
+ const entryIssues = [];
966
+ const props = convertProps(def.schema, `${def.type}.schema.props`, entryIssues);
967
+ issues.push(...entryIssues);
968
+ const out = { type: def.type, label: def.label ?? def.type, schema: { props } };
969
+ if (def.description) out.description = def.description;
970
+ if (def.category) out.category = def.category;
971
+ if (def.maxPerPage !== void 0) out.max_per_page = def.maxPerPage;
972
+ types.push(out);
973
+ }
974
+ issues.push(...validateOpsSectionTypes(types));
975
+ if (issues.length) throw new SectionTypesValidationError(issues);
976
+ return { types, warnings };
977
+ }
978
+ var SYNC_PATH = "/storefront/content/section-types";
979
+ var count = (v) => Array.isArray(v) ? v.length : typeof v === "number" ? v : 0;
980
+ async function syncSectionTypes(target, source, options = {}) {
981
+ const { types, warnings } = sectionTypesToOps(source, options);
982
+ let raw;
983
+ if ("request" in target && typeof target.request === "function") {
984
+ raw = await target.request({ method: "PUT", path: SYNC_PATH, body: { section_types: types } });
985
+ } else {
986
+ const t = target;
987
+ if (!t.token.startsWith("sk_")) throw new TypeError("syncSectionTypes: an sk_ token with content:write is required");
988
+ const f = t.fetch ?? globalThis.fetch.bind(globalThis);
989
+ const url = t.baseUrl.replace(/\/+$/, "") + SYNC_PATH;
990
+ const res = await f(url, {
991
+ method: "PUT",
992
+ headers: { Authorization: `Bearer ${t.token}`, "Content-Type": "application/json", Accept: "application/json" },
993
+ body: JSON.stringify({ section_types: types })
994
+ });
995
+ const text = await res.text();
996
+ let body;
997
+ try {
998
+ body = text ? JSON.parse(text) : void 0;
999
+ } catch {
1000
+ body = void 0;
1001
+ }
1002
+ if (!res.ok) {
1003
+ throw new StorefrontError({ status: res.status, code: body?.code ?? body?.error?.code ?? "unknown", message: body?.message ?? body?.error?.message ?? `Section types sync failed (${res.status})`, url });
1004
+ }
1005
+ raw = body;
1006
+ }
1007
+ const data = raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
1008
+ return { created: count(data?.created), updated: count(data?.updated), warnings };
1009
+ }
613
1010
  function defineSection(definition) {
614
1011
  return definition;
615
1012
  }
@@ -633,6 +1030,9 @@ function resolveSectionProps(schema, raw, path = "") {
633
1030
  else if (spec.required) errors.push({ prop: key, message: "is required" });
634
1031
  continue;
635
1032
  }
1033
+ if (spec.kind === "image" && typeof value === "object" && value !== null && typeof value.url === "string") {
1034
+ value = value.url;
1035
+ }
636
1036
  if (STRING_KINDS.has(spec.kind)) {
637
1037
  if (typeof value !== "string") {
638
1038
  fail2("must be a string");
@@ -756,6 +1156,14 @@ var SectionRegistry = class {
756
1156
  toJSONSchema() {
757
1157
  return Object.fromEntries(this.definitions().map((d) => [d.type, sectionJsonSchema(d)]));
758
1158
  }
1159
+ /**
1160
+ * Section types in the OPS `storefront_content` format, ready for
1161
+ * `PUT /storefront/content/section-types`. Throws `SectionTypesValidationError`
1162
+ * on anything OPS would reject; `warnings` flags built-in types.
1163
+ */
1164
+ toOpsSectionTypes(options) {
1165
+ return sectionTypesToOps(this, options);
1166
+ }
759
1167
  };
760
1168
  function createSectionRegistry(definitions = []) {
761
1169
  return new SectionRegistry(definitions);
@@ -783,59 +1191,6 @@ function Sections({ sections, registry, context, renderUnknown, onInvalid }) {
783
1191
  return /* @__PURE__ */ jsx(Component, { ...props, section: { id: section.id, type: section.type }, context }, key);
784
1192
  }) });
785
1193
  }
786
- var DEFAULT_FONT_FALLBACK = 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif';
787
- var MONO_FALLBACK = "ui-monospace, SFMono-Regular, Menlo, monospace";
788
- function tokenName(key) {
789
- return key.trim().replace(/^-+/, "").replace(/^color-/, "").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_.]+/g, "-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
790
- }
791
- function isSafeCssValue(value) {
792
- if (!value || value.length > 200) return false;
793
- if (/[;{}<>\\]/.test(value)) return false;
794
- if (/url\s*\(|expression\s*\(|@import/i.test(value)) return false;
795
- return true;
796
- }
797
- function fontStack(family, role, fallback) {
798
- const name = family.replace(/["']/g, "").trim();
799
- if (!name || !isSafeCssValue(name)) return null;
800
- return `"${name}", ${role === "mono" ? MONO_FALLBACK : fallback}`;
801
- }
802
- function themeToCssVariables(theme, options = {}) {
803
- const naming = options.naming ?? "both";
804
- const vars = {};
805
- for (const [key, raw] of Object.entries(theme?.tokens ?? {})) {
806
- const name = tokenName(key);
807
- const value = String(raw).trim();
808
- if (!name || !isSafeCssValue(value)) continue;
809
- if (naming !== "metacore") vars[`--color-${name}`] = value;
810
- if (naming !== "color") vars[`--${name}`] = value;
811
- }
812
- const fallback = options.fontFallback ?? DEFAULT_FONT_FALLBACK;
813
- for (const [role, family] of Object.entries(theme?.fonts ?? {})) {
814
- const name = tokenName(role);
815
- const stack = typeof family === "string" ? fontStack(family, name, fallback) : null;
816
- if (name && stack) vars[`--font-${name}`] = stack;
817
- }
818
- if (theme?.radius && isSafeCssValue(theme.radius.trim())) vars["--radius"] = theme.radius.trim();
819
- return vars;
820
- }
821
- function themeToCss(theme, options = {}) {
822
- const vars = themeToCssVariables(theme, options);
823
- const body = Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
824
- if (!body) return "";
825
- const selector = (options.selector ?? ":root").replace(/[{}<>]/g, "");
826
- return `${selector}{${body}}`;
827
- }
828
- function themeFontsHref(theme, weights = [400, 500, 600, 700]) {
829
- const families = [...new Set(Object.values(theme?.fonts ?? {}).map((f) => String(f).replace(/["']/g, "").trim()).filter((f) => f && isSafeCssValue(f)))];
830
- if (!families.length) return null;
831
- const params = families.map((f) => `family=${encodeURIComponent(f).replace(/%20/g, "+")}:wght@${weights.join(";")}`).join("&");
832
- return `https://fonts.googleapis.com/css2?${params}&display=swap`;
833
- }
834
- function ThemeStyle({ theme, nonce, id = "storefront-theme", ...options }) {
835
- const css = themeToCss(theme, options);
836
- if (!css) return null;
837
- return /* @__PURE__ */ jsx("style", { id, nonce, "data-storefront-theme": "", dangerouslySetInnerHTML: { __html: css } });
838
- }
839
1194
  var MESSAGES = {
840
1195
  unsupported: "Tu navegador no permite obtener tu ubicaci\xF3n.",
841
1196
  insecure: "La ubicaci\xF3n solo est\xE1 disponible en conexiones seguras (HTTPS).",
@@ -1183,6 +1538,6 @@ function NearestLocationForm({
1183
1538
  );
1184
1539
  }
1185
1540
 
1186
- export { AddToCartButton, CART_COOKIE_NAME, CartProvider, DirectionsLink, JsonLd, LocationList, LocationPicker, NearestLocationForm, OpenStatus, PickupOption, SELECTED_LOCATION_PARAM, SELECTED_LOCATION_STORAGE_KEY, SectionRegistry, Sections, ThemeStyle, VariantSelector, breadcrumbJsonLd, collectionJsonLd, collectionMeta, createSectionRegistry, defineSection, faqJsonLd, getCartIdFromCookie, isSafeCssValue, localBusinessJsonLd, openingHoursJsonLd, optimisticCart, organizationJsonLd, productJsonLd, productMeta, readCartCookie, readSelectedLocationId, resolveSectionProps, sectionJsonSchema, seoMeta, serializeCartCookie, serializeJsonLd, themeFontsHref, themeToCss, themeToCssVariables, toMerchandise, tokenName, useCart, useGeolocation, useNearestLocations, useOptionalCart, useProductOptions, useSelectedLocation, writeCartCookie };
1541
+ export { AddToCartButton, AvailabilityBadge, CART_COOKIE_NAME, CartLineProblem, CartProvider, DirectionsLink, JsonLd, LocationList, LocationPicker, NearestLocationForm, OPS_BUILTIN_SECTION_TYPES, OPS_PROP_KINDS, OpenStatus, PickupOption, RestockNotifyForm, SELECTED_LOCATION_PARAM, SELECTED_LOCATION_STORAGE_KEY, SectionRegistry, SectionTypesValidationError, Sections, SrOnly, StockConflictAlert, VariantOptionState, VariantSelector, breadcrumbJsonLd, collectionJsonLd, collectionMeta, createSectionRegistry, defineSection, faqJsonLd, getCartIdFromCookie, localBusinessJsonLd, openingHoursJsonLd, optimisticCart, organizationJsonLd, productJsonLd, productMeta, readCartCookie, readSelectedLocationId, resolveSectionProps, sectionJsonSchema, sectionTypesToOps, seoMeta, serializeCartCookie, serializeJsonLd, syncSectionTypes, toMerchandise, useAvailability, useCart, useGeolocation, useNearestLocations, useOptionalCart, useProductOptions, useSelectedLocation, validateOpsSectionTypes, writeCartCookie };
1187
1542
  //# sourceMappingURL=index.js.map
1188
1543
  //# sourceMappingURL=index.js.map