@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.
@@ -1,9 +1,10 @@
1
- # Gestión de tipos de Content para agentes
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, cree y mantenga **tipos y componentes** de
5
- Bacano Content. No permite administrar entradas, productos, ventas, clientes ni
6
- ningún otro dato de la empresa.
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 solo contiene el alcance
20
- `content_types:manage`. Cada petición vuelve a comprobar que el usuario que lo
21
- generó continúa vinculado a la empresa y conserva `websites.manage`. Al vencer
22
- o perder ese permiso deja de funcionar.
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: "cta_button",
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: "label",
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: "url",
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: "home_hero",
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: "content", title: "Contenido", sortOrder: 1 }],
191
+ sections: [{ key: "contenido", title: "Contenido", sortOrder: 1 }],
105
192
  fields: [
106
193
  {
107
- key: "title",
194
+ key: "titulo",
108
195
  label: "Título",
109
196
  type: "text",
110
197
  required: true,
111
198
  sortOrder: 1,
112
- sectionKey: "content",
199
+ sectionKey: "contenido",
113
200
  settings: { isVisible: true, maxLength: 120 },
114
201
  },
115
202
  {
116
- key: "actions",
203
+ key: "acciones",
117
204
  label: "Acciones",
118
205
  type: "repeater",
119
206
  required: false,
120
207
  sortOrder: 2,
121
- sectionKey: "content",
208
+ sectionKey: "contenido",
122
209
  settings: { isVisible: true, minRows: 0, maxRows: 3 },
123
210
  children: [
124
211
  {
125
- key: "button",
212
+ key: "boton",
126
213
  label: "Botón",
127
214
  type: "component",
128
215
  required: true,
129
216
  sortOrder: 1,
130
- componentKey: "cta_button",
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("home_hero", "delete");
256
+ const preview = await content.inspectLifecycle("hero_inicio", "delete");
170
257
 
171
258
  if (preview.allowed && userConfirmed) {
172
- await content.applyLifecycle("home_hero", "delete");
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 crea ni modifica entradas de Content.
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 el acceso temporal, el agente crea o ajusta los tipos y
191
- componentes aprobados, y el equipo editorial completa las entradas desde
192
- Bacano.
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 tipos de Content para agentes
24
+ ### Gestión de Bacano Content para agentes
25
25
 
26
- Un agente autorizado puede consultar, crear y mantener tipos y componentes con
27
- el entrypoint administrativo `@bacano/sdk/content-management`. El acceso se
28
- genera temporalmente desde Bacano, está limitado a una empresa y nunca debe
29
- incluirse en el storefront:
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, referencias,
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 tienda que conserve el cascarón, Static
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. Las entradas reales continúan
86
- creándose desde Bacano. Con autorización temporal, la skill puede crear o
87
- ajustar tipos y componentes mediante el entrypoint administrativo oficial; un
88
- fixture activo bloquea la aprobación de producción.
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
+ }
@@ -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
- `/types/${encodeIdentifier(identifier)}/lifecycle`,
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
  }
@@ -1,125 +1,5 @@
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 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;
@@ -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
- `/types/${encodeIdentifier(identifier)}/lifecycle`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacano/sdk",
3
- "version": "1.1.12",
3
+ "version": "1.1.13",
4
4
  "description": "TypeScript SDK for building websites on the Bacano ERP",
5
5
  "main": "./index.cjs",
6
6
  "module": "./index.js",
@@ -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 tienda virtual.
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) y
78
- [references/content-workflow.md](references/content-workflow.md). Preparar una
79
- matriz ruta → frame → componente → fuente de datos → estado responsive y
80
- determinar si el trabajo comienza desde Figma, desde Bacano Content o desde
81
- ambas fuentes.
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`. Solo crear o modificar
100
- tipos y componentes mediante el acceso temporal oficial; las entradas y media
101
- permanecen en el backoffice.
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. Proponer tipos, keys, campos y entradas en `content-blueprint.json`.
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": "home-hero",
87
+ "sectionKey": "hero-inicio",
70
88
  "figmaNodeId": "1:20",
71
89
  "component": "HomeHero",
72
90
  "ownership": "content",
73
- "contentTypeKey": "home_hero",
74
- "entryKey": "home-main-hero",
91
+ "contentTypeKey": "hero_inicio",
92
+ "entryKey": "hero-principal-inicio",
75
93
  "status": "missing",
76
94
  "fields": [
77
95
  {
78
- "key": "title",
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/home-hero.json"
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 componentes antes de
136
- sus consumidores y trabajar inicialmente en borrador. La skill no crea entradas,
137
- importaciones ni cargas de media.
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()`, `listTypes()` y `getReferences()`.
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 componentes y luego tipos consumidores, siempre en borrador.
165
- 6. Para archivar o eliminar, inspeccionar dependencias y confirmar nuevamente.
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. Las entradas y media permanecen bajo gestión editorial en Bacano.
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.