Documentación viva

DIL 1.0

Especificación completa de Declarative Interface Language: estructura, estilos, layout, estado, interacciones, runtime y validación.

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.

CampoValor
Versión de la especificación1.0
Formato de serializaciónJSON (UTF-8, perfil I-JSON, RFC 7493)
Extensión de archivo.dil.json (alternativa: .dil)
Media typeapplication/dil+json
Campo de versión en el documento"dil": "1.0"
Lenguaje de la especificaciónEspañol (identificadores y claves en inglés)

Tabla de contenidos

  1. Convenciones
  2. Introducción
  3. Conformidad
  4. Modelo de documento
  5. Tipos primitivos
  6. Identificadores, ámbitos y referencias
  7. Resolución de valores
  8. Tokens y temas
  9. Nodos
  10. Layout
  11. Estilo visual
  12. Tipografía y texto
  13. Controles y formularios
  14. Componentes
  15. Cascada y precedencia
  16. Responsive
  17. Estado, datos y bindings
  18. Lenguaje de expresiones
  19. Eventos y acciones
  20. Páginas, navegación, overlays y flujos
  21. Transiciones y animaciones
  22. Accesibilidad
  23. Assets
  24. Metadatos de editor
  25. Modelo de ejecución (runtime)
  26. Validación y diagnósticos
  27. Seguridad
  28. Compatibilidad y hoja de ruta
  29. Apéndice A — Tipos TypeScript
  30. Apéndice B — JSON Schema
  31. Apéndice C — Ejemplos completos
  32. 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:

PalabraSignificado
DEBE / NO DEBERequisito absoluto.
DEBERÍA / NO DEBERÍARecomendación fuerte; puede ignorarse solo con razones justificadas.
PUEDEOpcional.

0.2 Notación

  • Los bloques json son normativos cuando se presentan como ejemplo de sintaxis válida, salvo que se indique // inválido.
  • T? significa propiedad opcional. T | U significa unión.
  • Dynamic<T> significa que el valor puede ser un literal T, 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; x crece a la derecha, y crece 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.

text
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 + PROTOTIPO

1.2 Objetivos

  1. Diseño visual completo: desde mockups sencillos hasta diseños de alta fidelidad (layout, tipografía, color, sombras, gradientes, temas, componentes).
  2. Prototipos programables: estado, bindings, validación, condiciones, navegación, overlays, transiciones y animaciones, sin código arbitrario.
  3. Fácil de leer y escribir: lo frecuente es corto; lo avanzado es opcional (progressive complexity).
  4. Validable de forma estática: todo documento puede comprobarse (estructura, tipos, referencias, expresiones) sin ejecutarlo.
  5. Generable por máquinas: determinista, sin ambigüedad, apto para IA y para editores visuales.
  6. 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

#PrincipioConsecuencia
P1Simple cosas simples{ "type": "button", "text": "Save" } es un documento válido.
P2Complejidad progresivaSe añade variant, states, on, animation… solo cuando se necesitan, sin cambiar la estructura.
P3Lógica declarativa, no códigoExpresiones puras sin efectos + un conjunto cerrado de acciones.
P4Una forma canónicaLos atajos (shorthands) se normalizan a una única representación.
P5Runtime ≠ EditorLos metadatos del editor viven aparte y se pueden eliminar sin cambiar el comportamiento.
P6DeterminismoMisma entrada + mismos eventos ⇒ mismo resultado. El orden de claves JSON no es significativo.
P7Conceptos visualesUn diseñador debe poder leerlo como un árbol de capas (Frame → hijos).

1.5 Arquitectura

text
                         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
text
                         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:

json
{ "type": "button", "text": "Save" }

Intermedio — estilo e interacción:

json
{
  "type": "button",
  "text": "Save",
  "variant": "primary",
  "on": { "click": "save" }
}

Avanzado — estados, lógica, animación:

json
{
  "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

ClaseRequisitos
Documento DILCumple §3–§23 y supera la validación estructural y semántica (§25) sin errores.
Parser/NormalizadorImplementa §3.3–§3.4 y produce la forma canónica.
ValidadorImplementa los 4 pases de §25 y emite diagnósticos con los códigos de §25.3.
RuntimeImplementa §6, §14–§20, §24. Ignora editor (§23).
EditorLee/escribe documentos válidos, preserva las claves desconocidas x-* y editor.
CompiladorTransforma un documento válido a un destino; DEBE documentar qué features no soporta.

2.2 Modos de procesamiento

ModoPropiedades desconocidasFeatures futuras (dil minor mayor)Uso
strict (por defecto)Error DIL-E003Error DIL-E002CI, exportación, generación por IA
lenientAviso DIL-W007, se ignoranAviso, se ignoranEdició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 de validate).

3.2 Objeto raíz (Document)

json
{
  "dil": "1.0",
  "meta":        { },
  "theme":       "default",
  "breakpoints": { },
  "tokens":      { },
  "themes":      { },
  "styles":      { },
  "assets":      { },
  "components":  { },
  "state":       { },
  "computed":    { },
  "data":        { },
  "actions":     { },
  "animations":  { },
  "pages":       [ ],
  "flows":       [ ],
  "start":       "home",
  "editor":      { }
}
ClaveTipoObligatoriaDescripción
dil"1.0"SíVersión (§3.6).
pagesPage[]Sí (≥ 1)Pantallas y overlays (§19).
metaMetaNoNombre, autor, descripción…
themestringNoTema activo inicial. Por defecto "default" (§7.3).
breakpointsRecord<Id, number>NoPuntos de quiebre (§15).
tokensTokensNoVariables de diseño (§7).
themesRecord<Id, Theme>NoSobrescrituras de tokens por tema.
stylesRecord<Id, StylePatch>NoEstilos con nombre reutilizables (§8.5).
assetsRecord<Id, Asset>NoImágenes, SVG, fuentes (§22).
componentsRecord<Id, Component>NoDefiniciones de componentes (§13).
stateRecord<Name, StateDecl | literal>NoEstado global (§16).
computedRecord<Name, Expr>NoValores derivados globales.
dataRecord<Name, JSON>NoDatos mock de solo lectura (§16.5).
actionsRecord<Id, ActionSpec>NoAcciones con nombre (§18).
animationsRecord<Id, Animation>NoAnimaciones con nombre (§20).
flowsFlow[]NoFlujos de prototipo (§19.6).
startIdNoPágina inicial. Por defecto, la primera de pages.
editorEditorMetaNoSolo para el editor (§23).

Separación Runtime / Editor (normativo):

text
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:

text
 ① 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/Render

Las 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": 16paddingTop/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": 8radiusTopLeft/TopRight/BottomRight/BottomLeft: 8
"radius": "8 0"TL/BR 8, TR/BL 0
"gap": 16rowGap: 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.2scaleX: 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 idse asigna id automático determinista (auto:<ruta>)
"state": { "count": 0 }"state": { "count": { "type": "number", "default": 0 } }
"states": { "hover": {…} } en children implícitosin cambios
"children": [..] en instance"slots": { "default": [..] }

Reglas:

  1. La normalización DEBE ser idempotente: normalize(normalize(x)) == normalize(x).
  2. 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 ⇒ top 8, resto 16).
  3. Un IDs automático (auto:…) NO PUEDE ser referenciado desde acciones, overrides ni animaciones; para referenciar un nodo, este DEBE tener id explí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 strict y aviso en lenient (§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.N DEBE aceptar documentos 1.M con M ≤ N. Con M > 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.

TipoDefiniciónEjemplos válidosInválidos
NumberNúmero JSON finito0, -3.5"3", NaN
IntegerEntero33.2
Booleantrue / falsetrue"true"
StringCadena 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"
LengthNumber ≥ 0 salvo que se indique (dp)16"16px"
Percentage^-?\d+(\.\d+)?%$"50%", "100%""50"
FractionNumber en [0,1]0.51.2
OpacityFraction0.82
AngleNumber (grados)90, -45"90deg"
DurationNumber ≥ 0 (ms)150"150ms"
ColorVer §4.1"#4F46E5""blue"
SizeLength | Percentage | "auto" | "fit" | "fill"320, "fill""large"
SpacingLength | 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"
MustacheCadena que contiene {{ … }} (§6)"{{ count + 1 }}"—
ExprExpresión sin llaves (§17)"email != ''"—
EasingVer §20.1"ease-out", [0.2,0,0,1]—
Anchor"top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right""center"—
AssetRefId de un asset del documento, o Mustache"logo"—

4.1 Color

Un color es una cadena con una de estas formas:

FormaGramáticaEjemplo
Hex#RGB #RGBA #RRGGBB #RRGGBBAA"#4F46E5", "#4F46E5CC"
RGBrgb(R G B) / rgb(R G B / A) · R,G,B ∈ 0..255, A ∈ 0..1"rgb(79 70 229 / 0.8)"
HSLhsl(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

ValorSignificado
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

RegistroFormaÁmbito de unicidad
pagesarray (id en cada página)Documento
components, assets, styles, actions, animations, themesmapa (clave = id)Documento
flowsarrayDocumento
Nodos de una páginaidEsa página (su body)
Nodos de un componenteidEsa definición de componente
Variables de estado / propsNameSu ámbito (§16.2)
  • Dos registros diferentes PUEDEN usar el mismo id (p. ej. un componente Home y una página Home), porque las referencias son tipadas por contexto.
  • Dentro de un mismo registro/ámbito, DEBE ser único (DIL-E004).
  • id de nodos sin repeat: estable. En repeat, 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 /:

text
"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 componente

5.3 Referencias

ReferenciaApunta aError si falla
component: "Button"components.ButtonDIL-E005
navigate: "dashboard" / start / flows[].startpages[].idDIL-E005
"style": "card"styles.cardDIL-E005
"src": "logo"assets.logoDIL-E005
{ "call": "save" } / "on": {"click":"save"}actions.saveDIL-E005
{ "animate": "shake" }animations.shakeDIL-E005
"$color.primary"tokens.color.primaryDIL-E006
target: "emailField"nodo con ese id en la página/componenteDIL-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

text
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
        ▼
     literal

Equivalencia 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

  1. 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.
  2. Las expresiones se reevalúan solo cuando cambian sus dependencias (§24.3).
  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.
  4. {{ }} NO PUEDE anidarse dentro de otra expresión.
  5. 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íaTipo de valorAceptado en propiedades
colorColorfill(solid), color, borderColor, stroke, sombra color, stops
spaceLengthpadding*, margin*, gap, rowGap, columnGap
sizeLengthwidth, height, min*, max*, fontSize, strokeWidth, borderWidth*
radiusLengthradius*
opacityFractionopacity
durationDurationduration, delay
easingEasingeasing
fontString (familia)fontFamily
textTextStyle (compuesto)textStyle
shadowShadow | Shadow[]shadow
gradientGradientfill (como paint)

Usar un token en una propiedad cuya categoría no acepta ⇒ DIL-E007.

7.2 Declaración

json
{
  "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 es categorí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:

json
{
  "theme": "default",
  "themes": {
    "dark": {
      "extends": "default",
      "tokens": {
        "color": { "text": "#F8FAFC", "surface": "#0B1120" }
      }
    }
  }
}

Resolución de un token (tema activo T):

text
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-E006 si 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ón setTheme (§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.

text
Page
 └─ body: frame
      ├─ text
      ├─ frame
      │    ├─ image
      │    └─ button
      └─ repeat ─ template: frame

8.2 Catálogo de tipos de nodo

typeCategoríaHijosDescripción
frameContenedorSíContenedor con layout, estilo visual y clip. Unidad básica de diseño.
groupContenedorSíAgrupación lógica sin layout propio ni estilo visual (solo transformaciones/opacidad). Su caja = bounding box de los hijos.
textContenidoNoTexto (§11).
imageContenidoNoImagen de un asset (§22).
iconContenidoNoIcono vectorial.
shapeContenidoNoForma vectorial: rect, ellipse, line, polygon, star, path.
buttonControlNoBotón (§12).
inputControlNoCampo de texto de una línea.
textareaControlNoCampo de texto multilínea.
checkboxControlNoCasilla.
radioControlNoOpción de grupo exclusivo.
switchControlNoInterruptor.
selectControlNoLista desplegable.
sliderControlNoDeslizador numérico.
instanceComponenteNo¹Instancia de un componente (§13).
slotComponenteSí²Punto de inserción dentro de un componente (§13.6).
repeatEstructuralNoRepetició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)

PropiedadTipoDefectoNotas
typeuno de §8.2— (obligatoria)Discriminante.
idIdautoÚnico en su ámbito (§5.1).
nameString—Etiqueta humana (capas del editor). No afecta al runtime.
visibleDynamic<Boolean>truefalse ⇒ no se renderiza ni ocupa layout.
opacityDynamic<Opacity>1Se multiplica con la de los ancestros.
disabledDynamic<Boolean>falseDesactiva eventos de usuario en el subárbol y activa el estado disabled.
selectedDynamic<Boolean>falseActiva el estado selected.
loadingDynamic<Boolean>falseActiva el estado loading.
styleId | Id[]—Estilos con nombre (§8.5).
statesRecord<StateName, StatePatch>—Parches por estado (§13.4).
responsiveRecord<BreakpointName, StylePatch>—Parches por breakpoint (§15).
transitionTransition—Interpolación al cambiar valores (§20.2).
animationAnimationRef—Animación automática (§20.3).
onRecord<EventName, ActionSpec>—Eventos (§18).
a11yA11y—Accesibilidad (§21).
editorEditorMeta—Solo editor (§23).
Propiedades de ítem de layout§9position, x, y, width, grow, col…
Propiedades de transformación§9.8rotate, 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).

json
{
  "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):

json
{
  "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 StylePatch admite cualquier propiedad visual, de layout, de texto o de ítem excepto id, type, children, on, style.
  • Un nodo con "style": ["a", "b"] aplica a y luego b (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 en styles. Si no existe, el runtime PUEDE usar su apariencia por defecto (no normativa) y el validador emite DIL-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

PropiedadTipoNotas
kind"rect" | "ellipse" | "line" | "polygon" | "star" | "path"Obligatoria.
dStringSolo path: datos de trayecto SVG (M … Z).
points[number, number][]Solo polygon (≥ 3).
sidesInteger ≥ 3Solo polygon regular (alternativa a points).
ratioFractionSolo star: radio interior/exterior.
fillPaint | Paint[]§10.1
strokeDynamic<Color>Color del trazo.
strokeWidthDynamic<Length>Defecto 1.
strokeCap"butt" | "round" | "square"Defecto "butt".
strokeJoin"miter" | "round" | "bevel"Defecto "miter".
strokeDashLength[]Patrón de guiones.

rect usa radius* (§10.3). line va de (0,0) a (width,height) del nodo.

8.7 repeat (colecciones)

json
{
  "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" }
}
PropiedadTipoDefectoDescripción
eachExpr → Array | Integer— (obligatoria)Colección; un entero n repite n veces (índices 0..n-1).
asName"item"Variable local del elemento.
indexName"index"Variable local del índice (0-based) tras filter/sort/limit.
keyExpríndiceIdentidad estable del elemento (único; DIL-E004 en runtime si se repite).
filterExpr → Boolean—Se aplica antes de sort.
sort{ by: Expr, order?: "asc" | "desc" }—by se evalúa por elemento.
limitExpr → Integer—Máximo de elementos tras filter/sort.
templateNode— (obligatoria)Nodo que se instancia por elemento.
emptyNode—Se muestra si el resultado está vacío.
  • repeat es transparente al layout: sus instancias se insertan como hijos del padre, en la posición del repeat.
  • as e index son solo lectura y visibles solo dentro de template.
  • 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:

ValorModoHijos
"none" (alias "absolute")LibreTodos posicionados de forma absoluta (§9.5). Es el defecto (modelo de lienzo).
"flex"FlexboxFlujo 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)TipoDefectoDescripción
direction"row" | "column" | "row-reverse" | "column-reverse""row"Eje principal.
wrapBooleanfalsePermitir varias líneas.
gap, rowGap, columnGapDynamic<Length>0Separació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)TipoDefectoDescripción
growNumber ≥ 00 (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):

  1. 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 en 0 y participan en el reparto.
  2. espacioLibre = tamañoPrincipalDisponible − Σ tamañosBase − Σ gaps.
  3. Si espacioLibre > 0: se reparte entre hijos con grow > 0 proporcionalmente a grow; cada uno respeta minWidth/maxWidth (o Height). Si no hay hijos con grow, se aplica justify.
  4. Si espacioLibre < 0: ningún hijo se encoge; el contenido desborda (visible, clip u scroll según §9.7).
  5. Eje transversal: cada hijo usa alignSelf (o align). Con stretch, un hijo cuyo tamaño transversal es "auto" o "fill" ocupa todo el espacio transversal de la línea.
  6. wrap: true crea líneas nuevas cuando la suma de tamaños base + gaps excede el espacio principal; rowGap/columnGap separan líneas y elementos.
  7. justify: "between" | "around" | "evenly" solo aplica si no hay hijos grow > 0.

9.3 Grid

Propiedad (contenedor)TipoDefectoDescripción
columnsInteger | Track[]1Pistas de columna. 3 ≡ ["1fr","1fr","1fr"].
rowsInteger | Track[]autoPistas de fila explícitas; las filas extra son "auto".
flow"row" | "column""row"Sentido de colocación automática.
gap, rowGap, columnGapLength0
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)TipoDefectoDescripción
col, rowInteger ≥ 1autoPosición (1-based) de la celda inicial.
colSpan, rowSpanInteger ≥ 11Pistas 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

PropiedadTipoDefecto
width, heightDynamic<Size>"auto"
minWidth, maxWidth, minHeight, maxHeightDynamic<Length>sin límite
aspectRatioNumber > 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).

PropiedadTipoDescripció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.
rightDynamic<Number>Distancia desde el borde derecho del padre.
bottomDynamic<Number>Distancia desde el borde inferior del padre.
centerXDynamic<Number | true>Centra horizontalmente; el número es el desplazamiento desde el centro. true ≡ 0.
centerYDynamic<Number | true>Ídem en vertical.
zIndexIntegerOrden 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 declaradaResultado
xAnclado a la izquierda, ancho = width.
rightAnclado a la derecha.
x + rightEstirado: ancho = anchoPadre − x − right (se ignora width; si se declara width ⇒ DIL-E013).
centerXCentrado (+ desplazamiento).
x + centerX, o right + centerXInválido (DIL-E013).
ningunax = 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 con Percentage en x/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 de flex/grid. No colapsa. Se ignora en absolutos.
  • Los tamaños width/height son del border-box (incluyen padding y borde); es decir, box-sizing: border-box implícito.
text
┌───────────────── margin ─────────────────┐
│ ┌────────────── border-box ─────────────┐ │
│ │  border                               │ │
│ │ ┌───────────── padding ─────────────┐ │ │
│ │ │  ┌─────── content (hijos) ──────┐ │ │ │
│ │ │  │                               │ │ │ │
│ │ │  └───────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└────────────────────────────────────────────┘

9.7 Desbordamiento

PropiedadTipoDefectoDescripción
clipBooleanfalseRecorta 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).

PropiedadTipoDefecto
rotateDynamic<Angle>0
scaleX, scaleY (atajo scale)Dynamic<Number>1
translateX, translateYDynamic<Number>0
originAnchor"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.typeCampos
"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.

json
{
  "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

PropiedadTipoDefecto
borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidthDynamic<Length>0
borderColorDynamic<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

json
{ "shadow": [ { "x": 0, "y": 4, "blur": 16, "spread": 0, "color": "#0F172A1F", "inset": false } ] }
Campo de ShadowTipoDefecto
x, yNumber0
blurLength0
spreadNumber0
colorDynamic<Color>"#00000040"
insetBooleanfalse
PropiedadTipoDescripción
shadowShadow | Shadow[]Varias sombras; la primera queda encima.
blurLengthDesenfoque del propio nodo (layer blur).
backdropBlurLengthDesenfoque 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:

json
{
  "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

json
{ "type": "text", "text": "Hola, {{ name }}", "textStyle": "$text.title", "color": "$color.text" }
PropiedadTipoDefectoDescripción
textDynamic<String>""Contenido (texto plano; nunca se interpreta como marcado).
runsRun[]—Texto enriquecido; excluyente con text (DIL-E003).
textStyleTokenRef (text) o Id—Estilo tipográfico compuesto. Las propiedades explícitas lo sobrescriben.
fontFamilyDynamic<String>"system-ui"Familia (o lista separada por comas, con fallback).
fontSizeDynamic<Length>14
fontWeightDynamic<100..900 | "normal" | "bold">400normal=400, bold=700.
fontStyle"normal" | "italic""normal"
lineHeightDynamic<Length | Percentage | "auto">"auto""150%" = 1.5 × fontSize.
letterSpacingDynamic<Length | Percentage>0Porcentaje relativo a fontSize.
colorDynamic<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"
maxLinesInteger ≥ 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:

  • lineHeight acepta una longitud o un porcentaje. Por ejemplo, "145%" equivale a 1.45 × fontSize y "24px" fija una altura de línea.
  • letterSpacing acepta una longitud o un porcentaje. El número 0.5 representa 0.5 dp; "2%" es relativo al tamaño de fuente.
  • textTransform conserva none, uppercase, lowercase o capitalize.

Ejemplo de un token reutilizable con configuración avanzada:

json
{
  "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…).

ControlPropiedades específicas
buttontext, icon?: AssetRef|String, iconPosition?: "start"|"end" ("start"), variant?: String, buttonType?: "button"|"submit" ("button")
inputinputType?: "text"|"email"|"password"|"number"|"search"|"tel"|"url" ("text"), placeholder?, maxLength?, readOnly?, autofocus?
textareaplaceholder?, rows?: Integer (3), maxLength?, readOnly?, autofocus?
checkboxtext? (etiqueta), indeterminate?: Dynamic<Boolean>
radiotext?, value: Dynamic<any> (obligatorio), groupName: Name (obligatorio)
switchtext?
selectoptions: Option[] | Mustache, placeholder?
slidermin (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

json
{ "type": "input", "name": "email", "bind": "email", "placeholder": "tú@correo.com" }
  • bind es 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 (input con inputType: "number" → number); checkbox/switch → boolean; radio → el value del radio seleccionado; select → value de la opción; slider → number. Desajuste con el tipo declarado ⇒ DIL-E020.
  • Para estado solo lectura (sin escribir), usar expresiones en text/value en vez de bind.

12.3 Eventos de controles

EventoSe dispara cuando
inputCada cambio de valor del usuario (pulsación).
changeEl valor se confirma (blur/Enter/soltar).
focus / blurGana/pierde foco.
submitbutton con buttonType: "submit" pulsado, o Enter en input.
clickActivació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.

json
{
  "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" }
ruleCamposPasa si
required—Valor no null, no "" (tras trim), no [], y para checkbox/switch es true.
minLength / maxLengthvalue: Integerlength(valor) ≥ / ≤ value (si vacío y no required ⇒ pasa).
min / maxvalue: NumberValor numérico ≥ / ≤ value.
email—Coincide con un formato de correo razonable (RFC 5322 simplificado). Vacío ⇒ pasa.
patternvalue: String (regex, §17.5)Coincide por completo. Vacío ⇒ pasa.
oneOfvalue: any[]El valor está en la lista.
exprexpr: Expr → BooleanSe 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ónTipoDescripción
form.validbooleantrue si todos los campos cumplen sus reglas (siempre actual).
form.errorsRecord<Name, string[]>Mensajes de reglas fallidas por campo.
form.submittedbooleanSe ejecutó validate o submit al menos una vez.
form.<name>.valueanyValor actual.
form.<name>.validbooleanCumple todas sus reglas.
form.<name>.errorsstring[]Mensajes fallidos (actuales).
form.<name>.errorstring | nullPrimer mensaje si el campo fue tocado o form.submitted; si no, null.
form.<name>.touchedbooleanHa recibido y perdido foco.
form.<name>.dirtybooleanEl 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

json
{
  "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 ComponentTipoObligatorioDescripción
rootNodeSíNodo raíz.
name, descriptionStringNoDocumentación.
propsRecord<Name, PropDecl>NoInterfaz pública (§13.2).
stateRecord<Name, StateDecl>NoEstado interno de cada instancia (§16.2).
computedRecord<Name, Expr>NoDerivados internos.
eventsRecord<Id, { description?, payload? }>NoEventos personalizados que puede emitir (§18.4 emit).
variantsVariantAxis[]NoEjes de variante (§13.3).
compoundVariantsCompoundVariant[]NoCombinaciones (§13.3).

13.2 Props (PropDecl)

CampoTipoDescripción
typever tablaObligatorio.
defaultvalorValor si la instancia no lo da.
requiredBooleanSi true, la instancia DEBE darlo (DIL-E017). Incompatible con default.
optionsany[]Solo type: "enum".
descriptionString
typeValores
string number boolean colorprimitivos / Color
enumuno de options
iconAssetRef o "provider:name"
imageAssetRef
array object anyJSON; any desaconsejado
actionActionSpec (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.

  • variants es 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 usa default.
  • Un eje cuya opción no existe ⇒ DIL-E016.
  • Un OverrideMap mapea id de nodo (o "$root") a un `StylePatch` (§8.5; incluye states/responsive, pero no children).
  • Orden de aplicación: ejes en el orden del array (el último eje gana en caso de conflicto), y luego compoundVariants en orden.
  • compoundVariants[].when es 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:

json
{ "states": { "hover": { "fill": "$color.hover" }, "disabled": { "opacity": 0.4 } } }
EstadoSe activa cuandoFuente
hoverEl puntero está sobre el nodoRuntime
focusEl nodo (o un descendiente) tiene el focoRuntime
focusVisibleFoco por tecladoRuntime
pressedPuntero/tecla presionados sobre el nodoRuntime
selectedselected es truePropiedad
checkedControl checkbox/radio/switch marcadoControl
invalidEl campo falla validate y form.<name>.error != nullControl
loadingloading es truePropiedad
disableddisabled 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):
text
hover < focus < focusVisible < selected < checked < pressed < invalid < loading < disabled
  • StatePatch = StylePatch & { within?: OverrideMap }. within aplica parches a descendientes (claves = ids relativos al nodo) cuando el estado está activo; así hover del root puede cambiar el color de un hijo.
  • Un nodo disabled no dispara eventos de usuario ni recibe hover/pressed/focus.
  • Los estados son por nodo; un estado en un hijo no se activa por hover del padre (usar within en el padre).

13.5 Instancias

json
{
  "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" }
}
CampoTipoDescripción
componentIdObligatorio.
propsRecord<Name, Dynamic<any>>Valores de props. Desconocidas ⇒ DIL-E017.
variantsRecord<Name, Dynamic<String>>Selección de variantes.
overridesOverrideMapParches sobre nodos internos (§5.2 para rutas).
slotsRecord<Id, Node | Node[]>Contenido de slots (§13.6).
Propiedades StylePatchAplicadas al root del componente (equivalente a overrides.$root). Típico: x, y, width, grow, col, visible, disabled.
onEventos 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, usar slots.

13.6 Slots

json
{
  "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:

json
{ "type": "instance", "component": "Card", "slots": {
    "header":  { "type": "text", "text": "Resumen", "textStyle": "$text.title" },
    "default": [ { "type": "text", "text": "Contenido…" } ]
} }
  • slot.name es Id; "default" es el slot por defecto. instance.children ≡ slots.default.
  • Los children de un slot son el contenido de reemplazo (fallback) si la instancia no aporta ese slot.
  • Un slot es transparente al layout (como repeat): 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 slots de 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.

text
 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

  1. Granularidad: la unión se hace por propiedad canónica. paddingTop en una capa alta no altera paddingLeft definido en una baja. Las propiedades de lista (fill, shadow, validate, strokeDash, stops) se reemplazan completas, no se mezclan.
  2. `states` y `responsive` definidos en styles y en el nodo se mezclan por clave y luego por propiedad (el nodo gana).
  3. 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).
  4. Los valores de capas superiores pueden ser Dynamic; la cascada elige primero el valor crudo y después se resuelve (§6).
  5. Un valor null explícito en una capa elimina el valor de las capas inferiores y vuelve al defecto de la propiedad.
  6. L0 (herencia) solo aplica a propiedades heredables y solo si ninguna otra capa define la propiedad en el nodo.

14.2 Ejemplo resuelto

text
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 cambiarlo

15. Responsive

15.1 Breakpoints

json
{ "breakpoints": { "compact": 0, "medium": 600, "expanded": 1024 } }
  • Mapa Id → ancho mínimo en 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

json
{
  "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

json
{
  "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 StateDeclTipoDescripción
type"string" | "number" | "boolean" | "object" | "array"Obligatorio (inferido en la forma corta literal).
defaultDynamic<any>Valor inicial; se evalúa una vez al crear el ámbito.
enumany[]Valores permitidos (opcional).
descriptionString
  • Forma corta: "count": 0 ⇒ type: number, default: 0. Los literales null, {} 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

ÁmbitoDeclarado enVidaVisible en
Documentostate / computed raízSesión del prototipoTodo
Páginapage.state / page.computedMientras la página esté en la pila de navegaciónSu subárbol y sus overlays abiertos desde ella
Componentecomponent.stateCada instancia tiene su copiaDentro del componente
Local de repeatrepeat.as, repeat.indexUna instancia de la plantillaDentro de template
Local de eventoeventDurante el manejo de un eventoAcciones y expresiones de ese manejador

Resolución de nombres (de más interno a más externo; el interno oculta al externo):

text
locales de repeat → props del componente (props.x) y estado del componente
→ estado/params de página → estado de documento → integradas

Variables 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:

text
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 Dynamic con {{ … }} es un binding reactivo.
  • Escritura: controles con bind y acciones set/toggle/append/remove.
  • El grafo de dependencias DEBE ser acíclico; un ciclo entre computed/default ⇒ DIL-E008.

16.5 Datos mock

json
{
  "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" }
  }
}
  • data es inmutable y de solo lectura; accesible como data.<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)

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ónRegla
== / !=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.
TOKENValor 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.

GrupoFunciones
Númerosabs(n), min(a,b,…), max(a,b,…), round(n, d=0), floor(n), ceil(n), clamp(n, lo, hi), mod(a,b)
Textolength(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)
Listascount(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)
Objetoskeys(o), values(o), has(o,key)
TiposisNull(v), isEmpty(v) (null, "", [], {}), toString(v), toNumber(v) (inválido ⇒ null), toBoolean(v) (solo "true"/"false"/booleano)
FormatoformatNumber(n, decimals=0), formatCurrency(n, code), formatPercent(f, decimals=0), formatDate(ts, pattern), plural(n, one, other)
Coloralpha(color, a), mix(c1, c2, t), lighten(c, t), darken(c, t)
Entornonow() (ms epoch; no determinista, §24.5)
  • filter(a, key, value) filtra elementos cuyo key (nombre de propiedad, cadena) sea igual a value. Para predicados complejos usar repeat.filter.
  • formatDate usa patrón yyyy-MM-dd HH:mm:ss y 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)

CampoTipoForma
if (acción)Expr → booleanbare: "if": "email != ''"
repeat.each, .key, .filter, .limit, .sort.byExprbare
computed.*Exprbare
validate[].exprExpr → booleanbare
remove.where, append.ifExprbare
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.

EventoAplica aDescripciónDatos en event
clickTodosClic/tap/Enter/Espaciox, y
doubleClickTodosDoble clicx, y
longPressTodosPulsación ≥ 500 msx, y
pointerEnter / pointerLeaveTodosPuntero entra/salex, y
pointerDown / pointerUpTodosx, y
focus / blurInteractivos—
input / changeControles§12.3value
submitbutton, inputvalue
keyDown / keyUpInteractivos y páginakey, shift, ctrl, alt, meta
scrollframe con scrollscrollX, scrollY
visibleTodosEntra al viewport (una vez por montaje)—
load / unloadNodos y páginasMonta/desmonta—
resizePáginaCambia el viewportwidth, height
Eventos de componenteinstanceLos declarados en component.eventspayload
  • Un evento desconocido ⇒ DIL-E019.
  • Variable event accesible en expresiones del manejador (campos arriba).
  • Nodos disabled no 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

text
ActionSpec := String            -- referencia a actions[<id>]  (≡ { "call": id })
            | Action
            | ( String | Action )[]    -- secuencia, ejecución en orden

18.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.

json
{ "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

VerboValorEfecto
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).
togglePathInvierte 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.
ifExpr + then + else? (ActionSpec)Condicional.
parallelAction[]Ejecuta a la vez (para animate/wait coordinados); finaliza al terminar todas.
waitDuration (o Dynamic<Duration>)Pausa la secuencia.
callIdEjecuta actions[id]. Recursión directa/indirecta ⇒ DIL-E009.
navigateId | { to: Id, params?, transition?, replace?: Boolean }Cambia de página (§19.3).
backtrue | { transition? }Vuelve en la pila.
openId | { overlay: Id, anchor?: Id, params? }Abre una página overlay (§19.4).
closetrue | IdCierra el overlay superior o el indicado.
animateId | { ref: Id, target?: Id, await?: Boolean }Reproduce animations[ref]. target por defecto: el nodo que originó el evento.
setThemeDynamic<String>Cambia el tema activo.
focusId (nodo)Mueve el foco.
scrollToId | { 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.
emitId | { event: Id, payload?: Dynamic<any> }Dentro de un componente: dispara un evento declarado en component.events, manejado por instance.on.
runNameDentro 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

  1. Una secuencia se ejecuta en orden. wait, animate con await: true y navigate/open con transición suspenden la secuencia hasta terminar.
  2. Los cambios de estado de un set se aplican de forma atómica respecto a las expresiones reactivas (una sola actualización por "tick", §24.3).
  3. Un error de acción (destino inexistente, tipo inválido) detiene la secuencia actual, emite DIL-R003 y deja intacto el estado ya aplicado.
  4. Cada evento inicia su propia secuencia; dos secuencias pueden solaparse (se serializan sobre el estado por orden de llegada).
  5. Límite: ≤ 1 000 acciones por secuencia y profundidad de call ≤ 16 (DIL-R002).
  6. if evalúa Expr; condición no booleana ⇒ error de evaluación ⇒ se toma else.

18.6 Ejemplos

json
{ "on": { "click": { "set": { "count": "{{ count + 1 }}" } } } }
json
{
  "on": { "click": [
    { "validate": "form" },
    { "if": "form.valid",
      "then": [ { "set": { "saving": true } }, { "wait": 800 }, { "navigate": "dashboard" } ],
      "else": { "animate": "shake" } }
  ] }
}
json
{ "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

json
{
  "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": [ ] }
}
CampoTipoObligatorioDescripción
idIdSí
bodyNodeSíDebe ser frame.
nameStringNo
kind"screen" | "overlay"No ("screen")
framePageFrameNoTamaño de la superficie de diseño y background (Paint). device es una pista informativa (no normativa).
paramsRecord<Name, PropDecl>NoParámetros de entrada (params.x).
state, computedNoÁmbito de página (§16.2).
onNoEventos de página: load, unload, keyDown, resize.
overlayOverlayConfigSolo 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.

text
start → [home] ──navigate→ [home, detail] ──navigate(replace)→ [home, detail2] ──back→ [home]

19.3 navigate y transiciones de página

json
{ "navigate": {
    "to": "detail",
    "params": { "id": "{{ project.id }}" },
    "replace": false,
    "transition": { "type": "push", "direction": "left", "duration": 250, "easing": "ease-out" }
} }
transition.typedirectionEfecto
"none"—Cambio instantáneo (defecto).
"fade"—Fundido cruzado.
"slide"left | right | up | downLa nueva entra deslizando sobre la actual.
"push"left | right | up | downLa 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: true sustituye la página actual en la pila.
  • back revierte la transición por defecto de forma inversa.
  • params DEBE coincidir con page.params (DIL-E017).

19.4 Overlays

Una página con kind: "overlay" se presenta sobre la pantalla actual.

json
{
  "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": [ ] }
}
OverlayConfigTipoDefecto
presentation"modal" | "popover" | "sheet" | "drawer" | "toast""modal"
backdropPaint | falsefalse (popover/toast), "#00000066" (resto)
dismissOnBackdrop, dismissOnEscapeBooleantrue
placementAnchormodal: center · sheet: bottom · drawer: left · toast: top · popover: bottom
durationDurationSolo toast: cierre automático (defecto 3000)
  • open: { overlay, anchor } — anchor (id de nodo) solo aplica a popover: se posiciona junto a él.
  • Un modal bloquea 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 params propios.

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.

json
{
  "flows": [
    { "id": "onboarding", "name": "Onboarding", "start": "welcome",
      "description": "Primer uso hasta el dashboard",
      "state": { "email": "demo@ejemplo.com" },
      "frame": { "device": "iphone-15" } }
  ]
}
CampoTipoDescripción
id, nameId, String
startIdPágina inicial del flujo.
descriptionString
stateRecord<Name, any>Valores iniciales de estado de documento que se aplican al iniciar el flujo.
framePartial<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

FormaValores
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).

json
{ "transition": { "properties": ["opacity", "fill", "scaleX", "scaleY"], "duration": 150, "delay": 0, "easing": "ease-out" } }
CampoTipoDefecto
propertiesString[] | "all""all"
durationDynamic<Duration>0 (sin transición)
delayDynamic<Duration>0
easingDynamic<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)

json
{
  "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 } } ]
    }
  }
}
CampoTipoDefecto
durationDurationobligatorio
delayDuration0
easingEasing"ease-in-out" (entre keyframes)
iterationsNumber > 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 set DEBEN 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.
  • AnimationRef en node.animation: Id o { ref: Id, trigger: "load" | "visible" | "hover", loop?: Boolean } — reproduce la animación automáticamente.
  • animate como 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

CampoTipoDescripción
roleRoleRol semántico (tabla). Los controles tienen un rol implícito.
labelDynamic<String>Nombre accesible.
labelledByIdNodo cuyo texto es el nombre.
descriptionDynamic<String>Descripción adicional.
hiddenDynamic<Boolean>Oculto a tecnologías de asistencia (decorativo).
live"off" | "polite" | "assertive"Región viva.
focusableBooleanFuerza que un nodo no interactivo reciba foco.
orderIntegerOrden de foco; por defecto, orden del árbol (DFS).
level1..6Nivel de heading.
expandedDynamic<Boolean>Estado expandido/colapsado.
keyShortcutStringAtajo (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)

ReglaCódigo
image sin alt/a11y.label y sin a11y.hidden: trueDIL-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 cercanoDIL-W003
Objetivo táctil interactivo < 44 × 44 dpDIL-W004
Saltos de nivel de heading (h1→h3)DIL-W012
focusable/order con valores duplicados en una misma páginaDIL-W013

21.3 Teclado y foco

  • Todo nodo interactivo DEBE ser alcanzable por teclado en el orden de §21.1 (order o DFS).
  • click se activa también con Enter/Espacio sobre un nodo enfocado.
  • Un modal atrapa el foco; Esc cierra si dismissOnEscape.
  • focusVisible solo 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

json
{
  "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" }
  }
}
CampoTipoAplica aDescripción
kind"image" | "svg" | "font"TodosObligatorio.
uriStringTodosObligatorio. Relativo, https: o data:.
mimeStringTodosTipo MIME (informativo; DEBERÍA coincidir).
width, heightIntegerimage, svgDimensiones intrínsecas.
altStringimage, svgTexto alternativo por defecto.
hashStringTodossha256-<base64> para integridad (el runtime PUEDE verificar).
family, weight, stylefontFamilia y rasgos que expone.
  • Un image usa src: AssetRef, fit: "cover" | "contain" | "fill" | "none" | "scale-down" (defecto "cover"), alt.
  • aspectRatio conserva la relación ancho/alto al redimensionar. En este editor, el punto de recorte se guarda como extensión x-dodrawing-crop: { "x": 0..100, "y": 0..100 }; se aplica con fit: "cover" y por defecto queda centrado (50, 50). Otros runtimes pueden ignorar esta extensión.
  • icon: name es AssetRef a un svg, o "provider:name" (p. ej. "lucide:home") que resuelve el runtime. Los nombres de proveedor no son portables ⇒ DIL-W014.
  • svg usado en icon se tiñe con color si no contiene colores fijos.
  • Esquemas uri permitidos: 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):

ÁmbitoClaveTipoUso
NodolockedBooleanImpide selección/edición en el lienzo.
NodohiddenBooleanOculta en el lienzo (≠ visible de runtime).
NodocollapsedBooleanColapsado en el panel de capas.
NodolabelStringNombre de capa.
NodocolorStringColor de etiqueta.
NodonotesStringNotas de diseño.
NodoaspectLockedBooleanMantener proporción al redimensionar.
Raízcanvas.pagesRecord<Id, {x,y}>Posición de cada página en el lienzo.
Raízcanvas.zoom, canvas.scrollVista.
Raízgrid{ size, snap }Rejilla y ajuste.
Raízguides{ pageId, axis, position }[]Guías.
RaízcommentsComment[]Comentarios.
RaízselectionId[]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ónTipoUso
x-dodrawing-radius-cssString (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-shapeStringConserva 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

text
 ┌────────────┐   ┌──────────────┐   ┌──────────────┐   ┌────────────┐
 │ Cargar doc │ → │ Inicializar  │ → │ Montar start │ → │  Bucle de  │
 │ (§3.3 ①-④) │   │ estado docs  │   │ page + load  │   │  eventos   │
 └────────────┘   └──────────────┘   └──────────────┘   └─────┬──────┘
                                                              │
        evento (usuario/timer/sistema) ──► acciones ──► estado ──► reevaluar ──► render
  1. Inicialización de estado: se evalúan los default de state (documento) en orden topológico de dependencias.
  2. Montaje: se crea el ámbito de página (state, params), se instancia el árbol (§3.3 ⑤), se dispara load (nodos y página).
  3. Bucle: cada evento ejecuta su ActionSpec.

24.2 Instanciación

  • Componentes: cada instance crea un ámbito propio (props + state del componente) y expande su root.
  • `repeat`: cada elemento crea un ámbito con as/index. Se reconcilia por `key`: mismo key ⇒ se conserva ámbito y estado de componente; key nuevo ⇒ se monta; key ausente ⇒ 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 tick es una unidad atómica: se aplican todas las mutaciones de la secuencia hasta el siguiente wait/suspensión, se recalculan computed y propiedades afectadas una sola vez en orden topológico (sin estados intermedios observables, glitch-free), y se renderiza.
  • Cambios de viewport, theme, form y 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

RecursoLímiteCódigo
Nodos instanciados por página50 000DIL-R002
Elementos por repeat10 000DIL-E021
Profundidad de árbol64DIL-E021
Anidamiento de componentes16DIL-E021
Tamaño del documento16 MBDIL-E021
Acciones por secuencia1 000DIL-R002

25. Validación y diagnósticos

25.1 Pases del validador

PaseVerificaFases (§3.3)
P1 SintaxisJSON válido, I-JSON, UTF-8, sin claves duplicadas.①
P2 EstructuraCumple el JSON Schema (Apéndice B): tipos, enumeraciones, patrones, obligatorios.②④
P3 SemánticaUnicidad 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 LintAccesibilidad, 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

json
{
  "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ódigoSignificado
E001JSON inválido o violación de I-JSON (clave duplicada, número no finito).
E002Campo dil ausente o versión no soportada.
E003Violación de esquema: propiedad desconocida, tipo/enum/patrón/obligatoria, atajo contradictorio.
E004Id duplicado en su ámbito (o key duplicada).
E005Referencia no resuelta (componente, página, estilo, asset, acción, animación, nodo).
E006Token inexistente o tema que sobrescribe un token inexistente.
E007Categoría de token incompatible con la propiedad.
E008Ciclo (alias de tokens, extends de temas, computed/default de estado).
E009Recursión: componente que se contiene a sí mismo o acción call recursiva.
E010Error de sintaxis en expresión.
E011Error de tipo estático en expresión o argumentos de función; resultado incompatible con el tipo de la propiedad.
E012Identificador/función desconocido, o Path no asignable.
E013Combinación de layout inválida (p. ej. x+right+width; % en padre fit).
E014Override apunta a un nodo inexistente en el componente.
E015Slot referenciado inexistente.
E016Eje u opción de variante inexistente.
E017Prop/param requerido ausente, desconocido o de tipo incorrecto.
E018Acción inválida: verbo desconocido/reservado o más de un verbo.
E019Evento desconocido para ese tipo de nodo.
E020Valor incompatible con el tipo del estado (set/bind/default).
E021Límite estructural excedido (§24.6).
E022URI de asset no permitida.
E023Expresión regular inválida o prohibida.

Avisos (`DIL-W…`)

CódigoSignificado
W001Imagen sin texto alternativo.
W002Elemento interactivo sin nombre accesible.
W003Contraste insuficiente.
W004Objetivo táctil demasiado pequeño.
W005Página inalcanzable.
W006Token/asset/estilo/componente/acción sin uso.
W007Propiedad desconocida ignorada (lenient).
W008Característica deprecada.
W010Variante de control sin estilo "<type>.<variant>" en styles.
W011Dependencia circular de tamaño (fit con fill hijo).
W012Salto en jerarquía de encabezados.
W013Orden de foco duplicado.
W014Icono con proveedor no portable.

25.4 Diagnósticos de runtime (DIL-R…)

CódigoSignificado
R001Error de evaluación de expresión (el resultado es null; la propiedad usa su defecto).
R002Límite de ejecución excedido.
R003Fallo de acción (secuencia detenida).
R004Asset no disponible (carga fallida, hash inválido).

Un runtime NO DEBE terminar abruptamente por errores de R001–R004: degrada y continúa.


26. Seguridad

  1. 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).
  2. Texto inerte: el contenido de text es siempre texto; nunca se interpreta como HTML/marcado.
  3. Sin red en 1.0: ninguna construcción del lenguaje realiza peticiones. La carga de assets está limitada a los esquemas de §22.
  4. Expresiones acotadas: totales, sin recursión, con límites de pasos y tamaño (§17.7).
  5. Regex seguras: subconjunto sin retroceso catastrófico (§17.5).
  6. Datos no confiables: un documento generado por IA o recibido de terceros DEBE validarse en modo strict antes de ejecutarse.
  7. `host` es el único canal hacia la aplicación anfitriona; el anfitrión decide qué name atiende y DEBE tratar payload como entrada no confiable.
  8. Los metadatos editor y x-* NO DEBEN usarse para transportar comportamiento ejecutable.
  9. 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)

ÁreaIdea
Datos remotosfetch, auth, estados loading/error de petición, esquemas de datos.
LógicaExpresiones más potentes, funciones definidas por el usuario (puras), switch/match.
LayoutContainer queries, stack explícito, sticky, position: fixed, subgrid.
ComponentesBibliotecas externas importables (imports), versionado de componentes, parámetros en acciones con nombre.
FormulariosMúltiples formularios por página (forms.<id>), validación asíncrona.
Mediosvideo, audio, lottie, canvas.
InteracciónArrastrar y soltar, gestos (swipe/pinch), foco programable avanzado, stopPropagation.
PlataformaPlugins, persistencia local (store), compartir/descargar, i18n (t() y mensajes).
DiseñoMá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.

ts
/* ============================ 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.json DEBE generarse a partir de los tipos del Apéndice A siguiendo el mismo patrón (allOf + if/then por type, unevaluatedProperties: false salvo x-*).

json
{
  "$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

json
{
  "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)

json
{
  "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" en input resuelve al estilo input.default; (2) width: "{{ (p.progress * 100) + '%' }}" falla de tipo porque + mezcla number y string; 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)

json
{ "type": "frame", "height": 8, "width": "{{ toString(p.progress * 100) + '%' }}", "fill": "$color.brand.primary" }

C.4 Absoluto (mockup de lienzo libre)

json
{
  "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):

  1. [ ] DIL 1.0 Specification (este documento)
  2. [ ] dil-1.0.schema.json generado desde el Apéndice A
  3. [ ] Parser + Normalizador (§3.3–§3.4; idempotente; ids automáticos)
  4. [ ] Validador P1–P4 con el catálogo de §25.3
  5. [ ] Evaluador de expresiones (§17) con tipos estáticos y pruebas de la biblioteca estándar
  6. [ ] Motor de cascada (§14) y resolución de valores (§6, §7)
  7. [ ] Motor de layout flex/grid/absoluto (§9) con pruebas dorado (golden tests)
  8. [ ] Runtime: estado, reactividad, acciones, navegación, overlays, animaciones (§18–§20, §24)
  9. [ ] Editor: arrastrar y soltar ⇒ DIL; panel de propiedades con states/responsive/variantes; metadatos en editor
  10. [ ] Compilador (React/HTML) con tabla de características soportadas
  11. [ ] Suite de conformidad: documentos válidos/ inválidos con diagnósticos esperados (código + ruta)

Pruebas mínimas de conformidad (ejemplos):

CasoResultado esperado
{"dil":"1.0","pages":[]}DIL-E003 (pages ≥ 1)
Dos nodos con id: "a" en la misma páginaDIL-E004
"fill": "$color.nope"DIL-E006
"padding": "$color.primary"DIL-E007
Tokens a → b → aDIL-E008
Componente A contiene instance de ADIL-E009
"if": "email =="DIL-E010
"width": "{{ 'a' + 1 }}"DIL-E011
{"set": {"props.label": "x"}}DIL-E012
Hijo absoluto con x, right y widthDIL-E013
Acción con {"set": {}, "navigate": "x"}DIL-E018
{"fetch": {...}}DIL-E018 (verbo reservado)
image sin altDIL-W001
Eliminar todas las claves editorEl comportamiento NO cambia (§23)
normalize(normalize(d))== normalize(d)

Fin de la especificación DIL 1.0.