From acf689f97fd8a6b3dfde51c5b1c914af68f99e70 Mon Sep 17 00:00:00 2001 From: Abelino Chavez Date: Fri, 30 Jan 2026 20:31:25 -0600 Subject: [PATCH] fix: UI improvements and multiple keyboard shortcuts support - Add theme toggle to Projects page header (#689) - Fix nested button HTML validation errors in header and hero (#687) - Use asChild prop to render Links with button styles - Add ability to add multiple keyboard shortcuts per action (#696) - New "+" button to add additional shortcuts - Replace mode only removes the specific key being edited - Right-click to remove a shortcut when multiple exist Co-Authored-By: Claude Opus 4.5 --- apps/web/src/app/projects/page.tsx | 2 + .../editor/dialogs/shortcuts-dialog.tsx | 118 ++++++++++++++---- apps/web/src/components/header.tsx | 24 ++-- apps/web/src/components/landing/hero.tsx | 18 +-- 4 files changed, 117 insertions(+), 45 deletions(-) diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx index b855cefc..ea1ef522 100644 --- a/apps/web/src/app/projects/page.tsx +++ b/apps/web/src/app/projects/page.tsx @@ -56,6 +56,7 @@ import { import { DeleteProjectDialog } from "@/components/editor/dialogs/delete-project-dialog"; import { ProjectInfoDialog } from "@/components/editor/dialogs/project-info-dialog"; import { RenameProjectDialog } from "@/components/editor/dialogs/rename-project-dialog"; +import { ThemeToggle } from "@/components/theme-toggle"; import { cn } from "@/utils/ui"; const formatProjectDuration = ({ @@ -175,6 +176,7 @@ function ProjectsHeader() {
+
diff --git a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx b/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx index 565fe705..bb013a12 100644 --- a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx +++ b/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx @@ -16,6 +16,15 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { PlusSignIcon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import type { ShortcutKey } from "@/types/keybinding"; + +interface RecordingState { + shortcut: KeyboardShortcut; + mode: "add" | "replace"; + keyToReplace?: ShortcutKey; +} export function ShortcutsDialog({ isOpen, @@ -24,15 +33,15 @@ export function ShortcutsDialog({ isOpen: boolean; onOpenChange: (open: boolean) => void; }) { - const [recordingShortcut, setRecordingShortcut] = - useState(null); + const [recordingState, setRecordingState] = useState( + null, + ); const { updateKeybinding, removeKeybinding, getKeybindingString, validateKeybinding, - getKeybindingsForAction, setIsRecording, resetToDefaults, isRecording, @@ -43,7 +52,7 @@ export function ShortcutsDialog({ const categories = Array.from(new Set(shortcuts.map((s) => s.category))); useEffect(() => { - if (!isRecording || !recordingShortcut) return; + if (!isRecording || !recordingState) return; const handleKeyDown = (e: KeyboardEvent) => { e.preventDefault(); @@ -53,30 +62,31 @@ export function ShortcutsDialog({ if (keyString) { const conflict = validateKeybinding( keyString, - recordingShortcut.action, + recordingState.shortcut.action, ); if (conflict) { toast.error( `Key "${keyString}" is already bound to "${conflict.existingAction}"`, ); - setRecordingShortcut(null); + setRecordingState(null); + setIsRecording(false); return; } - const oldKeys = getKeybindingsForAction(recordingShortcut.action); - for (const key of oldKeys) { - removeKeybinding(key); + // Only remove the specific key being replaced, not all keys + if (recordingState.mode === "replace" && recordingState.keyToReplace) { + removeKeybinding(recordingState.keyToReplace); } - updateKeybinding(keyString, recordingShortcut.action); + updateKeybinding(keyString, recordingState.shortcut.action); setIsRecording(false); - setRecordingShortcut(null); + setRecordingState(null); } }; const handleClickOutside = () => { - setRecordingShortcut(null); + setRecordingState(null); setIsRecording(false); }; @@ -88,21 +98,29 @@ export function ShortcutsDialog({ document.removeEventListener("click", handleClickOutside); }; }, [ - recordingShortcut, + recordingState, getKeybindingString, updateKeybinding, removeKeybinding, validateKeybinding, - getKeybindingsForAction, setIsRecording, isRecording, ]); - const handleStartRecording = (shortcut: KeyboardShortcut) => { - setRecordingShortcut(shortcut); + const handleStartReplacing = (shortcut: KeyboardShortcut, key: ShortcutKey) => { + setRecordingState({ shortcut, mode: "replace", keyToReplace: key }); setIsRecording(true); }; + const handleStartAdding = (shortcut: KeyboardShortcut) => { + setRecordingState({ shortcut, mode: "add" }); + setIsRecording(true); + }; + + const handleRemoveKey = (key: ShortcutKey) => { + removeKeybinding(key); + }; + return ( @@ -125,9 +143,14 @@ export function ShortcutsDialog({ key={shortcut.action} shortcut={shortcut} isRecording={ - shortcut.action === recordingShortcut?.action + shortcut.action === recordingState?.shortcut.action } - onStartRecording={() => handleStartRecording(shortcut)} + recordingMode={recordingState?.mode} + onStartReplacing={(key: ShortcutKey) => + handleStartReplacing(shortcut, key) + } + onStartAdding={() => handleStartAdding(shortcut)} + onRemoveKey={handleRemoveKey} /> ))} @@ -148,11 +171,17 @@ export function ShortcutsDialog({ function ShortcutItem({ shortcut, isRecording, - onStartRecording, + recordingMode, + onStartReplacing, + onStartAdding, + onRemoveKey, }: { shortcut: KeyboardShortcut; isRecording: boolean; - onStartRecording: (params: { shortcut: KeyboardShortcut }) => void; + recordingMode?: "add" | "replace"; + onStartReplacing: (key: ShortcutKey) => void; + onStartAdding: () => void; + onRemoveKey: (key: ShortcutKey) => void; }) { const displayKeys = shortcut.keys.filter((key: string) => { if ( @@ -164,8 +193,14 @@ function ShortcutItem({ return true; }); + // Get raw keys for remove functionality (before formatting) + const { keybindings } = useKeybindingsStore(); + const rawKeys = Object.entries(keybindings) + .filter(([, action]) => action === shortcut.action) + .map(([key]) => key as ShortcutKey); + return ( -
+
{shortcut.icon && (
{shortcut.icon}
@@ -178,11 +213,17 @@ function ShortcutItem({
{key.split("+").map((keyPart: string, partIndex: number) => { const keyId = `${shortcut.id}-${index}-${partIndex}`; + const rawKey = rawKeys[index]; return ( onStartRecording({ shortcut })} + isRecording={isRecording && recordingMode === "replace"} + onStartRecording={() => rawKey && onStartReplacing(rawKey)} + onRemove={ + displayKeys.length > 1 && rawKey + ? () => onRemoveKey(rawKey) + : undefined + } > {keyPart} @@ -194,6 +235,19 @@ function ShortcutItem({ )}
))} +
); @@ -203,10 +257,12 @@ function EditableShortcutKey({ children, isRecording, onStartRecording, + onRemove, }: { children: React.ReactNode; isRecording: boolean; onStartRecording: () => void; + onRemove?: () => void; }) { const handleClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -214,13 +270,27 @@ function EditableShortcutKey({ onStartRecording(); }; + const handleRightClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (onRemove) { + onRemove(); + } + }; + return ( - + + ))}
@@ -67,18 +67,18 @@ export function Header() {
- - - - - + - + +
diff --git a/apps/web/src/components/landing/hero.tsx b/apps/web/src/components/landing/hero.tsx index 6f11ad7c..65a809be 100644 --- a/apps/web/src/components/landing/hero.tsx +++ b/apps/web/src/components/landing/hero.tsx @@ -28,17 +28,17 @@ export function Hero() {

- - - + +