This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ui-retired/vessels/el-style/src/color.rs
T
Will Anderson f4abfe6fdc feat: rename crates/ → vessels/ + add El ports per sub-vessel
Belated rename commit for foundation/el-ui — was missed in the
workspace-wide crates→vessels pass earlier today. Same structural
intent as the rename in the other repos: 'crates' is the Rust word,
'vessel' is El's, and the directory rename is the marker that this
slot holds an El buildable unit even if its current contents are
still Rust pending port.

Plus the El ports themselves — manifest.el + src/main.el per sub-
vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout,
el-platform, el-publish, el-secrets, el-services, el-style, el-ui-
compiler). The ui-compiler is a stub: elc only emits C right now;
generating browser-target JS/Wasm is the biggest open language gap
and gets its own project. Until then, el-ui-compiler emits a JS
module that throws elc.backend_missing so callers fail loudly.
Cross-repo path dependencies in Cargo.toml updated to vessels/.
2026-04-30 18:18:39 -05:00

256 lines
8.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Semantic color tokens and the Color type.
///
/// Prefer semantic tokens (Primary, Surface, etc.) over explicit values.
/// The theme maps tokens to actual colors — swap the theme, everything updates.
/// Use Hex/Rgba only as an escape hatch when you need a one-off value.
/// A color value — either a semantic token or an explicit value.
#[derive(Debug, Clone, PartialEq)]
pub enum Color {
// --- Semantic tokens (preferred) ---
/// Brand primary color. Use for key actions and highlights.
Primary,
/// Content drawn on top of Primary.
OnPrimary,
/// A less prominent container for primary-branded content.
PrimaryContainer,
/// Secondary accent color.
Secondary,
/// Content drawn on top of Secondary.
OnSecondary,
/// The page/screen background.
Background,
/// Content drawn on top of Background.
OnBackground,
/// Surface color for cards, sheets, dialogs.
Surface,
/// Content drawn on top of Surface.
OnSurface,
/// A surface variant, slightly different from Surface.
SurfaceVariant,
/// Error state color.
Error,
/// Content drawn on top of Error.
OnError,
/// Outline/border color.
Outline,
/// Muted outline (dividers, subtle borders).
OutlineVariant,
// --- Escape hatches ---
/// An explicit hex color string (e.g. "#3b82f6" or "#3b82f6ff").
Hex(String),
/// An explicit RGBA color (channels 0255, alpha 0.01.0).
Rgba(u8, u8, u8, f32),
/// Apply opacity to any color.
Opacity(Box<Color>, f32),
}
impl Color {
/// Create an Opacity variant.
pub fn with_opacity(self, opacity: f32) -> Self {
Color::Opacity(Box::new(self), opacity.clamp(0.0, 1.0))
}
/// Convenience: fully transparent.
pub fn transparent() -> Self {
Color::Rgba(0, 0, 0, 0.0)
}
/// Convenience: pure white.
pub fn white() -> Self {
Color::Rgba(255, 255, 255, 1.0)
}
/// Convenience: pure black.
pub fn black() -> Self {
Color::Rgba(0, 0, 0, 1.0)
}
/// Resolve a hex string like "#rrggbb" or "#rrggbbaa" to Rgba.
/// Returns None if the string is not a valid hex color.
pub fn parse_hex(s: &str) -> Option<Self> {
let s = s.trim_start_matches('#');
match s.len() {
6 => {
let r = u8::from_str_radix(&s[0..2], 16).ok()?;
let g = u8::from_str_radix(&s[2..4], 16).ok()?;
let b = u8::from_str_radix(&s[4..6], 16).ok()?;
Some(Color::Rgba(r, g, b, 1.0))
}
8 => {
let r = u8::from_str_radix(&s[0..2], 16).ok()?;
let g = u8::from_str_radix(&s[2..4], 16).ok()?;
let b = u8::from_str_radix(&s[4..6], 16).ok()?;
let a = u8::from_str_radix(&s[6..8], 16).ok()?;
Some(Color::Rgba(r, g, b, a as f32 / 255.0))
}
_ => None,
}
}
}
/// Maps semantic color tokens to actual RGBA values.
/// One ColorScheme per theme mode (light/dark).
#[derive(Debug, Clone)]
pub struct ColorScheme {
pub primary: (u8, u8, u8, f32),
pub on_primary: (u8, u8, u8, f32),
pub primary_container: (u8, u8, u8, f32),
pub secondary: (u8, u8, u8, f32),
pub on_secondary: (u8, u8, u8, f32),
pub background: (u8, u8, u8, f32),
pub on_background: (u8, u8, u8, f32),
pub surface: (u8, u8, u8, f32),
pub on_surface: (u8, u8, u8, f32),
pub surface_variant: (u8, u8, u8, f32),
pub error: (u8, u8, u8, f32),
pub on_error: (u8, u8, u8, f32),
pub outline: (u8, u8, u8, f32),
pub outline_variant: (u8, u8, u8, f32),
}
impl ColorScheme {
/// Default light color scheme.
pub fn light() -> Self {
Self {
primary: (59, 130, 246, 1.0), // blue-500
on_primary: (255, 255, 255, 1.0),
primary_container: (219, 234, 254, 1.0), // blue-100
secondary: (100, 116, 139, 1.0), // slate-500
on_secondary: (255, 255, 255, 1.0),
background: (255, 255, 255, 1.0),
on_background: (15, 23, 42, 1.0), // slate-900
surface: (248, 250, 252, 1.0), // slate-50
on_surface: (15, 23, 42, 1.0),
surface_variant: (241, 245, 249, 1.0), // slate-100
error: (239, 68, 68, 1.0), // red-500
on_error: (255, 255, 255, 1.0),
outline: (203, 213, 225, 1.0), // slate-300
outline_variant: (226, 232, 240, 1.0), // slate-200
}
}
/// Default dark color scheme.
pub fn dark() -> Self {
Self {
primary: (96, 165, 250, 1.0), // blue-400
on_primary: (15, 23, 42, 1.0),
primary_container: (30, 58, 138, 1.0), // blue-900
secondary: (148, 163, 184, 1.0), // slate-400
on_secondary: (15, 23, 42, 1.0),
background: (15, 23, 42, 1.0), // slate-900
on_background: (248, 250, 252, 1.0),
surface: (30, 41, 59, 1.0), // slate-800
on_surface: (248, 250, 252, 1.0),
surface_variant: (51, 65, 85, 1.0), // slate-700
error: (248, 113, 113, 1.0), // red-400
on_error: (15, 23, 42, 1.0),
outline: (71, 85, 105, 1.0), // slate-600
outline_variant: (51, 65, 85, 1.0), // slate-700
}
}
/// Resolve a semantic Color token to its RGBA tuple.
pub fn resolve(&self, color: &Color) -> (u8, u8, u8, f32) {
match color {
Color::Primary => self.primary,
Color::OnPrimary => self.on_primary,
Color::PrimaryContainer => self.primary_container,
Color::Secondary => self.secondary,
Color::OnSecondary => self.on_secondary,
Color::Background => self.background,
Color::OnBackground => self.on_background,
Color::Surface => self.surface,
Color::OnSurface => self.on_surface,
Color::SurfaceVariant => self.surface_variant,
Color::Error => self.error,
Color::OnError => self.on_error,
Color::Outline => self.outline,
Color::OutlineVariant => self.outline_variant,
Color::Hex(s) => Color::parse_hex(s)
.map(|c| self.resolve(&c))
.unwrap_or((0, 0, 0, 1.0)),
Color::Rgba(r, g, b, a) => (*r, *g, *b, *a),
Color::Opacity(inner, opacity) => {
let (r, g, b, a) = self.resolve(inner);
(r, g, b, a * opacity)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_hex_6_chars() {
let c = Color::parse_hex("#3b82f6").unwrap();
assert_eq!(c, Color::Rgba(0x3b, 0x82, 0xf6, 1.0));
}
#[test]
fn parse_hex_8_chars() {
let c = Color::parse_hex("#3b82f680").unwrap();
if let Color::Rgba(r, g, b, a) = c {
assert_eq!(r, 0x3b);
assert_eq!(g, 0x82);
assert_eq!(b, 0xf6);
assert!((a - 0x80 as f32 / 255.0).abs() < 0.01);
} else {
panic!("expected Rgba");
}
}
#[test]
fn parse_hex_invalid() {
assert!(Color::parse_hex("not-a-color").is_none());
assert!(Color::parse_hex("#gg0000").is_none());
}
#[test]
fn with_opacity() {
let c = Color::Primary.with_opacity(0.5);
assert!(matches!(c, Color::Opacity(_, _)));
}
#[test]
fn color_scheme_light_resolves_primary() {
let scheme = ColorScheme::light();
let (r, g, b, a) = scheme.resolve(&Color::Primary);
assert_eq!((r, g, b), (59, 130, 246));
assert!((a - 1.0).abs() < 0.001);
}
#[test]
fn color_scheme_dark_resolves_background() {
let scheme = ColorScheme::dark();
let (r, g, b, _) = scheme.resolve(&Color::Background);
assert_eq!((r, g, b), (15, 23, 42));
}
#[test]
fn opacity_modifies_alpha() {
let scheme = ColorScheme::light();
let color = Color::Primary.with_opacity(0.5);
let (_, _, _, a) = scheme.resolve(&color);
assert!((a - 0.5).abs() < 0.001);
}
#[test]
fn rgba_passthrough() {
let scheme = ColorScheme::light();
let color = Color::Rgba(10, 20, 30, 0.8);
let (r, g, b, a) = scheme.resolve(&color);
assert_eq!((r, g, b), (10, 20, 30));
assert!((a - 0.8).abs() < 0.001);
}
}