@bacano/sdk 1.1.12 → 1.1.13
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/CONTENT_TYPE_MANAGEMENT.md +238 -29
- package/README.md +17 -13
- package/content-management-types.d.ts +260 -0
- package/content-management.cjs +68 -4
- package/content-management.d.ts +2 -132
- package/content-management.js +68 -4
- package/package.json +1 -1
- package/skills/bacano-figma-web/SKILL.md +23 -9
- package/skills/bacano-figma-web/references/bacano-mapping.md +3 -3
- package/skills/bacano-figma-web/references/content-workflow.md +47 -14
- package/skills/bacano-figma-web/references/extraction-contract.md +3 -0
- package/skills/bacano-figma-web/references/implementation-quality.md +67 -0
- package/skills/bacano-figma-web/references/visual-validation.md +4 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# Gestión
|
|
1
|
+
# Gestión segura de Content para agentes
|
|
2
2
|
|
|
3
3
|
`@bacano/sdk/content-management` permite que una herramienta administrativa o
|
|
4
|
-
un agente autorizado consulte
|
|
5
|
-
Bacano Content.
|
|
6
|
-
|
|
4
|
+
un agente autorizado consulte y mantenga **tipos, componentes, grupos, listas
|
|
5
|
+
de opciones y entradas** de Bacano Content. Cada capacidad requiere un permiso explícito. El acceso no
|
|
6
|
+
autoriza productos, ventas, clientes, configuración general, GraphQL, SQL ni
|
|
7
|
+
Storage.
|
|
7
8
|
|
|
8
9
|
Este contrato es independiente del cliente público del storefront. Debe usarse
|
|
9
10
|
solo desde un proceso local o servidor confiable; nunca desde el navegador ni
|
|
@@ -13,17 +14,34 @@ dentro del bundle del sitio web.
|
|
|
13
14
|
|
|
14
15
|
1. En Bacano, abre **Sitios web → Contenido → Tipos** o **Componentes**.
|
|
15
16
|
2. Selecciona **Conectar agente**.
|
|
16
|
-
3. Elige 15, 30 o 60 minutos y genera el acceso.
|
|
17
|
+
3. Elige el alcance, 15, 30 o 60 minutos y genera el acceso.
|
|
17
18
|
4. Copia el token al entorno local del agente.
|
|
18
19
|
|
|
19
|
-
El token está firmado, pertenece a una sola empresa y
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
El token está firmado con un secreto exclusivo, pertenece a una sola empresa y
|
|
21
|
+
usuario y está respaldado por una concesión revocable. Cada petición comprueba
|
|
22
|
+
la firma, vigencia, concesión, scopes exactos y que el usuario continúa
|
|
23
|
+
vinculado a la empresa con `websites.manage`. Al vencer, revocarse o perder ese
|
|
24
|
+
permiso deja de funcionar.
|
|
25
|
+
|
|
26
|
+
Scopes disponibles:
|
|
27
|
+
|
|
28
|
+
- `content_types:manage`: tipos, componentes, listas de opciones, referencias y
|
|
29
|
+
sus ciclos de vida.
|
|
30
|
+
- `content_entries:manage`: grupos, entradas en borrador y sus ciclos de vida.
|
|
31
|
+
- `content_entries:publish`: publicar y modificar contenido publicado; requiere
|
|
32
|
+
también `content_entries:manage`.
|
|
33
|
+
- `content_media:upload`: reservado para una fase posterior; no se concede
|
|
34
|
+
mientras la carga administrativa de media no esté implementada.
|
|
23
35
|
|
|
24
36
|
No guardes el token en Git, Figma, Content, logs, variables públicas ni archivos
|
|
25
37
|
que se empaqueten con el storefront.
|
|
26
38
|
|
|
39
|
+
Para desplegar Bacano, la API requiere
|
|
40
|
+
`CONTENT_MANAGEMENT_TOKEN_SECRET`: un secreto aleatorio de al menos 32
|
|
41
|
+
caracteres, exclusivo de esta capacidad y distinto de `API_SECRET`, JWT y los
|
|
42
|
+
secretos de Hasura. Debe configurarse como secreto del entorno; nunca se entrega
|
|
43
|
+
al agente. Rotarlo invalida inmediatamente todos los accesos temporales activos.
|
|
44
|
+
|
|
27
45
|
## Crear el cliente
|
|
28
46
|
|
|
29
47
|
```ts
|
|
@@ -35,7 +53,7 @@ const content = createContentManagementClient({
|
|
|
35
53
|
});
|
|
36
54
|
|
|
37
55
|
const context = await content.getContext();
|
|
38
|
-
console.log(context.companyId, context.expiresAt);
|
|
56
|
+
console.log(context.companyId, context.scopes, context.expiresAt);
|
|
39
57
|
```
|
|
40
58
|
|
|
41
59
|
El cliente ofrece:
|
|
@@ -47,6 +65,75 @@ El cliente ofrece:
|
|
|
47
65
|
- `createType(input)` y `updateType(idOrKey, input)`.
|
|
48
66
|
- `inspectLifecycle(idOrKey, operation)`.
|
|
49
67
|
- `applyLifecycle(idOrKey, operation)`.
|
|
68
|
+
- `listEntries(filters)`, `getEntry(idOrKey, locale?)`.
|
|
69
|
+
- `createEntry(input)` y `updateEntry(idOrKey, input)`.
|
|
70
|
+
- `inspectEntryLifecycle(idOrKey, operation)`.
|
|
71
|
+
- `applyEntryLifecycle(idOrKey, operation)`.
|
|
72
|
+
- `listGroups()`, `getGroup(idOrKey)`, `createGroup(input)` y
|
|
73
|
+
`updateGroup(idOrKey, input)`.
|
|
74
|
+
- `inspectGroupLifecycle(idOrKey, operation)` y
|
|
75
|
+
`applyGroupLifecycle(idOrKey, operation)`.
|
|
76
|
+
- `listOptionLists()`, `getOptionList(idOrKey)`, `createOptionList(input)` y
|
|
77
|
+
`updateOptionList(idOrKey, input)`.
|
|
78
|
+
- `inspectOptionListLifecycle(idOrKey, operation)` y
|
|
79
|
+
`applyOptionListLifecycle(idOrKey, operation)`.
|
|
80
|
+
|
|
81
|
+
## Idioma, nombres y keys
|
|
82
|
+
|
|
83
|
+
Antes de proponer una estructura, resolver su idioma en este orden:
|
|
84
|
+
|
|
85
|
+
1. El idioma solicitado expresamente por el usuario.
|
|
86
|
+
2. La convención predominante del proyecto existente.
|
|
87
|
+
3. El idioma principal del website.
|
|
88
|
+
4. Si todavía existe una ambigüedad material, preguntar al usuario.
|
|
89
|
+
|
|
90
|
+
Para un website en español, los nombres visibles y las keys nuevas se crean en
|
|
91
|
+
español salvo instrucción contraria. Esta regla aplica a identificadores
|
|
92
|
+
definidos por la empresa; los valores reservados del SDK como `entry`,
|
|
93
|
+
`component`, `draft`, `website_content`, `text` o `repeater` no se traducen ni
|
|
94
|
+
se reemplazan por valores inventados.
|
|
95
|
+
|
|
96
|
+
Usar estas convenciones:
|
|
97
|
+
|
|
98
|
+
| Elemento | Convención | Ejemplo |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| Nombre visible | Lenguaje natural, con tildes | `Preguntas frecuentes` |
|
|
101
|
+
| Key de tipo, componente, sección o campo | `snake_case`, sin tildes | `preguntas_frecuentes` |
|
|
102
|
+
| Key o slug público de una entrada | `kebab-case`, sin tildes | `preguntas-frecuentes-inicio` |
|
|
103
|
+
|
|
104
|
+
Las keys deben describir el significado del contenido, no detalles accidentales
|
|
105
|
+
de su implementación visual. Preferir `titulo`, `descripcion`,
|
|
106
|
+
`imagen_principal`, `enlace_contacto`, `preguntas`, `planes` o `pasos`. Evitar
|
|
107
|
+
identificadores opacos como `data`, `content`, `items`, `field1`,
|
|
108
|
+
`step1_title` o `component_2` cuando existe un nombre semántico claro.
|
|
109
|
+
|
|
110
|
+
Para elementos repetidos, modelar un repetible y, cuando corresponda, un
|
|
111
|
+
componente semántico. Por ejemplo, usar `pasos` con un componente `paso` que
|
|
112
|
+
contenga `titulo` y `descripcion`, en vez de crear `paso1_titulo`,
|
|
113
|
+
`paso1_descripcion`, `paso2_titulo` y campos sucesivos.
|
|
114
|
+
|
|
115
|
+
Antes de crear algo nuevo:
|
|
116
|
+
|
|
117
|
+
1. Consultar `listTypes()` y `getReferences()`.
|
|
118
|
+
2. Reutilizar una estructura solo cuando conserva el mismo significado y la
|
|
119
|
+
misma forma, no únicamente porque algunos campos coinciden.
|
|
120
|
+
3. Dar un nombre neutral a una estructura reutilizable, por ejemplo
|
|
121
|
+
`seccion_preguntas_frecuentes`.
|
|
122
|
+
4. Incluir el contexto en el nombre cuando la estructura sea realmente
|
|
123
|
+
exclusiva, por ejemplo `calculadora_eficiencia_inicio`.
|
|
124
|
+
5. Evitar nombres demasiado genéricos como `contenido` y nombres que quedarían
|
|
125
|
+
engañosos al reutilizarse, como `contenedor_principal_servicios` para una
|
|
126
|
+
página de blog.
|
|
127
|
+
|
|
128
|
+
Una key activa es un contrato con el storefront. Nunca traducir, regenerar ni
|
|
129
|
+
renombrar automáticamente keys existentes. La convención de idioma aplica a
|
|
130
|
+
estructuras nuevas; cambiar una key utilizada requiere tratarlo como una
|
|
131
|
+
migración explícita y presentar su impacto. Tampoco crear sufijos como `_2` o
|
|
132
|
+
`-2` sin investigar primero el identificador existente.
|
|
133
|
+
|
|
134
|
+
Antes de ejecutar `createType()`, presentar al usuario el nombre, key, idioma,
|
|
135
|
+
uso, campos, componentes y decisión de creación o reutilización. Corregir
|
|
136
|
+
mezclas de idiomas y errores ortográficos antes de materializar el blueprint.
|
|
50
137
|
|
|
51
138
|
## Flujo seguro para un agente
|
|
52
139
|
|
|
@@ -71,13 +158,13 @@ como una eliminación estructural y puede requerir
|
|
|
71
158
|
|
|
72
159
|
```ts
|
|
73
160
|
await content.createType({
|
|
74
|
-
key: "
|
|
161
|
+
key: "boton_llamada_accion",
|
|
75
162
|
name: "Botón de llamada a la acción",
|
|
76
163
|
kind: "component",
|
|
77
164
|
status: "draft",
|
|
78
165
|
fields: [
|
|
79
166
|
{
|
|
80
|
-
key: "
|
|
167
|
+
key: "texto",
|
|
81
168
|
label: "Texto",
|
|
82
169
|
type: "text",
|
|
83
170
|
required: true,
|
|
@@ -85,7 +172,7 @@ await content.createType({
|
|
|
85
172
|
settings: { isVisible: true, maxLength: 80 },
|
|
86
173
|
},
|
|
87
174
|
{
|
|
88
|
-
key: "
|
|
175
|
+
key: "enlace",
|
|
89
176
|
label: "Enlace",
|
|
90
177
|
type: "link",
|
|
91
178
|
required: true,
|
|
@@ -96,38 +183,38 @@ await content.createType({
|
|
|
96
183
|
});
|
|
97
184
|
|
|
98
185
|
await content.createType({
|
|
99
|
-
key: "
|
|
186
|
+
key: "hero_inicio",
|
|
100
187
|
name: "Hero del inicio",
|
|
101
188
|
kind: "entry",
|
|
102
189
|
status: "draft",
|
|
103
190
|
usages: ["website_content"],
|
|
104
|
-
sections: [{ key: "
|
|
191
|
+
sections: [{ key: "contenido", title: "Contenido", sortOrder: 1 }],
|
|
105
192
|
fields: [
|
|
106
193
|
{
|
|
107
|
-
key: "
|
|
194
|
+
key: "titulo",
|
|
108
195
|
label: "Título",
|
|
109
196
|
type: "text",
|
|
110
197
|
required: true,
|
|
111
198
|
sortOrder: 1,
|
|
112
|
-
sectionKey: "
|
|
199
|
+
sectionKey: "contenido",
|
|
113
200
|
settings: { isVisible: true, maxLength: 120 },
|
|
114
201
|
},
|
|
115
202
|
{
|
|
116
|
-
key: "
|
|
203
|
+
key: "acciones",
|
|
117
204
|
label: "Acciones",
|
|
118
205
|
type: "repeater",
|
|
119
206
|
required: false,
|
|
120
207
|
sortOrder: 2,
|
|
121
|
-
sectionKey: "
|
|
208
|
+
sectionKey: "contenido",
|
|
122
209
|
settings: { isVisible: true, minRows: 0, maxRows: 3 },
|
|
123
210
|
children: [
|
|
124
211
|
{
|
|
125
|
-
key: "
|
|
212
|
+
key: "boton",
|
|
126
213
|
label: "Botón",
|
|
127
214
|
type: "component",
|
|
128
215
|
required: true,
|
|
129
216
|
sortOrder: 1,
|
|
130
|
-
componentKey: "
|
|
217
|
+
componentKey: "boton_llamada_accion",
|
|
131
218
|
settings: { isVisible: true },
|
|
132
219
|
},
|
|
133
220
|
],
|
|
@@ -166,10 +253,10 @@ otros usos editoriales son `website_content` y `product_additional`.
|
|
|
166
253
|
## Ciclo de vida
|
|
167
254
|
|
|
168
255
|
```ts
|
|
169
|
-
const preview = await content.inspectLifecycle("
|
|
256
|
+
const preview = await content.inspectLifecycle("hero_inicio", "delete");
|
|
170
257
|
|
|
171
258
|
if (preview.allowed && userConfirmed) {
|
|
172
|
-
await content.applyLifecycle("
|
|
259
|
+
await content.applyLifecycle("hero_inicio", "delete");
|
|
173
260
|
}
|
|
174
261
|
```
|
|
175
262
|
|
|
@@ -178,15 +265,137 @@ eliminar cuando existe contenido histórico. El backend aplica las mismas reglas
|
|
|
178
265
|
de dependencias, keys, componentes, secciones y cambios destructivos que el
|
|
179
266
|
backoffice; el SDK no crea un camino alterno.
|
|
180
267
|
|
|
268
|
+
## Entradas
|
|
269
|
+
|
|
270
|
+
Antes de crear una entrada, confirma que su tipo existe y que la estructura
|
|
271
|
+
aprobada ya está activa. Las entradas nacen en `draft` salvo que el usuario haya
|
|
272
|
+
concedido explícitamente `content_entries:publish`.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
const existing = await content.listEntries({
|
|
276
|
+
contentType: "hero_inicio",
|
|
277
|
+
locale: "es-CO",
|
|
278
|
+
limit: 100,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const current = existing.entries.find(
|
|
282
|
+
(entry) => entry.key === "hero-principal-inicio",
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (!current) {
|
|
286
|
+
await content.createEntry({
|
|
287
|
+
contentType: "hero_inicio",
|
|
288
|
+
key: "hero-principal-inicio",
|
|
289
|
+
name: "Hero principal del inicio",
|
|
290
|
+
locale: "es-CO",
|
|
291
|
+
status: "draft",
|
|
292
|
+
content: {
|
|
293
|
+
titulo: "Encuentra lo mejor para cada etapa",
|
|
294
|
+
descripcion: "Compra productos seleccionados para tu familia.",
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
await content.updateEntry(current.id, {
|
|
299
|
+
contentType: current.contentType.id,
|
|
300
|
+
key: current.key,
|
|
301
|
+
name: current.name,
|
|
302
|
+
locale: current.locale,
|
|
303
|
+
status: "draft",
|
|
304
|
+
content: {
|
|
305
|
+
...current.content,
|
|
306
|
+
titulo: "Encuentra lo mejor para cada etapa",
|
|
307
|
+
},
|
|
308
|
+
expectedUpdatedAt: current.updatedAt,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Reglas obligatorias:
|
|
314
|
+
|
|
315
|
+
1. Inventariar por tipo, key y locale antes de crear; nunca fusionar por nombre.
|
|
316
|
+
2. Usar `expectedUpdatedAt` al actualizar. Un conflicto exige volver a consultar
|
|
317
|
+
y presentar el cambio; no se sobrescribe silenciosamente.
|
|
318
|
+
3. Enviar el documento completo que debe permanecer. `updateEntry()` reemplaza
|
|
319
|
+
el contenido de la entrada después de aplicar las validaciones oficiales.
|
|
320
|
+
4. Reutilizar únicamente `fileId` ya existentes y pertenecientes a la misma
|
|
321
|
+
empresa. Este acceso no sube media ni acepta URLs arbitrarias.
|
|
322
|
+
5. Rich text usa el JSON controlado por Bacano, nunca HTML crudo.
|
|
323
|
+
6. Publicar, retirar o modificar una entrada publicada requiere
|
|
324
|
+
`content_entries:publish` y aprobación humana explícita.
|
|
325
|
+
7. Inspeccionar el ciclo de vida antes de archivar, restaurar o eliminar.
|
|
326
|
+
|
|
327
|
+
La API reutiliza el mismo servicio de guardado del backoffice: campos
|
|
328
|
+
obligatorios, componentes anidados, repetibles, listas, referencias, rich text,
|
|
329
|
+
media y pertenencia a la empresa se validan de forma autoritativa.
|
|
330
|
+
|
|
331
|
+
## Grupos y listas de opciones
|
|
332
|
+
|
|
333
|
+
Los grupos organizan entradas y usan el scope `content_entries:manage`. Las
|
|
334
|
+
listas proporcionan valores controlados para campos `select`, usan
|
|
335
|
+
`content_types:manage` y conservan sus relaciones padre-hijo.
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const group = await content.createGroup({
|
|
339
|
+
key: "pagina-principal",
|
|
340
|
+
name: "Página principal",
|
|
341
|
+
status: "active",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const materials = await content.createOptionList({
|
|
345
|
+
key: "materiales",
|
|
346
|
+
name: "Materiales",
|
|
347
|
+
status: "draft",
|
|
348
|
+
isExtensible: false,
|
|
349
|
+
items: [
|
|
350
|
+
{ value: "algodon", label: "Algodón", sortOrder: 1 },
|
|
351
|
+
{ value: "lino", label: "Lino", sortOrder: 2 },
|
|
352
|
+
],
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await content.updateOptionList(materials.id, {
|
|
356
|
+
key: materials.key,
|
|
357
|
+
name: materials.name,
|
|
358
|
+
status: "active",
|
|
359
|
+
isExtensible: materials.isExtensible,
|
|
360
|
+
items: materials.items.map((item) => ({
|
|
361
|
+
id: item.id,
|
|
362
|
+
value: item.value,
|
|
363
|
+
label: item.label,
|
|
364
|
+
parentItemId: item.parentItemId,
|
|
365
|
+
sortOrder: item.sortOrder,
|
|
366
|
+
isActive: item.isActive,
|
|
367
|
+
metadata: item.metadata,
|
|
368
|
+
})),
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`updateGroup()` y `updateOptionList()` reciben el documento completo que debe
|
|
373
|
+
permanecer. Las keys existentes son inmutables. Para retirar elementos,
|
|
374
|
+
archivar, restaurar o eliminar, primero usa la operación `inspect*Lifecycle()`.
|
|
375
|
+
El backend bloquea desactivar o retirar opciones que todavía usan listas
|
|
376
|
+
dependientes y exige `confirmDestructiveChanges` cuando corresponde; nunca
|
|
377
|
+
omitas opciones existentes por accidente en una actualización.
|
|
378
|
+
|
|
379
|
+
## Seguridad y auditoría
|
|
380
|
+
|
|
381
|
+
- El `companyId` y el usuario provienen del token; no se aceptan desde el body.
|
|
382
|
+
- Cada consulta y mutación está filtrada por la empresa firmada.
|
|
383
|
+
- UUIDs o keys de otra empresa responden como inexistentes y no se modifican.
|
|
384
|
+
- Las concesiones pueden revocarse individualmente desde Bacano.
|
|
385
|
+
- Emisión, revocación y mutaciones quedan en una auditoría inmutable sin guardar
|
|
386
|
+
el bearer token ni el documento completo.
|
|
387
|
+
- Las respuestas del endpoint usan `no-store`, límites de tamaño y rate limit.
|
|
388
|
+
- Los tokens v2 no son compatibles con tokens anteriores y tienen como máximo
|
|
389
|
+
60 minutos de vigencia.
|
|
390
|
+
|
|
181
391
|
## Límites de esta versión
|
|
182
392
|
|
|
183
|
-
- No
|
|
184
|
-
- No carga archivos ni media.
|
|
185
|
-
- No crea listas de opciones.
|
|
393
|
+
- No carga archivos ni crea media; puede relacionar media existente validada.
|
|
186
394
|
- No funciona desde el cliente público del storefront.
|
|
187
395
|
- No acepta GraphQL directo ni credenciales administrativas permanentes.
|
|
396
|
+
- No previsualiza borradores desde el storefront público.
|
|
188
397
|
|
|
189
398
|
El resultado esperado del flujo Figma es: el agente propone el blueprint, el
|
|
190
|
-
usuario habilita
|
|
191
|
-
|
|
192
|
-
|
|
399
|
+
usuario habilita únicamente los scopes necesarios, el agente crea o ajusta la
|
|
400
|
+
estructura y prepara entradas en borrador, y una persona revisa antes de
|
|
401
|
+
publicar.
|
package/README.md
CHANGED
|
@@ -21,20 +21,23 @@ React hooks are available from `@bacano/sdk/react` and require `react >= 18`
|
|
|
21
21
|
as a peer dependency. The core client has no runtime dependencies and works
|
|
22
22
|
without React.
|
|
23
23
|
|
|
24
|
-
### Gestión de
|
|
24
|
+
### Gestión de Bacano Content para agentes
|
|
25
25
|
|
|
26
|
-
Un agente autorizado puede consultar
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
Un agente autorizado puede consultar y mantener tipos, componentes, grupos,
|
|
27
|
+
listas de opciones y entradas con el entrypoint administrativo
|
|
28
|
+
`@bacano/sdk/content-management`. Cada capacidad exige un scope explícito. El
|
|
29
|
+
acceso se genera temporalmente desde Bacano, está limitado a una empresa y
|
|
30
|
+
nunca debe incluirse en el storefront:
|
|
30
31
|
|
|
31
32
|
```ts
|
|
32
33
|
import { createContentManagementClient } from "@bacano/sdk/content-management";
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
Lee [`CONTENT_TYPE_MANAGEMENT.md`](./CONTENT_TYPE_MANAGEMENT.md) antes de usarlo.
|
|
36
|
-
La guía contiene el flujo de autorización, contratos de campos
|
|
37
|
-
cambios destructivos y límites de seguridad.
|
|
37
|
+
La guía contiene el flujo de autorización, contratos de campos y entradas,
|
|
38
|
+
estados, referencias, cambios destructivos y límites de seguridad. La carga de
|
|
39
|
+
media continúa en el backoffice hasta que exista un contrato administrativo
|
|
40
|
+
oficial para ella.
|
|
38
41
|
|
|
39
42
|
### Auditoría del storefront
|
|
40
43
|
|
|
@@ -62,8 +65,8 @@ el usuario o agente invoca la skill.
|
|
|
62
65
|
### Implementación desde Figma
|
|
63
66
|
|
|
64
67
|
El SDK también incluye `bacano-figma-web`, la skill oficial para convertir un
|
|
65
|
-
diseño Figma en una web informativa o
|
|
66
|
-
First y contratos públicos de Bacano:
|
|
68
|
+
diseño Figma en una web informativa, tienda o sitio híbrido que conserve el
|
|
69
|
+
cascarón, Static First y contratos públicos de Bacano:
|
|
67
70
|
|
|
68
71
|
```bash
|
|
69
72
|
pnpm exec bacano-figma-web install --agent all
|
|
@@ -82,10 +85,11 @@ la presencia del token REST. El OAuth del MCP remoto debe confirmarse dentro del
|
|
|
82
85
|
agente con las herramientas de Figma. La extracción se conserva como snapshot
|
|
83
86
|
normalizado. Cuando el equipo de Content y desarrollo trabajan en tiempos
|
|
84
87
|
distintos, la skill genera un blueprint de entrega y permite fixtures locales
|
|
85
|
-
con el mismo contrato de los componentes.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
con el mismo contrato de los componentes. Con autorización temporal y scopes
|
|
89
|
+
explícitos, puede crear o ajustar tipos, componentes, grupos, listas y entradas
|
|
90
|
+
mediante el entrypoint administrativo oficial. Las entradas nacen en borrador;
|
|
91
|
+
publicar exige permiso y aprobación humana. Un fixture activo en producción
|
|
92
|
+
bloquea la aprobación del sitio.
|
|
89
93
|
|
|
90
94
|
El snapshot puede validarse con:
|
|
91
95
|
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
export type ContentManagementTypeKind = "entry" | "component";
|
|
2
|
+
export type ContentManagementTypeStatus = "draft" | "active" | "archived";
|
|
3
|
+
export type ContentManagementTypeUsage = "website_content" | "product_additional" | "product_specification";
|
|
4
|
+
export type ContentManagementScope = "content_types:manage" | "content_entries:manage" | "content_media:upload" | "content_entries:publish";
|
|
5
|
+
export type ContentManagementFieldType = "boolean" | "component" | "date" | "link" | "measure" | "media" | "number" | "reference" | "repeater" | "rich_text" | "rich_text_full" | "select" | "slug" | "text" | "textarea";
|
|
6
|
+
export interface ContentManagementChildFieldInput {
|
|
7
|
+
id?: string | null;
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
type: ContentManagementFieldType;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
sortOrder: number;
|
|
13
|
+
settings?: Record<string, unknown>;
|
|
14
|
+
componentKey?: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ContentManagementFieldInput extends ContentManagementChildFieldInput {
|
|
17
|
+
sectionKey?: string | null;
|
|
18
|
+
children?: ContentManagementChildFieldInput[];
|
|
19
|
+
}
|
|
20
|
+
export interface ContentManagementTypeInput {
|
|
21
|
+
key: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
kind: ContentManagementTypeKind;
|
|
25
|
+
status?: "draft" | "active";
|
|
26
|
+
settings?: Record<string, unknown>;
|
|
27
|
+
usages?: ContentManagementTypeUsage[];
|
|
28
|
+
sections?: Array<{
|
|
29
|
+
key: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
sortOrder: number;
|
|
33
|
+
}>;
|
|
34
|
+
specificationScopes?: Array<{
|
|
35
|
+
category?: string | null;
|
|
36
|
+
subcategoryId?: string | null;
|
|
37
|
+
}>;
|
|
38
|
+
deletedPaths?: string[];
|
|
39
|
+
confirmDestructiveChanges?: boolean;
|
|
40
|
+
fields?: ContentManagementFieldInput[];
|
|
41
|
+
}
|
|
42
|
+
export interface ContentManagementType extends Omit<ContentManagementTypeInput, "status"> {
|
|
43
|
+
id: string;
|
|
44
|
+
companyId: string;
|
|
45
|
+
description: string;
|
|
46
|
+
status: ContentManagementTypeStatus;
|
|
47
|
+
settings: Record<string, unknown>;
|
|
48
|
+
usages: ContentManagementTypeUsage[];
|
|
49
|
+
sections: Array<{
|
|
50
|
+
id: string;
|
|
51
|
+
key: string;
|
|
52
|
+
title: string;
|
|
53
|
+
description: string;
|
|
54
|
+
sortOrder: number;
|
|
55
|
+
}>;
|
|
56
|
+
specificationScopes: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
category: string | null;
|
|
59
|
+
subcategoryId: string | null;
|
|
60
|
+
}>;
|
|
61
|
+
fields: ContentManagementFieldInput[];
|
|
62
|
+
createdAt: string;
|
|
63
|
+
updatedAt: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ContentManagementContext {
|
|
66
|
+
companyId: string;
|
|
67
|
+
/** @deprecated Use scopes to inspect the complete grant. */
|
|
68
|
+
scope: "content_types:manage";
|
|
69
|
+
scopes: ContentManagementScope[];
|
|
70
|
+
expiresAt: string;
|
|
71
|
+
}
|
|
72
|
+
export type ContentManagementEntryStatus = "draft" | "published" | "archived";
|
|
73
|
+
export interface ContentManagementEntryInput {
|
|
74
|
+
contentType: string;
|
|
75
|
+
key: string;
|
|
76
|
+
name: string;
|
|
77
|
+
locale?: string;
|
|
78
|
+
categoryKey?: string;
|
|
79
|
+
categoryName?: string;
|
|
80
|
+
status?: Exclude<ContentManagementEntryStatus, "archived">;
|
|
81
|
+
tagIds?: string[];
|
|
82
|
+
content?: Record<string, unknown>;
|
|
83
|
+
expectedUpdatedAt?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface ContentManagementEntry {
|
|
86
|
+
id: string;
|
|
87
|
+
companyId: string;
|
|
88
|
+
contentType: {
|
|
89
|
+
id: string;
|
|
90
|
+
key: string;
|
|
91
|
+
name: string;
|
|
92
|
+
};
|
|
93
|
+
key: string;
|
|
94
|
+
name: string;
|
|
95
|
+
locale: string;
|
|
96
|
+
categoryKey: string;
|
|
97
|
+
status: ContentManagementEntryStatus;
|
|
98
|
+
tagIds: string[];
|
|
99
|
+
tags: string[];
|
|
100
|
+
content: Record<string, unknown>;
|
|
101
|
+
publishedAt: string | null;
|
|
102
|
+
createdAt: string;
|
|
103
|
+
updatedAt: string;
|
|
104
|
+
}
|
|
105
|
+
export interface ContentManagementEntryListInput {
|
|
106
|
+
contentType?: string;
|
|
107
|
+
locale?: string;
|
|
108
|
+
status?: ContentManagementEntryStatus;
|
|
109
|
+
limit?: number;
|
|
110
|
+
offset?: number;
|
|
111
|
+
}
|
|
112
|
+
export interface ContentManagementEntryList {
|
|
113
|
+
entries: ContentManagementEntry[];
|
|
114
|
+
total: number;
|
|
115
|
+
}
|
|
116
|
+
export interface ContentManagementReferences {
|
|
117
|
+
categories: Array<{
|
|
118
|
+
code: string;
|
|
119
|
+
name: string;
|
|
120
|
+
subcategories: Array<{
|
|
121
|
+
id: string;
|
|
122
|
+
name: string;
|
|
123
|
+
}>;
|
|
124
|
+
}>;
|
|
125
|
+
optionLists: Array<{
|
|
126
|
+
key: string;
|
|
127
|
+
name: string;
|
|
128
|
+
isExtensible: boolean;
|
|
129
|
+
options: Array<{
|
|
130
|
+
value: string;
|
|
131
|
+
label: string;
|
|
132
|
+
}>;
|
|
133
|
+
}>;
|
|
134
|
+
measureDimensions: Array<{
|
|
135
|
+
code: string;
|
|
136
|
+
name: string;
|
|
137
|
+
canonicalUnitCode: string;
|
|
138
|
+
units: Array<{
|
|
139
|
+
code: string;
|
|
140
|
+
name: string;
|
|
141
|
+
symbol: string;
|
|
142
|
+
}>;
|
|
143
|
+
}>;
|
|
144
|
+
}
|
|
145
|
+
export type ContentManagementGroupStatus = "active" | "inactive";
|
|
146
|
+
export interface ContentManagementGroupInput {
|
|
147
|
+
key: string;
|
|
148
|
+
name: string;
|
|
149
|
+
description?: string;
|
|
150
|
+
sortOrder?: number;
|
|
151
|
+
status?: ContentManagementGroupStatus;
|
|
152
|
+
}
|
|
153
|
+
export interface ContentManagementGroup {
|
|
154
|
+
id: string;
|
|
155
|
+
companyId: string;
|
|
156
|
+
key: string;
|
|
157
|
+
name: string;
|
|
158
|
+
description: string;
|
|
159
|
+
sortOrder: number;
|
|
160
|
+
status: ContentManagementGroupStatus;
|
|
161
|
+
createdAt: string;
|
|
162
|
+
updatedAt: string;
|
|
163
|
+
}
|
|
164
|
+
export type ContentManagementOptionListStatus = "draft" | "active" | "archived";
|
|
165
|
+
export interface ContentManagementOptionListItemInput {
|
|
166
|
+
id?: string | null;
|
|
167
|
+
value: string;
|
|
168
|
+
label: string;
|
|
169
|
+
parentItemId?: string | null;
|
|
170
|
+
sortOrder: number;
|
|
171
|
+
isActive?: boolean;
|
|
172
|
+
metadata?: Record<string, unknown>;
|
|
173
|
+
}
|
|
174
|
+
export interface ContentManagementOptionListInput {
|
|
175
|
+
key: string;
|
|
176
|
+
name: string;
|
|
177
|
+
description?: string;
|
|
178
|
+
status?: Exclude<ContentManagementOptionListStatus, "archived">;
|
|
179
|
+
parentListId?: string | null;
|
|
180
|
+
isExtensible?: boolean;
|
|
181
|
+
confirmDestructiveChanges?: boolean;
|
|
182
|
+
items?: ContentManagementOptionListItemInput[];
|
|
183
|
+
}
|
|
184
|
+
export interface ContentManagementOptionListItem {
|
|
185
|
+
id: string;
|
|
186
|
+
value: string;
|
|
187
|
+
label: string;
|
|
188
|
+
parentItemId: string | null;
|
|
189
|
+
sortOrder: number;
|
|
190
|
+
isActive: boolean;
|
|
191
|
+
metadata: Record<string, unknown>;
|
|
192
|
+
createdAt: string;
|
|
193
|
+
updatedAt: string;
|
|
194
|
+
}
|
|
195
|
+
export interface ContentManagementOptionList {
|
|
196
|
+
id: string;
|
|
197
|
+
companyId: string;
|
|
198
|
+
key: string;
|
|
199
|
+
name: string;
|
|
200
|
+
description: string;
|
|
201
|
+
status: ContentManagementOptionListStatus;
|
|
202
|
+
parentListId: string | null;
|
|
203
|
+
isExtensible: boolean;
|
|
204
|
+
items: ContentManagementOptionListItem[];
|
|
205
|
+
createdAt: string;
|
|
206
|
+
updatedAt: string;
|
|
207
|
+
}
|
|
208
|
+
export type ContentManagementLifecycleOperation = "archive" | "restore" | "delete";
|
|
209
|
+
export interface ContentManagementLifecycleResult {
|
|
210
|
+
success: boolean;
|
|
211
|
+
allowed: boolean;
|
|
212
|
+
applied: boolean;
|
|
213
|
+
entity_id: string | null;
|
|
214
|
+
entity_type: "type" | "component" | "entry" | "group" | "option_list" | null;
|
|
215
|
+
operation: ContentManagementLifecycleOperation | null;
|
|
216
|
+
name: string | null;
|
|
217
|
+
status: string | null;
|
|
218
|
+
dependencies: Array<{
|
|
219
|
+
code: string;
|
|
220
|
+
label: string;
|
|
221
|
+
count: number;
|
|
222
|
+
blocking: boolean;
|
|
223
|
+
}>;
|
|
224
|
+
message: string | null;
|
|
225
|
+
errors: string[];
|
|
226
|
+
}
|
|
227
|
+
export type ContentManagementAccessTokenProvider = () => string | Promise<string>;
|
|
228
|
+
export interface ContentManagementClientConfig {
|
|
229
|
+
apiUrl: string;
|
|
230
|
+
accessToken: string | ContentManagementAccessTokenProvider;
|
|
231
|
+
fetch?: typeof fetch;
|
|
232
|
+
}
|
|
233
|
+
export interface ContentManagementClient {
|
|
234
|
+
getContext(): Promise<ContentManagementContext>;
|
|
235
|
+
getReferences(): Promise<ContentManagementReferences>;
|
|
236
|
+
listTypes(): Promise<ContentManagementType[]>;
|
|
237
|
+
getType(identifier: string): Promise<ContentManagementType>;
|
|
238
|
+
createType(input: ContentManagementTypeInput): Promise<ContentManagementType>;
|
|
239
|
+
updateType(identifier: string, input: ContentManagementTypeInput): Promise<ContentManagementType>;
|
|
240
|
+
inspectLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
241
|
+
applyLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
242
|
+
listEntries(input?: ContentManagementEntryListInput): Promise<ContentManagementEntryList>;
|
|
243
|
+
getEntry(identifier: string, locale?: string): Promise<ContentManagementEntry>;
|
|
244
|
+
createEntry(input: ContentManagementEntryInput): Promise<ContentManagementEntry>;
|
|
245
|
+
updateEntry(identifier: string, input: ContentManagementEntryInput): Promise<ContentManagementEntry>;
|
|
246
|
+
inspectEntryLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
247
|
+
applyEntryLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
248
|
+
listGroups(): Promise<ContentManagementGroup[]>;
|
|
249
|
+
getGroup(identifier: string): Promise<ContentManagementGroup>;
|
|
250
|
+
createGroup(input: ContentManagementGroupInput): Promise<ContentManagementGroup>;
|
|
251
|
+
updateGroup(identifier: string, input: ContentManagementGroupInput): Promise<ContentManagementGroup>;
|
|
252
|
+
inspectGroupLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
253
|
+
applyGroupLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
254
|
+
listOptionLists(): Promise<ContentManagementOptionList[]>;
|
|
255
|
+
getOptionList(identifier: string): Promise<ContentManagementOptionList>;
|
|
256
|
+
createOptionList(input: ContentManagementOptionListInput): Promise<ContentManagementOptionList>;
|
|
257
|
+
updateOptionList(identifier: string, input: ContentManagementOptionListInput): Promise<ContentManagementOptionList>;
|
|
258
|
+
inspectOptionListLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
259
|
+
applyOptionListLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
260
|
+
}
|
package/content-management.cjs
CHANGED
|
@@ -58,6 +58,16 @@ function encodeIdentifier(identifier) {
|
|
|
58
58
|
}
|
|
59
59
|
return encodeURIComponent(normalized);
|
|
60
60
|
}
|
|
61
|
+
function entryListPath(input = {}) {
|
|
62
|
+
const search = new URLSearchParams();
|
|
63
|
+
if (input.contentType) search.set("contentType", input.contentType);
|
|
64
|
+
if (input.locale) search.set("locale", input.locale);
|
|
65
|
+
if (input.status) search.set("status", input.status);
|
|
66
|
+
if (input.limit !== void 0) search.set("limit", String(input.limit));
|
|
67
|
+
if (input.offset !== void 0) search.set("offset", String(input.offset));
|
|
68
|
+
const query = search.toString();
|
|
69
|
+
return query ? `/entries?${query}` : "/entries";
|
|
70
|
+
}
|
|
61
71
|
function createContentManagementClient(config) {
|
|
62
72
|
const apiUrl = normalizeApiUrl(config.apiUrl);
|
|
63
73
|
const fetchImplementation = config.fetch ?? globalThis.fetch;
|
|
@@ -109,8 +119,8 @@ function createContentManagementClient(config) {
|
|
|
109
119
|
}
|
|
110
120
|
return payload.data;
|
|
111
121
|
};
|
|
112
|
-
const lifecycle = (identifier, operation, dryRun) => request(
|
|
113
|
-
|
|
122
|
+
const lifecycle = (resource, identifier, operation, dryRun) => request(
|
|
123
|
+
`/${resource}/${encodeIdentifier(identifier)}/lifecycle`,
|
|
114
124
|
{
|
|
115
125
|
method: "POST",
|
|
116
126
|
body: JSON.stringify({ operation, dryRun })
|
|
@@ -129,7 +139,61 @@ function createContentManagementClient(config) {
|
|
|
129
139
|
method: "PUT",
|
|
130
140
|
body: JSON.stringify(input)
|
|
131
141
|
}),
|
|
132
|
-
inspectLifecycle: (identifier, operation) => lifecycle(identifier, operation, true),
|
|
133
|
-
applyLifecycle: (identifier, operation) => lifecycle(identifier, operation, false)
|
|
142
|
+
inspectLifecycle: (identifier, operation) => lifecycle("types", identifier, operation, true),
|
|
143
|
+
applyLifecycle: (identifier, operation) => lifecycle("types", identifier, operation, false),
|
|
144
|
+
listEntries: (input) => request(entryListPath(input)),
|
|
145
|
+
getEntry: (identifier, locale) => {
|
|
146
|
+
const query = locale ? `?locale=${encodeURIComponent(locale.trim())}` : "";
|
|
147
|
+
return request(
|
|
148
|
+
`/entries/${encodeIdentifier(identifier)}${query}`
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
createEntry: (input) => request("/entries", {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: JSON.stringify(input)
|
|
154
|
+
}),
|
|
155
|
+
updateEntry: (identifier, input) => request(
|
|
156
|
+
`/entries/${encodeIdentifier(identifier)}`,
|
|
157
|
+
{
|
|
158
|
+
method: "PUT",
|
|
159
|
+
body: JSON.stringify(input)
|
|
160
|
+
}
|
|
161
|
+
),
|
|
162
|
+
inspectEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, true),
|
|
163
|
+
applyEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, false),
|
|
164
|
+
listGroups: () => request("/groups"),
|
|
165
|
+
getGroup: (identifier) => request(
|
|
166
|
+
`/groups/${encodeIdentifier(identifier)}`
|
|
167
|
+
),
|
|
168
|
+
createGroup: (input) => request("/groups", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: JSON.stringify(input)
|
|
171
|
+
}),
|
|
172
|
+
updateGroup: (identifier, input) => request(
|
|
173
|
+
`/groups/${encodeIdentifier(identifier)}`,
|
|
174
|
+
{
|
|
175
|
+
method: "PUT",
|
|
176
|
+
body: JSON.stringify(input)
|
|
177
|
+
}
|
|
178
|
+
),
|
|
179
|
+
inspectGroupLifecycle: (identifier, operation) => lifecycle("groups", identifier, operation, true),
|
|
180
|
+
applyGroupLifecycle: (identifier, operation) => lifecycle("groups", identifier, operation, false),
|
|
181
|
+
listOptionLists: () => request("/option-lists"),
|
|
182
|
+
getOptionList: (identifier) => request(
|
|
183
|
+
`/option-lists/${encodeIdentifier(identifier)}`
|
|
184
|
+
),
|
|
185
|
+
createOptionList: (input) => request("/option-lists", {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: JSON.stringify(input)
|
|
188
|
+
}),
|
|
189
|
+
updateOptionList: (identifier, input) => request(
|
|
190
|
+
`/option-lists/${encodeIdentifier(identifier)}`,
|
|
191
|
+
{
|
|
192
|
+
method: "PUT",
|
|
193
|
+
body: JSON.stringify(input)
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
inspectOptionListLifecycle: (identifier, operation) => lifecycle("option-lists", identifier, operation, true),
|
|
197
|
+
applyOptionListLifecycle: (identifier, operation) => lifecycle("option-lists", identifier, operation, false)
|
|
134
198
|
};
|
|
135
199
|
}
|
package/content-management.d.ts
CHANGED
|
@@ -1,125 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
export type ContentManagementTypeUsage = "website_content" | "product_additional" | "product_specification";
|
|
4
|
-
export type ContentManagementFieldType = "boolean" | "component" | "date" | "link" | "measure" | "media" | "number" | "reference" | "repeater" | "rich_text" | "rich_text_full" | "select" | "slug" | "text" | "textarea";
|
|
5
|
-
export interface ContentManagementChildFieldInput {
|
|
6
|
-
id?: string | null;
|
|
7
|
-
key: string;
|
|
8
|
-
label: string;
|
|
9
|
-
type: ContentManagementFieldType;
|
|
10
|
-
required?: boolean;
|
|
11
|
-
sortOrder: number;
|
|
12
|
-
settings?: Record<string, unknown>;
|
|
13
|
-
componentKey?: string | null;
|
|
14
|
-
}
|
|
15
|
-
export interface ContentManagementFieldInput extends ContentManagementChildFieldInput {
|
|
16
|
-
sectionKey?: string | null;
|
|
17
|
-
children?: ContentManagementChildFieldInput[];
|
|
18
|
-
}
|
|
19
|
-
export interface ContentManagementTypeInput {
|
|
20
|
-
key: string;
|
|
21
|
-
name: string;
|
|
22
|
-
description?: string;
|
|
23
|
-
kind: ContentManagementTypeKind;
|
|
24
|
-
status?: "draft" | "active";
|
|
25
|
-
settings?: Record<string, unknown>;
|
|
26
|
-
usages?: ContentManagementTypeUsage[];
|
|
27
|
-
sections?: Array<{
|
|
28
|
-
key: string;
|
|
29
|
-
title: string;
|
|
30
|
-
description?: string;
|
|
31
|
-
sortOrder: number;
|
|
32
|
-
}>;
|
|
33
|
-
specificationScopes?: Array<{
|
|
34
|
-
category?: string | null;
|
|
35
|
-
subcategoryId?: string | null;
|
|
36
|
-
}>;
|
|
37
|
-
deletedPaths?: string[];
|
|
38
|
-
confirmDestructiveChanges?: boolean;
|
|
39
|
-
fields?: ContentManagementFieldInput[];
|
|
40
|
-
}
|
|
41
|
-
export interface ContentManagementType extends Omit<ContentManagementTypeInput, "status"> {
|
|
42
|
-
id: string;
|
|
43
|
-
companyId: string;
|
|
44
|
-
description: string;
|
|
45
|
-
status: ContentManagementTypeStatus;
|
|
46
|
-
settings: Record<string, unknown>;
|
|
47
|
-
usages: ContentManagementTypeUsage[];
|
|
48
|
-
sections: Array<{
|
|
49
|
-
id: string;
|
|
50
|
-
key: string;
|
|
51
|
-
title: string;
|
|
52
|
-
description: string;
|
|
53
|
-
sortOrder: number;
|
|
54
|
-
}>;
|
|
55
|
-
specificationScopes: Array<{
|
|
56
|
-
id: string;
|
|
57
|
-
category: string | null;
|
|
58
|
-
subcategoryId: string | null;
|
|
59
|
-
}>;
|
|
60
|
-
fields: ContentManagementFieldInput[];
|
|
61
|
-
createdAt: string;
|
|
62
|
-
updatedAt: string;
|
|
63
|
-
}
|
|
64
|
-
export interface ContentManagementContext {
|
|
65
|
-
companyId: string;
|
|
66
|
-
scope: "content_types:manage";
|
|
67
|
-
expiresAt: string;
|
|
68
|
-
}
|
|
69
|
-
export interface ContentManagementReferences {
|
|
70
|
-
categories: Array<{
|
|
71
|
-
code: string;
|
|
72
|
-
name: string;
|
|
73
|
-
subcategories: Array<{
|
|
74
|
-
id: string;
|
|
75
|
-
name: string;
|
|
76
|
-
}>;
|
|
77
|
-
}>;
|
|
78
|
-
optionLists: Array<{
|
|
79
|
-
key: string;
|
|
80
|
-
name: string;
|
|
81
|
-
isExtensible: boolean;
|
|
82
|
-
options: Array<{
|
|
83
|
-
value: string;
|
|
84
|
-
label: string;
|
|
85
|
-
}>;
|
|
86
|
-
}>;
|
|
87
|
-
measureDimensions: Array<{
|
|
88
|
-
code: string;
|
|
89
|
-
name: string;
|
|
90
|
-
canonicalUnitCode: string;
|
|
91
|
-
units: Array<{
|
|
92
|
-
code: string;
|
|
93
|
-
name: string;
|
|
94
|
-
symbol: string;
|
|
95
|
-
}>;
|
|
96
|
-
}>;
|
|
97
|
-
}
|
|
98
|
-
export type ContentManagementLifecycleOperation = "archive" | "restore" | "delete";
|
|
99
|
-
export interface ContentManagementLifecycleResult {
|
|
100
|
-
success: boolean;
|
|
101
|
-
allowed: boolean;
|
|
102
|
-
applied: boolean;
|
|
103
|
-
entity_id: string | null;
|
|
104
|
-
entity_type: "type" | "component" | null;
|
|
105
|
-
operation: ContentManagementLifecycleOperation | null;
|
|
106
|
-
name: string | null;
|
|
107
|
-
status: string | null;
|
|
108
|
-
dependencies: Array<{
|
|
109
|
-
code: string;
|
|
110
|
-
label: string;
|
|
111
|
-
count: number;
|
|
112
|
-
blocking: boolean;
|
|
113
|
-
}>;
|
|
114
|
-
message: string | null;
|
|
115
|
-
errors: string[];
|
|
116
|
-
}
|
|
117
|
-
export type ContentManagementAccessTokenProvider = () => string | Promise<string>;
|
|
118
|
-
export interface ContentManagementClientConfig {
|
|
119
|
-
apiUrl: string;
|
|
120
|
-
accessToken: string | ContentManagementAccessTokenProvider;
|
|
121
|
-
fetch?: typeof fetch;
|
|
122
|
-
}
|
|
1
|
+
import type { ContentManagementClient, ContentManagementClientConfig } from "./content-management-types";
|
|
2
|
+
export * from "./content-management-types";
|
|
123
3
|
export declare class ContentManagementError extends Error {
|
|
124
4
|
readonly code: string;
|
|
125
5
|
readonly status: number;
|
|
@@ -131,14 +11,4 @@ export declare class ContentManagementError extends Error {
|
|
|
131
11
|
details?: unknown;
|
|
132
12
|
});
|
|
133
13
|
}
|
|
134
|
-
export interface ContentManagementClient {
|
|
135
|
-
getContext(): Promise<ContentManagementContext>;
|
|
136
|
-
getReferences(): Promise<ContentManagementReferences>;
|
|
137
|
-
listTypes(): Promise<ContentManagementType[]>;
|
|
138
|
-
getType(identifier: string): Promise<ContentManagementType>;
|
|
139
|
-
createType(input: ContentManagementTypeInput): Promise<ContentManagementType>;
|
|
140
|
-
updateType(identifier: string, input: ContentManagementTypeInput): Promise<ContentManagementType>;
|
|
141
|
-
inspectLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
142
|
-
applyLifecycle(identifier: string, operation: ContentManagementLifecycleOperation): Promise<ContentManagementLifecycleResult>;
|
|
143
|
-
}
|
|
144
14
|
export declare function createContentManagementClient(config: ContentManagementClientConfig): ContentManagementClient;
|
package/content-management.js
CHANGED
|
@@ -33,6 +33,16 @@ function encodeIdentifier(identifier) {
|
|
|
33
33
|
}
|
|
34
34
|
return encodeURIComponent(normalized);
|
|
35
35
|
}
|
|
36
|
+
function entryListPath(input = {}) {
|
|
37
|
+
const search = new URLSearchParams();
|
|
38
|
+
if (input.contentType) search.set("contentType", input.contentType);
|
|
39
|
+
if (input.locale) search.set("locale", input.locale);
|
|
40
|
+
if (input.status) search.set("status", input.status);
|
|
41
|
+
if (input.limit !== void 0) search.set("limit", String(input.limit));
|
|
42
|
+
if (input.offset !== void 0) search.set("offset", String(input.offset));
|
|
43
|
+
const query = search.toString();
|
|
44
|
+
return query ? `/entries?${query}` : "/entries";
|
|
45
|
+
}
|
|
36
46
|
function createContentManagementClient(config) {
|
|
37
47
|
const apiUrl = normalizeApiUrl(config.apiUrl);
|
|
38
48
|
const fetchImplementation = config.fetch ?? globalThis.fetch;
|
|
@@ -84,8 +94,8 @@ function createContentManagementClient(config) {
|
|
|
84
94
|
}
|
|
85
95
|
return payload.data;
|
|
86
96
|
};
|
|
87
|
-
const lifecycle = (identifier, operation, dryRun) => request(
|
|
88
|
-
|
|
97
|
+
const lifecycle = (resource, identifier, operation, dryRun) => request(
|
|
98
|
+
`/${resource}/${encodeIdentifier(identifier)}/lifecycle`,
|
|
89
99
|
{
|
|
90
100
|
method: "POST",
|
|
91
101
|
body: JSON.stringify({ operation, dryRun })
|
|
@@ -104,8 +114,62 @@ function createContentManagementClient(config) {
|
|
|
104
114
|
method: "PUT",
|
|
105
115
|
body: JSON.stringify(input)
|
|
106
116
|
}),
|
|
107
|
-
inspectLifecycle: (identifier, operation) => lifecycle(identifier, operation, true),
|
|
108
|
-
applyLifecycle: (identifier, operation) => lifecycle(identifier, operation, false)
|
|
117
|
+
inspectLifecycle: (identifier, operation) => lifecycle("types", identifier, operation, true),
|
|
118
|
+
applyLifecycle: (identifier, operation) => lifecycle("types", identifier, operation, false),
|
|
119
|
+
listEntries: (input) => request(entryListPath(input)),
|
|
120
|
+
getEntry: (identifier, locale) => {
|
|
121
|
+
const query = locale ? `?locale=${encodeURIComponent(locale.trim())}` : "";
|
|
122
|
+
return request(
|
|
123
|
+
`/entries/${encodeIdentifier(identifier)}${query}`
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
createEntry: (input) => request("/entries", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: JSON.stringify(input)
|
|
129
|
+
}),
|
|
130
|
+
updateEntry: (identifier, input) => request(
|
|
131
|
+
`/entries/${encodeIdentifier(identifier)}`,
|
|
132
|
+
{
|
|
133
|
+
method: "PUT",
|
|
134
|
+
body: JSON.stringify(input)
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
inspectEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, true),
|
|
138
|
+
applyEntryLifecycle: (identifier, operation) => lifecycle("entries", identifier, operation, false),
|
|
139
|
+
listGroups: () => request("/groups"),
|
|
140
|
+
getGroup: (identifier) => request(
|
|
141
|
+
`/groups/${encodeIdentifier(identifier)}`
|
|
142
|
+
),
|
|
143
|
+
createGroup: (input) => request("/groups", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: JSON.stringify(input)
|
|
146
|
+
}),
|
|
147
|
+
updateGroup: (identifier, input) => request(
|
|
148
|
+
`/groups/${encodeIdentifier(identifier)}`,
|
|
149
|
+
{
|
|
150
|
+
method: "PUT",
|
|
151
|
+
body: JSON.stringify(input)
|
|
152
|
+
}
|
|
153
|
+
),
|
|
154
|
+
inspectGroupLifecycle: (identifier, operation) => lifecycle("groups", identifier, operation, true),
|
|
155
|
+
applyGroupLifecycle: (identifier, operation) => lifecycle("groups", identifier, operation, false),
|
|
156
|
+
listOptionLists: () => request("/option-lists"),
|
|
157
|
+
getOptionList: (identifier) => request(
|
|
158
|
+
`/option-lists/${encodeIdentifier(identifier)}`
|
|
159
|
+
),
|
|
160
|
+
createOptionList: (input) => request("/option-lists", {
|
|
161
|
+
method: "POST",
|
|
162
|
+
body: JSON.stringify(input)
|
|
163
|
+
}),
|
|
164
|
+
updateOptionList: (identifier, input) => request(
|
|
165
|
+
`/option-lists/${encodeIdentifier(identifier)}`,
|
|
166
|
+
{
|
|
167
|
+
method: "PUT",
|
|
168
|
+
body: JSON.stringify(input)
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
inspectOptionListLifecycle: (identifier, operation) => lifecycle("option-lists", identifier, operation, true),
|
|
172
|
+
applyOptionListLifecycle: (identifier, operation) => lifecycle("option-lists", identifier, operation, false)
|
|
109
173
|
};
|
|
110
174
|
}
|
|
111
175
|
export {
|
package/package.json
CHANGED
|
@@ -13,7 +13,9 @@ autenticación, marketing o Content por fuera de `@bacano/sdk`.
|
|
|
13
13
|
|
|
14
14
|
Antes de extraer el diseño, identificar:
|
|
15
15
|
|
|
16
|
-
- Web informativa o
|
|
16
|
+
- Web informativa, tienda virtual o sitio híbrido. Si permite una compra
|
|
17
|
+
completa, aplicar también los contratos de tienda a su superficie
|
|
18
|
+
transaccional.
|
|
17
19
|
- Enlace al archivo, página, sección o frame de Figma.
|
|
18
20
|
- Rutas que se implementarán y sus frames desktop/mobile.
|
|
19
21
|
- Repositorio y cascarón Bacano que se reutilizarán.
|
|
@@ -74,11 +76,12 @@ pnpm exec bacano-figma-web validate-snapshot .bacano/designs/<proyecto>/manifest
|
|
|
74
76
|
|
|
75
77
|
## 4. Mapear a Bacano antes de programar
|
|
76
78
|
|
|
77
|
-
Leer [references/bacano-mapping.md](references/bacano-mapping.md)
|
|
78
|
-
[references/content-workflow.md](references/content-workflow.md)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
Leer [references/bacano-mapping.md](references/bacano-mapping.md),
|
|
80
|
+
[references/content-workflow.md](references/content-workflow.md) y
|
|
81
|
+
[references/implementation-quality.md](references/implementation-quality.md).
|
|
82
|
+
Preparar una matriz ruta → frame → componente → fuente de datos → Content →
|
|
83
|
+
responsive → preview → estado, y determinar si el trabajo comienza desde
|
|
84
|
+
Figma, desde Bacano Content o desde ambas fuentes.
|
|
82
85
|
|
|
83
86
|
- Reutilizar el starter, tokens y componentes existentes.
|
|
84
87
|
- Consumir Bacano y Content mediante `@bacano/sdk`.
|
|
@@ -96,9 +99,16 @@ nunca repartir datos de prueba dentro de la UI. Si Content ya existe, mapearlo
|
|
|
96
99
|
sin sobrescribir estructuras o entradas. Si el usuario pide materializar el
|
|
97
100
|
blueprint, leer la sección de automatización de
|
|
98
101
|
[references/content-workflow.md](references/content-workflow.md) y el contrato
|
|
99
|
-
`CONTENT_TYPE_MANAGEMENT.md` incluido en `@bacano/sdk`.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
`CONTENT_TYPE_MANAGEMENT.md` incluido en `@bacano/sdk`. Crear o modificar tipos,
|
|
103
|
+
componentes, grupos, listas y entradas únicamente mediante el acceso temporal
|
|
104
|
+
y los scopes oficiales. Inventariar esos recursos antes de crear otros y
|
|
105
|
+
preparar entradas en borrador por defecto; la media permanece en el
|
|
106
|
+
backoffice y publicar requiere permiso y aprobación explícitos.
|
|
107
|
+
|
|
108
|
+
Al proponer keys nuevas, resolver primero el idioma según
|
|
109
|
+
`CONTENT_TYPE_MANAGEMENT.md`: instrucción explícita, convención existente y
|
|
110
|
+
después idioma principal del website. No traducir ni renombrar contratos ya
|
|
111
|
+
utilizados por el storefront.
|
|
102
112
|
|
|
103
113
|
Antes de modificar código, indicar brevemente qué rutas y componentes se
|
|
104
114
|
implementarán y qué decisiones de diseño son inferidas.
|
|
@@ -113,6 +123,10 @@ Trabajar por página o conjunto pequeño de secciones:
|
|
|
113
123
|
4. Integración Bacano real.
|
|
114
124
|
5. Comparación visual y correcciones.
|
|
115
125
|
|
|
126
|
+
No dar una página por validada únicamente porque compila. Comparar estructura,
|
|
127
|
+
contenido, responsive e interacciones del corte antes de continuar y registrar
|
|
128
|
+
lo pendiente cuando la revisión visual quede a cargo del usuario.
|
|
129
|
+
|
|
116
130
|
No añadir paquetes, fuentes, iconos o assets si el proyecto ya ofrece un
|
|
117
131
|
equivalente. Descargar assets mediante la fuente Figma autorizada; no incrustar
|
|
118
132
|
URLs temporales ni construir transformaciones de Storage manualmente.
|
|
@@ -7,9 +7,9 @@ negocio.
|
|
|
7
7
|
|
|
8
8
|
Antes de programar, registrar por ruta:
|
|
9
9
|
|
|
10
|
-
| Ruta | Frame | Componente | Fuente | Responsive | Estado |
|
|
11
|
-
| --- | --- | --- | --- | --- | --- |
|
|
12
|
-
| `/` | Home desktop/mobile | `Hero` | Content | 2 frames | pendiente |
|
|
10
|
+
| Ruta | Frame | Componente | Fuente | Content | Responsive | Preview | Estado |
|
|
11
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
12
|
+
| `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | 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.
|
|
@@ -19,12 +19,30 @@ Antes de crear campos o fixtures, asignar cada valor a una fuente:
|
|
|
19
19
|
No duplicar en Content información que ya tiene una fuente funcional. Un valor
|
|
20
20
|
visible en Figma no implica que sea editorial.
|
|
21
21
|
|
|
22
|
+
## Modelar sin extremos
|
|
23
|
+
|
|
24
|
+
No convertir una página extensa en un único tipo gigante ni crear un componente
|
|
25
|
+
para cada bloque visual. Separar una sección cuando tenga ciclo editorial,
|
|
26
|
+
responsable, reutilización o repetición propios. Crear un componente compartido
|
|
27
|
+
solo cuando coincidan tanto la estructura como el significado y la abstracción
|
|
28
|
+
reduzca duplicación real.
|
|
29
|
+
|
|
30
|
+
- Usar grupos para organizar entradas relacionadas por dominio o página.
|
|
31
|
+
- Usar repetibles para pasos, integrantes, preguntas, tarjetas y otras
|
|
32
|
+
colecciones homogéneas.
|
|
33
|
+
- Mantener directamente en el tipo los campos de un bloque exclusivo y simple.
|
|
34
|
+
- Crear primero listas y componentes realmente compartidos; después los tipos
|
|
35
|
+
consumidores.
|
|
36
|
+
- No hacer depender la UI del shape crudo de Content: fixture y mapper real
|
|
37
|
+
deben converger en el mismo view model.
|
|
38
|
+
|
|
22
39
|
## 2. Elegir el punto de partida
|
|
23
40
|
|
|
24
41
|
### Figma primero
|
|
25
42
|
|
|
26
43
|
1. Extraer el contenido público visible y sus assets autorizados.
|
|
27
|
-
2.
|
|
44
|
+
2. Resolver el idioma y proponer tipos, keys, campos y entradas en
|
|
45
|
+
`content-blueprint.json` siguiendo `CONTENT_TYPE_MANAGEMENT.md`.
|
|
28
46
|
3. Crear fixtures locales que implementen el view model de la página.
|
|
29
47
|
4. Implementar y validar el diseño contra esos fixtures.
|
|
30
48
|
5. Entregar el blueprint al equipo de Content para crearlo en Bacano.
|
|
@@ -66,22 +84,22 @@ Estructura recomendada:
|
|
|
66
84
|
"route": "/",
|
|
67
85
|
"sections": [
|
|
68
86
|
{
|
|
69
|
-
"sectionKey": "
|
|
87
|
+
"sectionKey": "hero-inicio",
|
|
70
88
|
"figmaNodeId": "1:20",
|
|
71
89
|
"component": "HomeHero",
|
|
72
90
|
"ownership": "content",
|
|
73
|
-
"contentTypeKey": "
|
|
74
|
-
"entryKey": "
|
|
91
|
+
"contentTypeKey": "hero_inicio",
|
|
92
|
+
"entryKey": "hero-principal-inicio",
|
|
75
93
|
"status": "missing",
|
|
76
94
|
"fields": [
|
|
77
95
|
{
|
|
78
|
-
"key": "
|
|
96
|
+
"key": "titulo",
|
|
79
97
|
"type": "text",
|
|
80
98
|
"required": true,
|
|
81
99
|
"sampleValue": "Compra para cada etapa"
|
|
82
100
|
}
|
|
83
101
|
],
|
|
84
|
-
"fixture": "fixtures/
|
|
102
|
+
"fixture": "fixtures/hero-inicio.json"
|
|
85
103
|
}
|
|
86
104
|
]
|
|
87
105
|
}
|
|
@@ -92,7 +110,9 @@ Estructura recomendada:
|
|
|
92
110
|
El blueprint es una propuesta y una evidencia de coordinación, no una orden de
|
|
93
111
|
escritura. No incluir IDs internos innecesarios, secretos, datos personales ni
|
|
94
112
|
respuestas crudas de GraphQL. Mantener keys estables y registrar cambios que
|
|
95
|
-
rompan el contrato.
|
|
113
|
+
rompan el contrato. Las propiedades técnicas del formato del blueprint
|
|
114
|
+
permanecen en inglés; los valores que representan tipos, componentes, campos y
|
|
115
|
+
entradas definidos por la empresa siguen el idioma resuelto para el proyecto.
|
|
96
116
|
|
|
97
117
|
## 4. Fixtures temporales
|
|
98
118
|
|
|
@@ -132,9 +152,10 @@ Entregar una tabla legible con:
|
|
|
132
152
|
El equipo de Content puede crear o ajustar la estructura desde Bacano. Si el
|
|
133
153
|
usuario autoriza que el agente materialice el blueprint, usar exclusivamente
|
|
134
154
|
`@bacano/sdk/content-management` y el token temporal generado desde **Conectar
|
|
135
|
-
agente**. Confirmar la empresa, inventariar primero, crear
|
|
136
|
-
sus consumidores y trabajar inicialmente en borrador. La
|
|
137
|
-
|
|
155
|
+
agente**. Confirmar la empresa y los scopes, inventariar primero, crear
|
|
156
|
+
componentes antes de sus consumidores y trabajar inicialmente en borrador. La
|
|
157
|
+
skill puede preparar entradas cuando el usuario conceda
|
|
158
|
+
`content_entries:manage`; no carga media ni publica sin el scope específico.
|
|
138
159
|
|
|
139
160
|
## 6. Cambio a Bacano real
|
|
140
161
|
|
|
@@ -159,11 +180,23 @@ escribir:
|
|
|
159
180
|
|
|
160
181
|
1. Pedir al usuario que genere un acceso temporal de 15, 30 o 60 minutos.
|
|
161
182
|
2. Guardarlo solo en el entorno local del proceso del agente.
|
|
162
|
-
3. Ejecutar `getContext()`, `
|
|
183
|
+
3. Ejecutar `getContext()`, comprobar sus `scopes`, y después `listTypes()`,
|
|
184
|
+
`listGroups()`, `listOptionLists()` y `getReferences()`.
|
|
163
185
|
4. Mostrar el plan y esperar aprobación cuando altere estructuras existentes.
|
|
164
|
-
5. Crear primero
|
|
165
|
-
|
|
186
|
+
5. Crear primero listas y componentes, luego tipos consumidores, y mantener las
|
|
187
|
+
estructuras nuevas en borrador hasta validarlas.
|
|
188
|
+
6. Si se concedió `content_entries:manage`, inventariar entradas por tipo, key y
|
|
189
|
+
locale; crear las faltantes en borrador y actualizar únicamente con
|
|
190
|
+
`expectedUpdatedAt`.
|
|
191
|
+
7. No fusionar por nombres, no usar HTML crudo y no relacionar URLs remotas. La
|
|
192
|
+
media debe existir en Bacano y pertenecer a la empresa autorizada.
|
|
193
|
+
8. Para archivar o eliminar tipos, entradas, grupos o listas, inspeccionar
|
|
194
|
+
dependencias y confirmar nuevamente. Una actualización de lista debe enviar
|
|
195
|
+
todos los elementos que deben permanecer, incluidos sus IDs existentes.
|
|
196
|
+
9. Publicar o modificar una entrada publicada solo con
|
|
197
|
+
`content_entries:publish` y una aprobación humana explícita.
|
|
166
198
|
|
|
167
199
|
Nunca exponer el token en el navegador, commit, blueprint, salida de consola o
|
|
168
200
|
chat. Nunca convertir el blueprint en una importación implícita ni usar GraphQL
|
|
169
|
-
directo.
|
|
201
|
+
directo. La carga de media permanece bajo gestión editorial en Bacano hasta que
|
|
202
|
+
exista el scope y endpoint administrativo oficial.
|
|
@@ -87,6 +87,9 @@ Estado de página: `pending`, `extracted`, `implemented` o `validated`.
|
|
|
87
87
|
|
|
88
88
|
Cada página requiere `route`, `name`, `status` y al menos un frame desktop o
|
|
89
89
|
mobile con `nodeId` o `url`. No guardar secretos ni respuestas MCP completas.
|
|
90
|
+
Antes de implementar, comprobar que las rutas no apuntan accidentalmente al
|
|
91
|
+
mismo frame o `nodeId`. Una reutilización intencional debe quedar registrada en
|
|
92
|
+
`assumptions`.
|
|
90
93
|
|
|
91
94
|
## Estrategia de consultas
|
|
92
95
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Calidad de implementación desde Figma
|
|
2
|
+
|
|
3
|
+
Aplicar estas reglas al construir la interfaz. Son criterios de resultado, no
|
|
4
|
+
una obligación de usar una librería concreta.
|
|
5
|
+
|
|
6
|
+
## Responsive e inferencias
|
|
7
|
+
|
|
8
|
+
Si Figma no incluye un frame móvil, construir una adaptación mobile first y
|
|
9
|
+
registrarla como inferencia. Conservar jerarquía, contenido, orden semántico y
|
|
10
|
+
acciones principales; apilar columnas cuando corresponda y evitar overflow
|
|
11
|
+
horizontal. Verificar al menos móvil, tablet y escritorio.
|
|
12
|
+
|
|
13
|
+
No convertir automáticamente toda colección en carrusel. Usarlo únicamente
|
|
14
|
+
cuando mejore la exploración y asegurar scroll táctil, teclado, controles
|
|
15
|
+
accesibles, dimensiones estables y pausa durante la interacción. Respetar
|
|
16
|
+
`prefers-reduced-motion` cuando exista autoplay o movimiento.
|
|
17
|
+
|
|
18
|
+
## Componentes e interacción
|
|
19
|
+
|
|
20
|
+
Inspeccionar dependencias y componentes del proyecto antes de añadir paquetes.
|
|
21
|
+
No imponer HeroUI, Embla, Motion, CSS Modules u otra librería; reutilizar el
|
|
22
|
+
sistema existente y pedir autorización antes de introducir una dependencia
|
|
23
|
+
visual relevante.
|
|
24
|
+
|
|
25
|
+
Todo control interactivo necesita estado de hover cuando aplique,
|
|
26
|
+
`focus-visible`, teclado, contraste y área táctil suficiente. Usar
|
|
27
|
+
`aria-current="page"` en la navegación activa y no comunicar estados solo con
|
|
28
|
+
color. Las animaciones no deben cambiar el layout final, dejar contenido útil
|
|
29
|
+
oculto sin JavaScript ni ejecutarse contra la preferencia de movimiento
|
|
30
|
+
reducido.
|
|
31
|
+
|
|
32
|
+
## Perfil de exportación estática
|
|
33
|
+
|
|
34
|
+
Cuando el hosting y el proyecto estén definidos como export estático, comprobar
|
|
35
|
+
la configuración efectiva del framework. En Next.js suele incluir
|
|
36
|
+
`output: "export"`, `trailingSlash: true` y una estrategia de imágenes
|
|
37
|
+
compatible con exportación estática.
|
|
38
|
+
|
|
39
|
+
Materializar durante el build contenido, rutas, metadata, sitemap y JSON-LD.
|
|
40
|
+
Las rutas dinámicas editoriales deben enumerar todas las entradas publicadas y
|
|
41
|
+
fallar de forma explícita si el snapshot esperado está vacío o incompleto.
|
|
42
|
+
|
|
43
|
+
No introducir Route Handlers, API Routes, Server Actions, middleware, ISR,
|
|
44
|
+
`force-dynamic` ni secretos de servidor en un proyecto que debe producir solo
|
|
45
|
+
archivos estáticos. Esta prohibición no aplica automáticamente a un storefront
|
|
46
|
+
híbrido o SSR: primero clasificar su arquitectura.
|
|
47
|
+
|
|
48
|
+
Mantener un único bootstrap de configuración pública y un cliente Bacano por
|
|
49
|
+
worker o árbol. Reutilizar esa misma información para páginas, metadata,
|
|
50
|
+
sitemap, JSON-LD y provider; no consultar cada ruta por separado si existe un
|
|
51
|
+
snapshot oficial compartido.
|
|
52
|
+
|
|
53
|
+
## Preview y rutas editoriales
|
|
54
|
+
|
|
55
|
+
Mantener una allowlist finita de patrones editoriales. Los índices pueden ser
|
|
56
|
+
explícitos y los slugs de detalle pueden derivarse de las entradas publicadas;
|
|
57
|
+
no aceptar una ruta arbitraria solo porque coincide con una cadena. Preview
|
|
58
|
+
reutiliza componentes y mappers de producción, permanece fuera del sitemap y
|
|
59
|
+
no habilita funciones transaccionales o marketing.
|
|
60
|
+
|
|
61
|
+
## Diagnóstico antes de cambiar código
|
|
62
|
+
|
|
63
|
+
Un `ORIGIN_NOT_ALLOWED` o un `403` al resolver la configuración durante el
|
|
64
|
+
build suele indicar que el origen utilizado no está autorizado para el website.
|
|
65
|
+
Verificar dominio canónico, orígenes permitidos y ambiente antes de alterar el
|
|
66
|
+
cliente SDK. Una advertencia aislada sobre ausencia de caché del build no prueba
|
|
67
|
+
por sí sola que el storefront esté defectuoso.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Validar por cortes para evitar corregir 70 secciones al final.
|
|
4
4
|
|
|
5
|
+
Para cada corte, distinguir `implementado`, `validado`, `pendiente de revisión
|
|
6
|
+
humana`, `no aplica` y `bloqueado`. Si el usuario conserva la validación visual,
|
|
7
|
+
no convertir su ausencia en una aprobación automática.
|
|
8
|
+
|
|
5
9
|
## Evidencia mínima por página
|
|
6
10
|
|
|
7
11
|
- URL/ruta implementada.
|