docs/advanced/notifications
アプリ機能 · Notifier

アプリ内通知

Notifier を使うと、作業を妨げないウィンドウ内通知(成功、エラー、進捗、取り消し、メッセージ)を表示できます。スタック、ホバーでの展開、8 つの表示位置はすべて Notifier が管理します。

約 9 分で読めます · 実ウィンドウの録画付き

Notifier とは

ui.widgets.Notifier はウィンドウ単位のサービスです。一度初期化すれば、どこからでも show を呼べます。カードはウィンドウのオーバーレイ portal の toast 層(z 2000、ダイアログより上、tooltip より下)に置かれ、ページのレイアウトには関与しません。各カードは固定のノードを持つため、1 枚を削除しても他のカードが再構築されたり入場アニメーションが再生されたりすることはありません。以前の ToastManager を置き換えるものです。

実際の Storybook:成功、エラー、警告、進捗、要操作、人物メッセージ、取り消し、控えめ、ダイジェストを順に表示し、続けて 4 件を連続表示してスタックにし、ホバーで展開してから中央の 1 件を閉じます。

フィールドごとの API(Notification、NotifierOptions、全メソッドのシグネチャ)は、コンポーネントサイトの Notification ページを参照してください。

Notifier の作成

ルートのマウント関数で一度だけ作成し、通知を出す必要があるコード(たとえば cx.bindState で保持するアプリ状態)にポインターを渡します。Notifier は渡した Scope が所有し、Scope とともに破棄されます。

app.zig
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,
});
NotifierOptions のフィールド役割
position8 つの表示位置のいずれか。既定は .bottom_center。実行時に setPosition で切り替え可能
max_visible展開時に表示する最大件数(2–6、既定 4)
expand_on_hoverfalse のとき、ホバーはタイマーを止めるだけで展開しない
content_insetsウィンドウ端からのインセット(タイトルバー、ツールバー、ステータスバー)。22px の配置マージンはここから数える
widthカードの幅。既定 392
stringsUI 文言("now"、"Undo"、"Reply…"、折りたたみ時のバッジなど)。ローカライズ時に差し替える
listenerユーザー操作と期限切れのコールバック。setListener でも設定可能

通知を表示する

show は ui.widgets.Notification の値を受け取り、その id を返します。文字列はすべて Notifier がコピーして保持するので、呼び出し側のバッファはすぐに再利用できます。画面上が 6 件を超えると、最も古いカードが通常の退場アニメーションで消えます。

notify.zig
// 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 で明示的に指定することもできます。

kindレイアウト
.defaultアイコン + タイトル + 本文。ボタン、返信欄、アバターは任意
.progressスピナー + 確定進捗バー + n / N のカウント。寿命バーは表示しない
.undo残り秒数を中央に表示するカウントダウンリング + 既定の「Undo」ボタン
tone用途既定の表示時間
.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 を呼んでも、その場で変換されます。

upload.zig
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 が含まれます。

EventKind発生するタイミング
.actionボタンがクリックされた(tag で区別)
.undoUndo がクリックされた。リングはその場で ✓ に変わっている
.reply返信欄が送信された(Enter または Send)。内容は text
.dismissedユーザーが閉じた(✕ またはスワイプで払う)
.expired時間切れで閉じた
uploads.zig
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 間隔で展開し、すべてのタイマーが一時停止します。離れると折りたたまれ、タイマーが再開します。

STACK
数値は notification/model.zig の Spec から:はみ出し 9px、層ごとの縮小 3.8%、展開時の間隔 9px。
01
すべて消去

バッジの横に「すべて消去」ボタンがあります。1 回目のクリックでラベルが現れ、2 回目で実際に消去し、ポインターを離すと元に戻ります。コードでは dismissAll() を呼びます。

02
スワイプで閉じる

1 枚のカードをドラッグして払いのけると閉じ、dismissed イベントが発生します。

03
ウィンドウのリサイズ

ウィンドウサイズが変わると、スタックはアンカーにぴったり追従し、補間による遅れは生じません。

表示位置

setPosition が変えるのはアンカー、伸びる方向、入場時のオフセットだけで、カードは再構築されません。退場中のカードは新しい方向で退場を終えます。

POSITIONS
同じ 4 件の通知を 8 つの位置に順に配置しています。左側の位置はウィンドウの左端に接するため、ウィンドウ全体を映しています。

通知センターはアプリ側の責務

履歴、おやすみモード、ベルの入口といった「通知センター」の機能はアプリのロジックであり、zenit には含まれません。show を呼ぶ場所で履歴を記録し、そもそもカードを出すかどうかを判断し、閉じる・期限切れ・ボタン操作は Listener で受け取ります。Storybook の「ダイジェスト」カードがこのパターンの例で、おやすみモードの終了後に 1 枚の quiet カードで届いた内容をまとめます。

zenit · デュアルライセンスオープンソースプロジェクトは GPL-3.0-only のもとで無料で使えます。クローズドソースや商用製品には商用ライセンスが必要です。作者への連絡先:zongyi.xzy#gmail.com(# を @ に置き換え)zenit 5f9add5+wip 2026-09-30