1// Package theme provides GTK CSS styling for UI components.
2package theme
3
4import (
5 "fmt"
6 "regexp"
7 "strings"
8
9 "github.com/bnema/dumber/internal/infrastructure/config"
10)
11
12// Palette holds semantic color tokens for theming.
13type Palette struct {
14 Background string // Main background color
15 Surface string // Elevated surfaces (cards, popups)
16 SurfaceVariant string // Secondary surfaces
17 Text string // Primary text color
18 Muted string // Secondary/disabled text
19 Accent string // Primary accent color (actions, highlights)
20 Border string // Border and divider lines
21 // Semantic status colors (not user-editable, derived defaults)
22 Success string // Success/positive feedback
23 Warning string // Warning/caution feedback
24 Destructive string // Error/destructive actions
25}
26
27// DefaultDarkPalette returns the default dark theme palette.
28func DefaultDarkPalette() Palette {
29 return Palette{
30 Background: "#0a0a0b",
31 Surface: "#1a1a1b",
32 SurfaceVariant: "#2d2d2d",
33 Text: "#ffffff",
34 Muted: "#909090",
35 Accent: "#4ade80",
36 Border: "#333333",
37 Success: "#4ade80",
38 Warning: "#fbbf24",
39 Destructive: "#ef4444",
40 }
41}
42
43// DefaultLightPalette returns the default light theme palette.
44func DefaultLightPalette() Palette {
45 return Palette{
46 Background: "#fafafa",
47 Surface: "#ffffff",
48 SurfaceVariant: "#f0f0f0",
49 Text: "#1a1a1a",
50 Muted: "#666666",
51 Accent: "#22c55e",
52 Border: "#dddddd",
53 Success: "#22c55e",
54 Warning: "#f59e0b",
55 Destructive: "#dc2626",
56 }
57}
58
59// PaletteFromConfig creates a Palette from config values, filling missing values with defaults.
60func PaletteFromConfig(cfg *config.ColorPalette, isDark bool) Palette {
61 var defaults Palette
62 if isDark {
63 defaults = DefaultDarkPalette()
64 } else {
65 defaults = DefaultLightPalette()
66 }
67
68 if cfg == nil {
69 return defaults
70 }
71
72 return Palette{
73 Background: Coalesce(cfg.Background, defaults.Background),
74 Surface: Coalesce(cfg.Surface, defaults.Surface),
75 SurfaceVariant: Coalesce(cfg.SurfaceVariant, defaults.SurfaceVariant),
76 Text: Coalesce(cfg.Text, defaults.Text),
77 Muted: Coalesce(cfg.Muted, defaults.Muted),
78 Accent: Coalesce(cfg.Accent, defaults.Accent),
79 Border: Coalesce(cfg.Border, defaults.Border),
80 // Semantic colors always use defaults (not user-editable)
81 Success: defaults.Success,
82 Warning: defaults.Warning,
83 Destructive: defaults.Destructive,
84 }
85}
86
87// Coalesce returns the first non-empty string.
88func Coalesce(values ...string) string {
89 for _, v := range values {
90 if v != "" {
91 return v
92 }
93 }
94 return ""
95}
96
97// hexColorRegex matches valid hex colors (#RGB, #RRGGBB, #RRGGBBAA).
98var hexColorRegex = regexp.MustCompile(`^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`)
99
100// ValidateHexColor checks if a string is a valid hex color.
101func ValidateHexColor(color string) error {
102 if color == "" {
103 return nil // Empty is valid (will use default)
104 }
105 if !hexColorRegex.MatchString(color) {
106 return fmt.Errorf("invalid hex color: %s", color)
107 }
108 return nil
109}
110
111// Validate checks all palette colors are valid hex values.
112func (p Palette) Validate() error {
113 colors := map[string]string{
114 "background": p.Background,
115 "surface": p.Surface,
116 "surface_variant": p.SurfaceVariant,
117 "text": p.Text,
118 "muted": p.Muted,
119 "accent": p.Accent,
120 "border": p.Border,
121 }
122
123 for name, color := range colors {
124 if err := ValidateHexColor(color); err != nil {
125 return fmt.Errorf("%s: %w", name, err)
126 }
127 }
128 return nil
129}
130
131// ToCSSVars generates CSS custom property declarations for GTK.
132func (p Palette) ToCSSVars() string {
133 var sb strings.Builder
134 sb.WriteString(" --bg: " + p.Background + ";\n")
135 sb.WriteString(" --surface: " + p.Surface + ";\n")
136 sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n")
137 sb.WriteString(" --text: " + p.Text + ";\n")
138 sb.WriteString(" --muted: " + p.Muted + ";\n")
139 sb.WriteString(" --accent: " + p.Accent + ";\n")
140 sb.WriteString(" --border: " + p.Border + ";\n")
141 sb.WriteString(" --success: " + p.Success + ";\n")
142 sb.WriteString(" --warning: " + p.Warning + ";\n")
143 sb.WriteString(" --destructive: " + p.Destructive + ";\n")
144 return sb.String()
145}
146
147// ToWebCSSVars generates CSS custom properties for web UI (Tailwind compatibility).
148// Maps GTK palette names to Tailwind variable names used in the WebUI homepage.
149func (p Palette) ToWebCSSVars() string {
150 var sb strings.Builder
151 // Tailwind-compatible variables
152 sb.WriteString(" --background: " + p.Background + " !important;\n")
153 sb.WriteString(" --foreground: " + p.Text + " !important;\n")
154 sb.WriteString(" --card: " + p.Surface + " !important;\n")
155 sb.WriteString(" --card-foreground: " + p.Text + " !important;\n")
156 sb.WriteString(" --primary: " + p.Accent + " !important;\n")
157 sb.WriteString(" --primary-foreground: " + p.Background + " !important;\n")
158 sb.WriteString(" --muted: " + p.SurfaceVariant + " !important;\n")
159 sb.WriteString(" --muted-foreground: " + p.Muted + " !important;\n")
160 sb.WriteString(" --border: " + p.Border + " !important;\n")
161 // Ring follows primary (accent)
162 sb.WriteString(" --ring: " + p.Accent + " !important;\n")
163 // Semantic colors from palette defaults
164 sb.WriteString(" --success: " + p.Success + " !important;\n")
165 sb.WriteString(" --success-foreground: " + p.Background + " !important;\n")
166 sb.WriteString(" --warning: " + p.Warning + " !important;\n")
167 sb.WriteString(" --warning-foreground: " + p.Text + " !important;\n")
168 sb.WriteString(" --destructive: " + p.Destructive + " !important;\n")
169 sb.WriteString(" --destructive-foreground: " + p.Background + " !important;\n")
170 // Keep GTK names for compatibility
171 sb.WriteString(" --bg: " + p.Background + " !important;\n")
172 sb.WriteString(" --surface: " + p.Surface + " !important;\n")
173 sb.WriteString(" --surface-variant: " + p.SurfaceVariant + " !important;\n")
174 sb.WriteString(" --text: " + p.Text + " !important;\n")
175 sb.WriteString(" --accent: " + p.Accent + " !important;\n")
176 return sb.String()
177}