fix(ui): theme-derived sftp/monitor surfaces, transfer cancel button, dialog cleanup; drop dead autoWrap; gentler bg-image default
This commit is contained in:
1 parent
fb65981bff
commit
b7938de464
11 files changed
+108
-56
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user