@tanstack/hotkeys-devtools 0.9.0 → 1.1.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.
Files changed (42) hide show
  1. package/README.md +15 -1
  2. package/dist/HotkeysContextProvider.js +1 -2
  3. package/dist/_registrations.js +47 -0
  4. package/dist/components/ActionButtons.js +2 -3
  5. package/dist/components/DetailsPanel.js +317 -325
  6. package/dist/components/HeldKeysTopbar.js +5 -3
  7. package/dist/components/HotkeyList.js +167 -196
  8. package/dist/components/Shell.js +5 -4
  9. package/dist/components/index.js +1 -2
  10. package/dist/core.d.ts +3 -6
  11. package/dist/core.js +1 -2
  12. package/dist/index.d.ts +2 -4
  13. package/dist/index.js +1 -2
  14. package/dist/sequence-progress.js +1 -2
  15. package/dist/styles/tokens.js +1 -2
  16. package/dist/styles/use-styles.js +5 -4
  17. package/package.json +11 -11
  18. package/dist/HotkeysContextProvider.js.map +0 -1
  19. package/dist/components/ActionButtons.js.map +0 -1
  20. package/dist/components/DetailsPanel.js.map +0 -1
  21. package/dist/components/HeldKeysTopbar.js.map +0 -1
  22. package/dist/components/HotkeyList.js.map +0 -1
  23. package/dist/components/Shell.js.map +0 -1
  24. package/dist/components/index.js.map +0 -1
  25. package/dist/core.js.map +0 -1
  26. package/dist/index.js.map +0 -1
  27. package/dist/sequence-progress.js.map +0 -1
  28. package/dist/styles/tokens.js.map +0 -1
  29. package/dist/styles/use-styles.js.map +0 -1
  30. package/src/HotkeysContextProvider.tsx +0 -130
  31. package/src/components/ActionButtons.tsx +0 -38
  32. package/src/components/DetailsPanel.tsx +0 -829
  33. package/src/components/HeldKeysTopbar.tsx +0 -46
  34. package/src/components/HotkeyList.tsx +0 -565
  35. package/src/components/Shell.tsx +0 -171
  36. package/src/components/index.tsx +0 -19
  37. package/src/core.tsx +0 -11
  38. package/src/index.ts +0 -11
  39. package/src/production.ts +0 -5
  40. package/src/sequence-progress.ts +0 -29
  41. package/src/styles/tokens.ts +0 -305
  42. package/src/styles/use-styles.ts +0 -687
@@ -1,46 +0,0 @@
1
- import { For, Show } from 'solid-js'
2
- import { detectPlatform, formatForDisplay } from '@tanstack/hotkeys'
3
- import { useStyles } from '../styles/use-styles'
4
- import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
5
- import type { RegisterableHotkey } from '@tanstack/hotkeys'
6
-
7
- export function HeldKeysBar() {
8
- const styles = useStyles()
9
- const state = useHotkeysDevtoolsState()
10
- const platform = detectPlatform()
11
-
12
- return (
13
- <div class={styles().heldKeysBar}>
14
- <span class={styles().heldKeysBarHeader}>Held</span>
15
- <div class={styles().heldKeysBarList}>
16
- <Show
17
- when={state.heldKeys().length > 0}
18
- fallback={<span class={styles().noKeysHeld}>--</span>}
19
- >
20
- <For each={state.heldKeys()}>
21
- {(key) => {
22
- const code = () => state.heldCodes()[key]
23
- const label = () =>
24
- formatForDisplay(key as RegisterableHotkey, {
25
- platform,
26
- useSymbols: true,
27
- })
28
- const codeLabel = () => {
29
- const c = code()
30
- return c || undefined
31
- }
32
- return (
33
- <span class={styles().keyCap}>
34
- <span>{label()}</span>
35
- <Show when={codeLabel() && codeLabel() !== key}>
36
- <span class={styles().keyCapCode}>{codeLabel()}</span>
37
- </Show>
38
- </span>
39
- )
40
- }}
41
- </For>
42
- </Show>
43
- </div>
44
- </div>
45
- )
46
- }
@@ -1,565 +0,0 @@
1
- import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
2
- import clsx from 'clsx'
3
- import { matchSorter } from 'match-sorter'
4
- import { formatForDisplay } from '@tanstack/hotkeys'
5
- import { useStyles } from '../styles/use-styles'
6
- import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
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
- }
17
-
18
- type HotkeyListProps = {
19
- selectedId: () => string | null
20
- setSelectedId: (id: string | null) => void
21
- }
22
-
23
- function getTargetLabel(target: HTMLElement | Document | Window): string {
24
- if (typeof document !== 'undefined' && target === document) {
25
- return 'document'
26
- }
27
- if (typeof window !== 'undefined' && target === window) {
28
- return 'window'
29
- }
30
- if (target instanceof HTMLElement) {
31
- return target.tagName.toLowerCase()
32
- }
33
- return 'element'
34
- }
35
-
36
- function getTargetTooltip(target: HTMLElement | Document | Window): string {
37
- if (typeof document !== 'undefined' && target === document) {
38
- return 'Listening on document'
39
- }
40
- if (typeof window !== 'undefined' && target === window) {
41
- return 'Listening on window'
42
- }
43
- if (target instanceof HTMLElement) {
44
- const tag = target.tagName.toLowerCase()
45
- const parts: Array<string> = [tag]
46
- if (target.id) {
47
- parts.push(`id="${target.id}"`)
48
- }
49
- if (target.className) {
50
- const classes = target.className.split(/\s+/).filter(Boolean).join(', ')
51
- parts.push(`class="${classes}"`)
52
- }
53
- // Collect data- attributes
54
- const dataAttrs = Array.from(target.attributes)
55
- .filter((attr) => attr.name.startsWith('data-'))
56
- .map((attr) => `${attr.name}="${attr.value}"`)
57
- if (dataAttrs.length > 0) {
58
- parts.push(...dataAttrs)
59
- }
60
- return `Listening on ${parts.join(' ')}`
61
- }
62
- return 'Listening on element'
63
- }
64
-
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
- function buildRowTooltip(meta?: {
118
- name?: string
119
- description?: string
120
- }): string | undefined {
121
- if (!meta) return undefined
122
- const parts: Array<string> = []
123
- if (meta.name) parts.push(meta.name)
124
- if (meta.description) parts.push(meta.description)
125
- return parts.length > 0 ? parts.join(' — ') : undefined
126
- }
127
-
128
- function SequenceListRow(props: {
129
- reg: SequenceRegistrationView
130
- getSequences: () => Array<SequenceRegistrationView>
131
- sequenceProgressNow: () => number
132
- selectedId: () => string | null
133
- setSelectedId: (id: string | null) => void
134
- }) {
135
- const styles = useStyles()
136
-
137
- const sequenceRegistrations = () => props.getSequences()
138
-
139
- const targetConflicts = () =>
140
- findSequenceTargetConflicts(props.reg, sequenceRegistrations())
141
- const scopeConflicts = () =>
142
- findSequenceScopeConflicts(props.reg, sequenceRegistrations())
143
-
144
- const hasTargetConflict = () => targetConflicts().length > 0
145
- 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
- }
178
-
179
- const enabled = () => props.reg.options.enabled !== false
180
-
181
- const liveSequenceReg = createMemo(
182
- () =>
183
- sequenceRegistrations().find((r) => r.id === props.reg.id) ?? props.reg,
184
- )
185
-
186
- const triggerCount = () =>
187
- sequenceRegistrations().find((r) => r.id === props.reg.id)?.triggerCount ??
188
- props.reg.triggerCount
189
-
190
- const matchedSteps = createMemo(() =>
191
- effectiveSequenceMatchedSteps(
192
- liveSequenceReg(),
193
- props.sequenceProgressNow(),
194
- ),
195
- )
196
-
197
- const [prevCount, setPrevCount] = createSignal(props.reg.triggerCount)
198
- const [pulsing, setPulsing] = createSignal(false)
199
-
200
- createEffect(
201
- on(triggerCount, (current) => {
202
- if (current > prevCount()) {
203
- setPulsing(true)
204
- setTimeout(() => setPulsing(false), 600)
205
- }
206
- setPrevCount(current)
207
- }),
208
- )
209
-
210
- const metaName = () => props.reg.options.meta?.name
211
- const rowTooltip = () => buildRowTooltip(props.reg.options.meta)
212
-
213
- return (
214
- <div
215
- class={clsx(
216
- styles().hotkeyRow,
217
- props.selectedId() === props.reg.id && styles().hotkeyRowSelected,
218
- pulsing() && styles().hotkeyRowTriggered,
219
- rowTooltip() && styles().tooltip,
220
- )}
221
- onClick={() => props.setSelectedId(props.reg.id)}
222
- >
223
- <Show when={rowTooltip()}>
224
- <span class={styles().rowTooltipText} data-tooltip>
225
- {rowTooltip()}
226
- </span>
227
- </Show>
228
- <span class={styles().hotkeyLabel}>
229
- <span class={styles().hotkeyLabelKeys}>
230
- <For each={liveSequenceReg().sequence}>
231
- {(step, i) => (
232
- <span>
233
- <Show when={i() > 0}> </Show>
234
- <span
235
- class={
236
- i() < matchedSteps()
237
- ? styles().sequenceStepMatched
238
- : undefined
239
- }
240
- >
241
- {formatForDisplay(step)}
242
- </span>
243
- </span>
244
- )}
245
- </For>
246
- </span>
247
- <Show when={metaName()}>
248
- <span class={styles().hotkeyLabelName}>{metaName()}</span>
249
- </Show>
250
- </span>
251
- <Show when={triggerCount() > 0}>
252
- <span class={styles().triggerCount}>x{triggerCount()}</span>
253
- </Show>
254
- <div class={styles().hotkeyBadges}>
255
- {hasTargetConflict() && (
256
- <span
257
- class={clsx(
258
- styles().badge,
259
- styles()[targetConflictBadge().style],
260
- styles().tooltip,
261
- )}
262
- >
263
- {targetConflictBadge().label}
264
- <span class={styles().tooltipText} data-tooltip>
265
- {targetConflictBadge().tooltip}
266
- </span>
267
- </span>
268
- )}
269
- {hasScopeConflict() && (
270
- <span
271
- class={clsx(styles().badge, styles().badgeInfo, styles().tooltip)}
272
- >
273
- i
274
- <span class={styles().tooltipText} data-tooltip>
275
- {scopeConflictTooltip()}
276
- </span>
277
- </span>
278
- )}
279
- <span
280
- class={clsx(
281
- styles().badge,
282
- enabled() ? styles().badgeEnabled : styles().badgeDisabled,
283
- styles().tooltip,
284
- )}
285
- >
286
- {enabled() ? 'on' : 'off'}
287
- <span class={styles().tooltipText} data-tooltip>
288
- {enabled() ? 'Sequence is enabled' : 'Sequence is disabled'}
289
- </span>
290
- </span>
291
- <span
292
- class={clsx(
293
- styles().badge,
294
- (props.reg.options.eventType ?? 'keydown') === 'keydown'
295
- ? styles().badgeKeydown
296
- : styles().badgeKeyup,
297
- styles().tooltip,
298
- )}
299
- >
300
- {props.reg.options.eventType ?? 'keydown'}
301
- <span class={styles().tooltipText} data-tooltip>
302
- Fires on {props.reg.options.eventType ?? 'keydown'} event
303
- </span>
304
- </span>
305
- <span
306
- class={clsx(styles().badge, styles().badgeTarget, styles().tooltip)}
307
- >
308
- {getTargetLabel(props.reg.target)}
309
- <span class={styles().tooltipText} data-tooltip>
310
- {getTargetTooltip(props.reg.target)}
311
- </span>
312
- </span>
313
- </div>
314
- </div>
315
- )
316
- }
317
-
318
- export function HotkeyList(props: HotkeyListProps) {
319
- const styles = useStyles()
320
- const state = useHotkeysDevtoolsState()
321
-
322
- const [searchQuery, setSearchQuery] = createSignal('')
323
-
324
- const registrations = createMemo(() => state.registrations())
325
- const sequenceRegistrations = createMemo(() => state.sequenceRegistrations())
326
-
327
- const filteredRegistrations = createMemo(() => {
328
- const query = searchQuery().trim()
329
- if (!query) return registrations()
330
- return matchSorter(registrations(), query, {
331
- keys: [
332
- (r) => formatForDisplay(r.hotkey),
333
- 'hotkey',
334
- 'options.meta.name',
335
- 'options.meta.description',
336
- ],
337
- })
338
- })
339
-
340
- const filteredSequenceRegistrations = createMemo(() => {
341
- const query = searchQuery().trim()
342
- if (!query) return sequenceRegistrations()
343
- return matchSorter(sequenceRegistrations(), query, {
344
- keys: [
345
- (r) => r.sequence.map((s) => formatForDisplay(s)).join(' '),
346
- (r) => r.sequence.join(' '),
347
- 'options.meta.name',
348
- 'options.meta.description',
349
- ],
350
- })
351
- })
352
-
353
- return (
354
- <>
355
- <div class={styles().searchContainer}>
356
- <input
357
- type="text"
358
- class={styles().searchInput}
359
- placeholder="Search hotkeys..."
360
- value={searchQuery()}
361
- onInput={(e) => setSearchQuery(e.currentTarget.value)}
362
- />
363
- <Show when={searchQuery()}>
364
- <button
365
- class={styles().searchClear}
366
- onClick={() => setSearchQuery('')}
367
- >
368
- &times;
369
- </button>
370
- </Show>
371
- </div>
372
-
373
- <div class={styles().panelHeader}>
374
- Hotkeys ({filteredRegistrations().length})
375
- </div>
376
- <div class={styles().hotkeyList}>
377
- <For each={filteredRegistrations()}>
378
- {(reg) => {
379
- const targetConflicts = () =>
380
- findTargetConflicts(reg, registrations())
381
- const scopeConflicts = () =>
382
- findScopeConflicts(reg, registrations())
383
-
384
- const hasTargetConflict = () => targetConflicts().length > 0
385
- 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
- }
419
-
420
- const enabled = () => reg.options.enabled !== false
421
-
422
- // Look up trigger count reactively from the registrations list
423
- // (reg is a stable object ref; the list re-derives on store change)
424
- const triggerCount = () =>
425
- registrations().find((r) => r.id === reg.id)?.triggerCount ??
426
- reg.triggerCount
427
-
428
- // Track trigger count changes for pulse animation
429
- const [prevCount, setPrevCount] = createSignal(reg.triggerCount)
430
- const [pulsing, setPulsing] = createSignal(false)
431
-
432
- createEffect(
433
- on(triggerCount, (current) => {
434
- if (current > prevCount()) {
435
- setPulsing(true)
436
- setTimeout(() => setPulsing(false), 600)
437
- }
438
- setPrevCount(current)
439
- }),
440
- )
441
-
442
- const metaName = () => reg.options.meta?.name
443
- const rowTooltip = () => buildRowTooltip(reg.options.meta)
444
-
445
- return (
446
- <div
447
- class={clsx(
448
- styles().hotkeyRow,
449
- props.selectedId() === reg.id && styles().hotkeyRowSelected,
450
- pulsing() && styles().hotkeyRowTriggered,
451
- rowTooltip() && styles().tooltip,
452
- )}
453
- onClick={() => props.setSelectedId(reg.id)}
454
- >
455
- <Show when={rowTooltip()}>
456
- <span class={styles().rowTooltipText} data-tooltip>
457
- {rowTooltip()}
458
- </span>
459
- </Show>
460
- <span class={styles().hotkeyLabel}>
461
- <span class={styles().hotkeyLabelKeys}>
462
- {formatForDisplay(reg.hotkey)}
463
- </span>
464
- <Show when={metaName()}>
465
- <span class={styles().hotkeyLabelName}>{metaName()}</span>
466
- </Show>
467
- </span>
468
- <Show when={triggerCount() > 0}>
469
- <span class={styles().triggerCount}>x{triggerCount()}</span>
470
- </Show>
471
- <div class={styles().hotkeyBadges}>
472
- {hasTargetConflict() && (
473
- <span
474
- class={clsx(
475
- styles().badge,
476
- styles()[targetConflictBadge().style],
477
- styles().tooltip,
478
- )}
479
- >
480
- {targetConflictBadge().label}
481
- <span class={styles().tooltipText} data-tooltip>
482
- {targetConflictBadge().tooltip}
483
- </span>
484
- </span>
485
- )}
486
- {hasScopeConflict() && (
487
- <span
488
- class={clsx(
489
- styles().badge,
490
- styles().badgeInfo,
491
- styles().tooltip,
492
- )}
493
- >
494
- i
495
- <span class={styles().tooltipText} data-tooltip>
496
- {scopeConflictTooltip()}
497
- </span>
498
- </span>
499
- )}
500
- <span
501
- class={clsx(
502
- styles().badge,
503
- enabled()
504
- ? styles().badgeEnabled
505
- : styles().badgeDisabled,
506
- styles().tooltip,
507
- )}
508
- >
509
- {enabled() ? 'on' : 'off'}
510
- <span class={styles().tooltipText} data-tooltip>
511
- {enabled() ? 'Hotkey is enabled' : 'Hotkey is disabled'}
512
- </span>
513
- </span>
514
- <span
515
- class={clsx(
516
- styles().badge,
517
- (reg.options.eventType ?? 'keydown') === 'keydown'
518
- ? styles().badgeKeydown
519
- : styles().badgeKeyup,
520
- styles().tooltip,
521
- )}
522
- >
523
- {reg.options.eventType ?? 'keydown'}
524
- <span class={styles().tooltipText} data-tooltip>
525
- Fires on {reg.options.eventType ?? 'keydown'} event
526
- </span>
527
- </span>
528
- <span
529
- class={clsx(
530
- styles().badge,
531
- styles().badgeTarget,
532
- styles().tooltip,
533
- )}
534
- >
535
- {getTargetLabel(reg.target)}
536
- <span class={styles().tooltipText} data-tooltip>
537
- {getTargetTooltip(reg.target)}
538
- </span>
539
- </span>
540
- </div>
541
- </div>
542
- )
543
- }}
544
- </For>
545
- </div>
546
-
547
- <div class={styles().panelHeader}>
548
- Sequences ({filteredSequenceRegistrations().length})
549
- </div>
550
- <div class={styles().hotkeyList}>
551
- <For each={filteredSequenceRegistrations()}>
552
- {(reg) => (
553
- <SequenceListRow
554
- reg={reg}
555
- getSequences={sequenceRegistrations}
556
- sequenceProgressNow={state.sequenceProgressNow}
557
- selectedId={props.selectedId}
558
- setSelectedId={props.setSelectedId}
559
- />
560
- )}
561
- </For>
562
- </div>
563
- </>
564
- )
565
- }