use iced::Background;
use iced::Color;
use iced::border;
use iced::widget::{container, scrollable};
use crate::WorkspaceStatus;
use crate::board::TicketPhase;
use iced_fonts::lucide;
use std::sync::atomic::AtomicBool;
pub const BG_BASE: Color = Color::from_rgb(0.063, 0.059, 0.059); pub const BG_SURFACE: Color = Color::from_rgb(0.110, 0.106, 0.102); pub const BG_ELEVATED: Color = Color::from_rgb(0.157, 0.153, 0.149);
pub const BORDER: Color = Color::from_rgba(0.808, 0.804, 0.765, 0.08);
pub const BORDER_STRONG: Color = Color::from_rgba(0.808, 0.804, 0.765, 0.12);
pub const TEXT_PRIMARY: Color = Color::from_rgb(0.808, 0.804, 0.765); pub const TEXT_SECONDARY: Color = Color::from_rgb(0.529, 0.522, 0.502); pub const TEXT_MUTED: Color = Color::from_rgb(0.341, 0.337, 0.325); pub const TEXT_FAINT: Color = Color::from_rgb(0.204, 0.200, 0.192);
pub const ACCENT: Color = Color::from_rgb(0.227, 0.663, 0.624); pub const ACCENT_LIGHT: Color = Color::from_rgb(0.357, 0.749, 0.710); pub const ACCENT_DIM: Color = Color::from_rgba(0.227, 0.663, 0.624, 0.3);
pub const FIND_MATCH_DIM: Color = Color::from_rgba(0.816, 0.635, 0.082, 0.25);
pub const FIND_MATCH_CURRENT: Color = Color::from_rgba(0.816, 0.635, 0.082, 0.45);
pub const BRACKET_MATCH: Color = Color::from_rgba(0.357, 0.749, 0.710, 0.35);
pub const HOVER: Color = Color::from_rgba(0.808, 0.804, 0.765, 0.05);
pub const HOVER_STRONG: Color = Color::from_rgba(0.808, 0.804, 0.765, 0.08);
pub const STATUS_SUCCESS: Color = Color::from_rgb(0.0, 0.902, 0.541); pub const STATUS_WARNING: Color = Color::from_rgb(1.0, 0.667, 0.0); pub const STATUS_ERROR: Color = Color::from_rgb(1.0, 0.267, 0.4);
pub const DIFF_ADDED_TINT: Color =
Color::from_rgba(STATUS_SUCCESS.r, STATUS_SUCCESS.g, STATUS_SUCCESS.b, 0.10);
pub const DIFF_REMOVED_TINT: Color =
Color::from_rgba(STATUS_ERROR.r, STATUS_ERROR.g, STATUS_ERROR.b, 0.10);
pub const DIFF_FILE_HEADER_BG: Color =
Color::from_rgba(STATUS_WARNING.r, STATUS_WARNING.g, STATUS_WARNING.b, 0.06);
#[must_use]
pub fn log_level_color(level: &str) -> (Color, Color) {
match level.to_uppercase().as_str() {
"ERROR" => (STATUS_ERROR, Color::from_rgba(0.937, 0.267, 0.267, 0.08)),
"WARN" => (STATUS_WARNING, Color::from_rgba(1.0, 0.667, 0.0, 0.08)),
"INFO" => (
Color::from_rgb(0.219, 0.741, 0.973),
Color::from_rgba(0.219, 0.741, 0.973, 0.08),
),
"DEBUG" => (
Color::from_rgb(0.655, 0.545, 0.980),
Color::from_rgba(0.655, 0.545, 0.980, 0.08),
),
"TRACE" => (TEXT_MUTED, Color::from_rgba(0.5, 0.5, 0.5, 0.08)),
_ => (TEXT_MUTED, HOVER),
}
}
const fn badge_bg(fg: Color) -> Color {
Color::from_rgba(fg.r, fg.g, fg.b, 0.1)
}
#[must_use]
#[allow(clippy::trivially_copy_pass_by_ref)]
pub const fn role_badge_color_for(role: &crate::Role) -> (Color, Color) {
let info = crate::role::role_info(role);
let (r, g, b) = info.badge_fg;
let fg = Color::from_rgb(r, g, b);
(fg, badge_bg(fg))
}
#[must_use]
pub fn role_badge_color(role: &str) -> (Color, Color) {
if let Some(r) = crate::management::stage_role(role) {
return role_badge_color_for(&r);
}
if let Ok(r) = role.parse::<crate::Role>() {
return role_badge_color_for(&r);
}
if let Some(idx) = role.rfind('_')
&& idx + 1 < role.len()
&& role.as_bytes()[idx + 1..].iter().all(u8::is_ascii_digit)
{
let stripped = &role[..idx];
if let Ok(r) = stripped.parse::<crate::Role>() {
return role_badge_color_for(&r);
}
}
(TEXT_MUTED, badge_bg(TEXT_MUTED))
}
#[must_use]
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn role_icon(role: &crate::Role) -> iced::widget::Text<'static, iced::Theme, iced::Renderer> {
match role {
crate::Role::Manager => lucide::bot(),
crate::Role::Engineer => lucide::wrench(),
crate::Role::Analyst => lucide::scan_search(),
crate::Role::Coder => lucide::code(),
crate::Role::Qa => lucide::gavel(),
crate::Role::Maintainer => lucide::cog(),
crate::Role::Discovery => lucide::search(),
crate::Role::Artist => lucide::palette(),
crate::Role::Reviewer => lucide::file_check(),
crate::Role::Sanitation => lucide::spray_can(),
crate::Role::Assistant => lucide::message_square(),
}
}
#[must_use]
pub fn general_context_icon() -> iced::widget::Text<'static, iced::Theme, iced::Renderer> {
lucide::book_open_text()
}
pub const FONT_BOLD: iced::Font = iced::Font {
family: iced::font::Family::Name("JetBrains Mono"),
weight: iced::font::Weight::Bold,
..iced::Font::DEFAULT
};
pub const FONT_REGULAR: iced::Font = iced::Font {
family: iced::font::Family::Name("JetBrains Mono"),
..iced::Font::DEFAULT
};
pub const MARKDOWN_TEXT_SIZE: f32 = 13.0;
#[must_use]
pub fn markdown_settings() -> iced::widget::markdown::Settings {
let style = iced::widget::markdown::Style {
font: FONT_REGULAR,
inline_code_highlight: iced::widget::markdown::Highlight {
background: iced::Color::from_rgba(0.0, 0.0, 0.0, 0.15).into(),
border: iced::border::rounded(4),
},
inline_code_padding: iced::padding::left(1).right(1),
inline_code_color: TEXT_PRIMARY,
inline_code_font: FONT_REGULAR,
code_block_font: FONT_REGULAR,
link_color: ACCENT,
};
iced::widget::markdown::Settings::with_text_size(MARKDOWN_TEXT_SIZE, style)
}
#[must_use]
pub const fn ticket_phase_color(phase: TicketPhase) -> (Color, Color) {
use TicketPhase::{
Analysis, Backlog, Cancelled, DiagnosticsDone, Done, Failed, InDevelopment, InDiagnostics,
InQa, InReview, InSanitation, Planning, QaPassed, ReadyForDevelopment, Reviewed,
SanitationPassed,
};
match phase {
Backlog => (
Color::from_rgb(0.176, 0.176, 0.176),
Color::from_rgb(0.808, 0.804, 0.765),
),
Planning => (
Color::from_rgb(0.263, 0.243, 0.114),
Color::from_rgb(0.902, 0.863, 0.784),
),
Analysis => (
Color::from_rgb(0.114, 0.216, 0.310),
Color::from_rgb(0.784, 0.863, 0.949),
),
ReadyForDevelopment => (
Color::from_rgb(0.263, 0.224, 0.114),
Color::from_rgb(0.902, 0.863, 0.784),
),
InDevelopment => (
Color::from_rgb(0.380, 0.216, 0.078),
Color::from_rgb(0.941, 0.878, 0.784),
),
InDiagnostics => (
Color::from_rgb(0.310, 0.224, 0.102),
Color::from_rgb(0.902, 0.863, 0.784),
),
DiagnosticsDone => (
Color::from_rgb(0.161, 0.235, 0.224),
Color::from_rgb(0.784, 0.902, 0.871),
),
InSanitation => (
Color::from_rgb(0.310, 0.310, 0.310),
Color::from_rgb(0.788, 0.788, 0.788),
),
SanitationPassed => (
Color::from_rgb(0.247, 0.247, 0.247),
Color::from_rgb(0.863, 0.863, 0.863),
),
InReview => (
Color::from_rgb(0.184, 0.216, 0.380),
Color::from_rgb(0.816, 0.816, 0.933),
),
Reviewed => (
Color::from_rgb(0.224, 0.208, 0.322),
Color::from_rgb(0.878, 0.816, 0.933),
),
InQa => (
Color::from_rgb(0.216, 0.184, 0.380),
Color::from_rgb(0.816, 0.816, 0.933),
),
QaPassed => (
Color::from_rgb(0.176, 0.310, 0.208),
Color::from_rgb(0.784, 0.902, 0.816),
),
Done => (
Color::from_rgb(0.114, 0.176, 0.114),
Color::from_rgb(0.753, 0.816, 0.753),
),
Cancelled => (
Color::from_rgb(0.145, 0.145, 0.145),
Color::from_rgb(0.690, 0.690, 0.690),
),
Failed => (
Color::from_rgb(0.310, 0.114, 0.114),
Color::from_rgb(0.878, 0.753, 0.753),
),
}
}
#[must_use]
pub fn ticket_priority_color(priority: i64) -> (Color, Color) {
match priority {
0 => (
Color::from_rgb(0.310, 0.114, 0.114),
Color::from_rgb(0.878, 0.753, 0.753),
),
1 => (
Color::from_rgb(0.380, 0.216, 0.078),
Color::from_rgb(0.941, 0.878, 0.784),
),
2 => (
Color::from_rgb(0.310, 0.224, 0.102),
Color::from_rgb(0.902, 0.863, 0.784),
),
3 => (
Color::from_rgb(0.176, 0.310, 0.208),
Color::from_rgb(0.784, 0.902, 0.816),
),
_ => (
Color::from_rgb(0.114, 0.176, 0.114),
Color::from_rgb(0.753, 0.816, 0.753),
),
}
}
static TIMESTAMP_PARSE_WARNED: AtomicBool = AtomicBool::new(false);
#[must_use]
pub fn format_timestamp(ts: &str) -> String {
if let Ok(dt) = crate::turso::parse_utc_timestamp(ts) {
dt.format("%b %-d, %H:%M").to_string()
} else {
if !TIMESTAMP_PARSE_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
tracing::warn!(timestamp = %ts, "Failed to parse timestamp, falling back to truncated string");
}
crate::util::truncate_bytes(ts, 16).to_string()
}
}
pub fn format_hhmmss(ts: &str) -> &str {
if ts.len() > 19 {
&ts[ts.floor_char_boundary(11)..ts.floor_char_boundary(19)]
} else {
ts
}
}
#[expect(clippy::cast_precision_loss)] pub(crate) fn format_compact_tokens(tokens: u64) -> String {
if tokens < 1_000 {
tokens.to_string()
} else if tokens < 999_950 {
format!("{:.1}k", tokens as f64 / 1_000.0)
} else if tokens < 999_950_000 {
format!("{:.1}M", tokens as f64 / 1_000_000.0)
} else {
format!("{:.1}G", tokens as f64 / 1_000_000_000.0)
}
}
#[must_use]
pub fn workspace_status_color(status: WorkspaceStatus) -> (Color, Color) {
match status {
WorkspaceStatus::Ready => (
Color::from_rgb(0.133, 0.773, 0.369),
Color::from_rgba(0.133, 0.773, 0.369, 0.1),
),
WorkspaceStatus::Analyzing => (
Color::from_rgb(0.851, 0.557, 0.0),
Color::from_rgba(0.851, 0.557, 0.0, 0.1),
),
WorkspaceStatus::Failed => (
Color::from_rgb(0.957, 0.247, 0.369),
Color::from_rgba(0.957, 0.247, 0.369, 0.1),
),
WorkspaceStatus::Pending => (
Color::from_rgb(0.631, 0.631, 0.631),
Color::from_rgba(0.631, 0.631, 0.631, 0.1),
),
}
}
pub const ANIM_LOG_FADE_MS: u64 = 100;
pub const ANIM_SELECTED_MS: u64 = 150;
#[must_use]
pub fn thin_scrollbar() -> scrollable::Scrollbar {
scrollable::Scrollbar::new().width(6).scroller_width(6)
}
#[must_use]
pub fn vertical_scrollbar() -> scrollable::Direction {
scrollable::Direction::Vertical(thin_scrollbar())
}
#[must_use]
pub fn horizontal_scrollbar() -> scrollable::Direction {
scrollable::Direction::Horizontal(thin_scrollbar())
}
#[must_use]
pub fn scrollbar_style(theme: &iced::Theme, status: scrollable::Status) -> scrollable::Style {
let base = scrollable::default(theme, status);
let opacity = match status {
scrollable::Status::Active { .. } => 0.4,
scrollable::Status::Hovered { .. } => 0.6,
scrollable::Status::Dragged { .. } => 0.8,
};
let rail = scrollable::Rail {
background: None,
border: border::rounded(2),
scroller: scrollable::Scroller {
background: Background::Color(TEXT_PRIMARY.scale_alpha(opacity)),
border: border::rounded(2),
},
};
scrollable::Style {
vertical_rail: rail,
horizontal_rail: rail,
..base
}
}
#[must_use]
pub fn button_transparent(
_: &iced::Theme,
_status: iced::widget::button::Status,
) -> iced::widget::button::Style {
iced::widget::button::Style {
background: None,
..Default::default()
}
}
#[must_use = "button style factory returns a style closure"]
fn transparent_button_style(
hover: Color,
pressed: Color,
radius: f32,
disabled: bool,
) -> impl Fn(&iced::Theme, iced::widget::button::Status) -> iced::widget::button::Style {
move |_: &iced::Theme, status| {
let bg = if disabled {
Color::TRANSPARENT
} else {
match status {
iced::widget::button::Status::Hovered => hover,
iced::widget::button::Status::Pressed => pressed,
_ => Color::TRANSPARENT,
}
};
iced::widget::button::Style {
background: Some(Background::Color(bg)),
border: iced::Border {
radius: radius.into(),
width: 0.0,
color: Color::TRANSPARENT,
},
..Default::default()
}
}
}
#[must_use = "button style factory returns a style closure"]
pub fn icon_button_style(
disabled: bool,
) -> impl Fn(&iced::Theme, iced::widget::button::Status) -> iced::widget::button::Style {
transparent_button_style(HOVER_STRONG, ACCENT_DIM, 6.0, disabled)
}
pub fn tab_button_style(
is_active: bool,
) -> impl Fn(&iced::Theme, iced::widget::button::Status) -> iced::widget::button::Style {
move |_: &iced::Theme, status| {
let bg = if is_active {
BG_ELEVATED
} else if status == iced::widget::button::Status::Hovered {
HOVER
} else {
BG_SURFACE
};
iced::widget::button::Style {
background: Some(Background::Color(bg)),
border: iced::Border {
radius: 0.0.into(),
width: 0.0,
color: Color::TRANSPARENT,
},
..Default::default()
}
}
}
#[must_use]
pub fn button_primary(
_: &iced::Theme,
status: iced::widget::button::Status,
) -> iced::widget::button::Style {
let base = match status {
iced::widget::button::Status::Active => ACCENT,
iced::widget::button::Status::Hovered => ACCENT_LIGHT,
iced::widget::button::Status::Pressed => Color::from_rgb(0.165, 0.482, 0.455),
iced::widget::button::Status::Disabled => TEXT_FAINT,
};
let text = match status {
iced::widget::button::Status::Disabled => TEXT_MUTED,
_ => BG_BASE,
};
iced::widget::button::Style {
background: Some(iced::Background::Color(base)),
text_color: text,
border: iced::Border {
radius: 4.0.into(),
width: 0.0,
color: Color::TRANSPARENT,
},
..iced::widget::button::Style::default()
}
}
#[must_use]
pub fn button_danger(
_: &iced::Theme,
status: iced::widget::button::Status,
) -> iced::widget::button::Style {
let base = match status {
iced::widget::button::Status::Active => Color::from_rgba(1.0, 0.267, 0.4, 0.15),
iced::widget::button::Status::Hovered => Color::from_rgba(1.0, 0.267, 0.4, 0.25),
iced::widget::button::Status::Pressed => Color::from_rgba(1.0, 0.267, 0.4, 0.35),
iced::widget::button::Status::Disabled => Color::TRANSPARENT,
};
let text = match status {
iced::widget::button::Status::Disabled => TEXT_MUTED,
_ => STATUS_ERROR,
};
iced::widget::button::Style {
background: Some(iced::Background::Color(base)),
text_color: text,
border: iced::Border {
radius: 4.0.into(),
width: 1.0,
color: Color::from_rgba(1.0, 0.267, 0.4, 0.2),
},
..iced::widget::button::Style::default()
}
}
#[must_use]
pub fn button_secondary(
_: &iced::Theme,
status: iced::widget::button::Status,
) -> iced::widget::button::Style {
let bg = match status {
iced::widget::button::Status::Active => HOVER,
iced::widget::button::Status::Hovered => HOVER_STRONG,
iced::widget::button::Status::Pressed => Color::from_rgba(0.808, 0.804, 0.765, 0.12),
iced::widget::button::Status::Disabled => Color::TRANSPARENT,
};
let text = match status {
iced::widget::button::Status::Disabled => TEXT_MUTED,
_ => TEXT_PRIMARY,
};
iced::widget::button::Style {
background: Some(iced::Background::Color(bg)),
text_color: text,
border: iced::Border {
radius: 4.0.into(),
width: 1.0,
color: BORDER,
},
..iced::widget::button::Style::default()
}
}
fn button_text_style(
status: iced::widget::button::Status,
enabled_color: Color,
) -> iced::widget::button::Style {
let bg = match status {
iced::widget::button::Status::Hovered => HOVER,
iced::widget::button::Status::Pressed => HOVER_STRONG,
_ => Color::TRANSPARENT,
};
let text = match status {
iced::widget::button::Status::Disabled => TEXT_MUTED,
_ => enabled_color,
};
iced::widget::button::Style {
background: Some(iced::Background::Color(bg)),
text_color: text,
border: iced::Border {
radius: 4.0.into(),
width: 0.0,
color: Color::TRANSPARENT,
},
..iced::widget::button::Style::default()
}
}
#[must_use]
pub fn button_text_danger(
_theme: &iced::Theme,
status: iced::widget::button::Status,
) -> iced::widget::button::Style {
button_text_style(status, STATUS_ERROR)
}
#[must_use]
pub fn button_text(
_theme: &iced::Theme,
status: iced::widget::button::Status,
) -> iced::widget::button::Style {
button_text_style(status, TEXT_PRIMARY)
}
pub(crate) fn container_style(
bg: Color,
radius: f32,
width: f32,
border_color: Color,
) -> container::Style {
container::Style {
background: Some(Background::Color(bg)),
border: iced::Border {
radius: radius.into(),
width,
color: border_color,
},
..container::Style::default()
}
}
#[must_use]
pub fn tooltip_style(_theme: &iced::Theme) -> container::Style {
container_style(BG_ELEVATED, 6.0, 1.0, BORDER_STRONG)
}
pub fn bubble_style(
bg: Color,
text_color: Option<Color>,
) -> impl Fn(&iced::Theme) -> container::Style {
move |_theme: &iced::Theme| container::Style {
background: Some(iced::Background::Color(bg)),
text_color,
border: iced::Border {
radius: 8.0.into(),
width: 0.0,
color: iced::Color::TRANSPARENT,
},
..container::Style::default()
}
}
#[must_use]
pub fn container_bar(_theme: &iced::Theme) -> container::Style {
container_style(BG_ELEVATED, 0.0, 0.0, Color::TRANSPARENT)
}
#[must_use]
pub fn surface_card_style(_theme: &iced::Theme) -> container::Style {
container_style(BG_SURFACE, 4.0, 1.0, BORDER)
}
#[must_use]
pub fn elevated_card_style(_theme: &iced::Theme) -> container::Style {
container_style(BG_ELEVATED, 4.0, 1.0, BORDER)
}
#[must_use]
pub fn dialog_container_style(_theme: &iced::Theme) -> container::Style {
container_style(BG_ELEVATED, 8.0, 1.0, BORDER_STRONG)
}
#[must_use]
pub fn base_container_style(_theme: &iced::Theme) -> container::Style {
container_style(BG_BASE, 0.0, 0.0, Color::TRANSPARENT)
}
#[must_use]
pub fn surface_container_style(_theme: &iced::Theme) -> container::Style {
container_style(BG_SURFACE, 0.0, 0.0, Color::TRANSPARENT)
}
pub fn pill_style(bg: Color) -> impl Fn(&iced::Theme) -> container::Style {
move |_theme: &iced::Theme| container_style(bg, 4.0, 0.0, Color::TRANSPARENT)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Role;
use strum::IntoEnumIterator;
#[test]
fn canonical_names_match() {
for role in Role::iter() {
let name = role.as_str();
assert_eq!(
role_badge_color_for(&role),
role_badge_color(name),
"role_badge_color_for and role_badge_color must agree for {name}"
);
}
}
#[test]
fn derivative_analyst_names_get_correct_color() {
let analyst_color = role_badge_color_for(&crate::Role::Analyst);
assert_eq!(role_badge_color("analyst_1"), analyst_color);
assert_eq!(role_badge_color("analyst_2"), analyst_color);
assert_eq!(role_badge_color("analyst_3"), analyst_color);
}
#[test]
fn derivative_other_role_names_get_correct_color() {
for role in Role::iter() {
let name = role.as_str();
let expected = role_badge_color_for(&role);
assert_eq!(
role_badge_color(&format!("{name}_1")),
expected,
"derivative {name}_1 should match role_badge_color_for"
);
assert_eq!(
role_badge_color(&format!("{name}_42")),
expected,
"derivative {name}_42 should match role_badge_color_for"
);
}
}
#[test]
fn non_numeric_suffix_is_unknown() {
assert_eq!(
role_badge_color("analyst_final"),
(TEXT_MUTED, badge_bg(TEXT_MUTED))
);
assert_eq!(
role_badge_color("coder_abc"),
(TEXT_MUTED, badge_bg(TEXT_MUTED))
);
}
#[test]
fn compact_token_format_edges() {
assert_eq!(format_compact_tokens(0), "0");
assert_eq!(format_compact_tokens(500), "500");
assert_eq!(format_compact_tokens(999), "999");
assert_eq!(format_compact_tokens(1_000), "1.0k");
assert_eq!(format_compact_tokens(12_300), "12.3k");
assert_eq!(format_compact_tokens(200_000), "200.0k");
assert_eq!(format_compact_tokens(999_949), "999.9k");
assert_eq!(format_compact_tokens(999_999), "1.0M");
assert_eq!(format_compact_tokens(1_000_000), "1.0M");
assert_eq!(format_compact_tokens(1_234_567), "1.2M");
assert_eq!(format_compact_tokens(999_949_999), "999.9M");
assert_eq!(format_compact_tokens(999_950_000), "1.0G");
assert_eq!(format_compact_tokens(1_000_000_000), "1.0G");
}
#[test]
fn joint_comment_stage_roles_get_their_role_colors() {
assert_eq!(
role_badge_color("Analysis"),
role_badge_color_for(&crate::Role::Analyst)
);
assert_eq!(
role_badge_color("Review"),
role_badge_color_for(&crate::Role::Reviewer)
);
assert_eq!(
role_badge_color("QA"),
role_badge_color_for(&crate::Role::Qa)
);
}
#[test]
fn llm_api_roles_are_unknown() {
assert_eq!(role_badge_color("user"), (TEXT_MUTED, badge_bg(TEXT_MUTED)));
assert_eq!(
role_badge_color("system"),
(TEXT_MUTED, badge_bg(TEXT_MUTED))
);
assert_eq!(role_badge_color("tool"), (TEXT_MUTED, badge_bg(TEXT_MUTED)));
}
#[test]
fn empty_and_garbage_are_unknown() {
assert_eq!(role_badge_color(""), (TEXT_MUTED, badge_bg(TEXT_MUTED)));
assert_eq!(
role_badge_color("garbage"),
(TEXT_MUTED, badge_bg(TEXT_MUTED))
);
assert_eq!(
role_badge_color("unknown_role"),
(TEXT_MUTED, badge_bg(TEXT_MUTED))
);
assert_eq!(role_badge_color("_1"), (TEXT_MUTED, badge_bg(TEXT_MUTED)));
}
#[test]
fn case_insensitive_parse() {
let analyst_color = role_badge_color_for(&crate::Role::Analyst);
assert_eq!(role_badge_color("ANALYST"), analyst_color);
assert_eq!(role_badge_color("Analyst"), analyst_color);
assert_eq!(role_badge_color("ANALYST_1"), analyst_color);
}
#[test]
fn badge_background_is_alpha_scaled_foreground() {
for role in Role::iter() {
let (fg, bg) = role_badge_color_for(&role);
assert_eq!(bg, badge_bg(fg), "canonical role {role:?}");
}
for name in [
"analyst_final",
"coder_abc",
"user",
"assistant",
"system",
"tool",
"Analysis",
"Review",
"QA",
"analyst_3",
"",
"garbage",
"unknown_role",
"_1",
] {
let (fg, bg) = role_badge_color(name);
assert_eq!(bg, badge_bg(fg), "fallback/stage/derivative role {name:?}");
}
}
#[test]
fn style_factory_values_unchanged() {
let theme = iced::Theme::Dark;
let bg = |c: iced::Color| Some(iced::Background::Color(c));
let border = |r: f32, w: f32, c: iced::Color| iced::Border {
radius: r.into(),
width: w,
color: c,
};
assert_eq!(
tooltip_style(&theme),
iced::widget::container::Style {
background: bg(BG_ELEVATED),
border: border(6.0, 1.0, BORDER_STRONG),
..iced::widget::container::Style::default()
}
);
assert_eq!(
container_bar(&theme),
iced::widget::container::Style {
background: bg(BG_ELEVATED),
border: border(0.0, 0.0, iced::Color::TRANSPARENT),
..iced::widget::container::Style::default()
}
);
assert_eq!(
surface_card_style(&theme),
iced::widget::container::Style {
background: bg(BG_SURFACE),
border: border(4.0, 1.0, BORDER),
..iced::widget::container::Style::default()
}
);
assert_eq!(
elevated_card_style(&theme),
iced::widget::container::Style {
background: bg(BG_ELEVATED),
border: border(4.0, 1.0, BORDER),
..iced::widget::container::Style::default()
}
);
assert_eq!(
pill_style(BG_ELEVATED)(&theme),
iced::widget::container::Style {
background: bg(BG_ELEVATED),
border: border(4.0, 0.0, iced::Color::TRANSPARENT),
..iced::widget::container::Style::default()
}
);
assert_eq!(
dialog_container_style(&theme),
iced::widget::container::Style {
background: bg(BG_ELEVATED),
border: border(8.0, 1.0, BORDER_STRONG),
..iced::widget::container::Style::default()
}
);
assert_eq!(
base_container_style(&theme),
iced::widget::container::Style {
background: bg(BG_BASE),
..iced::widget::container::Style::default()
}
);
assert_eq!(
surface_container_style(&theme),
iced::widget::container::Style {
background: bg(BG_SURFACE),
..iced::widget::container::Style::default()
}
);
for status in [
iced::widget::button::Status::Active,
iced::widget::button::Status::Hovered,
iced::widget::button::Status::Pressed,
iced::widget::button::Status::Disabled,
] {
let expected = |enabled: iced::Color| iced::widget::button::Style {
background: bg(match status {
iced::widget::button::Status::Hovered => HOVER,
iced::widget::button::Status::Pressed => HOVER_STRONG,
_ => iced::Color::TRANSPARENT,
}),
text_color: if status == iced::widget::button::Status::Disabled {
TEXT_MUTED
} else {
enabled
},
border: border(4.0, 0.0, iced::Color::TRANSPARENT),
..iced::widget::button::Style::default()
};
assert_eq!(button_text(&theme, status), expected(TEXT_PRIMARY));
assert_eq!(button_text_danger(&theme, status), expected(STATUS_ERROR));
}
}
}