@tanstack/hotkeys-devtools 0.2.0 → 0.3.1

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.
@@ -1,9 +1,17 @@
1
1
  import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
2
2
  import clsx from 'clsx'
3
- import { formatForDisplay } from '@tanstack/hotkeys'
3
+ import { formatForDisplay, formatHotkeySequence } from '@tanstack/hotkeys'
4
4
  import { useStyles } from '../styles/use-styles'
5
5
  import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
6
- import type { ConflictBehavior, HotkeyRegistration } from '@tanstack/hotkeys'
6
+ import type {
7
+ ConflictBehavior,
8
+ HotkeyRegistration,
9
+ SequenceRegistrationView,
10
+ } from '@tanstack/hotkeys'
11
+
12
+ function sequenceKey(sequence: Array<string>): string {
13
+ return sequence.join('|')
14
+ }
7
15
 
8
16
  type HotkeyListProps = {
9
17
  selectedId: () => string | null
@@ -78,11 +86,38 @@ function findScopeConflicts(
78
86
  )
79
87
  }
80
88
 
89
+ function findSequenceTargetConflicts(
90
+ registration: SequenceRegistrationView,
91
+ all: Array<SequenceRegistrationView>,
92
+ ): Array<SequenceRegistrationView> {
93
+ return all.filter(
94
+ (other) =>
95
+ other.id !== registration.id &&
96
+ sequenceKey(other.sequence) === sequenceKey(registration.sequence) &&
97
+ other.options.eventType === registration.options.eventType &&
98
+ other.target === registration.target,
99
+ )
100
+ }
101
+
102
+ function findSequenceScopeConflicts(
103
+ registration: SequenceRegistrationView,
104
+ all: Array<SequenceRegistrationView>,
105
+ ): Array<SequenceRegistrationView> {
106
+ return all.filter(
107
+ (other) =>
108
+ other.id !== registration.id &&
109
+ sequenceKey(other.sequence) === sequenceKey(registration.sequence) &&
110
+ other.options.eventType === registration.options.eventType &&
111
+ other.target !== registration.target,
112
+ )
113
+ }
114
+
81
115
  export function HotkeyList(props: HotkeyListProps) {
82
116
  const styles = useStyles()
83
117
  const state = useHotkeysDevtoolsState()
84
118
 
85
119
  const registrations = createMemo(() => state.registrations())
120
+ const sequenceRegistrations = createMemo(() => state.sequenceRegistrations())
86
121
 
87
122
  return (
88
123
  <>
@@ -243,6 +278,164 @@ export function HotkeyList(props: HotkeyListProps) {
243
278
  }}
244
279
  </For>
245
280
  </div>
281
+
282
+ <div class={styles().panelHeader}>
283
+ Sequences ({sequenceRegistrations().length})
284
+ </div>
285
+ <div class={styles().hotkeyList}>
286
+ <For each={sequenceRegistrations()}>
287
+ {(reg) => {
288
+ const targetConflicts = () =>
289
+ findSequenceTargetConflicts(reg, sequenceRegistrations())
290
+ const scopeConflicts = () =>
291
+ findSequenceScopeConflicts(reg, sequenceRegistrations())
292
+
293
+ const hasTargetConflict = () => targetConflicts().length > 0
294
+ const hasScopeConflict = () => scopeConflicts().length > 0
295
+
296
+ const conflictBehavior = (): ConflictBehavior =>
297
+ reg.options.conflictBehavior ?? 'warn'
298
+
299
+ const targetConflictBadge = () => {
300
+ const behavior = conflictBehavior()
301
+ const c = targetConflicts()
302
+ if (behavior === 'allow') {
303
+ return {
304
+ style: 'badgeAllow' as const,
305
+ label: '~',
306
+ tooltip: `Allowed: ${c.length} other binding${c.length > 1 ? 's' : ''} on same sequence and target (conflictBehavior: allow)`,
307
+ }
308
+ }
309
+ if (behavior === 'error') {
310
+ return {
311
+ style: 'badgeError' as const,
312
+ label: '!',
313
+ tooltip: `Error: ${c.length} conflicting binding${c.length > 1 ? 's' : ''} on same sequence and target (conflictBehavior: error)`,
314
+ }
315
+ }
316
+ return {
317
+ style: 'badgeConflict' as const,
318
+ label: '!',
319
+ tooltip: `Warning: ${c.length} other binding${c.length > 1 ? 's' : ''} on same sequence and target`,
320
+ }
321
+ }
322
+
323
+ const scopeConflictTooltip = () => {
324
+ const c = scopeConflicts()
325
+ return `Info: ${c.length} binding${c.length > 1 ? 's' : ''} with same sequence on different target${c.length > 1 ? 's' : ''}`
326
+ }
327
+
328
+ const enabled = () => reg.options.enabled !== false
329
+
330
+ const triggerCount = () =>
331
+ sequenceRegistrations().find((r) => r.id === reg.id)
332
+ ?.triggerCount ?? reg.triggerCount
333
+
334
+ const [prevCount, setPrevCount] = createSignal(reg.triggerCount)
335
+ const [pulsing, setPulsing] = createSignal(false)
336
+
337
+ createEffect(
338
+ on(triggerCount, (current) => {
339
+ if (current > prevCount()) {
340
+ setPulsing(true)
341
+ setTimeout(() => setPulsing(false), 600)
342
+ }
343
+ setPrevCount(current)
344
+ }),
345
+ )
346
+
347
+ return (
348
+ <div
349
+ class={clsx(
350
+ styles().hotkeyRow,
351
+ props.selectedId() === reg.id && styles().hotkeyRowSelected,
352
+ pulsing() && styles().hotkeyRowTriggered,
353
+ )}
354
+ onClick={() => props.setSelectedId(reg.id)}
355
+ >
356
+ <span class={styles().hotkeyLabel}>
357
+ {formatHotkeySequence(reg.sequence)}
358
+ </span>
359
+ <Show when={triggerCount() > 0}>
360
+ <span class={styles().triggerCount}>x{triggerCount()}</span>
361
+ </Show>
362
+ <div class={styles().hotkeyBadges}>
363
+ {hasTargetConflict() && (
364
+ <span
365
+ class={clsx(
366
+ styles().badge,
367
+ styles()[targetConflictBadge().style],
368
+ styles().tooltip,
369
+ )}
370
+ >
371
+ {targetConflictBadge().label}
372
+ <span class={styles().tooltipText} data-tooltip>
373
+ {targetConflictBadge().tooltip}
374
+ </span>
375
+ </span>
376
+ )}
377
+ {hasScopeConflict() && (
378
+ <span
379
+ class={clsx(
380
+ styles().badge,
381
+ styles().badgeInfo,
382
+ styles().tooltip,
383
+ )}
384
+ >
385
+ i
386
+ <span class={styles().tooltipText} data-tooltip>
387
+ {scopeConflictTooltip()}
388
+ </span>
389
+ </span>
390
+ )}
391
+ <span
392
+ class={clsx(
393
+ styles().badge,
394
+ enabled()
395
+ ? styles().badgeEnabled
396
+ : styles().badgeDisabled,
397
+ styles().tooltip,
398
+ )}
399
+ >
400
+ {enabled() ? 'on' : 'off'}
401
+ <span class={styles().tooltipText} data-tooltip>
402
+ {enabled()
403
+ ? 'Sequence is enabled'
404
+ : 'Sequence is disabled'}
405
+ </span>
406
+ </span>
407
+ <span
408
+ class={clsx(
409
+ styles().badge,
410
+ (reg.options.eventType ?? 'keydown') === 'keydown'
411
+ ? styles().badgeKeydown
412
+ : styles().badgeKeyup,
413
+ styles().tooltip,
414
+ )}
415
+ >
416
+ {reg.options.eventType ?? 'keydown'}
417
+ <span class={styles().tooltipText} data-tooltip>
418
+ Fires on {reg.options.eventType ?? 'keydown'} event
419
+ </span>
420
+ </span>
421
+ <span
422
+ class={clsx(
423
+ styles().badge,
424
+ styles().badgeTarget,
425
+ styles().tooltip,
426
+ )}
427
+ >
428
+ {getTargetLabel(reg.target)}
429
+ <span class={styles().tooltipText} data-tooltip>
430
+ {getTargetTooltip(reg.target)}
431
+ </span>
432
+ </span>
433
+ </div>
434
+ </div>
435
+ )
436
+ }}
437
+ </For>
438
+ </div>
246
439
  </>
247
440
  )
248
441
  }
@@ -16,7 +16,10 @@ export function Shell() {
16
16
  const selectedRegistration = createMemo(() => {
17
17
  const id = selectedId()
18
18
  if (!id) return null
19
- return state.registrations().find((r) => r.id === id) ?? null
19
+ const hotkey = state.registrations().find((r) => r.id === id)
20
+ if (hotkey) return hotkey
21
+ const sequence = state.sequenceRegistrations().find((r) => r.id === id)
22
+ return sequence ?? null
20
23
  })
21
24
 
22
25
  let dragStartX = 0