[client] Add light mode with system, light, and dark theme options (#7344)

* desktop UI light mode

* Theme review fixes plus macOS window outline fix

* Windows runtime chrome re-theming plus apply serialization

* Windows chrome threading and theme event ordering fixes

* Darken toggle and setting sidebar text

* resolve theme appearance, apply on UI thread

* read theme once per window

* Re-assert Windows dark opt-in after SetTheme

* split app-wide GTK theming from per-window chrome

* Update Wails dependency and checksums

* KDE tray icon panel fix

* Five review fixes: theme ordering, cgo dedup, KDE panel resolution

* Path guard hardening, toggle contrast, windows comment

* non-vacuous escape tests

* Default view edits

* Polish settings nav, controls, borders, and disc

* Profiles settings boarder, modals, and buttons

* Additional edits based on feedback

* Switch colors away from slight blue hue

* Update missing lang

* Fix vertical tab active view
This commit is contained in:
Brandon Hopkins
2026-09-11 08:25:10 -07:00
committed by GitHub
parent f422c41654
commit ec0c36b0e7
68 changed files with 1853 additions and 259 deletions
+52 -1
View File
@@ -49,10 +49,34 @@ func (v ViewMode) IsValid() bool {
return false
}
// Theme is the preferred UI appearance: follow the OS ("system") or force
// "light"/"dark".
type Theme string
const (
ThemeSystem Theme = "system"
ThemeLight Theme = "light"
ThemeDark Theme = "dark"
)
// DefaultTheme applies when no file exists or its theme is empty/unknown.
const DefaultTheme = ThemeSystem
var ErrUnsupportedTheme = errors.New("unsupported theme")
func (t Theme) IsValid() bool {
switch t {
case ThemeSystem, ThemeLight, ThemeDark:
return true
}
return false
}
// UIPreferences is rewritten in full on every change; there are no partial updates.
type UIPreferences struct {
Language i18n.LanguageCode `json:"language"`
ViewMode ViewMode `json:"viewMode"`
Theme Theme `json:"theme"`
OnboardingCompleted bool `json:"onboardingCompleted"`
// AutostartInitialized records that the one-time autostart default
// decision has run for this OS user. It only ever transitions to true
@@ -105,7 +129,7 @@ func NewStore(validator LanguageValidator, emitter Emitter) (*Store, error) {
path: path,
validator: validator,
emitter: emitter,
current: UIPreferences{ViewMode: DefaultViewMode},
current: UIPreferences{ViewMode: DefaultViewMode, Theme: DefaultTheme},
}
if err := s.load(); err != nil {
@@ -146,6 +170,30 @@ func (s *Store) SetViewMode(mode ViewMode) error {
return nil
}
// SetTheme validates, persists, and broadcasts. No-op if unchanged.
func (s *Store) SetTheme(theme Theme) error {
if !theme.IsValid() {
return fmt.Errorf("%w: %q", ErrUnsupportedTheme, theme)
}
s.mu.Lock()
if s.current.Theme == theme {
s.mu.Unlock()
return nil
}
next := s.current
next.Theme = theme
if err := s.persistLocked(next); err != nil {
s.mu.Unlock()
return fmt.Errorf("persist preferences: %w", err)
}
s.current = next
s.mu.Unlock()
s.broadcast(next)
return nil
}
// SetOnboardingCompleted persists the welcome-window dismissal. No-op if unchanged.
func (s *Store) SetOnboardingCompleted(done bool) error {
s.mu.Lock()
@@ -288,6 +336,9 @@ func (s *Store) load() error {
if !loaded.ViewMode.IsValid() {
loaded.ViewMode = DefaultViewMode
}
if !loaded.Theme.IsValid() {
loaded.Theme = DefaultTheme
}
s.mu.Lock()
s.current = loaded