커스텀 테마
테마는 하나의 ThemeTokens 값입니다. 색상, 간격, 모서리 반경, 글자 크기, 컨트롤 치수가 모두 들어 있습니다. 기본 제공 프리셋을 복사해 브랜드 관련 필드를 바꾸고 cx.setTheme로 설치하면 그것이 여러분의 테마입니다.
이 페이지는 테마를 정의하고 설치하는 방법을 다룹니다. 뷰가 token을 소비하는 방식(스타일 함수와 recipe)은 스타일과 테마에서 설명합니다.
기본 테마
각 창의 Cx는 처음에 ui.theme.dark를 가리킵니다. setTheme를 한 번도 호출하지 않는 앱은 다크 테마입니다 — Hello Button이 그렇습니다. 다른 테마를 쓰려면 노드가 생기기 전, 마운트 함수의 첫 줄에서 설정합니다:
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;
}창이 여러 개면 각 창이 자신의 Cx를 가지므로 각 마운트 함수에서 테마를 설정합니다. zenit은 아직 시스템 외관을 따라가지 않습니다. 시스템의 라이트/다크 전환은 다시 그리기만 한 번 일으키며, 시스템 외관을 읽는 공개 API도 없습니다. 대신 사용자에게 전환 스위치를 제공하십시오(“런타임 전환” 참고).
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
};color를 제외한 모든 필드에는 기본값이 있습니다. ColorTokens의 54개 색상에는 기본값이 없으므로 struct 리터럴을 처음부터 쓰지 마십시오. ui.theme.light, ui.theme.dark, ui.theme.high_contrast 중 하나를 복사해 필요한 필드만 바꿉니다.
브랜드 테마 파생
테마는 컨테이너 수준의 const로 작성합니다. 초기화 블록은 컴파일 타임에 평가되고 결과는 정적 저장소에 놓이므로, cx.setTheme(&brand)가 보관하는 포인터는 프로그램이 실행되는 동안 계속 유효합니다. 함수의 지역 변수에 둔 테마의 주소를 취하지 마십시오.
// 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;
};짝을 이루는 다크 테마
다크 버전은 라이트 브랜드 테마의 배경을 바꾸는 대신 ui.theme.dark에서 파생시킵니다. 다크 프리셋의 수십 개 배경, 텍스트, 테두리 값은 한 세트로 조정되어 있기 때문입니다. 어두운 배경에서는 브랜드 색상을 보통 밝게 해야 하며, primary 버튼의 텍스트는 어두운 색으로 바뀝니다.
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(라이트)
brand_dark
ui.theme.dark(기본)어떤 token이 무엇에 영향을 주는가
컴포넌트가 실제로 읽는 필드 기준으로 정리했습니다(v0.1.0-alpha 소스). 테마를 만들 때는 이것부터 바꾸십시오:
fg_primary · fg_secondary · fg_tertiary · fg_disabled거의 모든 텍스트와 아이콘bg_primary · bg_secondary · bg_tertiary패널, 카드, Calendar, Chip, Progress / Slider 트랙, Skeletonbg_hover · bg_active클릭 가능한 컨트롤의 호버·눌림 상태accent선택된 Checkbox / Switch, 링크, Tabs, Breadcrumb, 선택된 날짜, Badgebutton_primary_bg · button_primary_fgprimary Button(hover / 눌림은 배경에서 파생). fg는 Badge, Chip, Tabs의 반전 텍스트에도 사용border · border_strong · separator테두리와 구분선border_focus · input_bg · input_border · selection_bgInput, Select, NumberStepper, DatePicker 등 입력 컨트롤success · warning · danger · info · status_fgAlert, Badge, Tag, Progress, danger Buttonlist_hover_bg · list_selection_bgMenu, DropdownMenu, ComboBox, Table, Tree, DataTablecheckbox_* · switch_thumb · switch_track_offCheckbox / Switch의 미선택·비활성 상태tooltip_bg · tooltip_fg · overlay · scrollbar_thumbTooltip, Modal / Sheet 스크림, 스크롤바일부 컴포넌트는 ColorTokens를 읽지 않고 자체 색상을 가집니다. Notifier는 scheme에 따라 내장된 라이트 / 다크 팔레트 중 하나를 고르고, GlassBox의 유리 색조와 Progress, Select의 일부 세부 요소도 고정값입니다. 라이트/다크는 따라가지만 브랜드 색상으로 바뀌지는 않습니다.
모서리 반경, 간격, 컨트롤 치수
색상이 아닌 token은 필드 단위로 덮어쓸 수 있습니다. 가장 유용한 것은 control입니다. Button, Input, Select, ComboBox, DatePicker, DateRangePicker, Tabs, Chip은 xs / sm / md / lg 네 단계의 치수를 공유합니다. 컨트롤의 높이는 token이 아니라 계산됩니다:
xs2.5 × 2 + 12 × 1.25 = 20sm4.5 × 2 + 12 × 1.25 = 24md7.25 × 2 + 14 × 1.25 = 32lg10 × 2 + 16 × 1.25 = 40따라서 md 컨트롤을 더 높게 하려면 t.control.md.padding_y나 글자 크기를 바꾸면 되고, 같은 단계의 모든 컨트롤이 함께 바뀝니다. 컨트롤에 height를 하드코딩하지 마십시오. icon_size는 줄 높이보다 크지 않게 유지합니다.
런타임 전환
cx.setTheme는 언제든 호출할 수 있습니다. token 포인터를 교체하고, cx.root 서브트리(오버레이 portal 포함)에서 on_theme / before_render hook을 다시 실행한 뒤 다시 그리기를 요청합니다.
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;배포 전 점검
- ✓
scheme이 배경의 명암과 일치합니다(어두운 배경이면 반드시.dark). - ✓
button_primary_fg와button_primary_bg,status_fg와danger의 대비가 최소 4.5:1입니다 — 기본 제공 프리셋의 단위 테스트와 같은 기준입니다. - ✓
border가bg_primary와 여전히 구분됩니다(프리셋 테스트는 대비 ≥ 1.2를 요구). - ✓
Storybook을 여러분의 테마로 바꾸고 자주 쓰는 컴포넌트를 모두 눌러 봅니다(Component Wall story부터 시작하면 좋습니다).