@tanstack/hotkeys-devtools 0.4.4 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys-devtools",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Devtools for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "src"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@tanstack/hotkeys": "0.4.3"
40
+ "@tanstack/hotkeys": "0.5.0"
41
41
  },
42
42
  "dependencies": {
43
43
  "@tanstack/devtools-ui": "^0.5.1",
@@ -10,6 +10,7 @@ import {
10
10
  KeyStateTracker,
11
11
  SequenceManager,
12
12
  } from '@tanstack/hotkeys'
13
+ import { needsSequenceProgressClock } from './sequence-progress'
13
14
  import type { Accessor } from 'solid-js'
14
15
  import type {
15
16
  HotkeyRegistration,
@@ -21,6 +22,8 @@ interface HotkeysDevtoolsContextType {
21
22
  sequenceRegistrations: Accessor<Array<SequenceRegistrationView>>
22
23
  heldKeys: Accessor<Array<string>>
23
24
  heldCodes: Accessor<Record<string, string>>
25
+ /** Monotonic clock for sequence in-progress UI (updates while a sequence match is active). */
26
+ sequenceProgressNow: Accessor<number>
24
27
  }
25
28
 
26
29
  const HotkeysDevtoolsContext = createContext<HotkeysDevtoolsContextType>({
@@ -28,6 +31,7 @@ const HotkeysDevtoolsContext = createContext<HotkeysDevtoolsContextType>({
28
31
  sequenceRegistrations: () => [],
29
32
  heldKeys: () => [],
30
33
  heldCodes: () => ({}),
34
+ sequenceProgressNow: () => Date.now(),
31
35
  })
32
36
 
33
37
  export function HotkeysContextProvider(props: { children: any }) {
@@ -76,6 +80,36 @@ export function HotkeysContextProvider(props: { children: any }) {
76
80
  onCleanup(() => unsubscribe())
77
81
  })
78
82
 
83
+ const [sequenceProgressNow, setSequenceProgressNow] = createSignal(Date.now())
84
+
85
+ createEffect(() => {
86
+ const regs = sequenceRegistrations()
87
+ let id: ReturnType<typeof setInterval> | undefined
88
+
89
+ const tick = () => {
90
+ const t = Date.now()
91
+ setSequenceProgressNow(t)
92
+ if (!needsSequenceProgressClock(sequenceRegistrations(), t)) {
93
+ if (id !== undefined) {
94
+ clearInterval(id)
95
+ id = undefined
96
+ }
97
+ }
98
+ }
99
+
100
+ if (needsSequenceProgressClock(regs, Date.now())) {
101
+ id = setInterval(tick, 50)
102
+ } else {
103
+ setSequenceProgressNow(Date.now())
104
+ }
105
+
106
+ onCleanup(() => {
107
+ if (id !== undefined) {
108
+ clearInterval(id)
109
+ }
110
+ })
111
+ })
112
+
79
113
  return (
80
114
  <HotkeysDevtoolsContext.Provider
81
115
  value={{
@@ -83,6 +117,7 @@ export function HotkeysContextProvider(props: { children: any }) {
83
117
  sequenceRegistrations,
84
118
  heldKeys,
85
119
  heldCodes,
120
+ sequenceProgressNow,
86
121
  }}
87
122
  >
88
123
  {props.children}
@@ -1,7 +1,9 @@
1
1
  import { For, Show, createMemo } from 'solid-js'
2
+ import clsx from 'clsx'
2
3
  import { formatForDisplay, formatHotkeySequence } from '@tanstack/hotkeys'
3
4
  import { useStyles } from '../styles/use-styles'
4
5
  import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
6
+ import { effectiveSequenceMatchedSteps } from '../sequence-progress'
5
7
  import { ActionButtons } from './ActionButtons'
6
8
  import type {
7
9
  ConflictBehavior,
@@ -368,15 +370,23 @@ function SequenceDetails(props: {
368
370
  styles: ReturnType<typeof useStyles>
369
371
  }) {
370
372
  const reg = () => props.registration
373
+ const liveReg = createMemo(
374
+ () =>
375
+ props.state.sequenceRegistrations().find((r) => r.id === reg().id) ??
376
+ reg(),
377
+ )
378
+ const matchedSteps = createMemo(() =>
379
+ effectiveSequenceMatchedSteps(liveReg(), props.state.sequenceProgressNow()),
380
+ )
371
381
  const targetConflicts = createMemo(() =>
372
382
  props.findSequenceTargetConflicts(
373
- reg(),
383
+ liveReg(),
374
384
  props.state.sequenceRegistrations(),
375
385
  ),
376
386
  )
377
387
  const scopeConflicts = createMemo(() =>
378
388
  props.findSequenceScopeConflicts(
379
- reg(),
389
+ liveReg(),
380
390
  props.state.sequenceRegistrations(),
381
391
  ),
382
392
  )
@@ -385,7 +395,7 @@ function SequenceDetails(props: {
385
395
  ...scopeConflicts(),
386
396
  ])
387
397
  const conflictBehavior = (): ConflictBehavior =>
388
- reg().options.conflictBehavior ?? 'warn'
398
+ liveReg().options.conflictBehavior ?? 'warn'
389
399
 
390
400
  const styles = props.styles
391
401
 
@@ -393,18 +403,48 @@ function SequenceDetails(props: {
393
403
  <>
394
404
  <div class={styles().stateHeader}>
395
405
  <div class={styles().stateTitle}>
396
- {props.formatHotkeySequence(reg().sequence)}
406
+ <For each={liveReg().sequence}>
407
+ {(step, i) => (
408
+ <span>
409
+ <Show when={i() > 0}> </Show>
410
+ <span
411
+ class={
412
+ i() < matchedSteps()
413
+ ? styles().sequenceStepMatched
414
+ : undefined
415
+ }
416
+ >
417
+ {props.formatForDisplay(step)}
418
+ </span>
419
+ </span>
420
+ )}
421
+ </For>
397
422
  </div>
398
423
  <div class={styles().infoGrid}>
399
424
  <div class={styles().infoLabel}>ID</div>
400
- <div class={styles().infoValueMono}>{reg().id}</div>
425
+ <div class={styles().infoValueMono}>{liveReg().id}</div>
401
426
  <div class={styles().infoLabel}>Sequence</div>
402
427
  <div class={styles().infoValueMono}>
403
- {props.formatHotkeySequence(reg().sequence)}
428
+ <For each={liveReg().sequence}>
429
+ {(step, i) => (
430
+ <span>
431
+ <Show when={i() > 0}> </Show>
432
+ <span
433
+ class={
434
+ i() < matchedSteps()
435
+ ? styles().sequenceStepMatched
436
+ : undefined
437
+ }
438
+ >
439
+ {props.formatForDisplay(step)}
440
+ </span>
441
+ </span>
442
+ )}
443
+ </For>
404
444
  </div>
405
445
  <div class={styles().infoLabel}>Target</div>
406
446
  <div class={styles().infoValueMono}>
407
- {props.getTargetDescription(reg().target)}
447
+ {props.getTargetDescription(liveReg().target)}
408
448
  </div>
409
449
  </div>
410
450
  </div>
@@ -413,13 +453,18 @@ function SequenceDetails(props: {
413
453
  <div class={styles().detailSection}>
414
454
  <div class={styles().detailSectionHeader}>Key Breakdown</div>
415
455
  <div class={styles().keyBreakdown}>
416
- <For each={reg().sequence}>
456
+ <For each={liveReg().sequence}>
417
457
  {(step, i) => (
418
458
  <>
419
459
  <Show when={i() > 0}>
420
460
  <span class={styles().keyBreakdownPlus}>→</span>
421
461
  </Show>
422
- <span class={styles().keyCapLarge}>
462
+ <span
463
+ class={clsx(
464
+ styles().keyCapLarge,
465
+ i() < matchedSteps() && styles().keyCapLargeInProgress,
466
+ )}
467
+ >
423
468
  {props.formatForDisplay(step)}
424
469
  </span>
425
470
  </>
@@ -430,7 +475,7 @@ function SequenceDetails(props: {
430
475
 
431
476
  <div class={styles().detailSection}>
432
477
  <div class={styles().detailSectionHeader}>Actions</div>
433
- <ActionButtons registration={reg()} />
478
+ <ActionButtons registration={liveReg()} />
434
479
  </div>
435
480
 
436
481
  <div class={styles().detailSection}>
@@ -440,60 +485,60 @@ function SequenceDetails(props: {
440
485
  <span class={styles().optionLabel}>enabled</span>
441
486
  <span
442
487
  class={
443
- reg().options.enabled !== false
488
+ liveReg().options.enabled !== false
444
489
  ? styles().optionValueTrue
445
490
  : styles().optionValueFalse
446
491
  }
447
492
  >
448
- {String(reg().options.enabled !== false)}
493
+ {String(liveReg().options.enabled !== false)}
449
494
  </span>
450
495
  </div>
451
496
  <div class={styles().optionRow}>
452
497
  <span class={styles().optionLabel}>eventType</span>
453
498
  <span class={styles().optionValue}>
454
- {reg().options.eventType ?? 'keydown'}
499
+ {liveReg().options.eventType ?? 'keydown'}
455
500
  </span>
456
501
  </div>
457
502
  <div class={styles().optionRow}>
458
503
  <span class={styles().optionLabel}>timeout</span>
459
504
  <span class={styles().optionValue}>
460
- {reg().options.timeout ?? 1000}ms
505
+ {liveReg().options.timeout ?? 1000}ms
461
506
  </span>
462
507
  </div>
463
508
  <div class={styles().optionRow}>
464
509
  <span class={styles().optionLabel}>preventDefault</span>
465
510
  <span
466
511
  class={
467
- reg().options.preventDefault
512
+ liveReg().options.preventDefault
468
513
  ? styles().optionValueTrue
469
514
  : styles().optionValueFalse
470
515
  }
471
516
  >
472
- {String(!!reg().options.preventDefault)}
517
+ {String(!!liveReg().options.preventDefault)}
473
518
  </span>
474
519
  </div>
475
520
  <div class={styles().optionRow}>
476
521
  <span class={styles().optionLabel}>stopPropagation</span>
477
522
  <span
478
523
  class={
479
- reg().options.stopPropagation
524
+ liveReg().options.stopPropagation
480
525
  ? styles().optionValueTrue
481
526
  : styles().optionValueFalse
482
527
  }
483
528
  >
484
- {String(!!reg().options.stopPropagation)}
529
+ {String(!!liveReg().options.stopPropagation)}
485
530
  </span>
486
531
  </div>
487
532
  <div class={styles().optionRow}>
488
533
  <span class={styles().optionLabel}>ignoreInputs</span>
489
534
  <span
490
535
  class={
491
- reg().options.ignoreInputs !== false
536
+ liveReg().options.ignoreInputs !== false
492
537
  ? styles().optionValueTrue
493
538
  : styles().optionValueFalse
494
539
  }
495
540
  >
496
- {String(reg().options.ignoreInputs !== false)}
541
+ {String(liveReg().options.ignoreInputs !== false)}
497
542
  </span>
498
543
  </div>
499
544
  <div class={styles().optionRow}>
@@ -1,8 +1,9 @@
1
1
  import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
2
2
  import clsx from 'clsx'
3
- import { formatForDisplay, formatHotkeySequence } from '@tanstack/hotkeys'
3
+ import { formatForDisplay } from '@tanstack/hotkeys'
4
4
  import { useStyles } from '../styles/use-styles'
5
5
  import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
6
+ import { effectiveSequenceMatchedSteps } from '../sequence-progress'
6
7
  import type {
7
8
  ConflictBehavior,
8
9
  HotkeyRegistration,
@@ -112,6 +113,182 @@ function findSequenceScopeConflicts(
112
113
  )
113
114
  }
114
115
 
116
+ function SequenceListRow(props: {
117
+ reg: SequenceRegistrationView
118
+ getSequences: () => Array<SequenceRegistrationView>
119
+ sequenceProgressNow: () => number
120
+ selectedId: () => string | null
121
+ setSelectedId: (id: string | null) => void
122
+ }) {
123
+ const styles = useStyles()
124
+
125
+ const sequenceRegistrations = () => props.getSequences()
126
+
127
+ const targetConflicts = () =>
128
+ findSequenceTargetConflicts(props.reg, sequenceRegistrations())
129
+ const scopeConflicts = () =>
130
+ findSequenceScopeConflicts(props.reg, sequenceRegistrations())
131
+
132
+ const hasTargetConflict = () => targetConflicts().length > 0
133
+ const hasScopeConflict = () => scopeConflicts().length > 0
134
+
135
+ const conflictBehavior = (): ConflictBehavior =>
136
+ props.reg.options.conflictBehavior ?? 'warn'
137
+
138
+ const targetConflictBadge = () => {
139
+ const behavior = conflictBehavior()
140
+ const c = targetConflicts()
141
+ if (behavior === 'allow') {
142
+ return {
143
+ style: 'badgeAllow' as const,
144
+ label: '~',
145
+ tooltip: `Allowed: ${c.length} other binding${c.length > 1 ? 's' : ''} on same sequence and target (conflictBehavior: allow)`,
146
+ }
147
+ }
148
+ if (behavior === 'error') {
149
+ return {
150
+ style: 'badgeError' as const,
151
+ label: '!',
152
+ tooltip: `Error: ${c.length} conflicting binding${c.length > 1 ? 's' : ''} on same sequence and target (conflictBehavior: error)`,
153
+ }
154
+ }
155
+ return {
156
+ style: 'badgeConflict' as const,
157
+ label: '!',
158
+ tooltip: `Warning: ${c.length} other binding${c.length > 1 ? 's' : ''} on same sequence and target`,
159
+ }
160
+ }
161
+
162
+ const scopeConflictTooltip = () => {
163
+ const c = scopeConflicts()
164
+ return `Info: ${c.length} binding${c.length > 1 ? 's' : ''} with same sequence on different target${c.length > 1 ? 's' : ''}`
165
+ }
166
+
167
+ const enabled = () => props.reg.options.enabled !== false
168
+
169
+ const liveSequenceReg = createMemo(
170
+ () =>
171
+ sequenceRegistrations().find((r) => r.id === props.reg.id) ?? props.reg,
172
+ )
173
+
174
+ const triggerCount = () =>
175
+ sequenceRegistrations().find((r) => r.id === props.reg.id)?.triggerCount ??
176
+ props.reg.triggerCount
177
+
178
+ const matchedSteps = createMemo(() =>
179
+ effectiveSequenceMatchedSteps(
180
+ liveSequenceReg(),
181
+ props.sequenceProgressNow(),
182
+ ),
183
+ )
184
+
185
+ const [prevCount, setPrevCount] = createSignal(props.reg.triggerCount)
186
+ const [pulsing, setPulsing] = createSignal(false)
187
+
188
+ createEffect(
189
+ on(triggerCount, (current) => {
190
+ if (current > prevCount()) {
191
+ setPulsing(true)
192
+ setTimeout(() => setPulsing(false), 600)
193
+ }
194
+ setPrevCount(current)
195
+ }),
196
+ )
197
+
198
+ return (
199
+ <div
200
+ class={clsx(
201
+ styles().hotkeyRow,
202
+ props.selectedId() === props.reg.id && styles().hotkeyRowSelected,
203
+ pulsing() && styles().hotkeyRowTriggered,
204
+ )}
205
+ onClick={() => props.setSelectedId(props.reg.id)}
206
+ >
207
+ <span class={styles().hotkeyLabel}>
208
+ <For each={liveSequenceReg().sequence}>
209
+ {(step, i) => (
210
+ <span>
211
+ <Show when={i() > 0}> </Show>
212
+ <span
213
+ class={
214
+ i() < matchedSteps()
215
+ ? styles().sequenceStepMatched
216
+ : undefined
217
+ }
218
+ >
219
+ {formatForDisplay(step)}
220
+ </span>
221
+ </span>
222
+ )}
223
+ </For>
224
+ </span>
225
+ <Show when={triggerCount() > 0}>
226
+ <span class={styles().triggerCount}>x{triggerCount()}</span>
227
+ </Show>
228
+ <div class={styles().hotkeyBadges}>
229
+ {hasTargetConflict() && (
230
+ <span
231
+ class={clsx(
232
+ styles().badge,
233
+ styles()[targetConflictBadge().style],
234
+ styles().tooltip,
235
+ )}
236
+ >
237
+ {targetConflictBadge().label}
238
+ <span class={styles().tooltipText} data-tooltip>
239
+ {targetConflictBadge().tooltip}
240
+ </span>
241
+ </span>
242
+ )}
243
+ {hasScopeConflict() && (
244
+ <span
245
+ class={clsx(styles().badge, styles().badgeInfo, styles().tooltip)}
246
+ >
247
+ i
248
+ <span class={styles().tooltipText} data-tooltip>
249
+ {scopeConflictTooltip()}
250
+ </span>
251
+ </span>
252
+ )}
253
+ <span
254
+ class={clsx(
255
+ styles().badge,
256
+ enabled() ? styles().badgeEnabled : styles().badgeDisabled,
257
+ styles().tooltip,
258
+ )}
259
+ >
260
+ {enabled() ? 'on' : 'off'}
261
+ <span class={styles().tooltipText} data-tooltip>
262
+ {enabled() ? 'Sequence is enabled' : 'Sequence is disabled'}
263
+ </span>
264
+ </span>
265
+ <span
266
+ class={clsx(
267
+ styles().badge,
268
+ (props.reg.options.eventType ?? 'keydown') === 'keydown'
269
+ ? styles().badgeKeydown
270
+ : styles().badgeKeyup,
271
+ styles().tooltip,
272
+ )}
273
+ >
274
+ {props.reg.options.eventType ?? 'keydown'}
275
+ <span class={styles().tooltipText} data-tooltip>
276
+ Fires on {props.reg.options.eventType ?? 'keydown'} event
277
+ </span>
278
+ </span>
279
+ <span
280
+ class={clsx(styles().badge, styles().badgeTarget, styles().tooltip)}
281
+ >
282
+ {getTargetLabel(props.reg.target)}
283
+ <span class={styles().tooltipText} data-tooltip>
284
+ {getTargetTooltip(props.reg.target)}
285
+ </span>
286
+ </span>
287
+ </div>
288
+ </div>
289
+ )
290
+ }
291
+
115
292
  export function HotkeyList(props: HotkeyListProps) {
116
293
  const styles = useStyles()
117
294
  const state = useHotkeysDevtoolsState()
@@ -284,156 +461,15 @@ export function HotkeyList(props: HotkeyListProps) {
284
461
  </div>
285
462
  <div class={styles().hotkeyList}>
286
463
  <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
- }}
464
+ {(reg) => (
465
+ <SequenceListRow
466
+ reg={reg}
467
+ getSequences={sequenceRegistrations}
468
+ sequenceProgressNow={state.sequenceProgressNow}
469
+ selectedId={props.selectedId}
470
+ setSelectedId={props.setSelectedId}
471
+ />
472
+ )}
437
473
  </For>
438
474
  </div>
439
475
  </>