docs/advanced/notifications
앱 기능 · Notifier

앱 내 알림

Notifier를 사용하면 현재 작업을 막지 않는 창 내 알림(성공, 오류, 진행률, 실행 취소, 메시지)을 띄울 수 있습니다. 스택, 호버 시 펼치기, 8가지 고정 위치는 모두 Notifier가 처리합니다.

약 9분 분량 · 실제 창 녹화 포함

Notifier란

ui.widgets.Notifier는 창 단위 서비스입니다. 한 번 초기화한 뒤 어디서든 show를 호출하면 됩니다. 카드는 창 오버레이 portal의 toast 계층(z 2000, 대화상자보다 위, tooltip보다 아래)에 놓이며 페이지 레이아웃에 참여하지 않습니다. 각 카드는 고정된 노드를 가지므로 하나를 제거해도 다른 카드가 다시 빌드되거나 등장 애니메이션이 재생되지 않습니다. 이전의 ToastManager를 대체합니다.

실제 Storybook: 성공, 오류, 경고, 진행률, 동작 필요, 사람 메시지, 실행 취소, 조용한 알림, 요약을 차례로 띄운 뒤, 4개를 연달아 띄워 스택을 만들고 호버로 펼친 다음 가운데 카드를 닫습니다.

필드별 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(의미 색상) 두 축으로 정해집니다. 앞쪽 아이콘은 둘로부터 도출되며, 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는 하나의 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 });

스택과 호버

여러 알림은 하나의 스택으로 접힙니다. 가장 최신 카드가 앵커에 가장 가깝고 z 순서가 가장 높으며, 뒤의 각 층은 9px씩 드러나고 3.8%씩 작아지며 최대 세 층까지 보이고, "N개 더 · 호버하여 펼치기" 배지가 붙습니다. 포인터가 스택 경계(18px 확장)에 들어오면 9px 간격으로 펼쳐지고 모든 타이머가 멈추며, 벗어나면 다시 접히고 타이머가 재개됩니다.

STACK
수치는 notification/model.zig의 Spec에서 가져왔습니다: 드러남 9px, 층당 축소 3.8%, 펼침 간격 9px.
01
모두 지우기

배지 옆에 모두 지우기 버튼이 있습니다. 첫 클릭에 레이블이 펼쳐지고 두 번째 클릭에 실제로 지우며, 포인터를 치우면 다시 접힙니다. 코드에서는 dismissAll()을 호출합니다.

02
밀어서 닫기

카드 한 장을 끌어서 밀어내면 닫히며 dismissed 이벤트가 발생합니다.

03
창 크기 조절

창 크기가 바뀌면 스택이 앵커를 그대로 따라가며, 보간으로 인한 끌림이 없습니다.

위치

setPosition은 앵커, 성장 방향, 등장 오프셋만 바꾸며 카드는 다시 빌드되지 않습니다. 퇴장 중인 카드는 새 방향으로 퇴장을 마칩니다.

POSITIONS
같은 알림 4개를 8가지 위치에 차례로 고정한 모습입니다. 왼쪽 위치는 창 가장자리에 붙어 있어 창 전체를 담았습니다.

알림 센터는 앱의 몫

히스토리, 방해 금지, 종 모양 진입점 같은 알림 센터 기능은 앱 로직이며 zenit 밖에 있습니다. show를 호출하는 곳에서 히스토리를 기록하고 카드를 띄울지 결정하며, 닫힘·만료·버튼 동작은 Listener로 전달받습니다. Storybook의 요약 카드가 이 패턴을 보여 줍니다. 방해 금지가 끝나면 quiet 카드 하나로 그동안 도착한 내용을 요약합니다.

zenit · 이중 라이선스오픈 소스 프로젝트는 GPL-3.0-only로 무료 사용할 수 있으며, 비공개 소스나 상용 제품에는 상용 라이선스가 필요합니다.작성자 연락처: zongyi.xzy#gmail.com (#을 @로 바꾸세요)zenit 5f9add5+wip 2026-09-30