docs+chore: refresh README/STATE, cache electron in CI, lock chord by keycode, misc P3
- README: add lock-screen section, refresh test list (13 offline tests),
updater fallback order, data locations, architecture tree
- STATE.md: v1.0.20, current release.cjs flow (no --skip-github), M11-M13
- ci.yml: actions/cache for the ~100MB Electron binary keyed on lockfile
- .gitignore: ignore .env.* but keep committed templates
- scripts/archive-releases.cjs moved in from tmp-test; KEEP_TAG is now a
required CLI arg (a hardcoded default is how the wrong version gets wiped)
- lockShortcuts: isPanicLockChord matches input.code ('KeyL') so Dvorak and
non-Latin layouts trigger Ctrl+L lock correctly
- settings: drop the dead single-option update-channel Select from About tab
- Workspace/App: memo(IconRail/LayoutFlyout), useMemo layoutMenu keyed on
language, useCallback onOpenSettings; drop unused IconRail onSplit prop
This commit is contained in:
1 parent
d22923aedd
commit
5ff311ea0f
14 files changed
+231
-61
No files matched your search
@@ -6,6 +6,8 @@
|
||||
|
||||
export interface LockInputEvent {
|
||||
key: string
|
||||
/** Physical key position (`KeyL`, `F5`, …) — layout-independent, unlike `key`. */
|
||||
code: string
|
||||
control: boolean
|
||||
shift: boolean
|
||||
alt: boolean
|
||||
@@ -45,6 +47,12 @@ export function isLockBlockedShortcut(input: LockInputEvent): boolean {
|
||||
* because Ctrl+Shift+L is the switch-to-English chord on Chinese and Japanese
|
||||
* IMEs; Alt is excluded because AltGr arrives as Ctrl+Alt on most European
|
||||
* layouts. A chord that isn't exactly this must keep its original meaning.
|
||||
*
|
||||
* Matching is on `input.code` (the physical key) rather than `input.key`: the
|
||||
* key character depends on the active keyboard layout, so on Dvorak or a
|
||||
* non-Latin layout the physical L key no longer arrives as 'l' and matching
|
||||
* `key === 'l'` would silently stop locking. The code names the key position
|
||||
* and is identical on every layout.
|
||||
*/
|
||||
export function isPanicLockChord(input: LockInputEvent): boolean {
|
||||
return (
|
||||
@@ -52,6 +60,6 @@ export function isPanicLockChord(input: LockInputEvent): boolean {
|
||||
!input.shift &&
|
||||
!input.alt &&
|
||||
!input.meta &&
|
||||
input.key.toLowerCase() === 'l'
|
||||
input.code === 'KeyL'
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import zhTW from 'antd/locale/zh_TW'
|
||||
@@ -46,6 +46,9 @@ export default function App(): React.JSX.Element {
|
||||
const tabAccentColor = useSettingsStore((s) => s.settings.terminal.tabAccentColor)
|
||||
const storedLanguage = useSettingsStore((s) => s.settings.system.language ?? DEFAULT_LANGUAGE)
|
||||
const [settingsOpen, setSettingsOpen] = useState(false)
|
||||
// Stable so the memoized icon rail inside Workspace can bail out of the
|
||||
// re-renders Workspace does on every session/panel change.
|
||||
const openSettings = useCallback((): void => setSettingsOpen(true), [])
|
||||
/** Secure default: main may already hold a startup lock before this IPC
|
||||
* resolves, so the shell must not become interactive while unknown. `null`
|
||||
* means "not answered yet" and is kept distinct from "locked": the overlay
|
||||
@@ -171,7 +174,7 @@ export default function App(): React.JSX.Element {
|
||||
before the browser checks for a focused descendant. */}
|
||||
<div className="app-root" inert={locked || undefined} aria-hidden={locked || undefined}>
|
||||
<TitleBar />
|
||||
<Workspace onOpenSettings={() => setSettingsOpen(true)} />
|
||||
<Workspace onOpenSettings={openSettings} />
|
||||
<SettingsDialog open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
||||
<TransferPanel />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, Progress, Select, Switch, Tag } from 'antd'
|
||||
import { Button, Progress, Switch, Tag } from 'antd'
|
||||
import { DEFAULT_LANGUAGE, t, type Language } from '@shared/i18n'
|
||||
import type { AppInfo, ReleaseNote, UpdateState, UpdateStatus } from '@shared/ipc'
|
||||
import { useSettingsStore } from './store'
|
||||
@@ -103,11 +103,6 @@ export function AboutTab(): React.JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="about-controls">
|
||||
<Select
|
||||
value="stable"
|
||||
options={[{ value: 'stable', label: t('settings.about.channelStable') }]}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
<label className="about-autocheck">
|
||||
<Switch
|
||||
size="small"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { App, Button, Checkbox, Popover, Tooltip } from 'antd'
|
||||
import type { MenuProps } from 'antd'
|
||||
@@ -1152,28 +1152,34 @@ export default function Workspace({ onOpenSettings }: WorkspaceProps): React.JSX
|
||||
[message]
|
||||
)
|
||||
|
||||
const layoutMenu: MenuProps = {
|
||||
items: [
|
||||
{ key: 'split-right', label: t('workspace.layout.splitRight') },
|
||||
{ key: 'split-below', label: t('workspace.layout.splitBelow') },
|
||||
{ type: 'divider' },
|
||||
{ key: 'save', label: t('workspace.layout.saveTemplate') },
|
||||
{ key: 'apply', label: t('workspace.layout.applyTemplate') }
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'split-right') handleSplit('right')
|
||||
else if (key === 'split-below') handleSplit('below')
|
||||
else if (key === 'save') setSaveOpen(true)
|
||||
else if (key === 'apply') setApplyOpen(true)
|
||||
}
|
||||
}
|
||||
// Memoized so the memoized IconRail below can actually bail out: the labels
|
||||
// are translated at build time, so `language` has to be a dependency or a
|
||||
// language switch would leave stale menu text behind.
|
||||
const language = useSettingsStore((s) => s.settings.system.language)
|
||||
const layoutMenu = useMemo<MenuProps>(
|
||||
() => ({
|
||||
items: [
|
||||
{ key: 'split-right', label: t('workspace.layout.splitRight') },
|
||||
{ key: 'split-below', label: t('workspace.layout.splitBelow') },
|
||||
{ type: 'divider' },
|
||||
{ key: 'save', label: t('workspace.layout.saveTemplate') },
|
||||
{ key: 'apply', label: t('workspace.layout.applyTemplate') }
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
if (key === 'split-right') handleSplit('right')
|
||||
else if (key === 'split-below') handleSplit('below')
|
||||
else if (key === 'save') setSaveOpen(true)
|
||||
else if (key === 'apply') setApplyOpen(true)
|
||||
}
|
||||
}),
|
||||
[language, handleSplit]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="workspace-root">
|
||||
<IconRail
|
||||
onOpenSettings={onOpenSettings}
|
||||
onNewTerminal={handleRailNewTerminal}
|
||||
onSplit={handleSplit}
|
||||
onToggleSidebar={toggleSidebar}
|
||||
sidebarOpen={sidebarOpen}
|
||||
connecting={connecting}
|
||||
@@ -1394,11 +1400,14 @@ function WorkspaceBroadcastPopover({
|
||||
/**
|
||||
* XTerminal-style slim icon rail on the left edge — replaces the top toolbar.
|
||||
* Top: workspace actions; bottom: settings.
|
||||
*
|
||||
* Memoized: the workspace re-renders on every session/panel change, and the
|
||||
* rail only depends on sidebar visibility, the connecting spinner and the
|
||||
* (memoized) layout menu, so the shallow compare lets it skip those renders.
|
||||
*/
|
||||
function IconRail({
|
||||
const IconRail = memo(function IconRail({
|
||||
onOpenSettings,
|
||||
onNewTerminal,
|
||||
onSplit,
|
||||
onToggleSidebar,
|
||||
sidebarOpen,
|
||||
connecting,
|
||||
@@ -1406,7 +1415,6 @@ function IconRail({
|
||||
}: {
|
||||
onOpenSettings: () => void
|
||||
onNewTerminal: () => void
|
||||
onSplit: (direction: 'right' | 'below') => void
|
||||
onToggleSidebar: () => void
|
||||
sidebarOpen: boolean
|
||||
connecting: boolean
|
||||
@@ -1433,7 +1441,7 @@ function IconRail({
|
||||
<RailButton title={t('workspace.rail.settings')} icon={<SettingOutlined />} onClick={onOpenSettings} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/** M6.1: terminal / SSH workspace entries in the icon rail. */
|
||||
function ModeRailButtons(): React.JSX.Element {
|
||||
@@ -1464,7 +1472,7 @@ function ModeRailButtons(): React.JSX.Element {
|
||||
* rail's own stacking context, opens to the right of the rail, and closes only
|
||||
* on item pick, outside pointerdown, or Escape.
|
||||
*/
|
||||
function LayoutFlyout({ menu }: { menu: MenuProps }): React.JSX.Element {
|
||||
const LayoutFlyout = memo(function LayoutFlyout({ menu }: { menu: MenuProps }): React.JSX.Element {
|
||||
const [open, setOpen] = useState(false)
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -1527,7 +1535,7 @@ function LayoutFlyout({ menu }: { menu: MenuProps }): React.JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function RailButton({
|
||||
title,
|
||||
|
||||
@@ -307,7 +307,6 @@ const settings: Record<string, string> = {
|
||||
'settings.about.statusDownloaded': 'Download complete — restart to install',
|
||||
'settings.about.statusError': 'Check failed',
|
||||
'settings.about.statusDev': 'Update checks are unavailable in development mode',
|
||||
'settings.about.channelStable': 'Stable channel',
|
||||
'settings.about.autoCheck': 'Check automatically at startup',
|
||||
'settings.about.download': 'Download update',
|
||||
'settings.about.downloadGithub': ' (GitHub source)',
|
||||
|
||||
@@ -302,7 +302,6 @@ const settings: Record<string, string> = {
|
||||
'settings.about.statusDownloaded': 'ダウンロード完了。再起動してインストールできます',
|
||||
'settings.about.statusError': '確認に失敗しました',
|
||||
'settings.about.statusDev': '開発モードでは更新を確認できません',
|
||||
'settings.about.channelStable': '安定版チャンネル',
|
||||
'settings.about.autoCheck': '起動時に自動で確認',
|
||||
'settings.about.download': '更新をダウンロード',
|
||||
'settings.about.downloadGithub': '(GitHub ソース)',
|
||||
|
||||
@@ -291,7 +291,6 @@ const settings: Record<string, string> = {
|
||||
'settings.about.statusDownloaded': '下载完成,可重启安装',
|
||||
'settings.about.statusError': '检查失败',
|
||||
'settings.about.statusDev': '开发模式不支持更新检查',
|
||||
'settings.about.channelStable': '稳定更新通道',
|
||||
'settings.about.autoCheck': '启动时自动检查',
|
||||
'settings.about.download': '下载更新',
|
||||
'settings.about.downloadGithub': '(GitHub 源)',
|
||||
|
||||
@@ -291,7 +291,6 @@ const settings: Record<string, string> = {
|
||||
'settings.about.statusDownloaded': '下載完成,可重新啟動安裝',
|
||||
'settings.about.statusError': '檢查失敗',
|
||||
'settings.about.statusDev': '開發模式不支援檢查更新',
|
||||
'settings.about.channelStable': '穩定更新通道',
|
||||
'settings.about.autoCheck': '啟動時自動檢查',
|
||||
'settings.about.download': '下載更新',
|
||||
'settings.about.downloadGithub': '(GitHub 來源)',
|
||||
|
||||
Reference in new issue
Block a user