@bacano/sdk 1.1.17 → 1.1.18

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
@@ -118,6 +118,14 @@ official `BacanoForm` renderer or consume the same typed definition through
118
118
  `useBacanoForm()` without GraphQL, HTML stored in the database or a rebuild
119
119
  after each publication.
120
120
 
121
+ Every visible region sourced from a Bacano `ContentEntry` must expose the
122
+ entry-level marker defined in
123
+ [`STOREFRONT_CONTENT_LOCATOR.md`](./STOREFRONT_CONTENT_LOCATOR.md). Use
124
+ `getContentEntryLocatorAttributes(entry)` on the outer section or card so an
125
+ authorized internal browser tool can show the entry name and open its Bacano
126
+ editor. This contract identifies entries only; it does not expose field-level
127
+ editing or grant backoffice access.
128
+
121
129
  Static and hybrid stores must also follow
122
130
  [`STOREFRONT_PERFORMANCE.md`](./STOREFRONT_PERFORMANCE.md). It defines the
123
131
  official build bootstrap, live-state refresh, responsive media and request
@@ -0,0 +1,111 @@
1
+ # Localizador de entradas de Content
2
+
3
+ Este documento define el contrato público V1 que permite reconocer desde el
4
+ navegador qué entrada de Bacano Content produce una región visible de una web.
5
+ El objetivo es únicamente **identificar la entrada y abrir su editor**. No es
6
+ edición inline y no identifica campos individuales.
7
+
8
+ ## Contrato obligatorio
9
+
10
+ Toda región visual cuyo contenido editorial provenga de un `ContentEntry` debe
11
+ marcar su elemento DOM exterior significativo con:
12
+
13
+ ```html
14
+ <section
15
+ data-bacano-content-entry="v1"
16
+ data-bacano-entry-key="seccion-de-servicios"
17
+ data-bacano-entry-name="Sección de servicios"
18
+ data-bacano-entry-locale="es-CO"
19
+ >
20
+ <!-- contenido visible -->
21
+ </section>
22
+ ```
23
+
24
+ Usar el helper oficial evita diferencias en nombres, versión y normalización:
25
+
26
+ ```tsx
27
+ import {
28
+ getContentEntryLocatorAttributes,
29
+ type ContentEntry,
30
+ } from "@bacano/sdk";
31
+
32
+ function ServicesSection({ entry }: { entry: ContentEntry }) {
33
+ return (
34
+ <section {...getContentEntryLocatorAttributes(entry)}>
35
+ {/* La sección puede consumir un view model derivado de entry. */}
36
+ </section>
37
+ );
38
+ }
39
+ ```
40
+
41
+ El helper falla si la entrada no tiene una `key` estable o un `locale`. Una
42
+ región que no puede abrir una entrada inequívoca no debe aparentar que sí puede.
43
+
44
+ ## Granularidad
45
+
46
+ - Marcar una vez el bloque visual administrable: `section`, `article`, banner,
47
+ tarjeta o contenedor equivalente.
48
+ - No marcar títulos, párrafos, botones ni campos individuales dentro del mismo
49
+ bloque.
50
+ - Si una tarjeta visible usa otra entrada referenciada, la tarjeta recibe su
51
+ propio marcador anidado. El espacio restante conserva el marcador de la
52
+ entrada contenedora.
53
+ - Si una entrada produce varias regiones independientes, cada región puede
54
+ repetir el mismo marcador.
55
+ - No marcar texto perteneciente al código/i18n, productos del catálogo,
56
+ configuración pública ni documentos legales como si fueran Content.
57
+
58
+ Los mappers pueden transformar `contentJson`, pero deben conservar el origen
59
+ editorial necesario para llamar el helper. Por ejemplo:
60
+
61
+ ```ts
62
+ const section = {
63
+ locator: getContentEntryLocatorAttributes(entry),
64
+ title: readTitle(entry.contentJson),
65
+ };
66
+ ```
67
+
68
+ ```tsx
69
+ <section {...section.locator}>{section.title}</section>
70
+ ```
71
+
72
+ ## Responsabilidades del consumidor
73
+
74
+ Una extensión o herramienta de inspección:
75
+
76
+ 1. busca `[data-bacano-content-entry="v1"]`;
77
+ 2. muestra `data-bacano-entry-name` al usuario;
78
+ 3. usa `data-bacano-entry-key` y `data-bacano-entry-locale` para construir la
79
+ navegación hacia el backoffice;
80
+ 4. deja autenticación, empresa y permisos bajo control de Bacano.
81
+
82
+ El SDK no publica una URL de administración ni abre ventanas. Ese destino
83
+ pertenece a la herramienta interna y puede cambiar entre ambientes.
84
+
85
+ ## Seguridad y salida estática
86
+
87
+ Los cuatro atributos contienen exclusivamente identidad editorial ya pública:
88
+ versión, key, nombre y locale de una entrada publicada. Nunca incluir tokens,
89
+ URLs administrativas firmadas, secretos, IDs de usuarios ni datos privados.
90
+
91
+ Los marcadores no conceden permisos. El backoffice debe autenticar y autorizar
92
+ cualquier lectura o modificación. En exports estáticos los atributos quedan
93
+ materializados junto con el contenido publicado y se actualizan en el siguiente
94
+ build.
95
+
96
+ ## Verificación
97
+
98
+ Antes de publicar una web que usa Bacano Content:
99
+
100
+ - inventariar las entradas que producen regiones visibles;
101
+ - inspeccionar el HTML final, no solo JSX o componentes;
102
+ - comprobar que cada región tiene exactamente la key, nombre y locale de su
103
+ entrada fuente;
104
+ - probar marcadores anidados cuando una entrada contenedora renderiza entradas
105
+ referenciadas;
106
+ - comprobar que activar una herramienta de localización no altera navegación,
107
+ layout ni accesibilidad cuando vuelve a desactivarse.
108
+
109
+ Una región editorial sin marcador o con una key distinta de su fuente es un
110
+ fallo de integración, porque impide localizar de manera confiable el contenido
111
+ que la web declara administrable.
@@ -81,6 +81,47 @@ El renderer personalizado puede cambiar componentes y estilos, pero debe usar
81
81
  los campos, keys, opciones y validaciones entregados por el SDK. No debe crear
82
82
  un segundo cliente, resolver el website nuevamente ni usar GraphQL directo.
83
83
 
84
+ ## Honeypot y confirmación de persistencia
85
+
86
+ `honeypot` es un campo trampa contra bots, no un token de seguridad. No es una
87
+ marca de tiempo, un UUID, un número aleatorio ni un identificador del visitante.
88
+ Un usuario real debe enviarlo siempre vacío.
89
+
90
+ - `BacanoForm` administra el honeypot internamente. El storefront no debe crear
91
+ otro ni asignarle un valor.
92
+ - Un renderer personalizado debe omitir `honeypot` en
93
+ `client.forms.submitResponse(...)` salvo que implemente deliberadamente un
94
+ input trampa oculto. El SDK lo enviará como cadena vacía por defecto.
95
+ - Si se implementa el input trampa, debe quedar fuera del flujo visual y del
96
+ teclado, con autocompletado desactivado. Nunca debe reutilizar un campo real ni
97
+ recibir sugerencias del navegador o de un gestor de contraseñas.
98
+ - Bacano responde de manera deliberadamente genérica ante un honeypot lleno:
99
+ `accepted: true` y `responseId: null`. La respuesta no se persiste y el bot no
100
+ descubre que fue detectado.
101
+ - Para una prueba funcional o aprobación de producción, el envío debe devolver
102
+ `accepted: true` y un UUID en `responseId`, y debe aparecer una sola vez en
103
+ Respuestas dentro de Bacano.
104
+
105
+ No uses `responseId: null` para mostrar información distinta al visitante. Es
106
+ una señal técnica para pruebas y observabilidad segura, no un mensaje que deba
107
+ revelar la protección antispam.
108
+
109
+ ## Tipos semánticos de contacto
110
+
111
+ La etiqueta y la key no sustituyen el tipo del campo. Si un dato representa un
112
+ correo o un teléfono, el formulario debe definirlo como `email` o `phone`:
113
+
114
+ - Bacano valida el formato según el tipo declarado.
115
+ - Las respuestas conservan todos los valores, pero solo los campos `email` y
116
+ `phone` alimentan los datos de contacto normalizados de la respuesta.
117
+ - Un campo `text` llamado `correo`, `email`, `telefono` o similar continúa
118
+ siendo texto libre. No debe dependerse de su nombre para CRM, leads o futuras
119
+ automatizaciones.
120
+
121
+ Antes de publicar, revisa que los campos destinados a identificar o contactar a
122
+ la persona tengan el tipo correcto. Cambiar únicamente su etiqueta no corrige el
123
+ contrato de datos.
124
+
84
125
  ## Next.js y Static First
85
126
 
86
127
  Obtén la definición durante el build o desde un Server Component usando el
@@ -127,15 +168,32 @@ consultarlo.
127
168
  validación del navegador mejora la experiencia, pero no es autoritativa.
128
169
  - URL, referidor y atribución se sanean. Las respuestas y datos personales no
129
170
  se escriben en logs.
130
- - Una falla futura de correo, CRM o automatización no revierte la respuesta.
131
- - `CAPTURE_LEAD` se procesa en una fase posterior. Toda respuesta se conserva
132
- primero y de forma independiente.
171
+ - `pageUrl`, `referrerUrl` y la URL de atribución aceptan únicamente HTTP o
172
+ HTTPS. No envíes protocolos locales, ejecutables o de datos.
173
+ - Los teléfonos admiten un `+` inicial y separadores habituales; Bacano los
174
+ normaliza antes de resolver identidad y rechaza letras o signos mal ubicados.
175
+ - Los números usan notación decimal o científica explícita. Valores ambiguos
176
+ como `0x10` o `3 unidades` se rechazan aunque JavaScript pudiera convertirlos.
177
+ - Un checkbox opcional omitido se conserva como `null`; uno requerido debe
178
+ enviarse expresamente como `true`.
179
+ - Una falla de correo, CRM o automatización no revierte la respuesta.
180
+ - `STORE_RESPONSE` conserva la respuesta sin crear un lead.
181
+ - `CAPTURE_LEAD` conserva primero la respuesta y después resuelve internamente
182
+ la identidad canónica. Reutiliza un lead o cliente existente, crea un lead
183
+ cuando corresponde y manda a revisión las coincidencias contradictorias de
184
+ correo y teléfono sin fusionarlas automáticamente.
185
+ - El resultado público continúa siendo deliberadamente genérico. El storefront
186
+ no recibe ni debe inferir si Bacano creó un lead, encontró un cliente o marcó
187
+ una identidad para revisión.
133
188
 
134
189
  ## Checklist mínimo del storefront
135
190
 
136
191
  - Usar un solo `BacanoProvider` y el cliente global ya inicializado.
137
192
  - Renderizar con `BacanoForm` o con `useBacanoForm`; nunca copiar la definición
138
193
  a un esquema local ni consultar GraphQL directamente.
194
+ - Pueden coexistir varios `BacanoForm` en una página. El renderer oficial aísla
195
+ los identificadores de sus controles; no construyas IDs globales solo con la
196
+ key del campo.
139
197
  - Entregar `initialForm` cuando la página se genera de forma estática.
140
198
  - Respetar `ACTIVE`, `PAUSED`, versión y `definitionHash` sin inferir estados.
141
199
  - Mantener una `idempotencyKey` estable durante los reintentos del mismo envío.
@@ -143,5 +201,11 @@ consultarlo.
143
201
  reintento idempotente y error de red.
144
202
  - Confirmar en Bacano que una respuesta real quedó persistida antes de aprobar
145
203
  el flujo; no basta con mostrar el mensaje de éxito del navegador.
146
- - No prometer correo, lead, solicitud o automatización mientras la capacidad no
147
- esté implementada y activa en Bacano.
204
+ - Probar autocompletado del navegador y gestores de contraseñas: ningún campo
205
+ real puede llenar el honeypot. Un envío real debe devolver un UUID como
206
+ `responseId`; `null` no demuestra persistencia.
207
+ - Verificar que correos y teléfonos utilicen los tipos `email` y `phone`, no
208
+ campos de texto con una etiqueta parecida.
209
+ - No prometer correo, cupón, solicitud ni automatización mientras la capacidad
210
+ no esté implementada y activa en Bacano. Aunque `CAPTURE_LEAD` esté activo,
211
+ el mensaje público no debe afirmar qué identidad interna se creó o reutilizó.
@@ -0,0 +1,16 @@
1
+ import type { ContentEntry } from "./types";
2
+ export declare const CONTENT_ENTRY_LOCATOR_VERSION: "v1";
3
+ export declare const CONTENT_ENTRY_LOCATOR_SELECTOR: "[data-bacano-content-entry=\"v1\"]";
4
+ export type ContentEntryLocatorSource = Pick<ContentEntry, "key" | "locale" | "name">;
5
+ export interface ContentEntryLocatorAttributes {
6
+ "data-bacano-content-entry": typeof CONTENT_ENTRY_LOCATOR_VERSION;
7
+ "data-bacano-entry-key": string;
8
+ "data-bacano-entry-locale": string;
9
+ "data-bacano-entry-name": string;
10
+ }
11
+ /**
12
+ * Adds the public, non-authoritative marker consumed by Bacano's content
13
+ * locator. Spread the result only on the outermost meaningful DOM element
14
+ * whose visible editorial content is owned by the entry.
15
+ */
16
+ export declare function getContentEntryLocatorAttributes(entry: ContentEntryLocatorSource): ContentEntryLocatorAttributes;
package/index.cjs CHANGED
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthError: () => AuthError,
24
+ CONTENT_ENTRY_LOCATOR_SELECTOR: () => CONTENT_ENTRY_LOCATOR_SELECTOR,
25
+ CONTENT_ENTRY_LOCATOR_VERSION: () => CONTENT_ENTRY_LOCATOR_VERSION,
24
26
  CheckoutError: () => CheckoutError,
25
27
  GraphQLRequestError: () => GraphQLRequestError,
26
28
  MARKETING_SCHEMA_VERSION: () => MARKETING_SCHEMA_VERSION,
@@ -38,6 +40,7 @@ __export(index_exports, {
38
40
  createWebsiteMarketingRuntime: () => createWebsiteMarketingRuntime,
39
41
  formatMediaSrcSet: () => formatMediaSrcSet,
40
42
  getAllowedMarketingParameters: () => getAllowedMarketingParameters,
43
+ getContentEntryLocatorAttributes: () => getContentEntryLocatorAttributes,
41
44
  getPrimaryProductImage: () => getPrimaryProductImage,
42
45
  getProductDetailVariants: () => getProductDetailVariants,
43
46
  getProductImages: () => getProductImages,
@@ -6904,3 +6907,23 @@ function createBacanoClient(config) {
6904
6907
  }
6905
6908
  };
6906
6909
  }
6910
+
6911
+ // libs/sdk/src/content-entry-locator.ts
6912
+ var CONTENT_ENTRY_LOCATOR_VERSION = "v1";
6913
+ var CONTENT_ENTRY_LOCATOR_SELECTOR = `[data-bacano-content-entry="${CONTENT_ENTRY_LOCATOR_VERSION}"]`;
6914
+ function getContentEntryLocatorAttributes(entry) {
6915
+ const key = entry.key?.trim();
6916
+ if (!key) {
6917
+ throw new Error("A Bacano Content locator requires a stable entry key.");
6918
+ }
6919
+ const locale = entry.locale.trim();
6920
+ if (!locale) {
6921
+ throw new Error("A Bacano Content locator requires an entry locale.");
6922
+ }
6923
+ return {
6924
+ "data-bacano-content-entry": CONTENT_ENTRY_LOCATOR_VERSION,
6925
+ "data-bacano-entry-key": key,
6926
+ "data-bacano-entry-locale": locale,
6927
+ "data-bacano-entry-name": entry.name.trim() || key
6928
+ };
6929
+ }
package/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export type { BacanoClient } from "./client";
2
2
  export { createBacanoClient } from "./client";
3
+ export type { ContentEntryLocatorAttributes, ContentEntryLocatorSource, } from "./content-entry-locator";
4
+ export { CONTENT_ENTRY_LOCATOR_SELECTOR, CONTENT_ENTRY_LOCATOR_VERSION, getContentEntryLocatorAttributes, } from "./content-entry-locator";
3
5
  export { AuthError, CheckoutError, GraphQLRequestError, NotInitializedError, RateLimitError, WebsiteError, } from "./errors";
4
6
  export type { WebsiteAdvertisingConsent, WebsiteMarketingAttribution, WebsiteMarketingBrowserEvent, WebsiteMarketingBrowserEventName, WebsiteMarketingContactPlacement, WebsiteMarketingDiagnostic, WebsiteMarketingItem, WebsiteMarketingRuntime, WebsiteMarketingRuntimeOptions, WebsiteMarketingScriptLoadStrategy, WebsiteMarketingTouchpoint, } from "./marketing";
5
7
  export { captureWebsiteMarketingAttribution, createWebsiteMarketingRuntime, getAllowedMarketingParameters, getWebsiteMarketingAttribution, MARKETING_SCHEMA_VERSION, sanitizeMarketingUrl, WEBSITE_MARKETING_CONTACT_PLACEMENTS, } from "./marketing";
package/index.js CHANGED
@@ -6850,8 +6850,30 @@ function createBacanoClient(config) {
6850
6850
  }
6851
6851
  };
6852
6852
  }
6853
+
6854
+ // libs/sdk/src/content-entry-locator.ts
6855
+ var CONTENT_ENTRY_LOCATOR_VERSION = "v1";
6856
+ var CONTENT_ENTRY_LOCATOR_SELECTOR = `[data-bacano-content-entry="${CONTENT_ENTRY_LOCATOR_VERSION}"]`;
6857
+ function getContentEntryLocatorAttributes(entry) {
6858
+ const key = entry.key?.trim();
6859
+ if (!key) {
6860
+ throw new Error("A Bacano Content locator requires a stable entry key.");
6861
+ }
6862
+ const locale = entry.locale.trim();
6863
+ if (!locale) {
6864
+ throw new Error("A Bacano Content locator requires an entry locale.");
6865
+ }
6866
+ return {
6867
+ "data-bacano-content-entry": CONTENT_ENTRY_LOCATOR_VERSION,
6868
+ "data-bacano-entry-key": key,
6869
+ "data-bacano-entry-locale": locale,
6870
+ "data-bacano-entry-name": entry.name.trim() || key
6871
+ };
6872
+ }
6853
6873
  export {
6854
6874
  AuthError,
6875
+ CONTENT_ENTRY_LOCATOR_SELECTOR,
6876
+ CONTENT_ENTRY_LOCATOR_VERSION,
6855
6877
  CheckoutError,
6856
6878
  GraphQLRequestError,
6857
6879
  MARKETING_SCHEMA_VERSION,
@@ -6869,6 +6891,7 @@ export {
6869
6891
  createWebsiteMarketingRuntime,
6870
6892
  formatMediaSrcSet,
6871
6893
  getAllowedMarketingParameters,
6894
+ getContentEntryLocatorAttributes,
6872
6895
  getPrimaryProductImage,
6873
6896
  getProductDetailVariants,
6874
6897
  getProductImages,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacano/sdk",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "description": "TypeScript SDK for building websites on the Bacano ERP",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -3,6 +3,7 @@ import type { BacanoFormValues } from "./forms";
3
3
  type BacanoFormValue = BacanoFormValues[string];
4
4
  export declare function BacanoFormFieldControl(props: {
5
5
  field: WebsiteFormField;
6
+ id?: string;
6
7
  className?: string;
7
8
  value: BacanoFormValue;
8
9
  onChange(value: BacanoFormValue): void;
package/react/forms.cjs CHANGED
@@ -30,8 +30,9 @@ var import_react3 = require("react");
30
30
  var import_jsx_runtime = require("react/jsx-runtime");
31
31
  function BacanoFormFieldControl(props) {
32
32
  const { field, className, value, onChange } = props;
33
+ const controlId = props.id ?? field.key;
33
34
  const common = {
34
- id: field.key,
35
+ id: controlId,
35
36
  name: field.key,
36
37
  required: field.required,
37
38
  className
@@ -71,7 +72,7 @@ function BacanoFormFieldControl(props) {
71
72
  );
72
73
  }
73
74
  if (field.type === "radio") {
74
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-labelledby": `${field.key}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
75
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { role: "radiogroup", "aria-labelledby": `${controlId}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
75
76
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
76
77
  "input",
77
78
  {
@@ -210,13 +211,14 @@ function useBacanoForm(formKey, options) {
210
211
  }
211
212
  function FormFields(props) {
212
213
  return props.fields.map((field) => {
214
+ const controlId = `${props.idPrefix}-${field.key}`;
213
215
  const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
214
216
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: props.classNames?.field, children: [
215
217
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
216
218
  "label",
217
219
  {
218
- id: `${field.key}-label`,
219
- htmlFor: field.type === "radio" ? void 0 : field.key,
220
+ id: `${controlId}-label`,
221
+ htmlFor: field.type === "radio" ? void 0 : controlId,
220
222
  className: props.classNames?.label,
221
223
  children: field.label
222
224
  }
@@ -225,6 +227,7 @@ function FormFields(props) {
225
227
  BacanoFormFieldControl,
226
228
  {
227
229
  field,
230
+ id: controlId,
228
231
  value,
229
232
  className: props.classNames?.input,
230
233
  onChange: (nextValue) => props.onChange(field.key, nextValue)
@@ -242,6 +245,7 @@ function FormSections(props) {
242
245
  FormFields,
243
246
  {
244
247
  fields: section.fields,
248
+ idPrefix: props.idPrefix,
245
249
  values: props.values,
246
250
  classNames: props.classNames,
247
251
  onChange: props.onChange
@@ -250,6 +254,8 @@ function FormSections(props) {
250
254
  ] }, section.key));
251
255
  }
252
256
  function BacanoForm(props) {
257
+ const reactId = (0, import_react3.useId)();
258
+ const idPrefix = `${props.formKey}-${reactId.replace(/:/g, "")}`;
253
259
  const { client } = useBacanoState();
254
260
  const { form, loading, error } = useBacanoForm(props.formKey, {
255
261
  initialForm: props.initialForm
@@ -382,6 +388,7 @@ function BacanoForm(props) {
382
388
  FormFields,
383
389
  {
384
390
  fields: activeStep.fields,
391
+ idPrefix,
385
392
  values,
386
393
  classNames: props.classNames,
387
394
  onChange: setFieldValue
@@ -391,6 +398,7 @@ function BacanoForm(props) {
391
398
  FormSections,
392
399
  {
393
400
  sections: activeStep.sections,
401
+ idPrefix,
394
402
  values,
395
403
  classNames: props.classNames,
396
404
  onChange: setFieldValue
@@ -401,6 +409,7 @@ function BacanoForm(props) {
401
409
  FormFields,
402
410
  {
403
411
  fields,
412
+ idPrefix,
404
413
  values,
405
414
  classNames: props.classNames,
406
415
  onChange: setFieldValue
@@ -410,6 +419,7 @@ function BacanoForm(props) {
410
419
  FormSections,
411
420
  {
412
421
  sections,
422
+ idPrefix,
413
423
  values,
414
424
  classNames: props.classNames,
415
425
  onChange: setFieldValue
package/react/forms.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  useCallback as useCallback2,
4
4
  useEffect as useEffect2,
5
+ useId,
5
6
  useMemo,
6
7
  useRef as useRef2,
7
8
  useState as useState2
@@ -11,8 +12,9 @@ import {
11
12
  import { jsx, jsxs } from "react/jsx-runtime";
12
13
  function BacanoFormFieldControl(props) {
13
14
  const { field, className, value, onChange } = props;
15
+ const controlId = props.id ?? field.key;
14
16
  const common = {
15
- id: field.key,
17
+ id: controlId,
16
18
  name: field.key,
17
19
  required: field.required,
18
20
  className
@@ -52,7 +54,7 @@ function BacanoFormFieldControl(props) {
52
54
  );
53
55
  }
54
56
  if (field.type === "radio") {
55
- return /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-labelledby": `${field.key}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ jsxs("label", { children: [
57
+ return /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-labelledby": `${controlId}-label`, children: (field.options ?? []).map((option) => /* @__PURE__ */ jsxs("label", { children: [
56
58
  /* @__PURE__ */ jsx(
57
59
  "input",
58
60
  {
@@ -191,13 +193,14 @@ function useBacanoForm(formKey, options) {
191
193
  }
192
194
  function FormFields(props) {
193
195
  return props.fields.map((field) => {
196
+ const controlId = `${props.idPrefix}-${field.key}`;
194
197
  const value = props.values[field.key] ?? (field.type === "checkbox" ? false : field.type === "multi_select" ? [] : "");
195
198
  return /* @__PURE__ */ jsxs2("div", { className: props.classNames?.field, children: [
196
199
  /* @__PURE__ */ jsx2(
197
200
  "label",
198
201
  {
199
- id: `${field.key}-label`,
200
- htmlFor: field.type === "radio" ? void 0 : field.key,
202
+ id: `${controlId}-label`,
203
+ htmlFor: field.type === "radio" ? void 0 : controlId,
201
204
  className: props.classNames?.label,
202
205
  children: field.label
203
206
  }
@@ -206,6 +209,7 @@ function FormFields(props) {
206
209
  BacanoFormFieldControl,
207
210
  {
208
211
  field,
212
+ id: controlId,
209
213
  value,
210
214
  className: props.classNames?.input,
211
215
  onChange: (nextValue) => props.onChange(field.key, nextValue)
@@ -223,6 +227,7 @@ function FormSections(props) {
223
227
  FormFields,
224
228
  {
225
229
  fields: section.fields,
230
+ idPrefix: props.idPrefix,
226
231
  values: props.values,
227
232
  classNames: props.classNames,
228
233
  onChange: props.onChange
@@ -231,6 +236,8 @@ function FormSections(props) {
231
236
  ] }, section.key));
232
237
  }
233
238
  function BacanoForm(props) {
239
+ const reactId = useId();
240
+ const idPrefix = `${props.formKey}-${reactId.replace(/:/g, "")}`;
234
241
  const { client } = useBacanoState();
235
242
  const { form, loading, error } = useBacanoForm(props.formKey, {
236
243
  initialForm: props.initialForm
@@ -363,6 +370,7 @@ function BacanoForm(props) {
363
370
  FormFields,
364
371
  {
365
372
  fields: activeStep.fields,
373
+ idPrefix,
366
374
  values,
367
375
  classNames: props.classNames,
368
376
  onChange: setFieldValue
@@ -372,6 +380,7 @@ function BacanoForm(props) {
372
380
  FormSections,
373
381
  {
374
382
  sections: activeStep.sections,
383
+ idPrefix,
375
384
  values,
376
385
  classNames: props.classNames,
377
386
  onChange: setFieldValue
@@ -382,6 +391,7 @@ function BacanoForm(props) {
382
391
  FormFields,
383
392
  {
384
393
  fields,
394
+ idPrefix,
385
395
  values,
386
396
  classNames: props.classNames,
387
397
  onChange: setFieldValue
@@ -391,6 +401,7 @@ function BacanoForm(props) {
391
401
  FormSections,
392
402
  {
393
403
  sections,
404
+ idPrefix,
394
405
  values,
395
406
  classNames: props.classNames,
396
407
  onChange: setFieldValue
@@ -91,6 +91,11 @@ Figma, desde Bacano Content o desde ambas fuentes.
91
91
  los contratos específicos del SDK.
92
92
  - No usar GraphQL directo, endpoints internos ni lógica comercial duplicada.
93
93
  - No codificar textos editables si pertenecen a Bacano Content.
94
+ - Marcar obligatoriamente cada región visual proveniente de un `ContentEntry`
95
+ con `getContentEntryLocatorAttributes(entry)`. El marcador identifica la
96
+ entrada completa, nunca campos individuales. Conservar esta identidad al
97
+ transformar Content en view models y seguir
98
+ `STOREFRONT_CONTENT_LOCATOR.md`.
94
99
  - No alterar Baby Rex u otra referencia salvo que ese sea el proyecto indicado.
95
100
 
96
101
  Si Content todavía no existe, generar un blueprint de entrega y fixtures
@@ -147,6 +152,9 @@ proyecto.
147
152
  Antes de aprobar producción, comprobar que la fuente editorial efectiva sea
148
153
  Bacano Content. Un fixture, placeholder o fallback temporal accesible en el
149
154
  build de producción es un bloqueo, aunque visualmente coincida con Figma.
155
+ Inspeccionar además el HTML final: toda región alimentada por una entrada debe
156
+ conservar su marcador V1 con la key, nombre y locale correctos. Una región
157
+ editorial sin marcador bloquea la aprobación.
150
158
 
151
159
  Al finalizar, invocar `$bacano-web` si está disponible para la auditoría de
152
160
  producción completa. Esta skill valida fidelidad y correcta integración desde
@@ -7,9 +7,9 @@ negocio.
7
7
 
8
8
  Antes de programar, registrar por ruta:
9
9
 
10
- | Ruta | Frame | Componente | Fuente | Content | Responsive | Preview | Estado |
11
- | --- | --- | --- | --- | --- | --- | --- | --- |
12
- | `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | 2 frames | activa | pendiente |
10
+ | Ruta | Frame | Componente | Fuente | Content | Localizador | Responsive | Preview | Estado |
11
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
12
+ | `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | entrada V1 | 2 frames | activa | pendiente |
13
13
 
14
14
  Fuentes posibles: Content, catálogo estático, live state, public config,
15
15
  checkout, auth u otra operación documentada del SDK.
@@ -18,6 +18,8 @@ checkout, auth u otra operación documentada del SDK.
18
18
 
19
19
  - Usar componentes y tokens existentes antes de crear otros.
20
20
  - Mapear textos/imágenes editoriales a Content cuando deban ser administrables.
21
+ - Para cada fuente `ContentEntry`, conservar y renderizar el marcador de entrada
22
+ definido en `STOREFRONT_CONTENT_LOCATOR.md`; no bajar a nivel de campo.
21
23
  - Si el equipo de Content y desarrollo trabajan en tiempos distintos, seguir
22
24
  [content-workflow.md](content-workflow.md) y mantener un único contrato de
23
25
  datos entre fixtures temporales y Bacano real.
@@ -35,6 +35,10 @@ reduzca duplicación real.
35
35
  consumidores.
36
36
  - No hacer depender la UI del shape crudo de Content: fixture y mapper real
37
37
  deben converger en el mismo view model.
38
+ - Conservar en ese view model los atributos producidos por
39
+ `getContentEntryLocatorAttributes(entry)`. Toda región visual administrada
40
+ por una entrada debe exponerlos en su contenedor DOM exterior; no se marcan
41
+ campos individuales.
38
42
 
39
43
  ## 2. Elegir el punto de partida
40
44
 
@@ -138,6 +142,10 @@ Bacano mapper ─┘
138
142
  El cambio de fuente debe ocurrir en el loader o repositorio de contenido, nunca
139
143
  mediante condicionales repartidos por los componentes.
140
144
 
145
+ Los fixtures pueden simular el view model, pero no deben inventar marcadores de
146
+ entrada en producción. El marcador definitivo se deriva del `ContentEntry`
147
+ real para que nombre, key y locale no diverjan.
148
+
141
149
  ## 5. Entrega al equipo de Content
142
150
 
143
151
  Entregar una tabla legible con:
@@ -167,7 +175,9 @@ Antes de retirar el fixture:
167
175
  2. Validar campos requeridos, media y contenido largo.
168
176
  3. Comparar la página con Figma usando Content real.
169
177
  4. Verificar preview si la capacidad está activa.
170
- 5. Eliminar el fixture del camino de ejecución, no necesariamente de los tests.
178
+ 5. Inspeccionar el HTML y confirmar los marcadores definidos en
179
+ `STOREFRONT_CONTENT_LOCATOR.md` para cada región editorial.
180
+ 6. Eliminar el fixture del camino de ejecución, no necesariamente de los tests.
171
181
 
172
182
  En producción no se permite fallback silencioso a fixtures. Si una página
173
183
  depende de contenido temporal, reportar `❌ NO LISTO` con las rutas y secciones
@@ -58,6 +58,18 @@ no aceptar una ruta arbitraria solo porque coincide con una cadena. Preview
58
58
  reutiliza componentes y mappers de producción, permanece fuera del sitemap y
59
59
  no habilita funciones transaccionales o marketing.
60
60
 
61
+ ## Trazabilidad editorial
62
+
63
+ Al mapear un `ContentEntry`, conservar los atributos retornados por
64
+ `getContentEntryLocatorAttributes(entry)` hasta el componente visual. Aplicar
65
+ el spread al `section`, `article`, banner o tarjeta exterior que representa la
66
+ entrada. No repetirlo en cada título o párrafo. Si una tarjeta proviene de otra
67
+ entrada referenciada, esa tarjeta lleva su propio marcador anidado.
68
+
69
+ La salida final debe cumplir `STOREFRONT_CONTENT_LOCATOR.md`. Verificar el HTML
70
+ generado; encontrar la llamada al helper en TypeScript no demuestra que el
71
+ marcador haya llegado al DOM.
72
+
61
73
  ## Diagnóstico antes de cambiar código
62
74
 
63
75
  Un `ORIGIN_NOT_ALLOWED` o un `403` al resolver la configuración durante el
@@ -47,7 +47,8 @@ Después leer solo:
47
47
  Si `@bacano/sdk` está disponible, leer sus contratos canónicos aplicables antes
48
48
  de proponer cambios. Como mínimo revisar `STOREFRONT_LEGAL.md`,
49
49
  `STOREFRONT_MARKETING.md`, `STOREFRONT_PERFORMANCE.md` y
50
- `STOREFRONT_PREVIEW.md`; añadir
50
+ `STOREFRONT_PREVIEW.md`; si el sitio renderiza Bacano Content, leer también y
51
+ aplicar obligatoriamente `STOREFRONT_CONTENT_LOCATOR.md`; añadir
51
52
  `AUTHENTICATION.md`, `STOREFRONT_LEADS.md`, `STOREFRONT_CLARITY.md` y
52
53
  otros contratos únicamente cuando esas capacidades existan. Leer
53
54
  `STOREFRONT_CONSTRUCTION.md` cuando el website permita ocultarse temporalmente.
@@ -70,6 +71,8 @@ ejecutar compras/pagos sin autorización explícita.
70
71
  5. Comparar router, salida del build y sitemap; ninguna ruta pública debe
71
72
  omitirse silenciosamente.
72
73
  6. Inspeccionar HTML generado, metadata, JSON-LD, media y enlaces internos.
74
+ Si existe Content, comparar las entradas renderizadas con los marcadores V1
75
+ presentes en ese HTML; revisar únicamente el JSX no es evidencia suficiente.
73
76
  7. Si existe un dominio o una aplicación ejecutable, auditar sus rutas reales,
74
77
  la consola y un trace de red con nombres y conteos de operaciones GraphQL.
75
78
  8. Probar vistas representativas desktop y mobile.
@@ -29,6 +29,13 @@ función apagada en un bloqueo artificial.
29
29
  - Comprobar que preview, callbacks, cuenta, checkout y respuestas de pago no se
30
30
  indexen cuando corresponda.
31
31
  - Validar estados vacío, loading, error y ausencia de Content opcional.
32
+ - `P0`: toda región visual cuya fuente sea un `ContentEntry` expone en el HTML
33
+ final el marcador de `STOREFRONT_CONTENT_LOCATOR.md`, con la key, nombre y
34
+ locale de la entrada fuente. El control es por entrada completa, no por campo.
35
+ - Comparar inventario de entradas y regiones. Permitir marcadores repetidos si
36
+ una entrada aparece en varios lugares y marcadores anidados cuando una tarjeta
37
+ proviene de otra entrada referenciada. No exigirlos a código/i18n, catálogo,
38
+ `publicConfig` ni documentos legales.
32
39
 
33
40
  ## SEO y compartición
34
41
 
@@ -23,12 +23,25 @@ desde Bacano. Leer también `STOREFRONT_FORMS.md` completo desde el paquete SDK.
23
23
 
24
24
  - Enviar mediante `client.forms.submitResponse()` o el envío interno de
25
25
  `BacanoForm`.
26
+ - `honeypot` es un campo trampa, no un nonce, timestamp, UUID ni dato del
27
+ visitante. `BacanoForm` lo administra. En un renderer manual, omitirlo salvo
28
+ que exista un input trampa realmente oculto; para una persona siempre debe
29
+ permanecer vacío y el autocompletado no debe llenarlo.
30
+ - `accepted: true` con `responseId: null` es una aceptación silenciosa antispam:
31
+ no demuestra que la respuesta fue guardada. La verificación funcional exige
32
+ un UUID y la respuesta visible una sola vez en Bacano.
33
+ - Los datos de contacto deben conservar su semántica: usar campos `email` y
34
+ `phone`. Un campo `text` llamado correo o teléfono se guarda como texto libre
35
+ y no alimenta los datos de contacto normalizados para CRM o automatizaciones.
26
36
  - Conservar una `idempotencyKey` durante reintentos del mismo intento lógico.
27
37
  - Enviar la versión y `definitionHash` exactos de la definición renderizada.
28
38
  - Respetar estados `ACTIVE` y `PAUSED`; no renderizar borradores o archivados.
29
39
  - No escribir respuestas o PII en consola, analítica, URLs o logs del storefront.
30
- - No afirmar que se creó un lead, se envió un correo o se abrió una solicitud si
31
- esa acción aún no está implementada y verificada en Bacano.
40
+ - En `CAPTURE_LEAD`, Bacano conserva primero la respuesta y resuelve la identidad
41
+ internamente. No afirmar al visitante que se creó un lead o que ya era cliente:
42
+ la respuesta pública es genérica y no expone ese resultado.
43
+ - No afirmar que se envió un correo, se entregó un cupón o se abrió una solicitud
44
+ si esa acción aún no está implementada y verificada en Bacano.
32
45
  - Una brecha del SDK se reporta; nunca se resuelve con GraphQL directo ni una API
33
46
  paralela del storefront.
34
47
 
@@ -41,6 +54,10 @@ desde Bacano. Leer también `STOREFRONT_FORMS.md` completo desde el paquete SDK.
41
54
  - Error de red: estado recuperable sin promesa falsa de recepción.
42
55
  - Persistencia: la respuesta aparece una sola vez en Bacano con versión, origen y
43
56
  snapshot correctos.
57
+ - Autocompletado: completar datos con el navegador o un gestor de contraseñas no
58
+ llena el honeypot ni produce `responseId: null`.
59
+ - Semántica: correo y teléfono usan `email` y `phone`; no se infieren por label o
60
+ key.
44
61
  - Accesibilidad: label asociado, foco, teclado, mensajes de error y progreso de
45
62
  pasos comprensibles.
46
63
  - Static First: el HTML exportado conserva el formulario cuando corresponde.
package/types.d.ts CHANGED
@@ -224,11 +224,15 @@ export interface WebsiteFormResponseInput {
224
224
  idempotencyKey: string;
225
225
  values: Record<string, WebsiteFormResponseValue>;
226
226
  source?: WebsiteFormResponseSource;
227
- /** Bot-trap field. Storefronts should render it visually hidden and leave it empty. */
227
+ /**
228
+ * Bot-trap field, not a token or nonce. Omit it unless a real hidden trap is
229
+ * rendered; legitimate visitors must always submit an empty value.
230
+ */
228
231
  honeypot?: string;
229
232
  }
230
233
  export interface WebsiteFormResponseResult {
231
234
  accepted: boolean;
235
+ /** A null value is not proof of persistence (for example, an anti-bot response). */
232
236
  responseId: string | null;
233
237
  message: string;
234
238
  }