feat: finalizations for M0-M2

This commit is contained in:
2026-04-05 07:41:33 +02:00
parent e46294a022
commit ee61ad56ea
48 changed files with 1296 additions and 498 deletions

View File

@@ -1,5 +1,7 @@
pub mod navigation;
pub mod tabs;
pub mod toast;
pub use navigation::{SidebarView, PanelTab, TaskSnapshot, OutputEntry};
pub use tabs::{Tab, TabType};
pub use toast::{Toast, ToastLevel};

View File

@@ -0,0 +1,80 @@
/// Toast notification state.
///
/// Toasts are ephemeral, auto-dismissing messages shown at the top of
/// the workspace. Each toast has a severity level, a message, and a
/// monotonically increasing id used for targeted dismissal.
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_TOAST_ID: AtomicU64 = AtomicU64::new(1);
/// Severity determines the visual style.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToastLevel {
Info,
Success,
Warning,
Error,
}
/// A single toast notification.
#[derive(Debug, Clone)]
pub struct Toast {
pub id: u64,
pub level: ToastLevel,
pub message: String,
/// Unix-millis when this toast was created.
pub created_at: u64,
}
impl Toast {
/// Default display duration in milliseconds.
pub const DEFAULT_DURATION_MS: u64 = 4000;
pub fn new(level: ToastLevel, message: String) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
Self {
id: NEXT_TOAST_ID.fetch_add(1, Ordering::Relaxed),
level,
message,
created_at: now,
}
}
/// Whether this toast has exceeded its display duration.
pub fn is_expired(&self) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
now.saturating_sub(self.created_at) >= Self::DEFAULT_DURATION_MS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toast_ids_are_unique() {
let a = Toast::new(ToastLevel::Info, "a".into());
let b = Toast::new(ToastLevel::Info, "b".into());
assert_ne!(a.id, b.id);
}
#[test]
fn toast_levels() {
let t = Toast::new(ToastLevel::Error, "oops".into());
assert_eq!(t.level, ToastLevel::Error);
assert!(!t.message.is_empty());
}
#[test]
fn fresh_toast_not_expired() {
let t = Toast::new(ToastLevel::Info, "test".into());
assert!(!t.is_expired());
}
}