@tanstack/hotkeys-devtools 0.8.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,22 +3,22 @@ import clsx from 'clsx'
3
3
  import {
4
4
  detectPlatform,
5
5
  formatForDisplay,
6
- formatHotkeySequence,
7
- normalizeHotkey,
8
6
  normalizeHotkeyFromParsed,
9
7
  parseHotkey,
10
8
  } from '@tanstack/hotkeys'
11
9
  import { useStyles } from '../styles/use-styles'
12
10
  import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
13
11
  import { effectiveSequenceMatchedSteps } from '../sequence-progress'
12
+ import {
13
+ formatRegistration,
14
+ getBindingKind,
15
+ useRegistrationConflicts,
16
+ } from '../_registrations'
14
17
  import { ActionButtons } from './ActionButtons'
15
18
  import type {
16
19
  ConflictBehavior,
17
- FormatDisplayOptions,
18
20
  HotkeyRegistration,
19
- HotkeySequence,
20
21
  ParsedHotkey,
21
- RegisterableHotkey,
22
22
  SequenceRegistrationView,
23
23
  } from '@tanstack/hotkeys'
24
24
 
@@ -59,10 +59,6 @@ function KeyBreakdownPlatformTable(props: {
59
59
  )
60
60
  }
61
61
 
62
- function sequenceKey(sequence: Array<string>): string {
63
- return sequence.join('|')
64
- }
65
-
66
62
  function isSequenceRegistration(
67
63
  reg: HotkeyRegistration | SequenceRegistrationView,
68
64
  ): reg is SequenceRegistrationView {
@@ -94,129 +90,27 @@ function getTargetDescription(target: HTMLElement | Document | Window): string {
94
90
  return 'element'
95
91
  }
96
92
 
97
- function findTargetConflicts(
98
- registration: HotkeyRegistration,
99
- all: Array<HotkeyRegistration>,
100
- ): Array<HotkeyRegistration> {
101
- return all.filter(
102
- (other) =>
103
- other.id !== registration.id &&
104
- other.hotkey === registration.hotkey &&
105
- other.options.eventType === registration.options.eventType &&
106
- other.target === registration.target,
107
- )
108
- }
109
-
110
- function findScopeConflicts(
111
- registration: HotkeyRegistration,
112
- all: Array<HotkeyRegistration>,
113
- ): Array<HotkeyRegistration> {
114
- return all.filter(
115
- (other) =>
116
- other.id !== registration.id &&
117
- other.hotkey === registration.hotkey &&
118
- other.options.eventType === registration.options.eventType &&
119
- other.target !== registration.target,
120
- )
121
- }
122
-
123
- function findSequenceTargetConflicts(
124
- registration: SequenceRegistrationView,
125
- all: Array<SequenceRegistrationView>,
126
- ): Array<SequenceRegistrationView> {
127
- return all.filter(
128
- (other) =>
129
- other.id !== registration.id &&
130
- sequenceKey(other.sequence) === sequenceKey(registration.sequence) &&
131
- other.options.eventType === registration.options.eventType &&
132
- other.target === registration.target,
133
- )
134
- }
135
-
136
- function findSequenceScopeConflicts(
137
- registration: SequenceRegistrationView,
138
- all: Array<SequenceRegistrationView>,
139
- ): Array<SequenceRegistrationView> {
140
- return all.filter(
141
- (other) =>
142
- other.id !== registration.id &&
143
- sequenceKey(other.sequence) === sequenceKey(registration.sequence) &&
144
- other.options.eventType === registration.options.eventType &&
145
- other.target !== registration.target,
146
- )
147
- }
148
-
149
- function getConflictItemStyle(
150
- behavior: ConflictBehavior,
151
- isSameTarget: boolean,
152
- ):
153
- | 'conflictItem'
154
- | 'conflictItemAllow'
155
- | 'conflictItemError'
156
- | 'conflictItemScope' {
157
- if (!isSameTarget) return 'conflictItemScope'
158
- if (behavior === 'allow') return 'conflictItemAllow'
159
- if (behavior === 'error') return 'conflictItemError'
160
- return 'conflictItem'
161
- }
162
-
163
- function getConflictLabel(
164
- behavior: ConflictBehavior,
165
- isSameTarget: boolean,
166
- ): string {
167
- if (!isSameTarget) return 'scope'
168
- if (behavior === 'allow') return 'allowed'
169
- if (behavior === 'error') return 'error'
170
- if (behavior === 'replace') return 'replaced'
171
- return 'warning'
172
- }
173
-
174
93
  function HotkeyDetails(props: {
175
94
  registration: HotkeyRegistration
176
- state: ReturnType<typeof useHotkeysDevtoolsState>
177
95
  getTargetDescription: (t: HTMLElement | Document | Window) => string
178
- findTargetConflicts: (
179
- r: HotkeyRegistration,
180
- all: Array<HotkeyRegistration>,
181
- ) => Array<HotkeyRegistration>
182
- findScopeConflicts: (
183
- r: HotkeyRegistration,
184
- all: Array<HotkeyRegistration>,
185
- ) => Array<HotkeyRegistration>
186
- getConflictItemStyle: (
187
- b: ConflictBehavior,
188
- same: boolean,
189
- ) =>
190
- | 'conflictItem'
191
- | 'conflictItemAllow'
192
- | 'conflictItemError'
193
- | 'conflictItemScope'
194
- getConflictLabel: (b: ConflictBehavior, same: boolean) => string
195
- formatForDisplay: (
196
- hotkey: RegisterableHotkey,
197
- options?: FormatDisplayOptions,
198
- ) => string
199
96
  styles: ReturnType<typeof useStyles>
200
97
  }) {
201
98
  const reg = () => props.registration
202
99
  const parsed = () => reg().parsedHotkey
203
- const targetConflicts = createMemo(() =>
204
- props.findTargetConflicts(reg(), props.state.registrations()),
205
- )
206
- const scopeConflicts = createMemo(() =>
207
- props.findScopeConflicts(reg(), props.state.registrations()),
208
- )
209
- const allConflicts = createMemo(() => [
210
- ...targetConflicts(),
211
- ...scopeConflicts(),
212
- ])
100
+ const conflicts = useRegistrationConflicts(reg)
101
+ const targetConflicts = () => conflicts().overlapping
102
+ const scopeConflicts = () => conflicts().separate
103
+ const allConflicts = () => [...targetConflicts(), ...scopeConflicts()]
213
104
  const conflictBehavior = (): ConflictBehavior =>
214
105
  reg().options.conflictBehavior ?? 'warn'
215
106
 
216
107
  const registrationPlatform = () => reg().options.platform ?? detectPlatform()
217
108
 
218
- const rawModParts = createMemo(() =>
219
- normalizeHotkeyFromParsed(parsed(), registrationPlatform()).split('+'),
109
+ const keyParts = createMemo(() =>
110
+ formatForDisplay(parsed(), {
111
+ platform: registrationPlatform(),
112
+ parts: true,
113
+ }),
220
114
  )
221
115
 
222
116
  const styles = props.styles
@@ -224,12 +118,12 @@ function HotkeyDetails(props: {
224
118
  return (
225
119
  <>
226
120
  <div class={styles().stateHeader}>
227
- <div class={styles().stateTitle}>
228
- {props.formatForDisplay(reg().hotkey)}
229
- </div>
121
+ <div class={styles().stateTitle}>{formatRegistration(reg())}</div>
230
122
  <div class={styles().infoGrid}>
231
123
  <div class={styles().infoLabel}>ID</div>
232
124
  <div class={styles().infoValueMono}>{reg().id}</div>
125
+ <div class={styles().infoLabel}>Matches</div>
126
+ <div class={styles().infoValueMono}>{getBindingKind(reg())}</div>
233
127
  <div class={styles().infoLabel}>Raw</div>
234
128
  <div class={styles().infoValueMono}>{reg().hotkey}</div>
235
129
  <div class={styles().infoLabel}>Target</div>
@@ -245,7 +139,7 @@ function HotkeyDetails(props: {
245
139
  <div class={styles().keyBreakdownSplitRow}>
246
140
  <div class={styles().keyBreakdownSplitLeft}>
247
141
  <div class={styles().keyBreakdown}>
248
- <For each={rawModParts()}>
142
+ <For each={keyParts()}>
249
143
  {(part, i) => (
250
144
  <>
251
145
  <Show when={i() > 0}>
@@ -410,14 +304,10 @@ function HotkeyDetails(props: {
410
304
  <div class={styles().conflictList}>
411
305
  <For each={targetConflicts()}>
412
306
  {(conflict) => {
413
- const itemStyle = () =>
414
- props.getConflictItemStyle(conflictBehavior(), true)
415
- const label = () =>
416
- props.getConflictLabel(conflictBehavior(), true)
417
307
  return (
418
- <div class={styles()[itemStyle()]}>
419
- <span>{label()}</span> {conflict.id}:{' '}
420
- {props.formatForDisplay(conflict.hotkey)} (
308
+ <div class={styles().conflictItem}>
309
+ <span>overlap</span> {conflict.id}:{' '}
310
+ {formatRegistration(conflict)} (
421
311
  {conflict.options.eventType ?? 'keydown'}) on{' '}
422
312
  {props.getTargetDescription(conflict.target)}
423
313
  </div>
@@ -428,7 +318,7 @@ function HotkeyDetails(props: {
428
318
  {(conflict) => (
429
319
  <div class={styles().conflictItemScope}>
430
320
  <span>scope</span> {conflict.id}:{' '}
431
- {props.formatForDisplay(conflict.hotkey)} (
321
+ {formatRegistration(conflict)} (
432
322
  {conflict.options.eventType ?? 'keydown'}) on{' '}
433
323
  {props.getTargetDescription(conflict.target)}
434
324
  </div>
@@ -446,28 +336,6 @@ function SequenceDetails(props: {
446
336
  registration: SequenceRegistrationView
447
337
  state: ReturnType<typeof useHotkeysDevtoolsState>
448
338
  getTargetDescription: (t: HTMLElement | Document | Window) => string
449
- findSequenceTargetConflicts: (
450
- r: SequenceRegistrationView,
451
- all: Array<SequenceRegistrationView>,
452
- ) => Array<SequenceRegistrationView>
453
- findSequenceScopeConflicts: (
454
- r: SequenceRegistrationView,
455
- all: Array<SequenceRegistrationView>,
456
- ) => Array<SequenceRegistrationView>
457
- getConflictItemStyle: (
458
- b: ConflictBehavior,
459
- same: boolean,
460
- ) =>
461
- | 'conflictItem'
462
- | 'conflictItemAllow'
463
- | 'conflictItemError'
464
- | 'conflictItemScope'
465
- getConflictLabel: (b: ConflictBehavior, same: boolean) => string
466
- formatHotkeySequence: (seq: HotkeySequence) => string
467
- formatForDisplay: (
468
- hotkey: RegisterableHotkey,
469
- options?: FormatDisplayOptions,
470
- ) => string
471
339
  styles: ReturnType<typeof useStyles>
472
340
  }) {
473
341
  const reg = () => props.registration
@@ -479,22 +347,10 @@ function SequenceDetails(props: {
479
347
  const matchedSteps = createMemo(() =>
480
348
  effectiveSequenceMatchedSteps(liveReg(), props.state.sequenceProgressNow()),
481
349
  )
482
- const targetConflicts = createMemo(() =>
483
- props.findSequenceTargetConflicts(
484
- liveReg(),
485
- props.state.sequenceRegistrations(),
486
- ),
487
- )
488
- const scopeConflicts = createMemo(() =>
489
- props.findSequenceScopeConflicts(
490
- liveReg(),
491
- props.state.sequenceRegistrations(),
492
- ),
493
- )
494
- const allConflicts = createMemo(() => [
495
- ...targetConflicts(),
496
- ...scopeConflicts(),
497
- ])
350
+ const conflicts = useRegistrationConflicts(liveReg)
351
+ const targetConflicts = () => conflicts().overlapping
352
+ const scopeConflicts = () => conflicts().separate
353
+ const allConflicts = () => [...targetConflicts(), ...scopeConflicts()]
498
354
  const conflictBehavior = (): ConflictBehavior =>
499
355
  liveReg().options.conflictBehavior ?? 'warn'
500
356
 
@@ -519,7 +375,9 @@ function SequenceDetails(props: {
519
375
  : undefined
520
376
  }
521
377
  >
522
- {props.formatForDisplay(step)}
378
+ {formatForDisplay(step, {
379
+ platform: sequenceCanonicalPlatform(),
380
+ })}
523
381
  </span>
524
382
  </span>
525
383
  )}
@@ -528,6 +386,12 @@ function SequenceDetails(props: {
528
386
  <div class={styles().infoGrid}>
529
387
  <div class={styles().infoLabel}>ID</div>
530
388
  <div class={styles().infoValueMono}>{liveReg().id}</div>
389
+ <div class={styles().infoLabel}>Matches</div>
390
+ <div class={styles().infoValueMono}>{getBindingKind(liveReg())}</div>
391
+ <div class={styles().infoLabel}>Raw</div>
392
+ <div class={styles().infoValueMono}>
393
+ {liveReg().sequence.join(' → ')}
394
+ </div>
531
395
  <div class={styles().infoLabel}>Sequence</div>
532
396
  <div class={styles().infoValueMono}>
533
397
  <For each={liveReg().sequence}>
@@ -541,7 +405,9 @@ function SequenceDetails(props: {
541
405
  : undefined
542
406
  }
543
407
  >
544
- {props.formatForDisplay(step)}
408
+ {formatForDisplay(step, {
409
+ platform: sequenceCanonicalPlatform(),
410
+ })}
545
411
  </span>
546
412
  </span>
547
413
  )}
@@ -570,7 +436,9 @@ function SequenceDetails(props: {
570
436
  i() < matchedSteps() && styles().keyCapLargeInProgress,
571
437
  )}
572
438
  >
573
- {props.formatForDisplay(step)}
439
+ {formatForDisplay(step, {
440
+ platform: sequenceCanonicalPlatform(),
441
+ })}
574
442
  </span>
575
443
  </>
576
444
  )}
@@ -582,17 +450,21 @@ function SequenceDetails(props: {
582
450
  {(step, stepIdx) => {
583
451
  const stepParsed = () =>
584
452
  parseHotkey(step, sequenceCanonicalPlatform())
585
- const stepRawParts = () =>
586
- normalizeHotkey(step, sequenceCanonicalPlatform()).split('+')
453
+ const stepParts = () =>
454
+ formatForDisplay(stepParsed(), {
455
+ platform: sequenceCanonicalPlatform(),
456
+ parts: true,
457
+ })
587
458
  return (
588
459
  <div class={styles().sequenceChordDetail}>
589
460
  <div class={styles().keyBreakdownSubHeader}>
590
- Step {stepIdx() + 1}
461
+ Step {stepIdx() + 1} —{' '}
462
+ {stepParsed().code !== undefined ? 'Code' : 'Key'}
591
463
  </div>
592
464
  <div class={styles().keyBreakdownSplitRow}>
593
465
  <div class={styles().keyBreakdownSplitLeft}>
594
466
  <div class={styles().keyBreakdown}>
595
- <For each={stepRawParts()}>
467
+ <For each={stepParts()}>
596
468
  {(part, i) => (
597
469
  <>
598
470
  <Show when={i() > 0}>
@@ -746,14 +618,10 @@ function SequenceDetails(props: {
746
618
  <div class={styles().conflictList}>
747
619
  <For each={targetConflicts()}>
748
620
  {(conflict) => {
749
- const itemStyle = () =>
750
- props.getConflictItemStyle(conflictBehavior(), true)
751
- const label = () =>
752
- props.getConflictLabel(conflictBehavior(), true)
753
621
  return (
754
- <div class={styles()[itemStyle()]}>
755
- <span>{label()}</span> {conflict.id}:{' '}
756
- {props.formatHotkeySequence(conflict.sequence)} (
622
+ <div class={styles().conflictItem}>
623
+ <span>overlap</span> {conflict.id}:{' '}
624
+ {formatRegistration(conflict)} (
757
625
  {conflict.options.eventType ?? 'keydown'}) on{' '}
758
626
  {props.getTargetDescription(conflict.target)}
759
627
  </div>
@@ -764,7 +632,7 @@ function SequenceDetails(props: {
764
632
  {(conflict) => (
765
633
  <div class={styles().conflictItemScope}>
766
634
  <span>scope</span> {conflict.id}:{' '}
767
- {props.formatHotkeySequence(conflict.sequence)} (
635
+ {formatRegistration(conflict)} (
768
636
  {conflict.options.eventType ?? 'keydown'}) on{' '}
769
637
  {props.getTargetDescription(conflict.target)}
770
638
  </div>
@@ -798,13 +666,7 @@ export function DetailsPanel(props: DetailsPanelProps) {
798
666
  fallback={
799
667
  <HotkeyDetails
800
668
  registration={reg() as HotkeyRegistration}
801
- state={state}
802
669
  getTargetDescription={getTargetDescription}
803
- findTargetConflicts={findTargetConflicts}
804
- findScopeConflicts={findScopeConflicts}
805
- getConflictItemStyle={getConflictItemStyle}
806
- getConflictLabel={getConflictLabel}
807
- formatForDisplay={formatForDisplay}
808
670
  styles={styles}
809
671
  />
810
672
  }
@@ -813,12 +675,6 @@ export function DetailsPanel(props: DetailsPanelProps) {
813
675
  registration={reg() as SequenceRegistrationView}
814
676
  state={state}
815
677
  getTargetDescription={getTargetDescription}
816
- findSequenceTargetConflicts={findSequenceTargetConflicts}
817
- findSequenceScopeConflicts={findSequenceScopeConflicts}
818
- getConflictItemStyle={getConflictItemStyle}
819
- getConflictLabel={getConflictLabel}
820
- formatHotkeySequence={formatHotkeySequence}
821
- formatForDisplay={formatForDisplay}
822
678
  styles={styles}
823
679
  />
824
680
  </Show>
@@ -5,15 +5,12 @@ import { formatForDisplay } from '@tanstack/hotkeys'
5
5
  import { useStyles } from '../styles/use-styles'
6
6
  import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
7
7
  import { effectiveSequenceMatchedSteps } from '../sequence-progress'
8
- import type {
9
- ConflictBehavior,
10
- HotkeyRegistration,
11
- SequenceRegistrationView,
12
- } from '@tanstack/hotkeys'
13
-
14
- function sequenceKey(sequence: Array<string>): string {
15
- return sequence.join('|')
16
- }
8
+ import {
9
+ formatRegistration,
10
+ getBindingKind,
11
+ useRegistrationConflicts,
12
+ } from '../_registrations'
13
+ import type { SequenceRegistrationView } from '@tanstack/hotkeys'
17
14
 
18
15
  type HotkeyListProps = {
19
16
  selectedId: () => string | null
@@ -62,64 +59,14 @@ function getTargetTooltip(target: HTMLElement | Document | Window): string {
62
59
  return 'Listening on element'
63
60
  }
64
61
 
65
- function findTargetConflicts(
66
- registration: HotkeyRegistration,
67
- all: Array<HotkeyRegistration>,
68
- ): Array<HotkeyRegistration> {
69
- return all.filter(
70
- (other) =>
71
- other.id !== registration.id &&
72
- other.hotkey === registration.hotkey &&
73
- other.options.eventType === registration.options.eventType &&
74
- other.target === registration.target,
75
- )
76
- }
77
-
78
- function findScopeConflicts(
79
- registration: HotkeyRegistration,
80
- all: Array<HotkeyRegistration>,
81
- ): Array<HotkeyRegistration> {
82
- return all.filter(
83
- (other) =>
84
- other.id !== registration.id &&
85
- other.hotkey === registration.hotkey &&
86
- other.options.eventType === registration.options.eventType &&
87
- other.target !== registration.target,
88
- )
89
- }
90
-
91
- function findSequenceTargetConflicts(
92
- registration: SequenceRegistrationView,
93
- all: Array<SequenceRegistrationView>,
94
- ): Array<SequenceRegistrationView> {
95
- return all.filter(
96
- (other) =>
97
- other.id !== registration.id &&
98
- sequenceKey(other.sequence) === sequenceKey(registration.sequence) &&
99
- other.options.eventType === registration.options.eventType &&
100
- other.target === registration.target,
101
- )
102
- }
103
-
104
- function findSequenceScopeConflicts(
105
- registration: SequenceRegistrationView,
106
- all: Array<SequenceRegistrationView>,
107
- ): Array<SequenceRegistrationView> {
108
- return all.filter(
109
- (other) =>
110
- other.id !== registration.id &&
111
- sequenceKey(other.sequence) === sequenceKey(registration.sequence) &&
112
- other.options.eventType === registration.options.eventType &&
113
- other.target !== registration.target,
114
- )
115
- }
116
-
117
62
  function buildRowTooltip(meta?: {
118
63
  name?: string
119
64
  description?: string
65
+ group?: string
120
66
  }): string | undefined {
121
67
  if (!meta) return undefined
122
68
  const parts: Array<string> = []
69
+ if (meta.group) parts.push(meta.group)
123
70
  if (meta.name) parts.push(meta.name)
124
71
  if (meta.description) parts.push(meta.description)
125
72
  return parts.length > 0 ? parts.join(' — ') : undefined
@@ -136,45 +83,18 @@ function SequenceListRow(props: {
136
83
 
137
84
  const sequenceRegistrations = () => props.getSequences()
138
85
 
139
- const targetConflicts = () =>
140
- findSequenceTargetConflicts(props.reg, sequenceRegistrations())
141
- const scopeConflicts = () =>
142
- findSequenceScopeConflicts(props.reg, sequenceRegistrations())
143
-
86
+ const conflicts = useRegistrationConflicts(() => props.reg)
87
+ const targetConflicts = () => conflicts().overlapping
88
+ const scopeConflicts = () => conflicts().separate
144
89
  const hasTargetConflict = () => targetConflicts().length > 0
145
90
  const hasScopeConflict = () => scopeConflicts().length > 0
146
-
147
- const conflictBehavior = (): ConflictBehavior =>
148
- props.reg.options.conflictBehavior ?? 'warn'
149
-
150
- const targetConflictBadge = () => {
151
- const behavior = conflictBehavior()
152
- const c = targetConflicts()
153
- if (behavior === 'allow') {
154
- return {
155
- style: 'badgeAllow' as const,
156
- label: '~',
157
- tooltip: `Allowed: ${c.length} other binding${c.length > 1 ? 's' : ''} on same sequence and target (conflictBehavior: allow)`,
158
- }
159
- }
160
- if (behavior === 'error') {
161
- return {
162
- style: 'badgeError' as const,
163
- label: '!',
164
- tooltip: `Error: ${c.length} conflicting binding${c.length > 1 ? 's' : ''} on same sequence and target (conflictBehavior: error)`,
165
- }
166
- }
167
- return {
168
- style: 'badgeConflict' as const,
169
- label: '!',
170
- tooltip: `Warning: ${c.length} other binding${c.length > 1 ? 's' : ''} on same sequence and target`,
171
- }
172
- }
173
-
174
- const scopeConflictTooltip = () => {
175
- const c = scopeConflicts()
176
- return `Info: ${c.length} binding${c.length > 1 ? 's' : ''} with same sequence on different target${c.length > 1 ? 's' : ''}`
177
- }
91
+ const targetConflictBadge = () => ({
92
+ style: 'badgeConflict' as const,
93
+ label: '!',
94
+ tooltip: `Potential overlap: ${targetConflicts().length} other bindings on overlapping targets`,
95
+ })
96
+ const scopeConflictTooltip = () =>
97
+ `Info: ${scopeConflicts().length} related bindings on separate targets`
178
98
 
179
99
  const enabled = () => props.reg.options.enabled !== false
180
100
 
@@ -238,7 +158,9 @@ function SequenceListRow(props: {
238
158
  : undefined
239
159
  }
240
160
  >
241
- {formatForDisplay(step)}
161
+ {formatForDisplay(step, {
162
+ platform: props.reg.options.platform,
163
+ })}
242
164
  </span>
243
165
  </span>
244
166
  )}
@@ -252,6 +174,12 @@ function SequenceListRow(props: {
252
174
  <span class={styles().triggerCount}>x{triggerCount()}</span>
253
175
  </Show>
254
176
  <div class={styles().hotkeyBadges}>
177
+ <span
178
+ class={clsx(styles().badge, styles().badgeTarget)}
179
+ data-binding-kind
180
+ >
181
+ {getBindingKind(props.reg)}
182
+ </span>
255
183
  {hasTargetConflict() && (
256
184
  <span
257
185
  class={clsx(
@@ -329,10 +257,11 @@ export function HotkeyList(props: HotkeyListProps) {
329
257
  if (!query) return registrations()
330
258
  return matchSorter(registrations(), query, {
331
259
  keys: [
332
- (r) => formatForDisplay(r.hotkey),
260
+ (r) => formatRegistration(r),
333
261
  'hotkey',
334
262
  'options.meta.name',
335
263
  'options.meta.description',
264
+ 'options.meta.group',
336
265
  ],
337
266
  })
338
267
  })
@@ -342,10 +271,11 @@ export function HotkeyList(props: HotkeyListProps) {
342
271
  if (!query) return sequenceRegistrations()
343
272
  return matchSorter(sequenceRegistrations(), query, {
344
273
  keys: [
345
- (r) => r.sequence.map((s) => formatForDisplay(s)).join(' '),
274
+ (r) => formatRegistration(r),
346
275
  (r) => r.sequence.join(' '),
347
276
  'options.meta.name',
348
277
  'options.meta.description',
278
+ 'options.meta.group',
349
279
  ],
350
280
  })
351
281
  })
@@ -376,46 +306,18 @@ export function HotkeyList(props: HotkeyListProps) {
376
306
  <div class={styles().hotkeyList}>
377
307
  <For each={filteredRegistrations()}>
378
308
  {(reg) => {
379
- const targetConflicts = () =>
380
- findTargetConflicts(reg, registrations())
381
- const scopeConflicts = () =>
382
- findScopeConflicts(reg, registrations())
383
-
309
+ const conflicts = useRegistrationConflicts(() => reg)
310
+ const targetConflicts = () => conflicts().overlapping
311
+ const scopeConflicts = () => conflicts().separate
384
312
  const hasTargetConflict = () => targetConflicts().length > 0
385
313
  const hasScopeConflict = () => scopeConflicts().length > 0
386
-
387
- const conflictBehavior = (): ConflictBehavior =>
388
- reg.options.conflictBehavior ?? 'warn'
389
-
390
- const targetConflictBadge = () => {
391
- const behavior = conflictBehavior()
392
- const c = targetConflicts()
393
- if (behavior === 'allow') {
394
- return {
395
- style: 'badgeAllow' as const,
396
- label: '~',
397
- tooltip: `Allowed: ${c.length} other binding${c.length > 1 ? 's' : ''} on same key and target (conflictBehavior: allow)`,
398
- }
399
- }
400
- if (behavior === 'error') {
401
- return {
402
- style: 'badgeError' as const,
403
- label: '!',
404
- tooltip: `Error: ${c.length} conflicting binding${c.length > 1 ? 's' : ''} on same key and target (conflictBehavior: error)`,
405
- }
406
- }
407
- // 'warn' (default) or 'replace' (replacement already happened, but show warn-style if somehow present)
408
- return {
409
- style: 'badgeConflict' as const,
410
- label: '!',
411
- tooltip: `Warning: ${c.length} other binding${c.length > 1 ? 's' : ''} on same key and target`,
412
- }
413
- }
414
-
415
- const scopeConflictTooltip = () => {
416
- const c = scopeConflicts()
417
- return `Info: ${c.length} binding${c.length > 1 ? 's' : ''} with same key on different target${c.length > 1 ? 's' : ''}`
418
- }
314
+ const targetConflictBadge = () => ({
315
+ style: 'badgeConflict' as const,
316
+ label: '!',
317
+ tooltip: `Potential overlap: ${targetConflicts().length} other bindings on overlapping targets`,
318
+ })
319
+ const scopeConflictTooltip = () =>
320
+ `Info: ${scopeConflicts().length} related bindings on separate targets`
419
321
 
420
322
  const enabled = () => reg.options.enabled !== false
421
323
 
@@ -459,7 +361,7 @@ export function HotkeyList(props: HotkeyListProps) {
459
361
  </Show>
460
362
  <span class={styles().hotkeyLabel}>
461
363
  <span class={styles().hotkeyLabelKeys}>
462
- {formatForDisplay(reg.hotkey)}
364
+ {formatRegistration(reg)}
463
365
  </span>
464
366
  <Show when={metaName()}>
465
367
  <span class={styles().hotkeyLabelName}>{metaName()}</span>
@@ -469,6 +371,12 @@ export function HotkeyList(props: HotkeyListProps) {
469
371
  <span class={styles().triggerCount}>x{triggerCount()}</span>
470
372
  </Show>
471
373
  <div class={styles().hotkeyBadges}>
374
+ <span
375
+ class={clsx(styles().badge, styles().badgeTarget)}
376
+ data-binding-kind
377
+ >
378
+ {getBindingKind(reg)}
379
+ </span>
472
380
  {hasTargetConflict() && (
473
381
  <span
474
382
  class={clsx(