From 00b8ca25e27a4256044fe8e0d1b0207615bf365e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9A=AE=E6=98=9F?= <1009592348@qq.com> Date: Mon, 10 Aug 2026 03:40:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(theme):=20improve=20dark=20mode=20contrast?= =?UTF-8?q?=20=E6=94=B9=E5=96=84=E6=9A=97=E8=89=B2=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E5=AF=B9=E6=AF=94=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/index.html | 70 +++ ui/src/App.vue | 75 ++- ui/src/assets/base.css | 18 +- ui/src/assets/main.css | 9 +- ui/src/components/changes/GitChangesPanel.vue | 43 +- .../common/SplitDropdownControl.vue | 16 +- ui/src/components/common/ThemeSwitcher.vue | 13 +- ui/src/components/files/FileManagerPanel.vue | 88 +-- .../components/files/FilePreviewSurface.vue | 34 +- ui/src/components/kanban/KanbanBoard.vue | 7 +- ui/src/components/kanban/KanbanColumn.vue | 7 +- ui/src/components/kanban/TaskCard.vue | 17 +- ui/src/components/kanban/TaskDetailDrawer.vue | 14 +- ui/src/components/notepad/NotePad.vue | 50 +- ui/src/components/project/ProjectBrowser.vue | 21 +- ui/src/components/project/RecentProjects.vue | 85 +-- ui/src/components/terminal/TerminalPanel.vue | 109 ++-- .../terminal/TerminalSidebarRow.vue | 50 +- .../web-session/WebSessionComposerEditor.vue | 38 +- .../web-session/WebSessionImportDialog.vue | 28 +- .../web-session/WebSessionPanel.vue | 529 +++++++++++------- .../web-session/WebSessionSidebarRow.vue | 60 +- .../WebSessionSkillCatalogPanel.vue | 26 +- .../workspace/DockedNotificationSidebar.vue | 26 +- .../components/workspace/WorkspaceTabView.vue | 72 +-- ui/src/constants/themes.ts | 23 + ui/src/i18n/locales/en-US.ts | 4 +- ui/src/i18n/locales/zh-CN.ts | 4 +- ui/src/styles/chat-markdown.css | 132 +++-- ui/src/styles/project-badges.css | 19 + ui/src/styles/variables.css | 97 +++- ui/src/utils/themeMaintenanceWarning.ts | 12 +- ui/src/utils/themeOverrides.ts | 69 +-- ui/src/utils/themeSemanticPalette.ts | 86 +++ ui/src/views/GeneralSettings.vue | 82 +-- ui/src/views/LoginView.vue | 67 ++- ui/src/views/ProjectWorkspace.vue | 27 +- 37 files changed, 1371 insertions(+), 756 deletions(-) create mode 100644 ui/src/utils/themeSemanticPalette.ts diff --git a/ui/index.html b/ui/index.html index 4ee99a08..feeebf02 100644 --- a/ui/index.html +++ b/ui/index.html @@ -7,6 +7,76 @@ Code Kanban + diff --git a/ui/src/App.vue b/ui/src/App.vue index 9bcfd3e3..f3466988 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -11,9 +11,10 @@ import { useSettingsStore } from '@/stores/settings'; import { useProjectStore } from '@/stores/project'; import { useResponsive } from '@/composables/useResponsive'; import { useAiStatusSummary } from '@/composables/useAiStatusSummary'; -import { darkenColor, lightenColor, isDarkHex } from '@/utils/color'; +import { isDarkHex } from '@/utils/color'; import { formatBrowserTabTitle } from '@/utils/browserTitle'; import { createThemeOverrides } from '@/utils/themeOverrides'; +import { createThemeSemanticPalette } from '@/utils/themeSemanticPalette'; import { getPresetById } from '@/constants/themes'; import { APP_NAME } from '@/constants/app'; @@ -89,15 +90,14 @@ const resolvedTextColor = computed(() => { if (textColor && textColor.trim().length > 0) { return textColor; } - return isDarkTheme.value ? '#FFFFFFD9' : '#000000E0'; + return isDarkTheme.value ? '#D9D9D9' : '#333333'; }); -const inputBorderColor = computed(() => (isDarkTheme.value ? '#4B4B4B' : '#D0D5DD')); -const inputBorderHoverColor = computed(() => - isDarkTheme.value - ? lightenColor(inputBorderColor.value, 0.12) - : darkenColor(inputBorderColor.value, 0.12) +const semanticPalette = computed(() => + createThemeSemanticPalette(theme.value, resolvedTextColor.value) ); +const inputBorderColor = computed(() => semanticPalette.value.controlBorder); +const inputBorderHoverColor = computed(() => semanticPalette.value.controlBorderHover); // 根据当前语言动态切换 Naive UI 的 locale const naiveLocale = computed(() => (locale.value === 'zh-CN' ? zhCN : enUS)); @@ -169,6 +169,64 @@ const cssVarsToSet = computed(() => ({ '--app-text-color': resolvedTextColor.value, '--app-input-border-color': inputBorderColor.value, '--app-input-border-hover-color': inputBorderHoverColor.value, + '--app-canvas': semanticPalette.value.canvas, + '--app-surface': semanticPalette.value.surface, + '--app-surface-raised': semanticPalette.value.surfaceRaised, + '--app-surface-sunken': semanticPalette.value.surfaceSunken, + '--app-surface-hover': semanticPalette.value.surfaceHover, + '--app-surface-active': semanticPalette.value.surfaceActive, + '--app-text-primary': semanticPalette.value.textPrimary, + '--app-text-secondary': semanticPalette.value.textSecondary, + '--app-text-muted': semanticPalette.value.textMuted, + '--app-text-inverse': semanticPalette.value.textInverse, + '--app-border': semanticPalette.value.border, + '--app-border-strong': semanticPalette.value.borderStrong, + '--app-control-border': semanticPalette.value.controlBorder, + '--app-control-border-hover': semanticPalette.value.controlBorderHover, + '--app-focus-ring': semanticPalette.value.focusRing, + '--app-accent': semanticPalette.value.accent, + '--app-accent-hover': semanticPalette.value.accentHover, + '--app-accent-pressed': semanticPalette.value.accentPressed, + '--app-accent-soft': semanticPalette.value.accentSoft, + '--app-accent-contrast': semanticPalette.value.accentContrast, + '--app-link': semanticPalette.value.link, + '--app-link-hover': semanticPalette.value.linkHover, + '--app-success': semanticPalette.value.success, + '--app-success-soft': semanticPalette.value.successSoft, + '--app-warning': semanticPalette.value.warning, + '--app-warning-soft': semanticPalette.value.warningSoft, + '--app-error': semanticPalette.value.error, + '--app-error-soft': semanticPalette.value.errorSoft, + '--app-info': semanticPalette.value.info, + '--app-info-soft': semanticPalette.value.infoSoft, + '--app-overlay': semanticPalette.value.overlay, + '--app-shadow': semanticPalette.value.shadow, + '--n-primary-color': semanticPalette.value.accent, + '--n-color-primary': semanticPalette.value.accent, + '--n-primary-color-hover': semanticPalette.value.accentHover, + '--n-primary-color-pressed': semanticPalette.value.accentPressed, + '--n-primary-color-suppl': semanticPalette.value.accentHover, + '--n-text-color-base': semanticPalette.value.textPrimary, + '--n-text-color': semanticPalette.value.textPrimary, + '--n-text-color-1': semanticPalette.value.textPrimary, + '--n-text-color-2': semanticPalette.value.textSecondary, + '--n-text-color-3': semanticPalette.value.textMuted, + '--n-text-color-disabled': semanticPalette.value.textMuted, + '--n-body-color': semanticPalette.value.canvas, + '--n-color': semanticPalette.value.surface, + '--n-color-hover': semanticPalette.value.surfaceHover, + '--n-color-target': semanticPalette.value.accentSoft, + '--n-color-embedded': semanticPalette.value.surfaceSunken, + '--n-card-color': semanticPalette.value.surface, + '--n-modal-color': semanticPalette.value.surfaceRaised, + '--n-popover-color': semanticPalette.value.surfaceRaised, + '--n-border-color': semanticPalette.value.border, + '--n-divider-color': semanticPalette.value.border, + '--n-box-shadow-color': semanticPalette.value.shadow, + '--n-error-color': semanticPalette.value.error, + '--n-warning-color': semanticPalette.value.warning, + '--n-success-color': semanticPalette.value.success, + '--n-info-color': semanticPalette.value.info, })); watch( @@ -176,6 +234,9 @@ watch( vars => { if (typeof document !== 'undefined') { const root = document.documentElement; + root.dataset.colorScheme = semanticPalette.value.colorScheme; + root.style.colorScheme = semanticPalette.value.colorScheme; + root.style.backgroundColor = semanticPalette.value.canvas; Object.entries(vars).forEach(([key, value]) => { root.style.setProperty(key, value ?? ''); }); diff --git a/ui/src/assets/base.css b/ui/src/assets/base.css index 8816868a..3c6e0dd4 100644 --- a/ui/src/assets/base.css +++ b/ui/src/assets/base.css @@ -36,20 +36,6 @@ --section-gap: 160px; } -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--vt-c-black); - --color-background-soft: var(--vt-c-black-soft); - --color-background-mute: var(--vt-c-black-mute); - - --color-border: var(--vt-c-divider-dark-2); - --color-border-hover: var(--vt-c-divider-dark-1); - - --color-heading: var(--vt-c-text-dark-1); - --color-text: var(--vt-c-text-dark-2); - } -} - *, *::before, *::after { @@ -60,8 +46,8 @@ body { min-height: 100vh; - color: var(--color-text); - background: var(--color-background); + color: var(--app-text-primary, var(--color-text)); + background: var(--app-canvas, var(--color-background)); transition: color 0.5s, background-color 0.5s; diff --git a/ui/src/assets/main.css b/ui/src/assets/main.css index 81f715d1..e4d6a0b1 100644 --- a/ui/src/assets/main.css +++ b/ui/src/assets/main.css @@ -5,6 +5,8 @@ min-height: 100vh; margin: 0; padding: 0; + background-color: var(--app-canvas, var(--app-body-color, #f7f8fa)); + color: var(--app-text-primary, var(--app-text-color, #333333)); } main { @@ -125,18 +127,19 @@ main { left: 0; right: 0; bottom: 0; - background-color: var(--app-surface-color, #ffffff); + background-color: var(--app-surface-raised, var(--app-surface-color, #ffffff)); + color: var(--app-text-primary, var(--app-text-color, #333333)); z-index: 100; border-top-left-radius: 16px; border-top-right-radius: 16px; - box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15); + box-shadow: 0 -4px 20px var(--app-shadow, rgba(0, 0, 0, 0.15)); } /* 拖动条 (用于移动端面板) */ .drag-handle { width: 40px; height: 4px; - background-color: var(--n-border-color, #e0e0e0); + background-color: var(--app-border-strong, var(--n-border-color, #e0e0e0)); border-radius: 2px; margin: 8px auto; } diff --git a/ui/src/components/changes/GitChangesPanel.vue b/ui/src/components/changes/GitChangesPanel.vue index 4be6d219..441e7dfb 100644 --- a/ui/src/components/changes/GitChangesPanel.vue +++ b/ui/src/components/changes/GitChangesPanel.vue @@ -861,9 +861,8 @@ onBeforeUnmount(() => { min-height: 0; min-width: 0; overflow: hidden; - background: - linear-gradient(180deg, rgba(246, 248, 252, 0.98), rgba(238, 242, 247, 0.96)), - radial-gradient(circle at top right, rgba(53, 119, 186, 0.12), transparent 32%); + background: var(--app-canvas); + color: var(--app-text-primary); } .git-changes-toolbar { @@ -872,7 +871,8 @@ onBeforeUnmount(() => { gap: 12px; flex-wrap: wrap; padding: 12px 16px; - border-bottom: 1px solid rgba(24, 35, 51, 0.08); + border-bottom: 1px solid var(--app-border); + background: var(--app-surface); } .git-changes-scope { @@ -925,8 +925,8 @@ onBeforeUnmount(() => { flex: 0 0 360px; min-width: 280px; max-width: 440px; - border-right: 1px solid rgba(24, 35, 51, 0.08); - background: rgba(255, 255, 255, 0.72); + border-right: 1px solid var(--app-border); + background: var(--app-surface-sunken); display: flex; flex-direction: column; min-height: 0; @@ -959,7 +959,7 @@ onBeforeUnmount(() => { overflow: auto; padding: 12px; scrollbar-width: thin; - scrollbar-color: rgba(37, 90, 143, 0.38) transparent; + scrollbar-color: var(--app-border-strong) transparent; } .git-changes-list::-webkit-scrollbar { @@ -969,7 +969,7 @@ onBeforeUnmount(() => { .git-changes-list::-webkit-scrollbar-thumb { border-radius: 999px; - background: rgba(37, 90, 143, 0.28); + background: var(--app-border-strong); } .git-changes-empty, @@ -988,9 +988,10 @@ onBeforeUnmount(() => { display: flex; width: 100%; padding: 12px 14px; - border: 1px solid rgba(24, 35, 51, 0.08); + border: 1px solid var(--app-border); border-radius: 12px; - background: rgba(255, 255, 255, 0.92); + background: var(--app-surface-raised); + color: var(--app-text-primary); text-align: left; cursor: pointer; transition: @@ -1001,9 +1002,13 @@ onBeforeUnmount(() => { .git-change-row:hover, .git-change-row.is-active { - border-color: rgba(53, 119, 186, 0.32); - background: rgba(240, 247, 255, 0.9); - box-shadow: 0 10px 24px rgba(40, 73, 118, 0.08); + border-color: var(--app-accent); + background: var(--app-surface-hover); + box-shadow: 0 10px 24px color-mix(in srgb, var(--app-accent) 8%, transparent); +} + +.git-change-row.is-active { + background: var(--app-surface-active); } .git-change-main { @@ -1022,7 +1027,7 @@ onBeforeUnmount(() => { } .git-change-icon { - color: #255a8f; + color: var(--app-info); flex: 0 0 auto; } @@ -1042,12 +1047,12 @@ onBeforeUnmount(() => { } .git-change-stat-tag { - color: rgba(34, 46, 67, 0.78); + color: var(--app-text-secondary); } .git-change-path, .git-change-previous { - color: rgba(34, 46, 67, 0.7); + color: var(--app-text-muted); font-size: 12px; line-height: 1.45; word-break: break-all; @@ -1055,7 +1060,7 @@ onBeforeUnmount(() => { .git-changes-preview { flex: 1; - background: rgba(255, 255, 255, 0.82); + background: var(--app-surface); } .git-changes-mobile-preview-modal { @@ -1067,7 +1072,7 @@ onBeforeUnmount(() => { .git-changes-mobile-preview-surface { width: 100vw; min-height: min(100vh, 100dvh); - background: rgba(255, 255, 255, 0.98); + background: var(--app-surface); border-radius: 0; overflow: hidden; } @@ -1082,7 +1087,7 @@ onBeforeUnmount(() => { width: 100%; max-width: none; border-right: none; - border-bottom: 1px solid rgba(24, 35, 51, 0.08); + border-bottom: 1px solid var(--app-border); } .git-changes-panel:not(.is-mobile-preview) .git-changes-list { diff --git a/ui/src/components/common/SplitDropdownControl.vue b/ui/src/components/common/SplitDropdownControl.vue index f571fa28..5a004a92 100644 --- a/ui/src/components/common/SplitDropdownControl.vue +++ b/ui/src/components/common/SplitDropdownControl.vue @@ -75,8 +75,9 @@ function handleSelect(key: string | number) { border-radius: 6px; gap: 0; padding: 0; - border: 1px solid var(--kanban-notification-button-border, rgba(0, 0, 0, 0.2)); - background: var(--app-surface-color, var(--body-color, #ffffff)); + border: 1px solid var(--kanban-notification-button-border, var(--app-border, rgba(0, 0, 0, 0.2))); + background: var(--app-surface, var(--app-surface-color, var(--body-color, #ffffff))); + color: var(--app-text-primary, inherit); box-shadow: none; } @@ -103,12 +104,12 @@ function handleSelect(key: string | number) { .split-dropdown-menu { padding: 0 8px; - border-left: 1px solid rgba(0, 0, 0, 0.08); + border-left: 1px solid var(--app-border, rgba(0, 0, 0, 0.08)); justify-content: center; } .split-dropdown-control:hover { - box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15); + box-shadow: 0 4px 12px var(--app-shadow, rgba(15, 23, 42, 0.15)); } .split-dropdown-control.is-flat:hover { @@ -120,13 +121,14 @@ function handleSelect(key: string | number) { } .split-dropdown-control.is-active:hover { - box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15); + box-shadow: 0 4px 12px var(--app-shadow, rgba(15, 23, 42, 0.15)); } .split-dropdown-main:focus-visible, .split-dropdown-menu:focus-visible { - outline: none; - background: color-mix(in srgb, var(--n-primary-color) 4%, transparent); + outline: 2px solid var(--app-focus-ring, var(--n-primary-color)); + outline-offset: -2px; + background: var(--app-accent-soft, color-mix(in srgb, var(--n-primary-color) 4%, transparent)); } .split-dropdown-icon { diff --git a/ui/src/components/common/ThemeSwitcher.vue b/ui/src/components/common/ThemeSwitcher.vue index ceef8318..170cbe6c 100644 --- a/ui/src/components/common/ThemeSwitcher.vue +++ b/ui/src/components/common/ThemeSwitcher.vue @@ -16,9 +16,10 @@ import { storeToRefs } from 'pinia'; import { NIcon, useDialog } from 'naive-ui'; import { MoonOutline, SunnyOutline, CheckmarkOutline } from '@vicons/ionicons5'; import { useSettingsStore } from '@/stores/settings'; -import { THEME_PRESETS, getPresetById } from '@/constants/themes'; +import { THEME_PRESETS, isSupportedThemePreset } from '@/constants/themes'; import { useLocale } from '@/composables/useLocale'; import { useThemeOptions } from '@/composables/useThemeOptions'; +import { isDarkHex } from '@/utils/color'; import { createThemeMaintenanceWarningController, createThemeSelectionController, @@ -39,7 +40,7 @@ const props = withDefaults( const { t } = useLocale(); const dialog = useDialog(); const settingsStore = useSettingsStore(); -const { currentPresetId, followSystemTheme } = storeToRefs(settingsStore); +const { activeTheme, currentPresetId, followSystemTheme } = storeToRefs(settingsStore); const themeWarningController = createThemeMaintenanceWarningController({ t, warning: options => dialog.warning(options), @@ -51,13 +52,11 @@ const themeSelectionController = createThemeSelectionController({ toggleFollowSystemTheme: enabled => settingsStore.toggleFollowSystemTheme(enabled), confirmPresetThemeChange: themeWarningController.confirmPresetThemeChange, confirmFollowSystemEnable: themeWarningController.confirmFollowSystemEnable, + shouldConfirmPresetThemeChange: presetId => !isSupportedThemePreset(presetId), + shouldConfirmFollowSystemEnable: () => false, }); -// 判断当前是否为暗色主题 -const isDarkTheme = computed(() => { - const preset = getPresetById(currentPresetId.value); - return preset?.isDark ?? false; -}); +const isDarkTheme = computed(() => isDarkHex(activeTheme.value.bodyColor)); // 渲染颜色圆点 const renderColorDot = (color: string) => diff --git a/ui/src/components/files/FileManagerPanel.vue b/ui/src/components/files/FileManagerPanel.vue index 15eba094..d551f615 100644 --- a/ui/src/components/files/FileManagerPanel.vue +++ b/ui/src/components/files/FileManagerPanel.vue @@ -1583,9 +1583,12 @@ onBeforeUnmount(() => { min-width: 0; min-height: 0; overflow: hidden; - background: - linear-gradient(180deg, rgba(246, 248, 252, 0.98), rgba(238, 242, 247, 0.96)), - radial-gradient(circle at top right, rgba(53, 119, 186, 0.12), transparent 30%); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--app-canvas) 96%, var(--app-accent) 4%), + var(--app-canvas) + ); + color: var(--app-text-primary); } .file-manager-toolbar { @@ -1594,7 +1597,7 @@ onBeforeUnmount(() => { gap: 12px; align-items: center; padding: 12px 16px; - border-bottom: 1px solid rgba(24, 35, 51, 0.08); + border-bottom: 1px solid var(--app-border); } .scope-select { @@ -1613,7 +1616,7 @@ onBeforeUnmount(() => { .file-manager-breadcrumb-label { flex: 0 0 auto; - color: rgba(34, 46, 67, 0.62); + color: var(--app-text-muted); font-size: 12px; font-weight: 600; } @@ -1673,8 +1676,8 @@ onBeforeUnmount(() => { gap: 12px; flex-shrink: 0; padding: 8px 16px; - border-bottom: 1px solid rgba(24, 35, 51, 0.08); - background: rgba(255, 255, 255, 0.72); + border-bottom: 1px solid var(--app-border); + background: color-mix(in srgb, var(--app-surface) 72%, var(--app-canvas) 28%); } .file-manager-action-controls { @@ -1689,7 +1692,7 @@ onBeforeUnmount(() => { min-width: 72px; font-size: 13px; font-weight: 600; - color: rgba(34, 46, 67, 0.8); + color: var(--app-text-secondary); } .file-manager-body { @@ -1738,10 +1741,10 @@ onBeforeUnmount(() => { display: flex; align-items: center; justify-content: center; - border: 2px dashed rgba(53, 119, 186, 0.45); + border: 2px dashed color-mix(in srgb, var(--app-accent) 55%, transparent); border-radius: 18px; - background: rgba(255, 255, 255, 0.85); - color: #1f4c7f; + background: color-mix(in srgb, var(--app-surface-raised) 88%, var(--app-canvas) 12%); + color: var(--app-info); font-weight: 600; pointer-events: none; } @@ -1759,9 +1762,10 @@ onBeforeUnmount(() => { gap: 10px; min-height: 44px; padding: 8px 10px; - border: 1px solid rgba(24, 35, 51, 0.08); + border: 1px solid var(--app-border); border-radius: 10px; - background: rgba(255, 255, 255, 0.88); + background: color-mix(in srgb, var(--app-surface) 88%, var(--app-canvas) 12%); + color: var(--app-text-primary); text-align: left; cursor: pointer; transition: @@ -1772,15 +1776,20 @@ onBeforeUnmount(() => { .file-tree-row:hover, .file-tree-row.is-active { - border-color: rgba(53, 119, 186, 0.32); - background: rgba(240, 247, 255, 0.9); - box-shadow: 0 10px 24px rgba(40, 73, 118, 0.08); + border-color: color-mix(in srgb, var(--app-accent) 40%, var(--app-border) 60%); + background: color-mix(in srgb, var(--app-surface) 94%, var(--app-accent) 6%); + box-shadow: 0 10px 24px color-mix(in srgb, var(--app-shadow) 45%, transparent); +} + +.file-tree-row:focus-visible { + outline: 2px solid var(--app-focus-ring); + outline-offset: 1px; } .tree-expand-hit { width: 16px; flex: 0 0 16px; - color: rgba(34, 46, 67, 0.72); + color: var(--app-text-secondary); text-align: center; } @@ -1821,7 +1830,7 @@ onBeforeUnmount(() => { .file-preview-content, .file-transfer-items { scrollbar-width: thin; - scrollbar-color: rgba(37, 90, 143, 0.38) transparent; + scrollbar-color: color-mix(in srgb, var(--app-accent) 42%, transparent) transparent; } .file-list-scroll::-webkit-scrollbar, @@ -1841,13 +1850,13 @@ onBeforeUnmount(() => { .file-preview-content::-webkit-scrollbar-thumb, .file-transfer-items::-webkit-scrollbar-thumb { border-radius: 999px; - background: rgba(37, 90, 143, 0.28); + background: color-mix(in srgb, var(--app-accent) 32%, transparent); } .file-list-scroll::-webkit-scrollbar-thumb:hover, .file-preview-content::-webkit-scrollbar-thumb:hover, .file-transfer-items::-webkit-scrollbar-thumb:hover { - background: rgba(37, 90, 143, 0.42); + background: color-mix(in srgb, var(--app-accent) 48%, transparent); } .file-list-checkbox { @@ -1862,7 +1871,7 @@ onBeforeUnmount(() => { } .file-list-checkbox:hover { - background: rgba(37, 90, 143, 0.08); + background: var(--app-accent-soft); } .file-list-checkbox :deep(input[type='checkbox']) { @@ -1890,7 +1899,7 @@ onBeforeUnmount(() => { } .file-kind-icon { - color: #255a8f; + color: var(--app-info); } .file-name-text { @@ -1906,7 +1915,7 @@ onBeforeUnmount(() => { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - color: rgba(34, 46, 67, 0.78); + color: var(--app-text-secondary); font-size: 12px; text-align: right; } @@ -1927,20 +1936,25 @@ onBeforeUnmount(() => { border: none; border-radius: 8px; background: transparent; - color: rgba(34, 46, 67, 0.88); + color: var(--app-text-primary); cursor: pointer; text-align: left; } .file-display-option:hover, .file-display-option.is-selected { - background: rgba(37, 90, 143, 0.08); + background: var(--app-accent-soft); +} + +.file-display-option:focus-visible { + outline: 2px solid var(--app-focus-ring); + outline-offset: -2px; } .file-display-option-check { width: 14px; flex: 0 0 14px; - color: #255a8f; + color: var(--app-info); font-size: 13px; font-weight: 700; text-align: center; @@ -1958,8 +1972,8 @@ onBeforeUnmount(() => { .file-preview-pane { flex: 1; min-width: 0; - border-left: 1px solid rgba(24, 35, 51, 0.08); - background: rgba(255, 255, 255, 0.78); + border-left: 1px solid var(--app-border); + background: color-mix(in srgb, var(--app-surface) 78%, var(--app-canvas) 22%); display: flex; flex-direction: column; min-height: 0; @@ -1983,7 +1997,7 @@ onBeforeUnmount(() => { .file-mobile-preview-surface { min-height: min(100vh, 100dvh); - background: rgba(255, 255, 255, 0.98); + background: var(--app-surface-raised); } .file-image-modal-title { @@ -2000,13 +2014,13 @@ onBeforeUnmount(() => { max-height: 78vh; object-fit: contain; border-radius: 12px; - background: #f4f7fb; + background: var(--app-surface-sunken); } .file-transfer-queue { flex-shrink: 0; - border-top: 1px solid rgba(24, 35, 51, 0.08); - background: rgba(255, 255, 255, 0.88); + border-top: 1px solid var(--app-border); + background: color-mix(in srgb, var(--app-surface) 88%, var(--app-canvas) 12%); padding: 12px 16px; } @@ -2033,9 +2047,9 @@ onBeforeUnmount(() => { gap: 12px; align-items: flex-start; padding: 10px 12px; - border: 1px solid rgba(24, 35, 51, 0.08); + border: 1px solid var(--app-border); border-radius: 14px; - background: rgba(248, 250, 253, 0.96); + background: color-mix(in srgb, var(--app-canvas) 86%, var(--app-surface) 14%); } .file-transfer-main { @@ -2053,7 +2067,7 @@ onBeforeUnmount(() => { .file-transfer-meta { margin: 4px 0 8px; - color: rgba(34, 46, 67, 0.66); + color: var(--app-text-muted); font-size: 12px; } @@ -2065,7 +2079,7 @@ onBeforeUnmount(() => { .file-transfer-error { margin-top: 6px; - color: #b42318; + color: var(--app-error); font-size: 12px; } @@ -2089,7 +2103,7 @@ onBeforeUnmount(() => { min-width: 0; min-height: 220px; border-left: none; - border-top: 1px solid rgba(24, 35, 51, 0.08); + border-top: 1px solid var(--app-border); } } diff --git a/ui/src/components/files/FilePreviewSurface.vue b/ui/src/components/files/FilePreviewSurface.vue index 043e5bf4..9a1cb80a 100644 --- a/ui/src/components/files/FilePreviewSurface.vue +++ b/ui/src/components/files/FilePreviewSurface.vue @@ -185,6 +185,8 @@ const headerMeta = computed(() => flex: 1; flex-direction: column; min-height: 0; + background: var(--app-surface); + color: var(--app-text-primary); } .file-preview-header { @@ -193,8 +195,8 @@ const headerMeta = computed(() => justify-content: space-between; gap: 12px; padding: 16px; - border-bottom: 1px solid rgba(24, 35, 51, 0.08); - background: rgba(255, 255, 255, 0.86); + border-bottom: 1px solid var(--app-border); + background: color-mix(in srgb, var(--app-surface) 86%, var(--app-canvas) 14%); } .file-preview-shell.is-mobile .file-preview-header { @@ -228,7 +230,7 @@ const headerMeta = computed(() => align-items: center; padding: 3px; border-radius: 999px; - background: rgba(37, 90, 143, 0.08); + background: var(--app-accent-soft); gap: 4px; } @@ -236,7 +238,7 @@ const headerMeta = computed(() => border: none; border-radius: 999px; background: transparent; - color: rgba(34, 46, 67, 0.7); + color: var(--app-text-secondary); font-size: 12px; font-weight: 600; padding: 6px 10px; @@ -247,9 +249,14 @@ const headerMeta = computed(() => } .file-preview-mode-button.is-active { - background: #ffffff; - color: #1f4c7f; - box-shadow: 0 6px 16px rgba(31, 76, 127, 0.16); + background: var(--app-surface-raised); + color: var(--app-info); + box-shadow: 0 6px 16px color-mix(in srgb, var(--app-shadow) 65%, transparent); +} + +.file-preview-mode-button:focus-visible { + outline: 2px solid var(--app-focus-ring); + outline-offset: 1px; } .file-preview-back { @@ -266,7 +273,7 @@ const headerMeta = computed(() => .file-preview-meta { margin-top: 4px; - color: rgba(34, 46, 67, 0.62); + color: var(--app-text-muted); font-size: 12px; } @@ -278,7 +285,7 @@ const headerMeta = computed(() => -webkit-overflow-scrolling: touch; padding: 16px; scrollbar-width: thin; - scrollbar-color: rgba(37, 90, 143, 0.38) transparent; + scrollbar-color: color-mix(in srgb, var(--app-accent) 42%, transparent) transparent; } .file-preview-content::-webkit-scrollbar { @@ -292,11 +299,11 @@ const headerMeta = computed(() => .file-preview-content::-webkit-scrollbar-thumb { border-radius: 999px; - background: rgba(37, 90, 143, 0.28); + background: color-mix(in srgb, var(--app-accent) 32%, transparent); } .file-preview-content::-webkit-scrollbar-thumb:hover { - background: rgba(37, 90, 143, 0.42); + background: color-mix(in srgb, var(--app-accent) 48%, transparent); } .file-preview-image, @@ -304,7 +311,7 @@ const headerMeta = computed(() => .file-preview-media { width: 100%; border-radius: 14px; - background: #f4f7fb; + background: var(--app-surface-sunken); } .file-preview-image { @@ -357,7 +364,7 @@ const headerMeta = computed(() => .file-preview-binary, .file-preview-truncated { - color: rgba(34, 46, 67, 0.7); + color: var(--app-text-secondary); font-size: 13px; } @@ -372,6 +379,7 @@ const headerMeta = computed(() => height: 100%; min-height: 220px; padding: 16px; + color: var(--app-text-secondary); text-align: center; } diff --git a/ui/src/components/kanban/KanbanBoard.vue b/ui/src/components/kanban/KanbanBoard.vue index 732eba51..029f8010 100644 --- a/ui/src/components/kanban/KanbanBoard.vue +++ b/ui/src/components/kanban/KanbanBoard.vue @@ -708,12 +708,13 @@ function handleTaskViewEvent(event: { taskId?: string; projectId?: string }) { display: flex; flex-direction: column; height: 100%; - background-color: var(--app-surface-color, #ffffff); + background-color: var(--app-surface, var(--app-surface-color, #ffffff)); + color: var(--app-text-primary, var(--app-text-color, #333333)); } .board-header { padding: 16px 24px; - border-bottom: var(--kanban-border, 1px solid var(--n-border-color)); + border-bottom: var(--kanban-border, 1px solid var(--app-border, #e0e0e0)); } .board-header h2 { @@ -760,7 +761,7 @@ function handleTaskViewEvent(event: { taskId?: string; projectId?: string }) { position: sticky; top: 0; z-index: 10; - background-color: var(--app-surface-color, #ffffff); + background-color: var(--app-surface, var(--app-surface-color, #ffffff)); } .board-tabs-view :deep(.n-tabs-pane-wrapper) { diff --git a/ui/src/components/kanban/KanbanColumn.vue b/ui/src/components/kanban/KanbanColumn.vue index c199c6ba..45498ead 100644 --- a/ui/src/components/kanban/KanbanColumn.vue +++ b/ui/src/components/kanban/KanbanColumn.vue @@ -139,9 +139,10 @@ function handleChange(event: any) { .kanban-column { display: flex; flex-direction: column; - background-color: var(--kanban-board-bg, var(--app-body-color, #f5f5f5)); + background-color: var(--kanban-board-bg, var(--app-canvas, var(--app-body-color, #f5f5f5))); + color: var(--app-text-primary, var(--app-text-color, #333333)); border-radius: 8px; - border: 1px solid var(--n-border-color, #e0e0e0); + border: 1px solid var(--app-border, var(--n-border-color, #e0e0e0)); height: 100%; overflow: hidden; } @@ -151,7 +152,7 @@ function handleChange(event: any) { align-items: center; justify-content: space-between; padding: 12px 16px; - border-bottom: var(--kanban-border, 1px solid var(--n-border-color)); + border-bottom: var(--kanban-border, 1px solid var(--app-border, #e0e0e0)); flex-shrink: 0; } diff --git a/ui/src/components/kanban/TaskCard.vue b/ui/src/components/kanban/TaskCard.vue index c4fad014..49e8413b 100644 --- a/ui/src/components/kanban/TaskCard.vue +++ b/ui/src/components/kanban/TaskCard.vue @@ -173,13 +173,14 @@ const handleStartWorkSelect = (key: string | number) => { .task-card { cursor: pointer; transition: all 0.2s ease; - --n-color: var(--kanban-card-bg, var(--app-surface-color, #ffffff)); + --n-color: var(--kanban-card-bg, var(--app-surface-raised, var(--app-surface-color, #ffffff))); + color: var(--app-text-primary, var(--app-text-color, #333333)); position: relative; } .task-card:hover { transform: translateY(-2px); - box-shadow: 0 8px 16px var(--n-box-shadow-color, rgba(0, 0, 0, 0.12)); + box-shadow: 0 8px 16px var(--app-shadow, var(--n-box-shadow-color, rgba(0, 0, 0, 0.12))); } .task-card__header { @@ -238,15 +239,15 @@ const handleStartWorkSelect = (key: string | number) => { padding: 4px 10px; border-radius: 999px; border: none; - background-color: var(--kanban-terminal-pill-bg, #eef2ff); - color: var(--kanban-terminal-pill-fg, #4c1d95); + background-color: var(--kanban-terminal-pill-bg, var(--app-accent-soft, #eef2ff)); + color: var(--kanban-terminal-pill-fg, var(--app-accent, #4c1d95)); font-size: 12px; line-height: 1; cursor: pointer; } .task-card__terminal-status:focus-visible { - outline: 2px solid var(--n-color-primary); + outline: 2px solid var(--app-focus-ring, var(--n-color-primary)); outline-offset: 2px; } @@ -258,15 +259,15 @@ const handleStartWorkSelect = (key: string | number) => { } .task-card__status-dot.status-linked { - background-color: var(--n-primary-color, #4f8ff7); + background-color: var(--app-accent, var(--n-primary-color, #4f8ff7)); } .task-card__status-dot.status-waiting_input { - background-color: #94a3b8; + background-color: var(--app-text-muted, #94a3b8); } .task-card__status-dot.status-unknown { - background-color: #94a3b8; + background-color: var(--app-text-muted, #94a3b8); } .task-card__terminal-text { diff --git a/ui/src/components/kanban/TaskDetailDrawer.vue b/ui/src/components/kanban/TaskDetailDrawer.vue index aea67ea0..9d0932d9 100644 --- a/ui/src/components/kanban/TaskDetailDrawer.vue +++ b/ui/src/components/kanban/TaskDetailDrawer.vue @@ -757,12 +757,12 @@ const formatDate = (value: string) => dayjs(value).format('YYYY-MM-DD HH:mm'); gap: 8px; margin-top: 4px; font-size: 12px; - color: var(--n-text-color-3); + color: var(--app-text-muted, var(--n-text-color-3)); } .linked-session-time, .linked-session-count { - color: var(--n-text-color-3); + color: var(--app-text-muted, var(--n-text-color-3)); } /* 可用 AI Session 列表 */ @@ -781,7 +781,7 @@ const formatDate = (value: string) => dayjs(value).format('YYYY-MM-DD HH:mm'); gap: 12px; padding: 12px; margin-bottom: 8px; - background: var(--n-color-embedded); + background: var(--app-surface-sunken, var(--n-color-embedded)); border-radius: 8px; cursor: pointer; transition: all 0.2s ease; @@ -789,12 +789,12 @@ const formatDate = (value: string) => dayjs(value).format('YYYY-MM-DD HH:mm'); } .available-session-item:hover { - border-color: var(--n-border-color); + border-color: var(--app-border, var(--n-border-color)); } .available-session-item.selected { - border-color: var(--n-primary-color); - background: var(--n-color-embedded); + border-color: var(--app-accent, var(--n-primary-color)); + background: var(--app-accent-soft, var(--n-color-embedded)); } .available-session-info { @@ -816,7 +816,7 @@ const formatDate = (value: string) => dayjs(value).format('YYYY-MM-DD HH:mm'); align-items: center; gap: 8px; font-size: 12px; - color: var(--n-text-color-3); + color: var(--app-text-muted, var(--n-text-color-3)); } /* 移动端样式 */ diff --git a/ui/src/components/notepad/NotePad.vue b/ui/src/components/notepad/NotePad.vue index d38b1e5b..d752c2ae 100644 --- a/ui/src/components/notepad/NotePad.vue +++ b/ui/src/components/notepad/NotePad.vue @@ -513,9 +513,9 @@ onMounted(() => { top: 80px; right: 0; bottom: 40px; - background: var(--app-body-color, #fafafa); - border-left: 1px solid var(--n-border-color, #e0e0e0); - box-shadow: -2px 0 8px var(--n-box-shadow-color, rgba(0, 0, 0, 0.08)); + background: var(--app-canvas); + border-left: 1px solid var(--app-border); + box-shadow: -2px 0 8px var(--app-shadow); transition: transform 0.3s ease; display: flex; flex-direction: column; @@ -537,7 +537,7 @@ onMounted(() => { } .resize-handle:hover { - background: var(--n-color-target, var(--n-primary-color, #1890ff)); + background: var(--app-accent); } .notepad-toggle { @@ -546,8 +546,8 @@ onMounted(() => { top: 20px; width: 28px; height: 56px; - background: var(--app-surface-color, #fafafa); - border: 1px solid var(--n-border-color, #e0e0e0); + background: var(--app-surface); + border: 1px solid var(--app-border); border-right: none; border-radius: 6px 0 0 6px; display: flex; @@ -557,7 +557,8 @@ onMounted(() => { transition: background-color 0.2s, top 0.3s ease; - box-shadow: -2px 0 4px var(--n-box-shadow-color, rgba(0, 0, 0, 0.05)); + box-shadow: -2px 0 4px var(--app-shadow); + color: var(--app-text-secondary); } .notepad-container.collapsed .notepad-toggle { @@ -566,7 +567,8 @@ onMounted(() => { } .notepad-toggle:hover { - background: var(--app-body-color, #f0f0f0); + background: var(--app-surface-hover); + color: var(--app-text-primary); } .notepad-panel { @@ -574,13 +576,13 @@ onMounted(() => { display: flex; flex-direction: column; overflow: hidden; - background: var(--app-surface-color, #ffffff); + background: var(--app-surface); } .notepad-header { padding: 8px; - border-bottom: 1px solid var(--n-border-color, #e0e0e0); - background: var(--app-body-color, #fafafa); + border-bottom: 1px solid var(--app-border); + background: var(--app-surface-sunken); } .scope-switcher { @@ -593,7 +595,7 @@ onMounted(() => { flex: 1; padding: 8px; overflow-y: auto; - background: var(--app-surface-color, #ffffff); + background: var(--app-surface); } .tab-bar { @@ -615,11 +617,11 @@ onMounted(() => { align-items: center; gap: 4px; padding: 2px 8px; - border: 1px solid var(--n-border-color, #dedede); + border: 1px solid var(--app-border); border-radius: 4px; font-size: 12px; - background: var(--app-surface-color, #ffffff); - color: var(--n-text-color-2, #333333); + background: var(--app-surface); + color: var(--app-text-secondary); cursor: grab; user-select: none; transition: @@ -629,13 +631,9 @@ onMounted(() => { } .tab-item.active { - border-color: var(--n-primary-color, #1890ff); - color: var(--n-primary-color, #1890ff); - background: color-mix( - in srgb, - var(--n-primary-color, #1890ff) 8%, - var(--app-surface-color, #ffffff) - ); + border-color: var(--app-accent); + color: var(--app-accent); + background: var(--app-accent-soft); } .tab-item:active { @@ -660,7 +658,7 @@ onMounted(() => { } .tab-close-btn:hover { - color: var(--n-color-error, #ff4d4f); + color: var(--app-error); } .tab-ghost { @@ -676,7 +674,7 @@ onMounted(() => { align-items: center; gap: 6px; font-size: 12px; - color: var(--n-text-color-3, #8c8c8c); + color: var(--app-text-muted); margin-top: 6px; } @@ -684,6 +682,8 @@ onMounted(() => { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; font-size: 12px; line-height: 1.6; - background: var(--app-surface-color, #ffffff); + background: var(--app-surface); + color: var(--app-text-primary); + caret-color: var(--app-accent); } diff --git a/ui/src/components/project/ProjectBrowser.vue b/ui/src/components/project/ProjectBrowser.vue index ca0e1987..a42007e3 100644 --- a/ui/src/components/project/ProjectBrowser.vue +++ b/ui/src/components/project/ProjectBrowser.vue @@ -191,7 +191,7 @@ @@ -254,7 +254,10 @@ v-if="project.priority" size="small" :bordered="false" - :color="{ color: getPriorityTagColor(project.priority), textColor: '#fff' }" + :color="{ + color: getPriorityTagColor(project.priority), + textColor: getReadableTextColor(getPriorityTagColor(project.priority)), + }" > {{ row.projectBadge.label }} @@ -61,6 +64,7 @@ import { nextTick, onBeforeUnmount, ref } from 'vue'; import { NDropdown, NIcon, type DropdownOption } from 'naive-ui'; import { EllipsisHorizontal } from '@vicons/ionicons5'; import { useLocale } from '@/composables/useLocale'; +import { getReadableTextColor } from '@/utils/color'; export interface TerminalSidebarRowView { id: string; @@ -139,7 +143,7 @@ onBeforeUnmount(closeMenu); diff --git a/ui/src/components/workspace/DockedNotificationSidebar.vue b/ui/src/components/workspace/DockedNotificationSidebar.vue index a1d9eaf8..c6bb4de1 100644 --- a/ui/src/components/workspace/DockedNotificationSidebar.vue +++ b/ui/src/components/workspace/DockedNotificationSidebar.vue @@ -493,8 +493,9 @@ watch( overflow: hidden; display: flex; flex-direction: column; - border-left: 1px solid var(--n-border-color); - background: var(--app-surface-color, var(--n-card-color, #fff)); + border-left: 1px solid var(--app-border, var(--n-border-color)); + background: var(--app-surface, var(--app-surface-color, #fff)); + color: var(--app-text-primary, var(--n-text-color-1)); } .terminal-sidebar-header { @@ -504,7 +505,8 @@ watch( justify-content: space-between; gap: 8px; padding: 0 10px; - border-bottom: 1px solid color-mix(in srgb, var(--n-border-color) 70%, transparent); + border-bottom: 1px solid + color-mix(in srgb, var(--app-border, var(--n-border-color)) 70%, transparent); } .terminal-sidebar-title-wrap { @@ -559,8 +561,8 @@ watch( justify-content: center; padding: 0 4px; border-radius: 5px; - background: color-mix(in srgb, var(--n-primary-color) 12%, transparent); - color: var(--n-primary-color); + background: var(--app-accent-soft, color-mix(in srgb, var(--n-primary-color) 12%, transparent)); + color: var(--app-accent, var(--n-primary-color)); font-size: 10px; font-variant-numeric: tabular-nums; } @@ -569,7 +571,7 @@ watch( display: inline-flex; align-items: center; justify-content: center; - color: var(--n-text-color-3); + color: var(--app-text-muted, var(--n-text-color-3)); animation: terminal-sidebar-spin 0.9s linear infinite; } @@ -589,13 +591,13 @@ watch( border: 0; border-radius: 5px; background: transparent; - color: var(--n-text-color-3); + color: var(--app-text-muted, var(--n-text-color-3)); cursor: pointer; } .terminal-sidebar-reset:hover { - background: color-mix(in srgb, var(--n-border-color) 45%, transparent); - color: var(--n-text-color-1); + background: var(--app-surface-hover, color-mix(in srgb, var(--n-border-color) 45%, transparent)); + color: var(--app-text-primary, var(--n-text-color-1)); } .terminal-sidebar-list { @@ -613,7 +615,7 @@ watch( display: flex; align-items: center; justify-content: center; - color: var(--n-text-color-3); + color: var(--app-text-muted, var(--n-text-color-3)); font-size: 12px; } @@ -644,13 +646,13 @@ watch( .terminal-resizer:hover .resizer-handle { height: 40px; - background: var(--n-border-color, #d0d0d0); + background: var(--app-border-strong, #d0d0d0); opacity: 1; } .terminal-resizer.is-dragging .resizer-handle { height: 60px; - background: var(--n-primary-color, #3b82f6); + background: var(--app-accent, #3b82f6); opacity: 1; } diff --git a/ui/src/components/workspace/WorkspaceTabView.vue b/ui/src/components/workspace/WorkspaceTabView.vue index 5d5a53f5..c85fbdff 100644 --- a/ui/src/components/workspace/WorkspaceTabView.vue +++ b/ui/src/components/workspace/WorkspaceTabView.vue @@ -828,6 +828,8 @@ if (typeof window !== 'undefined') { flex-direction: column; height: 100%; overflow: hidden; + background: var(--app-canvas); + color: var(--app-text-primary); } .tab-header { @@ -836,8 +838,8 @@ if (typeof window !== 'undefined') { justify-content: space-between; padding: 0 12px; height: 40px; - border-bottom: 1px solid var(--n-border-color, var(--app-input-border-color, rgba(0, 0, 0, 0.12))); - background-color: var(--app-surface-color, var(--n-card-color, #ffffff)); + border-bottom: 1px solid var(--app-border); + background-color: var(--app-surface); flex-shrink: 0; } @@ -859,37 +861,37 @@ if (typeof window !== 'undefined') { border: none; border-radius: 6px; background: transparent; - color: var(--n-text-color-2); + color: var(--app-text-secondary); font-size: 13px; cursor: pointer; transition: all 0.2s ease; } .tab-item:disabled { - color: var(--n-text-color-disabled, rgba(15, 23, 42, 0.38)); + color: var(--app-text-muted); cursor: not-allowed; opacity: 0.6; } .tab-item:hover { - background-color: var(--n-color-hover); - color: var(--n-text-color); + background-color: var(--app-surface-hover); + color: var(--app-text-primary); } .tab-item:disabled:hover { background: transparent; - color: var(--n-text-color-disabled, rgba(15, 23, 42, 0.38)); + color: var(--app-text-muted); } .tab-item.active { - background-color: var(--n-color-target); - color: var(--n-primary-color); + background-color: var(--app-accent-soft); + color: var(--app-accent); font-weight: 500; } .tab-item:focus-visible { outline: none; - box-shadow: 0 0 0 2px var(--n-primary-color); + box-shadow: 0 0 0 2px var(--app-accent); } .tab-label { @@ -904,8 +906,8 @@ if (typeof window !== 'undefined') { height: 18px; padding: 0 5px; border-radius: 9px; - background-color: var(--n-primary-color); - color: #fff; + background-color: var(--app-accent); + color: var(--app-accent-contrast); font-size: 11px; font-weight: 500; } @@ -920,19 +922,19 @@ if (typeof window !== 'undefined') { min-width: auto; padding: 0 7px; font-variant-numeric: tabular-nums; - background: rgba(37, 90, 143, 0.12); - color: var(--n-text-color); + background: var(--app-accent-soft); + color: var(--app-text-primary); gap: 0; } .changes-summary-warning { margin-left: 3px; - color: #b45309; + color: var(--app-warning); } .changes-summary-loading { margin-left: 3px; - color: var(--n-primary-color); + color: var(--app-accent); animation: changes-summary-spin 1s linear infinite; } @@ -943,40 +945,40 @@ if (typeof window !== 'undefined') { } .tab-item.active .changes-summary-badge { - background: rgba(37, 90, 143, 0.16); - color: var(--n-primary-color); + background: var(--app-surface-raised); + color: var(--app-accent); } .changes-summary-count { - color: rgba(43, 59, 81, 0.9); + color: var(--app-text-primary); } .changes-summary-separator { - color: rgba(43, 59, 81, 0.78); + color: var(--app-text-muted); } .changes-summary-add { - color: #15803d; + color: var(--app-success); } .changes-summary-del { - color: #dc2626; + color: var(--app-error); } .tab-item.active .changes-summary-count { - color: rgba(31, 76, 127, 0.92); + color: var(--app-text-primary); } .tab-item.active .changes-summary-separator { - color: rgba(31, 76, 127, 0.72); + color: var(--app-text-muted); } .tab-item.active .changes-summary-add { - color: #15803d; + color: var(--app-success); } .tab-item.active .changes-summary-del { - color: #dc2626; + color: var(--app-error); } .header-action-btn { @@ -985,7 +987,7 @@ if (typeof window !== 'undefined') { border: none; border-radius: 8px; background-color: transparent; - color: var(--n-text-color-2); + color: var(--app-text-secondary); cursor: pointer; display: inline-flex; align-items: center; @@ -1000,19 +1002,19 @@ if (typeof window !== 'undefined') { } .header-action-btn:hover { - color: var(--n-text-color); - background-color: var(--n-color-hover); + color: var(--app-text-primary); + background-color: var(--app-surface-hover); opacity: 1; } .header-action-btn[aria-pressed='true'] { - color: var(--n-text-color); + color: var(--app-text-primary); background-color: transparent; opacity: 0.94; } .header-action-btn[aria-pressed='true']:hover { - color: var(--n-primary-color); + color: var(--app-accent); } .terminal-project-switch { @@ -1041,7 +1043,7 @@ if (typeof window !== 'undefined') { } .header-action-btn:focus-visible { - outline: 2px solid var(--n-primary-color); + outline: 2px solid var(--app-accent); outline-offset: 2px; } @@ -1073,11 +1075,11 @@ if (typeof window !== 'undefined') { .changes-pane, .files-pane { - background-color: var(--app-surface-color, #ffffff); + background-color: var(--app-surface); } .web-main { - background: linear-gradient(180deg, rgba(246, 241, 232, 0.78), rgba(255, 255, 255, 0.94)); + background: var(--app-canvas); } .terminal-split { @@ -1093,7 +1095,7 @@ if (typeof window !== 'undefined') { min-width: 0; min-height: 0; overflow: hidden; - border: 1px solid var(--n-border-color); + border: 1px solid var(--app-border); border-radius: 8px; } diff --git a/ui/src/constants/themes.ts b/ui/src/constants/themes.ts index 312bb2a2..1043a043 100644 --- a/ui/src/constants/themes.ts +++ b/ui/src/constants/themes.ts @@ -5,6 +5,7 @@ export interface ThemePreset { name: string; nameEn: string; isDark: boolean; + maintenance: 'supported' | 'preview'; colors: ThemeSettings; terminalThemeId: string; } @@ -15,6 +16,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: '亮色', nameEn: 'Light', isDark: false, + maintenance: 'supported', terminalThemeId: 'light', colors: { primaryColor: '#3B69A9', @@ -39,6 +41,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: '暗色', nameEn: 'Dark', isDark: true, + maintenance: 'supported', terminalThemeId: 'nord', colors: { primaryColor: '#007ACC', @@ -61,6 +64,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: '柔和暗色', nameEn: 'Dim', isDark: true, + maintenance: 'preview', terminalThemeId: 'github-dark', colors: { primaryColor: '#539BF5', @@ -83,6 +87,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: '暖色', nameEn: 'Warm', isDark: false, + maintenance: 'preview', terminalThemeId: 'warm-light', colors: { primaryColor: '#D97706', @@ -105,6 +110,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Material 暗色', nameEn: 'Material Dark', isDark: true, + maintenance: 'preview', terminalThemeId: 'material-dark', colors: { primaryColor: '#BB86FC', @@ -127,6 +133,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Xcode 暗色', nameEn: 'Xcode Dark', isDark: true, + maintenance: 'preview', terminalThemeId: 'xcode-dark', colors: { primaryColor: '#0A84FF', @@ -149,6 +156,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Solarized 亮色', nameEn: 'Solarized Light', isDark: false, + maintenance: 'preview', terminalThemeId: 'solarized-light', colors: { primaryColor: '#268BD2', @@ -171,6 +179,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Solarized 暗色', nameEn: 'Solarized Dark', isDark: true, + maintenance: 'preview', terminalThemeId: 'solarized-dark', colors: { primaryColor: '#859900', @@ -193,6 +202,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Nord', nameEn: 'Nord', isDark: true, + maintenance: 'preview', terminalThemeId: 'nord', colors: { primaryColor: '#88C0D0', @@ -215,6 +225,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Dracula', nameEn: 'Dracula', isDark: true, + maintenance: 'preview', terminalThemeId: 'dracula', colors: { primaryColor: '#BD93F9', @@ -237,6 +248,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'One Dark', nameEn: 'One Dark', isDark: true, + maintenance: 'preview', terminalThemeId: 'one-dark', colors: { primaryColor: '#61AFEF', @@ -259,6 +271,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Tokyo Night', nameEn: 'Tokyo Night', isDark: true, + maintenance: 'preview', terminalThemeId: 'tokyo-night', colors: { primaryColor: '#7AA2F7', @@ -281,6 +294,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'GitHub 暗色', nameEn: 'GitHub Dark', isDark: true, + maintenance: 'preview', terminalThemeId: 'github-dark', colors: { primaryColor: '#58A6FF', @@ -303,6 +317,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Monokai', nameEn: 'Monokai', isDark: true, + maintenance: 'preview', terminalThemeId: 'monokai', colors: { primaryColor: '#A6E22E', @@ -325,6 +340,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Gruvbox 暗色', nameEn: 'Gruvbox Dark', isDark: true, + maintenance: 'preview', terminalThemeId: 'gruvbox-dark', colors: { primaryColor: '#FABD2F', @@ -347,6 +363,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Catppuccin', nameEn: 'Catppuccin Mocha', isDark: true, + maintenance: 'preview', terminalThemeId: 'catppuccin', colors: { primaryColor: '#CBA6F7', @@ -369,6 +386,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'GitHub 亮色', nameEn: 'GitHub Light', isDark: false, + maintenance: 'preview', terminalThemeId: 'github-light', colors: { primaryColor: '#0969DA', @@ -391,6 +409,7 @@ export const THEME_PRESETS: ThemePreset[] = [ name: 'Gruvbox 亮色', nameEn: 'Gruvbox Light', isDark: false, + maintenance: 'preview', terminalThemeId: 'gruvbox-light', colors: { primaryColor: '#B57614', @@ -416,6 +435,10 @@ export function getPresetById(id: string): ThemePreset | undefined { return THEME_PRESETS.find(preset => preset.id === id); } +export function isSupportedThemePreset(id: string): boolean { + return getPresetById(id)?.maintenance === 'supported'; +} + export function getDefaultPreset(): ThemePreset { return getPresetById(DEFAULT_PRESET_ID) || THEME_PRESETS[0]; } diff --git a/ui/src/i18n/locales/en-US.ts b/ui/src/i18n/locales/en-US.ts index 204dfe51..982f57ef 100644 --- a/ui/src/i18n/locales/en-US.ts +++ b/ui/src/i18n/locales/en-US.ts @@ -1637,9 +1637,9 @@ export default { 'Default behaves the same as No and only indicates no explicit choice yet; choosing Yes will switch themes based on system dark/light mode', maintenanceWarningTitle: 'Theme Warning', maintenanceWarningPreset: - 'Except for the default Light theme, all other themes are lightly maintained and may produce unstable visuals. Continue switching?', + 'Themes other than Light and Dark are previews and may produce unstable visuals. Continue switching?', maintenanceWarningFollowSystem: - 'Enabling Follow System Theme may switch the app to a non-default theme. Except for the default Light theme, all other themes are lightly maintained and may produce unstable visuals. Continue?', + 'Follow System Theme automatically switches between the supported Light and Dark themes. Continue?', customTheme: 'Custom Theme', customThemeHint: 'Customizing colors will override preset theme settings', terminalColors: 'Terminal Colors', diff --git a/ui/src/i18n/locales/zh-CN.ts b/ui/src/i18n/locales/zh-CN.ts index deb8b3ff..2ff99f78 100644 --- a/ui/src/i18n/locales/zh-CN.ts +++ b/ui/src/i18n/locales/zh-CN.ts @@ -1558,9 +1558,9 @@ export default { '默认等同关闭,仅表示未显式设置;选择“是”后将根据系统深色/浅色模式自动切换主题', maintenanceWarningTitle: '主题警告', maintenanceWarningPreset: - '除默认亮色主题外,其余主题均缺乏维护,视觉效果不稳定。是否继续切换?', + '除亮色和暗色主题外,其余主题均为预览主题,视觉效果可能不稳定。是否继续切换?', maintenanceWarningFollowSystem: - '开启跟随系统主题后,应用可能切换到非默认主题。除默认亮色主题外,其余主题均缺乏维护,视觉效果不稳定。是否继续开启?', + '开启跟随系统主题后,应用将在受支持的亮色与暗色主题之间自动切换。是否继续开启?', customTheme: '自定义主题', customThemeHint: '自定义颜色后将覆盖预设主题配置', terminalColors: '终端颜色', diff --git a/ui/src/styles/chat-markdown.css b/ui/src/styles/chat-markdown.css index ed36f7ac..c0a593b9 100644 --- a/ui/src/styles/chat-markdown.css +++ b/ui/src/styles/chat-markdown.css @@ -1,5 +1,5 @@ .chat-markdown { - color: var(--app-text-color, var(--n-text-color-1, #111827)); + color: var(--app-text-primary, var(--app-text-color, #111827)); font-size: 14px; line-height: 1.72; word-break: break-word; @@ -8,7 +8,10 @@ .chat-markdown .markdown-search-highlight { padding: 0 0.12em; border-radius: 2px; - background: color-mix(in srgb, var(--n-warning-color, #f0a020) 48%, transparent); + background: var( + --app-warning-soft, + color-mix(in srgb, var(--n-warning-color, #f0a020) 48%, transparent) + ); color: inherit; } @@ -33,7 +36,7 @@ margin: 1.2em 0 0.55em; line-height: 1.3; font-weight: 700; - color: var(--app-text-color, var(--n-text-color-1, #111827)); + color: var(--app-text-primary, var(--app-text-color, #111827)); } .chat-markdown h1 { @@ -101,26 +104,32 @@ .chat-markdown .task-list-item input[type='checkbox'] { margin: 0.28em 0.6em 0 0; - accent-color: var(--n-primary-color); + accent-color: var(--app-accent, var(--n-primary-color)); } .chat-markdown blockquote { margin: 1em 0; padding: 0.2em 0 0.2em 14px; - border-left: 3px solid color-mix(in srgb, var(--n-primary-color) 28%, var(--n-border-color)); + border-left: 3px solid + color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 28%, + var(--app-border, var(--n-border-color)) + ); border-radius: 0 12px 12px 0; - color: var(--n-text-color-3); - background: color-mix(in srgb, var(--n-primary-color) 7%, transparent); + color: var(--app-text-muted, var(--n-text-color-3)); + background: color-mix(in srgb, var(--app-accent-soft, transparent) 70%, transparent); } .chat-markdown a { - color: color-mix(in srgb, var(--n-primary-color) 84%, #0f4ea8); + color: var(--app-link, var(--app-info, var(--n-primary-color))); text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 36%, transparent); text-underline-offset: 0.16em; } .chat-markdown a:hover { + color: var(--app-link-hover, var(--app-link, var(--n-primary-color))); text-decoration-color: currentColor; } @@ -138,10 +147,19 @@ justify-content: center; min-height: 22px; padding: 0 8px; - border: 1px solid color-mix(in srgb, var(--n-primary-color) 18%, var(--n-border-color)); + border: 1px solid + color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 18%, + var(--app-border, var(--n-border-color)) + ); border-radius: 999px; - background: color-mix(in srgb, var(--n-primary-color) 9%, var(--app-surface-color, #fff)); - color: var(--n-text-color-2); + background: color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 9%, + var(--app-surface-raised, #fff) + ); + color: var(--app-text-secondary, var(--n-text-color-2)); font-size: 11px; font-weight: 600; line-height: 1; @@ -154,24 +172,34 @@ } .chat-markdown .message-link-copy-button:hover { - border-color: color-mix(in srgb, var(--n-primary-color) 28%, var(--n-border-color)); - background: color-mix(in srgb, var(--n-primary-color) 16%, var(--app-surface-color, #fff)); - color: var(--n-primary-color); + border-color: color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 28%, + var(--app-border, var(--n-border-color)) + ); + background: color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 16%, + var(--app-surface-raised, #fff) + ); + color: var(--app-focus-ring, var(--n-primary-color)); } .chat-markdown .message-link-copy-button:focus-visible { - outline: 2px solid color-mix(in srgb, var(--n-primary-color) 35%, transparent); + outline: 2px solid + color-mix(in srgb, var(--app-focus-ring, var(--n-primary-color)) 58%, transparent); outline-offset: 2px; } .chat-markdown hr { margin: 1.2em 0; border: none; - border-top: 1px solid color-mix(in srgb, var(--n-border-color) 82%, transparent); + border-top: 1px solid + color-mix(in srgb, var(--app-border, var(--n-border-color)) 82%, transparent); } .chat-markdown :not(pre) > code { - color: color-mix(in srgb, var(--n-info-color, #2563eb) 86%, #0f4ea8); + color: var(--app-info, var(--n-info-color, #2563eb)); font-family: 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.92em; } @@ -183,14 +211,24 @@ .chat-markdown pre.markdown-code-block { position: relative; - border: 1px solid color-mix(in srgb, var(--n-primary-color) 14%, var(--n-border-color)); + border: 1px solid + color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 14%, + var(--app-border, var(--n-border-color)) + ); border-radius: 14px; background: linear-gradient( 180deg, - color-mix(in srgb, var(--app-surface-color, #fff) 92%, var(--n-primary-color) 8%), - color-mix(in srgb, var(--app-surface-color, #fff) 97%, black 3%) + color-mix( + in srgb, + var(--app-surface-raised, #fff) 92%, + var(--app-accent, var(--n-primary-color)) 8% + ), + var(--app-surface-sunken, color-mix(in srgb, var(--app-surface-color, #fff) 97%, black 3%)) ); - box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06); + box-shadow: 0 10px 28px + color-mix(in srgb, var(--app-shadow, rgba(15, 23, 42, 0.06)) 62%, transparent); } .chat-markdown .markdown-code-copy-button { @@ -201,7 +239,7 @@ padding: 0; border: none; background: transparent; - color: var(--n-text-color-2); + color: var(--app-text-secondary, var(--n-text-color-2)); font-size: 10px; line-height: 1; font-weight: 600; @@ -213,7 +251,7 @@ } .chat-markdown .markdown-code-copy-button:hover { - color: var(--n-primary-color); + color: var(--app-focus-ring, var(--n-primary-color)); opacity: 1; } @@ -221,7 +259,7 @@ content: attr(data-language); display: block; padding: 10px 14px 0; - color: var(--n-text-color-3); + color: var(--app-text-muted, var(--n-text-color-3)); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; @@ -237,7 +275,7 @@ min-width: max-content; padding: 12px 14px 14px; background: transparent; - color: var(--app-text-color, var(--n-text-color-1, #111827)); + color: var(--app-text-primary, var(--app-text-color, #111827)); font-family: 'Fira Code', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 13px; line-height: 1.68; @@ -256,19 +294,29 @@ margin: 1em 0; border-collapse: separate; border-spacing: 0; - border: 1px solid color-mix(in srgb, var(--n-primary-color) 12%, var(--n-border-color)); + border: 1px solid + color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 12%, + var(--app-border, var(--n-border-color)) + ); border-radius: 12px; overflow: hidden; } .chat-markdown thead { - background: color-mix(in srgb, var(--n-primary-color) 8%, transparent); + background: color-mix(in srgb, var(--app-accent-soft, transparent) 74%, transparent); } .chat-markdown th, .chat-markdown td { padding: 10px 12px; - border-bottom: 1px solid color-mix(in srgb, var(--n-primary-color) 8%, var(--n-border-color)); + border-bottom: 1px solid + color-mix( + in srgb, + var(--app-accent, var(--n-primary-color)) 8%, + var(--app-border, var(--n-border-color)) + ); text-align: left; vertical-align: top; } @@ -288,7 +336,7 @@ .chat-markdown .hljs-comment, .chat-markdown .hljs-quote { - color: color-mix(in srgb, var(--n-text-color-3) 92%, transparent); + color: var(--app-text-muted, var(--n-text-color-3)); font-style: italic; } @@ -297,7 +345,7 @@ .chat-markdown .hljs-literal, .chat-markdown .hljs-section, .chat-markdown .hljs-link { - color: color-mix(in srgb, var(--n-primary-color) 82%, #0f4ea8); + color: var(--app-link, var(--app-info, var(--n-primary-color))); } .chat-markdown .hljs-string, @@ -306,7 +354,7 @@ .chat-markdown .hljs-attr, .chat-markdown .hljs-attribute, .chat-markdown .hljs-template-tag { - color: color-mix(in srgb, var(--n-success-color) 76%, #0f766e); + color: var(--app-success, var(--n-success-color)); } .chat-markdown .hljs-number, @@ -315,30 +363,38 @@ .chat-markdown .hljs-variable, .chat-markdown .hljs-built_in, .chat-markdown .hljs-builtin-name { - color: color-mix(in srgb, var(--n-warning-color) 78%, #b45309); + color: var(--app-warning, var(--n-warning-color)); } .chat-markdown .hljs-type, .chat-markdown .hljs-class, .chat-markdown .hljs-function, .chat-markdown .hljs-title.function_ { - color: color-mix(in srgb, var(--app-text-color, #111827) 72%, var(--n-primary-color) 28%); + color: color-mix( + in srgb, + var(--app-text-primary, #111827) 72%, + var(--app-info, var(--n-primary-color)) 28% + ); } .chat-markdown .hljs-meta, .chat-markdown .hljs-regexp, .chat-markdown .hljs-operator { - color: color-mix(in srgb, var(--n-info-color, #2563eb) 72%, var(--n-text-color-2) 28%); + color: color-mix( + in srgb, + var(--app-info, var(--n-info-color, #2563eb)) 72%, + var(--app-text-secondary, var(--n-text-color-2)) 28% + ); } .chat-markdown .hljs-addition { - color: color-mix(in srgb, var(--n-success-color) 84%, #166534); - background: color-mix(in srgb, var(--n-success-color) 12%, transparent); + color: var(--app-success, var(--n-success-color)); + background: var(--app-success-soft, color-mix(in srgb, var(--n-success-color) 12%, transparent)); } .chat-markdown .hljs-deletion { - color: color-mix(in srgb, var(--n-error-color) 84%, #991b1b); - background: color-mix(in srgb, var(--n-error-color) 12%, transparent); + color: var(--app-error, var(--n-error-color)); + background: var(--app-error-soft, color-mix(in srgb, var(--n-error-color) 12%, transparent)); } .chat-markdown .hljs-emphasis { diff --git a/ui/src/styles/project-badges.css b/ui/src/styles/project-badges.css index 8e06f203..abc473d4 100644 --- a/ui/src/styles/project-badges.css +++ b/ui/src/styles/project-badges.css @@ -32,6 +32,25 @@ 0 4px 10px color-mix(in srgb, var(--project-badge-color) 20%, transparent); } +[data-color-scheme='dark'] .project-index-badge { + background: + radial-gradient( + circle at 30% 25%, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.08) 34%, + transparent 58% + ), + linear-gradient( + 135deg, + color-mix(in srgb, var(--project-badge-color) 64%, #111827 36%) 0%, + color-mix(in srgb, var(--project-badge-color) 54%, #111827 46%) 100% + ); + border-color: color-mix(in srgb, white 58%, var(--project-badge-color)); + box-shadow: + 0 1px 2px var(--app-shadow, rgba(15, 23, 42, 0.16)), + 0 4px 10px color-mix(in srgb, var(--project-badge-color) 20%, transparent); +} + .project-index-badge.is-single-project { visibility: hidden; pointer-events: none; diff --git a/ui/src/styles/variables.css b/ui/src/styles/variables.css index ff38085d..9de65d8d 100644 --- a/ui/src/styles/variables.css +++ b/ui/src/styles/variables.css @@ -1,7 +1,7 @@ /** * 全局 CSS 变量声明 * 这个文件定义了项目中使用的所有自定义 CSS 变量 - * 这些变量会被 Vue 的 useCssVars 在运行时动态更新 + * 这些变量会由 App.vue 在运行时动态更新 */ :root { @@ -12,6 +12,40 @@ --app-surface-color: #ffffff; --app-text-color: #333333; + /* 应用语义色。运行时由 App.vue 根据当前主题覆盖。 */ + --app-canvas: var(--app-body-color); + --app-surface: var(--app-surface-color); + --app-surface-raised: #ffffff; + --app-surface-sunken: #f2f3f5; + --app-surface-hover: #f9f9f9; + --app-surface-active: #f1f1f1; + --app-text-primary: var(--app-text-color); + --app-text-secondary: rgba(51, 51, 51, 0.74); + --app-text-muted: rgba(51, 51, 51, 0.68); + --app-text-inverse: #ffffff; + --app-border: #e0e0e0; + --app-border-strong: #b8bcc8; + --app-control-border: #d0d5dd; + --app-control-border-hover: #b8bcc8; + --app-focus-ring: #3b69a9; + --app-accent: #3b69a9; + --app-accent-hover: #4f7dbd; + --app-accent-pressed: #1c4a8a; + --app-accent-soft: rgba(59, 105, 169, 0.1); + --app-accent-contrast: #ffffff; + --app-link: #255a8f; + --app-link-hover: #1f4c7f; + --app-success: #137333; + --app-success-soft: rgba(19, 115, 51, 0.1); + --app-warning: #8a5d00; + --app-warning-soft: rgba(138, 93, 0, 0.1); + --app-error: #b42318; + --app-error-soft: rgba(180, 35, 24, 0.1); + --app-info: #255a8f; + --app-info-soft: rgba(37, 90, 143, 0.1); + --app-overlay: rgba(15, 23, 42, 0.38); + --app-shadow: rgba(24, 35, 51, 0.12); + /* 终端颜色 */ --kanban-terminal-bg: #0f111a; --kanban-terminal-fg: #f6f8ff; @@ -26,54 +60,59 @@ --terminal-overlay-color: #f6f8ff; /* 输入框边框颜色 */ - --app-input-border-color: #d0d5dd; - --app-input-border-hover-color: #b8bcc8; + --app-input-border-color: var(--app-control-border); + --app-input-border-hover-color: var(--app-control-border-hover); /* 预览面板颜色 */ --preview-banner-bg: #3b69a9; --preview-banner-text: #ffffff; - --preview-panel-bg: #ffffff; - --preview-content-bg: #ffffff; - --preview-content-text: #333333; + --preview-panel-bg: var(--app-surface); + --preview-content-bg: var(--app-surface-raised); + --preview-content-text: var(--app-text-primary); /* ==================== Naive UI 变量 ==================== */ /* 这些变量由 Naive UI 的 n-config-provider 和 n-global-style 注入 */ /* 在这里声明是为了让 IDE 识别,避免警告 */ /* 主色调 */ - --n-primary-color: #3b69a9; - --n-primary-color-hover: #4a7ab8; - --n-primary-color-pressed: #2f5689; - --n-primary-color-suppl: #4a7ab8; + --n-primary-color: var(--app-accent); + --n-color-primary: var(--app-accent); + --n-primary-color-hover: var(--app-accent-hover); + --n-primary-color-pressed: var(--app-accent-pressed); + --n-primary-color-suppl: var(--app-accent-hover); /* 文本颜色 */ - --n-text-color-base: #333333; - --n-text-color-1: #333333; - --n-text-color-2: #666666; - --n-text-color-3: #999999; + --n-text-color-base: var(--app-text-primary); + --n-text-color: var(--app-text-primary); + --n-text-color-1: var(--app-text-primary); + --n-text-color-2: var(--app-text-secondary); + --n-text-color-3: var(--app-text-muted); /* 背景颜色 */ - --n-body-color: #f7f8fa; - --n-color: #ffffff; - --n-color-hover: #f5f5f5; - --n-card-color: #ffffff; - --n-modal-color: #ffffff; - --n-popover-color: #ffffff; + --n-body-color: var(--app-canvas); + --n-color: var(--app-surface); + --n-color-hover: var(--app-surface-hover); + --n-color-target: var(--app-accent-soft); + --n-color-embedded: var(--app-surface-sunken); + --n-card-color: var(--app-surface); + --n-modal-color: var(--app-surface-raised); + --n-popover-color: var(--app-surface-raised); /* 边框颜色 */ - --n-border-color: #e0e0e0; + --n-border-color: var(--app-border); + --n-divider-color: var(--app-border); /* 阴影颜色 */ - --n-box-shadow-color: rgba(0, 0, 0, 0.12); + --n-box-shadow-color: var(--app-shadow); /* Tab 颜色 */ - --n-tab-color: #ffffff; + --n-tab-color: var(--app-surface); /* 功能色 */ - --n-error-color: #f5222d; - --n-warning-color: #fa8c16; - --n-success-color: #52c41a; - --n-info-color: #3b69a9; + --n-error-color: var(--app-error); + --n-warning-color: var(--app-warning); + --n-success-color: var(--app-success); + --n-info-color: var(--app-info); } /* ==================== 暗色主题变量参考 ==================== */ @@ -89,8 +128,8 @@ --app-text-color: #d4d4d4 --kanban-terminal-bg: #0f111a --kanban-terminal-fg: #f6f8ff - --app-input-border-color: #4b4b4b - --app-input-border-hover-color: #5a5a5a + --app-input-border-color: #686868 + --app-input-border-hover-color: #878787 Naive UI 暗色主题变量由 darkTheme 自动提供 */ diff --git a/ui/src/utils/themeMaintenanceWarning.ts b/ui/src/utils/themeMaintenanceWarning.ts index 83e1bb82..cdba09ca 100644 --- a/ui/src/utils/themeMaintenanceWarning.ts +++ b/ui/src/utils/themeMaintenanceWarning.ts @@ -27,6 +27,8 @@ interface ThemeSelectionControllerOptions { toggleFollowSystemTheme: (enabled: boolean) => void; confirmPresetThemeChange: () => Promise; confirmFollowSystemEnable: () => Promise; + shouldConfirmPresetThemeChange?: (presetId: string) => boolean; + shouldConfirmFollowSystemEnable?: () => boolean; } function createThemeMaintenanceWarningPromise( @@ -86,6 +88,8 @@ export function createThemeSelectionController({ toggleFollowSystemTheme, confirmPresetThemeChange, confirmFollowSystemEnable, + shouldConfirmPresetThemeChange = presetId => presetId !== DEFAULT_PRESET_ID, + shouldConfirmFollowSystemEnable = () => true, }: ThemeSelectionControllerOptions) { async function selectPresetWithConfirmation(presetId: string) { if (!presetId) { @@ -94,11 +98,7 @@ export function createThemeSelectionController({ if (presetId === getCurrentPresetId() && !isFollowSystemTheme()) { return false; } - if (presetId === DEFAULT_PRESET_ID) { - selectPreset(presetId); - return true; - } - if (!(await confirmPresetThemeChange())) { + if (shouldConfirmPresetThemeChange(presetId) && !(await confirmPresetThemeChange())) { return false; } selectPreset(presetId); @@ -113,7 +113,7 @@ export function createThemeSelectionController({ toggleFollowSystemTheme(false); return true; } - if (!(await confirmFollowSystemEnable())) { + if (shouldConfirmFollowSystemEnable() && !(await confirmFollowSystemEnable())) { return false; } toggleFollowSystemTheme(true); diff --git a/ui/src/utils/themeOverrides.ts b/ui/src/utils/themeOverrides.ts index 8d59a8a2..1990db30 100644 --- a/ui/src/utils/themeOverrides.ts +++ b/ui/src/utils/themeOverrides.ts @@ -1,6 +1,7 @@ import type { GlobalThemeOverrides } from 'naive-ui'; import type { ThemeSettings } from '@/stores/settings'; -import { darkenColor, lightenColor, isDarkHex } from './color'; +import { darkenColor, hexToRgba, lightenColor, isDarkHex } from './color'; +import { createThemeSemanticPalette } from './themeSemanticPalette'; /** * 根据主题设置创建 Naive UI 全局主题覆盖配置 @@ -23,19 +24,20 @@ export function createThemeOverrides( const primaryPressed = darkenColor(primaryColor, 0.12); const isDark = isDarkHex(bodyColor || '#ffffff'); + const palette = createThemeSemanticPalette(theme, resolvedTextColor); // 设置文字颜色(如果主题提供了 textColor 则使用,否则根据暗色/亮色自动设置) const primaryTextColor = resolvedTextColor; - const secondaryTextColor = isDark ? '#FFFFFFA6' : '#000000A6'; - const mutedTextColor = isDark ? '#FFFFFF73' : '#00000073'; + const secondaryTextColor = palette.textSecondary; + const mutedTextColor = palette.textMuted; return { common: { // 基础颜色 bodyColor, cardColor: surfaceColor, - modalColor: surfaceColor, - popoverColor: surfaceColor, + modalColor: palette.surfaceRaised, + popoverColor: palette.surfaceRaised, tableColor: surfaceColor, // 主色调配置 @@ -51,16 +53,16 @@ export function createThemeOverrides( textColor3: mutedTextColor, // 边框颜色 - borderColor: isDark ? '#3C3C3C' : '#E0E0E0', + borderColor: palette.border, // Tab 配置 - tabColor: isDark ? '#262626' : '#FFFFFF', + tabColor: palette.surface, // 功能色配置 - errorColor: '#F5222D', - warningColor: '#FA8C16', - successColor: '#52C41A', - infoColor: primaryColor, + errorColor: palette.error, + warningColor: palette.warning, + successColor: palette.success, + infoColor: palette.info, }, Layout: { color: surfaceColor, @@ -70,47 +72,47 @@ export function createThemeOverrides( textColor: primaryTextColor, }, Input: { - color: isDark ? '#1C1C1C' : '#FFFFFF', - colorFocus: isDark ? '#1C1C1C' : '#FFFFFF', + color: isDark ? palette.surfaceSunken : palette.surface, + colorFocus: isDark ? palette.surfaceSunken : palette.surface, textColor: primaryTextColor, placeholderColor: mutedTextColor, border: `1px solid ${inputBorderColor}`, borderHover: `1px solid ${inputBorderHoverColor}`, - borderFocus: `1px solid ${primaryColor}`, + borderFocus: `1px solid ${palette.focusRing}`, }, InputNumber: { - color: isDark ? '#1C1C1C' : '#FFFFFF', - colorFocus: isDark ? '#1C1C1C' : '#FFFFFF', + color: isDark ? palette.surfaceSunken : palette.surface, + colorFocus: isDark ? palette.surfaceSunken : palette.surface, textColor: primaryTextColor, border: `1px solid ${inputBorderColor}`, borderHover: `1px solid ${inputBorderHoverColor}`, - borderFocus: `1px solid ${primaryColor}`, + borderFocus: `1px solid ${palette.focusRing}`, }, Select: { peers: { InternalSelection: { - color: isDark ? '#1C1C1C' : '#FFFFFF', - colorActive: isDark ? '#1C1C1C' : '#FFFFFF', + color: isDark ? palette.surfaceSunken : palette.surface, + colorActive: isDark ? palette.surfaceSunken : palette.surface, textColor: primaryTextColor, placeholderColor: mutedTextColor, border: `1px solid ${inputBorderColor}`, borderHover: `1px solid ${inputBorderHoverColor}`, - borderActive: `1px solid ${primaryColor}`, - borderFocus: `1px solid ${primaryColor}`, + borderActive: `1px solid ${palette.focusRing}`, + borderFocus: `1px solid ${palette.focusRing}`, }, InternalSelectMenu: { - color: surfaceColor, + color: palette.surfaceRaised, optionTextColor: primaryTextColor, optionTextColorActive: primaryColor, - optionColorActive: isDark ? '#2A2A2A' : '#F5F5F5', + optionColorActive: palette.surfaceActive, }, }, }, Tag: { textColor: primaryTextColor, - color: isDark ? '#2A2A2A' : '#F5F5F5', - colorBordered: isDark ? '#2A2A2A' : '#F5F5F5', - border: isDark ? '1px solid #3C3C3C' : '1px solid #E0E0E0', + color: palette.surfaceHover, + colorBordered: palette.surfaceHover, + border: `1px solid ${palette.border}`, }, Button: { textColor: primaryTextColor, @@ -119,6 +121,9 @@ export function createThemeOverrides( textColorTextPressed: primaryPressed, textColorTextDisabled: mutedTextColor, colorPrimary: primaryColor, + textColorPrimary: palette.accentContrast, + textColorHoverPrimary: palette.accentContrast, + textColorPressedPrimary: palette.accentContrast, colorHoverPrimary: primaryHover, colorPressedPrimary: primaryPressed, borderPrimary: `1px solid ${primaryColor}`, @@ -128,24 +133,24 @@ export function createThemeOverrides( Scrollbar: { width: '8px', height: '8px', - color: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)', - colorHover: isDark ? 'rgba(255, 255, 255, 0.25)' : 'rgba(0, 0, 0, 0.25)', + color: hexToRgba(primaryTextColor, 0.18), + colorHover: hexToRgba(primaryTextColor, 0.3), }, Card: { color: surfaceColor, textColor: primaryTextColor, titleTextColor: primaryTextColor, - borderColor: isDark ? '#3C3C3C' : '#E0E0E0', + borderColor: palette.border, }, Divider: { - color: isDark ? '#3C3C3C' : '#E0E0E0', + color: palette.border, }, Popover: { - color: surfaceColor, + color: palette.surfaceRaised, textColor: primaryTextColor, }, Modal: { - color: surfaceColor, + color: palette.surfaceRaised, textColor: primaryTextColor, titleTextColor: primaryTextColor, }, diff --git a/ui/src/utils/themeSemanticPalette.ts b/ui/src/utils/themeSemanticPalette.ts new file mode 100644 index 00000000..30392a22 --- /dev/null +++ b/ui/src/utils/themeSemanticPalette.ts @@ -0,0 +1,86 @@ +import type { ThemeSettings } from '@/stores/settings'; +import { darkenColor, hexToRgba, isDarkHex, lightenColor } from './color'; + +export interface ThemeSemanticPalette { + colorScheme: 'light' | 'dark'; + canvas: string; + surface: string; + surfaceRaised: string; + surfaceSunken: string; + surfaceHover: string; + surfaceActive: string; + textPrimary: string; + textSecondary: string; + textMuted: string; + textInverse: string; + border: string; + borderStrong: string; + controlBorder: string; + controlBorderHover: string; + focusRing: string; + accent: string; + accentHover: string; + accentPressed: string; + accentSoft: string; + accentContrast: string; + link: string; + linkHover: string; + success: string; + successSoft: string; + warning: string; + warningSoft: string; + error: string; + errorSoft: string; + info: string; + infoSoft: string; + overlay: string; + shadow: string; +} + +export function createThemeSemanticPalette( + theme: ThemeSettings, + resolvedTextColor: string +): ThemeSemanticPalette { + const isDark = isDarkHex(theme.bodyColor || '#ffffff'); + const surface = theme.surfaceColor || (isDark ? '#252526' : '#ffffff'); + const accent = theme.primaryColor || (isDark ? '#007acc' : '#3b69a9'); + const textPrimary = resolvedTextColor || (isDark ? '#d4d4d4' : '#333333'); + + return { + colorScheme: isDark ? 'dark' : 'light', + canvas: theme.bodyColor, + surface, + surfaceRaised: lightenColor(surface, isDark ? 0.035 : 0.018), + surfaceSunken: isDark + ? darkenColor(theme.bodyColor, 0.025) + : darkenColor(theme.bodyColor, 0.018), + surfaceHover: isDark ? lightenColor(surface, 0.06) : darkenColor(surface, 0.025), + surfaceActive: isDark ? lightenColor(surface, 0.1) : darkenColor(surface, 0.055), + textPrimary, + textSecondary: hexToRgba(textPrimary, isDark ? 0.82 : 0.74), + textMuted: hexToRgba(textPrimary, isDark ? 0.66 : 0.68), + textInverse: isDark ? '#1e1e1e' : '#ffffff', + border: isDark ? '#3c3c3c' : '#e0e0e0', + borderStrong: isDark ? '#5a5a5a' : '#b8bcc8', + controlBorder: isDark ? '#686868' : '#d0d5dd', + controlBorderHover: isDark ? '#878787' : '#b8bcc8', + focusRing: isDark ? '#75beff' : accent, + accent, + accentHover: lightenColor(accent, 0.08), + accentPressed: darkenColor(accent, 0.12), + accentSoft: hexToRgba(accent, isDark ? 0.2 : 0.1), + accentContrast: isDarkHex(accent) ? '#ffffff' : '#1e1e1e', + link: isDark ? '#75beff' : '#255a8f', + linkHover: isDark ? '#9cdcfe' : '#1f4c7f', + success: isDark ? '#89d185' : '#137333', + successSoft: isDark ? 'rgba(137, 209, 133, 0.16)' : 'rgba(19, 115, 51, 0.1)', + warning: isDark ? '#cca700' : '#8a5d00', + warningSoft: isDark ? 'rgba(204, 167, 0, 0.16)' : 'rgba(138, 93, 0, 0.1)', + error: isDark ? '#f48771' : '#b42318', + errorSoft: isDark ? 'rgba(244, 135, 113, 0.16)' : 'rgba(180, 35, 24, 0.1)', + info: isDark ? '#75beff' : '#255a8f', + infoSoft: isDark ? 'rgba(117, 190, 255, 0.16)' : 'rgba(37, 90, 143, 0.1)', + overlay: isDark ? 'rgba(0, 0, 0, 0.62)' : 'rgba(15, 23, 42, 0.38)', + shadow: isDark ? 'rgba(0, 0, 0, 0.38)' : 'rgba(24, 35, 51, 0.12)', + }; +} diff --git a/ui/src/views/GeneralSettings.vue b/ui/src/views/GeneralSettings.vue index 8f6b817a..ecc3d19c 100644 --- a/ui/src/views/GeneralSettings.vue +++ b/ui/src/views/GeneralSettings.vue @@ -1950,6 +1950,7 @@ import { } from '@/stores/settings'; import type { WebSessionActivityDisplayMode } from '@/constants/webSessionActivityDisplayMode'; import { DEFAULT_EDITOR, EDITOR_OPTIONS, isEditorPreference } from '@/constants/editor'; +import { isSupportedThemePreset } from '@/constants/themes'; import { DEFAULT_TERMINAL_SNAPSHOT_INTERVAL_MS, TERMINAL_SNAPSHOT_INTERVAL_OPTIONS, @@ -2128,6 +2129,8 @@ const themeSelectionController = createThemeSelectionController({ toggleFollowSystemTheme: enabled => settingsStore.toggleFollowSystemTheme(enabled), confirmPresetThemeChange: themeWarningController.confirmPresetThemeChange, confirmFollowSystemEnable: themeWarningController.confirmFollowSystemEnable, + shouldConfirmPresetThemeChange: presetId => !isSupportedThemePreset(presetId), + shouldConfirmFollowSystemEnable: () => false, }); const settingsBackupExporting = ref(false); const settingsBackupImporting = ref(false); @@ -4416,7 +4419,7 @@ function formatShortcutLabel(event: KeyboardEvent) { border: none; border-radius: 10px; background: transparent; - color: var(--n-text-color-2); + color: var(--app-text-secondary); font-size: 14px; font-weight: 500; text-align: left; @@ -4428,14 +4431,14 @@ function formatShortcutLabel(event: KeyboardEvent) { } .settings-nav-item:hover { - background-color: var(--n-color-hover); - color: var(--n-text-color-1); + background-color: var(--app-surface-hover); + color: var(--app-text-primary); transform: translateX(2px); } .settings-nav-item.is-active { - background-color: var(--n-primary-color); - color: #ffffff; + background-color: var(--app-accent); + color: var(--app-accent-contrast); transform: translateX(0); } @@ -4447,7 +4450,7 @@ function formatShortcutLabel(event: KeyboardEvent) { width: 6px; height: 6px; border-radius: 50%; - background-color: #ffffff; + background-color: var(--app-accent-contrast); flex-shrink: 0; } @@ -4472,20 +4475,20 @@ function formatShortcutLabel(event: KeyboardEvent) { } .settings-card-shell.is-highlighted :deep(.n-card) { - box-shadow: 0 0 0 2px var(--n-primary-color); + box-shadow: 0 0 0 2px var(--app-focus-ring); } /* 表单样式 */ .form-tip { font-size: 13px; - color: var(--n-text-color-3); + color: var(--app-text-muted); margin-top: 6px; line-height: 1.5; } .form-error { font-size: 13px; - color: var(--n-error-color, #d03050); + color: var(--app-error); margin-top: 6px; line-height: 1.5; } @@ -4503,7 +4506,7 @@ function formatShortcutLabel(event: KeyboardEvent) { display: flex; align-items: flex-start; font-weight: 500; - color: var(--n-text-color-1); + color: var(--app-text-primary); font-size: 14px; padding-top: 6px; } @@ -4526,15 +4529,15 @@ function formatShortcutLabel(event: KeyboardEvent) { /* 卡片样式 */ :deep(.n-card) { - border: 1px solid var(--n-border-color); + border: 1px solid var(--app-border); border-radius: 16px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - background-color: var(--app-surface-color, var(--n-card-color)); + box-shadow: 0 1px 3px var(--app-shadow); + background-color: var(--app-surface); } :deep(.n-card-header) { padding: 18px 24px; - border-bottom: 1px solid var(--n-border-color); + border-bottom: 1px solid var(--app-border); } :deep(.n-card__content) { @@ -4559,12 +4562,12 @@ function formatShortcutLabel(event: KeyboardEvent) { .preview-panel { border-radius: 8px; overflow: hidden; - border: 1px solid var(--n-border-color); - background-color: var(--preview-panel-bg, var(--app-surface-color)); + border: 1px solid var(--app-border); + background-color: var(--preview-panel-bg, var(--app-surface)); } .preview-banner { - background-color: var(--preview-banner-bg, var(--n-primary-color)); + background-color: var(--preview-banner-bg, var(--app-accent)); color: var(--preview-banner-text, var(--kanban-terminal-fg)); padding: 12px; font-size: 14px; @@ -4573,7 +4576,7 @@ function formatShortcutLabel(event: KeyboardEvent) { .preview-content { padding: 16px; - background-color: var(--preview-content-bg, var(--app-surface-color)); + background-color: var(--preview-content-bg, var(--app-surface)); color: var(--preview-content-text, var(--kanban-terminal-fg)); } @@ -4601,7 +4604,7 @@ function formatShortcutLabel(event: KeyboardEvent) { .settings-backup-file-name { font-size: 13px; - color: var(--n-text-color-2); + color: var(--app-text-secondary); } .settings-backup-group, @@ -4619,25 +4622,25 @@ function formatShortcutLabel(event: KeyboardEvent) { .settings-backup-subgroup__title { font-size: 14px; font-weight: 600; - color: var(--n-text-color-1); + color: var(--app-text-primary); } .settings-backup-meta-label { font-size: 12px; - color: var(--n-text-color-3); + color: var(--app-text-muted); margin-bottom: 4px; } .settings-backup-meta-value { font-size: 14px; - color: var(--n-text-color-1); + color: var(--app-text-primary); font-weight: 500; } .settings-backup-section-title { font-size: 14px; font-weight: 600; - color: var(--n-text-color-1); + color: var(--app-text-primary); margin-bottom: 12px; } @@ -4648,23 +4651,23 @@ function formatShortcutLabel(event: KeyboardEvent) { } .settings-backup-section-item { - border: 1px solid var(--n-border-color); + border: 1px solid var(--app-border); border-radius: 10px; padding: 12px 14px; - background: var(--app-surface-color, var(--n-card-color)); + background: var(--app-surface); } .settings-backup-section-item__title { font-size: 14px; font-weight: 600; - color: var(--n-text-color-1); + color: var(--app-text-primary); } .settings-backup-section-item__meta, .settings-backup-section-item__keys { margin-top: 4px; font-size: 12px; - color: var(--n-text-color-3); + color: var(--app-text-muted); word-break: break-word; } @@ -4675,12 +4678,12 @@ function formatShortcutLabel(event: KeyboardEvent) { .shortcut-hint { font-size: 12px; - color: var(--n-text-color-3); + color: var(--app-text-muted); } .unit-label { font-size: 12px; - color: var(--n-text-color-3); + color: var(--app-text-muted); margin-left: 4px; } @@ -4734,10 +4737,10 @@ function formatShortcutLabel(event: KeyboardEvent) { width: 42px; height: 32px; padding: 0; - border: 1px solid var(--n-border-color); + border: 1px solid var(--app-border); border-radius: 8px; - background-color: var(--n-color, transparent); - color: var(--n-text-color-2); + background-color: var(--app-surface); + color: var(--app-text-secondary); cursor: pointer; transition: border-color 0.15s ease, @@ -4746,14 +4749,15 @@ function formatShortcutLabel(event: KeyboardEvent) { } .terminal-quick-action-icon-button:hover { - border-color: var(--n-primary-color-hover, var(--n-primary-color)); - color: var(--n-primary-color-hover, var(--n-primary-color)); + border-color: var(--app-accent-hover); + background-color: var(--app-surface-hover); + color: var(--app-accent-hover); } .terminal-quick-action-icon-button.is-active { - border-color: var(--n-primary-color); - background-color: color-mix(in srgb, var(--n-primary-color) 10%, transparent); - color: var(--n-primary-color); + border-color: var(--app-accent); + background-color: var(--app-accent-soft); + color: var(--app-accent); } .terminal-quick-action-icon-button :deep(.n-icon) { @@ -5035,8 +5039,8 @@ function formatShortcutLabel(event: KeyboardEvent) { 搜索高亮样式 ======================================== */ :deep(.search-highlight) { - background-color: var(--n-primary-color-hover, #ffec3d); - color: inherit; + background-color: var(--app-accent-soft); + color: var(--app-text-primary); padding: 1px 2px; border-radius: 2px; font-weight: 600; diff --git a/ui/src/views/LoginView.vue b/ui/src/views/LoginView.vue index 2cf8a058..82ba6a15 100644 --- a/ui/src/views/LoginView.vue +++ b/ui/src/views/LoginView.vue @@ -89,13 +89,22 @@ async function handleSubmit() { place-items: center; padding: 32px 20px; overflow: hidden; + color: var(--app-text-primary); background: - radial-gradient(circle at top left, rgba(15, 118, 110, 0.16), transparent 34%), - radial-gradient(circle at bottom right, rgba(194, 65, 12, 0.18), transparent 30%), + radial-gradient( + circle at top left, + color-mix(in srgb, var(--app-accent) 14%, transparent), + transparent 34% + ), + radial-gradient( + circle at bottom right, + color-mix(in srgb, var(--app-accent) 10%, transparent), + transparent 30% + ), linear-gradient( 145deg, - color-mix(in srgb, var(--app-body-color, #f3f4f6) 82%, #ffffff) 0%, - var(--app-body-color, #f3f4f6) 100% + color-mix(in srgb, var(--app-canvas) 82%, var(--app-surface-raised)) 0%, + var(--app-canvas) 100% ); } @@ -112,13 +121,13 @@ async function handleSubmit() { .login-page__glow--a { top: -72px; left: -64px; - background: rgba(13, 148, 136, 0.24); + background: color-mix(in srgb, var(--app-accent) 24%, transparent); } .login-page__glow--b { right: -48px; bottom: -88px; - background: rgba(249, 115, 22, 0.22); + background: color-mix(in srgb, var(--app-accent) 18%, transparent); } .login-card { @@ -126,8 +135,9 @@ async function handleSubmit() { z-index: 1; width: min(100%, 440px); border-radius: 24px; - background: color-mix(in srgb, var(--app-surface-color, #ffffff) 88%, rgba(255, 255, 255, 0.96)); - box-shadow: 0 24px 72px rgba(15, 23, 42, 0.16); + border: 1px solid var(--app-border); + background: color-mix(in srgb, var(--app-surface-raised) 92%, var(--app-canvas)); + box-shadow: 0 24px 72px var(--app-shadow); } .login-card__header { @@ -140,24 +150,59 @@ async function handleSubmit() { font-size: 12px; letter-spacing: 0.18em; text-transform: uppercase; - color: color-mix(in srgb, var(--n-text-color-3) 78%, #0f766e); + color: var(--app-link); } .login-card__title { margin: 0; font-size: clamp(26px, 5vw, 34px); line-height: 1.05; + color: var(--app-text-primary); } .login-card__subtitle { margin: 0; - color: var(--n-text-color-2); + color: var(--app-text-secondary); line-height: 1.6; } .login-card__hint { font-size: 12px; line-height: 1.6; - color: var(--n-text-color-3); + color: var(--app-text-muted); +} + +.login-card :deep(.n-form-item-label) { + color: var(--app-text-secondary); +} + +.login-card :deep(.n-input) { + --n-color: var(--app-surface-sunken) !important; + --n-color-disabled: var(--app-surface-hover) !important; + --n-text-color: var(--app-text-primary) !important; + --n-text-color-disabled: var(--app-text-muted) !important; + --n-placeholder-color: var(--app-text-muted) !important; + --n-placeholder-color-disabled: var(--app-text-muted) !important; + --n-border: 1px solid var(--app-input-border-color) !important; + --n-border-hover: 1px solid var(--app-input-border-hover-color) !important; + --n-border-focus: 1px solid var(--app-focus-ring) !important; + --n-box-shadow-focus: 0 0 0 2px color-mix(in srgb, var(--app-focus-ring) 24%, transparent) !important; + --n-caret-color: var(--app-accent) !important; +} + +.login-card :deep(.n-button.n-button--primary-type) { + --n-color: var(--app-accent) !important; + --n-color-hover: var(--app-accent-hover) !important; + --n-color-pressed: var(--app-accent-pressed) !important; + --n-color-focus: var(--app-accent-hover) !important; + --n-text-color: var(--app-accent-contrast) !important; + --n-text-color-hover: var(--app-accent-contrast) !important; + --n-text-color-pressed: var(--app-accent-contrast) !important; + --n-text-color-focus: var(--app-accent-contrast) !important; + --n-border: 1px solid var(--app-accent) !important; + --n-border-hover: 1px solid var(--app-accent-hover) !important; + --n-border-pressed: 1px solid var(--app-accent-pressed) !important; + --n-border-focus: 1px solid var(--app-focus-ring) !important; + --n-box-shadow-focus: 0 0 0 2px color-mix(in srgb, var(--app-focus-ring) 24%, transparent) !important; } diff --git a/ui/src/views/ProjectWorkspace.vue b/ui/src/views/ProjectWorkspace.vue index 4b2d899e..bb697f63 100644 --- a/ui/src/views/ProjectWorkspace.vue +++ b/ui/src/views/ProjectWorkspace.vue @@ -888,6 +888,8 @@ function setMobileView(view: MobileView, options: { syncRoute?: boolean } = {}) height: 100vh; height: 100dvh; overflow: hidden; + background-color: var(--app-canvas, var(--app-body-color, #f7f8fa)); + color: var(--app-text-primary, var(--app-text-color, #333333)); } .workspace-desktop-shell { @@ -902,8 +904,8 @@ function setMobileView(view: MobileView, options: { syncRoute?: boolean } = {}) min-height: 0; min-width: 0; overflow: hidden; - background-color: var(--app-surface-color, #ffffff); - border-right: 1px solid var(--n-border-color, #e0e0e0); + background-color: var(--app-surface, var(--app-surface-color, #ffffff)); + border-right: 1px solid var(--app-border, #e0e0e0); } .project-sidebar { @@ -938,19 +940,19 @@ function setMobileView(view: MobileView, options: { syncRoute?: boolean } = {}) } .project-sidebar-resizer:hover .project-sidebar-resizer-handle { - background-color: var(--n-border-color, #d0d0d0); + background-color: var(--app-border-strong, #d0d0d0); height: 48px; opacity: 1; } .project-sidebar-resizer.is-compact .project-sidebar-resizer-handle { - background-color: var(--n-border-color, #d0d0d0); + background-color: var(--app-border-strong, #d0d0d0); height: 40px; opacity: 0.72; } .project-sidebar-resizer.is-dragging .project-sidebar-resizer-handle { - background-color: var(--n-primary-color, #18a058); + background-color: var(--app-accent, #18a058); height: 64px; opacity: 1; } @@ -966,7 +968,7 @@ function setMobileView(view: MobileView, options: { syncRoute?: boolean } = {}) height: 100%; min-height: 0; overflow-y: auto; - background-color: var(--app-surface-color, #ffffff); + background-color: var(--app-surface, var(--app-surface-color, #ffffff)); } /* 移动端布局 */ @@ -1045,8 +1047,8 @@ function setMobileView(view: MobileView, options: { syncRoute?: boolean } = {}) display: flex; align-items: stretch; justify-content: space-around; - background-color: var(--app-surface-color, #ffffff); - border-top: 1px solid var(--n-border-color, #e0e0e0); + background-color: var(--app-surface, var(--app-surface-color, #ffffff)); + border-top: 1px solid var(--app-border, #e0e0e0); z-index: 200; transition: opacity 0.18s ease, @@ -1070,7 +1072,7 @@ function setMobileView(view: MobileView, options: { syncRoute?: boolean } = {}) padding: 8px 16px; border: none; background: transparent; - color: var(--n-text-color-3, #999); + color: var(--app-text-muted, #999); font-size: 12px; cursor: pointer; transition: color 0.2s; @@ -1078,7 +1080,12 @@ function setMobileView(view: MobileView, options: { syncRoute?: boolean } = {}) } .mobile-bottom-nav .nav-item.active { - color: var(--n-primary-color, #18a058); + color: var(--app-accent, #18a058); +} + +.mobile-bottom-nav .nav-item:focus-visible { + outline: 2px solid var(--app-focus-ring, #2080f0); + outline-offset: -3px; } .mobile-bottom-nav .nav-item:active {