Notification
ui.widgets.NotifierNotificaciones no bloqueantes: nueve tipos, progreso, acciones, apilado y ocho posiciones de anclaje.
BEHAVIOR CONTRACT
El Notifier gestiona la pila del portal, la entrada / salida, la expansión al pasar el cursor con temporizadores en pausa, y mantiene la pila fijada a su ancla al redimensionar; las tarjetas usan un material de vidrio blanco.
RECORDED INTERACTION
El harness lanza los nueve tipos (dejando que termine la tarjeta de progreso), dispara cuatro seguidas para formar una pila, la expande con el cursor, cierra la tarjeta central y luego las borra todas.
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;
}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.
Notification
ui.widgets.NotificationCampo
Tipo
Predeterminado
Descripción
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.NotifierOptionsCampo
Tipo
Predeterminado
Descripción
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回复发送失败:原地转为错误类型并把刚才的内容恢复进回复框。