Merge branch 'feat/keyboard-shortcuts' into 'main'

feat(editor): keyboard shortcuts j/k/Enter/n per handoff

See merge request 2-6/2-6-4/terravault/ordinis!60
This commit is contained in:
Александр Зимин
2026-05-11 13:00:50 +00:00
@@ -331,6 +331,48 @@ function DictionaryDetail() {
? 'indeterminate' ? 'indeterminate'
: allVisibleSelected : allVisibleSelected
// === Keyboard shortcuts per handoff Screen 2 line 438 ===
// j/k — move row cursor up/down
// Enter — open drawer для cursor row
// Esc — close drawer (handled Radix natively)
// n — open new-record drawer
// / — focus search (handled TopBar ⌘K shortcut; tab-style / focuses search)
//
// Cursor — index в visibleRecords. -1 когда нет focus (initial state).
const [rowCursor, setRowCursor] = useState(-1)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const target = e.target as HTMLElement
// Skip когда юзер в textarea/input — не мешаем typing.
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return
if (target && target.isContentEditable) return
// Skip когда drawer / modal открыт (focus inside Radix portal).
if (edit.kind !== 'closed') return
// Skip если другие routes — ловим только на /dictionaries/$name тут.
if (e.key === 'j' || e.key === 'ArrowDown') {
e.preventDefault()
setRowCursor((c) => Math.min(visibleRecords.length - 1, c + 1))
} else if (e.key === 'k' || e.key === 'ArrowUp') {
e.preventDefault()
setRowCursor((c) => Math.max(0, c - 1))
} else if (e.key === 'Enter' && rowCursor >= 0 && rowCursor < visibleRecords.length) {
e.preventDefault()
setEdit({ kind: 'edit', record: visibleRecords[rowCursor] })
} else if (e.key === 'n' && canMutate) {
e.preventDefault()
setEdit({ kind: 'create' })
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [visibleRecords, rowCursor, edit.kind, canMutate])
// Reset cursor когда visibleRecords меняется (filter, page).
useEffect(() => {
setRowCursor(-1)
}, [page, search, scopeFilter, aoi])
const toggleSelect = (key: string) => { const toggleSelect = (key: string) => {
setSelection((prev) => { setSelection((prev) => {
const next = new Set(prev) const next = new Set(prev)
@@ -790,8 +832,12 @@ function DictionaryDetail() {
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{visibleRecords.map((r) => ( {visibleRecords.map((r, idx) => (
<TableRow key={r.id} data-selected={selection.has(r.businessKey)}> <TableRow
key={r.id}
data-selected={selection.has(r.businessKey)}
className={rowCursor === idx ? 'bg-accent-bg/40 outline outline-1 outline-accent/30' : undefined}
>
{canMutate && ( {canMutate && (
<TableCell> <TableCell>
<Checkbox <Checkbox