Button
ui.widgets.ButtonBotones primarios, secundarios, destructivos, tipo enlace y solo icono.
BEHAVIOR CONTRACT
Un solo modelo para variant × size, loading, disabled, block, iconos iniciales y activación por teclado.
RECORDED INTERACTION
Recorre la matriz variant × size y luego hace hover y pulsa Primary.
pub fn buildButton(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
const a = cx.allocator;
const icons = ui.assets.common;
const c = try col(cx, 16);
const variant_specs = [_]struct { v: W.ButtonVariant, t: []const u8 }{
.{ .v = .primary, .t = "Primary" },
.{ .v = .secondary, .t = "Secondary" },
.{ .v = .ghost, .t = "Ghost" },
.{ .v = .danger, .t = "Danger" },
.{ .v = .link, .t = "Link" },
};
const size_specs = [_]struct { s: W.ButtonSize, t: []const u8 }{
.{ .s = .xs, .t = "XS" },
.{ .s = .sm, .t = "SM" },
.{ .s = .md, .t = "MD" },
.{ .s = .lg, .t = "LG" },
};
// ── variant × size 全矩阵 ──
try c.appendChild(a, try label(cx, "Variants × Sizes"));
const matrix = try col(cx, 8);
inline for (variant_specs) |vs| {
const r = try row(cx, 10);
inline for (size_specs) |ss| {
try r.appendChild(a, try W.Button(.{
.label = vs.t,
.variant = vs.v,
.size = ss.s,
}).mount(scope, cx));
}
try matrix.appendChild(a, r);
}
try c.appendChild(a, matrix);
// ── 带前置图标(leading icon)每个 variant ──
try c.appendChild(a, try label(cx, "With leading icon"));
const icon_row = try row(cx, 10);
inline for (variant_specs) |vs| {
try icon_row.appendChild(a, try W.Button(.{
.label = vs.t,
.variant = vs.v,
.icon_asset = icons.plus,
}).mount(scope, cx));
}
try c.appendChild(a, icon_row);
// ── leading icon × sizes(图标随尺寸缩放)──
try c.appendChild(a, try label(cx, "Leading icon × sizes"));
const icon_size_row = try row(cx, 10);
inline for (size_specs) |ss| {
try icon_size_row.appendChild(a, try W.Button(.{
.label = "Download",
.variant = .secondary,
.size = ss.s,
.icon_asset = icons.folder,
.icon_size = cx.tokens.control.get(ss.s).icon_size,
}).mount(scope, cx));
}
try c.appendChild(a, icon_size_row);
// ── icon-only(仅图标)每个 variant ──
try c.appendChild(a, try label(cx, "Icon only"));
const icon_only_row = try row(cx, 10);
inline for (variant_specs) |vs| {
try icon_only_row.appendChild(a, try W.Button(.{
.variant = vs.v,
.icon_only = true,
.icon_asset = icons.star,
}).mount(scope, cx));
}
try c.appendChild(a, icon_only_row);
// ── icon-only × sizes ──
try c.appendChild(a, try label(cx, "Icon only × sizes"));
const icon_only_sizes = try row(cx, 10);
inline for (size_specs) |ss| {
try icon_only_sizes.appendChild(a, try W.Button(.{
.variant = .secondary,
.icon_only = true,
.icon_asset = icons.search,
.size = ss.s,
.icon_size = cx.tokens.control.get(ss.s).icon_size,
}).mount(scope, cx));
}
try c.appendChild(a, icon_only_sizes);
// ── loading(加载中)每个 variant ──
try c.appendChild(a, try label(cx, "Loading"));
const loading_row = try row(cx, 10);
inline for (variant_specs) |vs| {
try loading_row.appendChild(a, try W.Button(.{
.label = "Saving…",
.variant = vs.v,
.loading = true,
}).mount(scope, cx));
}
try c.appendChild(a, loading_row);
// ── loading × sizes(spinner 随尺寸缩放)──
try c.appendChild(a, try label(cx, "Loading × sizes"));
const loading_sizes = try row(cx, 10);
inline for (size_specs) |ss| {
try loading_sizes.appendChild(a, try W.Button(.{
.label = "Loading",
.variant = .primary,
.size = ss.s,
.loading = true,
}).mount(scope, cx));
}
try c.appendChild(a, loading_sizes);
// ── 状态:normal / disabled / disabled+icon / loading+icon-only ──
try c.appendChild(a, try label(cx, "States"));
const states = try row(cx, 10);
try states.appendChild(a, try W.Button(.{ .label = "Normal" }).mount(scope, cx));
try states.appendChild(a, try W.Button(.{ .label = "Disabled", .disabled = true }).mount(scope, cx));
try states.appendChild(a, try W.Button(.{ .label = "Disabled", .disabled = true, .icon_asset = icons.plus }).mount(scope, cx));
try states.appendChild(a, try W.Button(.{ .label = "Loading", .loading = true }).mount(scope, cx));
try states.appendChild(a, try W.Button(.{ .icon_only = true, .icon_asset = icons.x_close, .loading = true, .variant = .secondary }).mount(scope, cx));
try c.appendChild(a, states);
// ── block(块级,宽度填充)──
try c.appendChild(a, try label(cx, "Block (full width)"));
const block_col = try col(cx, 8);
block_col.style.width = .{ .px = 280 };
try block_col.appendChild(a, try W.Button(.{ .label = "Block primary", .block = true }).mount(scope, cx));
try block_col.appendChild(a, try W.Button(.{ .label = "Block + icon", .block = true, .variant = .secondary, .icon_asset = icons.check }).mount(scope, cx));
try c.appendChild(a, block_col);
return c;
}El código literal de la story en el Storybook (zenit 5f9add5+wip 2026-09-30). W es ui.widgets; col / row / label son pequeños helpers de layout del Storybook.
ButtonProps
ui.widgets.Buttonfn Button(props: ButtonProps) ButtonBuilderCampo
Tipo
Predeterminado
Descripción
label?[]const u8null标签文本
variantButtonVariant.primary.secondary.ghost.danger.link.field.primary变体
sizeButtonSize.md尺寸
icon_onlyboolfalse图标模式(只显示图标)
loadingboolfalse加载中
disabledboolfalse禁用
blockboolfalse块级(宽度填充) 非 block Button 保持 intrinsic 宽度:父容器交叉轴为 stretch(默认)时按 start 排而不被拉伸;父显式 align_items=.center/.end 时照常居中/靠尾。
on_click?core.HandlerRefnull点击 handler
icon?u32null图标纹理 ID(放在 label 文本左侧)
icon_asset?svg_assets.AssetnullSVG 图标资源(优先于 texture icon)
icon_size?f32null图标尺寸(null = 跟随 control metrics 的 icon_size;应 ≤ 行高)
icon_tint?Colornull图标 tint 颜色(默认跟随 textColor)
on_event?GenericEventCallbacknull通用事件处理(支持 hover/key 等复合事件)
event_context?*anyopaquenull事件上下文
样式覆盖
styleStyleOverride.{}默认态样式覆盖
hover_style?StyleOverridenullhover 态样式覆盖
pressed_style?StyleOverridenullpressed 态样式覆盖
*Node
ui.widgets.Buttonmount devuelve *Node directamente: añádelo a un nodo padre.