应用内通知Notifications
用 Notifier 在窗口内弹出不打断当前任务的通知:成功、错误、进度、撤销、人员消息,堆叠、悬停展开、八个停靠位置都由它管理。Use the Notifier for in-window notifications that never block the task at hand — success, error, progress, undo and messages — with stacking, hover-to-expand and eight docking positions handled for you.
Notifier 是什么What the Notifier is
ui.widgets.Notifier 是一个窗口级服务:初始化一次,之后在任何地方调用 show。通知卡挂在窗口浮层 portal 的 toast 层(z 2000,高于对话框、低于 tooltip),不参与页面布局;每张卡有自己固定的节点,删除其中一张不会让其他卡片重建或重放入场动画。它取代了早期版本的 ToastManager。ui.widgets.Notifier is a window-level service: initialize it once, then call show from anywhere. Cards live on the toast tier of the window overlay portal (z 2000 — above dialogs, below tooltips) and never take part in page layout. Each card owns a fixed node, so removing one never rebuilds the others or replays their entrance. It replaces the earlier ToastManager.
每种组件的逐项 API(Notification 字段、NotifierOptions、全部方法签名)见组件站的 Notification 页面。The field-by-field API (Notification, NotifierOptions and every method signature) is on the component site’s Notification page.
创建 NotifierCreating a Notifier
在根挂载函数里创建一次,把指针交给需要发通知的代码(例如放进 cx.bindState 的应用状态)。Notifier 由传入的 Scope 持有,Scope 释放时一并移除。Create it once in your root mount function and hand the pointer to whatever needs to notify (for example, app state held by cx.bindState). The Notifier is owned by the Scope you pass and goes away with it.
const notifier = try ui.widgets.Notifier.init(scope, cx, .{
.position = .bottom_center,
// Keep the stack clear of a 56px toolbar.
.content_insets = .{ .top = 56 },
.max_visible = 4,
});position八个停靠位置之一,默认 .bottom_center;运行时可用 setPosition 切换One of eight positions, default .bottom_center; switch at runtime with setPositionmax_visible展开时最多显示的条数(2–6,默认 4)How many cards show when expanded (2–6, default 4)expand_on_hoverfalse 时悬停只暂停计时、不展开When false, hover only pauses timers and does not expandcontent_insets相对窗口的内缩(标题栏、工具栏、状态栏),22px 停靠边距从这里起算Insets from the window edges (title bar, toolbar, status bar); the 22px docking margin starts herewidth卡片宽度,默认 392Card width, default 392strings界面文案("刚刚"、"撤销"、"回复…"、折叠角标等),做本地化时替换UI strings ("now", "Undo", "Reply…", the collapsed badge …) for localizationlistener用户操作与到期事件的回调,也可用 setListener 设置Callback for user actions and expiry; also settable with setListener弹出一条通知Showing a notification
show 接收一个 ui.widgets.Notification 值并返回它的 id;字符串会被 Notifier 复制持有,调用方的缓冲区可以立即复用。同屏超过 6 条时,最旧的一条会走正常的退场动画。show takes a ui.widgets.Notification value and returns its id. The Notifier copies every string, so your buffers can be reused right away. Beyond six cards on screen, the oldest leaves with its normal exit animation.
// Success: auto-dismisses after 3.6 s.
_ = try notifier.show(.{
.tone = .success,
.title = "Saved to iCloud",
.body = "3 files synced · 2.1 MB",
});
// Error with two actions: errors stay until the user acts.
const failed = try notifier.show(.{
.tone = .@"error",
.title = "Upload failed",
.body = "network timeout · dist/app.zip",
.actions = &.{
.{ .label = "Retry", .primary = true, .tag = "retry" },
.{ .label = "View log", .tag = "log" },
},
});
// Undo: a 7 s countdown ring and a default "Undo" button.
_ = try notifier.show(.{
.kind = .undo,
.title = "Moved 4 items to the Trash",
.body = "You can undo this for 7 seconds.",
});
// A person's message with an inline reply box.
_ = try notifier.show(.{
.tone = .violet,
.avatar = "L",
.title = "Lin",
.body = "I finished the table widths in section three.",
.reply = true,
});类型、色调与停留时长Kinds, tones and lifetimes
外观由 kind(版式)和 tone(语义色)两个维度决定;行首图标默认从两者推导,也可以用 lead 或 glyph 覆盖。A card’s look comes from two axes: kind (layout) and tone (semantic color). The leading icon is derived from both, or set explicitly with lead / glyph.
.default图标 + 标题 + 正文,可选按钮、回复框、头像Icon, title and body, with optional buttons, reply box or avatar.progress旋转指示器 + 确定进度条 + n / N 计数,不显示生命条Spinner, determinate bar and an n / N count; no life bar.undo倒计时环(中心显示剩余秒数)+ 默认的「撤销」按钮Countdown ring showing seconds left, plus a default "Undo" button.success操作完成Something finished3.6 s.@"error"失败,需要用户处理A failure that needs attention常驻sticky.warning需要注意但不阻塞Worth noticing, not blocking5.6 s.info一般信息(默认)General information (default)4.2 s.violet人员、邀请等社交类消息People, invitations and other social messages4.2 s;带按钮时常驻4.2 s; sticky with buttons.quiet低打扰的状态提示(小圆点图标)Low-key status (a small dot instead of an icon)2.8 s- ✓
自动常驻:progress 卡、带回复框的卡、error 色调、带按钮的 violet 卡默认不会自动消失。Sticky by default: progress cards, cards with a reply box, error tones and violet cards with buttons never auto-dismiss.
- ✓
显式覆盖:
sticky = true强制常驻;duration_ms覆盖默认时长;life_bar控制底部生命条是否显示。Explicit overrides:sticky = trueforces it;duration_msreplaces the default;life_bartoggles the lifetime bar. - ✓
undo 的时长默认 7 秒,倒计时环与计时同步。Undo defaults to 7 seconds, with the ring tracking the timer.
原地转换与进度In-place updates and progress
update(id, …) 改写同一张卡的类型、色调、标题与正文,并重置生命周期;卡片不销毁、不重建,也不重放入场。进度卡用 setProgress 逐帧推进,只更新进度层。用同一个 id 再次调用 show 也会走原地转换。update(id, …) rewrites the same card’s kind, tone, title and body and restarts its lifetime — no destroy, no rebuild, no replayed entrance. Drive a progress card with setProgress, which touches only the progress layer. Calling show with an existing id also transforms in place.
const id = try notifier.show(.{
.kind = .progress,
.title = "Uploading attachment",
.body = "assets/hero-shot.png",
.progress = .{ .value = 0, .label = "Uploading · about 5 s", .count = "0 / 5" },
});
// Each tick: only the progress layer is touched, the card isn't rebuilt.
notifier.setProgress(id, 0.6, "Uploading · about 2 s", "3 / 5");
// Done: the same card turns into a success card in place —
// no new entrance, and it now dismisses itself after 3.4 s.
try notifier.update(id, .{
.tone = .success,
.title = "Upload complete",
.body = "assets/hero-shot.png",
.duration_ms = 3400,
});处理用户操作Handling user actions
Notifier 通过一个 ui.widgets.NotificationListener 回报事件。NotificationEvent 带有卡片 id、事件类型 kind,以及按钮的 tag / index 或回复内容 text。The Notifier reports through a single ui.widgets.NotificationListener. A NotificationEvent carries the card id, the event kind, and the button tag / index or the reply text.
.action点击了按钮(用 tag 区分)A button was clicked (tell them apart by tag).undo点击了撤销;倒计时环已原地换成 ✓Undo was clicked; the ring has already turned into a ✓.reply回复框提交(Enter 或「发送」),text 为内容The reply box was submitted (Enter or Send); text holds it.dismissed用户关闭(✕ 或拖拽甩出)The user closed it (✕ or a fling).expired到期自动收起It timed outconst Uploads = struct {
fn onEvent(ctx: ?*anyopaque, n: *ui.widgets.Notifier, e: ui.widgets.NotificationEvent) void {
const self: *Uploads = @ptrCast(@alignCast(ctx orelse return));
switch (e.kind) {
.action => if (std.mem.eql(u8, e.tag, "retry")) {
// Turn the sticky error card into a progress card in place.
n.update(e.id, .{
.kind = .progress,
.title = "Retrying upload",
.progress = .{ .value = 0, .label = "Uploading", .count = "0 / 5" },
}) catch return;
self.restart(e.id);
} else n.dismiss(e.id),
.reply => self.send(e.text),
.undo => self.restoreTrash(),
.dismissed, .expired => {},
}
}
// …restart / send / restoreTrash
};
const uploads = try cx.bindState(Uploads, .{});
notifier.setListener(.{ .context = @ptrCast(uploads), .callback = Uploads.onEvent });堆叠与悬停Stacking and hover
多条通知折叠成一叠:最新的一条离锚点最近、z 序最高,后面的每层露边 9px 并缩小 3.8%,最多露出三层,另有一个「还有 N 条 · 悬停展开」角标。指针进入整组外接矩形(外扩 18px)时展开,间距 9px,同时暂停所有计时;移开后折叠并继续计时。Several notifications collapse into a stack: the newest sits closest to the anchor with the highest z; each layer behind peeks out 9px and shrinks 3.8%, up to three layers, with an "N more · hover to expand" badge. When the pointer enters the stack’s bounds (grown by 18px), it expands with 9px gaps and every timer pauses; leaving collapses it and resumes.
角标旁有一个「全部清除」圆钮:第一下展开出文字,第二下才真正清除,移开即收回。代码里用 dismissAll()。Next to the badge sits a clear-all button: the first click reveals its label, the second actually clears, and moving away folds it back. In code, call dismissAll().
单张卡可以被拖出去关闭,会产生 dismissed 事件。A single card can be flung away, which reports dismissed.
窗口尺寸变化时堆叠刚性跟随锚点,不会出现补间拖尾。On window resize the stack follows its anchor rigidly, with no trailing tween.
停靠位置Positions
setPosition 只改锚点、增长方向和入场位移方向,卡片不重建;正在退场的卡片沿新方向继续完成退场。setPosition changes only the anchor, growth direction and entrance offset — cards aren’t rebuilt, and any card that is leaving finishes its exit in the new direction.
通知中心属于应用The notification center is yours
历史记录、勿扰模式、铃铛入口这些「通知中心」能力是业务逻辑,不在 zenit 里:在你调用 show 的地方记录历史、决定是否弹出,再通过 Listener 得知关闭、到期与按钮操作。Storybook 里的「汇总」卡片就是这种模式——勿扰结束后用一条 quiet 通知概括期间收到的内容。History, do-not-disturb and a bell entry point — the notification-center features — are app logic and live outside zenit: record history where you call show, decide whether to pop a card at all, and learn about dismissals, expiry and button actions through the Listener. The Storybook’s Digest card shows the pattern: after do-not-disturb ends, one quiet card summarizes what arrived.