El envelope
La unidad que carga un SDK: un objeto JSON con una versión, un diccionario de tokens, una o más views y un manifiesto de assets — más las props que lleva cada nodo.
El envelope —el sobre que el juego descarga— es el único fichero que un juego baja para tener su UI. Imagina la pantalla de tienda del gremio: el panel, las dos filas de objetos, los botones de comprar, el contador de oro. Todo eso viaja en un solo fichero JSON —las cajas y las etiquetas, los colores a los que apuntan, las imágenes que pintan— y el SDK que vive dentro del juego lee ese fichero y dibuja la pantalla. No hay un segundo fichero ni un paso de build en la máquina del jugador.
Formalmente: un objeto JSON con una versión, un diccionario de tokens, una o más views —cada una es una pantalla— y, cuando la UI usa imágenes, un manifiesto de assets. La forma de lo que lleva dentro es la IR, y estas páginas son su referencia.
Hay exactamente un camino de carga. Un fichero importado a mano en el editor y un hot-update —una actualización enviada a un juego ya publicado— son el mismo payload versionado, y se leen igual.
{
"v": 1,
"tokens": {
"color.primary": "#4f46e5",
"space.3": 12,
"radius.md": 6
},
"views": {
"hud": { "type": "Container", "children": [] }
},
"assets": {
"icons/coin.png": {
"hash": "9f2c…",
"mime": "image/png",
"size": 1204,
"width": 32,
"height": 32,
"data": "iVBORw0KGgo…"
}
}
}
Cuatro campos de primer nivel, y el resto de esta referencia describe lo que vive dentro de uno de ellos:
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| v | number | — | La versión mayor de la IR. Un SDK rechaza un payload cuya mayor no implementa. |
| tokens | Record<string, string | number> | — | Diccionario plano de tokens de diseño. |
| views | Record<string, ZNode> | — | Documentos (views, escenas) indexados por id de view. Hace falta al menos una view utilizable. |
| assets | Record<string, AssetEntry> | ausente | Manifiesto de assets indexado por id lógico. Los envelopes sin assets lo omiten. |
Las views
Una view es una pantalla. Un envelope lleva varias —un HUD, una tienda, una pantalla de ajustes— y el juego elige cuál renderizar. Cada valor es un árbol de nodos, las cajas y etiquetas de las que está hecha la pantalla; la clave es el id que el juego pide.
Los ids de view son cadenas opacas y pueden contener puntos, y por eso los
diagnósticos los direccionan entre corchetes: views["shop.main"].children[2].
Se espera que los id de nodo sean únicos dentro de una view. Son lo que el
juego nombra cuando maneja un nodo a través del
canal del host, y a lo
que apunta un Overlay
anclado. Dale a un nodo el id audio-section y setOpen("audio-section", true)
lo abre desde el código del juego; toda la superficie está en
El canal del host. Los duplicados cargan, con
un aviso, y resuelven a la primera coincidencia.
Los tokens
Un token es un valor con nombre —color.primary, space.3— al que los
estilos apuntan en vez de escribirlo. Los estilos no cuecen valores dentro:
referencian tokens, y el SDK los resuelve por nodo en tiempo de render. Cambiar
el diccionario re-tematiza toda la UI sin volver a emitir el árbol, y eso es lo
que hace que un tema se pueda hot-updatear por su cuenta.
Una referencia a token es una cadena entre llaves: "{color.primary}". Vale
en cualquier sitio donde se acepte un Dim o un ColorValue, y el diccionario
es plano: la clave es el nombre entero, puntos incluidos, así que buscar es
un acceso a la tabla hash y nunca un recorrido:
"tokens": { "color.primary": "#4f46e5", "space.3": 12 }
"style": { "background": "{color.primary}" },
"layout": { "padding": "{space.3}" }
En la tienda, el botón de comprar pide "background": "{color.primary}", y
también lo hace cualquier otro botón relleno del juego. Publica un envelope cuyo
diccionario mapee color.primary a otro hexadecimal y todos se repintan a la
vez: el árbol de nodos es byte a byte el que era.
No hay cascada ni herencia. Cada nodo lleva su propio estilo ya resuelto; nada se busca en un padre.
Un token que falta nunca rompe el frame
El pase de carga lo reporta una vez, nombrando el nodo y la propiedad
(unknown-token), y entonces:
- Un
Dimcae al valor por defecto de su propia propiedad. Un{space.3}que falta dapadding: 0, y un{size.card}que falta en unwidthdeja el nodo con tamaño automático. - Un
ColorValuedeclarado pinta el magenta de color-ausente. Es una señal deliberada y a gritos: el autor pidió un color y nombró uno que no existe, y un nodo transparente en silencio escondería la errata en vez de enseñarla. Un color ausente no es este caso: simplemente no pinta nada.
Los valores de los tokens son cadenas o números, y la propiedad decide cómo
leer uno: un Dim toma el número, un ColorValue toma la cadena. Un token del
tipo equivocado se trata exactamente igual que uno que falta.
Los assets
Las imágenes viajan dentro del envelope. Una entrada describe el contenido y, en v1, lleva los bytes:
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| hash | string | — | Identidad del contenido (SHA-256, hex). Hoy deduplica; mañana es la clave del caché direccionado por contenido. |
| mime | string | — | Por ejemplo "image/png". El formato es genérico: qué tipos MIME se aceptan es asunto de la exportación. |
| size | number | — | Tamaño en bytes del contenido decodificado. |
| width | number | ausente | Ancho en píxeles. Permite al layout reservar sitio antes de decodificar los bytes. |
| height | number | ausente | Alto en píxeles. |
| data | string | ausente | El contenido, codificado en base64. |
Los nodos referencian una entrada por su clave en el manifiesto, con prefijo:
"asset:icons/coin.png". El prefijo es lo que hace reconocible una referencia a
asset sin conocer el manifiesto — isAssetRef y assetIdFromRef en
@zabloo/format son los lectores compartidos para eso.
data es opcional solo en el esquema: una exportación v1 siempre lo incrusta.
El campo existe para que un futuro camino de entrega pueda enviar un envelope que
nombre sus assets por hash y deje que el SDK resuelva los bytes desde un caché o
una CDN, sin cambiar el formato. Un SDK que no encuentra data y no puede
resolver los bytes pinta el fondo del nodo y nada más — igual que una imagen que
todavía se está decodificando.
width y height importan para el layout, no para el pintado: una
Image toma el tamaño en
píxeles del origen como su tamaño intrínseco, así que un manifiesto que los
omite hace que el nodo mida cero hasta que se le dé un tamaño explícito.
El manifiesto lleva imágenes y solo imágenes —la exportación acepta .png,
.jpg y .jpeg— y ningún nodo tiene una prop que pueda nombrar una fuente,
así que no hay con qué referenciarla. Cada target rasteriza en su lugar la
misma tipografía incrustada. Las fuentes por proyecto llegan con el trabajo
del motor de texto, y aterrizarán aquí: el manifiesto es deliberadamente
genérico con los tipos MIME, así que añadir uno es asunto de la exportación y
no un cambio de formato.
La base de los nodos
Cada nodo de views es un objeto con un type y los campos de abajo. Estos son
los campos que tiene todo nodo, sea lo que sea: un Button, un Text y un
ScrollView los llevan los tres. Cada tipo de nodo añade después los suyos —un
Text añade text, un Slider añade min y max— y esos se documentan en
cada página del catálogo.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| type | string | — | La identidad del nodo. Decide su comportamiento y su pintado por defecto. |
| id | string | ausente | Nombre direccionable dentro de la view. |
| visible | Bindable<boolean> | true | El único mecanismo para ocultar, con semántica de display:none — un nodo oculto sale del layout por completo. |
| disabled | Bindable<boolean> | false | Saca este nodo y su subárbol del modelo de interacción. |
| layout | Layout | {} | Entradas del flex — dirección, gap, padding, tamaño, grow, wrap. |
| style | Style | {} | Cómo pinta el nodo, resuelto por nodo. |
| states | Partial<Record<StateName, { style?: Style }>> | {} | Sobrescrituras de estilo por estado, mezcladas en un único orden normativo. |
| transition | Transition | ausente | Interpola los valores animables de este nodo cuando cambian. |
| autofocus | boolean | false | Este nodo toma el foco inicial de su ámbito. |
| clip | boolean | false | Recorta el pintado y el hit-testing de los hijos al rect de este nodo. |
Tres de ellos llevan reglas que conviene decir en voz alta.
visible es la única forma de ocultar algo. No hay un segundo mecanismo: ni
display, ni hidden, ni un truco de opacidad con consecuencias en el layout.
Ocultar un nodo lo saca del pase de layout, así que
sus hermanos cierran el hueco; volver a mostrarlo lo trae de vuelta. Como es
Bindable, el juego abre y cierra UI moviendo un booleano en sus propios datos.
disabled es la única prop que se hereda. El valor efectivo de un nodo es
el suyo o el de cualquier ancestro, así que una sola prop apaga una sección
entera de un formulario; un
Overlay reinicia la
cadena, por ser la cima de su propio ámbito de entrada. Quita foco, hover,
pulsación y acciones — no el pintado, y no el scroll — y su aspecto es
states.disabled, ya que el formato no trae ningún look apagado de serie. La
regla entera está en
Input y focus.
clip es configuración de pintado, no estado de ejecución. El rect de
recorte efectivo de un nodo es la intersección del suyo con el de todos sus
ancestros, y corta la entrada además de los píxeles: un hijo pintado fuera del
rect tampoco es pulsable ahí. Un
ScrollView recorta
siempre e ignora un clip: false explícito.
Qué se puede bindear
Un binding —el enlace entre
un dato del juego y un nodo— es una dirección dentro de los datos del propio
juego, player.gold, escrita donde iría un valor fijo, para que el nodo enseñe
lo que el juego tenga ahí en cada momento.
visible y disabled son las dos props base que aceptan un binding en lugar de
un literal, y son las mismas dos en todos los tipos de nodo: el juego empuja un
booleano y el nodo entra o sale del layout, o el subárbol entero se cae del
modelo de interacción. Todo lo demás que se bindea es propio de cada tipo —el
text de un Text, el checked de un Toggle, los items de un Repeat— y
vive en Bindings y actions.
Fíjate en que style no es bindeable en ningún sitio. Una barra que cambia
de color con su valor se hace con el juego moviendo un token, no con la UI
calculando uno.
De dónde sale el árbol
@zabloo/react emite envelopes. zabloo export renderiza las views del
proyecto, recoge los assets que referencian y escribe el JSON. Los conceptos de
autoría —componentes de usuario, variantes,
compuestos—
se resuelven durante ese pase y nunca aparecen en la salida.
Leer un envelope de vuelta —parsear, validar, reparar— es Loading.