fix(ui): theme-derived sftp/monitor surfaces, transfer cancel button, dialog cleanup; drop dead autoWrap; gentler bg-image default

This commit is contained in:
Bill committed 2026-10-07 20:44:31 +08:00
1 parent fb65981bff
commit b7938de464
11 files changed
+108 -56

No files matched your search

+13 -11
View File
@@ -1,11 +1,13 @@
/* M3 server sysinfo monitor panel (SSH sessions only). */
/* Panel surface derived from the theme (a fixed dark value turned every
white-based child below unreadable on a light theme). */
.mm-root {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px 12px;
background: #1a1a1a;
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 6%, var(--chrome-bg, #181818));
color: var(--chrome-fg);
font-size: 12px;
overflow-y: auto;
@@ -32,14 +34,14 @@
gap: 1px;
line-height: 1.3;
padding-bottom: 8px;
border-bottom: 1px solid #262626;
border-bottom: 1px solid var(--chrome-border, #2d2d2d);
margin-bottom: 2px;
}
.mm-host {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.92);
color: var(--chrome-fg, #cccccc);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -55,9 +57,9 @@
/* ---------- blocks ---------- */
.mm-block {
border: 1px solid #2a2a2a;
border: 1px solid var(--chrome-border, #2d2d2d);
border-radius: 8px;
background: rgba(255, 255, 255, 0.018);
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 4%, transparent);
padding: 8px 10px;
display: flex;
flex-direction: column;
@@ -108,7 +110,7 @@
height: 5px;
border-radius: 3px;
overflow: hidden;
background: #2c2c2c;
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 12%, transparent);
width: 100%;
}
@@ -121,7 +123,7 @@
/* ---------- memory ---------- */
.mm-mem-size {
font-size: 12px;
color: rgba(255, 255, 255, 0.78);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 78%, transparent);
font-variant-numeric: tabular-nums;
}
@@ -163,7 +165,7 @@
}
.mm-mount {
color: rgba(255, 255, 255, 0.75);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 75%, transparent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -214,7 +216,7 @@
align-items: baseline;
gap: 4px;
font-variant-numeric: tabular-nums;
color: rgba(255, 255, 255, 0.82);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 82%, transparent);
}
.mm-net-unit {
@@ -243,7 +245,7 @@
}
.mm-uptime {
color: rgba(255, 255, 255, 0.85);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 85%, transparent);
}
/* ---------- empty / collecting state ---------- */
@@ -264,7 +266,7 @@
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 55%, transparent);
animation: mm-pulse 1.2s infinite ease-in-out;
}
+32 -6
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Button, Modal, Popconfirm, Tabs } from 'antd'
import type { TabsProps } from 'antd'
import { t } from '@shared/i18n'
@@ -33,6 +33,16 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps): React.JS
// is open, a main-window resize re-derives that default in real time.
const [size, setSize] = useState<{ w: number; h: number } | null>(null)
const [winSize, setWinSize] = useState({ w: window.innerWidth, h: window.innerHeight })
/** Tears down the drag in progress; null while no drag is active. */
const resizeCleanupRef = useRef<(() => void) | null>(null)
const endResize = useCallback((): void => {
const cleanup = resizeCleanupRef.current
if (!cleanup) return
resizeCleanupRef.current = null
cleanup()
}, [])
useEffect(() => {
if (open) setSize(null)
}, [open])
@@ -42,6 +52,13 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps): React.JS
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [open])
// A drag whose mouseup is never delivered (released outside the window) must
// not outlive the dialog: closing or unmounting drops its listeners and the
// body styles it installed.
useEffect(() => {
if (!open) endResize()
}, [open, endResize])
useEffect(() => endResize, [endResize])
const clampToWindow = (w: number, h: number): { w: number; h: number } => ({
w: Math.max(720, Math.min(w, winSize.w - 96)),
@@ -56,24 +73,33 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps): React.JS
const startResize = (e: React.MouseEvent<HTMLSpanElement>): void => {
e.preventDefault()
e.stopPropagation()
if (resizeCleanupRef.current) return
const startX = e.clientX
const startY = e.clientY
const base = dialogSize
const prevCursor = document.body.style.cursor
const prevUserSelect = document.body.style.userSelect
const onMove = (ev: MouseEvent): void => {
const w = Math.max(720, Math.min(window.innerWidth - 96, base.w + (ev.clientX - startX)))
const h = Math.max(440, Math.min(window.innerHeight - 140, base.h + (ev.clientY - startY)))
setSize({ w, h })
}
const onUp = (): void => {
// One teardown for every way the drag can end: mouseup, the window losing
// focus (the release lands on another window and never reaches us), the
// dialog closing, or unmount.
const cleanup = (): void => {
window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onUp)
document.body.style.cursor = ''
document.body.style.userSelect = ''
window.removeEventListener('mouseup', cleanup)
window.removeEventListener('blur', cleanup)
document.body.style.cursor = prevCursor
document.body.style.userSelect = prevUserSelect
}
resizeCleanupRef.current = cleanup
document.body.style.cursor = 'nwse-resize'
document.body.style.userSelect = 'none'
window.addEventListener('mousemove', onMove)
window.addEventListener('mouseup', onUp)
window.addEventListener('mouseup', cleanup)
window.addEventListener('blur', cleanup)
}
const tabItems: TabsProps['items'] = [
@@ -253,16 +253,6 @@ export function RenderSettingsTab(): React.JSX.Element {
<div className="settings-block-hint settings-render-hint">
{rendererModes.find((o) => o.value === settings.terminal.rendererMode)?.hint}
</div>
<SettingRow
label={t('settings.render.autoWrap')}
desc={t('settings.render.autoWrapDesc')}
control={
<Switch
checked={settings.terminal.autoWrap}
onChange={(checked) => void updateTerminal({ autoWrap: checked })}
/>
}
/>
<SettingRow
label={t('settings.render.copyOnSelect')}
desc={t('settings.render.copyOnSelectDesc')}
@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { Button, ColorPicker, Popconfirm, Slider } from 'antd'
import { t } from '@shared/i18n'
import type { TerminalSettings } from '@shared/settings'
@@ -31,6 +31,7 @@ export function ThemeSettingsTab({
const updateTerminal = useSettingsStore((s) => s.updateTerminal)
const setCustomThemes = useSettingsStore((s) => s.setCustomThemes)
const activeTheme = useResolvedTheme()
const [opacityDraft, setOpacityDraft] = useState<number | null>(null)
const themeCards = useMemo(
() => [...BUILTIN_THEMES, ...settings.customThemes],
@@ -80,8 +81,12 @@ export function ThemeSettingsTab({
min={10}
max={100}
step={5}
value={settings.terminal.backgroundImageOpacity}
onChange={(v) => void updateTerminal({ backgroundImageOpacity: v })}
value={opacityDraft ?? settings.terminal.backgroundImageOpacity}
onChange={(v) => setOpacityDraft(v)}
onChangeComplete={(v) => {
setOpacityDraft(null)
void updateTerminal({ backgroundImageOpacity: v })
}}
/>
</div>
)}
+15 -1
View File
@@ -123,7 +123,21 @@ export function TransferPanel(): React.JSX.Element | null {
? t('panels.transfer.zmodemUploading')
: t('panels.transfer.zmodemDownloading')}
</span>
{(tr.state === 'error' || tr.state === 'done') && (
{/* Only the SFTP engine's own transfers can be cancelled; zmodem
runs in a separate engine that ignores cancelTransfer. */}
{tr.state === 'running' && (tr.kind === 'upload' || tr.kind === 'download') && (
<button
type="button"
className="sftp-transfer-cancel"
onClick={() => window.api.cancelTransfer(id)}
>
{t('common.cancel')}
</button>
)}
{/* Cancelled rows dismiss themselves after 3s, but one that also
carries an error (a cancel racing a failed file) skips that
path, so every terminal state keeps a manual close. */}
{(tr.state === 'error' || tr.state === 'done' || tr.state === 'cancelled') && (
<button
type="button"
className="sftp-transfer-close"
+35 -10
View File
@@ -135,6 +135,8 @@
gap: 12px;
}
/* Preview strip inside the permission dialog: the same derived surface as the
transfer card, so the mode string keeps its contrast on a light theme. */
.sftp-perm-mode {
display: flex;
align-items: baseline;
@@ -144,13 +146,13 @@
font-size: 14px;
color: var(--chrome-fg);
padding: 6px 10px;
background: #141414;
border: 1px solid #2d2d2d;
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 8%, var(--chrome-bg, #181818));
border: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 20%, transparent);
border-radius: 6px;
}
.sftp-perm-sym {
color: rgba(255, 255, 255, 0.9);
color: var(--chrome-fg, #cccccc);
letter-spacing: 1px;
}
@@ -179,7 +181,7 @@
}
.sftp-perm-grid-label {
color: rgba(255, 255, 255, 0.7);
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 70%, transparent);
}
.sftp-perm-grid-head span:first-child,
@@ -237,8 +239,12 @@
width: 300px;
}
/* The card floats over the terminal canvas, so its surface is derived from the
theme rather than pinned to a dark value: mixing the theme foreground into the
theme background lifts it above the canvas on a dark theme and drops it below
a light one, and var(--chrome-fg) text stays readable either way. */
.sftp-transfer {
background: #1f1f1f;
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 8%, var(--chrome-bg, #181818));
border: 1px solid var(--chrome-border);
border-radius: 6px;
padding: 8px 10px;
@@ -253,6 +259,7 @@
}
.sftp-transfer-title {
flex: 1;
font-size: 12px;
color: var(--chrome-fg);
font-weight: 600;
@@ -267,24 +274,42 @@
}
.sftp-transfer-cancelled .sftp-transfer-title {
color: #8a8a8a;
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 70%, transparent);
}
.sftp-transfer-cancel {
flex: 0 0 auto;
border: 1px solid color-mix(in srgb, var(--chrome-fg, #cccccc) 25%, transparent);
background: none;
border-radius: 4px;
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 75%, transparent);
font-size: 11px;
line-height: 1;
padding: 2px 6px;
cursor: pointer;
}
.sftp-transfer-cancel:hover {
color: var(--chrome-fg, #cccccc);
border-color: color-mix(in srgb, var(--chrome-fg, #cccccc) 45%, transparent);
}
.sftp-transfer-close {
flex: 0 0 auto;
border: none;
background: none;
color: #8a8a8a;
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 65%, transparent);
cursor: pointer;
padding: 0 2px;
}
.sftp-transfer-close:hover {
color: #ffffff;
color: var(--chrome-fg, #cccccc);
}
.sftp-transfer-file {
font-size: 11px;
color: #9a9a9a;
color: color-mix(in srgb, var(--chrome-fg, #cccccc) 75%, transparent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -294,7 +319,7 @@
.sftp-transfer-bar {
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.08);
background: color-mix(in srgb, var(--chrome-fg, #cccccc) 14%, transparent);
overflow: hidden;
}
+1 -3
View File
@@ -69,8 +69,6 @@ const settings: Record<string, string> = {
'Pure CPU rendering with the widest compatibility; best without a GPU or over remote desktop',
'settings.render.modeLabel': 'Renderer',
'settings.render.modeHint': 'How terminal cells are accelerated; applies to every terminal',
'settings.render.autoWrap': 'Auto wrap',
'settings.render.autoWrapDesc': 'Wrap lines that exceed the width',
'settings.render.copyOnSelect': 'Copy on select',
'settings.render.copyOnSelectDesc': 'Copy text automatically when you select it with the mouse',
'settings.render.pasteRiskConfirm': 'Confirm risky paste',
@@ -286,7 +284,7 @@ const settings: Record<string, string> = {
'settings.theme.accent': 'Tab accent color',
'settings.theme.accentDesc': 'Accent color for the active tab border, the sidebar indicator, and SSH badges',
'settings.theme.backgroundImage': 'Background image',
'settings.theme.backgroundImageDesc': 'Show an image behind the terminal; remove it to go back to a solid background',
'settings.theme.backgroundImageDesc': 'Show an image behind the terminal; remove it to go back to a solid background. Lower the opacity if text gets hard to read (20–40% works well for bright images)',
'settings.theme.chooseImage': 'Choose image',
'settings.theme.clearImage': 'Remove',
'settings.theme.imageOpacity': 'Image opacity',
+1 -3
View File
@@ -67,8 +67,6 @@ const settings: Record<string, string> = {
'settings.rendererMode.domHint': 'CPU のみで描画し、互換性を最優先します。GPU がない環境やリモートデスクトップに適します',
'settings.render.modeLabel': '描画モード',
'settings.render.modeHint': 'ターミナルセルの描画を高速化する方式。変更はすべてのターミナルに適用されます',
'settings.render.autoWrap': '自動折り返し',
'settings.render.autoWrapDesc': '幅を超えた行を自動で折り返します',
'settings.render.copyOnSelect': '選択時にコピー',
'settings.render.copyOnSelectDesc': 'マウスで文字を選択すると自動でコピーします',
'settings.render.pasteRiskConfirm': '貼り付け前の確認',
@@ -281,7 +279,7 @@ const settings: Record<string, string> = {
'settings.theme.accent': 'タブのアクセントカラー',
'settings.theme.accentDesc': 'アクティブなタブの枠線、サイドバーのインジケーター、SSH バッジのアクセントカラー',
'settings.theme.backgroundImage': '背景画像',
'settings.theme.backgroundImageDesc': 'ターミナルの背後に画像を表示します。削除すると無地の背景に戻ります',
'settings.theme.backgroundImageDesc': 'ターミナルの背後に画像を表示します。削除すると無地の背景に戻ります。文字が読みにくい場合は不透明度を下げてください(明るい画像は 20–40% が目安です)',
'settings.theme.chooseImage': '画像を選択',
'settings.theme.clearImage': '削除',
'settings.theme.imageOpacity': '画像の不透明度',
+1 -3
View File
@@ -67,8 +67,6 @@ const settings: Record<string, string> = {
'settings.rendererMode.domHint': '纯 CPU 渲染,兼容性最好,适合无 GPU 或远程桌面环境',
'settings.render.modeLabel': '渲染模式',
'settings.render.modeHint': '终端单元渲染加速方式,改动将应用到所有终端',
'settings.render.autoWrap': '自动换行',
'settings.render.autoWrapDesc': '超出宽度时自动折行',
'settings.render.copyOnSelect': '选中即复制',
'settings.render.copyOnSelectDesc': '鼠标选中文字时自动复制',
'settings.render.pasteRiskConfirm': '粘贴风险确认',
@@ -270,7 +268,7 @@ const settings: Record<string, string> = {
'settings.theme.accent': '标签强调色',
'settings.theme.accentDesc': '活动标签边框、侧栏指示条与 SSH 徽章的强调颜色',
'settings.theme.backgroundImage': '背景图片',
'settings.theme.backgroundImageDesc': '为终端设置一张背景图片;移除后恢复纯色背景',
'settings.theme.backgroundImageDesc': '为终端设置一张背景图片;移除后恢复纯色背景;文字看不清时调低透明度(亮图建议 20–40%)',
'settings.theme.chooseImage': '选择图片',
'settings.theme.clearImage': '移除图片',
'settings.theme.imageOpacity': '图片不透明度',
+1 -3
View File
@@ -67,8 +67,6 @@ const settings: Record<string, string> = {
'settings.rendererMode.domHint': '純 CPU 渲染,相容性最佳,適合沒有 GPU 或遠端桌面的環境',
'settings.render.modeLabel': '渲染模式',
'settings.render.modeHint': '終端儲存格的渲染加速方式,變更將套用到所有終端',
'settings.render.autoWrap': '自動換行',
'settings.render.autoWrapDesc': '超出寬度時自動折行',
'settings.render.copyOnSelect': '選取即複製',
'settings.render.copyOnSelectDesc': '以滑鼠選取文字時自動複製',
'settings.render.pasteRiskConfirm': '貼上風險確認',
@@ -270,7 +268,7 @@ const settings: Record<string, string> = {
'settings.theme.accent': '標籤強調色',
'settings.theme.accentDesc': '作用中標籤的邊框、側邊欄指示條與 SSH 徽章的強調顏色',
'settings.theme.backgroundImage': '背景圖片',
'settings.theme.backgroundImageDesc': '為終端設定一張背景圖片;移除後恢復純色背景',
'settings.theme.backgroundImageDesc': '為終端設定一張背景圖片;移除後恢復純色背景;文字看不清時調低透明度(亮圖建議 20–40%)',
'settings.theme.chooseImage': '選擇圖片',
'settings.theme.clearImage': '移除圖片',
'settings.theme.imageOpacity': '圖片不透明度',
+1 -3
View File
@@ -63,7 +63,6 @@ export interface TerminalSettings {
cursorInactiveStyle: 'outline' | 'block' | 'bar' | 'underline' | 'none'
themeId: string
rendererMode: 'auto' | 'webgl' | 'dom'
autoWrap: boolean
copyOnSelect: boolean
/** confirm before pasting multi-line / suspicious text */
pasteRiskConfirm: boolean
@@ -445,7 +444,6 @@ export const DEFAULT_SETTINGS: AppSettings = {
cursorInactiveStyle: 'outline',
themeId: 'material-dark',
rendererMode: 'auto',
autoWrap: true,
copyOnSelect: false,
pasteRiskConfirm: true,
suggestEnabled: false,
@@ -461,7 +459,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
highlightThemeColors: false,
highlightPerHost: false,
backgroundImage: '',
backgroundImageOpacity: 60
backgroundImageOpacity: 30
},
customThemes: [],
highlightRules: DEFAULT_HIGHLIGHT_RULES,