diff --git a/src/App.svelte b/src/App.svelte index 762c0a0..d110032 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -146,10 +146,12 @@ AiReviewResult, AiCommitPlan, AiSettings, + AppAppearance, AppLanguage, AppTheme, AnalyticsSettings, CommitAiPhase, + CustomThemeColors, ConflictFile, DetectedExternalTool, ExplorerNode, @@ -253,6 +255,8 @@ const AI_SETTINGS_KEY = "gitlite.aiSettings.v1"; const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1"; const APP_THEME_KEY = "gitlite.theme.v1"; + const APP_APPEARANCE_KEY = "gitlite.appearance.v1"; + const CUSTOM_THEME_KEY = "gitlite.customTheme.v1"; const APP_LANGUAGE_KEY = "gitlite.language.v1"; const EXTERNAL_TOOLS_SETTINGS_KEY = "gitlite.externalTools.v1"; const AUTO_REFRESH_ENABLED_KEY = "gitlite.autoRefreshEnabled.v1"; @@ -364,6 +368,8 @@ let analyticsNoticeOpen = false; let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings(); let appTheme: AppTheme = loadThemePreference(); + let appAppearance: AppAppearance = loadAppearancePreference(); + let customTheme: CustomThemeColors = loadCustomTheme(); let appLanguage: AppLanguage = loadLanguagePreference(); let externalToolsSettings: ExternalToolsSettings = loadExternalToolsSettings(); let externalToolsConfigured = hasStoredExternalToolsSettings(); @@ -555,6 +561,7 @@ $: fileManagerToolName = externalToolDisplayName("fileManager", externalToolsSettings.fileManager, detectedExternalTools); $: applyThemePreference(appTheme); + $: applyAppearancePreference(appAppearance, customTheme); $: applyLanguagePreference(appLanguage); // ── Lifecycle ────────────────────────────────────────────────────────────── @@ -1119,6 +1126,98 @@ } } + function defaultCustomTheme(): CustomThemeColors { + return { + background: "#cfd5dc", + surface: "#e2e6ea", + accent: "#2eb5d1", + text: "#222328", + }; + } + + function isHexColor(value: unknown): value is string { + return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value); + } + + function loadAppearancePreference(): AppAppearance { + try { + const stored = localStorage.getItem(APP_APPEARANCE_KEY); + if (stored === "modern" || stored === "classic" || stored === "custom") return stored; + } catch { + // Local storage is optional; the current design remains the default. + } + return "modern"; + } + + function loadCustomTheme(): CustomThemeColors { + const fallback = defaultCustomTheme(); + try { + const stored = JSON.parse(localStorage.getItem(CUSTOM_THEME_KEY) ?? "null") as Partial | null; + if (!stored) return fallback; + return { + background: isHexColor(stored.background) ? stored.background : fallback.background, + surface: isHexColor(stored.surface) ? stored.surface : fallback.surface, + accent: isHexColor(stored.accent) ? stored.accent : fallback.accent, + text: isHexColor(stored.text) ? stored.text : fallback.text, + }; + } catch { + return fallback; + } + } + + function persistAppearancePreference(next: AppAppearance, colors: CustomThemeColors) { + try { + localStorage.setItem(APP_APPEARANCE_KEY, next); + localStorage.setItem(CUSTOM_THEME_KEY, JSON.stringify(colors)); + } catch { + // Ignore storage quota/private-mode errors. + } + } + + function applyAppearancePreference(next: AppAppearance, colors: CustomThemeColors) { + const root = document.documentElement; + root.dataset.appearance = next; + const customProperties = [ + "--app-bg", "--app-button-bg", "--app-input-bg", "--app-dialog-bg", "--app-dialog-chrome", + "--app-settings-row-bg", "--color-surface", "--color-surface-alt", "--color-surface-dim", + "--color-surface-hover", "--color-surface-raised", "--color-surface-solid", "--color-border", + "--color-border-subtle", "--color-border-input", "--color-primary", "--color-primary-dark", + "--color-accent", "--color-ink", "--color-ink-muted", "--color-ink-faint", "--color-ink-dim", + "--color-bar-text", "--color-bar-muted", + ]; + customProperties.forEach((property) => root.style.removeProperty(property)); + if (next !== "custom") return; + + const { background, surface, accent, text } = colors; + const values: Record = { + "--app-bg": background, + "--app-button-bg": surface, + "--app-input-bg": `color-mix(in srgb, ${surface} 88%, white)`, + "--app-dialog-bg": surface, + "--app-dialog-chrome": `color-mix(in srgb, ${surface} 90%, ${background})`, + "--app-settings-row-bg": `color-mix(in srgb, ${surface} 92%, ${background})`, + "--color-surface": surface, + "--color-surface-alt": `color-mix(in srgb, ${surface} 82%, ${background})`, + "--color-surface-dim": `color-mix(in srgb, ${surface} 88%, ${background})`, + "--color-surface-hover": `color-mix(in srgb, ${surface} 88%, ${text})`, + "--color-surface-raised": `color-mix(in srgb, ${surface} 92%, white)`, + "--color-surface-solid": surface, + "--color-border": `color-mix(in srgb, ${text} 32%, ${surface})`, + "--color-border-subtle": `color-mix(in srgb, ${text} 18%, ${surface})`, + "--color-border-input": `color-mix(in srgb, ${text} 38%, ${surface})`, + "--color-primary": accent, + "--color-primary-dark": `color-mix(in srgb, ${accent} 82%, black)`, + "--color-accent": accent, + "--color-ink": text, + "--color-ink-muted": `color-mix(in srgb, ${text} 76%, ${surface})`, + "--color-ink-faint": `color-mix(in srgb, ${text} 58%, ${surface})`, + "--color-ink-dim": `color-mix(in srgb, ${text} 68%, ${surface})`, + "--color-bar-text": text, + "--color-bar-muted": `color-mix(in srgb, ${text} 62%, ${surface})`, + }; + Object.entries(values).forEach(([property, value]) => root.style.setProperty(property, value)); + } + function loadLanguagePreference(): AppLanguage { try { const stored = localStorage.getItem(APP_LANGUAGE_KEY); @@ -1157,15 +1256,18 @@ if (appTheme === "system") applyThemePreference(appTheme); } - function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextLanguage: AppLanguage, nextAutoRefresh: boolean, nextExternalTools: ExternalToolsSettings) { + function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextAppearance: AppAppearance, nextCustomTheme: CustomThemeColors, nextLanguage: AppLanguage, nextAutoRefresh: boolean, nextExternalTools: ExternalToolsSettings) { const autoRefreshWasEnabled = autoRefreshEnabled; analyticsSettings = next; appTheme = nextTheme; + appAppearance = nextAppearance; + customTheme = nextCustomTheme; appLanguage = nextLanguage; autoRefreshEnabled = nextAutoRefresh; externalToolsSettings = nextExternalTools; persistAnalyticsSettings(next); persistThemePreference(nextTheme); + persistAppearancePreference(nextAppearance, nextCustomTheme); persistLanguagePreference(nextLanguage); persistStoredBoolean(AUTO_REFRESH_ENABLED_KEY, nextAutoRefresh); persistExternalToolsSettings(nextExternalTools); @@ -1173,7 +1275,7 @@ setTelemetryEnabled(next.enabled); appSettingsOpen = false; if (nextAutoRefresh && !autoRefreshWasEnabled) void autoRefreshTick(); - if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage, auto_refresh: nextAutoRefresh ? 1 : 0 }); + if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, appearance: nextAppearance, language: nextLanguage, auto_refresh: nextAutoRefresh ? 1 : 0 }); } function updateCommitMessage(message: string) { @@ -5599,6 +5701,8 @@ header { background: var(--color-surface-dim); } .repo-row-main:hover:not(:disabled) { background: var(--color-surface-hover); } +/* Dark control contrast -------------------------------------------------- + Keep GitKraken's restrained graphite palette while making interactive + boundaries and text states unambiguous on every dark surface. */ +:root:not([data-theme="light"]) .btn-primary { + border-color: #2eb5d1; + color: #ffffff; + background: #126f84; +} +:root:not([data-theme="light"]) .btn-primary:hover:not(:disabled) { + border-color: #55c5dc; + color: #ffffff; + background: #147c94; +} +:root:not([data-theme="light"]) .btn-secondary { + border-color: #596169; + color: #d7dadc; + background: #32363d; +} +:root:not([data-theme="light"]) .btn-secondary:hover:not(:disabled) { + border-color: #707982; + color: #ffffff; + background: #41464d; +} +:root:not([data-theme="light"]) input, +:root:not([data-theme="light"]) textarea, +:root:not([data-theme="light"]) select, +:root:not([data-theme="light"]) .select-menu-trigger { + border-color: #596169; +} +:root:not([data-theme="light"]) input:hover:not(:disabled), +:root:not([data-theme="light"]) textarea:hover:not(:disabled), +:root:not([data-theme="light"]) select:hover:not(:disabled), +:root:not([data-theme="light"]) .select-menu-trigger:hover:not(:disabled) { + border-color: #707982; +} +:root:not([data-theme="light"]) input:focus, +:root:not([data-theme="light"]) textarea:focus, +:root:not([data-theme="light"]) select:focus, +:root:not([data-theme="light"]) .select-menu.open .select-menu-trigger { + border-color: var(--color-accent); +} +:root:not([data-theme="light"]) .repo-action-group, +:root:not([data-theme="light"]) .dialog-icon-button { + border-color: #535a62; +} +:root:not([data-theme="light"]) .repo-action:hover:not(:disabled), +:root:not([data-theme="light"]) .repo-action.active, +:root:not([data-theme="light"]) .dialog-icon-button:hover:not(:disabled) { + border-color: #707982; + background: #3b4047; +} + +/* Dark app chrome separation ------------------------------------------- + Tabs, command toolbar and workspace should read as three adjacent + layers instead of one uninterrupted gray strip. */ +:root:not([data-theme="light"]) .repo-tabbar { + border-bottom-color: #505760; + background: #26292f; + box-shadow: + inset 0 -1px 0 #191b1f, + 0 1px 0 rgba(255, 255, 255, 0.035); +} +:root:not([data-theme="light"]) .repo-tabs-scroll { + border-left-color: #454b53; +} +:root:not([data-theme="light"]) .repo-tab-wrap { + border-right-color: #454b53; + background: #292c32; +} +:root:not([data-theme="light"]) .repo-tab-wrap:hover { + background: #30343a; +} +:root:not([data-theme="light"]) .repo-tab-wrap.active, +:root:not([data-theme="light"]) .repo-tab.management.active { + background: #343941; + box-shadow: + inset 0 -3px 0 var(--color-accent), + inset 1px 0 0 #4c535b, + inset -1px 0 0 #4c535b, + inset 0 1px 0 rgba(255, 255, 255, 0.045); +} +:root:not([data-theme="light"]) .repo-tab-add { + border-left-color: #454b53; + background: #292c32; +} +:root:not([data-theme="light"]) .repo-toolbar { + border-top: 1px solid #424850; + border-bottom-color: #505760; + background: #30343a; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.035), + 0 2px 0 #191b1f; +} +:root:not([data-theme="light"]) .repo-action-group { + border-color: #596169; + background: #292d32; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035); +} +:root:not([data-theme="light"]) .repo-action-group > .repo-action, +:root:not([data-theme="light"]) .repo-action-group > .repo-history-wrap > .repo-action { + border-right-color: #484e55; +} +:root:not([data-theme="light"]) .repo-toolbar-divider { + background: #555c64; +} +:root:not([data-theme="light"]) .workspace { + box-shadow: inset 0 1px 0 #17191c; +} + +/* Light app chrome separation ------------------------------------------ + Keep the same layered hierarchy as the dark theme with restrained, + cool-gray borders and a clean white active surface. */ +:root[data-theme="light"] .repo-tabbar { + border-bottom-color: #aeb8c5; + background: #e9edf2; + box-shadow: + inset 0 -1px 0 #d2d9e2, + 0 1px 0 rgba(255, 255, 255, 0.9); +} + +:root[data-theme="light"] .repo-tabs-scroll { + border-left-color: #c3cbd5; +} + +:root[data-theme="light"] .repo-tab-wrap { + border-right-color: #c3cbd5; + background: #eef1f5; +} + +:root[data-theme="light"] .repo-tab-wrap:hover { + background: #e3e8ee; +} + +:root[data-theme="light"] .repo-tab-wrap.active, +:root[data-theme="light"] .repo-tab.management.active { + background: #ffffff; + box-shadow: + inset 0 -3px 0 var(--color-primary), + inset 1px 0 0 #b7c1cd, + inset -1px 0 0 #b7c1cd, + inset 0 1px 0 rgba(255, 255, 255, 0.95); +} + +:root[data-theme="light"] .repo-tab-add { + border-left-color: #c3cbd5; + background: #eef1f5; +} + +:root[data-theme="light"] .repo-toolbar { + border-top: 1px solid #c4ccd6; + border-bottom-color: #aeb8c5; + background: #f4f6f8; + box-shadow: + inset 0 1px 0 #ffffff, + 0 2px 0 #d6dce4; +} + +:root[data-theme="light"] .repo-action-group { + border-color: #aeb8c5; + background: #ffffff; + box-shadow: + inset 0 1px 0 #ffffff, + 0 1px 2px rgba(34, 49, 78, 0.06); +} + +:root[data-theme="light"] .repo-action-group > .repo-action, +:root[data-theme="light"] .repo-action-group > .repo-history-wrap > .repo-action { + border-right-color: #c8d0da; +} + +:root[data-theme="light"] .repo-toolbar-divider { + background: #b5bec9; +} + +:root[data-theme="light"] .workspace { + box-shadow: inset 0 1px 0 #c9d1db; +} + @media (max-width: 1180px) { .repo-action { padding-inline: 10px; } .repo-toolbar-divider { margin-inline: 2px; } @@ -7872,3 +8052,474 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s .status-lane-head { align-items: flex-start; flex-direction: column; } .status-lane-actions { width: 100%; justify-content: flex-start; } } + +/* Muted light theme ----------------------------------------------------- + A lower-luminance light palette for long sessions. The hierarchy stays + light and readable without using pure white for every working surface. */ +:root[data-theme="light"] { + --ui-shadow: 0 10px 26px rgba(29, 39, 52, 0.12); + --app-bg: #cfd5dc; + --app-button-bg: #e2e7eb; + --app-input-bg: #e9edf0; + --app-select-option-bg: #e7ebef; + --app-dialog-backdrop: rgba(91, 103, 117, 0.38); + --app-dialog-bg: #e2e6ea; + --app-dialog-chrome: #d7dde3; + --app-settings-row-bg: #dce2e7; + --app-scrollbar-thumb: #aab4be; + --app-scrollbar-thumb-hover: #929eaa; + + --color-ink: #202832; + --color-ink-muted: #4d5a68; + --color-ink-faint: #687583; + --color-ink-dim: #596674; + --color-ink-quiet: #62707e; + --color-surface: #e2e6ea; + --color-surface-alt: #d8dde3; + --color-surface-dim: #dce1e6; + --color-surface-hover: #cfd6de; + --color-surface-raised: #e7ebee; + --color-surface-solid: #e1e5e9; + --color-border: #9faab5; + --color-border-subtle: #bbc3cc; + --color-border-input: #98a5b2; + --color-bar: #d6dce2; + --color-bar-text: #202832; + --color-bar-muted: #596674; + + --code-surface: #e1e5e9; + --code-surface-raised: #d9dfe5; + --code-surface-subtle: #dce2e7; + --code-surface-muted: #d4dae0; + --code-surface-meta: #ced5dc; + --code-input-bg: #e8ecef; + --code-hover-bg: #cfd7df; +} + +:root[data-theme="light"] .titlebar { + background: + linear-gradient(90deg, rgba(49, 95, 214, 0.035), rgba(15, 143, 181, 0.025)), + var(--color-bar); +} + +:root[data-theme="light"] .repo-tabbar { + border-bottom-color: #929da8; + background: #cbd2d9; + box-shadow: inset 0 -1px 0 #aeb7c0; +} + +:root[data-theme="light"] .repo-tab-wrap, +:root[data-theme="light"] .repo-tab-add { + border-color: #aeb8c2; + background: #d3d9df; +} + +:root[data-theme="light"] .repo-tab-wrap:hover, +:root[data-theme="light"] .repo-tab.management:hover:not(:disabled) { + background: #c8d0d8; +} + +:root[data-theme="light"] .repo-tab-wrap.active, +:root[data-theme="light"] .repo-tab.management.active { + background: #e3e7eb; + box-shadow: + inset 0 -3px 0 var(--color-primary), + inset 1px 0 0 #9faab5, + inset -1px 0 0 #9faab5; +} + +:root[data-theme="light"] .repo-toolbar { + border-top-color: #aeb7c0; + border-bottom-color: #929da8; + background: #d5dbe1; + box-shadow: 0 2px 0 #b9c1c9; +} + +:root[data-theme="light"] .repo-action-group { + border-color: #98a4af; + background: #e0e5e9; + box-shadow: 0 1px 2px rgba(29, 39, 52, 0.1); +} + +:root[data-theme="light"] .repo-action-group > .repo-action, +:root[data-theme="light"] .repo-action-group > .repo-history-wrap > .repo-action { + border-right-color: #aeb8c2; +} + +:root[data-theme="light"] .workspace { + box-shadow: inset 0 1px 0 #aab4be; +} + +:root[data-theme="light"] .repo-management, +:root[data-theme="light"] .main-panel { + background: var(--color-surface); +} + +:root[data-theme="light"] .repo-section { + background: #dbe0e5; +} + +:root[data-theme="light"] .repo-section > header, +: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: #d2d8de; +} + +:root[data-theme="light"] .repo-empty, +:root[data-theme="light"] .graph-list, +:root[data-theme="light"] .commit-body, +:root[data-theme="light"] .commit-files, +:root[data-theme="light"] .commit-file-button, +:root[data-theme="light"] .commit-ref-detail-item, +:root[data-theme="light"] .cred-card, +:root[data-theme="light"] .cred-body { + background: #e0e4e8; +} + +:root[data-theme="light"] .graph-gutter { + background: #d2d8de; +} + +:root[data-theme="light"] .graph-row:hover .commit-body { + background: #d3dae1; +} + +:root[data-theme="light"] .branch-filter-dialog, +:root[data-theme="light"] .status-panel-overlay-card { + background: #e2e6ea; +} + +:root[data-theme="light"] .cred-input input, +:root[data-theme="light"] .cred-expiry input[type="date"] { + background: var(--app-input-bg); +} + +/* Shared dark-mode accent language in the muted light theme. */ +:root[data-theme="light"] { + --color-primary: #25a7c7; + --color-primary-dark: #188ca9; + --color-accent: #2eb5d1; + --color-ink: #222328; + --color-ink-muted: #3f4852; + --color-ink-faint: #66707a; + --color-ink-dim: #515b65; + --color-ink-quiet: #5c6670; + --color-bar-text: #222328; + --color-bar-muted: #59636d; +} + +:root[data-theme="light"] .btn-primary { + border-color: #2eb5d1; + color: #f0f1f2; + background: #126f84; + box-shadow: none; +} + +:root[data-theme="light"] .btn-primary:hover:not(:disabled) { + border-color: #55c5dc; + color: #ffffff; + background: #147c94; +} + +:root[data-theme="light"] .btn-secondary, +:root[data-theme="light"] .dialog-icon-button { + border-color: #909ca8; + color: #2f3943; + background: #d2d8de; +} + +:root[data-theme="light"] .btn-secondary:hover:not(:disabled), +:root[data-theme="light"] .dialog-icon-button:hover:not(:disabled) { + border-color: #778591; + color: #222328; + background: #c3cbd3; +} + +:root[data-theme="light"] .repo-action-group { + border-color: #909ca8; + background: #d2d8de; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.32); +} + +:root[data-theme="light"] .repo-action { + color: #35404a; +} + +:root[data-theme="light"] .repo-action-group > .repo-action, +:root[data-theme="light"] .repo-action-group > .repo-history-wrap > .repo-action { + border-right-color: #aab4be; +} + +:root[data-theme="light"] .repo-action:hover:not(:disabled), +:root[data-theme="light"] .repo-action.active { + color: #222328; + background: #c3cbd3; +} + +:root[data-theme="light"] .repo-action.sync-primary, +:root[data-theme="light"] .repo-action.sync-primary svg { + color: #2eb5d1; +} + +/* Repository tabs stay light while sharing the cyan dark-theme accent. */ +:root[data-theme="light"] .repo-tabbar { + border-bottom-color: #929da8; + background: #cbd2d9; + box-shadow: inset 0 -1px 0 #aeb7c0; +} + +:root[data-theme="light"] .repo-tabs-scroll { + border-left-color: #aeb8c2; +} + +:root[data-theme="light"] .repo-tab-wrap, +:root[data-theme="light"] .repo-tab.management, +:root[data-theme="light"] .repo-tab-add { + border-color: #aeb8c2; + color: #3f4852; + background: #d3d9df; +} + +:root[data-theme="light"] .repo-tab-wrap:hover, +:root[data-theme="light"] .repo-tab.management:hover:not(:disabled), +:root[data-theme="light"] .repo-tab-add:hover:not(:disabled) { + color: #222328; + background: #c5ced6; +} + +:root[data-theme="light"] .repo-tab-wrap.active, +:root[data-theme="light"] .repo-tab.management.active { + color: #222328; + background: #e3e7eb; + box-shadow: + inset 0 -3px 0 var(--color-accent), + inset 1px 0 0 #9faab5, + inset -1px 0 0 #9faab5; +} + +:root[data-theme="light"] .repo-tab > svg, +:root[data-theme="light"] .repo-tab-close, +:root[data-theme="light"] .repo-tab-add { + color: #66707a; +} + +:root[data-theme="light"] .repo-tab-wrap.active .repo-tab, +:root[data-theme="light"] .repo-tab-wrap.active .repo-tab > svg, +:root[data-theme="light"] .repo-tab.management.active > svg, +:root[data-theme="light"] .repo-tab-add:hover:not(:disabled) { + color: #2eb5d1; +} + +:root[data-theme="light"] .repo-tab-wrap.active .repo-tab span, +:root[data-theme="light"] .repo-tab.management.active span { + color: #222328; +} + +:root[data-theme="light"] .repo-tab-close:hover:not(:disabled), +:root[data-theme="light"] .repo-tab-close:focus-visible:not(:disabled) { + color: #222328; + background: #c3cbd3; +} + +/* Appearance presets --------------------------------------------------- */ +:root[data-theme="light"][data-appearance="classic"] { + --ui-shadow: 0 10px 28px rgba(23, 40, 72, 0.09); + --app-bg: #f4f6f9; + --app-button-bg: #ffffff; + --app-input-bg: #ffffff; + --app-dialog-bg: #ffffff; + --app-dialog-chrome: #f4f7fc; + --app-settings-row-bg: #f8fafd; + --color-ink: #172033; + --color-ink-muted: #475569; + --color-ink-faint: #728098; + --color-ink-dim: #5f6f89; + --color-surface: #ffffff; + --color-surface-alt: #f7f8fa; + --color-surface-dim: #f8f9fb; + --color-surface-hover: #f0f4fa; + --color-surface-raised: #ffffff; + --color-surface-solid: #ffffff; + --color-border: #cfd6e1; + --color-border-subtle: #e3e7ed; + --color-border-input: #c9d1dd; + --color-primary: #0b63e6; + --color-primary-dark: #0755c8; + --color-accent: #0b63e6; +} + +:root[data-theme="light"][data-appearance="classic"] .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"][data-appearance="classic"] .repo-tabbar, +:root[data-theme="light"][data-appearance="classic"] .repo-management-tools { + border-color: var(--color-border); + background: rgba(247, 249, 252, 0.96); + box-shadow: none; +} + +:root[data-theme="light"][data-appearance="classic"] .repo-tab-wrap, +:root[data-theme="light"][data-appearance="classic"] .repo-tab.management, +:root[data-theme="light"][data-appearance="classic"] .repo-tab-add { + border-color: var(--color-border-subtle); + color: var(--color-ink-muted); + background: rgba(255, 255, 255, 0.5); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-tab-wrap:hover, +:root[data-theme="light"][data-appearance="classic"] .repo-tab.management:hover:not(:disabled), +:root[data-theme="light"][data-appearance="classic"] .repo-tab-add:hover:not(:disabled) { + color: var(--color-ink); + background: rgba(255, 255, 255, 0.9); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-tab-wrap.active, +:root[data-theme="light"][data-appearance="classic"] .repo-tab.management.active { + color: var(--color-ink); + background: #ffffff; + box-shadow: inset 0 -3px 0 var(--color-primary); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-tab-wrap.active .repo-tab span, +:root[data-theme="light"][data-appearance="classic"] .repo-tab.management.active span { + color: var(--color-ink); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-toolbar { + border-color: var(--color-border); + background: rgba(255, 255, 255, 0.78); + box-shadow: none; +} + +:root[data-theme="light"][data-appearance="classic"] .repo-action-group { + border-color: var(--color-border); + background: var(--color-surface-raised); + box-shadow: none; +} + +:root[data-theme="light"][data-appearance="classic"] .repo-action { + color: var(--color-ink-muted); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-action:hover:not(:disabled), +:root[data-theme="light"][data-appearance="classic"] .repo-action.active { + color: var(--color-ink); + background: var(--color-surface-hover); +} + +:root[data-theme="light"][data-appearance="classic"] .btn-primary { + border-color: rgba(49, 95, 214, 0.72); + color: #ffffff; + background: linear-gradient(135deg, #315fd6 0%, #0f8fb5 100%); + box-shadow: 0 10px 24px rgba(49, 95, 214, 0.18); +} + +:root[data-theme="light"][data-appearance="classic"] .btn-secondary, +:root[data-theme="light"][data-appearance="classic"] .dialog-icon-button { + 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"][data-appearance="classic"] .repo-management, +:root[data-theme="light"][data-appearance="classic"] .main-panel, +:root[data-theme="light"][data-appearance="classic"] .cred-card, +:root[data-theme="light"][data-appearance="classic"] .cred-body { + background: var(--color-surface); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-section { + background: rgba(255, 255, 255, 0.62); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-section > header, +:root[data-theme="light"][data-appearance="classic"] .repo-summary, +:root[data-theme="light"][data-appearance="classic"] .lane-header, +:root[data-theme="light"][data-appearance="classic"] .stash-toolbar, +:root[data-theme="light"][data-appearance="classic"] .dialog-footer, +:root[data-theme="light"][data-appearance="classic"] .cred-header { + background: var(--color-surface-dim); +} + +:root[data-theme="light"][data-appearance="classic"] .repo-empty { + background: rgba(255, 255, 255, 0.48); +} + +:root[data-theme="light"][data-appearance="classic"] .graph-list { + background: rgba(248, 250, 253, 0.9); +} + +:root[data-theme="light"][data-appearance="classic"] .commit-body, +:root[data-theme="light"][data-appearance="classic"] .commit-files, +:root[data-theme="light"][data-appearance="classic"] .commit-file-button { + background: rgba(255, 255, 255, 0.72); +} + +:root[data-appearance="custom"] .titlebar, +:root[data-appearance="custom"] .repo-tabbar, +:root[data-appearance="custom"] .repo-toolbar { + border-color: var(--color-border); + background: var(--color-surface-alt); + box-shadow: none; +} + +:root[data-appearance="custom"] .repo-tab-wrap, +:root[data-appearance="custom"] .repo-tab.management, +:root[data-appearance="custom"] .repo-tab-add, +:root[data-appearance="custom"] .repo-action-group, +:root[data-appearance="custom"] .btn-secondary, +:root[data-appearance="custom"] .dialog-icon-button { + border-color: var(--color-border); + color: var(--color-ink-muted); + background: var(--color-surface-raised); +} + +:root[data-appearance="custom"] .repo-tab-wrap.active, +:root[data-appearance="custom"] .repo-tab.management.active { + color: var(--color-ink); + background: var(--color-surface-raised); + box-shadow: inset 0 -3px 0 var(--color-accent); +} + +:root[data-appearance="custom"] .repo-tab-wrap.active .repo-tab span, +:root[data-appearance="custom"] .repo-tab.management.active span { + color: var(--color-ink); +} + +:root[data-appearance="custom"] .btn-primary { + border-color: var(--color-accent); + color: var(--color-surface-raised); + background: var(--color-primary-dark); + box-shadow: none; +} + +:root[data-appearance="custom"] .repo-management, +:root[data-appearance="custom"] .main-panel, +:root[data-appearance="custom"] .repo-section, +:root[data-appearance="custom"] .repo-empty, +:root[data-appearance="custom"] .graph-list, +:root[data-appearance="custom"] .commit-body, +:root[data-appearance="custom"] .commit-files, +:root[data-appearance="custom"] .commit-file-button, +:root[data-appearance="custom"] .cred-card, +:root[data-appearance="custom"] .cred-body, +:root[data-appearance="custom"] .branch-filter-dialog, +:root[data-appearance="custom"] .status-panel-overlay-card { + background: var(--color-surface); +} + +:root[data-appearance="custom"] .repo-section > header, +:root[data-appearance="custom"] .repo-summary, +:root[data-appearance="custom"] .lane-header, +:root[data-appearance="custom"] .stash-toolbar, +:root[data-appearance="custom"] .dialog-footer, +:root[data-appearance="custom"] .cred-header, +:root[data-appearance="custom"] .graph-gutter { + background: var(--color-surface-dim); +} diff --git a/src/lib/components/AppSettingsDialog.svelte b/src/lib/components/AppSettingsDialog.svelte index a448fc2..2a93ef8 100644 --- a/src/lib/components/AppSettingsDialog.svelte +++ b/src/lib/components/AppSettingsDialog.svelte @@ -31,8 +31,10 @@ } from "../externalTools"; import type { AnalyticsSettings, + AppAppearance, AppLanguage, AppTheme, + CustomThemeColors, DetectedExternalTool, ExternalToolsSettings, ToolOpenMode, @@ -44,6 +46,8 @@ interface Props { analytics: AnalyticsSettings; theme: AppTheme; + appearance: AppAppearance; + customTheme: CustomThemeColors; language: AppLanguage; autoRefresh: boolean; externalTools: ExternalToolsSettings; @@ -51,13 +55,15 @@ detectionPending: boolean; detectionUnavailable: boolean; onRefreshDetectedTools: () => void | Promise; - onSave: (settings: AnalyticsSettings, theme: AppTheme, language: AppLanguage, autoRefresh: boolean, externalTools: ExternalToolsSettings) => void; + onSave: (settings: AnalyticsSettings, theme: AppTheme, appearance: AppAppearance, customTheme: CustomThemeColors, language: AppLanguage, autoRefresh: boolean, externalTools: ExternalToolsSettings) => void; onClose: () => void; } let { analytics, theme = "system", + appearance = "modern", + customTheme = { background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" }, language = "en", autoRefresh = true, externalTools, @@ -76,6 +82,8 @@ let advancedOpen = $state(false); let analyticsEnabled = $state(true); let selectedTheme = $state("system"); + let selectedAppearance = $state("modern"); + let customColors = $state({ background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" }); let selectedLanguage = $state("en"); let autoRefreshEnabled = $state(true); let tools = $state(defaultExternalToolsSettings()); @@ -84,6 +92,8 @@ $effect(() => { analyticsEnabled = analytics.enabled; selectedTheme = theme; + selectedAppearance = appearance; + customColors = structuredClone(customTheme); selectedLanguage = language; autoRefreshEnabled = autoRefresh; tools = structuredClone(externalTools); @@ -94,7 +104,46 @@ ...analytics, enabled: analyticsEnabled, noticeSeen: true, - }, selectedTheme, selectedLanguage, autoRefreshEnabled, $state.snapshot(tools)); + }, selectedTheme, selectedAppearance, $state.snapshot(customColors), selectedLanguage, autoRefreshEnabled, $state.snapshot(tools)); + } + + function resetCustomColors() { + customColors = selectedTheme === "dark" + ? { background: "#222328", surface: "#2b2e34", accent: "#2eb5d1", text: "#f0f1f2" } + : { background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" }; + } + + function cssColorToHex(value: string, fallback: string): string { + const color = value.trim(); + if (/^#[0-9a-f]{6}$/i.test(color)) return color.toLowerCase(); + if (!color || typeof document === "undefined") return fallback; + const probe = document.createElement("span"); + probe.style.color = color; + if (!probe.style.color) return fallback; + probe.style.display = "none"; + document.body.appendChild(probe); + const match = getComputedStyle(probe).color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)/i); + probe.remove(); + if (!match) return fallback; + return `#${match.slice(1, 4).map((part) => Number(part).toString(16).padStart(2, "0")).join("")}`; + } + + function currentThemeColors(): CustomThemeColors { + const styles = getComputedStyle(document.documentElement); + const fallback = selectedTheme === "dark" + ? { background: "#222328", surface: "#2b2e34", accent: "#2eb5d1", text: "#f0f1f2" } + : { background: "#cfd5dc", surface: "#e2e6ea", accent: "#2eb5d1", text: "#222328" }; + return { + background: cssColorToHex(styles.getPropertyValue("--app-bg"), fallback.background), + surface: cssColorToHex(styles.getPropertyValue("--color-surface"), fallback.surface), + accent: cssColorToHex(styles.getPropertyValue("--color-accent"), fallback.accent), + text: cssColorToHex(styles.getPropertyValue("--color-ink"), fallback.text), + }; + } + + function selectAppearance(next: AppAppearance) { + if (next === "custom" && selectedAppearance !== "custom") customColors = currentThemeColors(); + selectedAppearance = next; } function toolLabel(kind: ExternalToolKind): string { @@ -292,6 +341,40 @@ +
+

{isGerman ? "Darstellungsstil" : "Design style"}

{isGerman ? "Aktuell, klassisch oder selbst gestaltet." : "Current, classic, or designed by you."}

+
+ + + +
+
+ + {#if selectedAppearance === "custom"} +
+
+ +

{isGerman ? "Theme-Generator" : "Theme generator"}

{isGerman ? "Erstelle dein eigenes Farbprofil." : "Create your own color profile."}

+ +
+
+ + +
+
+ + + + +
+

{isGerman ? "Die Farben werden beim Speichern auf die gesamte Oberfläche angewendet." : "The colors are applied across the interface when you save."}

+
+ {/if} +

{isGerman ? "Sprache" : "Language"}

{isGerman ? "Sprache der Oberfläche." : "Language used by the interface."}

@@ -534,6 +617,22 @@ .settings-segmented label { display: flex; align-items: center; justify-content: center; min-height: 31px; border: 1px solid transparent; border-radius: 6px; color: var(--color-ink-dim); font-size: 10.5px; font-weight: 750; } .settings-segmented label.active { border-color: var(--color-border-input); color: var(--color-ink); background: var(--color-surface-hover); } .settings-segmented input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } + .custom-theme-panel > header { align-items: center; } + .custom-theme-panel > header > div { min-width: 0; } + .theme-reset-button { display: inline-flex; align-items: center; gap: 5px; min-height: 27px; margin-left: auto; padding: 0 8px; border: 1px solid var(--color-border); color: var(--color-ink-muted); background: var(--color-surface-raised); font-size: 9.5px; font-weight: 750; } + .theme-reset-button:hover:not(:disabled) { border-color: var(--color-border-input); color: var(--color-ink); background: var(--color-surface-hover); } + .theme-preview { display: grid; grid-template-columns: 28% 1fr; min-height: 78px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--preview-text) 30%, var(--preview-surface)); background: var(--preview-bg); } + .theme-preview-sidebar { border-right: 1px solid color-mix(in srgb, var(--preview-text) 24%, var(--preview-surface)); background: color-mix(in srgb, var(--preview-surface) 86%, var(--preview-bg)); } + .theme-preview-content { display: grid; grid-template-columns: 1fr auto; align-content: start; gap: 8px; margin: 10px; padding: 10px; border: 1px solid color-mix(in srgb, var(--preview-text) 22%, var(--preview-surface)); color: var(--preview-text); background: var(--preview-surface); } + .theme-preview-content i { display: block; width: 54%; height: 7px; background: var(--preview-text); opacity: .82; } + .theme-preview-content b { display: block; width: 34px; height: 18px; grid-row: 1 / 3; grid-column: 2; background: var(--preview-accent); } + .theme-preview-content em { display: block; width: 76%; height: 5px; background: var(--preview-text); opacity: .32; } + .theme-color-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; } + .theme-color-grid label { display: grid; grid-template-columns: minmax(0, 1fr) 32px auto; align-items: center; gap: 8px; min-width: 0; min-height: 38px; padding: 5px 7px; border: 1px solid var(--color-border-subtle); background: var(--color-surface-raised); } + .theme-color-grid label > span { color: var(--color-ink-muted); font-size: 10px; font-weight: 750; } + .theme-color-grid input[type="color"] { width: 32px; height: 25px; padding: 2px; border: 1px solid var(--color-border-input); background: transparent; cursor: pointer; } + .theme-color-grid code { color: var(--color-ink-faint); font: 9px var(--font-mono); text-transform: uppercase; } + .theme-generator-note { margin: -5px 0 0 !important; color: var(--color-ink-faint) !important; } .settings-switch-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px; } .settings-switch-row span { display: grid; gap: 3px; } .settings-switch-row strong { color: var(--color-ink); font-size: 11px; } @@ -569,6 +668,7 @@ .tool-rescan-button { align-self: flex-start; } .general-settings-grid { grid-template-columns: 1fr; } .general-setting-panel.general-setting-wide { grid-column: auto; } + .theme-color-grid { grid-template-columns: 1fr; } .tool-config-panel { padding: 13px; } .tool-usage-callout { grid-template-columns: 1fr; gap: 4px; } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 6b13ea8..d1bb00a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -13,8 +13,16 @@ 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 type AppAppearance = "modern" | "classic" | "custom"; export type AppLanguage = "en" | "de"; +export interface CustomThemeColors { + background: string; + surface: string; + accent: string; + text: string; +} + export interface CommitAiStatus { phase: CommitAiPhase; model_id: string | null;