@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 +8 -0
- package/STOREFRONT_CONTENT_LOCATOR.md +111 -0
- package/STOREFRONT_FORMS.md +69 -5
- package/content-entry-locator.d.ts +16 -0
- package/index.cjs +23 -0
- package/index.d.ts +2 -0
- package/index.js +23 -0
- package/package.json +1 -1
- package/react/form-field-control.d.ts +1 -0
- package/react/forms.cjs +14 -4
- package/react/forms.js +15 -4
- package/skills/bacano-figma-web/SKILL.md +8 -0
- package/skills/bacano-figma-web/references/bacano-mapping.md +5 -3
- package/skills/bacano-figma-web/references/content-workflow.md +11 -1
- package/skills/bacano-figma-web/references/implementation-quality.md +12 -0
- package/skills/bacano-web/SKILL.md +4 -1
- package/skills/bacano-web/references/common.md +7 -0
- package/skills/bacano-web/references/forms.md +19 -2
- package/types.d.ts +5 -1
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.
|
package/STOREFRONT_FORMS.md
CHANGED
|
@@ -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
|
-
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
-
|
|
147
|
-
|
|
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
|
@@ -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:
|
|
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": `${
|
|
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: `${
|
|
219
|
-
htmlFor: field.type === "radio" ? void 0 :
|
|
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:
|
|
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": `${
|
|
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: `${
|
|
200
|
-
htmlFor: field.type === "radio" ? void 0 :
|
|
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.
|
|
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`;
|
|
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
|
-
-
|
|
31
|
-
|
|
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
|
-
/**
|
|
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
|
}
|