Zentto Studio — Guía para Desarrolladores
Qué es Zentto Studio
Sección titulada «Qué es Zentto Studio»Mini-framework web component que genera aplicaciones completas desde JSON. Un StudioSchema o AppConfig → app completa con sidebar, formularios, grids, charts, botones — todo renderizado dinámicamente.
Instalación
Sección titulada «Instalación»npm install @zentto/studio @zentto/studio-core @zentto/studio-reactnext.config.mjs
Sección titulada «next.config.mjs»transpilePackages: ['@zentto/studio', '@zentto/studio-core', '@zentto/studio-react', 'lit']Componentes Web
Sección titulada «Componentes Web»| Componente | Tag | Uso |
|---|---|---|
| Designer | <zs-page-designer> | Editor visual Figma-style |
| Renderer | <zentto-studio-renderer> | Renderiza StudioSchema |
| App | <zentto-studio-app> | App completa desde AppConfig |
| Wizard | <zs-app-wizard> | Wizard 7 pasos para crear apps |
Uso básico en React/Next.js
Sección titulada «Uso básico en React/Next.js»"use client";import { useEffect, useState, useRef } from "react";
export default function Page() { const [ready, setReady] = useState(false); const ref = useRef<any>(null);
useEffect(() => { import("@zentto/studio").then(() => setReady(true)); }, []);
useEffect(() => { if (!ready || !ref.current) return; ref.current.schema = { /* StudioSchema JSON */ }; }, [ready]);
return ready ? <zentto-studio-renderer ref={ref} /> : <div>Cargando...</div>;}Sistema de Eventos
Sección titulada «Sistema de Eventos»Zentto Studio emite CustomEvents estándar del DOM con bubbles: true, composed: true. La app host los escucha y decide qué hacer.
Eventos disponibles
Sección titulada «Eventos disponibles»| Evento | Cuándo se emite | Detail |
|---|---|---|
studio-action | Click en botón/link | { actionType, actionUrl, actionMethod, eventName, navigateTo } |
studio-submit | Botón tipo “submit” | { fieldId, url, method } |
studio-navigate | Botón tipo “navigate” | { path } |
studio-reset | Botón tipo “reset” | { fieldId } |
studio-action-start | Inicio de apiCall | { fieldId } |
studio-action-result | Resultado de apiCall | { fieldId, success, data, status } |
schema-change | Cambio en designer | { schema } |
field-change | Cambio en campo | { fieldId, field, value, data } |
field-action | Row-click en datagrid | { fieldId, action, row } |
grid-config-change | Config grid cambió | { columns, layout, gridId } |
Ejemplo: Escuchar eventos en React
Sección titulada «Ejemplo: Escuchar eventos en React»useEffect(() => { const el = studioRef.current;
el.addEventListener('studio-action', async (e) => { const { actionType, actionUrl, actionMethod } = e.detail;
if (actionType === 'apiCall') { const res = await fetch(actionUrl, { method: actionMethod, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(el.data), }); const data = await res.json(); console.log('API response:', data); }
if (actionType === 'navigate') { router.push(e.detail.navigateTo); }
if (actionType === 'custom') { console.log('Custom event:', e.detail.eventName); } });}, []);Tipos de Campo
Sección titulada «Tipos de Campo»Básicos
Sección titulada «Básicos»| Tipo | Descripción | Props clave |
|---|---|---|
text | Input texto | inputType, placeholder |
textarea | Área de texto | - |
number | Numérico | min, max, step |
currency | Moneda | isCurrency, currencySymbol |
percentage | Porcentaje | isPercentage |
date | Fecha | mode (date/time/datetime) |
select | Selector | options: [{value, label}] |
multiselect | Multi selector | multiple: true |
radio | Radio buttons | mode: 'radio', horizontal |
checkbox | Checkbox | - |
switch | Toggle | - |
email | validación automática | |
password | Password | inputType: 'password' |
phone | Teléfono | inputType: 'tel' |
Avanzados
Sección titulada «Avanzados»| Tipo | Descripción |
|---|---|
color | Color picker |
signature | Canvas firma digital |
slider | Slider numérico |
rating | Estrellas |
tags | Tags con autocomplete |
chips | Chips con colores |
address | Dirección compuesta |
| Tipo | Descripción | Props clave |
|---|---|---|
datagrid | Grid embebido (<zentto-grid>) | endpoint, columns, height, density, enableToolbar, enableSearch, enablePagination, enableConfigurator |
report | Report viewer embebido | templateId, height, zoom |
chart | Gráfico SVG | chartType, labelField, valueField |
lookup | Búsqueda async | endpoint, valueField, displayField |
treeview | Árbol recursivo | multiSelect, showCheckboxes |
| Tipo | Descripción | Props clave |
|---|---|---|
button | Botón de acción | buttonLabel, variant, actionType, actionUrl, eventName |
link | Enlace | navigateTo |
heading | Título H1-H4 | level |
separator | Línea divisoria | - |
spacer | Espacio vacío | height |
html | HTML libre | content |
alert | Alerta visual | content |
card | Tarjeta | content |
icon | Icono decorativo | iconName, iconSize |
progress | Barra de progreso | content |
avatar | Avatar circular | content |
Botón — Sistema de Acciones
Sección titulada «Botón — Sistema de Acciones»El botón es el componente clave para interactividad. Soporta 6 tipos de acción:
apiCall — Llamar API
Sección titulada «apiCall — Llamar API»{ "type": "button", "props": { "buttonLabel": "Guardar Cliente", "variant": "primary", "actionType": "apiCall", "actionUrl": "/v1/clientes", "actionMethod": "POST", "confirmMessage": "¿Guardar este cliente?", "successMessage": "Cliente guardado", "errorMessage": "Error al guardar" }}El botón emite studio-action con los datos del formulario. La app host hace el fetch real.
submit — Enviar formulario
Sección titulada «submit — Enviar formulario»{ "type": "button", "props": { "buttonLabel": "Enviar", "actionType": "submit", "actionUrl": "/v1/formularios", "actionMethod": "POST" }}navigate — Navegar
Sección titulada «navigate — Navegar»{ "type": "button", "props": { "buttonLabel": "Ver Detalle", "variant": "ghost", "actionType": "navigate", "navigateTo": "/clientes/{id}" }}custom — Evento personalizado
Sección titulada «custom — Evento personalizado»{ "type": "button", "props": { "buttonLabel": "Calcular Total", "actionType": "custom", "eventName": "onCalculateTotal" }}La app React escucha studio-action y ejecuta lógica custom cuando eventName === 'onCalculateTotal'.
Designer — Guardar/Cargar desde API
Sección titulada «Designer — Guardar/Cargar desde API»El <zs-page-designer> tiene guardar/cargar integrado que usa la API de addons:
<zs-page-designer ref={ref} save-api-url="http://localhost:4000" save-api-token={token}/>Endpoints que usa:
Sección titulada «Endpoints que usa:»GET /v1/studio/addons— listar schemasPOST /v1/studio/addons— crear addonPUT /v1/studio/addons/:id— actualizarDELETE /v1/studio/addons/:id— eliminar
DataGrid — Configuración Completa
Sección titulada «DataGrid — Configuración Completa»Cuando se agrega un DataGrid al designer, viene con todas las props:
{ "type": "datagrid", "props": { "endpoint": "/v1/clientes", "columns": [], "height": "400px", "pageSize": 25, "density": "compact", "enableToolbar": true, "enableSearch": true, "enablePagination": true, "enableConfigurator": true, "enableExport": false, "enableHeaderFilters": false, "showTotals": false, "gridLayout": null }}El grid auto-detecta columnas al conectar un endpoint. La configuración del usuario (columnas, anchos, orden, filtros) se persiste automáticamente via evento grid-config-change.
Expression Engine
Sección titulada «Expression Engine»Motor de expresiones seguro (sin eval). Sintaxis:
{precio} * {cantidad} * (1 + {iva}/100){role} == "admin" AND {total} > 1000IF({activo}, UPPER({nombre}), "INACTIVO")Funciones disponibles
Sección titulada «Funciones disponibles»- String: LEFT, RIGHT, MID, LEN, TRIM, UPPER, LOWER, REPLACE, CONTAINS, CONCAT
- Math: ABS, ROUND, FLOOR, CEIL, MIN, MAX, SUM, AVG, SQRT, POW
- Logic: IF, IIF, SWITCH, COALESCE, ISNULL, ISEMPTY, NOT
- Dates: NOW, TODAY, YEAR, MONTH, DAY, DATEADD, DATEDIFF
- Studio: FIELD, DATASOURCE, ROLE_IS, HAS_ROLE, CHANGED
StudioProvider — Bridge con React
Sección titulada «StudioProvider — Bridge con React»const provider = { fetchData: async (url, options) => { return await apiGet(url); // con auth automática }, handleAction: async (actionId, type, data) => { if (type === 'apiCall') { return await apiPost(data.actionUrl, data); } }, navigate: (path) => router.push(path), notify: (type, title, msg) => toast[type](msg), getUser: () => ({ id: session.userId, name: session.userName }), getAuthHeaders: () => ({ Authorization: `Bearer ${token}` }),};Addons — API REST
Sección titulada «Addons — API REST»Los addons son schemas guardados en el servidor que los usuarios pueden ejecutar como apps.
Endpoints
Sección titulada «Endpoints»| Método | Ruta | Descripción |
|---|---|---|
| GET | /v1/studio/addons | Listar addons |
| GET | /v1/studio/addons/:id | Obtener addon con config |
| POST | /v1/studio/addons | Crear addon |
| PUT | /v1/studio/addons/:id | Actualizar |
| DELETE | /v1/studio/addons/:id | Eliminar (soft) |
| GET | /v1/studio/addons/module/:moduleId | Filtrar por módulo |
Servicio compartido (shared-api)
Sección titulada «Servicio compartido (shared-api)»import { listAddons, getAddon, createAddon, updateAddon, deleteAddon } from '@zentto/shared-api';
// API first, localStorage como fallbackconst addons = await listAddons('compras');const addon = await getAddon('addon-123');await createAddon({ title: 'Mi App', modules: ['compras'], config: schema });Arquitectura npm
Sección titulada «Arquitectura npm»| Paquete | Descripción |
|---|---|
@zentto/studio-core | Lógica pura: types, engine, validation, providers |
@zentto/studio | Web components Lit 3.x: renderer, designer, wizard |
@zentto/studio-react | Wrapper React con createComponent |