Files
Ben GotowandClaude 76e70293ce Add system accent color support with dynamic theming (#2682)
* Mirror the OS accent / tint color in light and dark themes

Adds a SystemAccentWatcher in the main process that reads the accent
color via systemPreferences.getAccentColor() (supported on macOS,
Windows, and Linux as of Electron 37+) and broadcasts changes over IPC.
On Windows/Linux it subscribes to `accent-color-changed`; on macOS it
listens for `AppleColorPreferencesChangedNotification`; on all platforms
it also refreshes on nativeTheme `updated` events.

The renderer-side ThemeManager injects the value as CSS custom
properties (`--system-accent`, `--system-accent-dark`) in a managed
<style> tag with priority 2, so switching tint doesn't bust the on-disk
LESS cache or require recompilation.

Accent-chain LESS variables and color-function usages (darken/lighten/
fade/fadeout/mix applied to @accent-primary / @component-active-color)
are rewritten to use CSS color-mix() / relative color syntax, which
LESS passes through to the browser. Per-theme defaults remain as the
var() fallback so themes still look right when system accent is off.

A new `core.appearance.useSystemAccent` preference (default on)
provides a toggle in Preferences → Appearance → Theme and Style.

* Fix LESS compile errors and color-mix transparent shift

- Replace LESS color functions (fade, fadeout, desaturate, lighten,
  darken) on accent-chain variables with native CSS equivalents so
  LESS doesn't try to evaluate a var() at compile time.
- Replace `color-mix(in srgb, transparent, X P%)` with
  `rgb(from X r g b / P%)` so we only change alpha without dragging
  RGB channels toward black (CSS `transparent` is rgba(0,0,0,0)).

* Replace darken() on color-mix token vars

darken(@token-hover-bottom/top, N%) and darken(@token-selected-*)
broke LESS compile because those variables now resolve to
color-mix() CSS expressions rather than literal colors. LESS's
darken() only works on values it can evaluate to a color.

The resulting LESS runtime error triggered theme-manager's
encountered-theme-error IPC, which opens a blocking
dialog.showMessageBoxSync in the main process — causing the
test suite to hang for 20 minutes in headless CI.

Swap those calls for `color-mix(in srgb, X, black N%)`, which
LESS passes through as opaque CSS and composes correctly with
color-mix values at runtime.

* Replace darken() in text-emphasis/bg-variant LESS mixins

.text-emphasis-variant(@color) and .bg-variant(@color) in the
static/style/mixins directory call darken(@color, 10%) on their
argument. type.less calls both mixins with @accent-primary, which
is now a var(--system-accent, ...) expression rather than a literal
color — LESS can't evaluate darken() on that and throws during
compile of static/style/index.less.

The broken compile surfaces via theme-manager's
encountered-theme-error IPC which opens a blocking
dialog.showMessageBoxSync on the main process, hanging the test
suite for 20 minutes in headless CI.

Swap for color-mix(in srgb, @color, black 10%) which LESS passes
through as CSS.

* Place system-accent toggle inline with Change Theme button

The useSystemAccent ConfigSchemaItem was rendering on its own line
below the large "Change Theme..." button, leaving the space to the
right of that button empty. Wrap both in a flex row with a 20px gap
so the checkbox sits alongside the button.

* Drop useSystemAccent note and prevent button shrink

The useSystemAccent setting is self-explanatory from its title.
Remove the explanatory note. Add flex-shrink: 0 to the Change
Theme button so it keeps its full width when the inline checkbox
content grows.

The default of true was already in place, so users are opted in.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 14:50:27 -05:00

190 lines
4.3 KiB
Plaintext

@import 'ui-variables';
@import 'ui-mixins';
@count-color: fadeout(@text-color-subtle, 40%);
.outline-view {
order: 1;
section {
margin-bottom: @padding-base-vertical;
}
.item-children {
padding-left: @padding-base-horizontal;
margin-bottom: 0;
}
.heading {
display: flex;
align-items: center;
cursor: default;
color: @text-color-very-subtle;
font-weight: @font-weight-semi-bold;
font-size: @font-size-small * 0.9;
padding-left: @padding-small-horizontal;
padding-top: @padding-small-horizontal;
padding-right: @padding-xs-horizontal;
letter-spacing: -0.2px;
span:first-child {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.add-item-button {
margin-left: @padding-small-horizontal;
margin-right: @padding-small-horizontal;
cursor: pointer;
img {
background: @text-color-very-subtle;
}
display: none;
}
.collapse-button {
cursor: pointer;
display: none;
min-width: 30px;
margin-left: auto;
margin-right: 8px;
}
}
&:hover {
.heading {
.collapse-button,
.add-item-button {
display: inherit;
}
}
}
.item-container {
display: flex;
&.dropping {
background-color: lighten(@source-list-bg, 20%);
.item {
color: @source-list-active-color;
img.content-mask {
background-color: @source-list-active-color;
}
}
}
}
.item {
color: @text-color-subtle;
flex: 1;
display: flex;
align-items: center;
font-size: @font-size-base;
font-weight: 400;
padding-right: @spacing-standard;
line-height: @line-height-large;
clear: both;
height: 27px;
// The overflow needs to be `hidden` in order for the name to flex
// fit within the contained space.
overflow: hidden;
img.content-mask {
background-color: @text-color-subtle;
vertical-align: text-bottom;
}
.icon {
flex-shrink: 0;
padding-right: @padding-small-horizontal * 0.85;
order: 1;
}
.name {
order: 2;
overflow: hidden;
line-height: @line-height-small;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-count-box {
order: 3;
flex-shrink: 0;
font-weight: @font-weight-semi-bold;
color: @count-color;
margin-left: @padding-small-horizontal * 0.8;
box-shadow: 0 0.5px 0 @count-color, 0 -0.5px 0 @count-color, 0.5px 0 0 @count-color,
-0.5px 0 0 @count-color;
}
.item-count-box.alt-count {
color: @source-list-active-bg;
background: @source-list-active-color;
box-shadow: none;
}
.item-count-box.archive,
.item-count-box.all,
.item-count-box.spam {
display: none;
}
.item-action-button {
order: 4;
flex-shrink: 0;
margin-left: auto;
cursor: pointer;
font-size: @font-size-base * 0.5;
letter-spacing: 1px;
line-height: 18px;
width: 18px;
text-align: center;
border-radius: 3px;
color: @text-color-very-subtle;
display: none;
&:hover {
color: @text-color-subtle;
background: fade(@text-color-subtle, 15%);
}
}
&.selected {
background: @source-list-active-bg;
color: @source-list-active-color;
img.content-mask {
background-color: @source-list-active-color;
}
.item-action-button {
color: rgb(from @source-list-active-color r g b / 70%);
&:hover {
color: @source-list-active-color;
background: rgb(from @source-list-active-color r g b / 15%);
}
}
}
&.dropping {
background-color: lighten(@source-list-bg, 20%);
color: @source-list-active-color;
img.content-mask {
background-color: @source-list-active-color;
}
}
&.editing {
align-items: center;
.item-input {
order: 2;
font-size: @font-size-small;
margin-left: @padding-small-horizontal * 0.3;
height: 22px;
padding-left: 0;
text-indent: @padding-small-horizontal * 0.55;
width: 85%;
}
}
&:hover {
cursor: default;
.item-action-button {
display: block;
}
}
}
}