mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
feat(sidebar): keyboard shortcuts for stack menu actions (#729)
* feat(sidebar): implement keyboard shortcuts for stack menu actions Shortcut labels shown in the context menu and kebab menu were purely decorative. This wires them up to the corresponding actions on the currently selected stack. Cmd/Ctrl shortcuts: Enter (deploy), . (stop), R (restart), Up (update), Backspace (delete). Single-key shortcuts: a (alerts), h (auto-heal, paid), u (check updates), p (pin/unpin). Guards: shortcuts are blocked when an input element is focused, when the global command palette dialog is open, when no stack is selected, or when the stack is busy. All visibility and busy flags from the menu context are respected. * docs(sidebar): document keyboard shortcuts for stack actions
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import type { StackMenuCtx } from '@/components/sidebar/sidebar-types';
|
||||
|
||||
function isInputFocused(): boolean {
|
||||
const el = document.activeElement as HTMLElement | null;
|
||||
if (!el) return false;
|
||||
const tag = el.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
|
||||
}
|
||||
|
||||
function isPaletteOpen(): boolean {
|
||||
return !!document.querySelector('[role="dialog"] [cmdk-root]');
|
||||
}
|
||||
|
||||
export function useStackKeyboardShortcuts(
|
||||
selectedFile: string | null,
|
||||
buildMenuCtx: (file: string) => StackMenuCtx,
|
||||
) {
|
||||
const selectedFileRef = useRef(selectedFile);
|
||||
const buildMenuCtxRef = useRef(buildMenuCtx);
|
||||
|
||||
useEffect(() => { selectedFileRef.current = selectedFile; }, [selectedFile]);
|
||||
useEffect(() => { buildMenuCtxRef.current = buildMenuCtx; }, [buildMenuCtx]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
const file = selectedFileRef.current;
|
||||
if (!file) return;
|
||||
if (isInputFocused()) return;
|
||||
if (isPaletteOpen()) return;
|
||||
|
||||
const cmdOrCtrl = e.metaKey || e.ctrlKey;
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
const isCmdKey = cmdOrCtrl && ['enter', '.', 'r', 'arrowup', 'backspace'].includes(key);
|
||||
const isSingleKey = !cmdOrCtrl && ['a', 'h', 'u', 'p'].includes(key);
|
||||
if (!isCmdKey && !isSingleKey) return;
|
||||
|
||||
const ctx = buildMenuCtxRef.current(file);
|
||||
const { showDeploy, showStop, showRestart, showUpdate } = ctx.menuVisibility;
|
||||
|
||||
if (cmdOrCtrl) {
|
||||
if (key === 'enter' && showDeploy && !ctx.isBusy) {
|
||||
e.preventDefault();
|
||||
ctx.deploy();
|
||||
} else if (key === '.' && showStop && !ctx.isBusy) {
|
||||
e.preventDefault();
|
||||
ctx.stop();
|
||||
} else if (key === 'r' && showRestart && !ctx.isBusy) {
|
||||
e.preventDefault();
|
||||
ctx.restart();
|
||||
} else if (key === 'arrowup' && showUpdate && !ctx.isBusy) {
|
||||
e.preventDefault();
|
||||
ctx.update();
|
||||
} else if (key === 'backspace' && ctx.canDelete && !ctx.isBusy) {
|
||||
e.preventDefault();
|
||||
ctx.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'a') {
|
||||
e.preventDefault();
|
||||
ctx.openAlertSheet();
|
||||
} else if (key === 'h' && ctx.isPaid) {
|
||||
e.preventDefault();
|
||||
ctx.openAutoHeal();
|
||||
} else if (key === 'u') {
|
||||
e.preventDefault();
|
||||
ctx.checkUpdates();
|
||||
} else if (key === 'p') {
|
||||
e.preventDefault();
|
||||
if (ctx.isPinned) ctx.unpin();
|
||||
else ctx.pin();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}
|
||||
Reference in New Issue
Block a user