feat: theme-driven chrome, customizable tab accent, UI polish
- Sidebar/tab bars/dividers/settings dialog follow the terminal theme - Tab accent color customizable in theme settings (color picker) - Title bar shows the app icon; Material Dark is the default theme - Slimmer rectangular tabs (28px), slate scrollbars, visible pane dividers - Fix pure-black xterm viewport gaps after pane resize
This commit is contained in:
1 parent
0de0eee58d
commit
8cb193fa6a
13 files changed
+241
-66
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "open-terminal",
|
"name": "open-terminal",
|
||||||
"productName": "OpenTerminal",
|
"productName": "OpenTerminal",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "Open-source terminal with SSH, split panes, themes and fonts",
|
"description": "Open-source terminal with SSH, split panes, themes and fonts",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"author": "Bill",
|
"author": "Bill",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { SettingsDialog } from '@renderer/settings/SettingsDialog'
|
|||||||
import { TransferPanel } from '@renderer/sftp/TransferPanel'
|
import { TransferPanel } from '@renderer/sftp/TransferPanel'
|
||||||
import { useSettingsStore } from '@renderer/settings/store'
|
import { useSettingsStore } from '@renderer/settings/store'
|
||||||
import { getThemeById } from '@shared/theme'
|
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
|
/** Custom window title bar: draggable strip themed with the active terminal
|
||||||
* theme; native min/max/close buttons come from BrowserWindow titleBarOverlay. */
|
* theme; native min/max/close buttons come from BrowserWindow titleBarOverlay. */
|
||||||
@@ -15,6 +17,7 @@ function TitleBar(): React.JSX.Element {
|
|||||||
className="app-titlebar"
|
className="app-titlebar"
|
||||||
style={{ background: theme.colors.background, color: theme.colors.foreground }}
|
style={{ background: theme.colors.background, color: theme.colors.foreground }}
|
||||||
>
|
>
|
||||||
|
<img className="app-titlebar-icon" src={appIconUrl} alt="" draggable={false} />
|
||||||
<span className="app-titlebar-title">OpenTerminal</span>
|
<span className="app-titlebar-title">OpenTerminal</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -22,12 +25,25 @@ function TitleBar(): React.JSX.Element {
|
|||||||
|
|
||||||
export default function App(): React.JSX.Element {
|
export default function App(): React.JSX.Element {
|
||||||
const hydrate = useSettingsStore((s) => s.hydrate)
|
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)
|
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void hydrate()
|
void hydrate()
|
||||||
}, [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 (
|
return (
|
||||||
<div className="app-root">
|
<div className="app-root">
|
||||||
<TitleBar />
|
<TitleBar />
|
||||||
|
|||||||
@@ -346,7 +346,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connections-local-item.is-active {
|
.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 {
|
.connections-local-name {
|
||||||
|
|||||||
Vendored
+2
@@ -1,3 +1,5 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
import type { AppApi } from '../shared/api'
|
import type { AppApi } from '../shared/api'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ body,
|
|||||||
font-size: 13px;
|
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. */
|
tracks + arrow buttons that look broken on the dark surface. */
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(121, 121, 121, 0.45) transparent;
|
scrollbar-color: rgba(125, 145, 170, 0.5) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 12px;
|
||||||
height: 10px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
@@ -37,12 +37,17 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background: rgba(121, 121, 121, 0.45);
|
background: rgba(125, 145, 170, 0.5);
|
||||||
border-radius: 5px;
|
border: 3px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb:hover {
|
*::-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,
|
*::-webkit-scrollbar-button,
|
||||||
@@ -80,6 +85,14 @@ button {
|
|||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-titlebar-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.app-titlebar-title {
|
.app-titlebar-title {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import ReactDOM from 'react-dom/client'
|
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 { App as AntdApp, ConfigProvider, theme as antdTheme } from 'antd'
|
||||||
import type { AppApi } from '@shared/api'
|
import type { AppApi } from '@shared/api'
|
||||||
import { DEFAULT_SETTINGS } from '@shared/settings'
|
import { DEFAULT_SETTINGS } from '@shared/settings'
|
||||||
|
import { getThemeById } from '@shared/theme'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { ErrorBoundary } from './ErrorBoundary'
|
import { ErrorBoundary } from './ErrorBoundary'
|
||||||
import ZmodemOffers from './workspace/ZmodemOffers'
|
import ZmodemOffers from './workspace/ZmodemOffers'
|
||||||
import { useSettingsStore } from './settings/store'
|
import { useSettingsStore } from './settings/store'
|
||||||
|
import { mix } from './theme/chrome'
|
||||||
import './global.css'
|
import './global.css'
|
||||||
|
|
||||||
// Dev convenience: allow opening the renderer in a plain browser (vite page)
|
// Dev convenience: allow opening the renderer in a plain browser (vite page)
|
||||||
@@ -148,13 +150,33 @@ function FontHotkeyListener(): null {
|
|||||||
// antd <App> provides the context that App.useApp() consumers (workspace
|
// antd <App> provides the context that App.useApp() consumers (workspace
|
||||||
// message/notification) rely on; without it they get empty stubs and crash.
|
// message/notification) rely on; without it they get empty stubs and crash.
|
||||||
// ErrorBoundary keeps any render crash from black-screening the whole window.
|
// ErrorBoundary keeps any render crash from black-screening the whole window.
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|
||||||
|
/** 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 (
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
theme={{
|
theme={{
|
||||||
algorithm: antdTheme.darkAlgorithm,
|
algorithm: antdTheme.darkAlgorithm,
|
||||||
token: { colorBgContainer: '#1f1f1f', colorBgElevated: '#252525', borderRadius: 6 }
|
token: {
|
||||||
|
colorBgBase: colors.background,
|
||||||
|
colorBgContainer: mix(colors.background, colors.foreground, 0.06) ?? '#1f1f1f',
|
||||||
|
colorBgElevated: mix(colors.background, colors.foreground, 0.1) ?? '#252525',
|
||||||
|
colorTextBase: colors.foreground,
|
||||||
|
borderRadius: 6
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{children}
|
||||||
|
</ConfigProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<ThemedConfigProvider>
|
||||||
<AntdApp>
|
<AntdApp>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
@@ -162,5 +184,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|||||||
<FontHotkeyListener />
|
<FontHotkeyListener />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</AntdApp>
|
</AntdApp>
|
||||||
</ConfigProvider>
|
</ThemedConfigProvider>
|
||||||
)
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Button, Popconfirm } from 'antd'
|
import { Button, ColorPicker, Popconfirm } from 'antd'
|
||||||
import type { TerminalSettings } from '@shared/settings'
|
import type { TerminalSettings } from '@shared/settings'
|
||||||
import { DEFAULT_SETTINGS } from '@shared/settings'
|
import { DEFAULT_SETTINGS } from '@shared/settings'
|
||||||
import type { TerminalTheme, ThemeColors } from '@shared/theme'
|
import type { TerminalTheme, ThemeColors } from '@shared/theme'
|
||||||
@@ -46,6 +46,25 @@ export function ThemeSettingsTab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="theme-pane">
|
||||||
|
<div className="theme-accent-row">
|
||||||
|
<div>
|
||||||
|
<div className="settings-block-label">标签强调色</div>
|
||||||
|
<div className="settings-block-hint">活动标签边框、侧栏指示条与 SSH 徽章的强调颜色</div>
|
||||||
|
</div>
|
||||||
|
<div className="theme-accent-controls">
|
||||||
|
<ColorPicker
|
||||||
|
value={settings.terminal.tabAccentColor}
|
||||||
|
onChange={(color) => void updateTerminal({ tabAccentColor: color.toHexString() })}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => void updateTerminal({ tabAccentColor: DEFAULT_SETTINGS.terminal.tabAccentColor })}
|
||||||
|
>
|
||||||
|
恢复默认
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="theme-gallery">
|
<div className="theme-gallery">
|
||||||
{themeCards.map((t) => {
|
{themeCards.map((t) => {
|
||||||
const selected = t.id === activeTheme.id
|
const selected = t.id === activeTheme.id
|
||||||
@@ -117,5 +136,6 @@ export function ThemeSettingsTab({
|
|||||||
<div className="theme-card-new-text">新建主题</div>
|
<div className="theme-card-new-text">新建主题</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,13 @@
|
|||||||
/* ---------- settings dialog scaffolding ---------- */
|
/* ---------- settings dialog scaffolding ---------- */
|
||||||
|
|
||||||
.ant-modal-header {
|
.ant-modal-header {
|
||||||
border-bottom: 1px solid #2d2d2d;
|
border-bottom: 1px solid var(--chrome-border);
|
||||||
padding-bottom: 14px;
|
padding-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-modal .ant-tabs-nav {
|
.ant-modal .ant-tabs-nav {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
border-bottom: 1px solid #2d2d2d;
|
border-bottom: 1px solid var(--chrome-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-footer {
|
.settings-footer {
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border: 1px solid #2d2d2d;
|
border: 1px solid var(--chrome-border);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +140,30 @@
|
|||||||
|
|
||||||
/* ---------- theme gallery ---------- */
|
/* ---------- 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 {
|
.theme-gallery {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
@@ -333,7 +357,7 @@
|
|||||||
.theme-editor-preview-box {
|
.theme-editor-preview-box {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border: 1px solid #2d2d2d;
|
border: 1px solid var(--chrome-border);
|
||||||
font-family: 'Cascadia Mono', Consolas, 'JetBrains Mono', monospace;
|
font-family: 'Cascadia Mono', Consolas, 'JetBrains Mono', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -356,7 +380,7 @@
|
|||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #1e2531;
|
background: var(--chrome-hover);
|
||||||
color: #3fd06a;
|
color: #3fd06a;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ const SEARCH_DECORATIONS: NonNullable<NonNullable<Parameters<SearchAddon['findNe
|
|||||||
activeMatchColorOverviewRuler: '#c77800'
|
activeMatchColorOverviewRuler: '#c77800'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xterm's default overviewRulerBorder (#7f7f7f) renders as a light vertical line
|
||||||
|
// on the dark surface once scrollback exists. Force it transparent; search-match
|
||||||
|
// marks in the ruler keep their own colors.
|
||||||
|
function withChromeColors(colors: ITheme): ITheme {
|
||||||
|
return { ...colors, overviewRulerBorder: '#00000000' }
|
||||||
|
}
|
||||||
|
|
||||||
export interface TerminalHandle {
|
export interface TerminalHandle {
|
||||||
focus(): void
|
focus(): void
|
||||||
clear(): void
|
clear(): void
|
||||||
@@ -530,7 +537,7 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
cursorBlink: tSettings.cursorBlink,
|
cursorBlink: tSettings.cursorBlink,
|
||||||
cursorStyle: tSettings.cursorStyle,
|
cursorStyle: tSettings.cursorStyle,
|
||||||
cursorInactiveStyle: tSettings.cursorInactiveStyle,
|
cursorInactiveStyle: tSettings.cursorInactiveStyle,
|
||||||
theme: getThemeById(tSettings.themeId, settings.customThemes).colors as ITheme,
|
theme: withChromeColors(getThemeById(tSettings.themeId, settings.customThemes).colors as ITheme),
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
overviewRuler: { width: 9, showTopBorder: false, showBottomBorder: false },
|
overviewRuler: { width: 9, showTopBorder: false, showBottomBorder: false },
|
||||||
drawBoldTextInBrightColors: true,
|
drawBoldTextInBrightColors: true,
|
||||||
@@ -737,7 +744,7 @@ export const TerminalView: ForwardRefExoticComponent<TerminalViewProps & { ref?:
|
|||||||
term.options.cursorBlink = tSettings.cursorBlink
|
term.options.cursorBlink = tSettings.cursorBlink
|
||||||
term.options.cursorStyle = tSettings.cursorStyle
|
term.options.cursorStyle = tSettings.cursorStyle
|
||||||
term.options.cursorInactiveStyle = tSettings.cursorInactiveStyle
|
term.options.cursorInactiveStyle = tSettings.cursorInactiveStyle
|
||||||
term.options.theme = { ...(getThemeById(tSettings.themeId, settings.customThemes).colors as ITheme) }
|
term.options.theme = withChromeColors(getThemeById(tSettings.themeId, settings.customThemes).colors as ITheme)
|
||||||
scheduleFit()
|
scheduleFit()
|
||||||
}, [tSettings, settings.customThemes, scheduleFit])
|
}, [tSettings, settings.customThemes, scheduleFit])
|
||||||
|
|
||||||
|
|||||||
@@ -13,15 +13,24 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark slim scrollbar for the xterm viewport — the native Windows one renders
|
/* xterm.css defaults .xterm-viewport to #000; when the pane height isn't a
|
||||||
light track + arrow buttons that look broken on the dark surface. */
|
multiple of the cell height the leftover bottom strip shows that black.
|
||||||
|
Pin it to the (theme-derived) chrome background so the gap is invisible. */
|
||||||
|
.terminal-view .xterm .xterm-viewport {
|
||||||
|
background-color: var(--chrome-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slim slate scrollbar for the xterm viewport — the native Windows one renders
|
||||||
|
light track + arrow buttons that look broken on the dark surface. The thumb
|
||||||
|
sits inset inside a wider grab lane (transparent border + padding-box clip)
|
||||||
|
so it reads as a soft slate bar, never a white line. */
|
||||||
.terminal-view .xterm-viewport {
|
.terminal-view .xterm-viewport {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(121, 121, 121, 0.45) transparent;
|
scrollbar-color: rgba(125, 145, 170, 0.5) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-view .xterm-viewport::-webkit-scrollbar {
|
.terminal-view .xterm-viewport::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-view .xterm-viewport::-webkit-scrollbar-track {
|
.terminal-view .xterm-viewport::-webkit-scrollbar-track {
|
||||||
@@ -29,12 +38,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.terminal-view .xterm-viewport::-webkit-scrollbar-thumb {
|
.terminal-view .xterm-viewport::-webkit-scrollbar-thumb {
|
||||||
background: rgba(121, 121, 121, 0.45);
|
background: rgba(125, 145, 170, 0.5);
|
||||||
border-radius: 5px;
|
border: 3px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-view .xterm-viewport::-webkit-scrollbar-thumb:hover {
|
.terminal-view .xterm-viewport::-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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-view .xterm-viewport::-webkit-scrollbar-button,
|
.terminal-view .xterm-viewport::-webkit-scrollbar-button,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { ThemeColors } from '@shared/theme'
|
||||||
|
|
||||||
|
function parseHex(color: string): [number, number, number] | null {
|
||||||
|
const m = /^#([0-9a-f]{6})$/i.exec(color.trim())
|
||||||
|
if (!m) return null
|
||||||
|
const n = parseInt(m[1], 16)
|
||||||
|
return [(n >> 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)
|
||||||
|
}
|
||||||
@@ -55,8 +55,8 @@
|
|||||||
/* ---- M6.1 rail active state (mode entries) ---- */
|
/* ---- M6.1 rail active state (mode entries) ---- */
|
||||||
.workspace-rail-btn.is-active {
|
.workspace-rail-btn.is-active {
|
||||||
color: var(--dv-active-tab-foreground-color, #e8e8e8);
|
color: var(--dv-active-tab-foreground-color, #e8e8e8);
|
||||||
box-shadow: inset 2px 0 0 #3fb950;
|
box-shadow: inset 2px 0 0 var(--tab-accent, #3fb950);
|
||||||
background: rgba(63, 185, 80, 0.10);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-rail-spacer {
|
.workspace-rail-spacer {
|
||||||
@@ -173,19 +173,22 @@
|
|||||||
/* Align terminal chrome with the app chrome. */
|
/* Align terminal chrome with the app chrome. */
|
||||||
.dockview-theme-abyss {
|
.dockview-theme-abyss {
|
||||||
--dv-group-view-background-color: var(--chrome-bg);
|
--dv-group-view-background-color: var(--chrome-bg);
|
||||||
--dv-tabs-and-actions-container-background-color: #10141a;
|
--dv-tabs-and-actions-container-background-color: var(--chrome-bg-deep, #10141a);
|
||||||
--dv-tabs-and-actions-container-height: 32px;
|
--dv-tabs-and-actions-container-height: 28px;
|
||||||
--dv-tab-divider-color: transparent;
|
--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 {
|
.workspace-dockview .dv-tabs-and-actions-container {
|
||||||
padding: 4px 5px 0;
|
padding: 4px 5px 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-dockview .dv-tab {
|
.workspace-dockview .dv-tab {
|
||||||
border-radius: 9px;
|
border-radius: 3px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
@@ -202,14 +205,14 @@
|
|||||||
|
|
||||||
/* XTerminal-style active tab: green outline + tinted bg + green title. */
|
/* XTerminal-style active tab: green outline + tinted bg + green title. */
|
||||||
.workspace-dockview .dv-tab.dv-active-tab {
|
.workspace-dockview .dv-tab.dv-active-tab {
|
||||||
background: rgba(63, 185, 80, 0.10);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent);
|
||||||
box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.75);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 75%, transparent);
|
||||||
color: #3fd06a;
|
color: var(--tab-accent-fg, #3fd06a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-dockview .dv-tab.dv-active-tab:hover {
|
.workspace-dockview .dv-tab.dv-active-tab:hover {
|
||||||
box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.95);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 95%, transparent);
|
||||||
color: #4ad476;
|
color: var(--tab-accent-fg, #4ad476);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-dockview .dv-tab .workspace-terminal-tab {
|
.workspace-dockview .dv-tab .workspace-terminal-tab {
|
||||||
@@ -217,15 +220,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-dockview .dv-tab .workspace-terminal-tab-title {
|
.workspace-dockview .dv-tab .workspace-terminal-tab-title {
|
||||||
font-size: 12.5px;
|
font-size: 12px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Close glyph as its own mini outlined pill, mirroring the tab border. */
|
/* Close glyph as its own mini outlined pill, mirroring the tab border. */
|
||||||
.workspace-dockview .dv-tab .workspace-terminal-tab-close {
|
.workspace-dockview .dv-tab .workspace-terminal-tab-close {
|
||||||
width: 17px;
|
width: 15px;
|
||||||
height: 17px;
|
height: 15px;
|
||||||
border-radius: 4px;
|
border-radius: 2px;
|
||||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
|
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;
|
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 {
|
.workspace-dockview .dv-tab .workspace-terminal-tab-close:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: #3fd06a;
|
color: var(--tab-accent-fg, #3fd06a);
|
||||||
background: rgba(63, 185, 80, 0.08);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent);
|
||||||
box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.65);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 65%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- empty state overlay ---- */
|
/* ---- empty state overlay ---- */
|
||||||
@@ -377,11 +380,11 @@ body.ssh-resizing .ssh-bottom-divider {
|
|||||||
.workspace-terminal-tab-icon {
|
.workspace-terminal-tab-icon {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #3fb950;
|
color: var(--tab-accent, #3fb950);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-dockview .dv-tab .workspace-terminal-tab-ssh {
|
.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;
|
padding-left: 5px !important;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
@@ -397,9 +400,9 @@ body.ssh-resizing .ssh-bottom-divider {
|
|||||||
padding: 1px 6px 2px;
|
padding: 1px 6px 2px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
border: 1px solid rgba(63, 185, 80, 0.38);
|
border: 1px solid color-mix(in srgb, var(--tab-accent, #3fb950) 38%, transparent);
|
||||||
background: rgba(63, 185, 80, 0.08);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent);
|
||||||
color: #3fb950;
|
color: var(--tab-accent, #3fb950);
|
||||||
font-family: 'Cascadia Mono', 'Consolas', 'SFMono-Regular', monospace;
|
font-family: 'Cascadia Mono', 'Consolas', 'SFMono-Regular', monospace;
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
line-height: 1;
|
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 {
|
.workspace-dockview .dv-tab.dv-active-tab .workspace-terminal-tab-ssh .ssh-host-badge {
|
||||||
border-color: rgba(63, 185, 80, 0.6);
|
border-color: color-mix(in srgb, var(--tab-accent, #3fb950) 60%, transparent);
|
||||||
background: rgba(63, 185, 80, 0.14);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 14%, transparent);
|
||||||
color: #46c96a;
|
color: var(--tab-accent-fg, #46c96a);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- per-group "+" right after the last tab ----
|
/* ---- per-group "+" right after the last tab ----
|
||||||
@@ -438,9 +441,9 @@ body.ssh-resizing .ssh-bottom-divider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-tabbar-add:hover {
|
.workspace-tabbar-add:hover {
|
||||||
background: rgba(63, 185, 80, 0.08);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 8%, transparent);
|
||||||
box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.65);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 65%, transparent);
|
||||||
color: #3fd06a;
|
color: var(--tab-accent-fg, #3fd06a);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- M6 tab-bar workspace actions (broadcast + quick-input) ---- */
|
/* ---- M6 tab-bar workspace actions (broadcast + quick-input) ---- */
|
||||||
@@ -476,15 +479,15 @@ body.ssh-resizing .ssh-bottom-divider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-tabbar-btn2.is-active {
|
.workspace-tabbar-btn2.is-active {
|
||||||
color: #3fd06a;
|
color: var(--tab-accent-fg, #3fd06a);
|
||||||
background: rgba(63, 185, 80, 0.12);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 12%, transparent);
|
||||||
box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.55);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 55%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-tabbar-bcast-count {
|
.workspace-tabbar-bcast-count {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #3fb950;
|
color: var(--tab-accent, #3fb950);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,9 +568,9 @@ body.ssh-resizing .ssh-bottom-divider {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: #3fb950;
|
color: var(--tab-accent, #3fb950);
|
||||||
background: rgba(63, 185, 80, 0.1);
|
background: color-mix(in srgb, var(--tab-accent, #3fb950) 10%, transparent);
|
||||||
box-shadow: inset 0 0 0 1px rgba(63, 185, 80, 0.35);
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tab-accent, #3fb950) 35%, transparent);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export interface TerminalSettings {
|
|||||||
pasteRiskConfirm: boolean
|
pasteRiskConfirm: boolean
|
||||||
/** inline command suggestion popup while typing (history + library) */
|
/** inline command suggestion popup while typing (history + library) */
|
||||||
suggestEnabled: boolean
|
suggestEnabled: boolean
|
||||||
|
/** accent color for the active tab outline, rail indicator and SSH badges */
|
||||||
|
tabAccentColor: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SystemSettings {
|
export interface SystemSettings {
|
||||||
@@ -95,12 +97,13 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: 'block',
|
cursorStyle: 'block',
|
||||||
cursorInactiveStyle: 'outline',
|
cursorInactiveStyle: 'outline',
|
||||||
themeId: 'default-dark',
|
themeId: 'material-dark',
|
||||||
rendererMode: 'auto',
|
rendererMode: 'auto',
|
||||||
autoWrap: true,
|
autoWrap: true,
|
||||||
copyOnSelect: false,
|
copyOnSelect: false,
|
||||||
pasteRiskConfirm: true,
|
pasteRiskConfirm: true,
|
||||||
suggestEnabled: true
|
suggestEnabled: true,
|
||||||
|
tabAccentColor: '#3fb950'
|
||||||
},
|
},
|
||||||
customThemes: [],
|
customThemes: [],
|
||||||
highlightRules: DEFAULT_HIGHLIGHT_RULES,
|
highlightRules: DEFAULT_HIGHLIGHT_RULES,
|
||||||
|
|||||||
Reference in new issue
Block a user