@axzydev/axzy_ui_system 1.7.3 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/index.cjs +1845 -721
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +386 -15
  5. package/dist/index.d.ts +386 -15
  6. package/dist/index.js +1532 -418
  7. package/dist/index.js.map +1 -1
  8. package/dist/layered.css +1 -1
  9. package/package.json +4 -2
  10. package/src/App.tsx +6 -8
  11. package/src/components/atoms/button/button.tsx +7 -0
  12. package/src/components/atoms/flex/flex.props.ts +3 -1
  13. package/src/components/atoms/flex/flex.tsx +11 -3
  14. package/src/components/atoms/input/input.tsx +5 -0
  15. package/src/components/atoms/segmented-control/segmented-control.tsx +22 -5
  16. package/src/components/atoms/textarea/textarea.props.ts +4 -0
  17. package/src/components/atoms/textarea/textarea.tsx +13 -3
  18. package/src/components/molecules/card/card.props.ts +14 -0
  19. package/src/components/molecules/card/card.tsx +66 -23
  20. package/src/components/molecules/chip-input/chip-input.tsx +2 -0
  21. package/src/components/molecules/line-bar-chart/line-bar-chart.props.ts +4 -0
  22. package/src/components/molecules/line-bar-chart/line-bar-chart.tsx +57 -41
  23. package/src/components/molecules/multi-select/multi-select.tsx +2 -0
  24. package/src/components/molecules/tabs/tabs.tsx +22 -8
  25. package/src/components/organisms/dropfile/dropfile.props.ts +8 -2
  26. package/src/components/organisms/dropfile/dropfile.tsx +17 -0
  27. package/src/components/organisms/page-header/page-header.tsx +22 -6
  28. package/src/components/organisms/sidebar/sidebar.tsx +8 -5
  29. package/src/components/organisms/topbar/topbar.props.ts +2 -0
  30. package/src/components/organisms/topbar/topbar.tsx +56 -34
  31. package/src/components/templates/layout/layout.tsx +3 -2
  32. package/src/components/theme-provider/themeProvider.props.ts +1 -1
  33. package/src/components/theme-provider/themeProvider.tsx +54 -1
  34. package/src/i18n/locales/en.ts +7 -0
  35. package/src/i18n/locales/es.ts +7 -0
  36. package/src/index.ts +35 -7
  37. package/src/json-renderer/actions/effects.test.tsx +115 -0
  38. package/src/json-renderer/actions/effects.ts +291 -0
  39. package/src/json-renderer/actions/effectsChain.test.ts +117 -0
  40. package/src/json-renderer/binding/binding.test.tsx +168 -0
  41. package/src/json-renderer/binding/resolveValue.ts +193 -0
  42. package/src/json-renderer/catalog/axzyCatalog.ts +5 -0
  43. package/src/json-renderer/estado-acciones.test.tsx +166 -0
  44. package/src/json-renderer/form/FormNode.tsx +92 -0
  45. package/src/json-renderer/form/form.test.tsx +148 -0
  46. package/src/json-renderer/form/formLength.test.tsx +99 -0
  47. package/src/json-renderer/form/rules.ts +162 -0
  48. package/src/json-renderer/index.ts +21 -0
  49. package/src/json-renderer/kernel/SchemaRenderer.tsx +83 -7
  50. package/src/json-renderer/kernel/capabilities.ts +30 -0
  51. package/src/json-renderer/kernel/renderNode.tsx +256 -3
  52. package/src/json-renderer/kernel/schemaRenderer.props.ts +29 -3
  53. package/src/json-renderer/kernel/slots.test.tsx +104 -0
  54. package/src/json-renderer/kernel/slots.ts +34 -0
  55. package/src/json-renderer/kernel/types.ts +4 -0
  56. package/src/json-renderer/routing/match.ts +82 -0
  57. package/src/json-renderer/routing/navigation.ts +71 -0
  58. package/src/json-renderer/routing/routes.ts +179 -0
  59. package/src/json-renderer/routing/routing.test.ts +147 -0
  60. package/src/json-renderer/seguridad.test.tsx +299 -0
  61. package/src/json-renderer/shared/errors.ts +1 -0
  62. package/src/json-renderer/stream/tolerantParse.ts +5 -0
  63. package/src/json-renderer/stream/useSchemaStream.test.tsx +143 -0
  64. package/src/json-renderer/stream/useSchemaStream.ts +227 -0
  65. package/src/json-renderer/validation/jsonSchema.test.ts +105 -0
  66. package/src/json-renderer/validation/jsonSchema.ts +317 -0
  67. package/src/json-renderer/validation/validate.ts +17 -0
  68. package/src/sandbox/axzyConfig.test.ts +4 -0
  69. package/src/sandbox/axzyConfig.ts +1 -1
  70. package/src/sandbox/i18n.ts +44 -0
  71. package/src/sandbox/useHashRoute.ts +2 -1
  72. package/src/showcases/DemoJsonDashboard.tsx +218 -103
  73. package/src/showcases/DemoJsonDocs.tsx +11 -11
  74. package/src/showcases/FeedbackShowcases.tsx +2 -1
  75. package/src/showcases/JsonScreenShowcase.tsx +43 -53
  76. package/src/showcases/StructureShowcases.tsx +17 -2
  77. package/src/showcases/demoApp/demoJson.test.tsx +241 -0
  78. package/src/showcases/demoApp/routes.test.ts +63 -0
  79. package/src/showcases/demoApp/schemas.test.ts +6 -17
  80. package/src/theme/theme-context.ts +10 -2
  81. package/src/theme/theme.ts +1 -1
  82. package/src/theme/theme.types.ts +2 -1
@@ -0,0 +1,317 @@
1
+ /**
2
+ * JSON Schema (draft 2020-12) de los documentos del motor: pantallas y rutas.
3
+ *
4
+ * Se **deriva del catálogo** (vía el contrato de vocabulario), igual que el resto de
5
+ * artefactos del motor, así que no se desincroniza. Sirve para dos cosas:
6
+ *
7
+ * - **Autocompletado y avisos en el editor**: el JSON Schema lo lee VS Code (ver
8
+ * `.vscode/settings.json`), que sugiere `type`, props y valores de enum.
9
+ * - **Tester de documentos**: `pnpm schema:check` valida los JSON con ajv.
10
+ *
11
+ * Es una vista estructural. Las reglas que dependen de datos del host (que una
12
+ * fuente o una acción exista) no están aquí: esas las avisa el motor al pintar.
13
+ */
14
+
15
+ import { axzyCatalog } from "../catalog/axzyCatalog";
16
+ import type { Catalog } from "../catalog/catalog.props";
17
+ import { buildVocabularyContract, type PropContract } from "../vocabulary/buildContract";
18
+ import { CURRENT_SCHEMA_VERSION } from "./validate";
19
+ import { FORM_FIELD_KINDS } from "../form/rules";
20
+
21
+ /** Un objeto JSON Schema, tal cual se serializa. */
22
+ export type JsonSchema = Record<string, unknown>;
23
+
24
+ /** Dialecto de JSON Schema que usa el generador. */
25
+ const DIALECT = "https://json-schema.org/draft/2020-12/schema";
26
+
27
+ /** Tipos de nodo que el motor compila él mismo, fuera del catálogo. */
28
+ const ENGINE_NODE_TYPES = ["slot", "form"] as const;
29
+
30
+ /** Referencia declarativa: fuente, estado, parámetro o texto traducible. */
31
+ const REFERENCE: JsonSchema = {
32
+ type: "object",
33
+ description: "Referencia a un dato del host: fuente (`$source`), estado (`$state`), parámetro de ruta (`$param`) o texto traducible (`i18n`).",
34
+ properties: {
35
+ $source: { type: "string", description: "Fuente declarada en `data` del esquema." },
36
+ $state: { type: "string", description: "Clave del `state` de la pantalla." },
37
+ $param: { type: "string", description: "Parámetro de la dirección (`:id`)." },
38
+ i18n: { type: "string", description: "Clave de traducción." },
39
+ params: { type: "object", description: "Valores para los marcadores `{nombre}` del texto." },
40
+ },
41
+ additionalProperties: false,
42
+ anyOf: [{ required: ["$source"] }, { required: ["$state"] }, { required: ["$param"] }, { required: ["i18n"] }],
43
+ };
44
+
45
+ /** Acción declarada en una prop de tipo `action`. */
46
+ const ACTION: JsonSchema = {
47
+ type: "object",
48
+ description: "Acción que resuelve el host por nombre.",
49
+ required: ["action"],
50
+ properties: {
51
+ action: { type: "string", description: "Nombre de la acción registrada por el host." },
52
+ args: { type: "object", description: "Argumentos que recibe el host." },
53
+ confirm: { type: "object", description: "Confirmación previa: `{ title, message }`." },
54
+ then: { type: "array", description: "Efectos tras un resultado correcto.", items: { type: "object" } },
55
+ onError: { type: "object", description: "Efecto si la acción falla." },
56
+ toast: { type: "object", description: "Aviso al usuario: `{ variant, message }`." },
57
+ navigate: { type: "string", description: "Dirección a la que navegar." },
58
+ openOverlay: { type: "string", description: "Overlay a abrir." },
59
+ closeOverlay: { type: "string", description: "Overlay a cerrar." },
60
+ refresh: { type: "string", description: "Fuente de datos a refrescar." },
61
+ set: { type: "object", description: "Escribe en el estado de la pantalla: `{ clave: valor }`. Admite referencias." },
62
+ requires: {
63
+ anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
64
+ description: "Capacidad que exige la acción. Sin concesión, no se ejecuta (JR061).",
65
+ },
66
+ },
67
+ additionalProperties: false,
68
+ };
69
+
70
+ /** Valor de una prop según su tipo del catálogo. Las referencias se admiten en casi todas. */
71
+ function kindSchema(prop: PropContract): JsonSchema {
72
+ switch (prop.kind) {
73
+ case "string":
74
+ return { type: "string" };
75
+ case "number":
76
+ return { type: "number" };
77
+ case "integer":
78
+ return { type: "integer" };
79
+ case "boolean":
80
+ return { type: "boolean" };
81
+ case "enum":
82
+ return { type: "string", enum: prop.values ?? [] };
83
+ case "node":
84
+ return { $ref: "#/$defs/node" };
85
+ case "nodes":
86
+ return { type: "array", items: { $ref: "#/$defs/node" } };
87
+ case "columns":
88
+ return { type: "array", items: { type: "object" } };
89
+ case "array":
90
+ return { type: "array" };
91
+ case "object":
92
+ return { type: "object" };
93
+ default:
94
+ // `data`, `i18n`: cualquier valor; el host decide.
95
+ return {};
96
+ }
97
+ }
98
+
99
+ /** Schema de una prop: su tipo o una referencia. Las acciones solo admiten `action`. */
100
+ function propSchema(prop: PropContract): JsonSchema {
101
+ if (prop.kind === "action") {
102
+ return { ...ACTION, ...(prop.doc ? { description: prop.doc } : {}) };
103
+ }
104
+ const base = kindSchema(prop);
105
+ const schema: JsonSchema = { anyOf: [base, { $ref: "#/$defs/reference" }] };
106
+ if (prop.doc) schema.description = prop.doc;
107
+ return schema;
108
+ }
109
+
110
+ /** Props de un componente: objeto cerrado, con las obligatorias marcadas. */
111
+ function propsObjectSchema(props: PropContract[]): JsonSchema {
112
+ const properties: Record<string, JsonSchema> = {};
113
+ for (const prop of props) properties[prop.name] = propSchema(prop);
114
+ const required = props.filter((prop) => prop.required).map((prop) => prop.name);
115
+ return {
116
+ type: "object",
117
+ properties,
118
+ additionalProperties: false,
119
+ ...(required.length > 0 ? { required } : {}),
120
+ };
121
+ }
122
+
123
+ /** Reglas de cada tipo de nodo: props, hijos y texto. Se aplican con `if`/`then`. */
124
+ function typeRules(catalog: Catalog): JsonSchema[] {
125
+ const contract = buildVocabularyContract(catalog);
126
+ const rules: JsonSchema[] = contract.components.map((component) => ({
127
+ if: { properties: { type: { const: component.type } }, required: ["type"] },
128
+ then: {
129
+ properties: {
130
+ props: propsObjectSchema(component.props),
131
+ ...(component.children ? {} : { children: false }),
132
+ ...(component.text ? {} : { text: false }),
133
+ },
134
+ },
135
+ }));
136
+
137
+ // Slot: el host define sus props, así que solo se exige el nombre.
138
+ rules.push({
139
+ if: { properties: { type: { const: "slot" } }, required: ["type"] },
140
+ then: {
141
+ properties: {
142
+ props: { type: "object", required: ["name"], properties: { name: { type: "string", minLength: 1 } } },
143
+ children: false,
144
+ text: false,
145
+ },
146
+ },
147
+ });
148
+
149
+ // Form: campos declarados en JSON, validados por el motor.
150
+ rules.push({
151
+ if: { properties: { type: { const: "form" } }, required: ["type"] },
152
+ then: {
153
+ properties: {
154
+ props: {
155
+ type: "object",
156
+ required: ["fields"],
157
+ additionalProperties: false,
158
+ properties: {
159
+ fields: {
160
+ type: "array",
161
+ items: {
162
+ type: "object",
163
+ required: ["name"],
164
+ additionalProperties: false,
165
+ properties: {
166
+ name: { type: "string", minLength: 1, description: "Clave del valor enviado." },
167
+ kind: { type: "string", enum: [...FORM_FIELD_KINDS], description: "Tipo de campo." },
168
+ label: { $ref: "#/$defs/valueOrReference" },
169
+ required: { type: "boolean", description: "Si el campo es obligatorio." },
170
+ minLength: { type: "integer", minimum: 0, description: "Mínimo de caracteres." },
171
+ maxLength: { type: "integer", minimum: 0, description: "Máximo de caracteres." },
172
+ },
173
+ },
174
+ },
175
+ submitLabel: { $ref: "#/$defs/valueOrReference" },
176
+ onSubmit: { $ref: "#/$defs/action" },
177
+ },
178
+ },
179
+ children: false,
180
+ text: false,
181
+ },
182
+ },
183
+ });
184
+
185
+ return rules;
186
+ }
187
+
188
+ /**
189
+ * Genera el JSON Schema de una pantalla (`axzy/screen`).
190
+ *
191
+ * @example
192
+ * JSON.stringify(buildScreenSchema()); // lo que escribe `docs/json-renderer/screen.schema.json`
193
+ */
194
+ export function buildScreenSchema(catalog: Catalog = axzyCatalog): JsonSchema {
195
+ const contract = buildVocabularyContract(catalog);
196
+ const types = [...contract.components.map((component) => component.type), ...ENGINE_NODE_TYPES];
197
+
198
+ return {
199
+ $schema: DIALECT,
200
+ title: "AXZY · pantalla (axzy/screen)",
201
+ description: "Documento de pantalla del motor declarativo. Lo que no figura aquí no se pinta.",
202
+ type: "object",
203
+ required: ["version", "id", "content"],
204
+ additionalProperties: false,
205
+ properties: {
206
+ $schema: { const: "axzy/screen", description: "Identificador del formato." },
207
+ version: { type: "integer", minimum: 1, maximum: CURRENT_SCHEMA_VERSION, description: "Versión del formato." },
208
+ id: { type: "string", minLength: 1, description: "Identificador de la pantalla." },
209
+ title: { $ref: "#/$defs/valueOrReference", description: "Título, normalmente `{ i18n }`." },
210
+ description: { $ref: "#/$defs/valueOrReference", description: "Descripción corta para la cabecera." },
211
+ requires: { $ref: "#/$defs/requires" },
212
+ data: {
213
+ type: "object",
214
+ description: "Fuentes de datos: cada una nombra un handler que registra el host.",
215
+ additionalProperties: { $ref: "#/$defs/dataSource" },
216
+ },
217
+ state: { type: "object", description: "Estado inicial de la pantalla (lo lee `$state`)." },
218
+ content: {
219
+ description: "Nodo raíz o lista de nodos.",
220
+ anyOf: [{ $ref: "#/$defs/node" }, { type: "array", items: { $ref: "#/$defs/node" } }],
221
+ },
222
+ },
223
+ $defs: {
224
+ reference: REFERENCE,
225
+ action: ACTION,
226
+ valueOrReference: { anyOf: [{ type: "string" }, { $ref: "#/$defs/reference" }] },
227
+ requires: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }] },
228
+ dataSource: {
229
+ type: "object",
230
+ required: ["handler"],
231
+ additionalProperties: false,
232
+ properties: {
233
+ handler: { type: "string", description: "Nombre del handler que registra el host. Nunca una URL." },
234
+ params: { type: "object", description: "Parámetros; admiten referencias." },
235
+ refreshOn: { type: "array", items: { type: "string" }, description: "Fuentes o acciones que la invalidan." },
236
+ },
237
+ },
238
+ node: {
239
+ type: "object",
240
+ required: ["type"],
241
+ additionalProperties: false,
242
+ properties: {
243
+ type: { type: "string", enum: types, description: "Componente del catálogo o nodo del motor." },
244
+ id: { type: "string", description: "Ancla y referencia para depurar." },
245
+ props: { type: "object", description: "Props del componente (se validan según `type`)." },
246
+ children: {
247
+ description: "Hijos del nodo.",
248
+ anyOf: [{ $ref: "#/$defs/node" }, { type: "array", items: { $ref: "#/$defs/node" } }],
249
+ },
250
+ text: { $ref: "#/$defs/valueOrReference", description: "Atajo de contenido para componentes textuales." },
251
+ requires: { $ref: "#/$defs/requires" },
252
+ fallback: { $ref: "#/$defs/node", description: "Qué pintar si el nodo falla o no tiene permiso." },
253
+ when: { type: "object", description: "Condición para pintar el nodo." },
254
+ $status: { type: "string", enum: ["complete", "pending"], description: "Lo rellena el parser de streaming." },
255
+ },
256
+ allOf: typeRules(catalog),
257
+ },
258
+ },
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Genera el JSON Schema de un documento de rutas (`axzy/routes`).
264
+ *
265
+ * Un grupo lleva `children` y no `page`; una hoja lleva `page` y no `children`.
266
+ */
267
+ export function buildRoutesSchema(): JsonSchema {
268
+ const route: JsonSchema = {
269
+ type: "object",
270
+ required: ["id"],
271
+ additionalProperties: false,
272
+ properties: {
273
+ id: { type: "string", minLength: 1, description: "Identificador estable de la ruta." },
274
+ path: { type: "string", description: "Segmentos relativos al padre (`usuarios`, `:id`). Vacío hereda el del padre." },
275
+ page: { type: "string", minLength: 1, description: "Pantalla que pinta la ruta. Solo en hojas." },
276
+ query: {
277
+ type: "object",
278
+ description: "Query que exige la ruta.",
279
+ additionalProperties: { type: "string" },
280
+ },
281
+ nav: {
282
+ type: "object",
283
+ required: ["label"],
284
+ additionalProperties: false,
285
+ description: "Entrada de menú. Sin ella, la ruta no aparece.",
286
+ properties: {
287
+ label: { $ref: "#/$defs/valueOrReference" },
288
+ icon: { type: "string", description: "Icono del registro del host." },
289
+ badge: { type: ["integer", "string"], description: "Contador visible." },
290
+ },
291
+ },
292
+ children: { type: "array", items: { $ref: "#/$defs/route" }, description: "Rutas hijas. Si existen, es un grupo." },
293
+ },
294
+ if: { required: ["children"] },
295
+ then: { not: { required: ["page"] }, description: "Un grupo no lleva page." },
296
+ else: { required: ["page"], description: "Una hoja necesita page." },
297
+ };
298
+
299
+ return {
300
+ $schema: DIALECT,
301
+ title: "AXZY · rutas (axzy/routes)",
302
+ description: "Rutas declarativas: qué pantalla pinta cada dirección y qué entradas aparecen en el menú.",
303
+ type: "object",
304
+ required: ["version", "routes"],
305
+ additionalProperties: false,
306
+ properties: {
307
+ $schema: { const: "axzy/routes" },
308
+ version: { type: "integer", minimum: 1 },
309
+ routes: { type: "array", items: { $ref: "#/$defs/route" } },
310
+ },
311
+ $defs: {
312
+ route,
313
+ reference: REFERENCE,
314
+ valueOrReference: { anyOf: [{ type: "string" }, { $ref: "#/$defs/reference" }] },
315
+ },
316
+ };
317
+ }
@@ -12,6 +12,8 @@ import type { Catalog } from "../catalog/catalog.props";
12
12
  import { axzyCatalog } from "../catalog/axzyCatalog";
13
13
  import { resolveNode } from "../catalog/resolve";
14
14
  import type { NodeSchema, ScreenSchema } from "../kernel/types";
15
+ import { isSlotNode, slotName } from "../kernel/slots";
16
+ import { readFormFields } from "../form/rules";
15
17
  import { isNodePending } from "../stream/tolerantParse";
16
18
 
17
19
  /** Veredicto de la validación. */
@@ -58,6 +60,21 @@ function validateNodes(
58
60
  // Un nodo pendiente no es un error: aún no ha llegado.
59
61
  if (isNodePending(node)) return 1;
60
62
 
63
+ // El formulario lo compila el motor: sus campos se validan con las mismas reglas que al pintar.
64
+ if (node.type === "form") {
65
+ const { issues: avisos } = readFormFields(node.props?.fields, [...path, "props", "fields"]);
66
+ for (const item of avisos) (item.severity === "error" ? errors : warnings).push(item);
67
+ return 0;
68
+ }
69
+
70
+ // Un slot no está en el catálogo: su nombre lo registra el host y se comprueba al pintar.
71
+ if (isSlotNode(node)) {
72
+ if (!slotName(node)) {
73
+ errors.push(issue("JR011", "El slot no declara `props.name`.", formatPath([...path, "props", "name"]), { severity: "error" }));
74
+ }
75
+ return 0;
76
+ }
77
+
61
78
  const resolved = resolveNode(node, catalog, path);
62
79
  for (const item of resolved.issues) {
63
80
  (item.severity === "error" ? errors : warnings).push(item);
@@ -16,6 +16,10 @@ describe("axzy.json", () => {
16
16
  expect(config.showFab).toBe(true);
17
17
  });
18
18
 
19
+ it("acepta la apariencia bento", () => {
20
+ expect(mergeAxzyConfig({ appearance: "bento" }).appearance).toBe("bento");
21
+ });
22
+
19
23
  it("recorta fuera de rango en vez de romper", () => {
20
24
  const config = mergeAxzyConfig({ density: 9, radius: 99, shadow: -3 });
21
25
  expect(config.density).toBe(1.5);
@@ -71,7 +71,7 @@ export function mergeAxzyConfig(raw: unknown): AxzyConfig {
71
71
  density: clamp(entrada.density, 0.5, 1.5, DEFAULT_AXZY_CONFIG.density),
72
72
  radius: clamp(entrada.radius, 0, 24, DEFAULT_AXZY_CONFIG.radius, 0),
73
73
  shadow: clamp(entrada.shadow, 0, 3, DEFAULT_AXZY_CONFIG.shadow, 0),
74
- appearance: entrada.appearance === "flat" || entrada.appearance === "soft" ? entrada.appearance : DEFAULT_AXZY_CONFIG.appearance,
74
+ appearance: entrada.appearance === "flat" || entrada.appearance === "soft" || entrada.appearance === "bento" ? entrada.appearance : DEFAULT_AXZY_CONFIG.appearance,
75
75
  showFab: typeof entrada.showFab === "boolean" ? entrada.showFab : DEFAULT_AXZY_CONFIG.showFab,
76
76
  };
77
77
  }
@@ -85,6 +85,8 @@ export const sandboxMessages: ITMessagesByLocale = {
85
85
  "sandbox.app.inicio.lead": "Cada página es un JSON: esta trae métricas y la tabla de pedidos recientes, que pide los datos al host con `$source`.",
86
86
  "sandbox.app.inicio.ctaUsers": "Gestionar usuarios",
87
87
  "sandbox.app.inicio.ctaOrders": "Ver pedidos",
88
+ "sandbox.app.inicio.slotTitle": "Bloque del host",
89
+ "sandbox.app.inicio.slotBody": "Este aviso lo pinta un componente JSX registrado por el host (slot), no el JSON.",
88
90
  "sandbox.app.inicio.recent": "Pedidos recientes",
89
91
  "sandbox.app.inicio.recentLead": "Paginación, orden y filtros los resuelve la tabla; los datos, el host.",
90
92
  "sandbox.app.usuarios.title": "Usuarios",
@@ -93,6 +95,23 @@ export const sandboxMessages: ITMessagesByLocale = {
93
95
  "sandbox.app.usuarios.active": "Activos",
94
96
  "sandbox.app.usuarios.admins": "Admins",
95
97
  "sandbox.app.usuarios.inactive": "Inactivos",
98
+ "sandbox.app.usuarios.verFila": "Ver",
99
+ "sandbox.app.detail.title": "Detalle de usuario",
100
+ "sandbox.app.detail.lead": "Datos del usuario tomados de la dirección /usuarios/:id.",
101
+ "sandbox.app.detail.back": "Volver a usuarios",
102
+ "sandbox.app.detail.notFound": "No existe ningún usuario con ese id.",
103
+ "sandbox.app.reportes.export": "Exportar reporte",
104
+ "sandbox.app.reportes.chartTitle": "Ingresos por mes",
105
+ "sandbox.app.reportes.chartAria": "Ingresos y pedidos por mes",
106
+ "sandbox.app.reportes.chartLine": "Ingresos",
107
+ "sandbox.app.reportes.chartBar": "Pedidos",
108
+ "sandbox.app.export.title": "Exportar reporte",
109
+ "sandbox.app.export.message": "Se generará un archivo con los datos de la vista.",
110
+ "sandbox.app.export.confirm": "Exportar",
111
+ "sandbox.app.export.done": "Reporte exportado (simulado).",
112
+ "sandbox.app.contact.title": "Contacto rápido",
113
+ "sandbox.app.contact.send": "Enviar",
114
+ "sandbox.app.contact.sent": "Mensaje enviado (simulado).",
96
115
  "sandbox.app.pedidos.title": "Pedidos",
97
116
  "sandbox.app.pedidos.lead": "La columna Total usa el formateo de moneda de la tabla; el estado se filtra por catálogo.",
98
117
  "sandbox.app.pedidos.new": "Nuevo pedido",
@@ -679,6 +698,9 @@ export const sandboxMessages: ITMessagesByLocale = {
679
698
  "Layouts de card listos para producción construidos con ITCard",
680
699
  "showcase.structure.patternCourseProgress": "Progreso del curso",
681
700
  "showcase.structure.patternPricing": "Plan de precios destacado",
701
+ "showcase.structure.cardToneTitle": "Tarjetas pastel (tone)",
702
+ "showcase.structure.cardToneSubtitle": "La prop tone tiñe el fondo con el token --color-<tone>-soft; funciona en soft, flat y bento.",
703
+ "showcase.structure.cardToneBody": "Fondo tintado con el token suave del color.",
682
704
  "showcase.structure.patternActivity": "Item de feed de actividad",
683
705
  "showcase.structure.patternTeamMember": "Perfil de miembro del equipo",
684
706
  "showcase.structure.textDesc":
@@ -1142,6 +1164,8 @@ export const sandboxMessages: ITMessagesByLocale = {
1142
1164
  "sandbox.app.inicio.lead": "Every page is JSON: this one ships metrics and the recent orders table, which asks the host for data through `$source`.",
1143
1165
  "sandbox.app.inicio.ctaUsers": "Manage users",
1144
1166
  "sandbox.app.inicio.ctaOrders": "See orders",
1167
+ "sandbox.app.inicio.slotTitle": "Host block",
1168
+ "sandbox.app.inicio.slotBody": "This notice is rendered by a JSX component registered by the host (slot), not by the JSON.",
1145
1169
  "sandbox.app.inicio.recent": "Recent orders",
1146
1170
  "sandbox.app.inicio.recentLead": "Pagination, sorting and filters are the table's job; the data is the host's.",
1147
1171
  "sandbox.app.usuarios.title": "Users",
@@ -1150,6 +1174,23 @@ export const sandboxMessages: ITMessagesByLocale = {
1150
1174
  "sandbox.app.usuarios.active": "Active",
1151
1175
  "sandbox.app.usuarios.admins": "Admins",
1152
1176
  "sandbox.app.usuarios.inactive": "Inactive",
1177
+ "sandbox.app.usuarios.verFila": "View",
1178
+ "sandbox.app.detail.title": "User detail",
1179
+ "sandbox.app.detail.lead": "User data taken from the /usuarios/:id address.",
1180
+ "sandbox.app.detail.back": "Back to users",
1181
+ "sandbox.app.detail.notFound": "No user exists with that id.",
1182
+ "sandbox.app.reportes.export": "Export report",
1183
+ "sandbox.app.reportes.chartTitle": "Revenue per month",
1184
+ "sandbox.app.reportes.chartAria": "Revenue and orders per month",
1185
+ "sandbox.app.reportes.chartLine": "Revenue",
1186
+ "sandbox.app.reportes.chartBar": "Orders",
1187
+ "sandbox.app.export.title": "Export report",
1188
+ "sandbox.app.export.message": "A file with the view's data will be generated.",
1189
+ "sandbox.app.export.confirm": "Export",
1190
+ "sandbox.app.export.done": "Report exported (simulated).",
1191
+ "sandbox.app.contact.title": "Quick contact",
1192
+ "sandbox.app.contact.send": "Send",
1193
+ "sandbox.app.contact.sent": "Message sent (simulated).",
1153
1194
  "sandbox.app.pedidos.title": "Orders",
1154
1195
  "sandbox.app.pedidos.lead": "The Total column uses the table's currency formatting; status filters by catalog.",
1155
1196
  "sandbox.app.pedidos.new": "New order",
@@ -1732,6 +1773,9 @@ export const sandboxMessages: ITMessagesByLocale = {
1732
1773
  "Production-ready card layouts built with ITCard",
1733
1774
  "showcase.structure.patternCourseProgress": "Course progress",
1734
1775
  "showcase.structure.patternPricing": "Featured pricing plan",
1776
+ "showcase.structure.cardToneTitle": "Pastel cards (tone)",
1777
+ "showcase.structure.cardToneSubtitle": "The tone prop tints the background with the --color-<tone>-soft token; it works in soft, flat and bento.",
1778
+ "showcase.structure.cardToneBody": "Background tinted with the soft color token.",
1735
1779
  "showcase.structure.patternActivity": "Activity feed item",
1736
1780
  "showcase.structure.patternTeamMember": "Team member profile",
1737
1781
  "showcase.structure.textDesc":
@@ -69,7 +69,8 @@ export function parseHash(hash: string): HashRoute {
69
69
 
70
70
  if (parts[0] === "demo") {
71
71
  if (parts[1] === "app") {
72
- return { view: "demo", group: "app", item: parts[2] ?? "inicio", producto: null, raw, query, valid: parts.length <= 3 };
72
+ // El resto del path lo casa `routes.json` (puede tener varios segmentos): aquí solo se separa.
73
+ return { view: "demo", group: "app", item: parts.slice(2).join("/"), producto: null, raw, query, valid: true };
73
74
  }
74
75
  return { view: "demo", group: null, item: null, producto: null, raw, query, valid: parts.length === 1 };
75
76
  }