@axzydev/axzy_ui_system 1.7.0 → 1.7.2

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 (54) hide show
  1. package/dist/index.cjs +1938 -491
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +739 -3
  5. package/dist/index.d.ts +739 -3
  6. package/dist/index.js +1830 -417
  7. package/dist/index.js.map +1 -1
  8. package/dist/layered.css +1 -1
  9. package/package.json +14 -7
  10. package/src/App.tsx +60 -2
  11. package/src/components/atoms/image/image.props.ts +2 -0
  12. package/src/components/atoms/image/image.tsx +3 -2
  13. package/src/components/atoms/segmented-control/segmented-control.tsx +3 -0
  14. package/src/components/molecules/card/card.tsx +12 -0
  15. package/src/components/molecules/stat-card/stat-card.tsx +10 -0
  16. package/src/components/organisms/dialog/dialog.props.ts +2 -0
  17. package/src/components/organisms/dialog/dialog.tsx +4 -3
  18. package/src/dev.css +6 -0
  19. package/src/index.ts +90 -1
  20. package/src/json-renderer/catalog/axzyCatalog.ts +362 -0
  21. package/src/json-renderer/catalog/catalog.props.ts +128 -0
  22. package/src/json-renderer/catalog/resolve.ts +225 -0
  23. package/src/json-renderer/index.ts +70 -0
  24. package/src/json-renderer/kernel/NodeBoundary.tsx +53 -0
  25. package/src/json-renderer/kernel/SchemaRenderer.tsx +197 -0
  26. package/src/json-renderer/kernel/kernel.test.tsx +341 -0
  27. package/src/json-renderer/kernel/partial.ts +92 -0
  28. package/src/json-renderer/kernel/renderNode.tsx +113 -0
  29. package/src/json-renderer/kernel/schemaRenderer.props.ts +42 -0
  30. package/src/json-renderer/kernel/types.ts +159 -0
  31. package/src/json-renderer/shared/errors.ts +92 -0
  32. package/src/json-renderer/shared/limits.ts +87 -0
  33. package/src/json-renderer/shared/paths.ts +66 -0
  34. package/src/json-renderer/shared/shared.test.ts +108 -0
  35. package/src/json-renderer/stream/tolerantParse.test.ts +152 -0
  36. package/src/json-renderer/stream/tolerantParse.ts +449 -0
  37. package/src/json-renderer/test/setup.ts +38 -0
  38. package/src/json-renderer/validation/migrate.ts +83 -0
  39. package/src/json-renderer/validation/validate.ts +161 -0
  40. package/src/json-renderer/validation/validation.test.ts +165 -0
  41. package/src/json-renderer/vocabulary/buildContract.ts +158 -0
  42. package/src/sandbox/axzyConfig.test.ts +40 -0
  43. package/src/sandbox/axzyConfig.ts +77 -0
  44. package/src/sandbox/i18n.ts +218 -0
  45. package/src/sandbox/navigation.ts +1 -0
  46. package/src/sandbox/useHashRoute.ts +39 -9
  47. package/src/showcases/DemoJsonDashboard.tsx +489 -0
  48. package/src/showcases/DemoJsonDocs.tsx +339 -0
  49. package/src/showcases/DemoJsonPortfolio.tsx +257 -0
  50. package/src/showcases/JsonScreenShowcase.tsx +341 -0
  51. package/src/showcases/demoApp/demoData.ts +181 -0
  52. package/src/showcases/demoApp/nodeTree.test.ts +39 -0
  53. package/src/showcases/demoApp/nodeTree.ts +35 -0
  54. package/src/showcases/demoApp/schemas.test.ts +54 -0
@@ -17,10 +17,119 @@ export const sandboxMessages: ITMessagesByLocale = {
17
17
  "sandbox.themeSystem": "Sistema",
18
18
  "sandbox.notifications": "Notificaciones",
19
19
  "sandbox.nav.portfolio": "Portafolio",
20
+ "sandbox.nav.demo": "Demo en JSON",
20
21
  "sandbox.userMenu.portfolio": "Ir al Portafolio",
21
22
  "sandbox.search.label": "Buscar componentes",
22
23
  "sandbox.search.placeholder": "Buscar componente…",
23
24
  "sandbox.loading": "Cargando…",
25
+ // Portafolio de demostración (pintado solo con JSON)
26
+ "sandbox.app.toast.saved": "Guardado",
27
+ "sandbox.app.toast.deleted": "Eliminado",
28
+ "sandbox.app.form.create": "Nuevo usuario",
29
+ "sandbox.app.form.edit": "Editar usuario",
30
+ "sandbox.app.form.name": "Nombre",
31
+ "sandbox.app.form.email": "Correo",
32
+ "sandbox.app.form.role": "Rol",
33
+ "sandbox.app.form.status": "Estado",
34
+ "sandbox.app.form.age": "Edad",
35
+ "sandbox.app.form.company": "Empresa",
36
+ "sandbox.app.form.since": "Alta",
37
+ "sandbox.app.form.shift": "Turno",
38
+ "sandbox.app.form.save": "Guardar",
39
+ "sandbox.app.form.cancel": "Cancelar",
40
+ "sandbox.app.form.delete": "Eliminar",
41
+ "sandbox.app.title": "Panel de demo",
42
+ "sandbox.app.exit": "Ver portafolio JSON",
43
+ "sandbox.app.nav.aria": "Páginas del dashboard",
44
+ "sandbox.app.sub.todos": "Todos",
45
+ "sandbox.app.sub.activos": "Activos",
46
+ "sandbox.app.sub.inactivos": "Inactivos",
47
+ "sandbox.app.sub.pendientes": "Pendientes",
48
+ "sandbox.app.nav.docs": "Documentación",
49
+ "sandbox.app.docs.pick": "Esquema",
50
+ "sandbox.app.docs.copy": "Copiar JSON",
51
+ "sandbox.app.docs.copied": "JSON copiado",
52
+ "sandbox.app.docs.copyError": "No se pudo copiar",
53
+ "sandbox.app.docs.steps": "Cómo se pinta un esquema",
54
+ "sandbox.app.docs.node": "Nodo",
55
+ "sandbox.app.docs.whole": "Esquema completo",
56
+ "sandbox.app.docs.readOnly": "Inspector · solo lectura",
57
+ "sandbox.app.docs.editable": "Editable",
58
+ "sandbox.app.docs.rules": "Reglas que no se negocian",
59
+ "sandbox.app.docs.rule1": "El esquema nunca ejecuta código, ni hace red, ni pinta HTML.",
60
+ "sandbox.app.docs.rule2": "Todo componente y toda acción salen de listas blancas del host.",
61
+ "sandbox.app.docs.rule3": "Un componente que falla se aísla: el resto de la pantalla sigue.",
62
+ "sandbox.app.nav.inicio": "Inicio",
63
+ "sandbox.app.nav.usuarios": "Usuarios",
64
+ "sandbox.app.nav.pedidos": "Pedidos",
65
+ "sandbox.app.nav.productos": "Productos",
66
+ "sandbox.app.nav.reportes": "Reportes",
67
+ "sandbox.app.sub.pagados": "Pagados",
68
+ "sandbox.app.productos.title": "Productos",
69
+ "sandbox.app.productos.lead": "Catálogo con SKU, precio, stock y categoría; la tabla pagina y ordena desde el host.",
70
+ "sandbox.app.reportes.title": "Reportes",
71
+ "sandbox.app.reportes.lead": "Indicadores del periodo y el detalle de pedidos que los sostiene.",
72
+ "sandbox.app.reportes.ticket": "Ticket promedio",
73
+ "sandbox.app.reportes.conversion": "Conversión",
74
+ "sandbox.app.valid": "Esquema válido",
75
+ "sandbox.app.partial": "Esquema parcial",
76
+ "sandbox.app.invalid": "Esquema inválido",
77
+ "sandbox.app.loading": "Cargando la página…",
78
+ "sandbox.app.loadError": "No se pudo cargar la página",
79
+ "sandbox.app.back": "Volver al inicio",
80
+ "sandbox.app.statUsers": "Usuarios",
81
+ "sandbox.app.statOrders": "Pedidos",
82
+ "sandbox.app.statRevenue": "Ingresos",
83
+ "sandbox.app.statPending": "Pendientes",
84
+ "sandbox.app.inicio.title": "Resumen operativo",
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
+ "sandbox.app.inicio.ctaUsers": "Gestionar usuarios",
87
+ "sandbox.app.inicio.ctaOrders": "Ver pedidos",
88
+ "sandbox.app.inicio.recent": "Pedidos recientes",
89
+ "sandbox.app.inicio.recentLead": "Paginación, orden y filtros los resuelve la tabla; los datos, el host.",
90
+ "sandbox.app.usuarios.title": "Usuarios",
91
+ "sandbox.app.usuarios.lead": "La columna Nombre filtra y ordena; Rol y Estado filtran por catálogo. Todo declarado en el JSON.",
92
+ "sandbox.app.usuarios.new": "Nuevo usuario",
93
+ "sandbox.app.usuarios.active": "Activos",
94
+ "sandbox.app.usuarios.admins": "Admins",
95
+ "sandbox.app.usuarios.inactive": "Inactivos",
96
+ "sandbox.app.pedidos.title": "Pedidos",
97
+ "sandbox.app.pedidos.lead": "La columna Total usa el formateo de moneda de la tabla; el estado se filtra por catálogo.",
98
+ "sandbox.app.pedidos.new": "Nuevo pedido",
99
+ "sandbox.app.pedidos.paid": "Pagados",
100
+ "sandbox.app.pedidos.pending": "Pendientes",
101
+ "sandbox.app.pedidos.cancelled": "Cancelados",
102
+ "sandbox.demo.brand": "axzydev · portafolio en JSON",
103
+ "sandbox.demo.back": "Volver al sandbox",
104
+ "sandbox.demo.eyebrow": "Demo · motor de pantallas",
105
+ "sandbox.demo.title": "Del API a la interfaz: producto completo, en producción.",
106
+ "sandbox.demo.lead":
107
+ "Esta pantalla no tiene ni una línea de TSX: se carga un archivo JSON, se valida contra el catálogo y el motor la pinta. Todo lo que ves —layout, tarjetas, acciones e idioma— viene del esquema.",
108
+ "sandbox.demo.ctaProjects": "Ver proyectos",
109
+ "sandbox.demo.ctaContact": "Contacto",
110
+ "sandbox.demo.ctaPanel": "Abrir el panel",
111
+ "sandbox.demo.statExperience": "Experiencia",
112
+ "sandbox.demo.statProducts": "Productos",
113
+ "sandbox.demo.statTech": "Tecnologías",
114
+ "sandbox.demo.statMode": "Modalidad",
115
+ "sandbox.demo.statModeValue": "Remoto",
116
+ "sandbox.demo.projectsTitle": "Proyectos",
117
+ "sandbox.demo.projectsLead": "Cada tarjeta es un nodo del esquema; el botón pide una acción registrada por el host.",
118
+ "sandbox.demo.viewProject": "Ver detalle",
119
+ "sandbox.demo.p1.text": "Operación de seguridad: rondas con QR, incidencias y analítica de desempeño.",
120
+ "sandbox.demo.p1.alt": "CheckApp: panel de analítica de seguridad",
121
+ "sandbox.demo.p2.text": "63 componentes accesibles, temables y con i18n, publicados en npm.",
122
+ "sandbox.demo.p2.alt": "AXZY UI System: componentes en ejecución",
123
+ "sandbox.demo.p3.text": "Cliente HTTP para la terminal, con requests versionables en YAML y modo CI.",
124
+ "sandbox.demo.p3.alt": "axzy: cliente HTTP en la terminal",
125
+ "sandbox.demo.stackTitle": "Stack",
126
+ "sandbox.demo.stackLead": "Las etiquetas son nodos ITChip; añadir una es añadir un objeto al array.",
127
+ "sandbox.demo.contactTitle": "Contacto",
128
+ "sandbox.demo.contactLead": "Los botones no llevan URLs: piden acciones por nombre y el host decide a dónde van.",
129
+ "sandbox.demo.sendMail": "Escribir un correo",
130
+ "sandbox.demo.footer": "Asael Amaro · Fullstack Developer · Remoto, México",
131
+ "sandbox.demo.badge": "JSON válido",
132
+
24
133
  "sandbox.search.clear": "Limpiar búsqueda",
25
134
  "sandbox.search.results": "{count} resultados",
26
135
 
@@ -965,10 +1074,119 @@ export const sandboxMessages: ITMessagesByLocale = {
965
1074
  "sandbox.themeSystem": "System",
966
1075
  "sandbox.notifications": "Notifications",
967
1076
  "sandbox.nav.portfolio": "Portfolio",
1077
+ "sandbox.nav.demo": "JSON demo",
968
1078
  "sandbox.userMenu.portfolio": "Go to portfolio",
969
1079
  "sandbox.search.label": "Search components",
970
1080
  "sandbox.search.placeholder": "Search component…",
971
1081
  "sandbox.loading": "Loading…",
1082
+ // Demo portfolio (rendered from JSON only)
1083
+ "sandbox.app.toast.saved": "Saved",
1084
+ "sandbox.app.toast.deleted": "Deleted",
1085
+ "sandbox.app.form.create": "New user",
1086
+ "sandbox.app.form.edit": "Edit user",
1087
+ "sandbox.app.form.name": "Name",
1088
+ "sandbox.app.form.email": "Email",
1089
+ "sandbox.app.form.role": "Role",
1090
+ "sandbox.app.form.status": "Status",
1091
+ "sandbox.app.form.age": "Age",
1092
+ "sandbox.app.form.company": "Company",
1093
+ "sandbox.app.form.since": "Since",
1094
+ "sandbox.app.form.shift": "Shift",
1095
+ "sandbox.app.form.save": "Save",
1096
+ "sandbox.app.form.cancel": "Cancel",
1097
+ "sandbox.app.form.delete": "Delete",
1098
+ "sandbox.app.title": "Demo panel",
1099
+ "sandbox.app.exit": "Open JSON portfolio",
1100
+ "sandbox.app.nav.aria": "Dashboard pages",
1101
+ "sandbox.app.sub.todos": "All",
1102
+ "sandbox.app.sub.activos": "Active",
1103
+ "sandbox.app.sub.inactivos": "Inactive",
1104
+ "sandbox.app.sub.pendientes": "Pending",
1105
+ "sandbox.app.nav.docs": "Documentation",
1106
+ "sandbox.app.docs.pick": "Schema",
1107
+ "sandbox.app.docs.copy": "Copy JSON",
1108
+ "sandbox.app.docs.copied": "JSON copied",
1109
+ "sandbox.app.docs.copyError": "Could not copy",
1110
+ "sandbox.app.docs.steps": "How a schema is rendered",
1111
+ "sandbox.app.docs.node": "Node",
1112
+ "sandbox.app.docs.whole": "Whole schema",
1113
+ "sandbox.app.docs.readOnly": "Inspector · read only",
1114
+ "sandbox.app.docs.editable": "Editable",
1115
+ "sandbox.app.docs.rules": "Rules that are not negotiable",
1116
+ "sandbox.app.docs.rule1": "The schema never runs code, never fetches and never renders HTML.",
1117
+ "sandbox.app.docs.rule2": "Every component and action comes from host whitelists.",
1118
+ "sandbox.app.docs.rule3": "A component that fails is isolated: the rest of the screen keeps going.",
1119
+ "sandbox.app.nav.inicio": "Home",
1120
+ "sandbox.app.nav.usuarios": "Users",
1121
+ "sandbox.app.nav.pedidos": "Orders",
1122
+ "sandbox.app.nav.productos": "Products",
1123
+ "sandbox.app.nav.reportes": "Reports",
1124
+ "sandbox.app.sub.pagados": "Paid",
1125
+ "sandbox.app.productos.title": "Products",
1126
+ "sandbox.app.productos.lead": "Catalog with SKU, price, stock and category; the table paginates and sorts from the host.",
1127
+ "sandbox.app.reportes.title": "Reports",
1128
+ "sandbox.app.reportes.lead": "Period indicators and the order detail behind them.",
1129
+ "sandbox.app.reportes.ticket": "Average ticket",
1130
+ "sandbox.app.reportes.conversion": "Conversion",
1131
+ "sandbox.app.valid": "Valid schema",
1132
+ "sandbox.app.partial": "Partial schema",
1133
+ "sandbox.app.invalid": "Invalid schema",
1134
+ "sandbox.app.loading": "Loading the page…",
1135
+ "sandbox.app.loadError": "The page could not be loaded",
1136
+ "sandbox.app.back": "Back to home",
1137
+ "sandbox.app.statUsers": "Users",
1138
+ "sandbox.app.statOrders": "Orders",
1139
+ "sandbox.app.statRevenue": "Revenue",
1140
+ "sandbox.app.statPending": "Pending",
1141
+ "sandbox.app.inicio.title": "Operations overview",
1142
+ "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
+ "sandbox.app.inicio.ctaUsers": "Manage users",
1144
+ "sandbox.app.inicio.ctaOrders": "See orders",
1145
+ "sandbox.app.inicio.recent": "Recent orders",
1146
+ "sandbox.app.inicio.recentLead": "Pagination, sorting and filters are the table's job; the data is the host's.",
1147
+ "sandbox.app.usuarios.title": "Users",
1148
+ "sandbox.app.usuarios.lead": "The Name column filters and sorts; Role and Status filter by catalog. All declared in JSON.",
1149
+ "sandbox.app.usuarios.new": "New user",
1150
+ "sandbox.app.usuarios.active": "Active",
1151
+ "sandbox.app.usuarios.admins": "Admins",
1152
+ "sandbox.app.usuarios.inactive": "Inactive",
1153
+ "sandbox.app.pedidos.title": "Orders",
1154
+ "sandbox.app.pedidos.lead": "The Total column uses the table's currency formatting; status filters by catalog.",
1155
+ "sandbox.app.pedidos.new": "New order",
1156
+ "sandbox.app.pedidos.paid": "Paid",
1157
+ "sandbox.app.pedidos.pending": "Pending",
1158
+ "sandbox.app.pedidos.cancelled": "Cancelled",
1159
+ "sandbox.demo.brand": "axzydev · portfolio in JSON",
1160
+ "sandbox.demo.back": "Back to the sandbox",
1161
+ "sandbox.demo.eyebrow": "Demo · screen engine",
1162
+ "sandbox.demo.title": "From API to interface: a complete product, in production.",
1163
+ "sandbox.demo.lead":
1164
+ "This screen has no TSX at all: a JSON file is loaded, validated against the catalog and rendered by the engine. Everything you see —layout, cards, actions and language— comes from the schema.",
1165
+ "sandbox.demo.ctaProjects": "See projects",
1166
+ "sandbox.demo.ctaContact": "Contact",
1167
+ "sandbox.demo.ctaPanel": "Open the panel",
1168
+ "sandbox.demo.statExperience": "Experience",
1169
+ "sandbox.demo.statProducts": "Products",
1170
+ "sandbox.demo.statTech": "Technologies",
1171
+ "sandbox.demo.statMode": "Work mode",
1172
+ "sandbox.demo.statModeValue": "Remote",
1173
+ "sandbox.demo.projectsTitle": "Projects",
1174
+ "sandbox.demo.projectsLead": "Every card is a schema node; the button asks for an action registered by the host.",
1175
+ "sandbox.demo.viewProject": "View details",
1176
+ "sandbox.demo.p1.text": "Security operations: QR rounds, incidents and performance analytics.",
1177
+ "sandbox.demo.p1.alt": "CheckApp: security analytics dashboard",
1178
+ "sandbox.demo.p2.text": "63 accessible, themeable components with i18n, published on npm.",
1179
+ "sandbox.demo.p2.alt": "AXZY UI System: components running",
1180
+ "sandbox.demo.p3.text": "HTTP client for the terminal with YAML-versioned requests and a CI mode.",
1181
+ "sandbox.demo.p3.alt": "axzy: HTTP client in the terminal",
1182
+ "sandbox.demo.stackTitle": "Stack",
1183
+ "sandbox.demo.stackLead": "Tags are ITChip nodes; adding one means adding an object to the array.",
1184
+ "sandbox.demo.contactTitle": "Contact",
1185
+ "sandbox.demo.contactLead": "Buttons carry no URLs: they ask for named actions and the host decides where they go.",
1186
+ "sandbox.demo.sendMail": "Write an email",
1187
+ "sandbox.demo.footer": "Asael Amaro · Fullstack Developer · Remote, Mexico",
1188
+ "sandbox.demo.badge": "Valid JSON",
1189
+
972
1190
  "sandbox.search.clear": "Clear search",
973
1191
  "sandbox.search.results": "{count} results",
974
1192
 
@@ -104,6 +104,7 @@ export const SANDBOX_GROUPS: SandboxGroup[] = [
104
104
  { id: "toast", label: "ITToast" },
105
105
  { id: "loader", label: "ITLoader" },
106
106
  { id: "themeprovider", label: "ITThemeProvider" },
107
+ { id: "jsonscreen", label: "Motor de pantallas (nuevo)" },
107
108
  { id: "jsonrenderer", label: "ITJsonRenderer (PoC)" },
108
109
  { id: "jsoneditor", label: "ITJSONEditor" },
109
110
  ],
@@ -3,7 +3,7 @@ import { GROUP_SLUGS, groupBySlug, isValidItem } from "./navigation";
3
3
  import { PRODUCTOS } from "../showcases/productos";
4
4
 
5
5
  /** Top-level sandbox views. `home` is the portfolio, `producto` a project detail page. */
6
- export type SandboxView = "home" | "ui-system" | "producto";
6
+ export type SandboxView = "home" | "ui-system" | "producto" | "demo";
7
7
 
8
8
  /** Parsed representation of `window.location.hash`. */
9
9
  export interface HashRoute {
@@ -17,6 +17,8 @@ export interface HashRoute {
17
17
  producto: string | null;
18
18
  /** Raw hash exactly as read from the URL (used by the 404 page). */
19
19
  raw: string;
20
+ /** Parámetros `?clave=valor` del hash (filtros persistentes). */
21
+ query: Record<string, string>;
20
22
  /** Whether the route resolved to a real page. `false` → render 404. */
21
23
  valid: boolean;
22
24
  }
@@ -39,7 +41,17 @@ function isValidProducto(id: string): boolean {
39
41
  */
40
42
  export function parseHash(hash: string): HashRoute {
41
43
  const raw = hash;
42
- const clean = hash.startsWith("#") ? hash.slice(1) : hash;
44
+ const body = hash.startsWith("#") ? hash.slice(1) : hash;
45
+ const [clean, queryString = ""] = body.split("?");
46
+ const query: Record<string, string> = Object.fromEntries(
47
+ queryString
48
+ .split("&")
49
+ .filter(Boolean)
50
+ .map((par) => {
51
+ const [clave, valor = ""] = par.split("=");
52
+ return [decodeURIComponent(clave), decodeURIComponent(valor)];
53
+ })
54
+ );
43
55
  const parts = clean.split("/").filter(Boolean);
44
56
 
45
57
  if (parts[0] === "proyecto") {
@@ -50,24 +62,32 @@ export function parseHash(hash: string): HashRoute {
50
62
  item: null,
51
63
  producto,
52
64
  raw,
65
+ query,
53
66
  valid: Boolean(producto) && isValidProducto(producto as string),
54
67
  };
55
68
  }
56
69
 
70
+ if (parts[0] === "demo") {
71
+ if (parts[1] === "app") {
72
+ return { view: "demo", group: "app", item: parts[2] ?? "inicio", producto: null, raw, query, valid: parts.length <= 3 };
73
+ }
74
+ return { view: "demo", group: null, item: null, producto: null, raw, query, valid: parts.length === 1 };
75
+ }
76
+
57
77
  if (parts.length === 0 || parts[0] !== "ui-system") {
58
- return { view: "home", group: null, item: null, producto: null, raw, valid: true };
78
+ return { view: "home", group: null, item: null, producto: null, raw, query, valid: true };
59
79
  }
60
80
 
61
81
  if (parts.length === 1) {
62
- return { view: "ui-system", group: null, item: null, producto: null, raw, valid: true };
82
+ return { view: "ui-system", group: null, item: null, producto: null, raw, query, valid: true };
63
83
  }
64
84
 
65
85
  if (parts.length === 2) {
66
86
  const group = groupBySlug(parts[1]);
67
87
  if (!group) {
68
- return { view: "ui-system", group: null, item: null, producto: null, raw, valid: false };
88
+ return { view: "ui-system", group: null, item: null, producto: null, raw, query, valid: false };
69
89
  }
70
- return { view: "ui-system", group: group.id, item: null, producto: null, raw, valid: true };
90
+ return { view: "ui-system", group: group.id, item: null, producto: null, raw, query, valid: true };
71
91
  }
72
92
 
73
93
  if (parts.length === 3) {
@@ -79,13 +99,14 @@ export function parseHash(hash: string): HashRoute {
79
99
  item: null,
80
100
  producto: null,
81
101
  raw,
102
+ query,
82
103
  valid: false,
83
104
  };
84
105
  }
85
- return { view: "ui-system", group: group.id, item: parts[2], producto: null, raw, valid: true };
106
+ return { view: "ui-system", group: group.id, item: parts[2], producto: null, raw, query, valid: true };
86
107
  }
87
108
 
88
- return { view: "ui-system", group: null, item: null, producto: null, raw, valid: false };
109
+ return { view: "ui-system", group: null, item: null, producto: null, raw, query, valid: false };
89
110
  }
90
111
 
91
112
  /** Build a hash for a group/item pair, translating the group id to its slug. */
@@ -117,6 +138,8 @@ export interface UseHashRouteResult {
117
138
  goToProducto: (id: string, options?: GoToOptions) => void;
118
139
  /** Navigate back to the portfolio (strips the hash). */
119
140
  goToPortfolio: (options?: GoToOptions) => void;
141
+ /** Abre el portafolio de demostración pintado desde JSON (`#demo`). */
142
+ goToDemo: (options?: GoToOptions) => void;
120
143
  }
121
144
 
122
145
  /**
@@ -171,6 +194,13 @@ export function useHashRoute(): UseHashRouteResult {
171
194
  [navigate],
172
195
  );
173
196
 
197
+ const goToDemo = useCallback(
198
+ (options?: GoToOptions) => {
199
+ navigate("#demo", options);
200
+ },
201
+ [navigate],
202
+ );
203
+
174
204
  const goToPortfolio = useCallback((options?: GoToOptions) => {
175
205
  const url = `${window.location.pathname}${window.location.search}`;
176
206
  if (options?.replace) {
@@ -181,5 +211,5 @@ export function useHashRoute(): UseHashRouteResult {
181
211
  setRoute(parseHash(""));
182
212
  }, []);
183
213
 
184
- return { route, goToItem, goToLanding, goToProducto, goToPortfolio };
214
+ return { route, goToItem, goToLanding, goToProducto, goToPortfolio, goToDemo };
185
215
  }