DIL 1.0 — Declarative Interface Language
Especificación formal · Versión 1.0 · Estado: Draft para congelación
DIL is a declarative, JSON-based intermediate representation for describing user interfaces, component systems, layouts, visual states, interactions, application state, and interactive prototypes.
| Campo | Valor |
|---|---|
| Versión de la especificación | 1.0 |
| Formato de serialización | JSON (UTF-8, perfil I-JSON, RFC 7493) |
| Extensión de archivo | .dil.json (alternativa: .dil) |
| Media type | application/dil+json |
| Campo de versión en el documento | "dil": "1.0" |
| Lenguaje de la especificación | Español (identificadores y claves en inglés) |
Tabla de contenidos
- Convenciones
- Introducción
- Conformidad
- Modelo de documento
- Tipos primitivos
- Identificadores, ámbitos y referencias
- Resolución de valores
- Tokens y temas
- Nodos
- Layout
- Estilo visual
- Tipografía y texto
- Controles y formularios
- Componentes
- Cascada y precedencia
- Responsive
- Estado, datos y bindings
- Lenguaje de expresiones
- Eventos y acciones
- Páginas, navegación, overlays y flujos
- Transiciones y animaciones
- Accesibilidad
- Assets
- Metadatos de editor
- Modelo de ejecución (runtime)
- Validación y diagnósticos
- Seguridad
- Compatibilidad y hoja de ruta
- Apéndice A — Tipos TypeScript
- Apéndice B — JSON Schema
- Apéndice C — Ejemplos completos
- Apéndice D — Checklist de implementación
0. Convenciones
0.1 Palabras normativas
Las palabras DEBE, NO DEBE, DEBERÍA, NO DEBERÍA y PUEDE se interpretan como en RFC 2119:
| Palabra | Significado |
|---|---|
| DEBE / NO DEBE | Requisito absoluto. |
| DEBERÍA / NO DEBERÍA | Recomendación fuerte; puede ignorarse solo con razones justificadas. |
| PUEDE | Opcional. |
0.2 Notación
- Los bloques
jsonson normativos cuando se presentan como ejemplo de sintaxis válida, salvo que se indique// inválido. T?significa propiedad opcional.T | Usignifica unión.Dynamic<T>significa que el valor puede ser un literalT, una referencia a token ("$color.primary") o una expresión interpolada ("{{ count + 1 }}"). Ver §6.- Las secciones marcadas (informativa) no imponen requisitos.
- Las rutas de error usan JSON Pointer (RFC 6901).
0.3 Unidades y sistema de coordenadas
- Todas las longitudes numéricas son píxeles lógicos (dp). DIL 1.0 no define otras unidades.
- Origen
(0,0)en la esquina superior izquierda;xcrece a la derecha,ycrece hacia abajo. - Los ángulos están en grados, sentido horario.
- Las duraciones están en milisegundos.
- Las opacidades y fracciones están en el rango cerrado
[0, 1].
1. Introducción
1.1 ¿Qué es DIL?
DIL es una UI Intermediate Representation (UI-IR) declarativa. Describe qué es una interfaz y cómo se comporta, no cómo se implementa en una tecnología concreta. No sustituye a HTML/CSS/JS: es una capa superior que puede visualizarse, editarse, validarse, generarse con IA y compilarse a otros destinos.
HTML → lenguaje de documentos web
CSS → lenguaje de presentación
JavaScript → lenguaje de comportamiento
DIL → representación declarativa de
INTERFAZ + COMPONENTES + LAYOUT + ESTADO + LÓGICA + PROTOTIPO1.2 Objetivos
- Diseño visual completo: desde mockups sencillos hasta diseños de alta fidelidad (layout, tipografía, color, sombras, gradientes, temas, componentes).
- Prototipos programables: estado, bindings, validación, condiciones, navegación, overlays, transiciones y animaciones, sin código arbitrario.
- Fácil de leer y escribir: lo frecuente es corto; lo avanzado es opcional (progressive complexity).
- Validable de forma estática: todo documento puede comprobarse (estructura, tipos, referencias, expresiones) sin ejecutarlo.
- Generable por máquinas: determinista, sin ambigüedad, apto para IA y para editores visuales.
- Portable: interpretable por un runtime propio y compilable a React/HTML/otros.
1.3 No-objetivos de 1.0
- Ejecutar JavaScript u otro código arbitrario.
- Acceso a red (fetch/APIs/autenticación), plugins, persistencia (ver §27).
- Reemplazar CSS: DIL define conceptos propios, no CSS escrito como JSON.
- Edición colaborativa / control de versiones (responsabilidad del editor).
1.4 Principios de diseño
| # | Principio | Consecuencia |
|---|---|---|
| P1 | Simple cosas simples | { "type": "button", "text": "Save" } es un documento válido. |
| P2 | Complejidad progresiva | Se añade variant, states, on, animation… solo cuando se necesitan, sin cambiar la estructura. |
| P3 | Lógica declarativa, no código | Expresiones puras sin efectos + un conjunto cerrado de acciones. |
| P4 | Una forma canónica | Los atajos (shorthands) se normalizan a una única representación. |
| P5 | Runtime ≠ Editor | Los metadatos del editor viven aparte y se pueden eliminar sin cambiar el comportamiento. |
| P6 | Determinismo | Misma entrada + mismos eventos ⇒ mismo resultado. El orden de claves JSON no es significativo. |
| P7 | Conceptos visuales | Un diseñador debe poder leerlo como un árbol de capas (Frame → hijos). |
1.5 Arquitectura
DIL
│
┌─────────────┴─────────────┐
│ │
DESIGN BEHAVIOR
│ │
┌────────┼─────────┐ ┌────────┼─────────┐
│ │ │ │ │ │
Layout Style Components State Logic Events
│ │ │ │ │ │
Flex Tokens Variants Data Conditions Actions
Grid Themes States Bindings Validation Navigation
Absolute Type Slots Mock Expressions Animations DIL Document
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
DIL Editor DIL Runtime DIL Compiler
(diagramador) (preview/prototipo) (React / HTML / …)1.6 Los tres niveles de complejidad
Fácil — estructura y contenido:
{ "type": "button", "text": "Save" }Intermedio — estilo e interacción:
{
"type": "button",
"text": "Save",
"variant": "primary",
"on": { "click": "save" }
}Avanzado — estados, lógica, animación:
{
"type": "button",
"text": "Save",
"variant": "primary",
"states": { "hover": { "opacity": 0.8 } },
"transition": { "duration": 150 },
"on": {
"click": {
"if": "form.valid",
"then": [
{ "set": { "saving": true } },
{ "animate": "loading" },
{ "navigate": "dashboard" }
]
}
}
}Un procesador NO DEBE exigir conocer el nivel avanzado para procesar el nivel fácil.
2. Conformidad
2.1 Clases de producto conforme
| Clase | Requisitos |
|---|---|
| Documento DIL | Cumple §3–§23 y supera la validación estructural y semántica (§25) sin errores. |
| Parser/Normalizador | Implementa §3.3–§3.4 y produce la forma canónica. |
| Validador | Implementa los 4 pases de §25 y emite diagnósticos con los códigos de §25.3. |
| Runtime | Implementa §6, §14–§20, §24. Ignora editor (§23). |
| Editor | Lee/escribe documentos válidos, preserva las claves desconocidas x-* y editor. |
| Compilador | Transforma un documento válido a un destino; DEBE documentar qué features no soporta. |
2.2 Modos de procesamiento
| Modo | Propiedades desconocidas | Features futuras (dil minor mayor) | Uso |
|---|---|---|---|
strict (por defecto) | Error DIL-E003 | Error DIL-E002 | CI, exportación, generación por IA |
lenient | Aviso DIL-W007, se ignoran | Aviso, se ignoran | Edición, lectura de documentos más nuevos |
3. Modelo de documento
3.1 Codificación
- JSON (RFC 8259) con perfil I-JSON (RFC 7493): UTF-8, sin claves duplicadas, números finitos (sin
NaN/Infinity), enteros seguros. - El orden de las claves de un objeto NO es significativo. El orden de los arrays SÍ lo es.
- Todo lo que dependa de un orden se modela con arrays (p. ej.
children,variants, reglas devalidate).
3.2 Objeto raíz (Document)
{
"dil": "1.0",
"meta": { },
"theme": "default",
"breakpoints": { },
"tokens": { },
"themes": { },
"styles": { },
"assets": { },
"components": { },
"state": { },
"computed": { },
"data": { },
"actions": { },
"animations": { },
"pages": [ ],
"flows": [ ],
"start": "home",
"editor": { }
}| Clave | Tipo | Obligatoria | Descripción |
|---|---|---|---|
dil | "1.0" | Sí | Versión (§3.6). |
pages | Page[] | Sí (≥ 1) | Pantallas y overlays (§19). |
meta | Meta | No | Nombre, autor, descripción… |
theme | string | No | Tema activo inicial. Por defecto "default" (§7.3). |
breakpoints | Record<Id, number> | No | Puntos de quiebre (§15). |
tokens | Tokens | No | Variables de diseño (§7). |
themes | Record<Id, Theme> | No | Sobrescrituras de tokens por tema. |
styles | Record<Id, StylePatch> | No | Estilos con nombre reutilizables (§8.5). |
assets | Record<Id, Asset> | No | Imágenes, SVG, fuentes (§22). |
components | Record<Id, Component> | No | Definiciones de componentes (§13). |
state | Record<Name, StateDecl | literal> | No | Estado global (§16). |
computed | Record<Name, Expr> | No | Valores derivados globales. |
data | Record<Name, JSON> | No | Datos mock de solo lectura (§16.5). |
actions | Record<Id, ActionSpec> | No | Acciones con nombre (§18). |
animations | Record<Id, Animation> | No | Animaciones con nombre (§20). |
flows | Flow[] | No | Flujos de prototipo (§19.6). |
start | Id | No | Página inicial. Por defecto, la primera de pages. |
editor | EditorMeta | No | Solo para el editor (§23). |
Separación Runtime / Editor (normativo):
DIL Document
├── Runtime representation (todo excepto `editor`)
└── Editor metadata (claves `editor`, en raíz y en nodos)3.3 Pipeline de procesamiento
Todo procesador conforme se comporta como si ejecutara estas fases en orden:
① Parse JSON → árbol de valores (I-JSON)
② Normalize atajos → forma canónica; asignar ids faltantes
③ Link resolver referencias (componentes, páginas, estilos, assets, acciones, tokens)
④ Validate estructura · semántica · tipos estáticos · lint (§25)
⑤ Instantiate expandir instancias de componentes y repeat
⑥ Cascade elegir el valor ganador por propiedad (§14)
⑦ Resolve token → tema → expresión (§6)
⑧ Layout/RenderLas fases ①–④ son estáticas (no dependen de eventos). ⑤–⑧ son reactivas (se reevalúan cuando cambian estado, viewport, tema o interacción).
3.4 Forma canónica y atajos (shorthands)
El autor PUEDE escribir atajos; el normalizador DEBE expandirlos. La forma canónica usa propiedades longhand planas, y la cascada opera sobre ellas.
| Autoría (atajo) | Forma canónica |
|---|---|
"layout": "row" | "layout": "flex", "direction": "row" |
"layout": "column" | "layout": "flex", "direction": "column" |
"layout": "absolute" | "layout": "none" |
"padding": 16 | paddingTop/Right/Bottom/Left: 16 |
"padding": "16 24" | top/bottom 16, left/right 24 |
"padding": "8 16 24" | top 8, left/right 16, bottom 24 |
"padding": "1 2 3 4" | top, right, bottom, left (sentido horario) |
"padding": [16, 24] | equivale a "16 24" |
"margin" | ídem a padding → marginTop/Right/Bottom/Left |
"radius": 8 | radiusTopLeft/TopRight/BottomRight/BottomLeft: 8 |
"radius": "8 0" | TL/BR 8, TR/BL 0 |
"gap": 16 | rowGap: 16, columnGap: 16 |
"gap": "8 16" / [8,16] | rowGap: 8, columnGap: 16 |
"fill": "#FFF" | "fill": [{ "type": "solid", "color": "#FFF" }] |
"border": "1 solid #CCC" | borderTopWidth…: 1, borderStyle: "solid", borderColor: "#CCC" |
"shadow": {…} | "shadow": [{…}] |
"width": "50%" | igual (porcentaje de la dimensión disponible del padre) |
"scale": 1.2 | scaleX: 1.2, scaleY: 1.2 |
"on": { "click": "save" } | "on": { "click": [{ "call": "save" }] } |
"on": { "click": { "navigate": "x" } } | "on": { "click": [{ "navigate": { "to": "x" } }] } |
{ "type": "button", "text": "Hi" } sin id | se asigna id automático determinista (auto:<ruta>) |
"state": { "count": 0 } | "state": { "count": { "type": "number", "default": 0 } } |
"states": { "hover": {…} } en children implícito | sin cambios |
"children": [..] en instance | "slots": { "default": [..] } |
Reglas:
- La normalización DEBE ser idempotente:
normalize(normalize(x)) == normalize(x). - Un atajo y su longhand NO DEBEN coexistir en el mismo objeto con valores contradictorios (
DIL-E003). Si coexisten, el longhand gana solo si no hay contradicción (p. ej."padding": 16, "paddingTop": 8⇒ top8, resto16). - Un IDs automático (
auto:…) NO PUEDE ser referenciado desde acciones, overrides ni animaciones; para referenciar un nodo, este DEBE teneridexplícito.
3.5 Extensiones y propiedades desconocidas
- Cualquier objeto PUEDE contener claves con prefijo `x-` (p. ej.
"x-acme-lock": true). Los procesadores DEBEN preservarlas y NO DEBEN interpretarlas (a menos que el producto defina la extensión). - Cualquier otra clave desconocida es error en
stricty aviso enlenient(§2.2).
3.6 Versionado
- Formato:
"MAJOR.MINOR"(cadena). 1.0 es esta especificación. - MINOR (1.x): solo añade características opcionales y valores de enumeración; un documento 1.0 válido sigue siendo válido en 1.x.
- MAJOR: puede romper compatibilidad.
- Un procesador que soporte
1.NDEBE aceptar documentos1.MconM ≤ N. ConM > N:strict⇒DIL-E002;lenient⇒ aviso e ignorar lo desconocido. - Una característica deprecada se mantiene al menos un MAJOR completo y emite
DIL-W008.
4. Tipos primitivos
Estos tipos son la base de todas las tablas de propiedades. El Apéndice B los formaliza en JSON Schema.
| Tipo | Definición | Ejemplos válidos | Inválidos |
|---|---|---|---|
Number | Número JSON finito | 0, -3.5 | "3", NaN |
Integer | Entero | 3 | 3.2 |
Boolean | true / false | true | "true" |
String | Cadena UTF-8 | "Hola" | — |
Id | ^[A-Za-z_][A-Za-z0-9_-]{0,63}$ | "btn-save", "Home" | "1a", "a b", "a/b" |
Name | ^[A-Za-z_][A-Za-z0-9_]{0,63}$ (variables y props) | "count", "first_name" | "first-name" |
Length | Number ≥ 0 salvo que se indique (dp) | 16 | "16px" |
Percentage | ^-?\d+(\.\d+)?%$ | "50%", "100%" | "50" |
Fraction | Number en [0,1] | 0.5 | 1.2 |
Opacity | Fraction | 0.8 | 2 |
Angle | Number (grados) | 90, -45 | "90deg" |
Duration | Number ≥ 0 (ms) | 150 | "150ms" |
Color | Ver §4.1 | "#4F46E5" | "blue" |
Size | Length | Percentage | "auto" | "fit" | "fill" | 320, "fill" | "large" |
Spacing | Length | String(1–4 números) | Length[1..4] | 16, "16 24" | "16px" |
TokenRef | ^\$[A-Za-z_][A-Za-z0-9_-]*(\.[A-Za-z_][A-Za-z0-9_-]*)+$ | "$color.primary" | "$primary" |
Mustache | Cadena que contiene {{ … }} (§6) | "{{ count + 1 }}" | — |
Expr | Expresión sin llaves (§17) | "email != ''" | — |
Easing | Ver §20.1 | "ease-out", [0.2,0,0,1] | — |
Anchor | "top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right" | "center" | — |
AssetRef | Id de un asset del documento, o Mustache | "logo" | — |
4.1 Color
Un color es una cadena con una de estas formas:
| Forma | Gramática | Ejemplo |
|---|---|---|
| Hex | #RGB #RGBA #RRGGBB #RRGGBBAA | "#4F46E5", "#4F46E5CC" |
| RGB | rgb(R G B) / rgb(R G B / A) · R,G,B ∈ 0..255, A ∈ 0..1 | "rgb(79 70 229 / 0.8)" |
| HSL | hsl(H S% L%) / hsl(H S% L% / A) | "hsl(243 75% 59%)" |
| Palabra | "transparent" | "transparent" |
No se admiten nombres CSS ("red") en 1.0, para evitar diferencias entre runtimes. Las funciones también aceptan comas (rgba(79,70,229,0.8)) y se normalizan a espacios.
4.2 Size
| Valor | Significado |
|---|---|
Length (320) | Tamaño fijo en dp. |
Percentage ("50%") | Porcentaje de la dimensión disponible del padre (su content-box). Si el padre es fit en ese eje ⇒ DIL-E013. |
"fit" | Ajustar al contenido. |
"fill" | Ocupar el espacio disponible (§9.4). |
"auto" | Por defecto; se resuelve según contexto (§9.4). |
5. Identificadores, ámbitos y referencias
5.1 Registros y unicidad
| Registro | Forma | Ámbito de unicidad |
|---|---|---|
pages | array (id en cada página) | Documento |
components, assets, styles, actions, animations, themes | mapa (clave = id) | Documento |
flows | array | Documento |
| Nodos de una página | id | Esa página (su body) |
| Nodos de un componente | id | Esa definición de componente |
| Variables de estado / props | Name | Su ámbito (§16.2) |
- Dos registros diferentes PUEDEN usar el mismo id (p. ej. un componente
Homey una páginaHome), porque las referencias son tipadas por contexto. - Dentro de un mismo registro/ámbito, DEBE ser único (
DIL-E004). idde nodos sinrepeat: estable. Enrepeat, la instancia runtime tiene id lógico"<id>#<key>"(§8.7).
5.2 Rutas de override
Para apuntar a nodos dentro de instancias anidadas, se usa una ruta con /:
"icon" → nodo "icon" del componente directo
"header/title" → nodo "title" dentro de la instancia "header" (que está dentro del componente)
"$root" → nodo raíz del componente5.3 Referencias
| Referencia | Apunta a | Error si falla |
|---|---|---|
component: "Button" | components.Button | DIL-E005 |
navigate: "dashboard" / start / flows[].start | pages[].id | DIL-E005 |
"style": "card" | styles.card | DIL-E005 |
"src": "logo" | assets.logo | DIL-E005 |
{ "call": "save" } / "on": {"click":"save"} | actions.save | DIL-E005 |
{ "animate": "shake" } | animations.shake | DIL-E005 |
"$color.primary" | tokens.color.primary | DIL-E006 |
target: "emailField" | nodo con ese id en la página/componente | DIL-E005 |
6. Resolución de valores
Toda propiedad marcada Dynamic<T> admite tres formas. Se distinguen por su sintaxis, sin ambigüedad:
| Orden de reconocimiento | Forma | Sintaxis | Tipo resultante |
|:--:|---|---|---|
| 1 | Referencia a token | Cadena que coincide por completo con TokenRef: "$color.primary" | El valor del token (§7) |
| 2 | Expresión | Cadena completa entre llaves dobles: "{{ expr }}" | El tipo que devuelve expr |
| 3 | Interpolación | Cadena con uno o más {{ expr }} mezclados con texto: "Hola, {{ name }}!" | Siempre String |
| 4 | Literal | Cualquier otro valor | El propio valor |
Escape: una cadena que empiece con \ desactiva el reconocimiento de las formas 1–3 y la \ inicial se elimina. En JSON: "\\$5.00" ⇒ literal $5.00; "\\{{ x }}" ⇒ literal {{ x }}. Una cadena como "$5.00" ya es literal porque $5 no cumple TokenRef.
6.1 Orden de evaluación
Valor crudo elegido por la cascada (§14)
│
▼
┌────────────────┐ estático (por tema)
│ ¿TokenRef? │ ─► token activo del tema ─► (alias: seguir hasta valor final)
└────────────────┘
│ no
▼
┌────────────────┐ reactivo (state · props · data · viewport · form · …)
│ ¿Mustache? │ ─► evaluar expresión ─► valor
└────────────────┘
│ no
▼
literalEquivalencia con el modelo conceptual: literal → variable/token → tema → binding → expresión. El binding es simplemente una expresión que lee estado; el binding bidireccional se declara con bind (§12.2).
6.2 Reglas
- El resultado de la resolución DEBE ser asignable al tipo de la propiedad; si no ⇒
DIL-E011(estático, si es decidible) o diagnóstico en runtime y valor por defecto de la propiedad. - Las expresiones se reevalúan solo cuando cambian sus dependencias (§24.3).
- Un
Expr(sin llaves) aparece únicamente en campos de expresión (if,each,filter,key,limit,computed,rule: "expr"…). Se listan en §17.6. {{ }}NO PUEDE anidarse dentro de otra expresión.- Los valores literales de tipo objeto/array dentro de acciones (
set,append,host) se resuelven recursivamente hoja a hoja.
7. Tokens y temas
Los tokens son variables de diseño con nombre y tipo.
7.1 Categorías (cerradas en 1.0)
| Categoría | Tipo de valor | Aceptado en propiedades |
|---|---|---|
color | Color | fill(solid), color, borderColor, stroke, sombra color, stops |
space | Length | padding*, margin*, gap, rowGap, columnGap |
size | Length | width, height, min*, max*, fontSize, strokeWidth, borderWidth* |
radius | Length | radius* |
opacity | Fraction | opacity |
duration | Duration | duration, delay |
easing | Easing | easing |
font | String (familia) | fontFamily |
text | TextStyle (compuesto) | textStyle |
shadow | Shadow | Shadow[] | shadow |
gradient | Gradient | fill (como paint) |
Usar un token en una propiedad cuya categoría no acepta ⇒ DIL-E007.
7.2 Declaración
{
"tokens": {
"color": {
"brand": { "primary": "#4F46E5", "dark": "#3730A3" },
"text": "#0F172A",
"link": "$color.brand.primary"
},
"space": { "sm": 8, "md": 16, "lg": 24 },
"radius": { "md": 8 },
"font": { "base": "Inter" },
"duration": { "fast": 150 },
"easing": { "standard": [0.2, 0, 0, 1] },
"shadow": {
"card": { "x": 0, "y": 4, "blur": 16, "spread": 0, "color": "#0F172A1F" }
},
"text": {
"title": { "fontFamily": "$font.base", "fontSize": 28, "fontWeight": 700, "lineHeight": "125%" }
}
}
}- Se pueden agrupar tokens en niveles (
color.brand.primary). La ruta escategoría.segmento(.segmento)*. - Un token puede ser alias de otro (
"link": "$color.brand.primary"). Ciclos ⇒DIL-E008. - Un alias DEBE apuntar a un token de la misma categoría.
- Un token NO PUEDE contener expresiones
{{ }}(los tokens son estáticos).
7.3 Temas
Los tokens base forman el tema implícito "default". Un tema es una sobrescritura parcial:
{
"theme": "default",
"themes": {
"dark": {
"extends": "default",
"tokens": {
"color": { "text": "#F8FAFC", "surface": "#0B1120" }
}
}
}
}Resolución de un token (tema activo T):
tokens de T → (si T.extends) tokens del tema padre … → tokens base ("default")- Un tema solo puede sobrescribir tokens que existen en la base (
DIL-E006si no). - Un tema NO DEBE cambiar la categoría ni el tipo de un token.
- El tema activo es un valor reactivo accesible como la variable integrada
theme(cadena). Se cambia con la acciónsetTheme(§18.4). - Ciclos en
extends⇒DIL-E008.
8. Nodos
8.1 Árbol
Una interfaz es un árbol de nodos. Cada página tiene un body (nodo raíz) y cada componente un root.
Page
└─ body: frame
├─ text
├─ frame
│ ├─ image
│ └─ button
└─ repeat ─ template: frame8.2 Catálogo de tipos de nodo
type | Categoría | Hijos | Descripción |
|---|---|---|---|
frame | Contenedor | Sí | Contenedor con layout, estilo visual y clip. Unidad básica de diseño. |
group | Contenedor | Sí | Agrupación lógica sin layout propio ni estilo visual (solo transformaciones/opacidad). Su caja = bounding box de los hijos. |
text | Contenido | No | Texto (§11). |
image | Contenido | No | Imagen de un asset (§22). |
icon | Contenido | No | Icono vectorial. |
shape | Contenido | No | Forma vectorial: rect, ellipse, line, polygon, star, path. |
button | Control | No | Botón (§12). |
input | Control | No | Campo de texto de una línea. |
textarea | Control | No | Campo de texto multilínea. |
checkbox | Control | No | Casilla. |
radio | Control | No | Opción de grupo exclusivo. |
switch | Control | No | Interruptor. |
select | Control | No | Lista desplegable. |
slider | Control | No | Deslizador numérico. |
instance | Componente | No¹ | Instancia de un componente (§13). |
slot | Componente | Sí² | Punto de inserción dentro de un componente (§13.6). |
repeat | Estructural | No | Repetición de una plantilla sobre una colección (§8.7). |
¹ Su contenido se aporta por slots. ² Contenido por defecto (fallback).
8.3 Propiedades comunes (todos los nodos)
| Propiedad | Tipo | Defecto | Notas |
|---|---|---|---|
type | uno de §8.2 | — (obligatoria) | Discriminante. |
id | Id | auto | Único en su ámbito (§5.1). |
name | String | — | Etiqueta humana (capas del editor). No afecta al runtime. |
visible | Dynamic<Boolean> | true | false ⇒ no se renderiza ni ocupa layout. |
opacity | Dynamic<Opacity> | 1 | Se multiplica con la de los ancestros. |
disabled | Dynamic<Boolean> | false | Desactiva eventos de usuario en el subárbol y activa el estado disabled. |
selected | Dynamic<Boolean> | false | Activa el estado selected. |
loading | Dynamic<Boolean> | false | Activa el estado loading. |
style | Id | Id[] | — | Estilos con nombre (§8.5). |
states | Record<StateName, StatePatch> | — | Parches por estado (§13.4). |
responsive | Record<BreakpointName, StylePatch> | — | Parches por breakpoint (§15). |
transition | Transition | — | Interpolación al cambiar valores (§20.2). |
animation | AnimationRef | — | Animación automática (§20.3). |
on | Record<EventName, ActionSpec> | — | Eventos (§18). |
a11y | A11y | — | Accesibilidad (§21). |
editor | EditorMeta | — | Solo editor (§23). |
| Propiedades de ítem de layout | §9 | position, x, y, width, grow, col… | |
| Propiedades de transformación | §9.8 | rotate, scaleX… |
8.4 Contenedores: frame y group
frame admite además: propiedades de layout de contenedor (§9), estilo visual (§10), clip, scroll, children, y propiedades tipográficas heredables (§11.4).
{
"type": "frame",
"id": "card",
"layout": "column",
"gap": 12,
"padding": 16,
"fill": "$color.surface",
"radius": "$radius.md",
"children": [ { "type": "text", "text": "Título" } ]
}group admite propiedades comunes, transformaciones, children y las propiedades de layout de contenedor de §9. Cada grupo es un contexto independiente: su layout, dirección, separación, padding y alineaciones aplican solo a sus hijos y no heredan la alineación de su padre. Si omite layout, conserva el comportamiento libre (none) de §9.1. Un grupo creado en el editor puede guardar layout: "column", justify: "start" y align: "start" para disponer sus elementos como un div en columna alineado a la izquierda.
8.5 Estilos con nombre (styles)
Un estilo con nombre es un parche de propiedades reutilizable (equivalente tipado a una clase CSS):
{
"styles": {
"card": {
"fill": "$color.surface",
"radius": "$radius.md",
"shadow": "$shadow.card",
"padding": "$space.md"
},
"button.primary": {
"fill": "$color.brand.primary",
"color": "#FFFFFF",
"states": { "hover": { "opacity": 0.9 } }
}
}
}- Un
StylePatchadmite cualquier propiedad visual, de layout, de texto o de ítem exceptoid,type,children,on,style. - Un nodo con
"style": ["a", "b"]aplicaay luegob(el último gana), y después las propiedades propias del nodo (§14, capa L1). - Variante de control: en un control (
button,input…),"variant": "primary"equivale a añadir al principio el estilo implícito"<type>.<variant>"(p. ej.button.primary) si existe enstyles. Si no existe, el runtime PUEDE usar su apariencia por defecto (no normativa) y el validador emiteDIL-W010. Un editor DEBERÍA incluir un conjunto inicial (starter kit) de estos estilos al crear un documento. - Un estilo NO PUEDE referenciar a otros estilos.
8.6 shape
| Propiedad | Tipo | Notas |
|---|---|---|
kind | "rect" | "ellipse" | "line" | "polygon" | "star" | "path" | Obligatoria. |
d | String | Solo path: datos de trayecto SVG (M … Z). |
points | [number, number][] | Solo polygon (≥ 3). |
sides | Integer ≥ 3 | Solo polygon regular (alternativa a points). |
ratio | Fraction | Solo star: radio interior/exterior. |
fill | Paint | Paint[] | §10.1 |
stroke | Dynamic<Color> | Color del trazo. |
strokeWidth | Dynamic<Length> | Defecto 1. |
strokeCap | "butt" | "round" | "square" | Defecto "butt". |
strokeJoin | "miter" | "round" | "bevel" | Defecto "miter". |
strokeDash | Length[] | Patrón de guiones. |
rect usa radius* (§10.3). line va de (0,0) a (width,height) del nodo.
8.7 repeat (colecciones)
{
"type": "repeat",
"each": "data.projects",
"as": "project",
"index": "i",
"key": "project.id",
"sort": { "by": "project.name", "order": "asc" },
"filter": "project.active",
"limit": 20,
"template": { "type": "text", "text": "{{ i + 1 }}. {{ project.name }}" },
"empty": { "type": "text", "text": "Sin proyectos" }
}| Propiedad | Tipo | Defecto | Descripción |
|---|---|---|---|
each | Expr → Array | Integer | — (obligatoria) | Colección; un entero n repite n veces (índices 0..n-1). |
as | Name | "item" | Variable local del elemento. |
index | Name | "index" | Variable local del índice (0-based) tras filter/sort/limit. |
key | Expr | índice | Identidad estable del elemento (único; DIL-E004 en runtime si se repite). |
filter | Expr → Boolean | — | Se aplica antes de sort. |
sort | { by: Expr, order?: "asc" | "desc" } | — | by se evalúa por elemento. |
limit | Expr → Integer | — | Máximo de elementos tras filter/sort. |
template | Node | — (obligatoria) | Nodo que se instancia por elemento. |
empty | Node | — | Se muestra si el resultado está vacío. |
repeates transparente al layout: sus instancias se insertan como hijos del padre, en la posición delrepeat.aseindexson solo lectura y visibles solo dentro detemplate.- Orden de aplicación:
each→filter→sort→limit→key/index. - Límite: 10 000 elementos (
DIL-E021).
9. Layout
9.1 Modos de layout de contenedor
layout en un frame:
| Valor | Modo | Hijos |
|---|---|---|
"none" (alias "absolute") | Libre | Todos posicionados de forma absoluta (§9.5). Es el defecto (modelo de lienzo). |
"flex" | Flexbox | Flujo en una dimensión (§9.2). |
"row" / "column" | Atajos de flex | §3.4 |
"grid" | Cuadrícula | §9.3 |
Los hijos con position: "absolute" salen del flujo en flex y grid y se posicionan según §9.5.
9.2 Flex
| Propiedad (contenedor) | Tipo | Defecto | Descripción |
|---|---|---|---|
direction | "row" | "column" | "row-reverse" | "column-reverse" | "row" | Eje principal. |
wrap | Boolean | false | Permitir varias líneas. |
gap, rowGap, columnGap | Dynamic<Length> | 0 | Separación entre hijos/líneas. |
justify | "start" | "center" | "end" | "between" | "around" | "evenly" | "start" | Distribución en el eje principal. |
align | "start" | "center" | "end" | "stretch" | "baseline" | "start" | Alineación en el eje transversal. Con direction: "column", start corresponde al borde izquierdo. |
padding* | Dynamic<Length> | 0 | §9.6 |
| Propiedad (hijo) | Tipo | Defecto | Descripción |
|---|---|---|---|
grow | Number ≥ 0 | 0 (1 si el tamaño principal es "fill") | Peso para repartir el espacio sobrante. |
alignSelf | "auto" | "start" | "center" | "end" | "stretch" | "baseline" | "auto" | Sobrescribe align. |
Algoritmo normativo (simplificado):
- Cada hijo en flujo obtiene su tamaño base en el eje principal: fijo (
Length),Percentage(del content-box del contenedor), o contenido (fit). Los hijos con tamaño principal"fill"empiezan en0y participan en el reparto. espacioLibre = tamañoPrincipalDisponible − Σ tamañosBase − Σ gaps.- Si
espacioLibre > 0: se reparte entre hijos congrow > 0proporcionalmente agrow; cada uno respetaminWidth/maxWidth(oHeight). Si no hay hijos congrow, se aplicajustify. - Si
espacioLibre < 0: ningún hijo se encoge; el contenido desborda (visible,clipuscrollsegún §9.7). - Eje transversal: cada hijo usa
alignSelf(oalign). Constretch, un hijo cuyo tamaño transversal es"auto"o"fill"ocupa todo el espacio transversal de la línea. wrap: truecrea líneas nuevas cuando la suma de tamaños base + gaps excede el espacio principal;rowGap/columnGapseparan líneas y elementos.justify: "between" | "around" | "evenly"solo aplica si no hay hijosgrow > 0.
9.3 Grid
| Propiedad (contenedor) | Tipo | Defecto | Descripción |
|---|---|---|---|
columns | Integer | Track[] | 1 | Pistas de columna. 3 ≡ ["1fr","1fr","1fr"]. |
rows | Integer | Track[] | auto | Pistas de fila explícitas; las filas extra son "auto". |
flow | "row" | "column" | "row" | Sentido de colocación automática. |
gap, rowGap, columnGap | Length | 0 | |
justify | "start" | "center" | "end" | "stretch" | "stretch" | Posición horizontal del hijo dentro de su celda. |
align | "start" | "center" | "end" | "stretch" | "stretch" | Posición vertical del hijo dentro de su celda. |
Track = Length (fija) · Percentage · "auto" (según contenido) · "<n>fr" (fracción del espacio libre, n > 0) · { "min": Length, "max": Length \| "<n>fr" }.
| Propiedad (hijo) | Tipo | Defecto | Descripción |
|---|---|---|---|
col, row | Integer ≥ 1 | auto | Posición (1-based) de la celda inicial. |
colSpan, rowSpan | Integer ≥ 1 | 1 | Pistas que ocupa. |
Reglas: sin col/row los hijos se colocan en orden en la primera celda libre según flow. Un hijo fuera de las pistas explícitas crea pistas implícitas "auto". Solapamientos de hijos con col/row explícitos ⇒ permitido (se apilan por zIndex/orden).
9.4 Dimensiones
| Propiedad | Tipo | Defecto |
|---|---|---|
width, height | Dynamic<Size> | "auto" |
minWidth, maxWidth, minHeight, maxHeight | Dynamic<Length> | sin límite |
aspectRatio | Number > 0 (ancho/alto) | — |
Resolución de `"auto"` (normativo):
| Contexto del nodo | "auto" equivale a |
|---|---|
Hijo en flujo flex | "fit" (en eje principal) · en el transversal: "fill" si align: stretch, si no "fit" |
Hijo de grid | "fill" de su celda (sujeto a justify/align) |
| Hijo absoluto | "fit" |
body de página | "fill" |
`"fill"`: en flex principal ⇒ grow: 1 con base 0; transversal ⇒ stretch; en grid ⇒ ocupa la celda; en absoluto ⇒ requiere insets (§9.5) o equivale a 100% del padre.
`"fit"`: tamaño del contenido + padding. Si el contenido incluye hijos fill en ese mismo eje ⇒ ciclo ⇒ esos hijos se tratan como 0 y se emite DIL-W011.
Orden de aplicación: calcular tamaño → aplicar aspectRatio (si solo un eje es conocido) → recortar a min/max (min gana sobre max).
9.5 Posicionamiento absoluto
Un nodo es absoluto si position: "absolute" o si su padre tiene layout: "none" (o es group).
| Propiedad | Tipo | Descripción |
|---|---|---|
position | "flow" | "absolute" | Defecto: "flow" salvo en padres none/group (siempre absoluto). |
x (alias left) | Dynamic<Number> | Distancia desde el borde izquierdo del padre. |
y (alias top) | Dynamic<Number> | Distancia desde el borde superior del padre. |
right | Dynamic<Number> | Distancia desde el borde derecho del padre. |
bottom | Dynamic<Number> | Distancia desde el borde inferior del padre. |
centerX | Dynamic<Number | true> | Centra horizontalmente; el número es el desplazamiento desde el centro. true ≡ 0. |
centerY | Dynamic<Number | true> | Ídem en vertical. |
zIndex | Integer | Orden de apilado entre hermanos; defecto = orden en children. |
- El origen es la esquina superior izquierda de la caja de borde (border-box) del padre; el padding no desplaza a los hijos absolutos.
- Reglas horizontales (las verticales son análogas con
top/bottom/height):
| Combinación declarada | Resultado |
|---|---|
x | Anclado a la izquierda, ancho = width. |
right | Anclado a la derecha. |
x + right | Estirado: ancho = anchoPadre − x − right (se ignora width; si se declara width ⇒ DIL-E013). |
centerX | Centrado (+ desplazamiento). |
x + centerX, o right + centerX | Inválido (DIL-E013). |
| ninguna | x = 0. |
Para editores: los constraints de Figma se traducen a esta tabla (Left →
x; Right →right; Left & Right →x+right; Center →centerX). Los escalados proporcionales ("Scale") se expresan conPercentageenx/width.
9.6 Padding, margin y gap
padding*(paddingTop/Right/Bottom/Left): espacio interior entre el borde del contenedor y sus hijos en flujo. Solo en contenedores (frame) y controles.margin*(marginTop/…): espacio exterior; solo afecta a hijos en flujo deflex/grid. No colapsa. Se ignora en absolutos.- Los tamaños
width/heightson del border-box (incluyen padding y borde); es decir,box-sizing: border-boximplícito.
┌───────────────── margin ─────────────────┐
│ ┌────────────── border-box ─────────────┐ │
│ │ border │ │
│ │ ┌───────────── padding ─────────────┐ │ │
│ │ │ ┌─────── content (hijos) ──────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └───────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└────────────────────────────────────────────┘9.7 Desbordamiento
| Propiedad | Tipo | Defecto | Descripción |
|---|---|---|---|
clip | Boolean | false | Recorta el contenido a la forma (incluye radius) del contenedor. |
scroll | "none" | "x" | "y" | "both" | "none" | Hace desplazable el contenido (implica clip: true). |
9.8 Transformaciones
Se aplican después del layout (no alteran el tamaño ni la posición de los hermanos).
| Propiedad | Tipo | Defecto |
|---|---|---|
rotate | Dynamic<Angle> | 0 |
scaleX, scaleY (atajo scale) | Dynamic<Number> | 1 |
translateX, translateY | Dynamic<Number> | 0 |
origin | Anchor | "center" |
Orden de composición: translate → rotate → scale alrededor de origin.
10. Estilo visual
10.1 fill (relleno)
fill es una lista de Paint, apilados de abajo hacia arriba (el último queda encima). Un solo paint puede escribirse sin array; un color/token como cadena equivale a solid.
Paint.type | Campos |
|---|---|
"solid" | color: Dynamic<Color>, opacity?: Fraction |
"linear" | angle: Angle (0 = hacia arriba, 90 = hacia la derecha; defecto 180), stops: Stop[], opacity? |
"radial" | center?: [Fraction, Fraction] (defecto [0.5,0.5]), radius?: Fraction (defecto 0.5), stops, opacity? |
"conic" | center?, from?: Angle, stops, opacity? |
"image" | src: AssetRef, fit: "cover" | "contain" | "fill" | "tile", opacity? |
Stop = { at: Fraction, color: Dynamic<Color> } — al menos 2 stops, at no decreciente.
{
"fill": [
{ "type": "linear", "angle": 135,
"stops": [ { "at": 0, "color": "#6366F1" }, { "at": 1, "color": "#8B5CF6" } ] }
]
}Un token de categoría gradient es un Paint válido; uno de color, también.
10.2 Bordes
| Propiedad | Tipo | Defecto |
|---|---|---|
borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth | Dynamic<Length> | 0 |
borderColor | Dynamic<Color> | "transparent" |
borderStyle | "solid" | "dashed" | "dotted" | "solid" |
borderPosition | "inside" | "center" | "outside" | "inside" |
Atajo border: Number (ancho) | "<ancho> [estilo] <color>" | objeto { width, style, color, position }. borderPosition: "inside" mantiene el tamaño del border-box (recomendado para diseño).
10.3 Radio
radiusTopLeft, radiusTopRight, radiusBottomRight, radiusBottomLeft: Dynamic<Length> (defecto 0). Valor muy grande (p. ej. 9999) produce una "píldora"; el runtime DEBE limitarlo a min(ancho,alto)/2.
10.4 Sombras y efectos
{ "shadow": [ { "x": 0, "y": 4, "blur": 16, "spread": 0, "color": "#0F172A1F", "inset": false } ] }Campo de Shadow | Tipo | Defecto |
|---|---|---|
x, y | Number | 0 |
blur | Length | 0 |
spread | Number | 0 |
color | Dynamic<Color> | "#00000040" |
inset | Boolean | false |
| Propiedad | Tipo | Descripción |
|---|---|---|
shadow | Shadow | Shadow[] | Varias sombras; la primera queda encima. |
blur | Length | Desenfoque del propio nodo (layer blur). |
backdropBlur | Length | Desenfoque de lo que hay detrás (vidrio esmerilado). |
blend | "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "difference" | Modo de mezcla. Defecto "normal". |
pointerEvents | "auto" | "none" | none ⇒ el nodo no recibe eventos de puntero. |
Un nodo con backdropBlur desenfoca únicamente lo que hay detrás de su caja; no desenfoca sus hijos. Para un efecto glass, combínalo con un fill translúcido y, opcionalmente, un borde claro:
{
"type": "frame", "width": 280, "height": 160,
"fill": { "type": "solid", "color": "hsl(0 0% 100% / 0.18)" },
"backdropBlur": 16,
"border": { "width": 1, "color": "#FFFFFF66" },
"radius": 16
}El valor de backdropBlur es un Length no negativo. Un valor 0 o ausente no aplica filtro. El renderizador debe conservar la transparencia del relleno para que el desenfoque de fondo sea visible.
11. Tipografía y texto
11.1 Nodo text
{ "type": "text", "text": "Hola, {{ name }}", "textStyle": "$text.title", "color": "$color.text" }| Propiedad | Tipo | Defecto | Descripción |
|---|---|---|---|
text | Dynamic<String> | "" | Contenido (texto plano; nunca se interpreta como marcado). |
runs | Run[] | — | Texto enriquecido; excluyente con text (DIL-E003). |
textStyle | TokenRef (text) o Id | — | Estilo tipográfico compuesto. Las propiedades explícitas lo sobrescriben. |
fontFamily | Dynamic<String> | "system-ui" | Familia (o lista separada por comas, con fallback). |
fontSize | Dynamic<Length> | 14 | |
fontWeight | Dynamic<100..900 | "normal" | "bold"> | 400 | normal=400, bold=700. |
fontStyle | "normal" | "italic" | "normal" | |
lineHeight | Dynamic<Length | Percentage | "auto"> | "auto" | "150%" = 1.5 × fontSize. |
letterSpacing | Dynamic<Length | Percentage> | 0 | Porcentaje relativo a fontSize. |
color | Dynamic<Color> | "#000000" | Color del texto. |
textAlign | "start" | "center" | "end" | "justify" | "start" | Horizontal (respeta dirección de lectura). |
verticalAlign | "top" | "middle" | "bottom" | "top" | Dentro de una altura fija. |
decoration | "none" | "underline" | "strike" | "none" | |
textTransform | "none" | "uppercase" | "lowercase" | "capitalize" | "none" | |
whiteSpace | "normal" | "nowrap" | "pre" | "normal" | |
maxLines | Integer ≥ 1 | — | Limita líneas. |
truncate | "clip" | "ellipsis" | "clip" | Aplicado al exceder maxLines o el ancho con nowrap. |
direction | — | — | No aplicable a `text` (ver §9.2). Dirección de lectura: dir. |
dir | "auto" | "ltr" | "rtl" | "auto" |
Run = { text: String, fontWeight?, fontStyle?, color?, decoration?, link?: ActionSpec } — solo propiedades de carácter.
11.2 TextStyle (token compuesto)
{ fontFamily?, fontSize?, fontWeight?, fontStyle?, lineHeight?, letterSpacing?, textTransform?, decoration? }. No incluye color.
Los controles tipográficos avanzados del editor DEBEN conservarse en estas propiedades canónicas; no se agregan claves específicas del editor:
lineHeightacepta una longitud o un porcentaje. Por ejemplo,"145%"equivale a1.45 × fontSizey"24px"fija una altura de línea.letterSpacingacepta una longitud o un porcentaje. El número0.5representa0.5 dp;"2%"es relativo al tamaño de fuente.textTransformconservanone,uppercase,lowercaseocapitalize.
Ejemplo de un token reutilizable con configuración avanzada:
{
"tokens": {
"text": {
"bodyCompact": {
"fontFamily": "$font.base",
"fontSize": 14,
"fontWeight": 400,
"lineHeight": "145%",
"letterSpacing": 0.2
}
}
},
"pages": [
{
"id": "home",
"body": {
"type": "frame",
"children": [
{ "id": "intro", "type": "text", "text": "Descripción", "textStyle": "$text.bodyCompact" }
]
}
}
]
}El editor PUEDE mostrar lineHeight y letterSpacing dentro de un grupo avanzado plegable. Al guardar un tipo de texto, DEBE incluir los valores actuales de ambas propiedades para que el tipo y el nodo mantengan la misma apariencia.
11.3 Tamaño del texto
Un text con width: "auto" mide su contenido (fit) sin saltos de línea hasta alcanzar el ancho máximo disponible; con ancho fijo o fill ajusta líneas. La altura es la de las líneas resultantes salvo height explícita. Las métricas exactas dependen de la fuente; los runtimes DEBERÍAN usar las métricas de la fuente declarada en assets cuando exista.
11.4 Herencia tipográfica
Estas propiedades, declaradas en un contenedor (frame, group) o control, son heredadas por los descendientes que no las definan:
textStyle, fontFamily, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, color, textAlign, decoration, textTransform, whiteSpace, dir.
La herencia es la capa más baja de la cascada (L0, §14). Las demás propiedades no se heredan.
12. Controles y formularios
Los controles aportan semántica y comportamiento (foco, teclado, accesibilidad, estados, validación). Su caja se estiliza con las mismas propiedades que un frame (fill, border*, radius*, padding*, tipografía…).
12.1 Catálogo
| Control | Propiedades específicas |
|---|---|
button | text, icon?: AssetRef|String, iconPosition?: "start"|"end" ("start"), variant?: String, buttonType?: "button"|"submit" ("button") |
input | inputType?: "text"|"email"|"password"|"number"|"search"|"tel"|"url" ("text"), placeholder?, maxLength?, readOnly?, autofocus? |
textarea | placeholder?, rows?: Integer (3), maxLength?, readOnly?, autofocus? |
checkbox | text? (etiqueta), indeterminate?: Dynamic<Boolean> |
radio | text?, value: Dynamic<any> (obligatorio), groupName: Name (obligatorio) |
switch | text? |
select | options: Option[] | Mustache, placeholder? |
slider | min (0), max (100), step (1) |
Todos admiten: name?: Name (clave en form), bind?, validate?, disabled, variant?, y eventos (§12.3). Option = { value: any, label: String, disabled?: Boolean }.
12.2 bind — binding bidireccional
{ "type": "input", "name": "email", "bind": "email", "placeholder": "tú@correo.com" }bindes un `Path` (§16.3) a una variable de estado declarada y asignable. Si no existe ⇒DIL-E012.- El control lee el valor de la variable y la escribe al cambiar el usuario.
- Tipo de valor:
input/textarea→string(inputconinputType: "number"→number);checkbox/switch→boolean;radio→ elvaluedel radio seleccionado;select→valuede la opción;slider→number. Desajuste con el tipo declarado ⇒DIL-E020. - Para estado solo lectura (sin escribir), usar expresiones en
text/valueen vez debind.
12.3 Eventos de controles
| Evento | Se dispara cuando |
|---|---|
input | Cada cambio de valor del usuario (pulsación). |
change | El valor se confirma (blur/Enter/soltar). |
focus / blur | Gana/pierde foco. |
submit | button con buttonType: "submit" pulsado, o Enter en input. |
click | Activación (clic, tap, Enter/Espacio en control enfocado). |
12.4 Validación (validate)
validate es un array ordenado de reglas; se evalúa de arriba abajo y el primer fallo define error. Todas las reglas fallidas se acumulan en errors.
{
"validate": [
{ "rule": "required", "message": "El correo es obligatorio" },
{ "rule": "email", "message": "Formato de correo inválido" },
{ "rule": "minLength", "value": 6, "message": "Mínimo 6 caracteres" },
{ "rule": "pattern", "value": "^[^\\s]+quot;, "message": "Sin espacios" },
{ "rule": "expr", "expr": "email != confirmEmail", "message": "No coincide" }
],
"validateOn": "blur"
}rule | Campos | Pasa si |
|---|---|---|
required | — | Valor no null, no "" (tras trim), no [], y para checkbox/switch es true. |
minLength / maxLength | value: Integer | length(valor) ≥ / ≤ value (si vacío y no required ⇒ pasa). |
min / max | value: Number | Valor numérico ≥ / ≤ value. |
email | — | Coincide con un formato de correo razonable (RFC 5322 simplificado). Vacío ⇒ pasa. |
pattern | value: String (regex, §17.5) | Coincide por completo. Vacío ⇒ pasa. |
oneOf | value: any[] | El valor está en la lista. |
expr | expr: Expr → Boolean | Se evalúa a true. Puede leer cualquier estado (incluidos otros campos). |
Todas llevan message: Dynamic<String> (opcional; hay mensajes por defecto no normativos). validateOn: "change" \| "blur" \| "submit" (defecto "blur"; tras el primer touched se revalida en change).
12.5 El espacio de nombres form
Cada página tiene un único ámbito de formulario form. Los campos se registran por su name.
| Expresión | Tipo | Descripción |
|---|---|---|
form.valid | boolean | true si todos los campos cumplen sus reglas (siempre actual). |
form.errors | Record<Name, string[]> | Mensajes de reglas fallidas por campo. |
form.submitted | boolean | Se ejecutó validate o submit al menos una vez. |
form.<name>.value | any | Valor actual. |
form.<name>.valid | boolean | Cumple todas sus reglas. |
form.<name>.errors | string[] | Mensajes fallidos (actuales). |
form.<name>.error | string | null | Primer mensaje si el campo fue tocado o form.submitted; si no, null. |
form.<name>.touched | boolean | Ha recibido y perdido foco. |
form.<name>.dirty | boolean | El valor difiere del inicial. |
Patrón típico de mensaje de error: { "type": "text", "text": "{{ form.email.error }}", "visible": "{{ form.email.error != null }}" }.
13. Componentes
Un componente es una plantilla reutilizable con props, variantes, estados y slots.
13.1 Definición
{
"components": {
"Button": {
"name": "Button",
"description": "Botón principal de la app",
"props": {
"label": { "type": "string", "default": "Button" },
"icon": { "type": "icon" }
},
"state": { },
"events": { "press": { "description": "El usuario activó el botón" } },
"variants": [
{ "axis": "kind", "default": "primary", "options": {
"primary": {
"$root": { "fill": "$color.brand.primary" },
"label": { "color": "#FFFFFF" }
},
"outline": {
"$root": { "fill": "transparent", "border": "1 solid $color.border" },
"label": { "color": "$color.text" }
}
} },
{ "axis": "size", "default": "md", "options": {
"sm": { "$root": { "padding": "6 12" }, "label": { "fontSize": 12 } },
"md": { "$root": { "padding": "10 16" }, "label": { "fontSize": 14 } },
"lg": { "$root": { "padding": "14 24" }, "label": { "fontSize": 16 } }
} }
],
"compoundVariants": [
{ "when": { "kind": "outline", "size": "sm" }, "apply": { "$root": { "borderPosition": "inside" } } }
],
"root": {
"type": "frame", "layout": "row", "justify": "center", "align": "center", "gap": 8,
"radius": "$radius.md",
"states": {
"hover": { "opacity": 0.9 },
"pressed": { "opacity": 0.8, "scaleX": 0.98, "scaleY": 0.98 },
"disabled": { "opacity": 0.4 }
},
"transition": { "duration": 150, "easing": "ease-out" },
"a11y": { "role": "button", "label": "{{ props.label }}" },
"on": { "click": { "emit": "press" } },
"children": [
{ "type": "text", "id": "label", "text": "{{ props.label }}" }
]
}
}
}
}Campo de Component | Tipo | Obligatorio | Descripción |
|---|---|---|---|
root | Node | Sí | Nodo raíz. |
name, description | String | No | Documentación. |
props | Record<Name, PropDecl> | No | Interfaz pública (§13.2). |
state | Record<Name, StateDecl> | No | Estado interno de cada instancia (§16.2). |
computed | Record<Name, Expr> | No | Derivados internos. |
events | Record<Id, { description?, payload? }> | No | Eventos personalizados que puede emitir (§18.4 emit). |
variants | VariantAxis[] | No | Ejes de variante (§13.3). |
compoundVariants | CompoundVariant[] | No | Combinaciones (§13.3). |
13.2 Props (PropDecl)
| Campo | Tipo | Descripción |
|---|---|---|
type | ver tabla | Obligatorio. |
default | valor | Valor si la instancia no lo da. |
required | Boolean | Si true, la instancia DEBE darlo (DIL-E017). Incompatible con default. |
options | any[] | Solo type: "enum". |
description | String |
type | Valores |
|---|---|
string number boolean color | primitivos / Color |
enum | uno de options |
icon | AssetRef o "provider:name" |
image | AssetRef |
array object any | JSON; any desaconsejado |
action | ActionSpec (callback declarativo que el componente puede invocar con { "run": "<prop>" }, §18.4) |
Dentro del componente, las props se leen como `props.<name>` (solo lectura). El valor de una prop pasada por la instancia es Dynamic (se resuelve en el contexto de la instancia).
13.3 Variantes
Una variante es una familia de parches mutuamente excluyentes sobre nodos del componente.
variantses un array ordenado de ejes:{ axis: Name, default: String, options: Record<String, OverrideMap> }.- La instancia elige una opción por eje (
variants: { "kind": "outline" }); si no, se usadefault. - Un eje cuya opción no existe ⇒
DIL-E016. - Un
OverrideMapmapea id de nodo (o"$root") a un `StylePatch` (§8.5; incluyestates/responsive, pero nochildren). - Orden de aplicación: ejes en el orden del array (el último eje gana en caso de conflicto), y luego
compoundVariantsen orden. compoundVariants[].whenes un objeto{ axis: option }; aplica cuando todas las condiciones coinciden.
13.4 Estados
states (en cualquier nodo, §8.3) define parches según el estado de interacción/valor:
{ "states": { "hover": { "fill": "$color.hover" }, "disabled": { "opacity": 0.4 } } }| Estado | Se activa cuando | Fuente |
|---|---|---|
hover | El puntero está sobre el nodo | Runtime |
focus | El nodo (o un descendiente) tiene el foco | Runtime |
focusVisible | Foco por teclado | Runtime |
pressed | Puntero/tecla presionados sobre el nodo | Runtime |
selected | selected es true | Propiedad |
checked | Control checkbox/radio/switch marcado | Control |
invalid | El campo falla validate y form.<name>.error != null | Control |
loading | loading es true | Propiedad |
disabled | disabled es true (propio o heredado) | Propiedad |
- Varios estados PUEDEN estar activos a la vez. Si hay conflicto, gana el último en este orden fijo (de menor a mayor prioridad):
hover < focus < focusVisible < selected < checked < pressed < invalid < loading < disabledStatePatch = StylePatch & { within?: OverrideMap }.withinaplica parches a descendientes (claves = ids relativos al nodo) cuando el estado está activo; asíhoverdel root puede cambiar el color de un hijo.- Un nodo
disabledno dispara eventos de usuario ni recibehover/pressed/focus. - Los estados son por nodo; un estado en un hijo no se activa por hover del padre (usar
withinen el padre).
13.5 Instancias
{
"type": "instance",
"id": "saveBtn",
"component": "Button",
"props": { "label": "Guardar" },
"variants": { "kind": "{{ danger ? 'outline' : 'primary' }}", "size": "lg" },
"overrides": { "label": { "fontWeight": 700 }, "$root": { "states": { "hover": { "opacity": 1 } } } },
"slots": { },
"width": "fill",
"on": { "press": "save" }
}| Campo | Tipo | Descripción |
|---|---|---|
component | Id | Obligatorio. |
props | Record<Name, Dynamic<any>> | Valores de props. Desconocidas ⇒ DIL-E017. |
variants | Record<Name, Dynamic<String>> | Selección de variantes. |
overrides | OverrideMap | Parches sobre nodos internos (§5.2 para rutas). |
slots | Record<Id, Node | Node[]> | Contenido de slots (§13.6). |
Propiedades StylePatch | Aplicadas al root del componente (equivalente a overrides.$root). Típico: x, y, width, grow, col, visible, disabled. | |
on | Eventos del componente (press…) y genéricos (click, …) sobre la caja del root. |
- Recursión directa o indirecta de componentes (A contiene instancia de A) ⇒
DIL-E009. - Un override con clave que no existe en el componente ⇒
DIL-E014. - Los overrides NO PUEDEN cambiar
id,type, ni la estructura (children); para variar contenido, usarslots.
13.6 Slots
{
"components": {
"Card": {
"root": { "type": "frame", "layout": "column", "gap": 12, "padding": 16, "children": [
{ "type": "slot", "name": "header", "children": [ { "type": "text", "text": "Sin título" } ] },
{ "type": "slot", "name": "default" }
] }
}
}
}Uso:
{ "type": "instance", "component": "Card", "slots": {
"header": { "type": "text", "text": "Resumen", "textStyle": "$text.title" },
"default": [ { "type": "text", "text": "Contenido…" } ]
} }slot.nameesId;"default"es el slot por defecto.instance.children≡slots.default.- Los
childrende unslotson el contenido de reemplazo (fallback) si la instancia no aporta ese slot. - Un
slotes transparente al layout (comorepeat): su contenido participa en el layout del padre del slot. - Los nodos proporcionados a un slot se evalúan en el contexto de la instancia (su estado/props), no del componente.
- Slot inexistente en
slotsde la instancia ⇒DIL-E015. - Un nombre de slot repetido en un mismo componente ⇒
DIL-E004.
14. Cascada y precedencia
Para cada propiedad canónica (longhand) de cada nodo, el runtime elige el valor crudo ganador aplicando capas de menor a mayor prioridad. La última capa que define la propiedad gana.
prioridad ↑
L6 Runtime volátil animaciones y transiciones en curso (§20)
L5 Override de instancia instance.overrides[...] · propiedades de la instancia
L4 Estado node.states (orden de §13.4) · within
L3 Variante variants[eje] en orden · compoundVariants
L2 Responsive node.responsive (breakpoints ascendentes)
L1 Base del nodo styles (en orden) → propiedades propias del nodo
L0 Herencia propiedades heredables del padre (§11.4)Equivalencia con el modelo conceptual: component → variant → state → instance → override, con responsive y herencia como capas explícitas.
14.1 Reglas
- Granularidad: la unión se hace por propiedad canónica.
paddingTopen una capa alta no alterapaddingLeftdefinido en una baja. Las propiedades de lista (fill,shadow,validate,strokeDash,stops) se reemplazan completas, no se mezclan. - `states` y `responsive` definidos en
stylesy en el nodo se mezclan por clave y luego por propiedad (el nodo gana). - El parche de estado (L4) vence a la variante (L3) y al responsive (L2). Un override de instancia (L5) vence a todos ellos, incluso a los estados del componente: para cambiar solo un estado de una instancia, usar
overrides.<id>.states.<estado>(que se evalúa en L5 con el orden de §13.4). - Los valores de capas superiores pueden ser
Dynamic; la cascada elige primero el valor crudo y después se resuelve (§6). - Un valor
nullexplícito en una capa elimina el valor de las capas inferiores y vuelve al defecto de la propiedad. L0(herencia) solo aplica a propiedades heredables y solo si ninguna otra capa define la propiedad en el nodo.
14.2 Ejemplo resuelto
Componente Button: root.fill = $color.primary (L1)
Variante kind=outline: root.fill = transparent (L3)
Estado hover: root.fill = $color.hover (L4)
Instancia overrides: $root.fill = #FF0000 (L5)
Estado normal → #FF0000 (L5 vence a L3 y L1)
Estado hover → #FF0000 (L5 vence a L4) ← usar overrides.$root.states.hover para cambiarlo15. Responsive
15.1 Breakpoints
{ "breakpoints": { "compact": 0, "medium": 600, "expanded": 1024 } }- Mapa
Id → ancho mínimoen dp (enteros ≥ 0, valores únicos). Defecto:compact:0, medium:600, expanded:1024. - Enfoque mobile-first: el breakpoint activo es el de mayor mínimo
≤ viewport.width. - Variable integrada
viewport:{ width, height, breakpoint, orientation }(orientation:"portrait"|"landscape").
15.2 Uso
{
"type": "frame",
"layout": "grid",
"columns": 1,
"gap": 16,
"responsive": {
"medium": { "columns": 2 },
"expanded": { "columns": 3, "gap": 24 }
}
}Los parches de responsive aplican acumulativamente en orden ascendente de mínimo hasta el breakpoint activo (todos los de mínimo ≤ activo). Claves desconocidas ⇒ DIL-E003.
También se puede ramificar en expresiones: "visible": "{{ viewport.breakpoint != 'compact' }}".
16. Estado, datos y bindings
16.1 Declaración de estado
{
"state": {
"count": 0,
"email": { "type": "string", "default": "" },
"mode": { "type": "string", "default": "list", "enum": ["list", "grid"] },
"todos": { "type": "array", "default": "{{ data.todos }}" }
},
"computed": {
"remaining": "count(filter(todos, 'done', false))"
}
}Campo de StateDecl | Tipo | Descripción |
|---|---|---|
type | "string" | "number" | "boolean" | "object" | "array" | Obligatorio (inferido en la forma corta literal). |
default | Dynamic<any> | Valor inicial; se evalúa una vez al crear el ámbito. |
enum | any[] | Valores permitidos (opcional). |
description | String |
- Forma corta:
"count": 0⇒type: number,default: 0. Los literalesnull,{}y[]requieren forma larga (para fijar el tipo). - El tipo se verifica en cada
set(DIL-E020). computed: expresiones puras sin ciclos; solo lectura. Ciclos ⇒DIL-E008.
16.2 Ámbitos
| Ámbito | Declarado en | Vida | Visible en |
|---|---|---|---|
| Documento | state / computed raíz | Sesión del prototipo | Todo |
| Página | page.state / page.computed | Mientras la página esté en la pila de navegación | Su subárbol y sus overlays abiertos desde ella |
| Componente | component.state | Cada instancia tiene su copia | Dentro del componente |
| Local de repeat | repeat.as, repeat.index | Una instancia de la plantilla | Dentro de template |
| Local de evento | event | Durante el manejo de un evento | Acciones y expresiones de ese manejador |
Resolución de nombres (de más interno a más externo; el interno oculta al externo):
locales de repeat → props del componente (props.x) y estado del componente
→ estado/params de página → estado de documento → integradasVariables integradas (reservadas, no declarables): props, params, form, data, event, viewport, theme, index solo si es repeat.index por defecto. Declarar una variable con nombre reservado ⇒ DIL-E003.
16.3 Path (asignación)
Un Path es el destino de bind, set, toggle, append, remove:
Path := Name ( "." Name | "[" Integer "]" )*Ej.: "email", "user.name", "items[2].done". La raíz debe ser una variable de estado asignable (no props, no locales de repeat, no data, no computed). Si no ⇒ DIL-E012.
16.4 Bindings
- Lectura: cualquier
Dynamiccon{{ … }}es un binding reactivo. - Escritura: controles con
bindy accionesset/toggle/append/remove. - El grafo de dependencias DEBE ser acíclico; un ciclo entre
computed/default⇒DIL-E008.
16.5 Datos mock
{
"data": {
"projects": [
{ "id": 1, "name": "Alfa", "progress": 0.72, "active": true },
{ "id": 2, "name": "Beta", "progress": 0.35, "active": false }
],
"currentUser": { "name": "Ana", "email": "ana@ejemplo.com" }
}
}dataes inmutable y de solo lectura; accesible comodata.<name>.- Para modificarlos, copiarlos a estado:
"todos": { "type": "array", "default": "{{ data.todos }}" }. - Los valores son JSON puro (sin expresiones).
17. Lenguaje de expresiones
Las expresiones son puras (sin efectos), totales (siempre terminan) y tipadas dinámicamente con verificación estática cuando es posible.
17.1 Gramática (EBNF)
expr = ternary ;
ternary = nullish , [ "?" , expr , ":" , expr ] ;
nullish = or , { "??" , or } ;
or = and , { "||" , and } ;
and = equality , { "&&" , equality } ;
equality = relational , { ( "==" | "!=" ) , relational } ;
relational = additive , { ( "<" | "<=" | ">" | ">=" ) , additive } ;
additive = multiplicative , { ( "+" | "-" ) , multiplicative } ;
multiplicative = unary , { ( "*" | "/" | "%" ) , unary } ;
unary = ( "!" | "-" ) , unary | postfix ;
postfix = primary , { "." , IDENT | "?." , IDENT | "[" , expr , "]" } ;
primary = NUMBER | STRING | "true" | "false" | "null"
| call | IDENT | TOKEN
| "(" , expr , ")"
| "[" , [ expr , { "," , expr } ] , "]"
| "{" , [ pair , { "," , pair } ] , "}" ;
call = IDENT , "(" , [ expr , { "," , expr } ] , ")" ;
pair = ( IDENT | STRING ) , ":" , expr ;
IDENT = [A-Za-z_][A-Za-z0-9_]* ;
TOKEN = "quot; , IDENT , { "." , IDENT } ; (* referencia a token dentro de expresiones *)
NUMBER = digits , [ "." , digits ] , [ ("e"|"E") , ["+"|"-"] , digits ] ;
STRING = "'" , { char } , "'" | '"' , { char } , '"' ; (* escapes: \\ \' \" \n \t \uXXXX *)No existen: asignación, ++, funciones anónimas, bucles, new, llamadas a métodos, acceso a this/globales, acceso a prototipos, ni cadenas de plantillas.
17.2 Precedencia y asociatividad
| Nivel | Operadores | Asociatividad |
|:--:|---|---|
| 1 (mayor) | . ?. [ ] ( ) (llamada) | izquierda |
| 2 | ! - (unarios) | derecha |
| 3 | * / % | izquierda |
| 4 | + - | izquierda |
| 5 | < <= > >= | izquierda |
| 6 | == != | izquierda |
| 7 | && | izquierda |
| 8 | \|\| | izquierda |
| 9 | ?? | izquierda |
| 10 (menor) | ? : | derecha |
17.3 Tipos y semántica
Tipos: number, string, boolean, null, array, object.
| Operación | Regla |
|---|---|
== / != | Igualdad estructural estricta, sin coerción: 1 == "1" es false; arrays/objetos se comparan profundamente. |
< <= > >= | Solo number con number o string con string (orden lexicográfico por code point). Otro caso ⇒ error de evaluación. |
+ | number+number o string+string. Mezclas ⇒ error (usar toString). |
- * / % | Solo number. x/0 ⇒ error de evaluación (no Infinity). |
! && || | Operandos boolean; cortocircuito. Otro tipo ⇒ error. Usar isEmpty, == para convertir. |
?? | a ?? b → b si a es null (o acceso no resuelto); si no, a. |
. [ ] | Acceso a miembro/índice. Si el objeto es null ⇒ error, salvo con ?. (devuelve null). Clave/índice inexistente ⇒ null. |
? : | La condición DEBE ser boolean. Solo se evalúa la rama elegida. |
TOKEN | Valor del token en el tema activo (tipado por su categoría). |
Error de evaluación: el runtime NO se detiene; la expresión produce null (y la propiedad cae a su valor por defecto), y se emite un diagnóstico de runtime (DIL-R001, §25.4).
17.4 Biblioteca estándar
Todas son puras y deterministas.
| Grupo | Funciones |
|---|---|
| Números | abs(n), min(a,b,…), max(a,b,…), round(n, d=0), floor(n), ceil(n), clamp(n, lo, hi), mod(a,b) |
| Texto | length(s), upper(s), lower(s), trim(s), contains(s,sub), startsWith(s,p), endsWith(s,p), replace(s,from,to), split(s,sep), substring(s,start,end?), padStart(s,n,ch), matches(s,regex) |
| Listas | count(a), first(a), last(a), at(a,i), includes(a,v), join(a,sep), reverse(a), slice(a,s,e?), sum(a,key?), avg(a,key?), minOf(a,key?), maxOf(a,key?), pluck(a,key), filter(a,key,value), find(a,key,value), sortBy(a,key,order?), unique(a), range(n) |
| Objetos | keys(o), values(o), has(o,key) |
| Tipos | isNull(v), isEmpty(v) (null, "", [], {}), toString(v), toNumber(v) (inválido ⇒ null), toBoolean(v) (solo "true"/"false"/booleano) |
| Formato | formatNumber(n, decimals=0), formatCurrency(n, code), formatPercent(f, decimals=0), formatDate(ts, pattern), plural(n, one, other) |
| Color | alpha(color, a), mix(c1, c2, t), lighten(c, t), darken(c, t) |
| Entorno | now() (ms epoch; no determinista, §24.5) |
filter(a, key, value)filtra elementos cuyokey(nombre de propiedad, cadena) sea igual avalue. Para predicados complejos usarrepeat.filter.formatDateusa patrónyyyy-MM-dd HH:mm:ssy similares; la zona horaria es la del runtime.- Llamar a una función inexistente ⇒
DIL-E012. Número/tipo de argumentos incorrecto ⇒DIL-E011.
17.5 Expresiones regulares
Para pattern y matches: sintaxis RE2 / ECMAScript sin características peligrosas. Prohibido: backreferences, lookbehind, cuantificadores anidados catastróficos. Longitud máxima 256. Inválida ⇒ DIL-E023.
17.6 Campos de expresión (Expr, sin llaves) vs Dynamic (con llaves)
| Campo | Tipo | Forma |
|---|---|---|
if (acción) | Expr → boolean | bare: "if": "email != ''" |
repeat.each, .key, .filter, .limit, .sort.by | Expr | bare |
computed.* | Expr | bare |
validate[].expr | Expr → boolean | bare |
remove.where, append.if | Expr | bare |
Cualquier propiedad Dynamic<T> (text, visible, fill, width, value de set, props…) | Dynamic<T> | con llaves: "{{ expr }}" |
Regla mnemotécnica: si el campo es una pregunta o fuente de datos, va sin llaves; si es un valor que se pinta o asigna, va con llaves.
17.7 Límites de evaluación
Profundidad de sintaxis ≤ 32; ≤ 10 000 pasos por evaluación; cadenas ≤ 1 MB; listas ≤ 100 000. Exceder ⇒ DIL-R002 (el resultado es null).
18. Eventos y acciones
18.1 Eventos
on mapea nombres de evento → ActionSpec.
| Evento | Aplica a | Descripción | Datos en event |
|---|---|---|---|
click | Todos | Clic/tap/Enter/Espacio | x, y |
doubleClick | Todos | Doble clic | x, y |
longPress | Todos | Pulsación ≥ 500 ms | x, y |
pointerEnter / pointerLeave | Todos | Puntero entra/sale | x, y |
pointerDown / pointerUp | Todos | x, y | |
focus / blur | Interactivos | — | |
input / change | Controles | §12.3 | value |
submit | button, input | value | |
keyDown / keyUp | Interactivos y página | key, shift, ctrl, alt, meta | |
scroll | frame con scroll | scrollX, scrollY | |
visible | Todos | Entra al viewport (una vez por montaje) | — |
load / unload | Nodos y páginas | Monta/desmonta | — |
resize | Página | Cambia el viewport | width, height |
| Eventos de componente | instance | Los declarados en component.events | payload |
- Un evento desconocido ⇒
DIL-E019. - Variable
eventaccesible en expresiones del manejador (campos arriba). - Nodos
disabledno emiten eventos de puntero/teclado/foco. - Burbujeo: los eventos de puntero y teclado burbujean al ancestro más cercano con manejador, salvo que una acción
stop(reservada) lo impida — en 1.0 el primer manejador encontrado consume el evento.
18.2 ActionSpec
ActionSpec := String -- referencia a actions[<id>] (≡ { "call": id })
| Action
| ( String | Action )[] -- secuencia, ejecución en orden18.3 Forma de Action
Una Action es un objeto con exactamente un verbo (clave de §18.4) y, opcionalmente, id. El verbo if admite además then y else.
{ "set": { "count": "{{ count + 1 }}" } }Dos verbos en un mismo objeto ⇒ DIL-E018. Las acciones con nombre (actions) pueden ser una Action o una secuencia.
18.4 Catálogo de acciones
| Verbo | Valor | Efecto |
|---|---|---|
set | { Path: Dynamic<any> } | Asigna. Todos los valores se evalúan antes contra el estado previo, y luego se aplican (en orden alfabético de ruta). |
toggle | Path | Invierte un boolean. |
append | { to: Path, value: Dynamic<any>, index?: Integer } | Inserta (al final por defecto). |
remove | { from: Path, index?: Dynamic<Integer>, where?: Expr, as?: Name } | Elimina por índice o por predicado (as por defecto "item"). Exactamente uno de index/where. |
if | Expr + then + else? (ActionSpec) | Condicional. |
parallel | Action[] | Ejecuta a la vez (para animate/wait coordinados); finaliza al terminar todas. |
wait | Duration (o Dynamic<Duration>) | Pausa la secuencia. |
call | Id | Ejecuta actions[id]. Recursión directa/indirecta ⇒ DIL-E009. |
navigate | Id | { to: Id, params?, transition?, replace?: Boolean } | Cambia de página (§19.3). |
back | true | { transition? } | Vuelve en la pila. |
open | Id | { overlay: Id, anchor?: Id, params? } | Abre una página overlay (§19.4). |
close | true | Id | Cierra el overlay superior o el indicado. |
animate | Id | { ref: Id, target?: Id, await?: Boolean } | Reproduce animations[ref]. target por defecto: el nodo que originó el evento. |
setTheme | Dynamic<String> | Cambia el tema activo. |
focus | Id (nodo) | Mueve el foco. |
scrollTo | Id | { target: Id, behavior?: "auto"|"smooth", align?: "start"|"center"|"end" } | Desplaza hasta el nodo. |
validate | "form" | Name[] | Marca campos como touched, activa form.submitted y evalúa reglas. |
reset | "form" | Path[] | Restaura valores por defecto y limpia touched/dirty. |
emit | Id | { event: Id, payload?: Dynamic<any> } | Dentro de un componente: dispara un evento declarado en component.events, manejado por instance.on. |
run | Name | Dentro de un componente: ejecuta la prop de tipo action. |
host | { name: Id, payload?: Dynamic<any> } | Notifica a la aplicación anfitriona (punto de integración). No ejecuta código del documento. |
Verbos reservados para versiones futuras (usar ⇒ DIL-E018): fetch, auth, store, copy, download, share, log.
18.5 Semántica de ejecución
- Una secuencia se ejecuta en orden.
wait,animateconawait: trueynavigate/opencon transición suspenden la secuencia hasta terminar. - Los cambios de estado de un
setse aplican de forma atómica respecto a las expresiones reactivas (una sola actualización por "tick", §24.3). - Un error de acción (destino inexistente, tipo inválido) detiene la secuencia actual, emite
DIL-R003y deja intacto el estado ya aplicado. - Cada evento inicia su propia secuencia; dos secuencias pueden solaparse (se serializan sobre el estado por orden de llegada).
- Límite: ≤ 1 000 acciones por secuencia y profundidad de
call≤ 16 (DIL-R002). ifevalúaExpr; condición no booleana ⇒ error de evaluación ⇒ se tomaelse.
18.6 Ejemplos
{ "on": { "click": { "set": { "count": "{{ count + 1 }}" } } } }{
"on": { "click": [
{ "validate": "form" },
{ "if": "form.valid",
"then": [ { "set": { "saving": true } }, { "wait": 800 }, { "navigate": "dashboard" } ],
"else": { "animate": "shake" } }
] }
}{ "actions": {
"addTodo": [
{ "if": "newTodo != ''", "then": [
{ "append": { "to": "todos", "value": { "id": "{{ count(todos) + 1 }}", "text": "{{ newTodo }}", "done": false } } },
{ "set": { "newTodo": "" } }
] }
]
} }19. Páginas, navegación, overlays y flujos
19.1 Page
{
"id": "login",
"name": "Login",
"kind": "screen",
"frame": { "width": 390, "height": 844, "device": "iphone-15", "background": "$color.bg" },
"params": { "from": { "type": "string", "default": "" } },
"state": { },
"computed": { },
"on": { "load": { "set": { "email": "" } } },
"body": { "type": "frame", "width": "fill", "height": "fill", "children": [ ] }
}| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | Id | Sí | |
body | Node | Sí | Debe ser frame. |
name | String | No | |
kind | "screen" | "overlay" | No ("screen") | |
frame | PageFrame | No | Tamaño de la superficie de diseño y background (Paint). device es una pista informativa (no normativa). |
params | Record<Name, PropDecl> | No | Parámetros de entrada (params.x). |
state, computed | No | Ámbito de página (§16.2). | |
on | No | Eventos de página: load, unload, keyDown, resize. | |
overlay | OverlayConfig | Solo si kind: "overlay" | §19.4. |
El viewport de la página es frame.width × frame.height en diseño; en un runtime real, el tamaño disponible del anfitrión.
19.2 Pila de navegación
El runtime mantiene una pila de páginas (screen) y una pila de overlays.
start → [home] ──navigate→ [home, detail] ──navigate(replace)→ [home, detail2] ──back→ [home]19.3 navigate y transiciones de página
{ "navigate": {
"to": "detail",
"params": { "id": "{{ project.id }}" },
"replace": false,
"transition": { "type": "push", "direction": "left", "duration": 250, "easing": "ease-out" }
} }transition.type | direction | Efecto |
|---|---|---|
"none" | — | Cambio instantáneo (defecto). |
"fade" | — | Fundido cruzado. |
"slide" | left | right | up | down | La nueva entra deslizando sobre la actual. |
"push" | left | right | up | down | La nueva empuja a la actual. |
"scale" | — | La nueva escala desde el centro. |
"smart" | — | Anima nodos con el mismo `id` entre origen y destino (posición, tamaño, color); el resto hace fundido. |
replace: truesustituye la página actual en la pila.backrevierte la transición por defecto de forma inversa.paramsDEBE coincidir conpage.params(DIL-E017).
19.4 Overlays
Una página con kind: "overlay" se presenta sobre la pantalla actual.
{
"id": "confirmDelete",
"kind": "overlay",
"overlay": {
"presentation": "modal",
"backdrop": "#0F172A99",
"dismissOnBackdrop": true,
"dismissOnEscape": true
},
"body": { "type": "frame", "layout": "column", "gap": 12, "padding": 24,
"fill": "$color.surface", "radius": 16, "children": [ ] }
}OverlayConfig | Tipo | Defecto |
|---|---|---|
presentation | "modal" | "popover" | "sheet" | "drawer" | "toast" | "modal" |
backdrop | Paint | false | false (popover/toast), "#00000066" (resto) |
dismissOnBackdrop, dismissOnEscape | Boolean | true |
placement | Anchor | modal: center · sheet: bottom · drawer: left · toast: top · popover: bottom |
duration | Duration | Solo toast: cierre automático (defecto 3000) |
open: { overlay, anchor }—anchor(id de nodo) solo aplica apopover: se posiciona junto a él.- Un
modalbloquea la interacción con lo que está detrás y atrapa el foco. - Un overlay ve el estado de la página desde la que se abrió (§16.2) y
paramspropios.
19.5 start
start (raíz del documento) es la página inicial; DEBE ser kind: "screen" (DIL-E005).
19.6 Flujos de prototipo
Un flujo es un punto de entrada con nombre para presentar o probar un recorrido.
{
"flows": [
{ "id": "onboarding", "name": "Onboarding", "start": "welcome",
"description": "Primer uso hasta el dashboard",
"state": { "email": "demo@ejemplo.com" },
"frame": { "device": "iphone-15" } }
]
}| Campo | Tipo | Descripción |
|---|---|---|
id, name | Id, String | |
start | Id | Página inicial del flujo. |
description | String | |
state | Record<Name, any> | Valores iniciales de estado de documento que se aplican al iniciar el flujo. |
frame | Partial<PageFrame> | Pista de presentación. |
El validador emite DIL-W005 si una página screen no es alcanzable desde ningún start/flujo/navigate.
20. Transiciones y animaciones
20.1 Easing
| Forma | Valores |
|---|---|
| Nombre | "linear", "ease", "ease-in", "ease-out", "ease-in-out" |
| Bézier | [x1, y1, x2, y2] con x1,x2 ∈ [0,1] |
| Resorte | { "type": "spring", "stiffness": 170, "damping": 26, "mass": 1 } (duración derivada; duration se ignora) |
Defecto: "ease-out". Se admite TokenRef de categoría easing.
20.2 transition (implícita)
Interpola un nodo cuando el valor resuelto de una propiedad cambia (por estado, variante, binding, responsive o tema).
{ "transition": { "properties": ["opacity", "fill", "scaleX", "scaleY"], "duration": 150, "delay": 0, "easing": "ease-out" } }| Campo | Tipo | Defecto |
|---|---|---|
properties | String[] | "all" | "all" |
duration | Dynamic<Duration> | 0 (sin transición) |
delay | Dynamic<Duration> | 0 |
easing | Dynamic<Easing> | "ease-out" |
Propiedades animables: opacity, fill (entre solid; o entre gradientes de igual tipo y nº de stops), color, borderColor, border*Width, radius*, width/height/x/y/right/bottom (solo si ambos extremos son numéricos), padding*, margin*, gap, rotate, scaleX/Y, translateX/Y, shadow (mismo nº de sombras), blur, backdropBlur. No animables (cambio discreto): visible, text, layout, direction, type, children, enumeraciones.
20.3 Animation (con nombre)
{
"animations": {
"shake": {
"duration": 300,
"easing": "ease-in-out",
"iterations": 1,
"direction": "normal",
"fill": "none",
"keyframes": [
{ "at": 0, "set": { "translateX": 0 } },
{ "at": 0.25, "set": { "translateX": -8 } },
{ "at": 0.5, "set": { "translateX": 8 } },
{ "at": 0.75, "set": { "translateX": -4 } },
{ "at": 1, "set": { "translateX": 0 } }
]
},
"pulse": {
"duration": 1200, "iterations": "infinite", "direction": "alternate",
"keyframes": [ { "at": 0, "set": { "opacity": 1 } }, { "at": 1, "set": { "opacity": 0.5 } } ]
}
}
}| Campo | Tipo | Defecto |
|---|---|---|
duration | Duration | obligatorio |
delay | Duration | 0 |
easing | Easing | "ease-in-out" (entre keyframes) |
iterations | Number > 0 | "infinite" | 1 |
direction | "normal" | "reverse" | "alternate" | "normal" |
fill | "none" | "forwards" | "none" (forwards conserva el último keyframe) |
keyframes | { at: Fraction, set: AnimatableProps, easing?: Easing }[] | obligatorio, ≥ 2, at estrictamente creciente, empezando en 0 y terminando en 1 |
- Las propiedades en
setDEBEN ser animables (§20.2) (DIL-E003). - La animación opera en la capa L6 de la cascada; al terminar (sin
fill: forwards) el nodo vuelve a su valor L1–L5. AnimationRefennode.animation:Ido{ ref: Id, trigger: "load" | "visible" | "hover", loop?: Boolean }— reproduce la animación automáticamente.animatecomo acción (§18.4) la dispara bajo demanda.- Si el usuario tiene "reducir movimiento" activo, el runtime DEBE acortar a ≤ 1 ms las animaciones y transiciones no esenciales (§21.4).
21. Accesibilidad
21.1 a11y
| Campo | Tipo | Descripción |
|---|---|---|
role | Role | Rol semántico (tabla). Los controles tienen un rol implícito. |
label | Dynamic<String> | Nombre accesible. |
labelledBy | Id | Nodo cuyo texto es el nombre. |
description | Dynamic<String> | Descripción adicional. |
hidden | Dynamic<Boolean> | Oculto a tecnologías de asistencia (decorativo). |
live | "off" | "polite" | "assertive" | Región viva. |
focusable | Boolean | Fuerza que un nodo no interactivo reciba foco. |
order | Integer | Orden de foco; por defecto, orden del árbol (DFS). |
level | 1..6 | Nivel de heading. |
expanded | Dynamic<Boolean> | Estado expandido/colapsado. |
keyShortcut | String | Atajo (informativo). |
Roles (Role): button link textbox checkbox radio switch slider combobox heading image list listitem navigation main region form dialog alert status tab tablist tabpanel menu menuitem separator none.
Roles implícitos: button→button, input/textarea→textbox, checkbox→checkbox, radio→radio, switch→switch, select→combobox, slider→slider, image→image, text→none, frame→none, overlay modal→dialog.
21.2 Reglas de validación (lint)
| Regla | Código |
|---|---|
image sin alt/a11y.label y sin a11y.hidden: true | DIL-W001 |
Control o nodo interactivo (on.click…) sin nombre accesible (texto propio, a11y.label, labelledBy) | DIL-W002 |
| Contraste de texto < 4.5:1 (texto < 18 dp, o < 14 dp en negrita) o < 3:1 (texto grande) frente al fondo resuelto más cercano | DIL-W003 |
| Objetivo táctil interactivo < 44 × 44 dp | DIL-W004 |
Saltos de nivel de heading (h1→h3) | DIL-W012 |
focusable/order con valores duplicados en una misma página | DIL-W013 |
21.3 Teclado y foco
- Todo nodo interactivo DEBE ser alcanzable por teclado en el orden de §21.1 (
ordero DFS). clickse activa también con Enter/Espacio sobre un nodo enfocado.- Un
modalatrapa el foco;Esccierra sidismissOnEscape. focusVisiblesolo se activa con navegación por teclado.
21.4 Preferencias del usuario
El runtime DEBE respetar prefers-reduced-motion (§20.3) y DEBERÍA exponer viewport.colorScheme ("light"|"dark") y viewport.fontScale (Number) para que un documento adapte theme/tamaños con expresiones.
22. Assets
{
"assets": {
"logo": { "kind": "svg", "uri": "assets/logo.svg", "width": 120, "height": 32, "alt": "Acme" },
"hero": { "kind": "image", "uri": "https://cdn.ejemplo.com/hero.jpg", "mime": "image/jpeg", "width": 1600, "height": 900, "hash": "sha256-…" },
"avatar": { "kind": "image", "uri": "data:image/png;base64,iVBOR…", "alt": "Avatar" },
"inter-regular": { "kind": "font", "family": "Inter", "weight": 400, "style": "normal", "uri": "fonts/Inter-Regular.woff2" }
}
}| Campo | Tipo | Aplica a | Descripción |
|---|---|---|---|
kind | "image" | "svg" | "font" | Todos | Obligatorio. |
uri | String | Todos | Obligatorio. Relativo, https: o data:. |
mime | String | Todos | Tipo MIME (informativo; DEBERÍA coincidir). |
width, height | Integer | image, svg | Dimensiones intrínsecas. |
alt | String | image, svg | Texto alternativo por defecto. |
hash | String | Todos | sha256-<base64> para integridad (el runtime PUEDE verificar). |
family, weight, style | font | Familia y rasgos que expone. |
- Un
imageusasrc: AssetRef,fit: "cover" | "contain" | "fill" | "none" | "scale-down"(defecto"cover"),alt. aspectRatioconserva la relación ancho/alto al redimensionar. En este editor, el punto de recorte se guarda como extensiónx-dodrawing-crop: { "x": 0..100, "y": 0..100 }; se aplica confit: "cover"y por defecto queda centrado (50, 50). Otros runtimes pueden ignorar esta extensión.icon:nameesAssetRefa unsvg, o"provider:name"(p. ej."lucide:home") que resuelve el runtime. Los nombres de proveedor no son portables ⇒DIL-W014.svgusado eniconse tiñe concolorsi no contiene colores fijos.- Esquemas
uripermitidos: relativo,https:,data:con MIME de imagen/fuente. Otros (http:,file:,javascript:) ⇒DIL-E022. - El runtime NO DEBE realizar peticiones a otros destinos en 1.0.
23. Metadatos de editor
La clave editor existe en la raíz y en cualquier nodo/definición. Su contenido no afecta al comportamiento.
Regla normativa de equivalencia: eliminar todas las claves editor de un documento NO DEBE cambiar su comportamiento ni su render en ningún runtime conforme.
Claves recomendadas (el resto es libre y específico del editor):
| Ámbito | Clave | Tipo | Uso |
|---|---|---|---|
| Nodo | locked | Boolean | Impide selección/edición en el lienzo. |
| Nodo | hidden | Boolean | Oculta en el lienzo (≠ visible de runtime). |
| Nodo | collapsed | Boolean | Colapsado en el panel de capas. |
| Nodo | label | String | Nombre de capa. |
| Nodo | color | String | Color de etiqueta. |
| Nodo | notes | String | Notas de diseño. |
| Nodo | aspectLocked | Boolean | Mantener proporción al redimensionar. |
| Raíz | canvas.pages | Record<Id, {x,y}> | Posición de cada página en el lienzo. |
| Raíz | canvas.zoom, canvas.scroll | Vista. | |
| Raíz | grid | { size, snap } | Rejilla y ajuste. |
| Raíz | guides | { pageId, axis, position }[] | Guías. |
| Raíz | comments | Comment[] | Comentarios. |
| Raíz | selection | Id[] | Selección persistida. |
La aplicación Dodrawing conserva además algunas preferencias visuales como extensiones x-* del nodo, sin cambiar los campos normativos del runtime:
| Extensión | Tipo | Uso |
|---|---|---|
x-dodrawing-radius-css | String (px, rem, em o %) | Unidad CSS exacta del radio. radius conserva también su aproximación numérica en dp para runtimes que ignoren la extensión. |
x-dodrawing-shape | String | Conserva el nombre de la forma del editor (triangle, diamond, hexagon); la geometría interoperable se escribe también como shape.kind: "polygon" y sides. |
x-dodrawing-line-points | [Fraction, Fraction, Fraction, Fraction] | Puntos inicial y final normalizados de una línea, para mantener diagonales en ambas direcciones y líneas horizontales/verticales. |
x-dodrawing-arrow-end | "arrow" | "none" | Punta del conector en el editor. |
Los runtimes que no entiendan estas extensiones pueden ignorarlas; deben conservarlas al editar el documento.
Criterio de separación: si el valor lo necesita la aplicación para ser la interfaz (posición
x/y,disabled,visible), es runtime. Si solo lo necesita el diseñador para trabajar (bloqueo, notas, zoom), es editor.
Un editor DEBE preservar claves editor y x-* desconocidas al guardar (round-trip).
24. Modelo de ejecución (runtime)
24.1 Ciclo de vida
┌────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐
│ Cargar doc │ → │ Inicializar │ → │ Montar start │ → │ Bucle de │
│ (§3.3 ①-④) │ │ estado docs │ │ page + load │ │ eventos │
└────────────┘ └──────────────┘ └──────────────┘ └─────┬──────┘
│
evento (usuario/timer/sistema) ──► acciones ──► estado ──► reevaluar ──► render- Inicialización de estado: se evalúan los
defaultdestate(documento) en orden topológico de dependencias. - Montaje: se crea el ámbito de página (
state,params), se instancia el árbol (§3.3 ⑤), se disparaload(nodos y página). - Bucle: cada evento ejecuta su
ActionSpec.
24.2 Instanciación
- Componentes: cada
instancecrea un ámbito propio (props +statedel componente) y expande suroot. - `repeat`: cada elemento crea un ámbito con
as/index. Se reconcilia por `key`: mismokey⇒ se conserva ámbito y estado de componente;keynuevo ⇒ se monta;keyausente ⇒ se desmonta (unload). - `slot`: se sustituye por los nodos de la instancia.
24.3 Reactividad
- Cada expresión conoce sus dependencias (variables leídas).
- Un
tickes una unidad atómica: se aplican todas las mutaciones de la secuencia hasta el siguientewait/suspensión, se recalculancomputedy propiedades afectadas una sola vez en orden topológico (sin estados intermedios observables, glitch-free), y se renderiza. - Cambios de
viewport,theme,formy de estados de interacción (hover…) también inician un tick.
24.4 Layout
El layout se calcula cada vez que cambian propiedades que lo afectan, de forma determinista según §9. Dos runtimes conformes DEBEN producir las mismas cajas (tolerancia de ±1 dp por redondeo y métricas de fuente).
24.5 Determinismo
Con los mismos documento, entradas (secuencia de eventos con sus tiempos lógicos), viewport y now(), el resultado DEBE ser idéntico. now() es la única fuente de no determinismo: un runtime en modo reproducible DEBE permitir fijar su valor.
24.6 Límites
| Recurso | Límite | Código |
|---|---|---|
| Nodos instanciados por página | 50 000 | DIL-R002 |
Elementos por repeat | 10 000 | DIL-E021 |
| Profundidad de árbol | 64 | DIL-E021 |
| Anidamiento de componentes | 16 | DIL-E021 |
| Tamaño del documento | 16 MB | DIL-E021 |
| Acciones por secuencia | 1 000 | DIL-R002 |
25. Validación y diagnósticos
25.1 Pases del validador
| Pase | Verifica | Fases (§3.3) |
|---|---|---|
| P1 Sintaxis | JSON válido, I-JSON, UTF-8, sin claves duplicadas. | ① |
| P2 Estructura | Cumple el JSON Schema (Apéndice B): tipos, enumeraciones, patrones, obligatorios. | ②④ |
| P3 Semántica | Unicidad de ids; referencias (§5.3); ciclos (tokens, estado, componentes, acciones); tipos de tokens por propiedad; combinaciones de layout válidas; overrides, variantes y slots existentes; sintaxis y tipos estáticos de expresiones; caminos de asignación; límites. | ③④ |
| P4 Lint | Accesibilidad, contraste, objetivos táctiles, elementos no usados/inalcanzables. | ④ |
Un documento es válido si P1–P3 no producen errores. P4 solo produce avisos o información.
25.2 Formato de diagnóstico
{
"code": "DIL-E005",
"severity": "error",
"message": "Referencia no resuelta: la página 'dashbord' no existe.",
"path": "/pages/0/body/children/2/on/click/0/navigate",
"nodeId": "submitBtn",
"hint": "¿Quisiste decir 'dashboard'?"
}severity: "error" | "warning" | "info". path: JSON Pointer sobre la forma de autoría; el normalizador DEBE conservar la correspondencia.
25.3 Catálogo de códigos
Errores (`DIL-E…`)
| Código | Significado |
|---|---|
E001 | JSON inválido o violación de I-JSON (clave duplicada, número no finito). |
E002 | Campo dil ausente o versión no soportada. |
E003 | Violación de esquema: propiedad desconocida, tipo/enum/patrón/obligatoria, atajo contradictorio. |
E004 | Id duplicado en su ámbito (o key duplicada). |
E005 | Referencia no resuelta (componente, página, estilo, asset, acción, animación, nodo). |
E006 | Token inexistente o tema que sobrescribe un token inexistente. |
E007 | Categoría de token incompatible con la propiedad. |
E008 | Ciclo (alias de tokens, extends de temas, computed/default de estado). |
E009 | Recursión: componente que se contiene a sí mismo o acción call recursiva. |
E010 | Error de sintaxis en expresión. |
E011 | Error de tipo estático en expresión o argumentos de función; resultado incompatible con el tipo de la propiedad. |
E012 | Identificador/función desconocido, o Path no asignable. |
E013 | Combinación de layout inválida (p. ej. x+right+width; % en padre fit). |
E014 | Override apunta a un nodo inexistente en el componente. |
E015 | Slot referenciado inexistente. |
E016 | Eje u opción de variante inexistente. |
E017 | Prop/param requerido ausente, desconocido o de tipo incorrecto. |
E018 | Acción inválida: verbo desconocido/reservado o más de un verbo. |
E019 | Evento desconocido para ese tipo de nodo. |
E020 | Valor incompatible con el tipo del estado (set/bind/default). |
E021 | Límite estructural excedido (§24.6). |
E022 | URI de asset no permitida. |
E023 | Expresión regular inválida o prohibida. |
Avisos (`DIL-W…`)
| Código | Significado |
|---|---|
W001 | Imagen sin texto alternativo. |
W002 | Elemento interactivo sin nombre accesible. |
W003 | Contraste insuficiente. |
W004 | Objetivo táctil demasiado pequeño. |
W005 | Página inalcanzable. |
W006 | Token/asset/estilo/componente/acción sin uso. |
W007 | Propiedad desconocida ignorada (lenient). |
W008 | Característica deprecada. |
W010 | Variante de control sin estilo "<type>.<variant>" en styles. |
W011 | Dependencia circular de tamaño (fit con fill hijo). |
W012 | Salto en jerarquía de encabezados. |
W013 | Orden de foco duplicado. |
W014 | Icono con proveedor no portable. |
25.4 Diagnósticos de runtime (DIL-R…)
| Código | Significado |
|---|---|
R001 | Error de evaluación de expresión (el resultado es null; la propiedad usa su defecto). |
R002 | Límite de ejecución excedido. |
R003 | Fallo de acción (secuencia detenida). |
R004 | Asset no disponible (carga fallida, hash inválido). |
Un runtime NO DEBE terminar abruptamente por errores de R001–R004: degrada y continúa.
26. Seguridad
- Sin código arbitrario: no hay
eval, scripts, HTML crudo ni CSS crudo. La única lógica es el lenguaje de expresiones (§17) y las acciones cerradas (§18). - Texto inerte: el contenido de
textes siempre texto; nunca se interpreta como HTML/marcado. - Sin red en 1.0: ninguna construcción del lenguaje realiza peticiones. La carga de
assetsestá limitada a los esquemas de §22. - Expresiones acotadas: totales, sin recursión, con límites de pasos y tamaño (§17.7).
- Regex seguras: subconjunto sin retroceso catastrófico (§17.5).
- Datos no confiables: un documento generado por IA o recibido de terceros DEBE validarse en modo
strictantes de ejecutarse. - `host` es el único canal hacia la aplicación anfitriona; el anfitrión decide qué
nameatiende y DEBE tratarpayloadcomo entrada no confiable. - Los metadatos
editoryx-*NO DEBEN usarse para transportar comportamiento ejecutable. - Los runtimes DEBERÍAN aislar el render (sandbox/iframe) cuando muestren documentos de terceros.
27. Compatibilidad y hoja de ruta
27.1 Congelación de 1.0
DIL 1.0 se considera congelado cuando existan: (1) esta especificación, (2) el JSON Schema oficial generado de los tipos del Apéndice A, (3) un validador de referencia y (4) un conjunto de pruebas de conformidad (documentos válidos/inválidos con diagnósticos esperados).
27.2 Candidatas a 1.1+ (fuera del núcleo)
| Área | Idea |
|---|---|
| Datos remotos | fetch, auth, estados loading/error de petición, esquemas de datos. |
| Lógica | Expresiones más potentes, funciones definidas por el usuario (puras), switch/match. |
| Layout | Container queries, stack explícito, sticky, position: fixed, subgrid. |
| Componentes | Bibliotecas externas importables (imports), versionado de componentes, parámetros en acciones con nombre. |
| Formularios | Múltiples formularios por página (forms.<id>), validación asíncrona. |
| Medios | video, audio, lottie, canvas. |
| Interacción | Arrastrar y soltar, gestos (swipe/pinch), foco programable avanzado, stopPropagation. |
| Plataforma | Plugins, persistencia local (store), compartir/descargar, i18n (t() y mensajes). |
| Diseño | Máscaras, boolean ops en vectores, variables de modo adicionales (densidad), container de unidades relativas. |
27.3 Compatibilidad
Toda adición de 1.x será opcional y detectable por dil: "1.x". Las claves reservadas por esta especificación (verbos de §18.4, estados de §13.4) no se reutilizarán con otro significado.
Apéndice A — Tipos TypeScript
Definiciones normativas de forma (la fuente para generar el JSON Schema del Apéndice B). Si hay discrepancia con las tablas del texto, prevalece el texto.
/* ============================ PRIMITIVOS ============================ */
export type Id = string; // /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/
export type Name = string; // /^[A-Za-z_][A-Za-z0-9_]{0,63}$/
export type TokenRef = `${string}`; // /^\$[A-Za-z_][\w-]*(\.[A-Za-z_][\w-]*)+$/
export type Mustache = `${string}{{${string}}}${string}`;
export type Expr = string; // sin llaves
export type Dynamic<T> = T | TokenRef | Mustache;
export type Length = number; // dp
export type Percentage = `${number}%`;
export type Fraction = number; // 0..1
export type Angle = number; // grados
export type Duration = number; // ms
export type Color = string; // hex | rgb() | hsl() | "transparent"
export type Size = Length | Percentage | 'auto' | 'fit' | 'fill';
export type Spacing = number | string | [number] | [number, number] | [number, number, number] | [number, number, number, number];
export type Anchor =
| 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right'
| 'bottom-left' | 'bottom' | 'bottom-right';
export type AssetRef = Id | Mustache;
export type Easing =
| 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out'
| [number, number, number, number]
| { type: 'spring'; stiffness?: number; damping?: number; mass?: number };
/* ============================ DOCUMENTO ============================= */
export interface DilDocument {
dil: '1.0';
meta?: Meta;
theme?: string;
breakpoints?: Record<Id, number>;
tokens?: Tokens;
themes?: Record<Id, Theme>;
styles?: Record<Id, StylePatch>;
assets?: Record<Id, Asset>;
components?: Record<Id, Component>;
state?: Record<Name, StateDecl | string | number | boolean>;
computed?: Record<Name, Expr>;
data?: Record<Name, unknown>;
actions?: Record<Id, ActionSpec>;
animations?: Record<Id, Animation>;
pages: Page[]; // ≥ 1
flows?: Flow[];
start?: Id;
editor?: EditorMeta;
[ext: `x-${string}`]: unknown;
}
export interface Meta {
name?: string; description?: string; author?: string;
created?: string; modified?: string; // ISO 8601
license?: string; tags?: string[];
}
/* ============================== TOKENS ============================== */
export type TokenTree<T> = { [k: string]: T | TokenTree<T> };
export interface Tokens {
color?: TokenTree<Color | TokenRef>;
space?: TokenTree<Length | TokenRef>;
size?: TokenTree<Length | TokenRef>;
radius?: TokenTree<Length | TokenRef>;
opacity?: TokenTree<Fraction | TokenRef>;
duration?: TokenTree<Duration | TokenRef>;
easing?: TokenTree<Easing | TokenRef>;
font?: TokenTree<string | TokenRef>;
text?: TokenTree<TextStyle | TokenRef>;
shadow?: TokenTree<Shadow | Shadow[] | TokenRef>;
gradient?: TokenTree<Gradient | TokenRef>;
}
export interface Theme { extends?: string; tokens: Partial<Tokens>; }
/* ============================= ESTILO ============================== */
export interface Stop { at: Fraction; color: Dynamic<Color>; }
export type Gradient =
| { type: 'linear'; angle?: Angle; stops: Stop[]; opacity?: Fraction }
| { type: 'radial'; center?: [Fraction, Fraction]; radius?: Fraction; stops: Stop[]; opacity?: Fraction }
| { type: 'conic'; center?: [Fraction, Fraction]; from?: Angle; stops: Stop[]; opacity?: Fraction };
export type Paint =
| Dynamic<Color> // atajo solid
| { type: 'solid'; color: Dynamic<Color>; opacity?: Fraction }
| Gradient
| { type: 'image'; src: AssetRef; fit?: 'cover' | 'contain' | 'fill' | 'tile'; opacity?: Fraction };
export interface Shadow {
x?: number; y?: number; blur?: Length; spread?: number;
color?: Dynamic<Color>; inset?: boolean;
}
export interface TextStyle {
fontFamily?: Dynamic<string>; fontSize?: Dynamic<Length>;
fontWeight?: Dynamic<number | 'normal' | 'bold'>; fontStyle?: 'normal' | 'italic';
lineHeight?: Dynamic<Length | Percentage | 'auto'>; letterSpacing?: Dynamic<Length | Percentage>;
textTransform?: 'none' | 'uppercase' | 'lowercase' | 'capitalize';
decoration?: 'none' | 'underline' | 'strike';
}
/* ======================= PROPIEDADES DE NODO ======================== */
export interface ItemProps { // como hijo de un contenedor
position?: 'flow' | 'absolute';
x?: Dynamic<number>; left?: Dynamic<number>;
y?: Dynamic<number>; top?: Dynamic<number>;
right?: Dynamic<number>; bottom?: Dynamic<number>;
centerX?: Dynamic<number | true>; centerY?: Dynamic<number | true>;
zIndex?: number;
width?: Dynamic<Size>; height?: Dynamic<Size>;
minWidth?: Dynamic<Length>; maxWidth?: Dynamic<Length>;
minHeight?: Dynamic<Length>; maxHeight?: Dynamic<Length>;
aspectRatio?: number;
grow?: number;
alignSelf?: 'auto' | 'start' | 'center' | 'end' | 'stretch' | 'baseline';
col?: number; row?: number; colSpan?: number; rowSpan?: number;
margin?: Dynamic<Spacing>;
marginTop?: Dynamic<Length>; marginRight?: Dynamic<Length>;
marginBottom?: Dynamic<Length>; marginLeft?: Dynamic<Length>;
}
export interface TransformProps {
rotate?: Dynamic<Angle>; scale?: Dynamic<number>;
scaleX?: Dynamic<number>; scaleY?: Dynamic<number>;
translateX?: Dynamic<number>; translateY?: Dynamic<number>;
origin?: Anchor;
}
export interface ContainerProps { // frame
layout?: 'none' | 'absolute' | 'flex' | 'row' | 'column' | 'grid';
direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
wrap?: boolean;
gap?: Dynamic<Spacing>; rowGap?: Dynamic<Length>; columnGap?: Dynamic<Length>;
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly' | 'stretch';
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline';
columns?: number | Track[]; rows?: number | Track[]; flow?: 'row' | 'column';
padding?: Dynamic<Spacing>;
paddingTop?: Dynamic<Length>; paddingRight?: Dynamic<Length>;
paddingBottom?: Dynamic<Length>; paddingLeft?: Dynamic<Length>;
clip?: boolean; scroll?: 'none' | 'x' | 'y' | 'both';
}
export type Track = number | Percentage | 'auto' | `${number}fr`
| { min: Length; max: Length | `${number}fr` };
export interface VisualProps {
fill?: Paint | Paint[];
border?: number | string | { width?: Spacing; style?: 'solid' | 'dashed' | 'dotted'; color?: Dynamic<Color>; position?: 'inside' | 'center' | 'outside' };
borderTopWidth?: Dynamic<Length>; borderRightWidth?: Dynamic<Length>;
borderBottomWidth?: Dynamic<Length>; borderLeftWidth?: Dynamic<Length>;
borderColor?: Dynamic<Color>; borderStyle?: 'solid' | 'dashed' | 'dotted';
borderPosition?: 'inside' | 'center' | 'outside';
radius?: Dynamic<Spacing>;
radiusTopLeft?: Dynamic<Length>; radiusTopRight?: Dynamic<Length>;
radiusBottomRight?: Dynamic<Length>; radiusBottomLeft?: Dynamic<Length>;
shadow?: Dynamic<Shadow | Shadow[]>;
blur?: Dynamic<Length>; backdropBlur?: Dynamic<Length>;
blend?: 'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten' | 'color-dodge' | 'color-burn' | 'difference';
pointerEvents?: 'auto' | 'none';
}
export interface TextProps extends TextStyle {
textStyle?: TokenRef | Id;
color?: Dynamic<Color>;
textAlign?: 'start' | 'center' | 'end' | 'justify';
verticalAlign?: 'top' | 'middle' | 'bottom';
whiteSpace?: 'normal' | 'nowrap' | 'pre';
maxLines?: number; truncate?: 'clip' | 'ellipsis';
dir?: 'auto' | 'ltr' | 'rtl';
}
export type StateName =
| 'hover' | 'focus' | 'focusVisible' | 'selected' | 'checked'
| 'pressed' | 'invalid' | 'loading' | 'disabled';
/** Parche de propiedades: lo que puede aparecer en styles, variantes, estados, responsive y overrides. */
export type StylePatch = Partial<ItemProps & TransformProps & ContainerProps & VisualProps & TextProps> & {
opacity?: Dynamic<Fraction>; visible?: Dynamic<boolean>;
disabled?: Dynamic<boolean>; selected?: Dynamic<boolean>; loading?: Dynamic<boolean>;
states?: Partial<Record<StateName, StatePatch>>;
responsive?: Record<Id, StylePatch>;
transition?: Transition;
a11y?: A11y;
};
export type StatePatch = StylePatch & { within?: OverrideMap };
export type OverrideMap = Record<string /* "$root" | id | "a/b" */, StylePatch>;
export interface Transition {
properties?: string[] | 'all';
duration?: Dynamic<Duration>; delay?: Dynamic<Duration>; easing?: Dynamic<Easing>;
}
/* =============================== NODOS ============================== */
interface NodeBase extends ItemProps, TransformProps {
id?: Id; name?: string;
visible?: Dynamic<boolean>; opacity?: Dynamic<Fraction>;
disabled?: Dynamic<boolean>; selected?: Dynamic<boolean>; loading?: Dynamic<boolean>;
style?: Id | Id[];
states?: Partial<Record<StateName, StatePatch>>;
responsive?: Record<Id, StylePatch>;
transition?: Transition;
animation?: Id | { ref: Id; trigger?: 'load' | 'visible' | 'hover'; loop?: boolean };
on?: Partial<Record<EventName, ActionSpec>>;
a11y?: A11y;
editor?: EditorMeta;
[ext: `x-${string}`]: unknown;
}
export interface FrameNode extends NodeBase, ContainerProps, VisualProps, TextProps {
type: 'frame'; children?: Node[];
}
export interface GroupNode extends NodeBase, ContainerProps, VisualProps, TextProps { type: 'group'; children?: Node[]; }
export interface TextNode extends NodeBase, VisualProps, TextProps {
type: 'text'; text?: Dynamic<string>; runs?: Run[];
}
export interface Run {
text: string; fontWeight?: number | 'normal' | 'bold'; fontStyle?: 'normal' | 'italic';
color?: Dynamic<Color>; decoration?: 'none' | 'underline' | 'strike'; link?: ActionSpec;
}
export interface ImageNode extends NodeBase, VisualProps {
type: 'image'; src: AssetRef; fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; alt?: Dynamic<string>;
}
export interface IconNode extends NodeBase {
type: 'icon'; name: AssetRef | string; size?: Dynamic<Length>; color?: Dynamic<Color>;
}
export interface ShapeNode extends NodeBase, VisualProps {
type: 'shape'; kind: 'rect' | 'ellipse' | 'line' | 'polygon' | 'star' | 'path';
d?: string; points?: [number, number][]; sides?: number; ratio?: Fraction;
stroke?: Dynamic<Color>; strokeWidth?: Dynamic<Length>;
strokeCap?: 'butt' | 'round' | 'square'; strokeJoin?: 'miter' | 'round' | 'bevel'; strokeDash?: Length[];
}
export type ValidationRule =
| { rule: 'required'; message?: Dynamic<string> }
| { rule: 'minLength' | 'maxLength' | 'min' | 'max'; value: number; message?: Dynamic<string> }
| { rule: 'email'; message?: Dynamic<string> }
| { rule: 'pattern'; value: string; message?: Dynamic<string> }
| { rule: 'oneOf'; value: unknown[]; message?: Dynamic<string> }
| { rule: 'expr'; expr: Expr; message?: Dynamic<string> };
interface ControlBase extends NodeBase, ContainerProps, VisualProps, TextProps {
name?: string; bind?: string; variant?: string;
validate?: ValidationRule[]; validateOn?: 'change' | 'blur' | 'submit';
}
export interface ButtonNode extends ControlBase {
type: 'button'; text?: Dynamic<string>; icon?: AssetRef | string;
iconPosition?: 'start' | 'end'; buttonType?: 'button' | 'submit';
}
export interface InputNode extends ControlBase {
type: 'input'; inputType?: 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url';
placeholder?: Dynamic<string>; maxLength?: number; readOnly?: Dynamic<boolean>; autofocus?: boolean;
}
export interface TextareaNode extends ControlBase {
type: 'textarea'; placeholder?: Dynamic<string>; rows?: number; maxLength?: number; readOnly?: Dynamic<boolean>; autofocus?: boolean;
}
export interface CheckboxNode extends ControlBase { type: 'checkbox'; text?: Dynamic<string>; indeterminate?: Dynamic<boolean>; }
export interface RadioNode extends ControlBase { type: 'radio'; text?: Dynamic<string>; value: Dynamic<unknown>; groupName: Name; }
export interface SwitchNode extends ControlBase { type: 'switch'; text?: Dynamic<string>; }
export interface SelectNode extends ControlBase {
type: 'select'; options: Option[] | Mustache; placeholder?: Dynamic<string>;
}
export interface Option { value: unknown; label: string; disabled?: boolean; }
export interface SliderNode extends ControlBase { type: 'slider'; min?: number; max?: number; step?: number; }
export interface InstanceNode extends NodeBase, StylePatch {
type: 'instance'; component: Id;
props?: Record<Name, Dynamic<unknown>>;
variants?: Record<Name, Dynamic<string>>;
overrides?: OverrideMap;
slots?: Record<Id, Node | Node[]>;
children?: Node[]; // ≡ slots.default
}
export interface SlotNode extends NodeBase { type: 'slot'; name: Id; children?: Node[]; }
export interface RepeatNode extends NodeBase {
type: 'repeat'; each: Expr; as?: Name; index?: Name; key?: Expr;
filter?: Expr; sort?: { by: Expr; order?: 'asc' | 'desc' }; limit?: Expr;
template: Node; empty?: Node;
}
export type Node =
| FrameNode | GroupNode | TextNode | ImageNode | IconNode | ShapeNode
| ButtonNode | InputNode | TextareaNode | CheckboxNode | RadioNode | SwitchNode | SelectNode | SliderNode
| InstanceNode | SlotNode | RepeatNode;
/* ============================ COMPONENTES =========================== */
export type PropDecl = {
type: 'string' | 'number' | 'boolean' | 'color' | 'enum' | 'icon' | 'image' | 'array' | 'object' | 'any' | 'action';
default?: unknown; required?: boolean; options?: unknown[]; description?: string;
};
export interface VariantAxis { axis: Name; default: string; options: Record<string, OverrideMap>; }
export interface CompoundVariant { when: Record<Name, string>; apply: OverrideMap; }
export interface Component {
name?: string; description?: string;
props?: Record<Name, PropDecl>;
state?: Record<Name, StateDecl | string | number | boolean>;
computed?: Record<Name, Expr>;
events?: Record<Id, { description?: string; payload?: PropDecl['type'] }>;
variants?: VariantAxis[];
compoundVariants?: CompoundVariant[];
root: Node;
}
/* ============================== ESTADO ============================== */
export interface StateDecl {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
default?: Dynamic<unknown>; enum?: unknown[]; description?: string;
}
/* ============================ ACCIONES ============================== */
export type Path = string;
export type EventName =
| 'click' | 'doubleClick' | 'longPress' | 'pointerEnter' | 'pointerLeave' | 'pointerDown' | 'pointerUp'
| 'focus' | 'blur' | 'input' | 'change' | 'submit' | 'keyDown' | 'keyUp' | 'scroll'
| 'visible' | 'load' | 'unload' | 'resize' | (string & {}); // + eventos de componente
export type PageTransition = {
type: 'none' | 'fade' | 'slide' | 'push' | 'scale' | 'smart';
direction?: 'left' | 'right' | 'up' | 'down'; duration?: Duration; easing?: Easing;
};
export type Action = { id?: Id } & (
| { set: Record<Path, Dynamic<unknown>> }
| { toggle: Path }
| { append: { to: Path; value: Dynamic<unknown>; index?: number } }
| { remove: { from: Path; index?: Dynamic<number> ; where?: Expr; as?: Name } }
| { if: Expr; then: ActionSpec; else?: ActionSpec }
| { parallel: Action[] }
| { wait: Dynamic<Duration> }
| { call: Id }
| { navigate: Id | { to: Id; params?: Record<Name, Dynamic<unknown>>; transition?: PageTransition; replace?: boolean } }
| { back: true | { transition?: PageTransition } }
| { open: Id | { overlay: Id; anchor?: Id; params?: Record<Name, Dynamic<unknown>> } }
| { close: true | Id }
| { animate: Id | { ref: Id; target?: Id; await?: boolean } }
| { setTheme: Dynamic<string> }
| { focus: Id }
| { scrollTo: Id | { target: Id; behavior?: 'auto' | 'smooth'; align?: 'start' | 'center' | 'end' } }
| { validate: 'form' | Name[] }
| { reset: 'form' | Path[] }
| { emit: Id | { event: Id; payload?: Dynamic<unknown> } }
| { run: Name }
| { host: { name: Id; payload?: Dynamic<unknown> } }
);
export type ActionSpec = string | Action | Array<string | Action>;
/* ============================ ANIMACIÓN ============================= */
export interface Keyframe { at: Fraction; set: Record<string, unknown>; easing?: Easing; }
export interface Animation {
duration: Duration; delay?: Duration; easing?: Easing;
iterations?: number | 'infinite'; direction?: 'normal' | 'reverse' | 'alternate';
fill?: 'none' | 'forwards'; keyframes: Keyframe[];
}
/* ============================== PÁGINAS ============================= */
export interface PageFrame { width?: number; height?: number; device?: string; background?: Paint; }
export interface OverlayConfig {
presentation?: 'modal' | 'popover' | 'sheet' | 'drawer' | 'toast';
backdrop?: Paint | false; dismissOnBackdrop?: boolean; dismissOnEscape?: boolean;
placement?: Anchor; duration?: Duration;
}
export interface Page {
id: Id; name?: string; kind?: 'screen' | 'overlay';
frame?: PageFrame; params?: Record<Name, PropDecl>;
state?: Record<Name, StateDecl | string | number | boolean>; computed?: Record<Name, Expr>;
on?: Partial<Record<'load' | 'unload' | 'keyDown' | 'resize', ActionSpec>>;
overlay?: OverlayConfig;
body: FrameNode;
editor?: EditorMeta;
}
export interface Flow {
id: Id; name?: string; start: Id; description?: string;
state?: Record<Name, unknown>; frame?: Partial<PageFrame>;
}
/* ============================== VARIOS ============================== */
export type Role =
| 'button' | 'link' | 'textbox' | 'checkbox' | 'radio' | 'switch' | 'slider' | 'combobox'
| 'heading' | 'image' | 'list' | 'listitem' | 'navigation' | 'main' | 'region' | 'form'
| 'dialog' | 'alert' | 'status' | 'tab' | 'tablist' | 'tabpanel' | 'menu' | 'menuitem'
| 'separator' | 'none';
export interface A11y {
role?: Role; label?: Dynamic<string>; labelledBy?: Id; description?: Dynamic<string>;
hidden?: Dynamic<boolean>; live?: 'off' | 'polite' | 'assertive'; focusable?: boolean;
order?: number; level?: 1 | 2 | 3 | 4 | 5 | 6; expanded?: Dynamic<boolean>; keyShortcut?: string;
}
export interface Asset {
kind: 'image' | 'svg' | 'font'; uri: string; mime?: string;
width?: number; height?: number; alt?: string; hash?: string;
family?: string; weight?: number; style?: 'normal' | 'italic';
}
export interface EditorMeta { [key: string]: unknown; }
/* ============================ DIAGNÓSTICOS ========================== */
export interface Diagnostic {
code: `DIL-${'E' | 'W' | 'R'}${string}`;
severity: 'error' | 'warning' | 'info';
message: string; path?: string; nodeId?: string; hint?: string;
}Apéndice B — JSON Schema
JSON Schema draft 2020-12. Se incluye completo el núcleo (primitivos, documento, discriminación de nodos y dos nodos de ejemplo). El esquema oficial
dil-1.0.schema.jsonDEBE generarse a partir de los tipos del Apéndice A siguiendo el mismo patrón (allOf+if/thenportype,unevaluatedProperties: falsesalvox-*).
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://dil-lang.org/schema/1.0/dil.schema.json",
"title": "DIL 1.0 Document",
"type": "object",
"required": ["dil", "pages"],
"properties": {
"dil": { "const": "1.0" },
"meta": { "$ref": "#/$defs/Meta" },
"theme": { "type": "string" },
"breakpoints": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "type": "integer", "minimum": 0 } },
"tokens": { "$ref": "#/$defs/Tokens" },
"themes": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "$ref": "#/$defs/Theme" } },
"styles": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "$ref": "#/$defs/StylePatch" } },
"assets": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "$ref": "#/$defs/Asset" } },
"components": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "$ref": "#/$defs/Component" } },
"state": { "$ref": "#/$defs/StateMap" },
"computed": { "type": "object", "propertyNames": { "$ref": "#/$defs/Name" },
"additionalProperties": { "type": "string" } },
"data": { "type": "object", "propertyNames": { "$ref": "#/$defs/Name" } },
"actions": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "$ref": "#/$defs/ActionSpec" } },
"animations": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "$ref": "#/$defs/Animation" } },
"pages": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/Page" } },
"flows": { "type": "array", "items": { "$ref": "#/$defs/Flow" } },
"start": { "$ref": "#/$defs/Id" },
"editor": { "type": "object" }
},
"patternProperties": { "^x-": {} },
"unevaluatedProperties": false,
"$defs": {
"Id": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_-]{0,63}quot; },
"Name": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]{0,63}quot; },
"TokenRef": { "type": "string", "pattern": "^\\$[A-Za-z_][A-Za-z0-9_-]*(\\.[A-Za-z_][A-Za-z0-9_-]*)+quot; },
"Mustache": { "type": "string", "pattern": "\\{\\{[\\s\\S]+?\\}\\}" },
"Percentage": { "type": "string", "pattern": "^-?\\d+(\\.\\d+)?%quot; },
"Color": {
"type": "string",
"pattern": "^(#([0-9A-Fa-f]{3,4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})|(rgb|rgba|hsl|hsla)\\([^)]*\\)|transparent)quot;
},
"Length": { "type": "number", "minimum": 0 },
"Fraction": { "type": "number", "minimum": 0, "maximum": 1 },
"Duration": { "type": "number", "minimum": 0 },
"Angle": { "type": "number" },
"Dynamic": {
"description": "Plantilla: T | TokenRef | Mustache. Las propiedades concretas combinan este $ref con su tipo base mediante anyOf.",
"anyOf": [ { "$ref": "#/$defs/TokenRef" }, { "$ref": "#/$defs/Mustache" } ]
},
"DynamicLength": { "anyOf": [ { "$ref": "#/$defs/Length" }, { "$ref": "#/$defs/Dynamic" } ] },
"DynamicNumber": { "anyOf": [ { "type": "number" }, { "$ref": "#/$defs/Dynamic" } ] },
"DynamicBoolean": { "anyOf": [ { "type": "boolean" }, { "$ref": "#/$defs/Dynamic" } ] },
"DynamicString": { "type": "string" },
"DynamicColor": { "anyOf": [ { "$ref": "#/$defs/Color" }, { "$ref": "#/$defs/Dynamic" } ] },
"DynamicOpacity": { "anyOf": [ { "$ref": "#/$defs/Fraction" }, { "$ref": "#/$defs/Dynamic" } ] },
"Size": {
"anyOf": [
{ "$ref": "#/$defs/Length" },
{ "$ref": "#/$defs/Percentage" },
{ "enum": ["auto", "fit", "fill"] },
{ "$ref": "#/$defs/Dynamic" }
]
},
"Spacing": {
"anyOf": [
{ "type": "number", "minimum": 0 },
{ "type": "string", "pattern": "^\\s*\\d+(\\.\\d+)?(\\s+\\d+(\\.\\d+)?){0,3}\\s*quot; },
{ "type": "array", "minItems": 1, "maxItems": 4, "items": { "type": "number", "minimum": 0 } },
{ "$ref": "#/$defs/Dynamic" }
]
},
"Easing": {
"anyOf": [
{ "enum": ["linear", "ease", "ease-in", "ease-out", "ease-in-out"] },
{ "type": "array", "minItems": 4, "maxItems": 4, "items": { "type": "number" } },
{ "type": "object", "required": ["type"],
"properties": {
"type": { "const": "spring" },
"stiffness": { "type": "number", "exclusiveMinimum": 0 },
"damping": { "type": "number", "exclusiveMinimum": 0 },
"mass": { "type": "number", "exclusiveMinimum": 0 }
}, "additionalProperties": false },
{ "$ref": "#/$defs/TokenRef" }
]
},
"Anchor": { "enum": ["top-left","top","top-right","left","center","right","bottom-left","bottom","bottom-right"] },
"Stop": {
"type": "object", "required": ["at", "color"],
"properties": { "at": { "$ref": "#/$defs/Fraction" }, "color": { "$ref": "#/$defs/DynamicColor" } },
"additionalProperties": false
},
"Paint": {
"anyOf": [
{ "$ref": "#/$defs/DynamicColor" },
{ "type": "object", "required": ["type", "color"],
"properties": { "type": { "const": "solid" }, "color": { "$ref": "#/$defs/DynamicColor" }, "opacity": { "$ref": "#/$defs/Fraction" } },
"additionalProperties": false },
{ "type": "object", "required": ["type", "stops"],
"properties": { "type": { "enum": ["linear", "radial", "conic"] },
"angle": { "$ref": "#/$defs/Angle" }, "from": { "$ref": "#/$defs/Angle" },
"center": { "type": "array", "minItems": 2, "maxItems": 2, "items": { "$ref": "#/$defs/Fraction" } },
"radius": { "$ref": "#/$defs/Fraction" }, "opacity": { "$ref": "#/$defs/Fraction" },
"stops": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/Stop" } } },
"additionalProperties": false },
{ "type": "object", "required": ["type", "src"],
"properties": { "type": { "const": "image" }, "src": { "type": "string" },
"fit": { "enum": ["cover", "contain", "fill", "tile"] }, "opacity": { "$ref": "#/$defs/Fraction" } },
"additionalProperties": false }
]
},
"Shadow": {
"type": "object",
"properties": {
"x": { "type": "number" }, "y": { "type": "number" },
"blur": { "$ref": "#/$defs/Length" }, "spread": { "type": "number" },
"color": { "$ref": "#/$defs/DynamicColor" }, "inset": { "type": "boolean" }
},
"additionalProperties": false
},
"Transition": {
"type": "object",
"properties": {
"properties": { "anyOf": [ { "const": "all" }, { "type": "array", "items": { "type": "string" } } ] },
"duration": { "anyOf": [ { "$ref": "#/$defs/Duration" }, { "$ref": "#/$defs/Dynamic" } ] },
"delay": { "anyOf": [ { "$ref": "#/$defs/Duration" }, { "$ref": "#/$defs/Dynamic" } ] },
"easing": { "$ref": "#/$defs/Easing" }
},
"additionalProperties": false
},
"StateName": { "enum": ["hover","focus","focusVisible","selected","checked","pressed","invalid","loading","disabled"] },
"StylePatch": {
"type": "object",
"description": "Parche de propiedades (visual, layout, texto, ítem, transformación). Lista completa: Apéndice A.",
"properties": {
"states": { "type": "object", "propertyNames": { "$ref": "#/$defs/StateName" },
"additionalProperties": { "$ref": "#/$defs/StylePatch" } },
"responsive": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" },
"additionalProperties": { "$ref": "#/$defs/StylePatch" } },
"transition": { "$ref": "#/$defs/Transition" }
},
"not": { "anyOf": [ { "required": ["id"] }, { "required": ["type"] }, { "required": ["children"] } ] }
},
"OverrideMap": { "type": "object", "additionalProperties": { "$ref": "#/$defs/StylePatch" } },
"StateDecl": {
"anyOf": [
{ "type": ["string", "number", "boolean"] },
{ "type": "object", "required": ["type"],
"properties": {
"type": { "enum": ["string", "number", "boolean", "object", "array"] },
"default": {}, "enum": { "type": "array" }, "description": { "type": "string" } },
"additionalProperties": false }
]
},
"StateMap": { "type": "object", "propertyNames": { "$ref": "#/$defs/Name" },
"additionalProperties": { "$ref": "#/$defs/StateDecl" } },
"Action": {
"type": "object",
"minProperties": 1,
"oneOf": [
{ "required": ["set"] }, { "required": ["toggle"] }, { "required": ["append"] },
{ "required": ["remove"] }, { "required": ["if"] }, { "required": ["parallel"] },
{ "required": ["wait"] }, { "required": ["call"] }, { "required": ["navigate"] },
{ "required": ["back"] }, { "required": ["open"] }, { "required": ["close"] },
{ "required": ["animate"] }, { "required": ["setTheme"] }, { "required": ["focus"] },
{ "required": ["scrollTo"] }, { "required": ["validate"] }, { "required": ["reset"] },
{ "required": ["emit"] }, { "required": ["run"] }, { "required": ["host"] }
],
"properties": {
"id": { "$ref": "#/$defs/Id" },
"set": { "type": "object", "minProperties": 1 },
"toggle": { "type": "string" },
"if": { "type": "string" },
"then": { "$ref": "#/$defs/ActionSpec" },
"else": { "$ref": "#/$defs/ActionSpec" },
"wait": { "anyOf": [ { "$ref": "#/$defs/Duration" }, { "$ref": "#/$defs/Dynamic" } ] },
"call": { "$ref": "#/$defs/Id" },
"navigate": { "anyOf": [ { "$ref": "#/$defs/Id" }, { "type": "object", "required": ["to"] } ] },
"back": {}, "open": {}, "close": {}, "animate": {}, "append": {}, "remove": {},
"parallel": { "type": "array", "items": { "$ref": "#/$defs/Action" } },
"setTheme": {}, "focus": {}, "scrollTo": {}, "validate": {}, "reset": {},
"emit": {}, "run": {}, "host": {}
},
"dependentRequired": { "then": ["if"], "else": ["if"] },
"additionalProperties": false
},
"ActionSpec": {
"anyOf": [
{ "$ref": "#/$defs/Id" },
{ "$ref": "#/$defs/Action" },
{ "type": "array", "items": { "anyOf": [ { "$ref": "#/$defs/Id" }, { "$ref": "#/$defs/Action" } ] } }
]
},
"NodeCommon": {
"type": "object",
"properties": {
"id": { "$ref": "#/$defs/Id" },
"name": { "type": "string" },
"visible": { "$ref": "#/$defs/DynamicBoolean" },
"opacity": { "$ref": "#/$defs/DynamicOpacity" },
"backdropBlur": { "$ref": "#/$defs/DynamicLength" },
"disabled": { "$ref": "#/$defs/DynamicBoolean" },
"selected": { "$ref": "#/$defs/DynamicBoolean" },
"loading": { "$ref": "#/$defs/DynamicBoolean" },
"style": { "anyOf": [ { "$ref": "#/$defs/Id" }, { "type": "array", "items": { "$ref": "#/$defs/Id" } } ] },
"position": { "enum": ["flow", "absolute"] },
"x": { "$ref": "#/$defs/DynamicNumber" }, "y": { "$ref": "#/$defs/DynamicNumber" },
"left": { "$ref": "#/$defs/DynamicNumber" }, "top": { "$ref": "#/$defs/DynamicNumber" },
"right": { "$ref": "#/$defs/DynamicNumber" }, "bottom": { "$ref": "#/$defs/DynamicNumber" },
"width": { "$ref": "#/$defs/Size" }, "height": { "$ref": "#/$defs/Size" },
"minWidth": { "$ref": "#/$defs/DynamicLength" }, "maxWidth": { "$ref": "#/$defs/DynamicLength" },
"minHeight": { "$ref": "#/$defs/DynamicLength" }, "maxHeight": { "$ref": "#/$defs/DynamicLength" },
"grow": { "type": "number", "minimum": 0 },
"margin": { "$ref": "#/$defs/Spacing" },
"states": { "$ref": "#/$defs/StylePatch/properties/states" },
"responsive": { "$ref": "#/$defs/StylePatch/properties/responsive" },
"transition": { "$ref": "#/$defs/Transition" },
"on": { "type": "object", "additionalProperties": { "$ref": "#/$defs/ActionSpec" } },
"a11y": { "type": "object" },
"editor": { "type": "object" }
},
"patternProperties": { "^x-": {} }
},
"Node": {
"type": "object",
"required": ["type"],
"properties": {
"type": { "enum": ["frame","group","text","image","icon","shape","button","input","textarea",
"checkbox","radio","switch","select","slider","instance","slot","repeat"] }
},
"allOf": [
{ "$ref": "#/$defs/NodeCommon" },
{ "if": { "properties": { "type": { "enum": ["frame", "group"] } } },
"then": { "properties": {
"layout": { "enum": ["none","absolute","flex","row","column","grid"] },
"direction": { "enum": ["row","column","row-reverse","column-reverse"] },
"wrap": { "type": "boolean" },
"gap": { "$ref": "#/$defs/Spacing" },
"justify": { "enum": ["start","center","end","between","around","evenly","stretch"] },
"align": { "enum": ["start","center","end","stretch","baseline"] },
"padding": { "$ref": "#/$defs/Spacing" },
"fill": { "anyOf": [ { "$ref": "#/$defs/Paint" }, { "type": "array", "items": { "$ref": "#/$defs/Paint" } } ] },
"radius": { "$ref": "#/$defs/Spacing" },
"shadow": { "anyOf": [ { "$ref": "#/$defs/Shadow" }, { "type": "array", "items": { "$ref": "#/$defs/Shadow" } }, { "$ref": "#/$defs/Dynamic" } ] },
"clip": { "type": "boolean" },
"scroll": { "enum": ["none","x","y","both"] },
"children": { "type": "array", "items": { "$ref": "#/$defs/Node" } }
} } },
{ "if": { "properties": { "type": { "const": "text" } } },
"then": { "properties": {
"text": { "type": "string" },
"runs": { "type": "array", "items": { "type": "object", "required": ["text"] } },
"fontSize": { "$ref": "#/$defs/DynamicLength" },
"fontWeight": { "anyOf": [ { "type": "integer", "minimum": 100, "maximum": 900 }, { "enum": ["normal","bold"] }, { "$ref": "#/$defs/Dynamic" } ] },
"color": { "$ref": "#/$defs/DynamicColor" },
"textAlign": { "enum": ["start","center","end","justify"] }
},
"not": { "required": ["text", "runs"] } } },
{ "if": { "properties": { "type": { "const": "button" } } },
"then": { "properties": {
"text": { "type": "string" }, "variant": { "type": "string" },
"buttonType": { "enum": ["button", "submit"] },
"icon": { "type": "string" }, "iconPosition": { "enum": ["start", "end"] },
"bind": { "type": "string" }
} } },
{ "if": { "properties": { "type": { "const": "input" } } },
"then": { "properties": {
"name": { "$ref": "#/$defs/Name" }, "bind": { "type": "string" },
"inputType": { "enum": ["text","email","password","number","search","tel","url"] },
"placeholder": { "type": "string" },
"validate": { "type": "array", "items": { "type": "object", "required": ["rule"],
"properties": { "rule": { "enum": ["required","minLength","maxLength","min","max","email","pattern","oneOf","expr"] } } } },
"validateOn": { "enum": ["change", "blur", "submit"] }
} } },
{ "if": { "properties": { "type": { "const": "instance" } } },
"then": { "required": ["component"],
"properties": {
"component": { "$ref": "#/$defs/Id" },
"props": { "type": "object", "propertyNames": { "$ref": "#/$defs/Name" } },
"variants": { "type": "object", "propertyNames": { "$ref": "#/$defs/Name" } },
"overrides": { "$ref": "#/$defs/OverrideMap" },
"slots": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" } }
} } },
{ "if": { "properties": { "type": { "const": "repeat" } } },
"then": { "required": ["each", "template"],
"properties": {
"each": { "type": "string" }, "as": { "$ref": "#/$defs/Name" }, "index": { "$ref": "#/$defs/Name" },
"key": { "type": "string" }, "filter": { "type": "string" }, "limit": { "type": "string" },
"template": { "$ref": "#/$defs/Node" }, "empty": { "$ref": "#/$defs/Node" }
} } },
{ "if": { "properties": { "type": { "const": "slot" } } },
"then": { "required": ["name"], "properties": { "name": { "$ref": "#/$defs/Id" } } } },
{ "if": { "properties": { "type": { "const": "shape" } } },
"then": { "required": ["kind"],
"properties": { "kind": { "enum": ["rect","ellipse","line","polygon","star","path"] } } } }
]
},
"Page": {
"type": "object",
"required": ["id", "body"],
"properties": {
"id": { "$ref": "#/$defs/Id" }, "name": { "type": "string" },
"kind": { "enum": ["screen", "overlay"] },
"frame": { "type": "object" },
"params": { "type": "object", "propertyNames": { "$ref": "#/$defs/Name" } },
"state": { "$ref": "#/$defs/StateMap" },
"computed": { "type": "object" },
"on": { "type": "object", "propertyNames": { "enum": ["load","unload","keyDown","resize"] },
"additionalProperties": { "$ref": "#/$defs/ActionSpec" } },
"overlay": { "type": "object" },
"body": { "allOf": [ { "$ref": "#/$defs/Node" }, { "properties": { "type": { "const": "frame" } } } ] },
"editor": { "type": "object" }
},
"if": { "properties": { "kind": { "const": "overlay" } }, "required": ["kind"] },
"then": { "required": ["overlay"] },
"unevaluatedProperties": false
},
"Component": {
"type": "object",
"required": ["root"],
"properties": {
"name": { "type": "string" }, "description": { "type": "string" },
"props": { "type": "object", "propertyNames": { "$ref": "#/$defs/Name" } },
"state": { "$ref": "#/$defs/StateMap" },
"computed": { "type": "object" },
"events": { "type": "object", "propertyNames": { "$ref": "#/$defs/Id" } },
"variants": { "type": "array", "items": {
"type": "object", "required": ["axis", "default", "options"],
"properties": { "axis": { "$ref": "#/$defs/Name" }, "default": { "type": "string" },
"options": { "type": "object", "additionalProperties": { "$ref": "#/$defs/OverrideMap" } } },
"additionalProperties": false } },
"compoundVariants": { "type": "array", "items": {
"type": "object", "required": ["when", "apply"],
"properties": { "when": { "type": "object" }, "apply": { "$ref": "#/$defs/OverrideMap" } } } },
"root": { "$ref": "#/$defs/Node" }
},
"unevaluatedProperties": false
},
"Animation": {
"type": "object", "required": ["duration", "keyframes"],
"properties": {
"duration": { "$ref": "#/$defs/Duration" }, "delay": { "$ref": "#/$defs/Duration" },
"easing": { "$ref": "#/$defs/Easing" },
"iterations": { "anyOf": [ { "type": "number", "exclusiveMinimum": 0 }, { "const": "infinite" } ] },
"direction": { "enum": ["normal", "reverse", "alternate"] },
"fill": { "enum": ["none", "forwards"] },
"keyframes": { "type": "array", "minItems": 2, "items": {
"type": "object", "required": ["at", "set"],
"properties": { "at": { "$ref": "#/$defs/Fraction" }, "set": { "type": "object" }, "easing": { "$ref": "#/$defs/Easing" } },
"additionalProperties": false } }
},
"additionalProperties": false
},
"Flow": {
"type": "object", "required": ["id", "start"],
"properties": { "id": { "$ref": "#/$defs/Id" }, "name": { "type": "string" }, "start": { "$ref": "#/$defs/Id" },
"description": { "type": "string" }, "state": { "type": "object" }, "frame": { "type": "object" } },
"additionalProperties": false
},
"Asset": {
"type": "object", "required": ["kind", "uri"],
"properties": {
"kind": { "enum": ["image", "svg", "font"] },
"uri": { "type": "string", "pattern": "^(?!(?:[a-zA-Z][a-zA-Z0-9+.-]*:))|^https:|^data:(image|font)/" },
"mime": { "type": "string" }, "width": { "type": "integer" }, "height": { "type": "integer" },
"alt": { "type": "string" }, "hash": { "type": "string", "pattern": "^sha256-" },
"family": { "type": "string" }, "weight": { "type": "integer", "minimum": 100, "maximum": 900 },
"style": { "enum": ["normal", "italic"] }
},
"additionalProperties": false
},
"Meta": {
"type": "object",
"properties": { "name": { "type": "string" }, "description": { "type": "string" }, "author": { "type": "string" },
"created": { "type": "string", "format": "date-time" }, "modified": { "type": "string", "format": "date-time" },
"license": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } } },
"additionalProperties": false
},
"Tokens": {
"type": "object",
"properties": {
"color": { "type": "object" }, "space": { "type": "object" }, "size": { "type": "object" },
"radius": { "type": "object" }, "opacity": { "type": "object" }, "duration": { "type": "object" },
"easing": { "type": "object" }, "font": { "type": "object" }, "text": { "type": "object" },
"shadow": { "type": "object" }, "gradient": { "type": "object" }
},
"additionalProperties": false
},
"Theme": {
"type": "object", "required": ["tokens"],
"properties": { "extends": { "type": "string" }, "tokens": { "$ref": "#/$defs/Tokens" } },
"additionalProperties": false
},
"Asset_Placeholder_NoOp": { "description": "Reservado; sin efecto." }
}
}Nota de implementación: el esquema valida forma. Las reglas de P3 (referencias, unicidad, ciclos, tipos de tokens, expresiones, layout) NO son expresables en JSON Schema y las implementa el validador (§25.1).
Apéndice C — Ejemplos completos
C.1 Mínimo absoluto
{
"dil": "1.0",
"pages": [
{ "id": "home", "body": { "type": "frame", "layout": "column", "gap": 16, "padding": 24,
"children": [
{ "type": "text", "text": "Hello World" },
{ "type": "button", "text": "Continue" }
] } }
]
}C.2 Prototipo completo: Login → Dashboard (tema oscuro, validación, componente con variantes, repeat, animación)
{
"dil": "1.0",
"meta": { "name": "Demo Login", "description": "Prototipo de ejemplo de DIL 1.0" },
"theme": "default",
"breakpoints": { "compact": 0, "medium": 600, "expanded": 1024 },
"tokens": {
"color": {
"bg": "#F8FAFC", "surface": "#FFFFFF", "text": "#0F172A", "muted": "#64748B",
"border": "#E2E8F0", "danger": "#DC2626",
"brand": { "primary": "#4F46E5", "onPrimary": "#FFFFFF" }
},
"space": { "sm": 8, "md": 16, "lg": 24, "xl": 32 },
"radius": { "md": 10, "lg": 16 },
"font": { "base": "Inter, system-ui, sans-serif" },
"shadow": { "card": { "x": 0, "y": 4, "blur": 16, "color": "#0F172A1F" } },
"text": {
"title": { "fontFamily": "$font.base", "fontSize": 28, "fontWeight": 700, "lineHeight": "125%" },
"body": { "fontFamily": "$font.base", "fontSize": 14, "lineHeight": "150%" }
}
},
"themes": {
"dark": {
"extends": "default",
"tokens": {
"color": { "bg": "#0B1120", "surface": "#111827", "text": "#F8FAFC", "muted": "#94A3B8", "border": "#1F2937" }
}
}
},
"styles": {
"card": { "fill": "$color.surface", "radius": "$radius.lg", "shadow": "$shadow.card", "padding": "$space.lg" },
"input.default": {
"fill": "$color.surface", "border": "1 solid $color.border", "radius": "$radius.md",
"padding": "12 14", "color": "$color.text", "width": "fill",
"states": { "focus": { "borderColor": "$color.brand.primary" }, "invalid": { "borderColor": "$color.danger" } }
}
},
"state": {
"email": "",
"password": "",
"loading": false,
"dark": false
},
"data": {
"projects": [
{ "id": 1, "name": "Alfa", "progress": 0.72, "active": true },
{ "id": 2, "name": "Beta", "progress": 0.35, "active": true },
{ "id": 3, "name": "Gamma", "progress": 1.00, "active": false }
]
},
"animations": {
"shake": {
"duration": 300,
"keyframes": [
{ "at": 0, "set": { "translateX": 0 } },
{ "at": 0.25, "set": { "translateX": -8 } },
{ "at": 0.75, "set": { "translateX": 8 } },
{ "at": 1, "set": { "translateX": 0 } }
]
}
},
"components": {
"Button": {
"props": { "label": { "type": "string", "default": "Button" } },
"events": { "press": { "description": "Activación del botón" } },
"variants": [
{ "axis": "kind", "default": "primary", "options": {
"primary": { "$root": { "fill": "$color.brand.primary" }, "label": { "color": "$color.brand.onPrimary" } },
"outline": { "$root": { "fill": "transparent", "border": "1 solid $color.border" }, "label": { "color": "$color.text" } }
} }
],
"root": {
"type": "frame", "layout": "row", "justify": "center", "align": "center",
"padding": "12 20", "radius": "$radius.md",
"states": { "hover": { "opacity": 0.92 }, "pressed": { "opacity": 0.8 }, "disabled": { "opacity": 0.5 }, "loading": { "opacity": 0.7 } },
"transition": { "duration": 150 },
"a11y": { "role": "button", "label": "{{ props.label }}" },
"on": { "click": { "emit": "press" } },
"children": [ { "type": "text", "id": "label", "text": "{{ props.label }}", "fontWeight": 600 } ]
}
}
},
"start": "login",
"pages": [
{
"id": "login",
"name": "Login",
"frame": { "width": 390, "height": 844, "background": "$color.bg" },
"body": {
"type": "frame", "id": "loginBody", "width": "fill", "height": "fill",
"layout": "column", "justify": "center", "align": "stretch", "gap": 16, "padding": "$space.lg",
"color": "$color.text", "fontFamily": "$font.base",
"children": [
{ "type": "text", "text": "Bienvenida", "textStyle": "$text.title" },
{ "type": "input", "id": "emailField", "name": "email", "bind": "email", "variant": "default",
"inputType": "email", "placeholder": "tú@correo.com",
"validate": [
{ "rule": "required", "message": "El correo es obligatorio" },
{ "rule": "email", "message": "Formato inválido" }
] },
{ "type": "text", "text": "{{ form.email.error }}", "color": "$color.danger", "fontSize": 12,
"visible": "{{ form.email.error != null }}" },
{ "type": "input", "id": "passField", "name": "password", "bind": "password", "variant": "default",
"inputType": "password", "placeholder": "Contraseña",
"validate": [ { "rule": "minLength", "value": 6, "message": "Mínimo 6 caracteres" } ] },
{ "type": "text", "text": "{{ form.password.error }}", "color": "$color.danger", "fontSize": 12,
"visible": "{{ form.password.error != null }}" },
{ "type": "instance", "id": "submitBtn", "component": "Button",
"props": { "label": "{{ loading ? 'Entrando…' : 'Entrar' }}" },
"loading": "{{ loading }}",
"on": { "press": [
{ "validate": "form" },
{ "if": "form.valid",
"then": [
{ "set": { "loading": true } },
{ "wait": 700 },
{ "set": { "loading": false } },
{ "navigate": { "to": "dashboard", "transition": { "type": "push", "direction": "left", "duration": 250 } } }
],
"else": { "animate": "shake" } }
] } }
]
}
},
{
"id": "dashboard",
"name": "Dashboard",
"frame": { "width": 390, "height": 844, "background": "$color.bg" },
"body": {
"type": "frame", "width": "fill", "height": "fill", "layout": "column", "gap": 16, "padding": "$space.lg",
"color": "$color.text", "fontFamily": "$font.base", "scroll": "y",
"children": [
{ "type": "frame", "layout": "row", "justify": "between", "align": "center", "width": "fill", "children": [
{ "type": "text", "text": "Hola, {{ email }}", "textStyle": "$text.title", "fontSize": 20 },
{ "type": "switch", "id": "themeSwitch", "bind": "dark", "text": "Oscuro",
"a11y": { "label": "Cambiar tema" },
"on": { "change": { "setTheme": "{{ dark ? 'dark' : 'default' }}" } } }
] },
{ "type": "frame", "layout": "grid", "columns": 1, "gap": 16, "width": "fill",
"responsive": { "medium": { "columns": 2 }, "expanded": { "columns": 3 } },
"children": [
{ "type": "repeat",
"each": "data.projects", "as": "p", "key": "p.id",
"filter": "p.active",
"sort": { "by": "p.name" },
"template": {
"type": "frame", "style": "card", "layout": "column", "gap": 8,
"children": [
{ "type": "text", "text": "{{ p.name }}", "fontWeight": 600, "fontSize": 16 },
{ "type": "text", "text": "Progreso: {{ formatPercent(p.progress) }}", "color": "$color.muted" },
{ "type": "frame", "width": "fill", "height": 8, "radius": 4, "fill": "$color.border", "clip": true,
"children": [
{ "type": "frame", "height": 8, "width": "{{ (p.progress * 100) + '%' }}", "fill": "$color.brand.primary" }
] }
]
},
"empty": { "type": "text", "text": "Sin proyectos activos", "color": "$color.muted" } }
] },
{ "type": "instance", "component": "Button", "props": { "label": "Cerrar sesión" },
"variants": { "kind": "outline" },
"on": { "press": [ { "set": { "password": "" } }, { "back": true } ] } }
]
}
}
]
}Notas del ejemplo: (1)
variant: "default"eninputresuelve al estiloinput.default; (2)width: "{{ (p.progress * 100) + '%' }}"falla de tipo porque+mezclanumberystring; la forma correcta es"{{ toString(p.progress * 100) + '%' }}". Este tipo de error es exactamente el que detecta el pase P3 (DIL-E011).
C.3 Corrección del ejemplo anterior (barra de progreso)
{ "type": "frame", "height": 8, "width": "{{ toString(p.progress * 100) + '%' }}", "fill": "$color.brand.primary" }C.4 Absoluto (mockup de lienzo libre)
{
"type": "frame", "width": 320, "height": 200, "layout": "none", "fill": "#FFFFFF", "radius": 12, "clip": true,
"children": [
{ "type": "image", "src": "hero", "x": 0, "y": 0, "right": 0, "height": 120, "fit": "cover", "alt": "Portada" },
{ "type": "text", "text": "Título", "x": 16, "y": 136, "fontSize": 18, "fontWeight": 700 },
{ "type": "button", "text": "Ver", "right": 16, "bottom": 16, "variant": "primary" }
]
}Apéndice D — Checklist de implementación
Orden recomendado para construir el ecosistema (según el plan de DIL 1.0):
- [ ]
DIL 1.0 Specification(este documento) - [ ]
dil-1.0.schema.jsongenerado desde el Apéndice A - [ ] Parser + Normalizador (§3.3–§3.4; idempotente; ids automáticos)
- [ ] Validador P1–P4 con el catálogo de §25.3
- [ ] Evaluador de expresiones (§17) con tipos estáticos y pruebas de la biblioteca estándar
- [ ] Motor de cascada (§14) y resolución de valores (§6, §7)
- [ ] Motor de layout flex/grid/absoluto (§9) con pruebas dorado (golden tests)
- [ ] Runtime: estado, reactividad, acciones, navegación, overlays, animaciones (§18–§20, §24)
- [ ] Editor: arrastrar y soltar ⇒ DIL; panel de propiedades con
states/responsive/variantes; metadatos eneditor - [ ] Compilador (React/HTML) con tabla de características soportadas
- [ ] Suite de conformidad: documentos válidos/ inválidos con diagnósticos esperados (código + ruta)
Pruebas mínimas de conformidad (ejemplos):
| Caso | Resultado esperado |
|---|---|
{"dil":"1.0","pages":[]} | DIL-E003 (pages ≥ 1) |
Dos nodos con id: "a" en la misma página | DIL-E004 |
"fill": "$color.nope" | DIL-E006 |
"padding": "$color.primary" | DIL-E007 |
Tokens a → b → a | DIL-E008 |
Componente A contiene instance de A | DIL-E009 |
"if": "email ==" | DIL-E010 |
"width": "{{ 'a' + 1 }}" | DIL-E011 |
{"set": {"props.label": "x"}} | DIL-E012 |
Hijo absoluto con x, right y width | DIL-E013 |
Acción con {"set": {}, "navigate": "x"} | DIL-E018 |
{"fetch": {...}} | DIL-E018 (verbo reservado) |
image sin alt | DIL-W001 |
Eliminar todas las claves editor | El comportamiento NO cambia (§23) |
normalize(normalize(d)) | == normalize(d) |
Fin de la especificación DIL 1.0.