From 9c9c53afeaa9b85eaa6f4d396bc71f6750e6f1c6 Mon Sep 17 00:00:00 2001 From: reugenio Date: Sat, 27 Dec 2025 22:23:49 +0100 Subject: [PATCH] feat(style): Z-Design panel color derivation system Laravel-inspired color system for semantic panel coloring: - Add blendTowards(target, percent) for color washing - Add Laravel color constants (red, blue, green, amber, cyan, etc.) - Add soft_black/soft_white for better aesthetics - Add ThemeMode enum (dark/light) - Add PanelColorScheme with 10 documented fields - Implement derivePanelPalette(base, mode) to derive full palette - deriveDarkPalette: 5% tint on dark background - deriveLightPalette: 3% tint on light background - Tests for blendTowards and derivePanelPalette This enables deriving a complete 10-color panel palette from a single base color, achieving Laravel Forge/Nova aesthetics. --- src/core/style.zig | 224 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/src/core/style.zig b/src/core/style.zig index 6e62258..e2b6fc3 100644 --- a/src/core/style.zig +++ b/src/core/style.zig @@ -110,6 +110,20 @@ pub const Color = struct { }; } + /// Blend this color towards a target color by percentage (0-100). + /// Useful for "washing" colors: base.blendTowards(white, 95) = 95% white + 5% base + /// This is the key function for Laravel-style "subtle tint" backgrounds. + pub fn blendTowards(self: Self, target: Self, percent: u8) Self { + const p = @as(u16, @min(percent, 100)); + const inv_p = 100 - p; + return .{ + .r = @intCast((@as(u16, target.r) * p + @as(u16, self.r) * inv_p) / 100), + .g = @intCast((@as(u16, target.g) * p + @as(u16, self.g) * inv_p) / 100), + .b = @intCast((@as(u16, target.b) * p + @as(u16, self.b) * inv_p) / 100), + .a = 255, + }; + } + /// Return same color with different alpha pub fn withAlpha(self: Self, alpha: u8) Self { return .{ @@ -146,6 +160,23 @@ pub const Color = struct { pub const warning = Color.rgb(255, 152, 0); pub const danger = Color.rgb(244, 67, 54); pub const border = Color.rgb(80, 80, 80); + + // ========================================================================= + // Laravel-inspired colors (from Forge/Nova/Vapor) + // Used as base colors for semantic panel derivation (Z-Design) + // ========================================================================= + pub const laravel_red = Color.rgb(239, 68, 68); // #EF4444 - Facturas, alertas + pub const laravel_blue = Color.rgb(59, 130, 246); // #3B82F6 - Clientes, links + pub const laravel_green = Color.rgb(34, 197, 94); // #22C55E - Exito, pagado + pub const laravel_amber = Color.rgb(245, 158, 11); // #F59E0B - Pedidos, warning + pub const laravel_cyan = Color.rgb(6, 182, 212); // #06B6D4 - Albaranes, info + pub const laravel_gray = Color.rgb(107, 114, 128); // #6B7280 - Presupuestos, neutral + pub const laravel_purple = Color.rgb(139, 92, 246); // #8B5CF6 - Especial + pub const laravel_pink = Color.rgb(236, 72, 153); // #EC4899 - Destacado + + // Base colors for derivation (soft black/white for better aesthetics) + pub const soft_black = Color.rgb(17, 17, 20); // Not pure black + pub const soft_white = Color.rgb(250, 250, 252); // Not pure white }; /// Visual style for widgets @@ -632,3 +663,196 @@ test "ThemeManager setTheme" { tm.setTheme(&Theme.high_contrast_dark); try std.testing.expect(std.mem.eql(u8, tm.current.name, "high_contrast_dark")); } + +// ============================================================================= +// Z-DESIGN: Panel Color Derivation System +// ============================================================================= +// +// Inspired by Laravel Forge/Nova/Vapor aesthetic. +// Derives a complete 10-color panel palette from a single base color. +// +// Architecture: +// 1. Base color (e.g., laravel_red for invoices) +// 2. ThemeMode (dark or light) +// 3. derivePanelPalette() generates all 10 colors mathematically +// +// This enables "semantic coloring" where entity type determines panel appearance. + +/// Theme mode for panel derivation +pub const ThemeMode = enum { + dark, // Dark backgrounds, light text + light, // Light backgrounds, dark text +}; + +/// Complete color scheme for a panel (10 colors) +/// Designed to be derived from a single base color. +pub const PanelColorScheme = struct { + /// Background when panel has focus (subtle tint of base color) + fondo_con_focus: Color, + + /// Background when panel doesn't have focus (neutral) + fondo_sin_focus: Color, + + /// Data/content text color (high contrast) + datos: Color, + + /// Label text color (secondary text) + etiquetas: Color, + + /// Header/title background + header: Color, + + /// Placeholder text color + placeholder: Color, + + /// Selection background when focused (base color shines here) + seleccion_fondo_con_focus: Color, + + /// Selection background when unfocused (muted) + seleccion_fondo_sin_focus: Color, + + /// Border when focused (accent) + borde_con_focus: Color, + + /// Border when unfocused (subtle) + borde_sin_focus: Color, +}; + +/// Derives a complete 10-color panel palette from a single base color. +/// +/// The base color "tints" the panel subtly, creating visual cohesion +/// while maintaining readability. Selection and borders use the base +/// color at full strength as the accent. +/// +/// Example: +/// ```zig +/// const invoice_palette = derivePanelPalette(Color.laravel_red, .dark); +/// // invoice_palette.fondo_con_focus = subtle red-tinted dark background +/// // invoice_palette.seleccion_fondo_con_focus = full laravel_red +/// ``` +pub fn derivePanelPalette(base: Color, mode: ThemeMode) PanelColorScheme { + return switch (mode) { + .dark => deriveDarkPalette(base), + .light => deriveLightPalette(base), + }; +} + +/// Derive palette for dark mode (dark backgrounds, light text) +fn deriveDarkPalette(base: Color) PanelColorScheme { + // Reference colors for dark mode + const black = Color.soft_black; // RGB(17, 17, 20) - not pure black + const white = Color.rgb(245, 245, 245); // Off-white for softer look + const gray = Color.rgb(128, 128, 128); + const dark_border = Color.rgb(60, 60, 65); + + return .{ + // Backgrounds: subtle tint of base color (5% base, 95% black) + .fondo_con_focus = base.blendTowards(black, 95), + .fondo_sin_focus = black, + + // Text: high contrast + .datos = white, + .etiquetas = white.darken(30), // ~70% brightness + .placeholder = gray, + + // Header: darkened base color + .header = base.darken(60), + + // Selection: where the base color SHINES + .seleccion_fondo_con_focus = base, // Full color! + .seleccion_fondo_sin_focus = base.blendTowards(gray, 50), // Muted + + // Borders: accent on focus, subtle otherwise + .borde_con_focus = base, + .borde_sin_focus = dark_border, + }; +} + +/// Derive palette for light mode (light backgrounds, dark text) +fn deriveLightPalette(base: Color) PanelColorScheme { + // Reference colors for light mode + const white = Color.soft_white; // RGB(250, 250, 252) - slight cool tint + const black = Color.rgb(20, 20, 25); + const gray = Color.rgb(128, 128, 128); + const light_border = Color.rgb(220, 220, 225); + + return .{ + // Backgrounds: subtle tint of base color (3% base, 97% white) + .fondo_con_focus = base.blendTowards(white, 97), + .fondo_sin_focus = white, + + // Text: high contrast + .datos = black, + .etiquetas = black.lighten(40), // ~60% darkness + .placeholder = gray, + + // Header: very light version of base + .header = base.blendTowards(white, 85), + + // Selection: base color (slightly lightened for readability) + .seleccion_fondo_con_focus = base, + .seleccion_fondo_sin_focus = base.blendTowards(white, 60), + + // Borders: accent on focus, subtle otherwise + .borde_con_focus = base, + .borde_sin_focus = light_border, + }; +} + +// ============================================================================= +// Z-Design Tests +// ============================================================================= + +test "blendTowards basic" { + const red = Color.rgb(255, 0, 0); + const white = Color.rgb(255, 255, 255); + + // 50% blend towards white + const result = red.blendTowards(white, 50); + try std.testing.expectEqual(@as(u8, 255), result.r); // Red stays 255 + try std.testing.expectEqual(@as(u8, 127), result.g); // 0 -> 127 + try std.testing.expectEqual(@as(u8, 127), result.b); // 0 -> 127 +} + +test "blendTowards extremes" { + const base = Color.rgb(100, 100, 100); + const target = Color.rgb(200, 200, 200); + + // 0% = pure base + const zero = base.blendTowards(target, 0); + try std.testing.expectEqual(@as(u8, 100), zero.r); + + // 100% = pure target + const hundred = base.blendTowards(target, 100); + try std.testing.expectEqual(@as(u8, 200), hundred.r); +} + +test "derivePanelPalette dark mode" { + const palette = derivePanelPalette(Color.laravel_red, .dark); + + // Selection should be the full base color + try std.testing.expectEqual(Color.laravel_red.r, palette.seleccion_fondo_con_focus.r); + try std.testing.expectEqual(Color.laravel_red.g, palette.seleccion_fondo_con_focus.g); + try std.testing.expectEqual(Color.laravel_red.b, palette.seleccion_fondo_con_focus.b); + + // Background should be very dark (close to black, slight red tint) + // 95% blend towards black = mostly black with 5% of base color + try std.testing.expect(palette.fondo_con_focus.r < 35); // Red tint visible + try std.testing.expect(palette.fondo_con_focus.g < 25); + try std.testing.expect(palette.fondo_con_focus.b < 25); + + // The red component should be slightly higher than G/B (tint visible) + try std.testing.expect(palette.fondo_con_focus.r >= palette.fondo_con_focus.g); +} + +test "derivePanelPalette light mode" { + const palette = derivePanelPalette(Color.laravel_blue, .light); + + // Selection should be the full base color + try std.testing.expectEqual(Color.laravel_blue.r, palette.seleccion_fondo_con_focus.r); + + // Background should be very light (close to white, slight blue tint) + try std.testing.expect(palette.fondo_con_focus.r > 240); + try std.testing.expect(palette.fondo_con_focus.g > 245); + try std.testing.expect(palette.fondo_con_focus.b > 250); +}