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.
This commit is contained in:
parent
5c7964bacc
commit
9c9c53afea
1 changed files with 224 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue