feat(ui): Implement theme persistence and system preference support

Adds comprehensive theme management across the application, allowing users to save their preferred color scheme (light/dark) or automatically follow the operating system's settings. This required updating state management in App.svelte to handle theme loading and applying changes dynamically via CSS variables.

The styling layer was significantly updated with new CSS variables and rules for both dark and light modes, ensuring visual consistency across all components like dialogs, buttons, and sidebars.

- Added logic to detect system color scheme preference
- Implemented full support for persistent light/dark mode switching
- Overhauled global CSS variables for improved theme adaptability
This commit is contained in:
Christoph Brandau
2026-07-10 12:24:14 +02:00
parent 7ef1a2dac8
commit 25b98d7c4d
7 changed files with 680 additions and 35 deletions
+512 -21
View File
@@ -29,26 +29,88 @@
--font-mono: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
}
:root {
--app-color-scheme: dark;
--app-bg:
radial-gradient(circle at 22% 8%, rgba(189, 52, 254, 0.28), transparent 34%),
radial-gradient(circle at 74% 10%, rgba(65, 209, 255, 0.22), transparent 30%),
radial-gradient(circle at 88% 78%, rgba(255, 211, 67, 0.1), transparent 26%),
linear-gradient(135deg, #090912 0%, #111122 45%, #0b0b14 100%);
--app-button-bg: linear-gradient(180deg, rgba(42, 43, 70, 0.9), rgba(27, 28, 48, 0.92));
--app-input-bg: rgba(13, 14, 26, 0.78);
--app-select-option-bg: #1c2033;
--app-panel-highlight: linear-gradient(180deg, rgba(255,255,255,0.045), transparent 72%);
--app-panel-shadow: 0 18px 60px rgba(0,0,0,0.24), inset 0 1px 0 rgba(255,255,255,0.05);
--app-dialog-backdrop: #050712;
--app-dialog-bg: #111321;
--app-dialog-chrome: #171a2b;
--app-dialog-shadow: 0 24px 72px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0,0,0,0.4);
--app-settings-row-bg: #151827;
--app-scrollbar-thumb: #2a2f45;
--app-scrollbar-thumb-hover: #3a3f58;
}
:root[data-theme="light"] {
--color-ink: #172033;
--color-ink-muted: #475569;
--color-ink-faint: #728098;
--color-ink-dim: #5f6f89;
--color-ink-quiet: #66758d;
--color-surface: rgba(255, 255, 255, 0.86);
--color-surface-alt: #f3f6fb;
--color-surface-dim: rgba(244, 247, 252, 0.92);
--color-surface-hover: rgba(226, 233, 245, 0.82);
--color-surface-raised: rgba(255, 255, 255, 0.94);
--color-surface-solid: #ffffff;
--color-border: rgba(61, 89, 142, 0.24);
--color-border-subtle: rgba(34, 49, 78, 0.11);
--color-border-input: rgba(46, 117, 182, 0.28);
--color-primary: #315fd6;
--color-primary-dark: #284cb4;
--color-accent: #0f8fb5;
--color-bar: rgba(248, 251, 255, 0.94);
--color-bar-text: #172033;
--color-bar-muted: #64728a;
--app-color-scheme: light;
--app-bg:
radial-gradient(circle at 12% 0%, rgba(49, 95, 214, 0.12), transparent 30%),
radial-gradient(circle at 82% 4%, rgba(15, 143, 181, 0.12), transparent 28%),
linear-gradient(135deg, #f7f9fd 0%, #eef3f9 48%, #f8fafc 100%);
--app-button-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(235, 240, 249, 0.95));
--app-input-bg: rgba(255, 255, 255, 0.9);
--app-select-option-bg: #ffffff;
--app-panel-highlight: linear-gradient(180deg, rgba(255,255,255,0.72), transparent 70%);
--app-panel-shadow: 0 18px 48px rgba(28, 44, 74, 0.12), inset 0 1px 0 rgba(255,255,255,0.86);
--app-dialog-backdrop: rgba(232, 238, 248, 0.96);
--app-dialog-bg: #ffffff;
--app-dialog-chrome: #f4f7fc;
--app-dialog-shadow: 0 24px 72px rgba(28, 44, 74, 0.2), 0 2px 12px rgba(28,44,74,0.1);
--app-settings-row-bg: #f8fafd;
--app-scrollbar-thumb: #c2cada;
--app-scrollbar-thumb-hover: #aeb8cb;
}
@layer base {
*, *::before, *::after { box-sizing: border-box; }
html, body, #app { width: 100%; height: 100%; margin: 0; }
* { scrollbar-width: thin; scrollbar-color: #2a2f45 transparent; }
* { scrollbar-width: thin; scrollbar-color: var(--app-scrollbar-thumb) transparent; }
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #2a2f45; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #3a3f58; }
::-webkit-scrollbar-thumb { background: var(--app-scrollbar-thumb); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--app-scrollbar-thumb-hover); }
html { color-scheme: dark; }
html { color-scheme: var(--app-color-scheme); }
body {
overflow: hidden;
color: var(--color-ink);
background:
radial-gradient(circle at 22% 8%, rgba(189, 52, 254, 0.28), transparent 34%),
radial-gradient(circle at 74% 10%, rgba(65, 209, 255, 0.22), transparent 30%),
radial-gradient(circle at 88% 78%, rgba(255, 211, 67, 0.1), transparent 26%),
linear-gradient(135deg, #090912 0%, #111122 45%, #0b0b14 100%);
background: var(--app-bg);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -68,7 +130,7 @@
border: 1px solid var(--color-border);
border-radius: 8px;
color: var(--color-ink-muted);
background: linear-gradient(180deg, rgba(42, 43, 70, 0.9), rgba(27, 28, 48, 0.92));
background: var(--app-button-bg);
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
font-size: 13px;
@@ -85,7 +147,7 @@
border: 1px solid var(--color-border-input);
border-radius: 8px;
color: var(--color-ink);
background: rgba(13, 14, 26, 0.78);
background: var(--app-input-bg);
outline: none;
}
input:focus, textarea:focus, select:focus {
@@ -117,7 +179,7 @@
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(90, 140, 248, 0.18);
}
select option { background: #1c2033; color: var(--color-ink); }
select option { background: var(--app-select-option-bg); color: var(--color-ink); }
}
@layer components {
@@ -169,9 +231,9 @@
border: 1px solid var(--color-border);
border-radius: 12px;
background:
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 72%),
var(--app-panel-highlight),
var(--color-surface);
box-shadow: 0 18px 60px rgba(0,0,0,0.24), inset 0 1px 0 rgba(255,255,255,0.05);
box-shadow: var(--app-panel-shadow);
backdrop-filter: blur(16px);
}
@@ -2630,7 +2692,7 @@
display: grid;
place-items: center;
padding: 24px;
background-color: #050712;
background-color: var(--app-dialog-backdrop);
background-image: none;
}
@@ -2645,9 +2707,9 @@
height: min(760px, 100%);
border: 1px solid var(--color-border);
border-radius: 12px;
background-color: #111321;
background-color: var(--app-dialog-bg);
background-image: none;
box-shadow: 0 24px 72px rgba(0, 0, 0, 0.6), 0 2px 12px rgba(0,0,0,0.4);
box-shadow: var(--app-dialog-shadow);
overflow: hidden;
}
.compare-dialog {
@@ -2795,6 +2857,56 @@
font-size: 15px;
font-weight: 750;
}
.settings-segmented {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
min-width: 0;
padding: 4px;
border: 1px solid var(--color-border-subtle);
border-radius: 8px;
background: var(--app-settings-row-bg);
}
.settings-segmented label {
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 32px;
padding: 0 10px;
border: 1px solid transparent;
border-radius: 6px;
color: var(--color-ink-dim);
cursor: pointer;
font-size: 12px;
font-weight: 800;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.settings-segmented label:hover {
border-color: var(--color-border-subtle);
color: var(--color-ink);
background: var(--color-surface-hover);
}
.settings-segmented label.active {
border-color: var(--color-border-input);
color: var(--color-ink);
background: var(--color-surface-raised);
box-shadow: 0 1px 4px rgba(28,44,74,0.08);
}
.settings-segmented input {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.settings-segmented span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.settings-toggle-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
@@ -2804,7 +2916,7 @@
padding: 12px;
border: 1px solid var(--color-border-subtle);
border-radius: 8px;
background: #151827;
background: var(--app-settings-row-bg);
color: var(--color-ink-muted);
cursor: pointer;
}
@@ -2970,7 +3082,7 @@
gap: 8px;
}
.dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--color-border-subtle); background: #171a2b; }
.dialog-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.dialog-header > div:first-child { min-width: 0; }
.dialog-header-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex: 0 0 auto; min-width: 0; }
.compare-restore { max-width: 170px; min-width: 0; }
@@ -2984,7 +3096,7 @@
.dialog-body { display: grid; grid-template-columns: 280px minmax(0, 1fr); min-height: 0; }
.compare-dialog .dialog-body { grid-template-columns: minmax(260px, 320px) minmax(0, 1fr); }
.dialog-files { display: grid; align-content: start; gap: 4px; padding: 8px; overflow: auto; border-right: 1px solid var(--color-border-subtle); background: #171a2b; }
.dialog-files { display: grid; align-content: start; gap: 4px; padding: 8px; overflow: auto; border-right: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.dialog-file-row {
display: grid;
@@ -3132,7 +3244,7 @@
letter-spacing: 0;
}
.dialog-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-top: 1px solid var(--color-border-subtle); background: #171a2b; }
.dialog-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.dialog-footer-info { color: var(--color-ink-dim); font-size: 13px; font-weight: 700; }
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #4eca76; font-size: 12px; font-weight: 700; }
@@ -4388,6 +4500,381 @@
.resolve-path { overflow: hidden; color: var(--color-ink-faint); font-family: var(--font-mono); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
}
:root[data-theme="light"] button:hover:not(:disabled) {
background: var(--color-surface-hover);
}
:root[data-theme="light"] input:focus,
:root[data-theme="light"] textarea:focus,
:root[data-theme="light"] select:focus {
box-shadow: 0 0 0 3px rgba(49, 95, 214, 0.14), 0 10px 24px rgba(28, 44, 74, 0.06);
}
:root[data-theme="light"] .btn-primary {
border-color: rgba(49, 95, 214, 0.72);
background: linear-gradient(135deg, #315fd6 0%, #0f8fb5 100%);
box-shadow: 0 10px 24px rgba(49, 95, 214, 0.18);
}
:root[data-theme="light"] .btn-primary:hover:not(:disabled) {
border-color: rgba(15, 143, 181, 0.72);
background: linear-gradient(135deg, #3d6ee4 0%, #139dc7 100%);
}
:root[data-theme="light"] .btn-secondary {
border-color: rgba(15, 143, 181, 0.28);
color: #0c6683;
background: linear-gradient(180deg, rgba(15, 143, 181, 0.08), rgba(49, 95, 214, 0.06));
}
:root[data-theme="light"] .btn-secondary:hover:not(:disabled) {
color: #0d4f9e;
background: linear-gradient(180deg, rgba(15, 143, 181, 0.13), rgba(49, 95, 214, 0.1));
}
:root[data-theme="light"] .titlebar {
background:
linear-gradient(90deg, rgba(49, 95, 214, 0.08), rgba(15, 143, 181, 0.05), rgba(214, 156, 49, 0.04)),
var(--color-bar);
}
:root[data-theme="light"] .titlebar-brand,
:root[data-theme="light"] .titlebar-right {
border-color: rgba(34,49,78,0.1);
}
:root[data-theme="light"] .titlebar-brand-icon img {
filter: drop-shadow(0 1px 2px rgba(28,44,74,0.18));
}
:root[data-theme="light"] .tb-version {
color: rgba(23,32,51,0.36);
}
:root[data-theme="light"] .titlebar-info svg {
color: var(--color-accent);
}
:root[data-theme="light"] .tb-repo,
:root[data-theme="light"] .tb-no-repo {
color: var(--color-bar-muted);
}
:root[data-theme="light"] .tb-sep {
color: rgba(23,32,51,0.28);
}
:root[data-theme="light"] .tb-branch {
color: var(--color-bar-text);
}
:root[data-theme="light"] .tb-action:hover:not(:disabled),
:root[data-theme="light"] .tb-btn:hover:not(:disabled) {
background: rgba(49,95,214,0.08);
color: var(--color-ink);
}
:root[data-theme="light"] .titlebar-divider {
background: rgba(34,49,78,0.1);
}
:root[data-theme="light"] .tb-btn {
color: var(--color-bar-muted);
}
:root[data-theme="light"] .topbar {
border-color: var(--color-border);
background:
linear-gradient(90deg, rgba(49,95,214,0.06), rgba(15,143,181,0.045), rgba(214,156,49,0.035)),
var(--color-surface);
}
:root[data-theme="light"] .repo-tabbar,
:root[data-theme="light"] .repo-management-tools {
background: rgba(255,255,255,0.78);
}
:root[data-theme="light"] .repo-tab-wrap {
background: rgba(244,247,252,0.72);
}
:root[data-theme="light"] .repo-tab-wrap.active {
background: rgba(49,95,214,0.1);
}
:root[data-theme="light"] .repo-row {
border-bottom-color: rgba(34,49,78,0.08);
}
:root[data-theme="light"] .repo-row-main:hover:not(:disabled) {
background: rgba(49,95,214,0.07);
}
:root[data-theme="light"] .repo-row-icon {
border-left-color: rgba(34,49,78,0.08);
}
:root[data-theme="light"] .notice.error,
:root[data-theme="light"] .cred-error {
color: #b33445;
background: rgba(210, 55, 75, 0.08);
}
:root[data-theme="light"] .notice.busy {
color: #315fd6;
}
:root[data-theme="light"] .notice.conflict,
:root[data-theme="light"] .notice-action.conflict,
:root[data-theme="light"] .resolve-status,
:root[data-theme="light"] .resolve-conflict-label {
color: #96620f;
}
:root[data-theme="light"] .notice.rebase,
:root[data-theme="light"] .notice-action.rebase {
color: #6f3ca8;
}
:root[data-theme="light"] .update-toast {
box-shadow: 0 22px 56px rgba(28,44,74,0.18), 0 0 0 1px rgba(255,255,255,0.75) inset;
}
:root[data-theme="light"] .branch-filter-backdrop {
background: rgba(232,238,248,0.96);
}
:root[data-theme="light"] .branch-filter-dialog {
background:
linear-gradient(180deg, #ffffff 0%, #f4f7fc 72%),
#f4f7fc;
box-shadow: var(--app-dialog-shadow);
}
:root[data-theme="light"] .graph-branch-dialog-button,
:root[data-theme="light"] .branch-filter-option {
color: #16723b;
}
:root[data-theme="light"] .graph-branch-dialog-button span {
color: #ffffff;
background: #22834a;
}
:root[data-theme="light"] .main-panel {
background: var(--color-surface);
box-shadow: var(--app-panel-shadow);
}
:root[data-theme="light"] .repo-management {
background: var(--color-surface);
box-shadow: var(--app-panel-shadow);
}
:root[data-theme="light"] .repo-section {
background: rgba(255,255,255,0.62);
}
:root[data-theme="light"] .repo-section > header {
background: rgba(244,247,252,0.88);
}
:root[data-theme="light"] .repo-empty {
background: rgba(255,255,255,0.48);
}
:root[data-theme="light"] .repo-summary,
:root[data-theme="light"] .lane-header,
:root[data-theme="light"] .stash-toolbar,
:root[data-theme="light"] .dialog-footer,
:root[data-theme="light"] .cred-header {
background: var(--color-surface-dim);
}
:root[data-theme="light"] .file-row.selected,
:root[data-theme="light"] .branch-row.current,
:root[data-theme="light"] .file-history-row.active {
background: rgba(49,95,214,0.08);
}
:root[data-theme="light"] .graph-list {
background: rgba(248,250,253,0.9);
}
:root[data-theme="light"] .graph-gutter {
border-right-color: rgba(34,49,78,0.1);
background:
linear-gradient(90deg, rgba(49,95,214,0.035), rgba(255,255,255,0.18)),
rgba(238,243,249,0.78);
}
:root[data-theme="light"] .graph-dot {
border-color: #ffffff;
box-shadow: 0 0 0 1px rgba(34,49,78,0.16), 0 2px 6px rgba(28,44,74,0.12);
}
:root[data-theme="light"] .graph-dot.merge {
background: #ffffff;
box-shadow: 0 0 0 1px rgba(34,49,78,0.14);
}
:root[data-theme="light"] .commit-body {
background: rgba(255,255,255,0.72);
}
:root[data-theme="light"] .graph-row + .graph-row .commit-body {
border-top-color: rgba(34,49,78,0.08);
}
:root[data-theme="light"] .graph-row:hover .commit-body {
background: rgba(235,241,250,0.9);
}
:root[data-theme="light"] .graph-row.merge-row .commit-body {
background: rgba(248,244,252,0.82);
}
:root[data-theme="light"] .graph-row.tip-row .commit-body {
background:
linear-gradient(90deg, rgba(49,95,214,0.075), transparent 34%),
rgba(255,255,255,0.82);
}
:root[data-theme="light"] .commit-summary {
color: var(--color-ink);
}
:root[data-theme="light"] .commit-avatar {
border-color: rgba(49,95,214,0.18);
color: #315fd6;
background: rgba(232,238,248,0.92);
}
:root[data-theme="light"] .commit-kind {
background: rgba(244,247,252,0.82);
}
:root[data-theme="light"] .commit-hash {
color: #315fd6;
}
:root[data-theme="light"] .commit-branch-chip,
:root[data-theme="light"] .graph-hover-branches span {
border-color: rgba(31,128,76,0.22);
color: #146b3b;
background: rgba(224,246,233,0.92);
box-shadow: 0 8px 22px rgba(28,44,74,0.12);
}
:root[data-theme="light"] .graph-hover-branches span.remote,
:root[data-theme="light"] .branch-filter-option.remote {
border-color: rgba(49,95,214,0.22);
color: #315fd6;
background: rgba(231,237,255,0.92);
}
:root[data-theme="light"] .graph-hover-branches span.ahead {
border-color: rgba(150,98,15,0.28);
color: #96620f;
background: rgba(255,244,224,0.95);
}
:root[data-theme="light"] .graph-hover-branches span.behind {
border-color: rgba(49,95,214,0.26);
color: #315fd6;
background: rgba(231,237,255,0.95);
}
:root[data-theme="light"] .ref-list .ref-chip.head {
color: #ffffff;
background: linear-gradient(135deg, #0f8fb5, #315fd6);
}
:root[data-theme="light"] .ref-list .ref-chip.branch {
color: #16723b;
background: rgba(78,202,118,0.12);
}
:root[data-theme="light"] .ref-list .ref-chip.remote {
color: #315fd6;
background: rgba(49,95,214,0.09);
}
:root[data-theme="light"] .commit-files,
:root[data-theme="light"] .commit-file-button {
background: rgba(248,250,253,0.78);
}
:root[data-theme="light"] .file-history-rail-toggle {
background: linear-gradient(180deg, rgba(49,95,214,0.08), rgba(15,143,181,0.04));
}
:root[data-theme="light"] .file-history-rail-toggle:hover:not(:disabled) {
background: linear-gradient(180deg, rgba(49,95,214,0.12), rgba(15,143,181,0.08));
}
:root[data-theme="light"] .file-history-loading {
background:
radial-gradient(circle at center 42%, rgba(49,95,214,0.1), transparent 52%),
linear-gradient(180deg, rgba(244,247,252,0.88), transparent);
}
:root[data-theme="light"] .file-history-loading-icon {
filter:
drop-shadow(0 10px 14px rgba(28,44,74,0.18))
drop-shadow(0 0 12px rgba(49,95,214,0.18));
}
:root[data-theme="light"] .branch-context-menu {
box-shadow: 0 18px 50px rgba(28,44,74,0.18);
}
:root[data-theme="light"] .dialog-file-row:hover,
:root[data-theme="light"] .branch-row:hover,
:root[data-theme="light"] .explorer-row:hover,
:root[data-theme="light"] .commit-files-toggle:hover:not(:disabled) {
background: var(--color-surface-hover);
}
:root[data-theme="light"] .split-span.split-meta {
background: #e9eef7;
}
:root[data-theme="light"] .resolve-split,
:root[data-theme="light"] .resolve-num,
:root[data-theme="light"] .resolve-cell.empty,
:root[data-theme="light"] .resolve-num.empty {
background: rgba(234,239,248,0.72);
}
:root[data-theme="light"] .diff-line.meta,
:root[data-theme="light"] .resolve-split-marker {
color: #64728a;
}
:root[data-theme="light"] .cred-input input,
:root[data-theme="light"] .cred-expiry input[type="date"] {
background: rgba(255,255,255,0.92);
color-scheme: light;
}
:root[data-theme="light"] .cred-token-hint code {
color: #0d78a0;
}
:root[data-theme="light"] .status-panel-overlay {
background:
radial-gradient(circle at 50% 38%, rgba(49, 95, 214, 0.12), transparent 55%),
rgba(244, 247, 252, 0.68);
}
:root[data-theme="light"] .status-panel-overlay-card {
background:
linear-gradient(180deg, rgba(255,255,255,0.96), rgba(244,247,252,0.94)),
var(--color-surface-raised);
box-shadow: 0 18px 44px rgba(28,44,74,0.16), inset 0 1px 0 rgba(255,255,255,0.9);
}
/* Custom tooltip — appended to body, not clipped by overflow:hidden parents */
.path-tooltip {
position: fixed;
@@ -4476,6 +4963,10 @@
@media (max-width: 740px) {
body { overflow: auto; }
.shell-body { min-height: 100%; gap: 6px; }
.titlebar-info { padding: 0 8px; }
.titlebar-actions .tb-action:not(.auto-toggle):not(:last-child) { display: none; }
.tb-action { width: 36px; padding: 0 8px; }
.tb-btn { width: 34px; }
.workspace { grid-template-columns: 1fr; gap: 6px; }
.left-sidebar-resize-handle { display: none; }
.history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(200px, 1fr) minmax(150px, 0.5fr); min-height: 380px; }