Skip to content

Commit 9b92e9a

Browse files
authored
feat: Initial keyboard shortcut sheet (#338)
TBH I didn't even know we had half of these keyboard shortcuts until I made the shortcut sheet, now I'm zipping around much faster, I suspect others will do the same.
1 parent e0bd971 commit 9b92e9a

File tree

16 files changed

+497
-626
lines changed

16 files changed

+497
-626
lines changed

apps/array/src/renderer/components/GlobalEventHandlers.tsx

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
22
import { useRightSidebarStore } from "@features/right-sidebar";
33
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
4+
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
45
import { clearApplicationStorage } from "@renderer/lib/clearStorage";
56
import { useNavigationStore } from "@stores/navigationStore";
67
import { useCallback, useEffect } from "react";
@@ -9,11 +10,13 @@ import { trpcReact } from "@/renderer/trpc";
910

1011
interface GlobalEventHandlersProps {
1112
onToggleCommandMenu: () => void;
13+
onToggleShortcutsSheet: () => void;
1214
commandMenuOpen: boolean;
1315
}
1416

1517
export function GlobalEventHandlers({
1618
onToggleCommandMenu,
19+
onToggleShortcutsSheet,
1720
commandMenuOpen,
1821
}: GlobalEventHandlersProps) {
1922
const toggleSettings = useNavigationStore((state) => state.toggleSettings);
@@ -26,13 +29,9 @@ export function GlobalEventHandlers({
2629
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
2730
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);
2831

29-
const handleOpenSettings = useCallback(
30-
(data?: unknown) => {
31-
if (!data) return;
32-
toggleSettings();
33-
},
34-
[toggleSettings],
35-
);
32+
const handleOpenSettings = useCallback(() => {
33+
toggleSettings();
34+
}, [toggleSettings]);
3635

3736
const handleFocusTaskMode = useCallback(
3837
(data?: unknown) => {
@@ -56,55 +55,33 @@ export function GlobalEventHandlers({
5655
clearApplicationStorage();
5756
}, []);
5857

59-
// Keyboard hotkeys
60-
useHotkeys("mod+k", onToggleCommandMenu, {
61-
enabled: !commandMenuOpen,
58+
const globalOptions = {
6259
enableOnFormTags: true,
6360
enableOnContentEditable: true,
6461
preventDefault: true,
65-
});
66-
useHotkeys("mod+t", onToggleCommandMenu, {
67-
enabled: !commandMenuOpen,
68-
enableOnFormTags: true,
69-
enableOnContentEditable: true,
62+
} as const;
63+
64+
const nonEditorOptions = {
65+
enableOnFormTags: false,
66+
enableOnContentEditable: false,
7067
preventDefault: true,
71-
});
72-
useHotkeys("mod+p", onToggleCommandMenu, {
68+
} as const;
69+
70+
useHotkeys(SHORTCUTS.COMMAND_MENU, onToggleCommandMenu, {
71+
...globalOptions,
7372
enabled: !commandMenuOpen,
74-
enableOnFormTags: true,
75-
enableOnContentEditable: true,
76-
preventDefault: true,
77-
});
78-
useHotkeys("mod+n", handleFocusTaskMode, {
79-
enableOnFormTags: true,
80-
enableOnContentEditable: true,
81-
preventDefault: true,
82-
});
83-
useHotkeys("mod+,", handleOpenSettings, {
84-
enableOnFormTags: true,
85-
enableOnContentEditable: true,
86-
preventDefault: true,
87-
});
88-
useHotkeys("mod+[", goBack, {
89-
enableOnFormTags: true,
90-
enableOnContentEditable: true,
91-
preventDefault: true,
92-
});
93-
useHotkeys("mod+]", goForward, {
94-
enableOnFormTags: true,
95-
enableOnContentEditable: true,
96-
preventDefault: true,
97-
});
98-
useHotkeys("mod+b", toggleLeftSidebar, {
99-
enableOnFormTags: true,
100-
enableOnContentEditable: true,
101-
preventDefault: true,
102-
});
103-
useHotkeys("mod+shift+b", toggleRightSidebar, {
104-
enableOnFormTags: true,
105-
enableOnContentEditable: true,
106-
preventDefault: true,
10773
});
74+
useHotkeys(SHORTCUTS.NEW_TASK, handleFocusTaskMode, globalOptions);
75+
useHotkeys(SHORTCUTS.SETTINGS, handleOpenSettings, globalOptions);
76+
useHotkeys(SHORTCUTS.GO_BACK, goBack, globalOptions);
77+
useHotkeys(SHORTCUTS.GO_FORWARD, goForward, globalOptions);
78+
useHotkeys(
79+
SHORTCUTS.TOGGLE_LEFT_SIDEBAR,
80+
toggleLeftSidebar,
81+
nonEditorOptions,
82+
);
83+
useHotkeys(SHORTCUTS.TOGGLE_RIGHT_SIDEBAR, toggleRightSidebar, globalOptions);
84+
useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions);
10885

10986
// Mouse back/forward buttons
11087
useEffect(() => {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { Box, Dialog, Flex, Kbd, Text } from "@radix-ui/themes";
2+
import {
3+
CATEGORY_LABELS,
4+
formatHotkey,
5+
getShortcutsByCategory,
6+
type ShortcutCategory,
7+
} from "@renderer/constants/keyboard-shortcuts";
8+
import { useMemo } from "react";
9+
10+
interface KeyboardShortcutsSheetProps {
11+
open: boolean;
12+
onOpenChange: (open: boolean) => void;
13+
}
14+
15+
export function KeyboardShortcutsSheet({
16+
open,
17+
onOpenChange,
18+
}: KeyboardShortcutsSheetProps) {
19+
const shortcutsByCategory = useMemo(() => getShortcutsByCategory(), []);
20+
21+
// Order categories for display
22+
const categoryOrder: ShortcutCategory[] = [
23+
"general",
24+
"navigation",
25+
"panels",
26+
"taskList",
27+
"editor",
28+
];
29+
30+
return (
31+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
32+
<Dialog.Content
33+
maxWidth="600px"
34+
style={{ maxHeight: "80vh", overflow: "hidden" }}
35+
>
36+
<Dialog.Title size="4" mb="4">
37+
Keyboard Shortcuts
38+
</Dialog.Title>
39+
40+
<Box
41+
style={{
42+
overflowY: "auto",
43+
maxHeight: "calc(80vh - 100px)",
44+
paddingRight: "8px",
45+
}}
46+
>
47+
<Flex direction="column" gap="5">
48+
{categoryOrder.map((category) => {
49+
const shortcuts = shortcutsByCategory[category];
50+
if (shortcuts.length === 0) return null;
51+
52+
// Deduplicate shortcuts with same description (e.g., multiple keys for command menu)
53+
const uniqueShortcuts = shortcuts.reduce(
54+
(acc, shortcut) => {
55+
const existing = acc.find(
56+
(s) => s.description === shortcut.description,
57+
);
58+
if (existing) {
59+
// Keep the first one (primary shortcut)
60+
return acc;
61+
}
62+
return [...acc, shortcut];
63+
},
64+
[] as typeof shortcuts,
65+
);
66+
67+
return (
68+
<Flex key={category} direction="column" gap="2">
69+
<Text size="2" weight="bold" color="gray">
70+
{CATEGORY_LABELS[category]}
71+
</Text>
72+
<Box
73+
style={{
74+
borderRadius: "var(--radius-2)",
75+
border: "1px solid var(--gray-5)",
76+
overflow: "hidden",
77+
}}
78+
>
79+
{uniqueShortcuts.map((shortcut, index) => (
80+
<Flex
81+
key={shortcut.id}
82+
align="center"
83+
justify="between"
84+
px="3"
85+
py="2"
86+
style={{
87+
borderBottom:
88+
index < uniqueShortcuts.length - 1
89+
? "1px solid var(--gray-4)"
90+
: undefined,
91+
backgroundColor:
92+
index % 2 === 0 ? "var(--gray-2)" : "var(--gray-1)",
93+
}}
94+
>
95+
<Flex direction="column" gap="1">
96+
<Text size="2">{shortcut.description}</Text>
97+
{shortcut.context && (
98+
<Text size="1" color="gray">
99+
{shortcut.context}
100+
</Text>
101+
)}
102+
</Flex>
103+
<ShortcutKeys keys={shortcut.keys} />
104+
</Flex>
105+
))}
106+
</Box>
107+
</Flex>
108+
);
109+
})}
110+
</Flex>
111+
</Box>
112+
113+
<Flex justify="end" mt="4">
114+
<Dialog.Close>
115+
<Text
116+
size="1"
117+
color="gray"
118+
style={{ cursor: "pointer" }}
119+
onClick={() => onOpenChange(false)}
120+
>
121+
Press <Kbd size="1">Esc</Kbd> to close
122+
</Text>
123+
</Dialog.Close>
124+
</Flex>
125+
</Dialog.Content>
126+
</Dialog.Root>
127+
);
128+
}
129+
130+
function ShortcutKeys({ keys }: { keys: string }) {
131+
const formatted = formatHotkey(keys);
132+
const isMac =
133+
typeof navigator !== "undefined" && /Mac/.test(navigator.platform);
134+
135+
if (isMac) {
136+
return (
137+
<Kbd size="2" style={{ fontFamily: "system-ui" }}>
138+
{formatted}
139+
</Kbd>
140+
);
141+
}
142+
143+
const keyParts = formatted.split("+");
144+
return (
145+
<Flex gap="1" align="center">
146+
{keyParts.map((part) => (
147+
<Kbd key={part} size="2">
148+
{part}
149+
</Kbd>
150+
))}
151+
</Flex>
152+
);
153+
}

apps/array/src/renderer/components/MainLayout.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { HeaderRow } from "@components/HeaderRow";
2+
import { KeyboardShortcutsSheet } from "@components/KeyboardShortcutsSheet";
23
import { StatusBar } from "@components/StatusBar";
34
import { UpdatePrompt } from "@components/UpdatePrompt";
45
import { CommandMenu } from "@features/command/components/CommandMenu";
@@ -10,6 +11,7 @@ import { TaskInput } from "@features/task-detail/components/TaskInput";
1011
import { useIntegrations } from "@hooks/useIntegrations";
1112
import { Box, Flex } from "@radix-ui/themes";
1213
import { useNavigationStore } from "@stores/navigationStore";
14+
import { useShortcutsSheetStore } from "@stores/shortcutsSheetStore";
1315
import { useCallback, useState } from "react";
1416
import { Toaster } from "sonner";
1517
import { useTaskDeepLink } from "../hooks/useTaskDeepLink";
@@ -18,8 +20,12 @@ import { GlobalEventHandlers } from "./GlobalEventHandlers";
1820
export function MainLayout() {
1921
const { view } = useNavigationStore();
2022
const [commandMenuOpen, setCommandMenuOpen] = useState(false);
23+
const {
24+
isOpen: shortcutsSheetOpen,
25+
toggle: toggleShortcutsSheet,
26+
close: closeShortcutsSheet,
27+
} = useShortcutsSheetStore();
2128

22-
// Initialize integrations
2329
useIntegrations();
2430
useTaskDeepLink();
2531

@@ -53,9 +59,14 @@ export function MainLayout() {
5359
<StatusBar />
5460

5561
<CommandMenu open={commandMenuOpen} onOpenChange={setCommandMenuOpen} />
62+
<KeyboardShortcutsSheet
63+
open={shortcutsSheetOpen}
64+
onOpenChange={(open) => (open ? null : closeShortcutsSheet())}
65+
/>
5666
<UpdatePrompt />
5767
<GlobalEventHandlers
5868
onToggleCommandMenu={handleToggleCommandMenu}
69+
onToggleShortcutsSheet={toggleShortcutsSheet}
5970
commandMenuOpen={commandMenuOpen}
6071
/>
6172
<Toaster position="bottom-right" />

0 commit comments

Comments
 (0)