アプリ内通知
Notifier を使うと、作業を妨げないウィンドウ内通知(成功、エラー、進捗、取り消し、メッセージ)を表示できます。スタック、ホバーでの展開、8 つの表示位置はすべて Notifier が管理します。
Notifier とは
ui.widgets.Notifier はウィンドウ単位のサービスです。一度初期化すれば、どこからでも show を呼べます。カードはウィンドウのオーバーレイ portal の toast 層(z 2000、ダイアログより上、tooltip より下)に置かれ、ページのレイアウトには関与しません。各カードは固定のノードを持つため、1 枚を削除しても他のカードが再構築されたり入場アニメーションが再生されたりすることはありません。以前の ToastManager を置き換えるものです。
フィールドごとの API(Notification、NotifierOptions、全メソッドのシグネチャ)は、コンポーネントサイトの Notification ページを参照してください。
Notifier の作成
ルートのマウント関数で一度だけ作成し、通知を出す必要があるコード(たとえば cx.bindState で保持するアプリ状態)にポインターを渡します。Notifier は渡した Scope が所有し、Scope とともに破棄されます。
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,
});position8 つの表示位置のいずれか。既定は .bottom_center。実行時に setPosition で切り替え可能max_visible展開時に表示する最大件数(2–6、既定 4)expand_on_hoverfalse のとき、ホバーはタイマーを止めるだけで展開しないcontent_insetsウィンドウ端からのインセット(タイトルバー、ツールバー、ステータスバー)。22px の配置マージンはここから数えるwidthカードの幅。既定 392stringsUI 文言("now"、"Undo"、"Reply…"、折りたたみ時のバッジなど)。ローカライズ時に差し替えるlistenerユーザー操作と期限切れのコールバック。setListener でも設定可能通知を表示する
show は ui.widgets.Notification の値を受け取り、その id を返します。文字列はすべて Notifier がコピーして保持するので、呼び出し側のバッファはすぐに再利用できます。画面上が 6 件を超えると、最も古いカードが通常の退場アニメーションで消えます。
// 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,
});種類・トーン・表示時間
カードの見た目は kind(レイアウト)と tone(意味色)の 2 軸で決まります。先頭のアイコンは両者から導かれますが、lead / glyph で明示的に指定することもできます。
.defaultアイコン + タイトル + 本文。ボタン、返信欄、アバターは任意.progressスピナー + 確定進捗バー + n / N のカウント。寿命バーは表示しない.undo残り秒数を中央に表示するカウントダウンリング + 既定の「Undo」ボタン.success操作の完了3.6 s.@"error"対応が必要な失敗常駐.warning注意が必要だが妨げない5.6 s.info一般的な情報(既定)4.2 s.violet人物、招待などのソーシャルなメッセージ4.2 s。ボタン付きなら常駐.quiet控えめなステータス(アイコンの代わりに小さな点)2.8 s- ✓
既定で常駐:progress カード、返信欄付きのカード、error トーン、ボタン付きの violet カードは自動では消えません。
- ✓
明示的な上書き:
sticky = trueで常駐を強制、duration_msで既定の時間を置き換え、life_barで下部の寿命バーの表示を切り替えます。 - ✓
undo の既定は 7 秒で、カウントダウンリングはタイマーと同期します。
その場での更新と進捗
update(id, …) は同じカードの種類、トーン、タイトル、本文を書き換え、寿命をリセットします。破棄も再構築も入場の再生もありません。進捗カードは setProgress で進め、これは進捗レイヤーだけを更新します。既存の id で show を呼んでも、その場で変換されます。
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,
});ユーザー操作の処理
Notifier は 1 つの ui.widgets.NotificationListener を通じてイベントを通知します。NotificationEvent にはカードの id、イベントの kind、そしてボタンの tag / index または返信内容の text が含まれます。
.actionボタンがクリックされた(tag で区別).undoUndo がクリックされた。リングはその場で ✓ に変わっている.reply返信欄が送信された(Enter または Send)。内容は text.dismissedユーザーが閉じた(✕ またはスワイプで払う).expired時間切れで閉じたconst 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 });スタックとホバー
複数の通知は 1 つのスタックに折りたたまれます。最新のカードがアンカーに最も近く z 順が最上位で、後ろの各層は 9px ずつはみ出して 3.8% ずつ縮小し、最大 3 層まで見え、「あと N 件 · ホバーで展開」のバッジが付きます。ポインターがスタックの外接矩形(18px 拡張)に入ると 9px 間隔で展開し、すべてのタイマーが一時停止します。離れると折りたたまれ、タイマーが再開します。
バッジの横に「すべて消去」ボタンがあります。1 回目のクリックでラベルが現れ、2 回目で実際に消去し、ポインターを離すと元に戻ります。コードでは dismissAll() を呼びます。
1 枚のカードをドラッグして払いのけると閉じ、dismissed イベントが発生します。
ウィンドウサイズが変わると、スタックはアンカーにぴったり追従し、補間による遅れは生じません。
表示位置
setPosition が変えるのはアンカー、伸びる方向、入場時のオフセットだけで、カードは再構築されません。退場中のカードは新しい方向で退場を終えます。
通知センターはアプリ側の責務
履歴、おやすみモード、ベルの入口といった「通知センター」の機能はアプリのロジックであり、zenit には含まれません。show を呼ぶ場所で履歴を記録し、そもそもカードを出すかどうかを判断し、閉じる・期限切れ・ボタン操作は Listener で受け取ります。Storybook の「ダイジェスト」カードがこのパターンの例で、おやすみモードの終了後に 1 枚の quiet カードで届いた内容をまとめます。