Temas personalizados
Un tema es un único valor ThemeTokens: colores, espaciado, radios, tamaños de texto y métricas de controles. Copia un preset incluido, cambia los campos que llevan tu marca, instálalo con cx.setTheme y ya es tu tema.
Esta página trata de definir e instalar un tema. Cómo consumen los tokens las vistas — funciones de estilo y recipes — se explica en Estilos y temas.
El tema por defecto
El Cx de cada ventana empieza en ui.theme.dark. Una app que nunca llama a setTheme es oscura — Hello Button lo es. Para usar otro tema, establécelo en la primera línea de tu función de montaje, antes de que exista ningún nodo:
fn mountUI(cx: *ui.Cx, scope: *ui.Scope) anyerror!*ui.Node {
// Cx starts on ui.theme.dark. Pick the theme before building any node,
// so every component reads the right tokens when it mounts.
cx.setTheme(&ui.theme.light);
const root = try ui.boxStyled(cx, S.root, .{});
// …
return root;
}Con varias ventanas, cada una tiene su propio Cx, así que establece el tema en cada función de montaje. zenit no sigue todavía la apariencia del sistema: un cambio claro/oscuro del sistema solo provoca un redibujado, y no hay una API pública para leer la apariencia del sistema. Ofrece en su lugar un interruptor a tus usuarios (consulta “Cambiar en tiempo de ejecución”).
Anatomía de ThemeTokens
pub const ThemeTokens = struct {
name: []const u8 = "unnamed",
scheme: ColorScheme = .light, // .light / .dark — the one flag for "is this dark?"
color: ColorTokens, // 54 colors, no defaults: you must provide all of them
space: SpaceScale = .{}, // _0 … _12 (0 … 48 px, 4 px grid)
radius: RadiusScale = .{}, // none sm md lg xl full
font_size: FontSizeScale = .{}, // xxs … xxxl (9 … 24)
border_width: BorderWidthScale = .{},
size: SizeTokens = .{}, // checkbox, switch, badge, dot, close button
control: ControlScale = .{}, // xs / sm / md / lg metrics for every control
duration: DurationTokens = .{}, // fast / normal / slow (seconds)
shadow: ShadowTokens = .{}, // sm / md / lg
};Todo salvo color tiene valores por defecto. Los 54 colores de ColorTokens no tienen ninguno, así que no escribas un literal de struct desde cero: copia ui.theme.light, ui.theme.dark o ui.theme.high_contrast y cambia solo lo que te interese.
Derivar un tema de marca
Escribe los temas como const a nivel de contenedor; el bloque inicializador se ejecuta en tiempo de compilación y el resultado tiene almacenamiento estático, de modo que el puntero que guarda cx.setTheme(&brand) sigue siendo válido durante toda la vida del programa. Nunca tomes la dirección de un tema guardado en una variable local de una función.
// theme.zig — your app's themes. Container-level consts: they are
// evaluated at compile time and live for the whole program, which is
// what cx.setTheme(&brand) needs (it keeps the pointer).
const ui = @import("ui");
pub const brand: ui.ThemeTokens = blk: {
var t = ui.theme.light; // copy a complete preset (every color is required)
t.name = "brand";
t.scheme = .light;
const blue = ui.Color.hex(0x1144AA);
t.color.accent = blue; // links, checked states, active tabs…
t.color.accent_hover = ui.Color.hex(0x0D3688);
t.color.accent_subtle = ui.Color.hex(0xE6ECF7);
t.color.button_primary_bg = blue; // primary buttons read these two,
t.color.button_primary_fg = ui.Color.WHITE; // not accent
t.color.border_focus = blue;
t.color.selection_bg = ui.Color.hex(0xC9D6F0);
t.color.list_selection_bg = ui.Color.hex(0xE6ECF7);
t.radius.lg = 10;
t.control.md.radius = 10;
t.control.md.padding_h = 14;
break :blk t;
};Un tema oscuro a juego
Deriva la variante oscura de ui.theme.dark en lugar de recolorear el tema de marca claro: las decenas de valores de fondo, texto y borde del preset oscuro están ajustados como conjunto. Sobre fondos oscuros, el color de marca suele necesitar aclararse, y el texto de los botones primary pasa a ser oscuro.
pub const brand_dark: ui.ThemeTokens = blk: {
var t = ui.theme.dark; // start from the dark preset, not from brand
t.name = "brand_dark";
t.scheme = .dark; // Notification palettes and DevTools key off this
const blue = ui.Color.hex(0x7FA2F0); // lighter on dark backgrounds
t.color.accent = blue;
t.color.button_primary_bg = blue;
t.color.button_primary_fg = ui.Color.hex(0x0D1119);
t.color.border_focus = blue;
break :blk t;
};
brand (claro)
brand_dark
ui.theme.dark (por defecto)Qué tokens afectan a qué
Agrupados según lo que realmente leen los componentes (código fuente de v0.1.0-alpha). Empieza por estos al crear un tema:
fg_primary · fg_secondary · fg_tertiary · fg_disabledCasi todo el texto y los iconosbg_primary · bg_secondary · bg_tertiaryPaneles, tarjetas, Calendar, Chip, pistas de Progress / Slider, Skeletonbg_hover · bg_activeEstados hover y pulsado de los controles clicablesaccentCheckbox / Switch marcados, enlaces, Tabs, Breadcrumb, fechas seleccionadas, Badgebutton_primary_bg · button_primary_fgButton primary (hover / pulsado se derivan del fondo); fg también invierte el texto en Badge, Chip y Tabsborder · border_strong · separatorBordes y separadoresborder_focus · input_bg · input_border · selection_bgInput, Select, NumberStepper, DatePicker y otros campossuccess · warning · danger · info · status_fgAlert, Badge, Tag, Progress, Button dangerlist_hover_bg · list_selection_bgMenu, DropdownMenu, ComboBox, Table, Tree, DataTablecheckbox_* · switch_thumb · switch_track_offCheckbox / Switch sin marcar y deshabilitadostooltip_bg · tooltip_fg · overlay · scrollbar_thumbTooltip, velo de Modal / Sheet, barras de desplazamientoAlgunos componentes llevan sus propios colores en lugar de leer ColorTokens: el Notifier elige entre sus paletas clara y oscura incluidas según scheme, y los tintes de GlassBox y algunos detalles de Progress y Select son valores fijos. Siguen el modo claro/oscuro, pero no tu color de marca.
Radios, espaciado y métricas de controles
Los tokens que no son de color pueden sobrescribirse campo a campo. El más útil es control: Button, Input, Select, ComboBox, DatePicker, DateRangePicker, Tabs y Chip comparten cuatro conjuntos de métricas, xs / sm / md / lg. La altura de un control no es un token: se calcula:
xs2.5 × 2 + 12 × 1.25 = 20sm4.5 × 2 + 12 × 1.25 = 24md7.25 × 2 + 14 × 1.25 = 32lg10 × 2 + 16 × 1.25 = 40Así que, para que los controles md sean más altos, cambia t.control.md.padding_y o el tamaño de fuente y todos los controles md lo seguirán; no fijes a mano la height de un control. Mantén icon_size no mayor que la altura de línea.
Cambiar en tiempo de ejecución
cx.setTheme puede llamarse en cualquier momento: cambia el puntero de tokens, vuelve a ejecutar los hooks on_theme / before_render en todo el subárbol de cx.root (incluido el portal de capas flotantes) y solicita un redibujado.
const Prefs = struct {
cx: *ui.Cx,
dark: bool = false,
pub fn toggle(self: *Prefs) void {
self.dark = !self.dark;
self.cx.setTheme(if (self.dark) &themes.brand_dark else &themes.brand);
}
};
// In mountUI:
const prefs = try cx.bindState(Prefs, .{ .cx = cx });
cx.setTheme(&themes.brand);
// …
.on_click = cx.on(Prefs, prefs, Prefs.toggle),// A reactive read of the current theme (lives as long as `scope`).
const theme_sig = try cx.themeSignal(scope);
const is_dark = theme_sig.get().scheme == .dark;Antes de publicar
- ✓
schemecoincide con el fondo (un fondo oscuro debe ser.dark). - ✓
button_primary_fgsobrebutton_primary_bgystatus_fgsobredangeralcanzan al menos 4.5:1 — la misma comprobación con la que se prueban los presets incluidos. - ✓
bordersigue distinguiéndose debg_primary(los tests de los presets exigen ≥ 1.2). - ✓
Cambia el Storybook a tu tema y recorre los componentes habituales (la story Component Wall es un buen punto de partida).