feat(settings): replicate the WorkBuddy keyboard shortcuts panel and wire global bindings (TODO 12)
Recover the WorkBuddy 5.8.3 快捷键 page from REA evidence (keyboard-shortcuts-DOrivHwf.css + tokens): 49 recovered module rules, scoped light/dark tokens, the 24-row 命令/按键绑定/操作 sheet (panel 638x620, header 39, scroll 638x533, rows 41/45/44, keycaps 22) and the recording/ESC-cancel interaction. Wire the panel into SettingsDialog and keep only real bindings honest: 14 of 24 rows are marked supported, the remaining rows stay visually 1:1 but are labelled 尚未接入 and cannot pretend to save custom keys. Add Ctrl+K search focus, Ctrl+B / Ctrl+Shift+B left/right sidebar toggles, Ctrl+N new conversation (App -> Sidebar path) and Esc-to-stop while generating; fix the sidebar search focus race introduced by conditional mounting. Verify with scripts/verify-shortcuts-settings.cjs: 32 assertions covering geometry, row/key order, search filtering, recording/cancel, supported ids, dark theme surfaces and the four new global shortcuts in a real main window. All existing regressions stay green.
This commit is contained in:
1 parent
b36b52831b
commit
19460f0b13
19 files changed
+1008
-12
No files matched your search
@@ -260,12 +260,47 @@ export default function App(): React.JSX.Element {
|
||||
setResizing(true)
|
||||
}
|
||||
|
||||
// Ctrl+, 打开设置
|
||||
// 全局快捷键(TODO 12):与「设置 → 快捷键」中已接入的行保持一致。
|
||||
// Ctrl+, 设置;Ctrl+K 聚焦全局搜索;Ctrl+B / Ctrl+Shift+B 切换左/右侧栏;
|
||||
// Ctrl+N 新建对话;生成中按 Esc 停止。其余键位只在该页面展示,未接入时不宣称生效。
|
||||
const shortcutActions = useRef({
|
||||
openSettings: (): void => {},
|
||||
focusSearch: (): void => {},
|
||||
toggleSidebar: (): void => {},
|
||||
toggleRightSidebar: (): void => {},
|
||||
stopRun: (): void => {}
|
||||
})
|
||||
shortcutActions.current = {
|
||||
openSettings,
|
||||
focusSearch: () => {
|
||||
setSidebarCollapsed(false)
|
||||
if (narrow.isNarrowForSidebar) setLeftDrawerOpen(true)
|
||||
window.dispatchEvent(new CustomEvent('ycode:focus-search'))
|
||||
},
|
||||
toggleSidebar: () => {
|
||||
if (narrow.isNarrowForSidebar) setLeftDrawerOpen((value) => !value)
|
||||
else setSidebarCollapsed((value) => !value)
|
||||
},
|
||||
toggleRightSidebar: () => setRightVisible((value) => !value),
|
||||
stopRun: () => { if (useChatStore.getState().running) void useChatStore.getState().cancel() }
|
||||
}
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === ',') {
|
||||
e.preventDefault()
|
||||
openSettings()
|
||||
const onKey = (event: KeyboardEvent): void => {
|
||||
if (event.isComposing) return
|
||||
const actions = shortcutActions.current
|
||||
if (event.key === 'Escape' && !event.defaultPrevented) {
|
||||
// 与 WorkBuddy 一致:Esc 只在生成中停止本次运行,不吞掉其他弹层的 Esc。
|
||||
if (useChatStore.getState().running) { event.preventDefault(); actions.stopRun() }
|
||||
return
|
||||
}
|
||||
if ((!event.ctrlKey && !event.metaKey) || event.altKey) return
|
||||
const key = event.key.toLowerCase()
|
||||
if (key === ',') { event.preventDefault(); actions.openSettings(); return }
|
||||
if (key === 'k' && !event.shiftKey) { event.preventDefault(); actions.focusSearch(); return }
|
||||
if (key === 'b') { event.preventDefault(); if (event.shiftKey) actions.toggleRightSidebar(); else actions.toggleSidebar(); return }
|
||||
if (key === 'n' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
window.dispatchEvent(new CustomEvent('ycode:new-task'))
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSettingsStore } from '../stores/settings'
|
||||
import ModelSettingsPanel from './ModelSettingsPanel'
|
||||
import GeneralSettingsPanel from './GeneralSettingsPanel'
|
||||
import AppearanceSettingsPanel from './AppearanceSettingsPanel'
|
||||
import ShortcutsSettingsPanel from './ShortcutsSettingsPanel'
|
||||
import SourceSettingsNavigation from './SourceSettingsNavigation'
|
||||
import { useSharedTheme } from '../useSharedTheme'
|
||||
import { SOURCE_WINDOWS_SETTINGS_TABS } from './SourceSettingsNavigation'
|
||||
@@ -53,7 +54,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
|
||||
return () => window.removeEventListener('keydown', escape)
|
||||
}, [onClose])
|
||||
const modal = <section className={`settings-modal${nativeWindow ? ' settings-modal--window' : ''}`} role="dialog" aria-label="设置" aria-modal={!nativeWindow}>
|
||||
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance']} visibleTabs={VISIBLE_SETTINGS_TABS}
|
||||
<div className="settings-modal__nav"><SourceSettingsNavigation activeTab={section} supportedTabs={['settings', 'models', 'appearance', 'keyboardShortcuts']} visibleTabs={VISIBLE_SETTINGS_TABS}
|
||||
onTabChange={(id) => { setError(''); setSection(id === 'models' ? 'model' : id) }} onUnavailable={() => setError('该设置尚未接入')} /></div>
|
||||
<div className="settings-modal__content"><div className="settings-modal__panel">
|
||||
{error && <p role="alert">{error}</p>}
|
||||
@@ -61,6 +62,7 @@ export default function SettingsDialog({ theme, onThemeChange, onClose, nativeWi
|
||||
{section === 'model' && <ModelSettingsPanel models={settings.models} createRequest={createRequest} onCreateHandled={() => setCreateRequest(0)} onSave={settings.saveModel} onDelete={settings.deleteModel} onTest={(input) => window.ycode.models.test(input)} />}
|
||||
{section === 'workspace' && <div className="settings-window-panel"><h2>工作空间</h2>{settings.workspaces.map((space) => <button className="btn" key={space.id} title={space.root} onClick={() => void window.ycode.workspace.open(space.id).catch((error) => setError(String(error.message ?? error)))}>{space.name}</button>)}<button className="btn" onClick={() => void settings.attachWorkspace().catch((error) => setError(String(error.message ?? error)))}>打开已有文件夹</button></div>}
|
||||
{section === 'appearance' && <AppearanceSettingsPanel theme={theme} onThemeChange={onThemeChange} />}
|
||||
{section === 'keyboardShortcuts' && <ShortcutsSettingsPanel />}
|
||||
{section === 'about' && <div className="settings-window-panel"><h2>YCode</h2><p>{settings.appInfo?.version}</p></div>}
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 设置 → 快捷键:WorkBuddy 5.8.3 原版快捷键页的 1:1 复刻(TODO 12)。
|
||||
*
|
||||
* 结构与几何来自 REA `ev_5f3148…`(浅色)与 `ev_f4fd41…`(深色);搜索、禁用「全部恢复默认」、
|
||||
* 点击绑定进入「按下快捷键 / ESC 取消」录制态来自 REA `ev_b3e014…`。样式取自 REA 回收的
|
||||
* `keyboard-shortcuts-DOrivHwf.css`(见 source-shortcuts-panel-recovered.css)。
|
||||
*
|
||||
* YCode 侧的真实性边界:
|
||||
* - 搜索过滤、录制态/取消、深色主题是真实行为;
|
||||
* - `shortcuts-catalog.ts` 中 supported=true 的 14 行与 YCode 现有绑定一致;
|
||||
* - 其余行只还原原版展示,title 明确标注「YCode 尚未接入」,编辑后的自定义键位、删除单条键位和
|
||||
* 整表「全部恢复默认」尚未接入,按钮保持禁用,不伪造成功。
|
||||
*/
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { SHORTCUT_ROWS, type ShortcutRow } from './shortcuts-catalog'
|
||||
import './source-shortcuts-panel.css'
|
||||
|
||||
const UNSUPPORTED_HINT = 'YCode 尚未接入该快捷键'
|
||||
const EDIT_UNSUPPORTED_HINT = '自定义键位尚未接入,已保留默认绑定'
|
||||
|
||||
function TrashIcon(): React.JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M2.75 4.5h10.5M6.25 4.5V3.25c0-.414.336-.75.75-.75h2c.414 0 .75.336.75.75V4.5M4.25 4.5l.5 8.25c.02.414.36.75.775.75h5.45c.415 0 .755-.336.775-.75l.5-8.25M6.75 7v4M9.25 7v4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function matches(row: ShortcutRow, needle: string): boolean {
|
||||
if (!needle) return true
|
||||
const haystack = `${row.label} ${row.prefix ?? ''} ${row.keys}`.toLocaleLowerCase()
|
||||
return haystack.includes(needle)
|
||||
}
|
||||
|
||||
export default function ShortcutsSettingsPanel(): React.JSX.Element {
|
||||
const [query, setQuery] = useState('')
|
||||
const [recordingId, setRecordingId] = useState<string | null>(null)
|
||||
const [errorId, setErrorId] = useState<string | null>(null)
|
||||
const errorTimer = useRef<number | null>(null)
|
||||
const needle = query.trim().toLocaleLowerCase()
|
||||
const rows = useMemo(() => SHORTCUT_ROWS.filter((row) => matches(row, needle)), [needle])
|
||||
|
||||
const cancelRecording = (): void => {
|
||||
setRecordingId(null)
|
||||
}
|
||||
|
||||
const startRecording = (row: ShortcutRow): void => {
|
||||
if (!row.editable) return
|
||||
if (errorTimer.current !== null) {
|
||||
window.clearTimeout(errorTimer.current)
|
||||
errorTimer.current = null
|
||||
}
|
||||
setErrorId(null)
|
||||
setRecordingId(row.id)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!recordingId) return
|
||||
const onKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.isComposing) return
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
cancelRecording()
|
||||
return
|
||||
}
|
||||
// 只按修饰键(Ctrl/Shift/Alt/Meta)时继续等待完整组合键。
|
||||
if (['Control', 'Shift', 'Alt', 'Meta'].includes(event.key)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
setRecordingId(null)
|
||||
setErrorId(recordingId)
|
||||
if (errorTimer.current !== null) window.clearTimeout(errorTimer.current)
|
||||
errorTimer.current = window.setTimeout(() => {
|
||||
errorTimer.current = null
|
||||
setErrorId(null)
|
||||
}, 2600)
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown, true)
|
||||
return () => document.removeEventListener('keydown', onKeyDown, true)
|
||||
}, [recordingId])
|
||||
|
||||
useEffect(() => () => {
|
||||
if (errorTimer.current !== null) window.clearTimeout(errorTimer.current)
|
||||
}, [])
|
||||
|
||||
const panelClass = `keyboard-shortcuts-panel${recordingId ? ' keyboard-shortcuts-panel--recording' : ''}`
|
||||
return (
|
||||
<div className={panelClass}>
|
||||
<div className="keyboard-shortcuts-panel__toolbar">
|
||||
<div className="keyboard-shortcuts-panel__search">
|
||||
<div className="wb-input-wrapper">
|
||||
<input className="wb-input__inner" type="search" value={query} placeholder="搜索快捷键"
|
||||
aria-label="搜索快捷键" onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={(event) => { if (event.key === 'Escape' && query) { event.preventDefault(); event.stopPropagation(); setQuery('') } }} />
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="wb-button wb-button--medium wb-button--secondary wb-button--disabled" disabled
|
||||
title="暂无自定义键位,恢复默认暂未接入">全部恢复默认</button>
|
||||
</div>
|
||||
<div className="keyboard-shortcuts-panel__list">
|
||||
<div className="keyboard-shortcuts-panel__column-header">
|
||||
<span className="keyboard-shortcuts-panel__col-command">命令</span>
|
||||
<span className="keyboard-shortcuts-panel__col-binding">按键绑定</span>
|
||||
<span className="keyboard-shortcuts-panel__col-action">操作</span>
|
||||
</div>
|
||||
<div className="keyboard-shortcuts-panel__list-scroll">
|
||||
{rows.length ? rows.map((row) => {
|
||||
const recording = recordingId === row.id
|
||||
const error = errorId === row.id
|
||||
const title = row.supported
|
||||
? `${row.label}:${row.action ?? row.keys}`
|
||||
: `${UNSUPPORTED_HINT}(原版绑定:${row.keys})`
|
||||
return (
|
||||
<div key={row.id} data-shortcut-id={row.id} data-shortcut-supported={row.supported ? 'true' : 'false'}
|
||||
className={`shortcut-item${row.editable ? '' : ' shortcut-item--not-editable'}${recording ? ' shortcut-item--recording' : ''}`}>
|
||||
<div className="shortcut-item__label">{row.label}</div>
|
||||
{recording ? (
|
||||
<div className="shortcut-item__binding shortcut-item__binding--recording-layout">
|
||||
<div className="shortcut-item__recording-row">
|
||||
<span className={`shortcut-item__recording-box${error ? ' shortcut-item__recording-box--error' : ''}`}>
|
||||
<span className={`shortcut-item__recording-text${error ? ' shortcut-item__recording-text--error' : ''}`}>按下快捷键</span>
|
||||
<span className="shortcut-item__recording-cursor" aria-hidden="true" />
|
||||
</span>
|
||||
<button type="button" className="shortcut-item__esc-cancel" onClick={cancelRecording}>ESC 取消</button>
|
||||
</div>
|
||||
{error ? <span className="shortcut-item__error-hint" role="alert">{EDIT_UNSUPPORTED_HINT}</span> : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="shortcut-item__binding">
|
||||
{row.editable ? (
|
||||
<button type="button" className="shortcut-item__binding-tooltip-anchor shortcut-item__binding-tooltip-anchor--clickable"
|
||||
title={title} aria-label={`${row.label},当前绑定 ${row.prefix ? `${row.prefix} ` : ''}${row.keys}`}
|
||||
onClick={() => startRecording(row)}>
|
||||
<span className="shortcut-item__keys">{row.prefix ? <span className="shortcut-item__prefix">{row.prefix}</span> : null}<kbd className="shortcut-item__key">{row.keys}</kbd></span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="shortcut-item__binding-tooltip-anchor" title={title}>
|
||||
<span className="shortcut-item__keys"><kbd className="shortcut-item__key">{row.keys}</kbd></span>
|
||||
</div>
|
||||
)}
|
||||
{error ? <span className="shortcut-item__error-hint" role="alert">{EDIT_UNSUPPORTED_HINT}</span> : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="shortcut-item__actions">
|
||||
{row.editable ? (
|
||||
<div className="shortcut-item__action-btn-group">
|
||||
<button type="button" className="shortcut-item__action-btn shortcut-item__action-btn--delete wb-button wb-button--xsmall wb-button--ghost"
|
||||
aria-disabled="true" disabled title="删除自定义键位暂未接入">
|
||||
<span className="wb-button__icon"><TrashIcon /></span>
|
||||
</button>
|
||||
</div>
|
||||
) : <span className="shortcut-item__actions-placeholder" aria-hidden="true">—</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}) : <div className="keyboard-shortcuts-panel__empty">未找到匹配的快捷键</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -151,6 +151,14 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
|
||||
} catch (error) { setHint(error instanceof Error ? error.message : '无法新建任务') }
|
||||
finally { taskStartLock.current = false }
|
||||
}
|
||||
// 全局快捷键 Ctrl+N(TODO 12):App 广播,复用「新建任务」按钮的同一条路径。
|
||||
const newTaskRef = useRef(newTask)
|
||||
newTaskRef.current = newTask
|
||||
useEffect(() => {
|
||||
const handler = (): void => { void newTaskRef.current() }
|
||||
window.addEventListener('ycode:new-task', handler)
|
||||
return () => window.removeEventListener('ycode:new-task', handler)
|
||||
}, [])
|
||||
const openSpaceMenu = (space: WorkspaceRecord, rect: { left: number; bottom: number; top: number }, moreButton = false): void => {
|
||||
setSpaceMenu({ space, left: rect.left - (moreButton ? 16 : 0), top: rect.bottom + 4, anchorTop: rect.top })
|
||||
}
|
||||
@@ -164,6 +172,12 @@ export default function Sidebar({ collapsed, onToggleCollapse, activeRoute = 'ch
|
||||
window.addEventListener('ycode:focus-search', onFocusSearch)
|
||||
return () => window.removeEventListener('ycode:focus-search', onFocusSearch)
|
||||
}, [])
|
||||
// 搜索框是条件渲染:等提交完成后再聚焦,避免 requestAnimationFrame 先于 DOM 挂载导致焦点丢失。
|
||||
useEffect(() => {
|
||||
if (!searchVisible) return
|
||||
searchRef.current?.focus()
|
||||
searchRef.current?.select()
|
||||
}, [searchVisible])
|
||||
|
||||
// 全局「尚未实现」提示(标题栏等外围入口共用同一反馈通道)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 设置 → 快捷键:WorkBuddy 5.8.3 原版表格的 24 行命令与按键绑定。
|
||||
*
|
||||
* REA 依据(设置窗口 target 6D5A7FEE…,900×680):
|
||||
* - `ev_5f3148eb51ea4a6e5b6f2ffea5f6e3dfb94e4009ecf2d16173fa676ce7c51b4b`:浅色顶部布局/计算样式/模块 CSS
|
||||
* - `ev_7266946b025216056fc67f7c6d3c991894e985b6a8486a3153c3508e21771f73`:四段滚动采样(顶部/中/底/回顶)
|
||||
* - `ev_b3e014e5cff4971d7c5e75fefbbfefb0088db9ce655ef7ff33114cd3c4caead9`:搜索、禁用恢复默认、录制/取消交互帧
|
||||
* - `ev_f4fd417445481ef93c34a7e85be45e63327daa2acbccaf169706d7e19bacc531`:深色同页布局与计算样式
|
||||
* - `ev_528724699462fa08e2e4119bb8504c0180483c86cd844461cc969422b915650b`:最终浅色顶部稳定态
|
||||
*
|
||||
* 文案与键位读自 REA PNG 帧;几何/样式读自 REA layout JSON。前 16 行在原版可编辑(有删除按钮),
|
||||
* 后 8 行带 `shortcut-item--not-editable`、键帽不透明度 0.5、操作列显示「—」。
|
||||
*
|
||||
* `supported` 只描述 YCode 当前是否真的绑定了该动作:true 的行按键与实际行为一致;
|
||||
* false 的行只还原原版展示,通过 title 明确标注「YCode 尚未接入」,不做能力宣称。
|
||||
*/
|
||||
export interface ShortcutRow {
|
||||
id: string
|
||||
label: string
|
||||
/** 原版键帽文本(组合键用 `+` 连接、无空格)。 */
|
||||
keys: string
|
||||
/** 原版把「长按」渲染为键帽前的纯文本(仅语音录制开关一行)。 */
|
||||
prefix?: string
|
||||
editable: boolean
|
||||
supported: boolean
|
||||
/** 真实动作说明;仅 supported 行填写。 */
|
||||
action?: string
|
||||
}
|
||||
|
||||
export const SHORTCUT_ROWS: ReadonlyArray<ShortcutRow> = [
|
||||
{ id: 'global-search-open', label: '打开全局搜索', keys: 'Ctrl+K', editable: true, supported: true, action: '展开侧栏并聚焦任务搜索框' },
|
||||
{ id: 'open-settings', label: '打开设置', keys: 'Ctrl+,', editable: true, supported: true, action: '打开设置窗口' },
|
||||
{ id: 'voice-record', label: '语音录制开关', keys: 'Alt', prefix: '长按', editable: true, supported: false },
|
||||
{ id: 'conversation-search', label: '对话内搜索', keys: 'Ctrl+F', editable: true, supported: false },
|
||||
{ id: 'global-search', label: '全局搜索', keys: 'Ctrl+K', editable: true, supported: true, action: '展开侧栏并聚焦任务搜索框' },
|
||||
{ id: 'send-message', label: '发送消息', keys: 'Enter', editable: true, supported: true, action: '输入框内 Enter 发送' },
|
||||
{ id: 'newline', label: '输入时换行', keys: 'Shift+Enter', editable: true, supported: true, action: '输入框内 Shift+Enter 换行' },
|
||||
{ id: 'new-conversation', label: '新建对话', keys: 'Ctrl+N', editable: true, supported: true, action: '新建任务并切到对话页' },
|
||||
{ id: 'stop-generation', label: '停止生成', keys: 'Esc', editable: true, supported: true, action: '生成中按 Esc 停止本次运行' },
|
||||
{ id: 'previous-task', label: '上一个任务', keys: 'Ctrl+[', editable: true, supported: false },
|
||||
{ id: 'next-task', label: '下一个任务', keys: 'Ctrl+]', editable: true, supported: false },
|
||||
{ id: 'locate-task', label: '定位当前任务', keys: 'Ctrl+Shift+E', editable: true, supported: false },
|
||||
{ id: 'toggle-left-sidebar', label: '切换左侧栏', keys: 'Ctrl+B', editable: true, supported: true, action: '收起/展开左侧栏' },
|
||||
{ id: 'toggle-right-sidebar', label: '切换右侧栏', keys: 'Ctrl+Shift+B', editable: true, supported: true, action: '收起/展开右侧栏' },
|
||||
{ id: 'toggle-fullscreen', label: '进入/退出全屏', keys: 'F11', editable: true, supported: false },
|
||||
{ id: 'toggle-main-window', label: '唤起/隐藏主窗口', keys: 'Shift+Alt+W', editable: true, supported: false },
|
||||
{ id: 'font-increase', label: '放大内容字号', keys: 'Ctrl+"+"', editable: false, supported: true, action: 'Ctrl+= / Ctrl+Shift+= 提升一档字号' },
|
||||
{ id: 'font-decrease', label: '缩小内容字号', keys: 'Ctrl+"-"', editable: false, supported: true, action: 'Ctrl+- 降低一档字号' },
|
||||
{ id: 'font-reset', label: '重置内容字号', keys: 'Ctrl+0', editable: false, supported: true, action: 'Ctrl+0 恢复标准字号' },
|
||||
{ id: 'mention-picker', label: '唤起选择器', keys: '@', editable: false, supported: false },
|
||||
{ id: 'slash-command', label: '唤起斜杠命令', keys: '/', editable: false, supported: false },
|
||||
{ id: 'force-external-open', label: '按住后点击,始终外部打开', keys: 'Ctrl', editable: false, supported: false },
|
||||
{ id: 'new-tab', label: '新建标签页', keys: 'Ctrl+T', editable: false, supported: true, action: '工作台焦点内新建标签页' },
|
||||
{ id: 'close-tab', label: '关闭标签页', keys: 'Ctrl+W', editable: false, supported: true, action: '工作台焦点内关闭当前标签页' }
|
||||
]
|
||||
|
||||
export const SUPPORTED_SHORTCUT_COUNT = SHORTCUT_ROWS.filter((row) => row.supported).length
|
||||
@@ -0,0 +1,54 @@
|
||||
/* REA ev_5f3148eb51ea4a6e5b6f2ffea5f6e3dfb94e4009ecf2d16173fa676ce7c51b4b; keyboard-shortcuts-DOrivHwf.css sha256=d2c570c8d65db87c1cb9df06b59e6aa191b772330857376b85fd923aed580ae8; tokens sha256=13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696. */
|
||||
.keyboard-shortcuts-panel { box-sizing: border-box; flex-direction: column; width: 100%; min-width: 0px; height: 100%; padding: 0px; display: flex; position: relative; }
|
||||
.keyboard-shortcuts-panel__toolbar { align-items: center; gap: var(--wb-spacing-4); margin-bottom: var(--wb-spacing-5); display: flex; }
|
||||
.keyboard-shortcuts-panel__search { flex: 1 1 0%; min-width: 0px; }
|
||||
.keyboard-shortcuts-panel__column-header { border-bottom: 1px solid var(--wb-border-subtle); z-index: 1; background: var(--wb-bg-primary); align-items: center; display: flex; position: sticky; top: 0px; }
|
||||
.keyboard-shortcuts-panel__column-header span { color: var(--wb-color-text-tertiary); font-size: var(--wb-font-spec-body-md-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-md-line-height); letter-spacing: 0px; }
|
||||
.keyboard-shortcuts-panel__col-command { width: 40%; min-width: 0px; padding: var(--wb-spacing-3) var(--wb-spacing-5); text-align: left; text-overflow: ellipsis; flex-shrink: 0; overflow: hidden; }
|
||||
.keyboard-shortcuts-panel__col-binding { box-sizing: border-box; width: calc(60% - 112px); min-width: 0px; padding: var(--wb-spacing-3) var(--wb-spacing-5); text-align: left; flex-shrink: 0; overflow: hidden; }
|
||||
.keyboard-shortcuts-panel__col-action { box-sizing: border-box; width: 112px; padding: var(--wb-spacing-3) var(--wb-spacing-5); text-align: left; flex-shrink: 0; }
|
||||
.keyboard-shortcuts-panel--readonly .keyboard-shortcuts-panel__col-binding,.keyboard-shortcuts-panel--readonly .shortcut-item__binding { width: 60%; }
|
||||
.keyboard-shortcuts-panel__list { border-radius: var(--wb-radius-xl); background: var(--wb-bg-primary); border: none; flex-direction: column; flex: 0 1 auto; min-height: 0px; display: flex; overflow: hidden; }
|
||||
.keyboard-shortcuts-panel__list-scroll { scrollbar-gutter: stable; flex-direction: column; flex: 1 1 0%; min-height: 0px; display: flex; overflow: hidden auto; }
|
||||
.keyboard-shortcuts-panel__list-scroll::-webkit-scrollbar { width: 6px; }
|
||||
.keyboard-shortcuts-panel__list-scroll::-webkit-scrollbar-track { background: 0px 0px; }
|
||||
.keyboard-shortcuts-panel__list-scroll::-webkit-scrollbar-thumb { border-radius: var(--wb-radius-sm); transition: background-color var(--wb-motion-duration-base) var(--wb-motion-easing-standard); background-color: rgba(0, 0, 0, 0); }
|
||||
.keyboard-shortcuts-panel__list-scroll:hover::-webkit-scrollbar-thumb { background-color: color-mix(in srgb, var(--wb-color-text-secondary) 28%, transparent); }
|
||||
.keyboard-shortcuts-panel__empty { padding: var(--wb-spacing-10) 0; color: var(--wb-color-text-tertiary); font-size: var(--wb-font-spec-body-md-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-md-line-height); text-align: center; }
|
||||
.shortcut-item { background: var(--wb-bg-primary); max-width: 100%; transition: background-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard); align-items: center; display: flex; }
|
||||
.shortcut-item:not(:last-child) { border-bottom: 1px solid var(--wb-border-subtle); }
|
||||
.shortcut-item:hover:not(.shortcut-item--recording) { background: var(--wb-bg-hover-subtle); }
|
||||
.shortcut-item--recording { overflow: visible; }
|
||||
.shortcut-item--not-editable .shortcut-item__key { opacity: 0.5; }
|
||||
.shortcut-item__label { width: 40%; min-width: 0px; padding: var(--wb-spacing-3) var(--wb-spacing-5); color: var(--wb-color-text-primary); font-size: var(--wb-font-spec-body-md-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-md-line-height); text-overflow: ellipsis; white-space: nowrap; flex-shrink: 0; overflow: hidden; }
|
||||
.shortcut-item__binding { box-sizing: border-box; width: calc(60% - 112px); min-width: 0px; padding: var(--wb-spacing-3) var(--wb-spacing-5); flex-shrink: 0; align-items: center; gap: 0px; display: flex; overflow: hidden; }
|
||||
.shortcut-item__binding--recording-layout { flex-direction: column; align-items: flex-start; overflow: visible; }
|
||||
.shortcut-item__recording-row { align-items: center; gap: var(--wb-spacing-2); display: flex; }
|
||||
.shortcut-item__binding-tooltip-anchor { align-items: center; display: inline-flex; }
|
||||
.shortcut-item__binding-tooltip-anchor--clickable { border-radius: var(--wb-radius-sm); cursor: pointer; }
|
||||
.shortcut-item__actions { justify-content: flex-start; align-items: center; gap: var(--wb-spacing-1); box-sizing: border-box; width: 112px; padding: var(--wb-spacing-3) var(--wb-spacing-5); flex-shrink: 0; display: flex; overflow: visible; }
|
||||
.shortcut-item__action-btn-group { align-items: center; gap: var(--wb-spacing-3); display: flex; }
|
||||
.shortcut-item__action-btn-placeholder { flex-shrink: 0; width: 28px; height: 28px; }
|
||||
.shortcut-item__keys { align-items: center; gap: var(--wb-spacing-2); display: flex; }
|
||||
.shortcut-item__key { border-radius: var(--wb-radius-md); background: var(--wb-color-bg-primary-default); min-width: 22px; height: 22px; color: var(--wb-color-text-primary); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", monospace; font-size: var(--wb-font-spec-body-sm-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-sm-line-height); white-space: nowrap; justify-content: center; align-items: center; padding: 0px 6px; display: inline-flex; }
|
||||
.shortcut-item__unassigned,.shortcut-item__recording-hint { color: var(--wb-color-text-disabled); font-size: var(--wb-font-spec-body-sm-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-md-line-height); }
|
||||
.shortcut-item__error-hint { color: var(--wb-status-error); font-size: var(--wb-font-spec-body-sm-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-sm-line-height); white-space: nowrap; }
|
||||
.shortcut-item__recording-box { align-items: center; gap: var(--wb-spacing-1); min-width: 120px; min-height: 24px; padding: 0 var(--wb-spacing-3); border: 1px solid var(--wb-border-control); border-radius: var(--wb-radius-md); background: var(--wb-bg-primary); transition: border-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard); display: inline-flex; }
|
||||
.shortcut-item__recording-box--error { border-color: var(--wb-status-error); }
|
||||
.shortcut-item__recording-text { color: var(--wb-color-text-primary); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", monospace; font-size: var(--wb-font-spec-body-sm-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-sm-line-height); white-space: nowrap; }
|
||||
.shortcut-item__recording-text--error { color: var(--wb-status-error); }
|
||||
.shortcut-item__recording-cursor { background: var(--wb-color-text-primary); flex-shrink: 0; width: 1px; height: 14px; animation: 1s step-end 0s infinite normal none running recording-cursor-blink; display: inline-block; }
|
||||
.shortcut-item__action-btn.wb-button .wb-button__icon { width: var(--wb-icon-size-md); height: var(--wb-icon-size-md); }
|
||||
.shortcut-item__action-btn--reset.wb-button .wb-button__icon svg[fill="none"] { fill: none; }
|
||||
.shortcut-item__action-btn:focus-visible { outline: 2px solid var(--wb-border-focus); outline-offset: var(--wb-spacing-1); }
|
||||
.shortcut-item__actions-placeholder { width: 28px; height: 28px; color: var(--wb-color-text-secondary); font-size: var(--wb-font-body-size); opacity: 0.6; user-select: none; justify-content: center; align-items: center; display: inline-flex; }
|
||||
.shortcut-item__esc-cancel { padding: var(--wb-spacing-1) var(--wb-spacing-3); border-radius: var(--wb-radius-sm); color: var(--wb-color-text-tertiary); font-size: var(--wb-font-spec-body-sm-size); font-weight: var(--wb-font-weight-400); line-height: var(--wb-font-spec-body-sm-line-height); cursor: pointer; white-space: nowrap; transition: background-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard), color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard); }
|
||||
.shortcut-item__esc-cancel:hover { background: var(--wb-bg-hover); color: var(--wb-color-text-primary); }
|
||||
.shortcut-item__esc-cancel:focus-visible { outline: 2px solid var(--wb-border-focus); outline-offset: var(--wb-spacing-1); }
|
||||
[data-theme=dark] .shortcut-item__key { background: var(--wb-bg-hover-light); }
|
||||
@keyframes recording-cursor-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
.keyboard-shortcuts-panel{--wb-spacing-4:12px;--wb-spacing-5:16px;--wb-border-subtle:var(--wb-palette-gray-3);--wb-bg-primary:var(--wb-palette-white-100);--wb-color-text-tertiary:var(--wb-palette-black-50);--wb-font-spec-body-md-size:var(--wb-font-size-5);--wb-font-weight-400:400;--wb-font-spec-body-md-line-height:var(--wb-font-line-height-5-2);--wb-spacing-3:8px;--wb-radius-xl:12px;--wb-radius-sm:4px;--wb-motion-duration-base:.2s;--wb-motion-easing-standard:cubic-bezier(.4, 0, .2, 1);--wb-color-text-secondary:var(--wb-palette-black-70);--wb-spacing-10:48px;--wb-motion-duration-fast:.15s;--wb-bg-hover-subtle:var(--wb-palette-gray-1);--wb-color-text-primary:var(--wb-palette-black-100);--wb-spacing-2:4px;--wb-spacing-1:2px;--wb-radius-md:6px;--wb-color-bg-primary-default:var(--wb-palette-gray-3);--wb-font-spec-body-sm-size:var(--wb-font-size-3);--wb-font-spec-body-sm-line-height:var(--wb-font-line-height-5);--wb-color-text-disabled:var(--wb-palette-black-30);--wb-status-error:var(--wb-palette-red-8);--wb-border-control:var(--wb-palette-gray-4);--wb-icon-size-md:var(--wb-size-4);--wb-border-focus:var(--wb-color-border-focus);--wb-font-body-size:var(--wb-font-size-5);--wb-bg-hover:color-mix(in srgb, var(--wb-palette-black-100) 5%, transparent);--wb-bg-hover-light:var(--wb-palette-gray-3);--wb-palette-gray-3:#f2f2f2;--wb-palette-white-100:#fff;--wb-palette-black-50:#00000080;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-black-70:#000000b3;--wb-palette-gray-1:#fafafa;--wb-bg-tertiary:var(--wb-color-bg-primary-hover);--wb-palette-black-100:#000;--wb-font-size-3:12px;--wb-font-line-height-5:20px;--wb-palette-black-30:#0000004d;--wb-palette-red-8:#f64041;--wb-color-text-error-default:var(--wb-palette-red-8);--wb-palette-gray-4:#ebebeb;--wb-size-4:16px;--wb-color-border-focus:var(--wb-palette-black-75);--wb-color-bg-primary-hover:var(--wb-palette-gray-4);--wb-palette-red-7:#e85c5c;--wb-palette-black-75:#000000bf;--wb-palette-white-70:#ffffffb3}
|
||||
:root[data-theme=dark] .keyboard-shortcuts-panel{--wb-spacing-4:12px;--wb-spacing-5:16px;--wb-border-subtle:color-mix(in srgb, var(--wb-palette-white-100) 6%, transparent);--wb-bg-primary:var(--wb-palette-gray-3);--wb-color-text-tertiary:#ffffff80;--wb-font-spec-body-md-size:var(--wb-font-size-5);--wb-font-weight-400:400;--wb-font-spec-body-md-line-height:var(--wb-font-line-height-5-2);--wb-spacing-3:8px;--wb-radius-xl:12px;--wb-radius-sm:4px;--wb-motion-duration-base:.2s;--wb-motion-easing-standard:cubic-bezier(.4, 0, .2, 1);--wb-color-text-secondary:#ffffffa6;--wb-spacing-10:48px;--wb-motion-duration-fast:.15s;--wb-bg-hover-subtle:var(--wb-bg-tertiary);--wb-color-text-primary:#fff;--wb-spacing-2:4px;--wb-spacing-1:2px;--wb-radius-md:6px;--wb-color-bg-primary-default:var(--wb-palette-gray-3);--wb-font-spec-body-sm-size:var(--wb-font-size-3);--wb-font-spec-body-sm-line-height:var(--wb-font-line-height-5);--wb-color-text-disabled:#ffffff59;--wb-status-error:var(--wb-color-text-error-default);--wb-border-control:color-mix(in srgb, var(--wb-palette-white-100) 22%, transparent);--wb-icon-size-md:var(--wb-size-4);--wb-border-focus:var(--wb-color-border-focus);--wb-font-body-size:var(--wb-font-size-5);--wb-bg-hover:color-mix(in srgb, var(--wb-palette-white-100) 6%, transparent);--wb-bg-hover-light:color-mix(in srgb, var(--wb-palette-white-100) 8%, transparent);--wb-palette-gray-3:#1f1f1f;--wb-palette-white-100:#fff;--wb-palette-black-50:#00000080;--wb-font-size-5:14px;--wb-font-line-height-5-2:22px;--wb-palette-black-70:#000000b3;--wb-palette-gray-1:#141414;--wb-bg-tertiary:var(--wb-palette-gray-4);--wb-palette-black-100:#000;--wb-font-size-3:12px;--wb-font-line-height-5:20px;--wb-palette-black-30:#0000004d;--wb-palette-red-8:#af191a;--wb-color-text-error-default:var(--wb-palette-red-7);--wb-palette-gray-4:#242424;--wb-size-4:16px;--wb-color-border-focus:var(--wb-palette-white-70);--wb-color-bg-primary-hover:var(--wb-palette-gray-4);--wb-palette-red-7:#a31717;--wb-palette-black-75:#000000bf;--wb-palette-white-70:#ffffffb3}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 设置 → 快捷键(TODO 12)。
|
||||
* 第一段:REA 回收的原版模块样式(keyboard-shortcuts-DOrivHwf.css 的投影)。
|
||||
* 第二段:本地适配——原版把输入/按钮基件放在 UI kit 样式表里,这里只补快捷键面板引用到的那几个基件,
|
||||
* 取值来自 REA `ev_5f3148…` 的同页 computed style(浅色)与 tokens 深色取值。
|
||||
*/
|
||||
@import './source-shortcuts-panel-recovered.css';
|
||||
|
||||
/* ---- UI kit 基件适配:搜索输入框(原版 .wb-input-wrapper / .wb-input__inner) ---- */
|
||||
.keyboard-shortcuts-panel .wb-input-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--wb-spacing-2);
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 4px 0 8px;
|
||||
background: var(--wb-bg-primary);
|
||||
border: 1px solid var(--wb-input-border, #e6e6e6);
|
||||
border-radius: 8px;
|
||||
transition: border-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard);
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-input-wrapper:hover {
|
||||
border-color: var(--wb-input-border-hover, #d9d9d9);
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-input-wrapper:focus-within {
|
||||
border-color: var(--wb-color-border-focus);
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-input__inner {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
color: var(--wb-color-text-primary);
|
||||
font: inherit;
|
||||
font-size: var(--wb-font-spec-body-md-size);
|
||||
line-height: var(--wb-font-spec-body-md-line-height);
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-input__inner::placeholder {
|
||||
color: var(--wb-color-text-tertiary);
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-input__inner::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
}
|
||||
:root[data-theme=dark] .keyboard-shortcuts-panel .wb-input-wrapper {
|
||||
border-color: color-mix(in srgb, var(--wb-palette-white-100) 15%, transparent);
|
||||
}
|
||||
|
||||
/* ---- UI kit 基件适配:按钮(原版 .wb-button / .wb-button--medium / --xsmall / --ghost) ---- */
|
||||
.keyboard-shortcuts-panel .wb-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: PingFang SC, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard),
|
||||
color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard),
|
||||
border-color var(--wb-motion-duration-fast) var(--wb-motion-easing-standard);
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--medium {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
gap: 4px;
|
||||
font-size: var(--wb-font-spec-body-md-size);
|
||||
line-height: var(--wb-font-spec-body-md-line-height);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--secondary {
|
||||
color: var(--wb-color-text-primary);
|
||||
background: var(--wb-bg-primary);
|
||||
border: 1px solid var(--wb-button-secondary-border, #ebebeb);
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--secondary.wb-button--disabled,
|
||||
.keyboard-shortcuts-panel .wb-button--secondary:disabled {
|
||||
color: var(--wb-color-text-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--xsmall {
|
||||
/* REA ev_5f3148…:可编辑行高 41 = 8 + 24 + 8 + 1(行分隔线),删除按钮为 24×24。 */
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--xsmall.wb-button--icon-only {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--ghost {
|
||||
color: var(--wb-color-text-secondary);
|
||||
background: transparent;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--ghost:hover:not(:disabled) {
|
||||
background: var(--wb-bg-hover);
|
||||
color: var(--wb-color-text-primary);
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button--ghost:disabled {
|
||||
color: var(--wb-color-text-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--wb-icon-size-md);
|
||||
height: var(--wb-icon-size-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.keyboard-shortcuts-panel .wb-button__icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: currentColor;
|
||||
}
|
||||
:root[data-theme=dark] .keyboard-shortcuts-panel .wb-button--secondary {
|
||||
border-color: var(--wb-border-subtle);
|
||||
}
|
||||
/*
|
||||
* REA ev_f4fd41…(深色同页 layout):`.shortcut-item__label` / `.shortcut-item__key` 的 color 为
|
||||
* rgb(242,243,245)。原版设置窗口作用域把 `--wb-color-text-primary` 覆盖成 #f2f3f5,比全局 tokens
|
||||
* 的 #fff 更具体;这里按同一优先级还原,避免正文变成纯白。
|
||||
*/
|
||||
:root[data-theme=dark] .keyboard-shortcuts-panel {
|
||||
--wb-color-text-primary: #f2f3f5;
|
||||
}
|
||||
|
||||
/* ---- 可点击绑定锚点:用 button 承载,去掉浏览器默认外观 ---- */
|
||||
.keyboard-shortcuts-panel button.shortcut-item__binding-tooltip-anchor {
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
.keyboard-shortcuts-panel .shortcut-item__content,
|
||||
.keyboard-shortcuts-panel .shortcut-item__binding .shortcut-item__error-hint {
|
||||
margin-left: var(--wb-spacing-3);
|
||||
}
|
||||
.keyboard-shortcuts-panel .shortcut-item__prefix {
|
||||
margin-right: var(--wb-spacing-2);
|
||||
color: var(--wb-color-text-primary);
|
||||
font-size: var(--wb-font-spec-body-sm-size);
|
||||
line-height: var(--wb-font-spec-body-sm-line-height);
|
||||
}
|
||||
Reference in new issue
Block a user