@asteby/storefront-react 0.5.0 → 0.7.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/README.md CHANGED
@@ -149,6 +149,33 @@ defaults, coerces numeric strings, drops unknown props and skips a section
149
149
  missing a required prop. `registry.toJSONSchema()` / `sectionJsonSchema(def)`
150
150
  emit the JSON Schema the OPS editor reads (`section.schema.json`).
151
151
 
152
+ ### Sincronizar tipos de sección con OPS
153
+
154
+ Convierte los `defineSection` al formato del addon `storefront_content` y los
155
+ publica sin escribir conversores a mano:
156
+
157
+ ```ts
158
+ import { createStorefrontClient } from '@asteby/storefront'
159
+ import { syncSectionTypes } from '@asteby/storefront-react'
160
+
161
+ const sf = createStorefrontClient({ baseUrl: process.env.OPS_GW!, token: process.env.OPS_SK! }) // sk_ con content:write
162
+ const { created, updated, warnings } = await syncSectionTypes(sf, registry)
163
+ // o sin cliente: syncSectionTypes({ baseUrl, token, fetch? }, registry)
164
+ // o solo exportar: registry.toOpsSectionTypes() -> { types, warnings }
165
+ ```
166
+
167
+ Conversión: `items`→`fields` (recursivo), `url`/`color`→`text` (la URL viaja como
168
+ texto; `link` de OPS es `{label,url}`), `products`→`product_list`. `max` fija el
169
+ tope de texto en OPS. `defineSection` acepta `category` y `maxPerPage`.
170
+
171
+ Los tipos integrados de OPS (`hero, product_grid, collection_list, rich_text,
172
+ faq, image_with_text`) se excluyen (`excludeBuiltin`, por omisión `true`) y
173
+ generan `warnings`; pasa `builtinProps: { hero: ['title', ...] }` para avisar
174
+ solo de las props locales que no existen en OPS. Antes de llamar a OPS se valida
175
+ localmente (lista sin `fields`, `select` sin `options`, kind desconocido, clave no
176
+ snake_case, etiqueta vacía): lanza `SectionTypesValidationError` con `issues[]`
177
+ (`{path,message}`); también disponible como `validateOpsSectionTypes(types)`.
178
+
152
179
  ### Theme
153
180
 
154
181
  `themeToCss(theme, { selector?, naming?, fontFallback? })` turns
@@ -492,3 +519,17 @@ import { CartProgressBars, CrossSellModal, OrderNote, PromoPopup } from '@asteby
492
519
  | `ProductImage`, `ProductMockup`, `MockupProvider`, `createMockupRegistry`, `registerMockup`, `BUILTIN_MOCKUPS`, `MockupFrame`, `MockupText`, `TireMockup`, `PartMockup`, `SupplementMockup`, `GenericMockup` | Mockups de respaldo (subpath `/mockups`) |
493
520
  | `PaymentBrick`, `useEmbeddedPayment`, `VoucherCard`, `PaymentChallenge`, `createFetchTransport`, `mercadoPagoBricksAdapter`, `loadPaymentSdk`, `paymentCsp`, `readPaymentTheme` | Pago integrado (subpath `/payments`) |
494
521
  | `PromoPopup`, `CartProgressBars`, `PromoProgressBar`, `CrossSellModal`, `OrderNote`, `isExcludedPath`, `frequencyAllows`, `isCrawler` | Promociones y venta cruzada (subpath `/promotions`) |
522
+
523
+ ## `@asteby/storefront-react/ui`
524
+
525
+ Piezas de interfaz de tienda, sin dependencia de router. Importa los estilos una vez (`import '@asteby/storefront-react/ui.css'`)
526
+ y define los tokens `--background --foreground --border --tint --primary --primary-foreground --ring`.
527
+
528
+ | Pieza | Para qué |
529
+ |---|---|
530
+ | `Select` | Selector con teclado completo y tema claro/oscuro; sin JavaScript es un `<select>` nativo. |
531
+ | `Swatch` | Muestra 2D plana de una variante: color, combinación (partes iguales) o emoji. Aro del color primario solo si el color se pierde con el fondo. |
532
+ | `parseSwatchAttribute` / `resolveSwatchSpec` | El catálogo manda: atributo `"Nombre:#hex\|Nombre:#hex+#hex\|Nombre:🍓"`; si no hay dato, se resuelve por nombre (colores y sabores en español e inglés). |
533
+ | `ThemeToggle`, `THEME_INIT_SCRIPT` | Claro/oscuro con `html[data-theme]` y localStorage, sin parpadeo. |
534
+ | `InstallApp`, `useServiceWorker` | Instalación PWA (Android/Chrome y pista para iOS) y registro del service worker. |
535
+ | `.select`, `.check`, `.radio` | Controles de formulario con los tokens de la tienda. |
package/dist/index.cjs CHANGED
@@ -1010,6 +1010,156 @@ function JsonLd({ data, nonce }) {
1010
1010
  if (!data || Array.isArray(data) && data.length === 0) return null;
1011
1011
  return /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", nonce, dangerouslySetInnerHTML: { __html: serializeJsonLd(data) } });
1012
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
+ }
1013
1163
  function defineSection(definition) {
1014
1164
  return definition;
1015
1165
  }
@@ -1033,6 +1183,9 @@ function resolveSectionProps(schema, raw, path = "") {
1033
1183
  else if (spec.required) errors.push({ prop: key, message: "is required" });
1034
1184
  continue;
1035
1185
  }
1186
+ if (spec.kind === "image" && typeof value === "object" && value !== null && typeof value.url === "string") {
1187
+ value = value.url;
1188
+ }
1036
1189
  if (STRING_KINDS.has(spec.kind)) {
1037
1190
  if (typeof value !== "string") {
1038
1191
  fail2("must be a string");
@@ -1156,6 +1309,14 @@ var SectionRegistry = class {
1156
1309
  toJSONSchema() {
1157
1310
  return Object.fromEntries(this.definitions().map((d) => [d.type, sectionJsonSchema(d)]));
1158
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
+ }
1159
1320
  };
1160
1321
  function createSectionRegistry(definitions = []) {
1161
1322
  return new SectionRegistry(definitions);
@@ -1596,6 +1757,8 @@ exports.LocationList = LocationList;
1596
1757
  exports.LocationPicker = LocationPicker;
1597
1758
  exports.Money = Money;
1598
1759
  exports.NearestLocationForm = NearestLocationForm;
1760
+ exports.OPS_BUILTIN_SECTION_TYPES = OPS_BUILTIN_SECTION_TYPES;
1761
+ exports.OPS_PROP_KINDS = OPS_PROP_KINDS;
1599
1762
  exports.OpenStatus = OpenStatus;
1600
1763
  exports.PickupOption = PickupOption;
1601
1764
  exports.ProductPrice = ProductPrice;
@@ -1603,6 +1766,7 @@ exports.RestockNotifyForm = RestockNotifyForm;
1603
1766
  exports.SELECTED_LOCATION_PARAM = SELECTED_LOCATION_PARAM;
1604
1767
  exports.SELECTED_LOCATION_STORAGE_KEY = SELECTED_LOCATION_STORAGE_KEY;
1605
1768
  exports.SectionRegistry = SectionRegistry;
1769
+ exports.SectionTypesValidationError = SectionTypesValidationError;
1606
1770
  exports.Sections = Sections;
1607
1771
  exports.SrOnly = SrOnly;
1608
1772
  exports.StockConflictAlert = StockConflictAlert;
@@ -1629,9 +1793,11 @@ exports.readCartCookie = readCartCookie;
1629
1793
  exports.readSelectedLocationId = readSelectedLocationId;
1630
1794
  exports.resolveSectionProps = resolveSectionProps;
1631
1795
  exports.sectionJsonSchema = sectionJsonSchema;
1796
+ exports.sectionTypesToOps = sectionTypesToOps;
1632
1797
  exports.seoMeta = seoMeta;
1633
1798
  exports.serializeCartCookie = serializeCartCookie;
1634
1799
  exports.serializeJsonLd = serializeJsonLd;
1800
+ exports.syncSectionTypes = syncSectionTypes;
1635
1801
  exports.themeFontsHref = themeFontsHref;
1636
1802
  exports.themeToCss = themeToCss;
1637
1803
  exports.themeToCssVariables = themeToCssVariables;
@@ -1647,6 +1813,7 @@ exports.useProductOptions = useProductOptions;
1647
1813
  exports.useSelectedLocation = useSelectedLocation;
1648
1814
  exports.useStorefront = useStorefront;
1649
1815
  exports.useStorefrontClient = useStorefrontClient;
1816
+ exports.validateOpsSectionTypes = validateOpsSectionTypes;
1650
1817
  exports.writeCartCookie = writeCartCookie;
1651
1818
  //# sourceMappingURL=index.cjs.map
1652
1819
  //# sourceMappingURL=index.cjs.map