Notificaciones
Usa el Notifier para notificaciones dentro de la ventana que nunca bloquean la tarea en curso — éxito, error, progreso, deshacer y mensajes —; el apilado, la expansión al pasar el cursor y las ocho posiciones de anclaje ya vienen resueltos.
Qué es el Notifier
ui.widgets.Notifier es un servicio de nivel de ventana: inicialízalo una vez y luego llama a show desde cualquier sitio. Las tarjetas viven en la capa toast del portal de superposición de la ventana (z 2000: por encima de los diálogos, por debajo de los tooltips) y nunca participan en el layout de la página. Cada tarjeta tiene un nodo fijo, así que quitar una nunca reconstruye las demás ni repite su entrada. Sustituye al antiguo ToastManager.
La API campo por campo (Notification, NotifierOptions y la firma de cada método) está en la página de Notification del sitio de componentes.
Crear un Notifier
Créalo una vez en tu función de montaje raíz y pasa el puntero a lo que necesite notificar (por ejemplo, el estado de la app guardado con cx.bindState). El Notifier pertenece al Scope que le pasas y desaparece con él.
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,
});positionUna de ocho posiciones, por defecto .bottom_center; cámbiala en tiempo de ejecución con setPositionmax_visibleCuántas tarjetas se muestran al expandir (2–6, por defecto 4)expand_on_hoverSi es false, el hover solo pausa los temporizadores y no expandecontent_insetsMárgenes interiores desde los bordes de la ventana (barra de título, barra de herramientas, barra de estado); el margen de anclaje de 22px empieza aquíwidthAncho de la tarjeta, por defecto 392stringsTextos de la UI ("now", "Undo", "Reply…", la insignia plegada…) para localizarlistenerCallback para acciones del usuario y expiración; también se puede fijar con setListenerMostrar una notificación
show recibe un valor ui.widgets.Notification y devuelve su id. El Notifier copia cada cadena, así que puedes reutilizar tus búferes de inmediato. Con más de seis tarjetas en pantalla, la más antigua se va con su animación de salida normal.
// 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,
});Tipos, tonos y duración
El aspecto de una tarjeta depende de dos ejes: kind (layout) y tone (color semántico). El icono inicial se deriva de ambos o se fija explícitamente con lead / glyph.
.defaultIcono, título y cuerpo, con botones, cuadro de respuesta o avatar opcionales.progressSpinner, barra determinada y un contador n / N; sin barra de vida.undoAnillo de cuenta atrás con los segundos restantes, más un botón "Undo" por defecto.successAlgo terminó3.6 s.@"error"Un fallo que requiere atenciónfija.warningConviene verlo, no bloquea5.6 s.infoInformación general (por defecto)4.2 s.violetPersonas, invitaciones y otros mensajes sociales4.2 s; fija con botones.quietEstado discreto (un punto pequeño en lugar de un icono)2.8 s- ✓
Fijas por defecto: las tarjetas progress, las que tienen cuadro de respuesta, el tono error y las tarjetas violet con botones nunca se cierran solas.
- ✓
Ajustes explícitos:
sticky = truela fija;duration_mssustituye la duración por defecto;life_baractiva o desactiva la barra de vida. - ✓
Undo dura 7 segundos por defecto, con el anillo siguiendo el temporizador.
Actualizaciones en el sitio y progreso
update(id, …) reescribe el tipo, el tono, el título y el cuerpo de la misma tarjeta y reinicia su duración: sin destruir, sin reconstruir, sin repetir la entrada. Haz avanzar una tarjeta de progreso con setProgress, que solo toca la capa de progreso. Llamar a show con un id existente también la transforma en el sitio.
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,
});Gestionar las acciones del usuario
El Notifier informa a través de un único ui.widgets.NotificationListener. Un NotificationEvent lleva el id de la tarjeta, el kind del evento y el tag / index del botón o el text de la respuesta.
.actionSe pulsó un botón (distínguelos por tag).undoSe pulsó Undo; el anillo ya se ha convertido en un ✓.replySe envió el cuadro de respuesta (Enter o Send); text lo contiene.dismissedEl usuario la cerró (✕ o arrastrándola fuera).expiredExpiró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 });Apilado y hover
Varias notificaciones se pliegan en una pila: la más reciente queda más cerca del ancla con la z más alta; cada capa de detrás asoma 9px y se reduce un 3.8%, hasta tres capas, con una insignia "N más · pasa el cursor para expandir". Cuando el puntero entra en los límites de la pila (ampliados 18px), se expande con separaciones de 9px y todos los temporizadores se pausan; al salir se pliega y se reanudan.
Junto a la insignia hay un botón para borrar todo: el primer clic muestra su texto, el segundo borra de verdad y, al apartar el cursor, se vuelve a plegar. En código, llama a dismissAll().
Una tarjeta suelta se puede lanzar fuera, lo que emite dismissed.
Al redimensionar la ventana, la pila sigue a su ancla de forma rígida, sin interpolación que se quede atrás.
Posiciones
setPosition solo cambia el ancla, la dirección de crecimiento y el desplazamiento de entrada: las tarjetas no se reconstruyen, y cualquier tarjeta que esté saliendo termina su salida en la nueva dirección.
El centro de notificaciones es tuyo
El historial, el modo no molestar y un punto de entrada con campana — las funciones de un centro de notificaciones — son lógica de la app y quedan fuera de zenit: registra el historial donde llamas a show, decide si mostrar una tarjeta o no y entérate de cierres, expiraciones y acciones de botones a través del Listener. La tarjeta de resumen del Storybook muestra el patrón: al terminar el modo no molestar, una tarjeta quiet resume lo que llegó.