Notification
ui.widgets.Notifier不打断当前任务的系统级通知:九种类型、进度、操作按钮、堆叠与八个停靠位置。
BEHAVIOR CONTRACT
Notifier 管理 portal 栈、入场 / 退场、悬停展开与暂停计时、窗口尺寸变化时堆叠跟随锚点;卡片使用白玻璃材质。
RECORDED INTERACTION
Harness 依次触发全部九种类型(进度卡跑完到成功),再连发 4 条形成堆叠,悬停展开并关闭中间一条,最后全部收起。
pub fn build(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
const t = cx.tokens;
const root = try ui.box(cx, .{ .width = .{ .grow = .{} }, .direction = .column, .gap = 12 }, .{});
const n = try Notifier.init(scope, cx, .{
.position = .bottom_center,
// storybook 顶部 56px 工具栏。
.content_insets = .{ .top = 56 },
.strings = zh_strings,
});
const status = try ui.text(cx, "event · idle", .{ .font_size = 12, .font_weight = 550, .color = t.color.fg_secondary });
status.meta.ownership.meta.test_id = "story.notify.status";
const state = try cx.bindState(Story, .{ .n = n, .cx = cx, .status = status });
n.setListener(.{ .context = @ptrCast(state), .callback = Story.onEvent });
root.meta.per_frame.hooks.slots.anim_state = @ptrCast(state);
root.meta.per_frame.hooks.before_render.main = storyHook;
const kinds = try controlRow(cx);
try addButton(scope, cx, kinds, state, "成功 Success", "story.notify.btn.success", Story.success, .secondary);
try addButton(scope, cx, kinds, state, "错误 Error", "story.notify.btn.error", Story.failure, .secondary);
try addButton(scope, cx, kinds, state, "警告 Warning", "story.notify.btn.warning", Story.warning, .secondary);
try addButton(scope, cx, kinds, state, "进行中 Progress", "story.notify.btn.progress", Story.progress, .secondary);
try addButton(scope, cx, kinds, state, "需要操作 Action", "story.notify.btn.action", Story.action, .secondary);
try addButton(scope, cx, kinds, state, "人员消息 Person", "story.notify.btn.person", Story.person, .secondary);
try addButton(scope, cx, kinds, state, "撤销 Undo", "story.notify.btn.undo", Story.undo, .secondary);
try addButton(scope, cx, kinds, state, "静默 Quiet", "story.notify.btn.quiet", Story.quiet, .secondary);
try addButton(scope, cx, kinds, state, "汇总 Digest", "story.notify.btn.digest", Story.digest, .secondary);
try root.appendChild(cx.allocator, try group(cx, try sectionLabel(cx, "九种类型 Nine kinds", "卡片 392 宽 · 圆角 18 · 液态玻璃;按钮在分隔线下方,中性灰底不上色。"), kinds));
const stack = try controlRow(cx);
try addButton(scope, cx, stack, state, "连发 4 条", "story.notify.btn.burst", Story.burst, .primary);
try addButton(scope, cx, stack, state, "全部收起", "story.notify.btn.clear", Story.clear, .ghost);
try stack.appendChild(cx.allocator, status);
try root.appendChild(cx.allocator, try group(cx, try sectionLabel(cx, "堆叠 Stack", "折叠露边 9px · 悬停展开并暂停所有计时 · 删除中间一条平滑补位。"), stack));
const pos = try controlRow(cx);
try addButton(scope, cx, pos, state, "上左", "story.notify.pos.tl", Story.posTL, .secondary);
try addButton(scope, cx, pos, state, "上中", "story.notify.pos.tc", Story.posTC, .secondary);
try addButton(scope, cx, pos, state, "上右", "story.notify.pos.tr", Story.posTR, .secondary);
try addButton(scope, cx, pos, state, "右中", "story.notify.pos.rc", Story.posRC, .secondary);
try addButton(scope, cx, pos, state, "右下", "story.notify.pos.br", Story.posBR, .secondary);
try addButton(scope, cx, pos, state, "下中", "story.notify.pos.bc", Story.posBC, .secondary);
try addButton(scope, cx, pos, state, "下左", "story.notify.pos.bl", Story.posBL, .secondary);
try addButton(scope, cx, pos, state, "左中", "story.notify.pos.lc", Story.posLC, .secondary);
try root.appendChild(cx.allocator, try group(cx, try sectionLabel(cx, "八个停靠位置 Positions", "位置只改锚点、增长方向、入场位移方向;切换不重建卡片。"), pos));
// 玻璃需要有内容的背景才看得出来:模拟卡片身后的正文(设计稿里提醒浮在编辑器正文上)。
const backdrop = try ui.box(cx, .{ .width = .{ .grow = .{} }, .direction = .column, .gap = 0 }, .{});
backdrop.meta.ownership.meta.test_id = "story.notify.backdrop";
const tints = [_]u24{ 0xE4572E, 0x17BEBB, 0xFFC914, 0x2E282A, 0x76B041, 0x8059BB };
var li: usize = 0;
while (li < 36) : (li += 1) {
const line = try ui.box(cx, .{ .width = .{ .grow = .{} }, .height = .{ .px = 22 }, .direction = .row, .gap = 28, .padding = ui.Padding.symmetric(0, 12), .align_items = .center, .background = ui.arb.hex(tints[li % tints.len]) }, .{});
var k: usize = 0;
while (k < 6) : (k += 1) try line.appendChild(cx.allocator, try ui.text(cx, "The foundation of any great editor", .{ .font_size = 13, .color = ui.arb.hex(0xFFFFFF) }));
try backdrop.appendChild(cx.allocator, line);
}
try root.appendChild(cx.allocator, backdrop);
if (!n.portaled) try root.appendChild(cx.allocator, n.container);
return root;
}这是 Storybook 中该 story 的原样代码(zenit 5f9add5+wip 2026-09-30)。其中 W 即 ui.widgets,col / row / label 是 Storybook 内的布局小工具。
Notification
ui.widgets.Notification字段
类型
默认值
说明
id?Idnull已存在的 id 会原地转换(保留 born,不重放入场)。
kindKind.default.progress.undo.default—
toneTone.success.@"error".warning.info.violet.quiet.info—
title[]const u8""—
body[]const u8""—
glyph?svg_assets.Assetnull覆盖语义色调的默认字形(✓ / ✕ / △ …)。
lead?Lead.chip.spinner.ring.avatar.dotnull覆盖行首图标位的默认呈现(由 kind / tone / avatar 推导)。
avatar[]const u8""人员消息的头像首字。
actions[]const Action&.{}分隔线下方的等宽按钮(最多 2 个)。undo 类型缺省为「撤销」。
replyboolfalse内联回复框(Enter 或「发送」提交)。
quote?[]const u8null回复已发出后的引用行(原地转换用)。
progress?Progressnull—
stickyboolfalse常驻:不自动消失。
duration_ms?u32null覆盖类型默认停留时长。
life_bar?boolnull覆盖生命条显示(默认:有时限且非 progress 时显示)。
Options
ui.widgets.NotifierOptions字段
类型
默认值
说明
positionPosition.top_left.top_center.top_right.right_center.bottom_right.bottom_center.bottom_left.left_center.bottom_center—
max_visibleu8Spec.default_max_visible展开时最多显示条数(2–6)。
expand_on_hoverbooltruefalse:悬停只暂停计时,不展开。
content_insetsInsets.{}内容区相对窗口的内缩(标题栏 / 状态栏),停靠边距 22 从这里起算。
widthf32Spec.card_width—
stringsStrings.{}—
listener?Listenernull—
Notifier
ui.widgets.Notifierfn init(scope: *Scope, cx: *Cx, options: Options) !*Notifier—
fn show(self: *Notifier, n: Notification) !Id弹出一条提醒;id 已存在时原地转换。同屏超过 6 条时最旧一条走正常退场。
fn update(self: *Notifier, id: Id, n: Notification) !void原地转换:改写 kind / tone / title / body,重置 life,换图标 —— 不销毁、 不重建卡片,born 不变,不重放入场(16.6)。
fn setProgress(self: *Notifier, id: Id, value: f32, label: ?[]const u8, count_text: ?[]const u8) void更新进度卡的进度(不重建内容层)。
fn dismiss(self: *Notifier, id: Id) void—
fn dismissAll(self: *Notifier) void—
fn setPosition(self: *Notifier, position: Position) void位置切换不重建卡片,只改锚点参数;退场中的沿新方向继续完成退场。
fn setListener(self: *Notifier, listener: ?Listener) void—
fn setExpandOnHover(self: *Notifier, on: bool) void—
fn setMaxVisible(self: *Notifier, n: u8) void—
fn count(self: *const Notifier) usize在场(未退场)的条数。
fn isExpanded(self: *const Notifier) bool—
fn isPaused(self: *const Notifier) bool—
fn cardForId(self: *Notifier, id: Id) ?*Card取卡片(测试 id、埋点、截图断言用)。
fn clearAll(self: *Notifier) void用户「全部清除」:所有在屏提醒按用户关闭退场(逐条回调 `.dismissed`)。
fn replyFailed(self: *Notifier, id: Id, title: []const u8, body: []const u8) void回复发送失败:原地转为错误类型并把刚才的内容恢复进回复框。