diff --git a/package.json b/package.json index 47f457f..a0ab678 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "open-terminal", "productName": "OpenTerminal", - "version": "1.0.1", + "version": "1.0.2", "description": "Open-source terminal with SSH, split panes, themes and fonts", "main": "out/main/index.js", "author": "Bill", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 5f44934..c4cb907 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -4,6 +4,8 @@ import { SettingsDialog } from '@renderer/settings/SettingsDialog' import { TransferPanel } from '@renderer/sftp/TransferPanel' import { useSettingsStore } from '@renderer/settings/store' import { getThemeById } from '@shared/theme' +import { applyChromeTheme, applyTabAccent } from '@renderer/theme/chrome' +import appIconUrl from '../../../build/icon.png' /** Custom window title bar: draggable strip themed with the active terminal * theme; native min/max/close buttons come from BrowserWindow titleBarOverlay. */ @@ -15,6 +17,7 @@ function TitleBar(): React.JSX.Element { className="app-titlebar" style={{ background: theme.colors.background, color: theme.colors.foreground }} > + OpenTerminal ) @@ -22,12 +25,25 @@ function TitleBar(): React.JSX.Element { export default function App(): React.JSX.Element { const hydrate = useSettingsStore((s) => s.hydrate) + const themeId = useSettingsStore((s) => s.settings.terminal.themeId) + const customThemes = useSettingsStore((s) => s.settings.customThemes) + const tabAccentColor = useSettingsStore((s) => s.settings.terminal.tabAccentColor) const [settingsOpen, setSettingsOpen] = useState(false) useEffect(() => { void hydrate() }, [hydrate]) + // Sidebar / tab bars / dividers follow the active terminal theme. + useEffect(() => { + applyChromeTheme(getThemeById(themeId, customThemes).colors) + }, [themeId, customThemes]) + + // Active-tab accent color (rail indicator, tab outline, SSH badges). + useEffect(() => { + applyTabAccent(tabAccentColor) + }, [tabAccentColor]) + return (
diff --git a/src/renderer/src/connections/connections.css b/src/renderer/src/connections/connections.css index 62476b3..8a2027c 100644 --- a/src/renderer/src/connections/connections.css +++ b/src/renderer/src/connections/connections.css @@ -346,7 +346,7 @@ } .connections-local-item.is-active { - background: rgba(63, 185, 80, 0.12); + background: color-mix(in srgb, var(--tab-accent, #3fb950) 12%, transparent); } .connections-local-name { diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index e8e6140..40cab1c 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -1,3 +1,5 @@ +/// + import type { AppApi } from '../shared/api' declare global { diff --git a/src/renderer/src/global.css b/src/renderer/src/global.css index 5551b7f..e2f001b 100644 --- a/src/renderer/src/global.css +++ b/src/renderer/src/global.css @@ -20,16 +20,16 @@ body, font-size: 13px; } -/* App-wide slim dark scrollbars: the native Windows style renders light +/* App-wide slim slate scrollbars: the native Windows style renders light tracks + arrow buttons that look broken on the dark surface. */ * { scrollbar-width: thin; - scrollbar-color: rgba(121, 121, 121, 0.45) transparent; + scrollbar-color: rgba(125, 145, 170, 0.5) transparent; } *::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 12px; + height: 12px; } *::-webkit-scrollbar-track { @@ -37,12 +37,17 @@ body, } *::-webkit-scrollbar-thumb { - background: rgba(121, 121, 121, 0.45); - border-radius: 5px; + background: rgba(125, 145, 170, 0.5); + border: 3px solid transparent; + border-radius: 6px; + background-clip: padding-box; + min-height: 28px; } *::-webkit-scrollbar-thumb:hover { - background: rgba(160, 160, 160, 0.6); + background: rgba(150, 172, 200, 0.7); + border: 3px solid transparent; + background-clip: padding-box; } *::-webkit-scrollbar-button, @@ -80,6 +85,14 @@ button { letter-spacing: 0.2px; } +.app-titlebar-icon { + width: 16px; + height: 16px; + margin-right: 7px; + border-radius: 4px; + pointer-events: none; +} + .app-titlebar-title { opacity: 0.75; font-weight: 500; diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index d707d1f..a7fdabc 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,12 +1,14 @@ import ReactDOM from 'react-dom/client' -import { useEffect } from 'react' +import { useEffect, type ReactNode } from 'react' import { App as AntdApp, ConfigProvider, theme as antdTheme } from 'antd' import type { AppApi } from '@shared/api' import { DEFAULT_SETTINGS } from '@shared/settings' +import { getThemeById } from '@shared/theme' import App from './App' import { ErrorBoundary } from './ErrorBoundary' import ZmodemOffers from './workspace/ZmodemOffers' import { useSettingsStore } from './settings/store' +import { mix } from './theme/chrome' import './global.css' // Dev convenience: allow opening the renderer in a plain browser (vite page) @@ -148,13 +150,33 @@ function FontHotkeyListener(): null { // antd provides the context that App.useApp() consumers (workspace // message/notification) rely on; without it they get empty stubs and crash. // ErrorBoundary keeps any render crash from black-screening the whole window. + +/** antd surfaces (settings dialog, menus, popovers) follow the terminal theme: + * base/container/elevated backgrounds derive from the theme's background. */ +function ThemedConfigProvider({ children }: { children: ReactNode }): React.JSX.Element { + const themeId = useSettingsStore((s) => s.settings.terminal.themeId) + const customThemes = useSettingsStore((s) => s.settings.customThemes) + const colors = getThemeById(themeId, customThemes).colors + return ( + + {children} + + ) +} + ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - + @@ -162,5 +184,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - + ) diff --git a/src/renderer/src/settings/ThemeSettingsTab.tsx b/src/renderer/src/settings/ThemeSettingsTab.tsx index 5ab1610..52941d6 100644 --- a/src/renderer/src/settings/ThemeSettingsTab.tsx +++ b/src/renderer/src/settings/ThemeSettingsTab.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { Button, Popconfirm } from 'antd' +import { Button, ColorPicker, Popconfirm } from 'antd' import type { TerminalSettings } from '@shared/settings' import { DEFAULT_SETTINGS } from '@shared/settings' import type { TerminalTheme, ThemeColors } from '@shared/theme' @@ -46,7 +46,26 @@ export function ThemeSettingsTab({ } return ( -
+
+
+
+
标签强调色
+
活动标签边框、侧栏指示条与 SSH 徽章的强调颜色
+
+
+ void updateTerminal({ tabAccentColor: color.toHexString() })} + /> + +
+
+
{themeCards.map((t) => { const selected = t.id === activeTheme.id const isCustom = t.builtin === false @@ -116,6 +135,7 @@ export function ThemeSettingsTab({
+
新建主题
+
) } \ No newline at end of file diff --git a/src/renderer/src/settings/settings.css b/src/renderer/src/settings/settings.css index 51607a2..99ba909 100644 --- a/src/renderer/src/settings/settings.css +++ b/src/renderer/src/settings/settings.css @@ -5,13 +5,13 @@ /* ---------- settings dialog scaffolding ---------- */ .ant-modal-header { - border-bottom: 1px solid #2d2d2d; + border-bottom: 1px solid var(--chrome-border); padding-bottom: 14px; } .ant-modal .ant-tabs-nav { margin-bottom: 8px; - border-bottom: 1px solid #2d2d2d; + border-bottom: 1px solid var(--chrome-border); } .settings-footer { @@ -116,7 +116,7 @@ margin-top: 14px; border-radius: 6px; padding: 12px 14px; - border: 1px solid #2d2d2d; + border: 1px solid var(--chrome-border); overflow: hidden; } @@ -140,6 +140,30 @@ /* ---------- theme gallery ---------- */ +/* ---- theme tab: accent picker + gallery ---- */ +.theme-pane { + display: flex; + flex-direction: column; + gap: 14px; + min-height: 0; +} + +.theme-accent-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--chrome-border); + border-radius: 8px; +} + +.theme-accent-controls { + display: flex; + align-items: center; + gap: 8px; +} + .theme-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); @@ -333,7 +357,7 @@ .theme-editor-preview-box { border-radius: 6px; padding: 10px 14px; - border: 1px solid #2d2d2d; + border: 1px solid var(--chrome-border); font-family: 'Cascadia Mono', Consolas, 'JetBrains Mono', monospace; font-size: 12px; } @@ -356,7 +380,7 @@ width: 48px; height: 48px; border-radius: 12px; - background: #1e2531; + background: var(--chrome-hover); color: #3fd06a; display: flex; align-items: center; diff --git a/src/renderer/src/terminal/TerminalView.tsx b/src/renderer/src/terminal/TerminalView.tsx index 6340def..7801f92 100644 --- a/src/renderer/src/terminal/TerminalView.tsx +++ b/src/renderer/src/terminal/TerminalView.tsx @@ -44,6 +44,13 @@ const SEARCH_DECORATIONS: NonNullable> 16) & 255, (n >> 8) & 255, n & 255] +} + +function toHex(v: number): string { + return Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0') +} + +/** t=0 → a, t=1 → b */ +export function mix(a: string, b: string, t: number): string | null { + const ca = parseHex(a) + const cb = parseHex(b) + if (!ca || !cb) return null + return `#${toHex(ca[0] + (cb[0] - ca[0]) * t)}${toHex(ca[1] + (cb[1] - ca[1]) * t)}${toHex(ca[2] + (cb[2] - ca[2]) * t)}` +} + +/** Derive the app chrome palette (sidebar, tab bars, dividers) from the active + * terminal theme so the whole window follows theme switches instead of only + * the terminal canvas. Values land on :root CSS variables; anything the theme + * can't provide keeps the static fallback in global.css. */ +export function applyChromeTheme(colors: ThemeColors): void { + const style = document.documentElement.style + const set = (key: string, value: string | null): void => { + if (value) style.setProperty(key, value) + } + set('--chrome-bg', colors.background) + set('--chrome-fg', colors.foreground) + set('--chrome-bg-deep', mix(colors.background, '#000000', 0.35)) + set('--chrome-hover', mix(colors.background, colors.foreground, 0.08)) + set('--chrome-border', mix(colors.background, colors.foreground, 0.16)) +} + +/** Tab accent: base color + a lightened foreground variant for text on dark + * tinted backgrounds. Consumed via var(--tab-accent*) in workspace.css. */ +export function applyTabAccent(color: string): void { + const style = document.documentElement.style + // Settings hydrated from an older main process may lack the key; clear the + // vars so the CSS fallbacks apply instead of an invalid "undefined" value. + if (!color) { + style.removeProperty('--tab-accent') + style.removeProperty('--tab-accent-fg') + return + } + style.setProperty('--tab-accent', color) + style.setProperty('--tab-accent-fg', mix(color, '#ffffff', 0.18) ?? color) +} diff --git a/src/renderer/src/workspace/workspace.css b/src/renderer/src/workspace/workspace.css index de9ce66..28bd0c0 100644 --- a/src/renderer/src/workspace/workspace.css +++ b/src/renderer/src/workspace/workspace.css @@ -55,8 +55,8 @@ /* ---- M6.1 rail active state (mode entries) ---- */ .workspace-rail-btn.is-active { color: var(--dv-active-tab-foreground-color, #e8e8e8); - box-shadow: inset 2px 0 0 #3fb950; - background: rgba(63, 185, 80, 0.10); + box-shadow: inset 2px 0 0 var(--tab-accent, #3fb950); + background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent); } .workspace-rail-spacer { @@ -173,19 +173,22 @@ /* Align terminal chrome with the app chrome. */ .dockview-theme-abyss { --dv-group-view-background-color: var(--chrome-bg); - --dv-tabs-and-actions-container-background-color: #10141a; - --dv-tabs-and-actions-container-height: 32px; + --dv-tabs-and-actions-container-background-color: var(--chrome-bg-deep, #10141a); + --dv-tabs-and-actions-container-height: 28px; --dv-tab-divider-color: transparent; + /* Pane split dividers: soft slate band at rest, accent blue on hover/drag. */ + --dv-sash-color: rgba(148, 163, 184, 0.28); + --dv-active-sash-color: rgba(59, 142, 234, 0.75); } -/* ---- tab polish: rounded pills, quiet inactive, green active ---- */ +/* ---- tab polish: low rectangular tabs, quiet inactive, green active ---- */ .workspace-dockview .dv-tabs-and-actions-container { padding: 4px 5px 0; box-sizing: border-box; } .workspace-dockview .dv-tab { - border-radius: 9px; + border-radius: 3px; padding: 0 8px; margin-right: 4px; background: rgba(255, 255, 255, 0.04); @@ -202,14 +205,14 @@ /* XTerminal-style active tab: green outline + tinted bg + green title. */ .workspace-dockview .dv-tab.dv-active-tab { - background: rgba(63, 185, 80, 0.10); - box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.75); - color: #3fd06a; + background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 75%, transparent); + color: var(--tab-accent-fg, #3fd06a); } .workspace-dockview .dv-tab.dv-active-tab:hover { - box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.95); - color: #4ad476; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 95%, transparent); + color: var(--tab-accent-fg, #4ad476); } .workspace-dockview .dv-tab .workspace-terminal-tab { @@ -217,15 +220,15 @@ } .workspace-dockview .dv-tab .workspace-terminal-tab-title { - font-size: 12.5px; + font-size: 12px; line-height: 1; } /* Close glyph as its own mini outlined pill, mirroring the tab border. */ .workspace-dockview .dv-tab .workspace-terminal-tab-close { - width: 17px; - height: 17px; - border-radius: 4px; + width: 15px; + height: 15px; + border-radius: 2px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16); opacity: 0.8; transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease; @@ -233,9 +236,9 @@ .workspace-dockview .dv-tab .workspace-terminal-tab-close:hover { opacity: 1; - color: #3fd06a; - background: rgba(63, 185, 80, 0.08); - box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.65); + color: var(--tab-accent-fg, #3fd06a); + background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 65%, transparent); } /* ---- empty state overlay ---- */ @@ -377,11 +380,11 @@ body.ssh-resizing .ssh-bottom-divider { .workspace-terminal-tab-icon { flex: 0 0 auto; font-size: 12px; - color: #3fb950; + color: var(--tab-accent, #3fb950); } .workspace-dockview .dv-tab .workspace-terminal-tab-ssh { - border-left: 2px solid #3fb950; + border-left: 2px solid var(--tab-accent, #3fb950); padding-left: 5px !important; gap: 5px; } @@ -397,9 +400,9 @@ body.ssh-resizing .ssh-bottom-divider { padding: 1px 6px 2px; box-sizing: border-box; border-radius: 9px; - border: 1px solid rgba(63, 185, 80, 0.38); - background: rgba(63, 185, 80, 0.08); - color: #3fb950; + border: 1px solid color-mix(in srgb, var(--tab-accent, #3fb950) 38%, transparent); + background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent); + color: var(--tab-accent, #3fb950); font-family: 'Cascadia Mono', 'Consolas', 'SFMono-Regular', monospace; font-size: 10.5px; line-height: 1; @@ -407,9 +410,9 @@ body.ssh-resizing .ssh-bottom-divider { } .workspace-dockview .dv-tab.dv-active-tab .workspace-terminal-tab-ssh .ssh-host-badge { - border-color: rgba(63, 185, 80, 0.6); - background: rgba(63, 185, 80, 0.14); - color: #46c96a; + border-color: color-mix(in srgb, var(--tab-accent, #3fb950) 60%, transparent); + background: color-mix(in srgb, var(--tab-accent, #3fb950) 14%, transparent); + color: var(--tab-accent-fg, #46c96a); } /* ---- per-group "+" right after the last tab ---- @@ -438,9 +441,9 @@ body.ssh-resizing .ssh-bottom-divider { } .workspace-tabbar-add:hover { - background: rgba(63, 185, 80, 0.08); - box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.65); - color: #3fd06a; + background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 65%, transparent); + color: var(--tab-accent-fg, #3fd06a); } /* ---- M6 tab-bar workspace actions (broadcast + quick-input) ---- */ @@ -476,15 +479,15 @@ body.ssh-resizing .ssh-bottom-divider { } .workspace-tabbar-btn2.is-active { - color: #3fd06a; - background: rgba(63, 185, 80, 0.12); - box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.55); + color: var(--tab-accent-fg, #3fd06a); + background: color-mix(in srgb, var(--tab-accent, #3fb950) 12%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 55%, transparent); } .workspace-tabbar-bcast-count { font-size: 10px; font-weight: 600; - color: #3fb950; + color: var(--tab-accent, #3fb950); line-height: 1; } @@ -565,9 +568,9 @@ body.ssh-resizing .ssh-bottom-divider { font-size: 10px; font-weight: 600; line-height: 1; - color: #3fb950; - background: rgba(63, 185, 80, 0.1); - box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.35); + color: var(--tab-accent, #3fb950); + background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 35%, transparent); user-select: none; } diff --git a/src/shared/settings.ts b/src/shared/settings.ts index 49341a0..cdf0886 100644 --- a/src/shared/settings.ts +++ b/src/shared/settings.ts @@ -36,6 +36,8 @@ export interface TerminalSettings { pasteRiskConfirm: boolean /** inline command suggestion popup while typing (history + library) */ suggestEnabled: boolean + /** accent color for the active tab outline, rail indicator and SSH badges */ + tabAccentColor: string } export interface SystemSettings { @@ -95,12 +97,13 @@ export const DEFAULT_SETTINGS: AppSettings = { cursorBlink: true, cursorStyle: 'block', cursorInactiveStyle: 'outline', - themeId: 'default-dark', + themeId: 'material-dark', rendererMode: 'auto', autoWrap: true, copyOnSelect: false, pasteRiskConfirm: true, - suggestEnabled: true + suggestEnabled: true, + tabAccentColor: '#3fb950' }, customThemes: [], highlightRules: DEFAULT_HIGHLIGHT_RULES,