feat(theme): inject CSS variables into WebUI pages for dynamic theming
Add complete theme injection infrastructure to sync GTK palette with WebUI via CSS custom properties. Implements port interface, use case, and webkit injector for CSS variable injection. Updates palette converter to generate Tailwind-compatible CSS variables and refines GTK button styling for accent color usage (borders only, per design requirements).
- commit
- ccc1c3eb6582f112377fe9f10e9a526b6d812767
- author
- brice · · 2025-12-18 17:03 UTC
- parent
- 8a86fff
- signature
- unsigned
6 files · +141 -6
M cmd/dumber/main.go +8 -0
| @@ -136,6 +136,14 @@ func runGUI() { | |||
| 136 | 136 | ||
| 137 | 137 | settings := webkit.NewSettingsManager(ctx, cfg) | |
| 138 | 138 | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | |
| 139 | + | ||
| 140 | + | // Prepare theme CSS for WebUI pages (dumb://*) | |
| 141 | + | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | |
| 142 | + | themeCSSVars := themeManager.GetCurrentPalette().ToWebCSSVars() | |
| 143 | + | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSVars}); err != nil { | |
| 144 | + | logger.Warn().Err(err).Msg("failed to prepare WebUI theme CSS") | |
| 145 | + | } | |
| 146 | + | ||
| 139 | 147 | messageRouter := webkit.NewMessageRouter(ctx) | |
| 140 | 148 | poolCfg := webkit.DefaultPoolConfig() | |
| 141 | 149 | pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter) | |
| @@ -136,6 +136,14 @@ func runGUI() { | |||||
| 136 | 136 | ||||
| 137 | settings := webkit.NewSettingsManager(ctx, cfg) | 137 | settings := webkit.NewSettingsManager(ctx, cfg) | ||
| 138 | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | 138 | injector := webkit.NewContentInjector(themeManager.PrefersDark()) | ||
| 139 | + | ||||
| 140 | + | // Prepare theme CSS for WebUI pages (dumb://*) | |||
| 141 | + | prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector) | |||
| 142 | + | themeCSSVars := themeManager.GetCurrentPalette().ToWebCSSVars() | |||
| 143 | + | if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSVars}); err != nil { | |||
| 144 | + | logger.Warn().Err(err).Msg("failed to prepare WebUI theme CSS") | |||
| 145 | + | } | |||
| 146 | + | ||||
| 139 | messageRouter := webkit.NewMessageRouter(ctx) | 147 | messageRouter := webkit.NewMessageRouter(ctx) | ||
| 140 | poolCfg := webkit.DefaultPoolConfig() | 148 | poolCfg := webkit.DefaultPoolConfig() | ||
| 141 | pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter) | 149 | pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter) | ||
A internal/application/port/content_injector.go +11 -0
| @@ -0,0 +1,11 @@ | |||
| 1 | + | package port | |
| 2 | + | ||
| 3 | + | import "context" | |
| 4 | + | ||
| 5 | + | // ContentInjector defines the port interface for injecting scripts and styles into web views. | |
| 6 | + | // This abstracts platform-specific content injection implementations (WebKit, etc.). | |
| 7 | + | type ContentInjector interface { | |
| 8 | + | // InjectThemeCSS injects CSS variables into the page for theming. | |
| 9 | + | // The css parameter should contain CSS custom property declarations. | |
| 10 | + | InjectThemeCSS(ctx context.Context, css string) error | |
| 11 | + | } | |
| @@ -0,0 +1,11 @@ | |||||
| 1 | + | package port | |||
| 2 | + | ||||
| 3 | + | import "context" | |||
| 4 | + | ||||
| 5 | + | // ContentInjector defines the port interface for injecting scripts and styles into web views. | |||
| 6 | + | // This abstracts platform-specific content injection implementations (WebKit, etc.). | |||
| 7 | + | type ContentInjector interface { | |||
| 8 | + | // InjectThemeCSS injects CSS variables into the page for theming. | |||
| 9 | + | // The css parameter should contain CSS custom property declarations. | |||
| 10 | + | InjectThemeCSS(ctx context.Context, css string) error | |||
| 11 | + | } | |||
A internal/application/usecase/prepare_webui_theme.go +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | package usecase | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | ||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |
| 7 | + | "github.com/bnema/dumber/internal/logging" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // PrepareWebUIThemeUseCase handles theme CSS injection for internal web pages. | |
| 11 | + | type PrepareWebUIThemeUseCase struct { | |
| 12 | + | injector port.ContentInjector | |
| 13 | + | } | |
| 14 | + | ||
| 15 | + | // NewPrepareWebUIThemeUseCase creates a new theme preparation use case. | |
| 16 | + | func NewPrepareWebUIThemeUseCase(injector port.ContentInjector) *PrepareWebUIThemeUseCase { | |
| 17 | + | return &PrepareWebUIThemeUseCase{ | |
| 18 | + | injector: injector, | |
| 19 | + | } | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | // PrepareWebUIThemeInput contains parameters for theme preparation. | |
| 23 | + | type PrepareWebUIThemeInput struct { | |
| 24 | + | // CSSVars is the CSS custom property declarations to inject. | |
| 25 | + | // Should be generated from palette.ToWebCSSVars(). | |
| 26 | + | CSSVars string | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | // Execute injects theme CSS variables into the web view. | |
| 30 | + | func (uc *PrepareWebUIThemeUseCase) Execute(ctx context.Context, input PrepareWebUIThemeInput) error { | |
| 31 | + | log := logging.FromContext(ctx) | |
| 32 | + | log.Debug().Msg("preparing WebUI theme CSS") | |
| 33 | + | ||
| 34 | + | if uc.injector == nil { | |
| 35 | + | log.Warn().Msg("content injector is nil, skipping theme injection") | |
| 36 | + | return nil | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | if input.CSSVars == "" { | |
| 40 | + | log.Debug().Msg("no CSS vars to inject") | |
| 41 | + | return nil | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | if err := uc.injector.InjectThemeCSS(ctx, input.CSSVars); err != nil { | |
| 45 | + | log.Error().Err(err).Msg("failed to inject theme CSS") | |
| 46 | + | return err | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | log.Debug().Msg("theme CSS injected successfully") | |
| 50 | + | return nil | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | package usecase | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/port" | |||
| 7 | + | "github.com/bnema/dumber/internal/logging" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // PrepareWebUIThemeUseCase handles theme CSS injection for internal web pages. | |||
| 11 | + | type PrepareWebUIThemeUseCase struct { | |||
| 12 | + | injector port.ContentInjector | |||
| 13 | + | } | |||
| 14 | + | ||||
| 15 | + | // NewPrepareWebUIThemeUseCase creates a new theme preparation use case. | |||
| 16 | + | func NewPrepareWebUIThemeUseCase(injector port.ContentInjector) *PrepareWebUIThemeUseCase { | |||
| 17 | + | return &PrepareWebUIThemeUseCase{ | |||
| 18 | + | injector: injector, | |||
| 19 | + | } | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | // PrepareWebUIThemeInput contains parameters for theme preparation. | |||
| 23 | + | type PrepareWebUIThemeInput struct { | |||
| 24 | + | // CSSVars is the CSS custom property declarations to inject. | |||
| 25 | + | // Should be generated from palette.ToWebCSSVars(). | |||
| 26 | + | CSSVars string | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | // Execute injects theme CSS variables into the web view. | |||
| 30 | + | func (uc *PrepareWebUIThemeUseCase) Execute(ctx context.Context, input PrepareWebUIThemeInput) error { | |||
| 31 | + | log := logging.FromContext(ctx) | |||
| 32 | + | log.Debug().Msg("preparing WebUI theme CSS") | |||
| 33 | + | ||||
| 34 | + | if uc.injector == nil { | |||
| 35 | + | log.Warn().Msg("content injector is nil, skipping theme injection") | |||
| 36 | + | return nil | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | if input.CSSVars == "" { | |||
| 40 | + | log.Debug().Msg("no CSS vars to inject") | |||
| 41 | + | return nil | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | if err := uc.injector.InjectThemeCSS(ctx, input.CSSVars); err != nil { | |||
| 45 | + | log.Error().Err(err).Msg("failed to inject theme CSS") | |||
| 46 | + | return err | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | log.Debug().Msg("theme CSS injected successfully") | |||
| 50 | + | return nil | |||
| 51 | + | } | |||
M internal/infrastructure/webkit/injector.go +43 -2
| @@ -3,6 +3,7 @@ package webkit | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "strings" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/logging" | |
| 8 | 9 | "github.com/bnema/puregotk-webkit/webkit" | |
| @@ -15,6 +16,15 @@ const ( | |||
| 15 | 16 | MessageHandlerName = "dumber" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| 19 | + | // themeCSSScript injects CSS custom properties into the page. | |
| 20 | + | // The %s placeholder is replaced with the CSS variable declarations (newlines escaped as \n). | |
| 21 | + | const themeCSSScript = `(function() { | |
| 22 | + | var style = document.createElement('style'); | |
| 23 | + | style.setAttribute('data-dumber-theme', ''); | |
| 24 | + | style.textContent = ':root {%s}'; | |
| 25 | + | (document.head || document.documentElement).appendChild(style); | |
| 26 | + | })();` | |
| 27 | + | ||
| 18 | 28 | // darkModeScript patches window.matchMedia and sets theme class on <html>. | |
| 19 | 29 | // It must be injected at document start, after __dumber_gtk_prefers_dark is set. | |
| 20 | 30 | const darkModeScript = `(function() { | |
| @@ -75,9 +85,12 @@ const darkModeScript = `(function() { | |||
| 75 | 85 | })();` | |
| 76 | 86 | ||
| 77 | 87 | // ContentInjector encapsulates script injection into WebViews. | |
| 78 | - | // It injects minimal scripts for dark mode detection in web pages. | |
| 88 | + | // It injects minimal scripts for dark mode detection in web pages | |
| 89 | + | // and theme CSS variables for internal pages (dumb://). | |
| 90 | + | // Implements port.ContentInjector interface. | |
| 79 | 91 | type ContentInjector struct { | |
| 80 | - | prefersDark bool | |
| 92 | + | prefersDark bool | |
| 93 | + | themeCSSVars string // CSS custom property declarations for WebUI | |
| 81 | 94 | } | |
| 82 | 95 | ||
| 83 | 96 | // NewContentInjector creates a new injector instance. | |
| @@ -88,6 +101,16 @@ func NewContentInjector(prefersDark bool) *ContentInjector { | |||
| 88 | 101 | } | |
| 89 | 102 | } | |
| 90 | 103 | ||
| 104 | + | // InjectThemeCSS stores CSS variables for injection into internal pages. | |
| 105 | + | // Implements port.ContentInjector interface. | |
| 106 | + | // The CSS will be injected when InjectScripts is called on WebView creation. | |
| 107 | + | func (ci *ContentInjector) InjectThemeCSS(ctx context.Context, css string) error { | |
| 108 | + | log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger() | |
| 109 | + | ci.themeCSSVars = css | |
| 110 | + | log.Debug().Int("css_len", len(css)).Msg("theme CSS vars set for injection") | |
| 111 | + | return nil | |
| 112 | + | } | |
| 113 | + | ||
| 91 | 114 | // PrefersDark returns the current dark mode preference. | |
| 92 | 115 | func (ci *ContentInjector) PrefersDark() bool { | |
| 93 | 116 | return ci.prefersDark | |
| @@ -161,5 +184,23 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||
| 161 | 184 | "dark-mode-handler", | |
| 162 | 185 | ) | |
| 163 | 186 | ||
| 187 | + | // 4. Inject theme CSS variables for internal pages (dumb://* only) | |
| 188 | + | if ci.themeCSSVars != "" { | |
| 189 | + | // Escape newlines for JS string literal | |
| 190 | + | escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\n", "\\n") | |
| 191 | + | themeCSSInjectionScript := fmt.Sprintf(themeCSSScript, escapedCSS) | |
| 192 | + | addScript( | |
| 193 | + | webkit.NewUserScript( | |
| 194 | + | themeCSSInjectionScript, | |
| 195 | + | webkit.UserContentInjectTopFrameValue, | |
| 196 | + | webkit.UserScriptInjectAtDocumentEndValue, | |
| 197 | + | nil, // Inject on all pages - allowlist patterns don't work for custom schemes | |
| 198 | + | nil, | |
| 199 | + | ), | |
| 200 | + | "theme-css-vars", | |
| 201 | + | ) | |
| 202 | + | log.Debug().Msg("theme CSS vars injection configured") | |
| 203 | + | } | |
| 204 | + | ||
| 164 | 205 | log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected") | |
| 165 | 206 | } | |
| @@ -3,6 +3,7 @@ package webkit | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "strings" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/logging" | 8 | "github.com/bnema/dumber/internal/logging" | ||
| 8 | "github.com/bnema/puregotk-webkit/webkit" | 9 | "github.com/bnema/puregotk-webkit/webkit" | ||
| @@ -15,6 +16,15 @@ const ( | |||||
| 15 | MessageHandlerName = "dumber" | 16 | MessageHandlerName = "dumber" | ||
| 16 | ) | 17 | ) | ||
| 17 | 18 | ||||
| 19 | + | // themeCSSScript injects CSS custom properties into the page. | |||
| 20 | + | // The %s placeholder is replaced with the CSS variable declarations (newlines escaped as \n). | |||
| 21 | + | const themeCSSScript = `(function() { | |||
| 22 | + | var style = document.createElement('style'); | |||
| 23 | + | style.setAttribute('data-dumber-theme', ''); | |||
| 24 | + | style.textContent = ':root {%s}'; | |||
| 25 | + | (document.head || document.documentElement).appendChild(style); | |||
| 26 | + | })();` | |||
| 27 | + | ||||
| 18 | // darkModeScript patches window.matchMedia and sets theme class on <html>. | 28 | // darkModeScript patches window.matchMedia and sets theme class on <html>. | ||
| 19 | // It must be injected at document start, after __dumber_gtk_prefers_dark is set. | 29 | // It must be injected at document start, after __dumber_gtk_prefers_dark is set. | ||
| 20 | const darkModeScript = `(function() { | 30 | const darkModeScript = `(function() { | ||
| @@ -75,9 +85,12 @@ const darkModeScript = `(function() { | |||||
| 75 | })();` | 85 | })();` | ||
| 76 | 86 | ||||
| 77 | // ContentInjector encapsulates script injection into WebViews. | 87 | // ContentInjector encapsulates script injection into WebViews. | ||
| 78 | - | // It injects minimal scripts for dark mode detection in web pages. | 88 | + | // It injects minimal scripts for dark mode detection in web pages |
| 89 | + | // and theme CSS variables for internal pages (dumb://). | |||
| 90 | + | // Implements port.ContentInjector interface. | |||
| 79 | type ContentInjector struct { | 91 | type ContentInjector struct { | ||
| 80 | - | prefersDark bool | 92 | + | prefersDark bool |
| 93 | + | themeCSSVars string // CSS custom property declarations for WebUI | |||
| 81 | } | 94 | } | ||
| 82 | 95 | ||||
| 83 | // NewContentInjector creates a new injector instance. | 96 | // NewContentInjector creates a new injector instance. | ||
| @@ -88,6 +101,16 @@ func NewContentInjector(prefersDark bool) *ContentInjector { | |||||
| 88 | } | 101 | } | ||
| 89 | } | 102 | } | ||
| 90 | 103 | ||||
| 104 | + | // InjectThemeCSS stores CSS variables for injection into internal pages. | |||
| 105 | + | // Implements port.ContentInjector interface. | |||
| 106 | + | // The CSS will be injected when InjectScripts is called on WebView creation. | |||
| 107 | + | func (ci *ContentInjector) InjectThemeCSS(ctx context.Context, css string) error { | |||
| 108 | + | log := logging.FromContext(ctx).With().Str("component", "content-injector").Logger() | |||
| 109 | + | ci.themeCSSVars = css | |||
| 110 | + | log.Debug().Int("css_len", len(css)).Msg("theme CSS vars set for injection") | |||
| 111 | + | return nil | |||
| 112 | + | } | |||
| 113 | + | ||||
| 91 | // PrefersDark returns the current dark mode preference. | 114 | // PrefersDark returns the current dark mode preference. | ||
| 92 | func (ci *ContentInjector) PrefersDark() bool { | 115 | func (ci *ContentInjector) PrefersDark() bool { | ||
| 93 | return ci.prefersDark | 116 | return ci.prefersDark | ||
| @@ -161,5 +184,23 @@ func (ci *ContentInjector) InjectScripts(ctx context.Context, ucm *webkit.UserCo | |||||
| 161 | "dark-mode-handler", | 184 | "dark-mode-handler", | ||
| 162 | ) | 185 | ) | ||
| 163 | 186 | ||||
| 187 | + | // 4. Inject theme CSS variables for internal pages (dumb://* only) | |||
| 188 | + | if ci.themeCSSVars != "" { | |||
| 189 | + | // Escape newlines for JS string literal | |||
| 190 | + | escapedCSS := strings.ReplaceAll(ci.themeCSSVars, "\n", "\\n") | |||
| 191 | + | themeCSSInjectionScript := fmt.Sprintf(themeCSSScript, escapedCSS) | |||
| 192 | + | addScript( | |||
| 193 | + | webkit.NewUserScript( | |||
| 194 | + | themeCSSInjectionScript, | |||
| 195 | + | webkit.UserContentInjectTopFrameValue, | |||
| 196 | + | webkit.UserScriptInjectAtDocumentEndValue, | |||
| 197 | + | nil, // Inject on all pages - allowlist patterns don't work for custom schemes | |||
| 198 | + | nil, | |||
| 199 | + | ), | |||
| 200 | + | "theme-css-vars", | |||
| 201 | + | ) | |||
| 202 | + | log.Debug().Msg("theme CSS vars injection configured") | |||
| 203 | + | } | |||
| 204 | + | ||||
| 164 | log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected") | 205 | log.Debug().Bool("prefers_dark", ci.prefersDark).Msg("minimal scripts injected") | ||
| 165 | } | 206 | } | ||
M internal/ui/theme/css.go +4 -3
| @@ -99,7 +99,8 @@ button.tab-button:hover { | |||
| 99 | 99 | } | |
| 100 | 100 | ||
| 101 | 101 | button.tab-button.tab-button-active { | |
| 102 | - | background-color: shade(var(--surface-variant), 1.4); | |
| 102 | + | background-color: shade(var(--surface-variant), 1.2); | |
| 103 | + | border-bottom: 0.125em solid var(--accent); | |
| 103 | 104 | font-weight: 600; | |
| 104 | 105 | } | |
| 105 | 106 | ||
| @@ -353,7 +354,7 @@ button.stacked-pane-title-button { | |||
| 353 | 354 | } | |
| 354 | 355 | ||
| 355 | 356 | button.stacked-pane-title-button:hover { | |
| 356 | - | background-color: alpha(var(--accent), 0.15); | |
| 357 | + | background-color: shade(var(--surface-variant), 1.15); | |
| 357 | 358 | } | |
| 358 | 359 | ||
| 359 | 360 | button.stacked-pane-title-button:focus { | |
| @@ -363,7 +364,7 @@ button.stacked-pane-title-button:focus { | |||
| 363 | 364 | ||
| 364 | 365 | /* Title bar content box */ | |
| 365 | 366 | .stacked-pane-titlebar.active { | |
| 366 | - | background-color: alpha(var(--accent), 0.1); | |
| 367 | + | background-color: shade(var(--surface-variant), 1.2); | |
| 367 | 368 | border-left: 0.1875em solid var(--accent); | |
| 368 | 369 | } | |
| 369 | 370 | ||
| @@ -99,7 +99,8 @@ button.tab-button:hover { | |||||
| 99 | } | 99 | } | ||
| 100 | 100 | ||||
| 101 | button.tab-button.tab-button-active { | 101 | button.tab-button.tab-button-active { | ||
| 102 | - | background-color: shade(var(--surface-variant), 1.4); | 102 | + | background-color: shade(var(--surface-variant), 1.2); |
| 103 | + | border-bottom: 0.125em solid var(--accent); | |||
| 103 | font-weight: 600; | 104 | font-weight: 600; | ||
| 104 | } | 105 | } | ||
| 105 | 106 | ||||
| @@ -353,7 +354,7 @@ button.stacked-pane-title-button { | |||||
| 353 | } | 354 | } | ||
| 354 | 355 | ||||
| 355 | button.stacked-pane-title-button:hover { | 356 | button.stacked-pane-title-button:hover { | ||
| 356 | - | background-color: alpha(var(--accent), 0.15); | 357 | + | background-color: shade(var(--surface-variant), 1.15); |
| 357 | } | 358 | } | ||
| 358 | 359 | ||||
| 359 | button.stacked-pane-title-button:focus { | 360 | button.stacked-pane-title-button:focus { | ||
| @@ -363,7 +364,7 @@ button.stacked-pane-title-button:focus { | |||||
| 363 | 364 | ||||
| 364 | /* Title bar content box */ | 365 | /* Title bar content box */ | ||
| 365 | .stacked-pane-titlebar.active { | 366 | .stacked-pane-titlebar.active { | ||
| 366 | - | background-color: alpha(var(--accent), 0.1); | 367 | + | background-color: shade(var(--surface-variant), 1.2); |
| 367 | border-left: 0.1875em solid var(--accent); | 368 | border-left: 0.1875em solid var(--accent); | ||
| 368 | } | 369 | } | ||
| 369 | 370 | ||||
M internal/ui/theme/palette.go +24 -1
| @@ -114,7 +114,7 @@ func (p Palette) Validate() error { | |||
| 114 | 114 | return nil | |
| 115 | 115 | } | |
| 116 | 116 | ||
| 117 | - | // ToCSSVars generates CSS custom property declarations. | |
| 117 | + | // ToCSSVars generates CSS custom property declarations for GTK. | |
| 118 | 118 | func (p Palette) ToCSSVars() string { | |
| 119 | 119 | var sb strings.Builder | |
| 120 | 120 | sb.WriteString(" --bg: " + p.Background + ";\n") | |
| @@ -126,3 +126,26 @@ func (p Palette) ToCSSVars() string { | |||
| 126 | 126 | sb.WriteString(" --border: " + p.Border + ";\n") | |
| 127 | 127 | return sb.String() | |
| 128 | 128 | } | |
| 129 | + | ||
| 130 | + | // ToWebCSSVars generates CSS custom properties for web UI (Tailwind compatibility). | |
| 131 | + | // Maps GTK palette names to Tailwind variable names used in the WebUI homepage. | |
| 132 | + | func (p Palette) ToWebCSSVars() string { | |
| 133 | + | var sb strings.Builder | |
| 134 | + | // Tailwind-compatible variables | |
| 135 | + | sb.WriteString(" --background: " + p.Background + ";\n") | |
| 136 | + | sb.WriteString(" --foreground: " + p.Text + ";\n") | |
| 137 | + | sb.WriteString(" --card: " + p.Surface + ";\n") | |
| 138 | + | sb.WriteString(" --card-foreground: " + p.Text + ";\n") | |
| 139 | + | sb.WriteString(" --primary: " + p.Accent + ";\n") | |
| 140 | + | sb.WriteString(" --primary-foreground: " + p.Background + ";\n") | |
| 141 | + | sb.WriteString(" --muted: " + p.SurfaceVariant + ";\n") | |
| 142 | + | sb.WriteString(" --muted-foreground: " + p.Muted + ";\n") | |
| 143 | + | sb.WriteString(" --border: " + p.Border + ";\n") | |
| 144 | + | // Keep GTK names for compatibility | |
| 145 | + | sb.WriteString(" --bg: " + p.Background + ";\n") | |
| 146 | + | sb.WriteString(" --surface: " + p.Surface + ";\n") | |
| 147 | + | sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n") | |
| 148 | + | sb.WriteString(" --text: " + p.Text + ";\n") | |
| 149 | + | sb.WriteString(" --accent: " + p.Accent + ";\n") | |
| 150 | + | return sb.String() | |
| 151 | + | } | |
| @@ -114,7 +114,7 @@ func (p Palette) Validate() error { | |||||
| 114 | return nil | 114 | return nil | ||
| 115 | } | 115 | } | ||
| 116 | 116 | ||||
| 117 | - | // ToCSSVars generates CSS custom property declarations. | 117 | + | // ToCSSVars generates CSS custom property declarations for GTK. |
| 118 | func (p Palette) ToCSSVars() string { | 118 | func (p Palette) ToCSSVars() string { | ||
| 119 | var sb strings.Builder | 119 | var sb strings.Builder | ||
| 120 | sb.WriteString(" --bg: " + p.Background + ";\n") | 120 | sb.WriteString(" --bg: " + p.Background + ";\n") | ||
| @@ -126,3 +126,26 @@ func (p Palette) ToCSSVars() string { | |||||
| 126 | sb.WriteString(" --border: " + p.Border + ";\n") | 126 | sb.WriteString(" --border: " + p.Border + ";\n") | ||
| 127 | return sb.String() | 127 | return sb.String() | ||
| 128 | } | 128 | } | ||
| 129 | + | ||||
| 130 | + | // ToWebCSSVars generates CSS custom properties for web UI (Tailwind compatibility). | |||
| 131 | + | // Maps GTK palette names to Tailwind variable names used in the WebUI homepage. | |||
| 132 | + | func (p Palette) ToWebCSSVars() string { | |||
| 133 | + | var sb strings.Builder | |||
| 134 | + | // Tailwind-compatible variables | |||
| 135 | + | sb.WriteString(" --background: " + p.Background + ";\n") | |||
| 136 | + | sb.WriteString(" --foreground: " + p.Text + ";\n") | |||
| 137 | + | sb.WriteString(" --card: " + p.Surface + ";\n") | |||
| 138 | + | sb.WriteString(" --card-foreground: " + p.Text + ";\n") | |||
| 139 | + | sb.WriteString(" --primary: " + p.Accent + ";\n") | |||
| 140 | + | sb.WriteString(" --primary-foreground: " + p.Background + ";\n") | |||
| 141 | + | sb.WriteString(" --muted: " + p.SurfaceVariant + ";\n") | |||
| 142 | + | sb.WriteString(" --muted-foreground: " + p.Muted + ";\n") | |||
| 143 | + | sb.WriteString(" --border: " + p.Border + ";\n") | |||
| 144 | + | // Keep GTK names for compatibility | |||
| 145 | + | sb.WriteString(" --bg: " + p.Background + ";\n") | |||
| 146 | + | sb.WriteString(" --surface: " + p.Surface + ";\n") | |||
| 147 | + | sb.WriteString(" --surface-variant: " + p.SurfaceVariant + ";\n") | |||
| 148 | + | sb.WriteString(" --text: " + p.Text + ";\n") | |||
| 149 | + | sb.WriteString(" --accent: " + p.Accent + ";\n") | |||
| 150 | + | return sb.String() | |||
| 151 | + | } | |||