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:
+47
-2
@@ -98,6 +98,7 @@
|
||||
|
||||
import type {
|
||||
AiSettings,
|
||||
AppTheme,
|
||||
AnalyticsSettings,
|
||||
CommitAiPhase,
|
||||
ConflictFile,
|
||||
@@ -165,6 +166,7 @@
|
||||
const REPO_STATUS_CACHE_KEY = "gitlite.repoStatusCache.v1";
|
||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
|
||||
const APP_THEME_KEY = "gitlite.theme.v1";
|
||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
||||
const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1";
|
||||
@@ -239,6 +241,7 @@
|
||||
let appSettingsOpen = false;
|
||||
let analyticsNoticeOpen = false;
|
||||
let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings();
|
||||
let appTheme: AppTheme = loadThemePreference();
|
||||
let localModelOptions: LocalModelOption[] = [];
|
||||
let errorMessage = "";
|
||||
let operation = "";
|
||||
@@ -337,6 +340,7 @@
|
||||
let fileHistoryResizeStartX = 0;
|
||||
let fileHistoryResizeStartWidth = 0;
|
||||
let fileHistoryCollapsed = true;
|
||||
let themeMediaQuery: MediaQueryList | undefined;
|
||||
|
||||
// ── Derived ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -389,13 +393,18 @@
|
||||
$: leftSidebarRows = buildLeftSidebarRows(branchPanelCollapsed, stashPanelCollapsed, explorerPanelCollapsed);
|
||||
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
||||
|
||||
$: applyThemePreference(appTheme);
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||
|
||||
onMount(() => {
|
||||
themeMediaQuery = window.matchMedia("(prefers-color-scheme: light)");
|
||||
themeMediaQuery.addEventListener("change", handleSystemThemeChange);
|
||||
void runStartupSequence();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
themeMediaQuery?.removeEventListener("change", handleSystemThemeChange);
|
||||
if (autoRefreshTimer) clearInterval(autoRefreshTimer);
|
||||
if (backgroundRepoStatusTimer) clearInterval(backgroundRepoStatusTimer);
|
||||
if (backgroundFetchTimer) clearInterval(backgroundFetchTimer);
|
||||
@@ -754,11 +763,46 @@
|
||||
trackEvent("app_started", { first_run: 1 });
|
||||
}
|
||||
|
||||
function saveAppSettings(next: AnalyticsSettings) {
|
||||
function loadThemePreference(): AppTheme {
|
||||
try {
|
||||
const stored = localStorage.getItem(APP_THEME_KEY);
|
||||
if (stored === "system" || stored === "light" || stored === "dark") return stored;
|
||||
} catch {
|
||||
// Local storage is optional; system theme is a sane default.
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
|
||||
function persistThemePreference(next: AppTheme) {
|
||||
try {
|
||||
localStorage.setItem(APP_THEME_KEY, next);
|
||||
} catch {
|
||||
// Ignore storage quota/private-mode errors.
|
||||
}
|
||||
}
|
||||
|
||||
function applyThemePreference(next: AppTheme) {
|
||||
const prefersLight = themeMediaQuery?.matches ?? window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||
const resolved = next === "system"
|
||||
? prefersLight
|
||||
? "light"
|
||||
: "dark"
|
||||
: next;
|
||||
document.documentElement.dataset.themePreference = next;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
}
|
||||
|
||||
function handleSystemThemeChange() {
|
||||
if (appTheme === "system") applyThemePreference(appTheme);
|
||||
}
|
||||
|
||||
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme) {
|
||||
analyticsSettings = next;
|
||||
appTheme = nextTheme;
|
||||
persistAnalyticsSettings(next);
|
||||
persistThemePreference(nextTheme);
|
||||
appSettingsOpen = false;
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1 });
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme });
|
||||
}
|
||||
|
||||
function updateCommitMessage(message: string) {
|
||||
@@ -3932,6 +3976,7 @@
|
||||
{#if appSettingsOpen}
|
||||
<AppSettingsDialog
|
||||
analytics={analyticsSettings}
|
||||
theme={appTheme}
|
||||
onSave={saveAppSettings}
|
||||
onClose={() => { appSettingsOpen = false; }}
|
||||
/>
|
||||
|
||||
+512
-21
@@ -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; }
|
||||
|
||||
+30
-8
@@ -24,22 +24,44 @@
|
||||
export let onToggleAutoRefresh: () => void = () => {};
|
||||
export let onOpenSettings: () => void = () => {};
|
||||
|
||||
const win = getCurrentWindow();
|
||||
let win: ReturnType<typeof getCurrentWindow> | null = null;
|
||||
let isMaximized = false;
|
||||
let unlisten: (() => void) | undefined;
|
||||
let appVersion = "";
|
||||
|
||||
onMount(async () => {
|
||||
isMaximized = await win.isMaximized();
|
||||
unlisten = await win.onResized(async () => {
|
||||
try {
|
||||
win = getCurrentWindow();
|
||||
isMaximized = await win.isMaximized();
|
||||
});
|
||||
appVersion = await getVersion();
|
||||
unlisten = await win.onResized(async () => {
|
||||
isMaximized = await win?.isMaximized() ?? false;
|
||||
});
|
||||
} catch {
|
||||
win = null;
|
||||
}
|
||||
|
||||
try {
|
||||
appVersion = await getVersion();
|
||||
} catch {
|
||||
appVersion = "";
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unlisten?.();
|
||||
});
|
||||
|
||||
function minimizeWindow() {
|
||||
void win?.minimize();
|
||||
}
|
||||
|
||||
function toggleMaximizeWindow() {
|
||||
void win?.toggleMaximize();
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
void win?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="titlebar" data-tauri-drag-region>
|
||||
@@ -200,12 +222,12 @@
|
||||
<div class="titlebar-divider" aria-hidden="true"></div>
|
||||
|
||||
<div class="titlebar-controls">
|
||||
<button class="tb-btn" onclick={() => win.minimize()} title="Minimize" aria-label="Minimize">
|
||||
<button class="tb-btn" onclick={minimizeWindow} title="Minimize" aria-label="Minimize">
|
||||
<Minus size={12} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="tb-btn"
|
||||
onclick={() => win.toggleMaximize()}
|
||||
onclick={toggleMaximizeWindow}
|
||||
title={isMaximized ? "Restore" : "Maximize"}
|
||||
aria-label={isMaximized ? "Restore" : "Maximize"}
|
||||
>
|
||||
@@ -220,7 +242,7 @@
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button class="tb-btn close" onclick={() => win.close()} title="Close" aria-label="Close">
|
||||
<button class="tb-btn close" onclick={closeWindow} title="Close" aria-label="Close">
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { Check, Settings, X } from "@lucide/svelte";
|
||||
import type { AnalyticsSettings } from "../types";
|
||||
import type { AnalyticsSettings, AppTheme } from "../types";
|
||||
|
||||
interface Props {
|
||||
analytics: AnalyticsSettings;
|
||||
onSave: (settings: AnalyticsSettings) => void;
|
||||
theme: AppTheme;
|
||||
onSave: (settings: AnalyticsSettings, theme: AppTheme) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { analytics, onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||
let { analytics, theme = "system", onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||
|
||||
let analyticsEnabled = $state(true);
|
||||
let selectedTheme = $state<AppTheme>("system");
|
||||
|
||||
$effect(() => {
|
||||
analyticsEnabled = analytics.enabled;
|
||||
selectedTheme = theme;
|
||||
});
|
||||
|
||||
function save() {
|
||||
@@ -21,7 +24,7 @@
|
||||
...analytics,
|
||||
enabled: analyticsEnabled,
|
||||
noticeSeen: true,
|
||||
});
|
||||
}, selectedTheme);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -38,6 +41,31 @@
|
||||
</header>
|
||||
|
||||
<form class="app-settings-form" onsubmit={(event) => { event.preventDefault(); save(); }}>
|
||||
<section class="settings-section">
|
||||
<header>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">Appearance</span>
|
||||
<h3>Theme</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="settings-segmented" role="radiogroup" aria-label="Theme">
|
||||
<label class:active={selectedTheme === "system"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="system" />
|
||||
<span>System</span>
|
||||
</label>
|
||||
<label class:active={selectedTheme === "light"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="light" />
|
||||
<span>Light</span>
|
||||
</label>
|
||||
<label class:active={selectedTheme === "dark"}>
|
||||
<input type="radio" bind:group={selectedTheme} value="dark" />
|
||||
<span>Dark</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<header>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
|
||||
@@ -187,6 +187,47 @@
|
||||
animation: bar-slide 1.35s ease-in-out infinite;
|
||||
}
|
||||
|
||||
:global(:root[data-theme="light"]) .repo-loading {
|
||||
background:
|
||||
radial-gradient(circle at 50% 42%, rgba(49, 95, 214, 0.12), transparent 34%),
|
||||
linear-gradient(135deg, #f7f9fd 0%, #eef3f9 48%, #f8fafc 100%);
|
||||
}
|
||||
|
||||
:global(:root[data-theme="light"]) .repo-loading-grid {
|
||||
opacity: 0.38;
|
||||
background-image:
|
||||
linear-gradient(rgba(49, 95, 214, 0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(15, 143, 181, 0.08) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
:global(:root[data-theme="light"]) .repo-loading-card {
|
||||
border-color: rgba(61, 89, 142, 0.2);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,255,255,0.96), rgba(244,247,252,0.94)),
|
||||
var(--color-surface-raised);
|
||||
box-shadow:
|
||||
0 26px 72px rgba(28,44,74,0.18),
|
||||
inset 0 1px 0 rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
:global(:root[data-theme="light"]) .repo-loading-traces .trace {
|
||||
filter: drop-shadow(0 0 8px rgba(49,95,214,0.22));
|
||||
}
|
||||
|
||||
:global(:root[data-theme="light"]) .repo-loading-traces .trace-cut {
|
||||
stroke: rgba(49,95,214,0.34);
|
||||
}
|
||||
|
||||
:global(:root[data-theme="light"]) .repo-loading-icon {
|
||||
filter:
|
||||
drop-shadow(0 18px 24px rgba(28,44,74,0.2))
|
||||
drop-shadow(0 0 18px rgba(49,95,214,0.18));
|
||||
}
|
||||
|
||||
:global(:root[data-theme="light"]) .repo-loading-bar {
|
||||
background: rgba(49,95,214,0.12);
|
||||
}
|
||||
|
||||
@keyframes overlay-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes grid-drift { to { transform: translate3d(42px, 42px, 0); } }
|
||||
@keyframes icon-float {
|
||||
|
||||
@@ -10,6 +10,7 @@ export type FileStatusKind =
|
||||
export type CommitAiPhase = "idle" | "loading" | "ready" | "error";
|
||||
export type CommitAiProvider = "local" | "openai" | "anthropic" | "custom";
|
||||
export type CommitAiLocalProfile = "fast" | "balanced" | "detailed";
|
||||
export type AppTheme = "system" | "light" | "dark";
|
||||
|
||||
export interface CommitAiStatus {
|
||||
phase: CommitAiPhase;
|
||||
|
||||
+17
@@ -3,12 +3,29 @@ import { mount } from "svelte";
|
||||
import App from "./App.svelte";
|
||||
import "./app.css";
|
||||
|
||||
const THEME_KEY = "gitlite.theme.v1";
|
||||
const target = document.getElementById("app");
|
||||
|
||||
if (!target) {
|
||||
throw new Error("App target element was not found.");
|
||||
}
|
||||
|
||||
try {
|
||||
const storedTheme = localStorage.getItem(THEME_KEY);
|
||||
const preference = storedTheme === "light" || storedTheme === "dark" || storedTheme === "system"
|
||||
? storedTheme
|
||||
: "system";
|
||||
const resolved = preference === "system" && window.matchMedia("(prefers-color-scheme: light)").matches
|
||||
? "light"
|
||||
: preference === "system"
|
||||
? "dark"
|
||||
: preference;
|
||||
document.documentElement.dataset.themePreference = preference;
|
||||
document.documentElement.dataset.theme = resolved;
|
||||
} catch {
|
||||
// Storage is best-effort; the CSS system theme fallback still applies.
|
||||
}
|
||||
|
||||
const app = mount(App, { target });
|
||||
|
||||
export default app;
|
||||
|
||||
Reference in New Issue
Block a user