Collapse
Una sección que se abre y se cierra. Su contenido sale de verdad del layout al cerrarse, así que todo lo de debajo sube a ocupar el hueco.
primitivedesde v1sin focus
Un Collapse es una sección que el jugador puede abrir y cerrar. Su primer hijo
es la cabecera —tocarla abre o cierra la sección— y todo lo que va detrás es el
contenido, que sale de verdad del layout cuando la sección se cierra, así que
los hermanos de abajo suben a llenar el hueco. Piensa en una página de ajustes
larga donde solo ocupa sitio la sección en la que estás. Es además el nodo que
demostró que este formato nunca podría publicar rectángulos ya cocinados: una
pantalla que lleve uno se vuelve a medir mientras el jugador la está mirando.
Viewport: 960 × 360

- hover — inactivo
- pressed — inactivo
- focused — inactivo
- selected — inactivo
- disabled — inactivo
Se lee del último frame dibujado
Parado: pulsa Ejecutar para dibujarlo en la GPU.
import { Accordion, Button, Collapse, Column, Row, Text } from "@zabloo/react";
/** A header is `children[0]`, and tapping it toggles — whatever it is. */
function Header({ label }: { label: string }) {
return (
<Button
variant="secondary"
layout={{ padding: "{space.3}", justify: "start", align: "center" }}
>
<Text style={{ color: "{color.text}", fontSize: "{text.sm}" }}>{label}</Text>
</Button>
);
}
function Line({ name, value }: { name: string; value: string }) {
return (
<Row layout={{ justify: "space-between", align: "center" }}>
<Text style={{ color: "{color.muted}", fontSize: "{text.sm}" }}>{name}</Text>
<Text style={{ color: "{color.text}", fontSize: "{text.sm}" }}>{value}</Text>
</Row>
);
}
export default function CollapseAccordion() {
return (
<Column
// `align: "stretch"` on the ROOT is what hands the panel the view's own
// width; centring it instead would pin the panel to its content and no
// change of viewport could reach it (ZAB-153).
layout={{ grow: 1, justify: "center", align: "stretch", padding: "{space.6}" }}
style={{ background: "{color.bg}" }}
>
<Column
id="panel"
layout={{ padding: "{space.5}", gap: "{space.4}", align: "stretch" }}
style={{
background: "{color.surface}",
radius: "{radius.lg}",
borderWidth: "{border.hairline}",
borderColor: "{color.line}",
}}
>
<Text style={{ color: "{color.text}", fontSize: "{text.lg}" }}>Options</Text>
<Accordion layout={{ gap: "{space.2}", align: "stretch" }}>
<Collapse
id="audio"
open
transition={{ duration: "{motion.fast}" }}
layout={{ gap: "{space.2}", align: "stretch" }}
>
<Header label="Audio" />
{/* Everything from here down is content: it leaves the layout when
the section closes, and the siblings below move up. */}
<Column
layout={{ padding: "{space.3}", gap: "{space.2}", align: "stretch" }}
style={{ background: "{color.slot}", radius: "{radius.md}" }}
>
<Line name="Master volume" value="80" />
<Line name="Music" value="45" />
</Column>
</Collapse>
<Collapse
id="video"
open={false}
transition={{ duration: "{motion.fast}" }}
layout={{ gap: "{space.2}", align: "stretch" }}
>
<Header label="Video" />
<Column
layout={{ padding: "{space.3}", gap: "{space.2}", align: "stretch" }}
style={{ background: "{color.slot}", radius: "{radius.md}" }}
>
<Line name="Resolution" value="1920 × 1080" />
<Line name="Quality" value="High" />
</Column>
</Collapse>
</Accordion>
</Column>
</Column>
);
}{
"v": 1,
"tokens": {
"color.bg": "#0b0d13",
"color.surface": "#0e1016",
"color.raised": "#14141a",
"color.line": "#ffffff1f",
"color.line-strong": "#ffffff24",
"color.text": "#ffffff",
"color.muted": "#a1a1aa",
"color.faint": "#8a8a93",
"color.on-brand": "#ffffff",
"color.brand": "#8b5cf6",
"color.brand-strong": "#7c3aed",
"color.brand-hover": "#8b5cf6",
"color.brand-pressed": "#6d28d9",
"color.brand-soft": "#8b5cf61f",
"color.gold": "#fcd34d",
"color.slot": "#ffffff08",
"color.danger": "#f87171",
"radius.sm": 6,
"radius.md": 10,
"radius.lg": 14,
"radius.pill": 999,
"border.hairline": 1,
"border.focus": 2,
"space.1": 4,
"space.2": 8,
"space.3": 12,
"space.4": 16,
"space.5": 20,
"space.6": 24,
"text.xs": 11,
"text.sm": 13,
"text.md": 15,
"text.lg": 17,
"motion.fast": 120,
"motion.loop": 900
},
"views": {
"collapse-accordion": {
"type": "Container",
"layout": {
"direction": "column",
"grow": 1,
"justify": "center",
"align": "stretch",
"padding": "{space.6}"
},
"style": {
"background": "{color.bg}"
},
"children": [
{
"type": "Container",
"id": "panel",
"layout": {
"direction": "column",
"padding": "{space.5}",
"gap": "{space.4}",
"align": "stretch"
},
"style": {
"background": "{color.surface}",
"radius": "{radius.lg}",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line}"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.lg}"
},
"text": "Options"
},
{
"type": "Container",
"layout": {
"direction": "column",
"gap": "{space.2}",
"align": "stretch"
},
"group": "exclusive-open",
"children": [
{
"type": "Collapse",
"id": "audio",
"layout": {
"gap": "{space.2}",
"align": "stretch"
},
"transition": {
"duration": "{motion.fast}"
},
"open": true,
"children": [
{
"type": "Button",
"layout": {
"padding": "{space.3}",
"justify": "start",
"align": "center"
},
"style": {
"background": "#00000000",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line-strong}",
"radius": "{radius.md}",
"color": "{color.text}"
},
"states": {
"hover": {
"style": {
"background": "{color.slot}"
}
},
"pressed": {
"style": {
"background": "{color.brand-soft}"
}
},
"focused": {
"style": {
"borderWidth": "{border.focus}",
"borderColor": "{color.brand}"
}
},
"disabled": {
"style": {
"opacity": 0.45
}
}
},
"transition": {
"duration": "{motion.fast}"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "Audio"
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"padding": "{space.3}",
"gap": "{space.2}",
"align": "stretch"
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Master volume"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "80"
}
]
},
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Music"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "45"
}
]
}
]
}
]
},
{
"type": "Collapse",
"id": "video",
"layout": {
"gap": "{space.2}",
"align": "stretch"
},
"transition": {
"duration": "{motion.fast}"
},
"open": false,
"children": [
{
"type": "Button",
"layout": {
"padding": "{space.3}",
"justify": "start",
"align": "center"
},
"style": {
"background": "#00000000",
"borderWidth": "{border.hairline}",
"borderColor": "{color.line-strong}",
"radius": "{radius.md}",
"color": "{color.text}"
},
"states": {
"hover": {
"style": {
"background": "{color.slot}"
}
},
"pressed": {
"style": {
"background": "{color.brand-soft}"
}
},
"focused": {
"style": {
"borderWidth": "{border.focus}",
"borderColor": "{color.brand}"
}
},
"disabled": {
"style": {
"opacity": 0.45
}
}
},
"transition": {
"duration": "{motion.fast}"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "Video"
}
]
},
{
"type": "Container",
"layout": {
"direction": "column",
"padding": "{space.3}",
"gap": "{space.2}",
"align": "stretch"
},
"style": {
"background": "{color.slot}",
"radius": "{radius.md}"
},
"children": [
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Resolution"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "1920 × 1080"
}
]
},
{
"type": "Container",
"layout": {
"direction": "row",
"justify": "space-between",
"align": "center"
},
"children": [
{
"type": "Text",
"style": {
"color": "{color.muted}",
"fontSize": "{text.sm}"
},
"text": "Quality"
},
{
"type": "Text",
"style": {
"color": "{color.text}",
"fontSize": "{text.sm}"
},
"text": "High"
}
]
}
]
}
]
}
]
}
]
}
]
}
}
}Las dos tablas de abajo son casi la misma, que en este nodo es justo lo interesante: lo único que lleva el formato es dónde empieza la sección. Si está abierta ahora mismo es estado de ejecución que guarda el SDK, y no está por ninguna parte de lo que se publica.
Props de autoría
Lo que escribes en @zabloo/react.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| open | boolean | true | Estado de apertura inicial. |
| children | ReactNode | absent | Primer hijo = cabecera; el resto = contenido. |
Además de estas, todo componente acepta las props base del nodo —id,
visible, disabled, layout, style, states, transition, autofocus,
clip— más variant, que el theme resuelve al exportar y que nunca aparece en
la IR.
Props de IR
Lo que llega al juego, cuando ya no queda capa de autoría.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| open | boolean | true | Estado inicial. No enlazable — el estado de ejecución es del SDK. |
| children | ZNode[] | [] | children[0] = cabecera; children[1..] = contenido plegable. |
A partir de ahí es estado de ejecución que guarda el SDK —como el
pressed de un Button— y no sobrevive ni a serializarse ni a una recarga. No es enlazable
a propósito: un open enlazado haría al juego dueño de algo que está sosteniendo el jugador, y
los dos se pelearían. Un juego que necesite manejarlo lo dice por fuera, con SetOpen(id, open).
Los slots
children[0]es la cabecera. Siempre está en el layout, y tocarla abre o cierra la región. Es focusable sea lo que sea: la cabecera de unCollapseentra en el conjunto de lo focusable igual que lo hace unButton.children[1..]es el contenido. Entra y sale del layout con la semántica dedisplay:none—el mismo y único mecanismo de ocultar quevisible—, así que cerrar lo quita de verdad y los hermanos de abajo suben.
Comportamiento
- Estados
El
Collapseen sí no lleva ninguno propio; su cabecera llevahover,pressedyfocused. Esepressedviene solo del teclado o del mando: una cabecera no es uno de los tipos que el puntero pulsa, así que un toque abre o cierra la sección sin llegar a encender el estado de “pulsado”. Los dos llevandisabled: declarado en elCollapsele llega a la cabecera, y una cabecera deshabilitada ya no abre ni cierra la sección.- Focusable
El nodo en sí no lo es; su cabecera sí, sea el tipo de nodo que sea. Es el único sitio del formato donde un hijo entra en el conjunto de lo focusable por su posición.
- Movimiento
Con una
transition, unCollapseanima su propia altura entre cerrado y la altura natural de su contenido, y recorta mientras lo hace — así que el contenido lo corta la caja que se cierra sobre él, sin que quien lo escribe tenga que pedir ningún clip.- Actions
El jugador toca la cabecera → la sección se abre o se cierra y el layout de debajo se recoloca → el juego no oye nada. Abrir no es una named action. Si el juego sí necesita enterarse, haz que la cabecera sea un
Buttoncon unonClick; el otro sentido es el host channel, conSetOpen(id, open).
Dentro de un acordeón
Dentro de un
Container con group: "exclusive-open",
abrir un Collapse cierra a sus hermanos. El comportamiento es del padre: el
Collapse no declara nada al respecto, y un SDK que ignore el group deja
collapses independientes. Ese reparto es el mismo que sigue cualquier
comportamiento de grupo — el hijo sigue siendo un hijo, y la regla vive un nivel
más arriba.
Degradación
En un SDK más antiguo todas las secciones están abiertas y siguen abiertas. No se esconde nada — pero tampoco se puede recoger nada.
Como Container: se ve todo, abierto para siempre, y la cabecera es un hijo más. No se pierde nada — lo que se va es la capacidad de cerrarlo, que es la forma menos dañina en que puede fallar un control de plegado.
Composición
La cabecera es un nodo normal, así que su aspecto es una cuestión de layout.
- Un
Buttoncomo cabecera — cuando el juego necesite oír que la sección se ha abierto, ya que el plegado en sí no envía nada. - Un
Rowcon una etiqueta y una flecha — para el caso normal, en el que la cabecera es solo un asa. - Un
<Accordion>alrededor de varios — cuando las secciones sean lo bastante largas como para que dos abiertas a la vez entierren al resto. - Una
transition— cuando la sección sea corta; en una larga, animar la altura es una forma lenta de decir lo mismo.
// A section that starts closed. The header is children[0], whatever it is.
<Collapse open={false}>
<Button onClick="audio-toggled"><Text>Audio</Text></Button>
<Slider value={{ bind: "settings.volume" }} />
</Collapse>
// An accordion: the "only one open" rule is the parent's, not the children's.
<Accordion layout={{ gap: 8 }}>
<Collapse open={false}>…</Collapse>
<Collapse open={false}>…</Collapse>
</Accordion>
// With a transition it animates its own height, and clips while it does.
<Collapse open transition={{ duration: 160 }}>
<Text>Advanced</Text>
<Column layout={{ gap: 8 }}>…</Column>
</Collapse>