Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/components/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export default function Terminal({
const { gutterRef } = gutterApi;

// ── 主题/壁纸与设置事件监听 ──
const { T, themeToggle, bgInfo } = useTerminalTheme({ termRef, wrapperRef });
const { T, themeToggle, bgInfo, terminalContainerStyle } = useTerminalTheme({ termRef, wrapperRef });
useTerminalSettingsEvents({
...gutterApi,
termRef, fitAddonRef, shortcutsRef, localEchoRef, timestampsEnabledRef, commandBlocksEnabledRef,
Expand Down Expand Up @@ -219,6 +219,7 @@ export default function Terminal({
ref={wrapperRef}
onContextMenu={handleContextMenu}
onClick={closeContextMenu}
style={terminalContainerStyle}
// 主题底色 + 色调层;壁纸半透明叠在上面
className="relative h-full flex flex-col overflow-hidden bg-[var(--term-container-bg)]"
>
Expand Down
27 changes: 26 additions & 1 deletion frontend/src/components/terminal/useTerminalTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ export function useTerminalTheme(deps: {
// ponytail: getTerminalTheme() 每次渲染调用 30+ 次,缓存为 1 次
const T = useMemo(() => getTerminalTheme(), [themeToggle]);

// ponytail: container 颜色同步计算为 CSS 属性对象,确保挂载首帧即生效,防止浅色模式建连时深色闪烁
const terminalContainerStyle = useMemo<React.CSSProperties>(() => {
const c = T.container || {};
const cssVar = (value: string | undefined) => value || '';
return {
'--term-container-bg': cssVar(c.containerBg),
'--term-tint': c.tint || 'transparent',
'--term-status-bg': cssVar(c.statusBarBg),
'--term-status-border': cssVar(c.statusBarBorder),
'--term-status-color': cssVar(c.statusBarColor),
'--term-server-color': cssVar(c.serverNameColor),
'--term-input-bar-bg': cssVar(c.inputBarBg),
'--term-input-bar-border': cssVar(c.inputBarBorder),
'--term-input-bg': cssVar(c.inputBg),
'--term-input-color': cssVar(c.inputColor),
'--term-input-placeholder': c.inputPlaceholder || c.mutedColor || '',
'--term-btn-border': cssVar(c.btnBorder),
'--term-separator': cssVar(c.separator),
'--term-muted': cssVar(c.mutedColor),
'--term-context-bg': cssVar(c.contextBg),
'--term-context-border': cssVar(c.contextBorder),
'--term-context-shadow': cssVar(c.contextShadow),
} as React.CSSProperties;
}, [T]);

// ── 背景管理与刷新 ─────────────────────────────────────────────────
const [bgInfo, setBgInfo] = useState({
image: localStorage.getItem('termBgImage') || '',
Expand Down Expand Up @@ -105,5 +130,5 @@ export function useTerminalTheme(deps: {
}
}, [T]);

return { T, themeToggle, bgInfo };
return { T, themeToggle, bgInfo, terminalContainerStyle };
}
24 changes: 24 additions & 0 deletions frontend/src/styles/vendor/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
/* ── 终端容器 CSS 变量默认值(JS 主题切换时覆盖) ── */
:root {
--term-container-bg: #0e1218;
--term-tint: transparent;
--term-status-bg: var(--surface-raised);
--term-status-border: 1px solid var(--border-subtle);
--term-status-color: #818fa0;
Expand All @@ -22,6 +23,7 @@
--term-input-bar-border: 1px solid var(--border-subtle);
--term-input-bg: var(--surface-sunken);
--term-input-color: #eaf0f7;
--term-input-placeholder: #5a6578;
--term-btn-border: rgba(77,158,255,0.1);
--term-separator: rgba(77,158,255,0.08);
--term-muted: #5a6578;
Expand All @@ -32,6 +34,28 @@
--term-scrollbar-thumb-hover: rgba(160, 190, 225, 0.55);
}

body.theme-light {
--term-container-bg: #f7f9fc;
--term-tint: transparent;
--term-status-bg: var(--surface-raised);
--term-status-border: 1px solid var(--border-subtle);
--term-status-color: #1d4ed8;
--term-server-color: var(--text-primary);
--term-input-bar-bg: var(--surface-raised);
--term-input-bar-border: 1px solid var(--border-subtle);
--term-input-bg: var(--surface-sunken);
--term-input-color: var(--text-primary);
--term-input-placeholder: var(--text-muted);
--term-btn-border: var(--border);
--term-separator: var(--border-subtle);
--term-muted: var(--text-muted);
--term-context-bg: var(--surface-overlay);
--term-context-border: 1px solid var(--border-subtle);
--term-context-shadow: 0 8px 32px rgba(28,25,23,0.12), 0 2px 8px rgba(28,25,23,0.06);
--term-scrollbar-thumb: rgba(100, 116, 139, 0.35);
--term-scrollbar-thumb-hover: rgba(71, 85, 105, 0.6);
}

/* ── 彻底消除 xterm-viewport 的原生浏览器滚动条,避免空视口常驻滚动条槽位 ── */
.xterm .xterm-viewport,
.xterm-viewport {
Expand Down
36 changes: 36 additions & 0 deletions frontend/src/utils/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,23 @@ const THEME_COMPONENT_CSS_VARS = [
'--tab-active-border',
'--tab-active-text',
'--tab-radius',
'--term-container-bg',
'--term-tint',
'--term-status-bg',
'--term-status-border',
'--term-status-color',
'--term-server-color',
'--term-input-bar-bg',
'--term-input-bar-border',
'--term-input-bg',
'--term-input-color',
'--term-input-placeholder',
'--term-btn-border',
'--term-separator',
'--term-muted',
'--term-context-bg',
'--term-context-border',
'--term-context-shadow',
];

// 主题包 token 键(与 buildBaseThemeTokens 对齐)。切换主题前先清掉,避免旧包内联值残留盖住 CSS。
Expand Down Expand Up @@ -654,6 +671,8 @@ function applyComponentThemeVariables(themePackage: ThemePackage | null | undefi
if (!target) return;
const fileManager = getResolvedThemeComponentTheme(themePackage, 'fileManager');
const topbar = getResolvedThemeComponentTheme(themePackage, 'topbar');
const terminal = getTerminalTheme();
const c = terminal.container || {};
const mappings: Record<string, string | undefined> = {
'--file-manager-panel-bg': fileManager.panelBg,
'--file-manager-toolbar-bg': fileManager.toolbarBg,
Expand All @@ -670,6 +689,23 @@ function applyComponentThemeVariables(themePackage: ThemePackage | null | undefi
'--topbar-bg': topbar.background,
'--topbar-border-color': topbar.borderBottomColor,
'--topbar-title-color': topbar.titleColor,
'--term-container-bg': c.containerBg,
'--term-tint': c.tint || 'transparent',
'--term-status-bg': c.statusBarBg,
'--term-status-border': c.statusBarBorder,
'--term-status-color': c.statusBarColor,
'--term-server-color': c.serverNameColor,
'--term-input-bar-bg': c.inputBarBg,
'--term-input-bar-border': c.inputBarBorder,
'--term-input-bg': c.inputBg,
'--term-input-color': c.inputColor,
'--term-input-placeholder': c.inputPlaceholder || c.mutedColor,
'--term-btn-border': c.btnBorder,
'--term-separator': c.separator,
'--term-muted': c.mutedColor,
'--term-context-bg': c.contextBg,
'--term-context-border': c.contextBorder,
'--term-context-shadow': c.contextShadow,
};
Object.entries(mappings).forEach(([cssVar, value]) => {
if (value) {
Expand Down