@@ -16,6 +16,15 @@ import {
1616 DialogHeader ,
1717 DialogTitle ,
1818} from "@/components/ui/dialog" ;
19+ import { PlusSignIcon } from "@hugeicons/core-free-icons" ;
20+ import { HugeiconsIcon } from "@hugeicons/react" ;
21+ import type { ShortcutKey } from "@/types/keybinding" ;
22+
23+ interface RecordingState {
24+ shortcut : KeyboardShortcut ;
25+ mode : "add" | "replace" ;
26+ keyToReplace ?: ShortcutKey ;
27+ }
1928
2029export function ShortcutsDialog ( {
2130 isOpen,
@@ -24,15 +33,15 @@ export function ShortcutsDialog({
2433 isOpen : boolean ;
2534 onOpenChange : ( open : boolean ) => void ;
2635} ) {
27- const [ recordingShortcut , setRecordingShortcut ] =
28- useState < KeyboardShortcut | null > ( null ) ;
36+ const [ recordingState , setRecordingState ] = useState < RecordingState | null > (
37+ null ,
38+ ) ;
2939
3040 const {
3141 updateKeybinding,
3242 removeKeybinding,
3343 getKeybindingString,
3444 validateKeybinding,
35- getKeybindingsForAction,
3645 setIsRecording,
3746 resetToDefaults,
3847 isRecording,
@@ -43,7 +52,7 @@ export function ShortcutsDialog({
4352 const categories = Array . from ( new Set ( shortcuts . map ( ( s ) => s . category ) ) ) ;
4453
4554 useEffect ( ( ) => {
46- if ( ! isRecording || ! recordingShortcut ) return ;
55+ if ( ! isRecording || ! recordingState ) return ;
4756
4857 const handleKeyDown = ( e : KeyboardEvent ) => {
4958 e . preventDefault ( ) ;
@@ -53,30 +62,31 @@ export function ShortcutsDialog({
5362 if ( keyString ) {
5463 const conflict = validateKeybinding (
5564 keyString ,
56- recordingShortcut . action ,
65+ recordingState . shortcut . action ,
5766 ) ;
5867 if ( conflict ) {
5968 toast . error (
6069 `Key "${ keyString } " is already bound to "${ conflict . existingAction } "` ,
6170 ) ;
62- setRecordingShortcut ( null ) ;
71+ setRecordingState ( null ) ;
72+ setIsRecording ( false ) ;
6373 return ;
6474 }
6575
66- const oldKeys = getKeybindingsForAction ( recordingShortcut . action ) ;
67- for ( const key of oldKeys ) {
68- removeKeybinding ( key ) ;
76+ // Only remove the specific key being replaced, not all keys
77+ if ( recordingState . mode === "replace" && recordingState . keyToReplace ) {
78+ removeKeybinding ( recordingState . keyToReplace ) ;
6979 }
7080
71- updateKeybinding ( keyString , recordingShortcut . action ) ;
81+ updateKeybinding ( keyString , recordingState . shortcut . action ) ;
7282
7383 setIsRecording ( false ) ;
74- setRecordingShortcut ( null ) ;
84+ setRecordingState ( null ) ;
7585 }
7686 } ;
7787
7888 const handleClickOutside = ( ) => {
79- setRecordingShortcut ( null ) ;
89+ setRecordingState ( null ) ;
8090 setIsRecording ( false ) ;
8191 } ;
8292
@@ -88,21 +98,29 @@ export function ShortcutsDialog({
8898 document . removeEventListener ( "click" , handleClickOutside ) ;
8999 } ;
90100 } , [
91- recordingShortcut ,
101+ recordingState ,
92102 getKeybindingString ,
93103 updateKeybinding ,
94104 removeKeybinding ,
95105 validateKeybinding ,
96- getKeybindingsForAction ,
97106 setIsRecording ,
98107 isRecording ,
99108 ] ) ;
100109
101- const handleStartRecording = ( shortcut : KeyboardShortcut ) => {
102- setRecordingShortcut ( shortcut ) ;
110+ const handleStartReplacing = ( shortcut : KeyboardShortcut , key : ShortcutKey ) => {
111+ setRecordingState ( { shortcut, mode : "replace" , keyToReplace : key } ) ;
103112 setIsRecording ( true ) ;
104113 } ;
105114
115+ const handleStartAdding = ( shortcut : KeyboardShortcut ) => {
116+ setRecordingState ( { shortcut, mode : "add" } ) ;
117+ setIsRecording ( true ) ;
118+ } ;
119+
120+ const handleRemoveKey = ( key : ShortcutKey ) => {
121+ removeKeybinding ( key ) ;
122+ } ;
123+
106124 return (
107125 < Dialog open = { isOpen } onOpenChange = { onOpenChange } >
108126 < DialogContent className = "flex max-h-[80vh] max-w-2xl flex-col p-0" >
@@ -125,9 +143,14 @@ export function ShortcutsDialog({
125143 key = { shortcut . action }
126144 shortcut = { shortcut }
127145 isRecording = {
128- shortcut . action === recordingShortcut ?. action
146+ shortcut . action === recordingState ?. shortcut . action
147+ }
148+ recordingMode = { recordingState ?. mode }
149+ onStartReplacing = { ( key : ShortcutKey ) =>
150+ handleStartReplacing ( shortcut , key )
129151 }
130- onStartRecording = { ( ) => handleStartRecording ( shortcut ) }
152+ onStartAdding = { ( ) => handleStartAdding ( shortcut ) }
153+ onRemoveKey = { handleRemoveKey }
131154 />
132155 ) ) }
133156 </ div >
@@ -148,11 +171,17 @@ export function ShortcutsDialog({
148171function ShortcutItem ( {
149172 shortcut,
150173 isRecording,
151- onStartRecording,
174+ recordingMode,
175+ onStartReplacing,
176+ onStartAdding,
177+ onRemoveKey,
152178} : {
153179 shortcut : KeyboardShortcut ;
154180 isRecording : boolean ;
155- onStartRecording : ( params : { shortcut : KeyboardShortcut } ) => void ;
181+ recordingMode ?: "add" | "replace" ;
182+ onStartReplacing : ( key : ShortcutKey ) => void ;
183+ onStartAdding : ( ) => void ;
184+ onRemoveKey : ( key : ShortcutKey ) => void ;
156185} ) {
157186 const displayKeys = shortcut . keys . filter ( ( key : string ) => {
158187 if (
@@ -164,8 +193,14 @@ function ShortcutItem({
164193 return true ;
165194 } ) ;
166195
196+ // Get raw keys for remove functionality (before formatting)
197+ const { keybindings } = useKeybindingsStore ( ) ;
198+ const rawKeys = Object . entries ( keybindings )
199+ . filter ( ( [ , action ] ) => action === shortcut . action )
200+ . map ( ( [ key ] ) => key as ShortcutKey ) ;
201+
167202 return (
168- < div className = "flex items-center justify-between" >
203+ < div className = "flex items-center justify-between py-1 " >
169204 < div className = "flex items-center gap-3" >
170205 { shortcut . icon && (
171206 < div className = "text-muted-foreground" > { shortcut . icon } </ div >
@@ -178,11 +213,17 @@ function ShortcutItem({
178213 < div className = "flex items-center gap-1" >
179214 { key . split ( "+" ) . map ( ( keyPart : string , partIndex : number ) => {
180215 const keyId = `${ shortcut . id } -${ index } -${ partIndex } ` ;
216+ const rawKey = rawKeys [ index ] ;
181217 return (
182218 < EditableShortcutKey
183219 key = { keyId }
184- isRecording = { isRecording }
185- onStartRecording = { ( ) => onStartRecording ( { shortcut } ) }
220+ isRecording = { isRecording && recordingMode === "replace" }
221+ onStartRecording = { ( ) => rawKey && onStartReplacing ( rawKey ) }
222+ onRemove = {
223+ displayKeys . length > 1 && rawKey
224+ ? ( ) => onRemoveKey ( rawKey )
225+ : undefined
226+ }
186227 >
187228 { keyPart }
188229 </ EditableShortcutKey >
@@ -194,6 +235,19 @@ function ShortcutItem({
194235 ) }
195236 </ div >
196237 ) ) }
238+ < Button
239+ variant = "outline"
240+ size = "sm"
241+ onClick = { ( e ) => {
242+ e . preventDefault ( ) ;
243+ e . stopPropagation ( ) ;
244+ onStartAdding ( ) ;
245+ } }
246+ title = "Add another shortcut"
247+ className = { isRecording && recordingMode === "add" ? "ring-2 ring-primary" : "" }
248+ >
249+ < HugeiconsIcon icon = { PlusSignIcon } className = "size-3" />
250+ </ Button >
197251 </ div >
198252 </ div >
199253 ) ;
@@ -203,24 +257,40 @@ function EditableShortcutKey({
203257 children,
204258 isRecording,
205259 onStartRecording,
260+ onRemove,
206261} : {
207262 children : React . ReactNode ;
208263 isRecording : boolean ;
209264 onStartRecording : ( ) => void ;
265+ onRemove ?: ( ) => void ;
210266} ) {
211267 const handleClick = ( e : React . MouseEvent ) => {
212268 e . preventDefault ( ) ;
213269 e . stopPropagation ( ) ;
214270 onStartRecording ( ) ;
215271 } ;
216272
273+ const handleRightClick = ( e : React . MouseEvent ) => {
274+ e . preventDefault ( ) ;
275+ e . stopPropagation ( ) ;
276+ if ( onRemove ) {
277+ onRemove ( ) ;
278+ }
279+ } ;
280+
217281 return (
218282 < Button
219283 variant = "outline"
220284 size = "sm"
221285 onClick = { handleClick }
286+ onContextMenu = { handleRightClick }
287+ className = { isRecording ? "ring-2 ring-primary" : "" }
222288 title = {
223- isRecording ? "Press any key combination..." : "Click to edit shortcut"
289+ isRecording
290+ ? "Press any key combination..."
291+ : onRemove
292+ ? "Click to edit, right-click to remove"
293+ : "Click to edit shortcut"
224294 }
225295 >
226296 { children }
0 commit comments