@tanstack/hotkeys-devtools 0.0.1 → 0.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.
- package/LICENSE +21 -0
- package/README.md +121 -45
- package/dist/HotkeysContextProvider.js +47 -0
- package/dist/HotkeysContextProvider.js.map +1 -0
- package/dist/HotkeysDevtools.js +14 -0
- package/dist/HotkeysDevtools.js.map +1 -0
- package/dist/components/ActionButtons.js +33 -0
- package/dist/components/ActionButtons.js.map +1 -0
- package/dist/components/DetailsPanel.js +268 -0
- package/dist/components/DetailsPanel.js.map +1 -0
- package/dist/components/HeldKeysTopbar.js +75 -0
- package/dist/components/HeldKeysTopbar.js.map +1 -0
- package/dist/components/HotkeyList.js +188 -0
- package/dist/components/HotkeyList.js.map +1 -0
- package/dist/components/Shell.js +98 -0
- package/dist/components/Shell.js.map +1 -0
- package/dist/core.d.ts +24 -0
- package/dist/core.js +9 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/production.d.ts +2 -0
- package/dist/production.js +5 -0
- package/dist/styles/tokens.js +301 -0
- package/dist/styles/tokens.js.map +1 -0
- package/dist/styles/use-styles.js +496 -0
- package/dist/styles/use-styles.js.map +1 -0
- package/package.json +59 -7
- package/src/HotkeysContextProvider.tsx +67 -0
- package/src/HotkeysDevtools.tsx +10 -0
- package/src/components/ActionButtons.tsx +25 -0
- package/src/components/DetailsPanel.tsx +298 -0
- package/src/components/HeldKeysTopbar.tsx +42 -0
- package/src/components/HotkeyList.tsx +248 -0
- package/src/components/Shell.tsx +101 -0
- package/src/core.tsx +11 -0
- package/src/index.ts +10 -0
- package/src/production.ts +5 -0
- package/src/styles/tokens.ts +305 -0
- package/src/styles/use-styles.ts +493 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HotkeyManager } from '@tanstack/hotkeys'
|
|
2
|
+
import { useStyles } from '../styles/use-styles'
|
|
3
|
+
import type { HotkeyRegistration } from '@tanstack/hotkeys'
|
|
4
|
+
|
|
5
|
+
type ActionButtonsProps = {
|
|
6
|
+
registration: HotkeyRegistration
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ActionButtons(props: ActionButtonsProps) {
|
|
10
|
+
const styles = useStyles()
|
|
11
|
+
|
|
12
|
+
const handleTrigger = () => {
|
|
13
|
+
const manager = HotkeyManager.getInstance()
|
|
14
|
+
manager.triggerRegistration(props.registration.id)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div class={styles().actionsRow}>
|
|
19
|
+
<button class={styles().actionButton} onMouseDown={handleTrigger}>
|
|
20
|
+
<span class={styles().actionDotGreen} />
|
|
21
|
+
Trigger
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { For, Show, createMemo } from 'solid-js'
|
|
2
|
+
import { formatForDisplay } from '@tanstack/hotkeys'
|
|
3
|
+
import { useStyles } from '../styles/use-styles'
|
|
4
|
+
import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
|
|
5
|
+
import { ActionButtons } from './ActionButtons'
|
|
6
|
+
import type { ConflictBehavior, HotkeyRegistration } from '@tanstack/hotkeys'
|
|
7
|
+
|
|
8
|
+
type DetailsPanelProps = {
|
|
9
|
+
selectedRegistration: () => HotkeyRegistration | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getTargetDescription(target: HTMLElement | Document | Window): string {
|
|
13
|
+
if (typeof document !== 'undefined' && target === document) {
|
|
14
|
+
return 'document'
|
|
15
|
+
}
|
|
16
|
+
if (typeof window !== 'undefined' && target === window) {
|
|
17
|
+
return 'window'
|
|
18
|
+
}
|
|
19
|
+
if (target instanceof HTMLElement) {
|
|
20
|
+
const tag = target.tagName.toLowerCase()
|
|
21
|
+
const id = target.id ? `#${target.id}` : ''
|
|
22
|
+
const cls = target.className
|
|
23
|
+
? `.${target.className.split(' ').join('.')}`
|
|
24
|
+
: ''
|
|
25
|
+
return `${tag}${id}${cls}`
|
|
26
|
+
}
|
|
27
|
+
return 'element'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findTargetConflicts(
|
|
31
|
+
registration: HotkeyRegistration,
|
|
32
|
+
all: Array<HotkeyRegistration>,
|
|
33
|
+
): Array<HotkeyRegistration> {
|
|
34
|
+
return all.filter(
|
|
35
|
+
(other) =>
|
|
36
|
+
other.id !== registration.id &&
|
|
37
|
+
other.hotkey === registration.hotkey &&
|
|
38
|
+
other.options.eventType === registration.options.eventType &&
|
|
39
|
+
other.target === registration.target,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findScopeConflicts(
|
|
44
|
+
registration: HotkeyRegistration,
|
|
45
|
+
all: Array<HotkeyRegistration>,
|
|
46
|
+
): Array<HotkeyRegistration> {
|
|
47
|
+
return all.filter(
|
|
48
|
+
(other) =>
|
|
49
|
+
other.id !== registration.id &&
|
|
50
|
+
other.hotkey === registration.hotkey &&
|
|
51
|
+
other.options.eventType === registration.options.eventType &&
|
|
52
|
+
other.target !== registration.target,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getConflictItemStyle(
|
|
57
|
+
behavior: ConflictBehavior,
|
|
58
|
+
isSameTarget: boolean,
|
|
59
|
+
):
|
|
60
|
+
| 'conflictItem'
|
|
61
|
+
| 'conflictItemAllow'
|
|
62
|
+
| 'conflictItemError'
|
|
63
|
+
| 'conflictItemScope' {
|
|
64
|
+
if (!isSameTarget) return 'conflictItemScope'
|
|
65
|
+
if (behavior === 'allow') return 'conflictItemAllow'
|
|
66
|
+
if (behavior === 'error') return 'conflictItemError'
|
|
67
|
+
return 'conflictItem'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getConflictLabel(
|
|
71
|
+
behavior: ConflictBehavior,
|
|
72
|
+
isSameTarget: boolean,
|
|
73
|
+
): string {
|
|
74
|
+
if (!isSameTarget) return 'scope'
|
|
75
|
+
if (behavior === 'allow') return 'allowed'
|
|
76
|
+
if (behavior === 'error') return 'error'
|
|
77
|
+
if (behavior === 'replace') return 'replaced'
|
|
78
|
+
return 'warning'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function DetailsPanel(props: DetailsPanelProps) {
|
|
82
|
+
const styles = useStyles()
|
|
83
|
+
const state = useHotkeysDevtoolsState()
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div class={styles().stateDetails}>
|
|
87
|
+
<Show
|
|
88
|
+
when={props.selectedRegistration()}
|
|
89
|
+
fallback={
|
|
90
|
+
<div class={styles().noSelection}>
|
|
91
|
+
Select a hotkey from the list to view its details
|
|
92
|
+
</div>
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
{(reg) => {
|
|
96
|
+
const parsed = () => reg().parsedHotkey
|
|
97
|
+
const targetConflicts = createMemo(() =>
|
|
98
|
+
findTargetConflicts(reg(), state.registrations()),
|
|
99
|
+
)
|
|
100
|
+
const scopeConflicts = createMemo(() =>
|
|
101
|
+
findScopeConflicts(reg(), state.registrations()),
|
|
102
|
+
)
|
|
103
|
+
const allConflicts = createMemo(() => [
|
|
104
|
+
...targetConflicts(),
|
|
105
|
+
...scopeConflicts(),
|
|
106
|
+
])
|
|
107
|
+
const conflictBehavior = (): ConflictBehavior =>
|
|
108
|
+
reg().options.conflictBehavior ?? 'warn'
|
|
109
|
+
|
|
110
|
+
// Build key parts for visual breakdown
|
|
111
|
+
const keyParts = createMemo(() => {
|
|
112
|
+
const parts: Array<string> = []
|
|
113
|
+
const p = parsed()
|
|
114
|
+
if (p.ctrl) parts.push('Ctrl')
|
|
115
|
+
if (p.shift) parts.push('Shift')
|
|
116
|
+
if (p.alt) parts.push('Alt')
|
|
117
|
+
if (p.meta) parts.push('Meta')
|
|
118
|
+
parts.push(p.key)
|
|
119
|
+
return parts
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
<div class={styles().stateHeader}>
|
|
125
|
+
<div class={styles().stateTitle}>
|
|
126
|
+
{formatForDisplay(reg().hotkey)}
|
|
127
|
+
</div>
|
|
128
|
+
<div class={styles().infoGrid}>
|
|
129
|
+
<div class={styles().infoLabel}>ID</div>
|
|
130
|
+
<div class={styles().infoValueMono}>{reg().id}</div>
|
|
131
|
+
<div class={styles().infoLabel}>Raw</div>
|
|
132
|
+
<div class={styles().infoValueMono}>{reg().hotkey}</div>
|
|
133
|
+
<div class={styles().infoLabel}>Target</div>
|
|
134
|
+
<div class={styles().infoValueMono}>
|
|
135
|
+
{getTargetDescription(reg().target)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class={styles().detailsGrid}>
|
|
141
|
+
{/* Key Breakdown */}
|
|
142
|
+
<div class={styles().detailSection}>
|
|
143
|
+
<div class={styles().detailSectionHeader}>Key Breakdown</div>
|
|
144
|
+
<div class={styles().keyBreakdown}>
|
|
145
|
+
<For each={keyParts()}>
|
|
146
|
+
{(part, i) => (
|
|
147
|
+
<>
|
|
148
|
+
<Show when={i() > 0}>
|
|
149
|
+
<span class={styles().keyBreakdownPlus}>+</span>
|
|
150
|
+
</Show>
|
|
151
|
+
<span class={styles().keyCapLarge}>{part}</span>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
</For>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Actions */}
|
|
159
|
+
<div class={styles().detailSection}>
|
|
160
|
+
<div class={styles().detailSectionHeader}>Actions</div>
|
|
161
|
+
<ActionButtons registration={reg()} />
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Options */}
|
|
165
|
+
<div class={styles().detailSection}>
|
|
166
|
+
<div class={styles().detailSectionHeader}>Options</div>
|
|
167
|
+
<div>
|
|
168
|
+
<div class={styles().optionRow}>
|
|
169
|
+
<span class={styles().optionLabel}>enabled</span>
|
|
170
|
+
<span
|
|
171
|
+
class={
|
|
172
|
+
reg().options.enabled !== false
|
|
173
|
+
? styles().optionValueTrue
|
|
174
|
+
: styles().optionValueFalse
|
|
175
|
+
}
|
|
176
|
+
>
|
|
177
|
+
{String(reg().options.enabled !== false)}
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
<div class={styles().optionRow}>
|
|
181
|
+
<span class={styles().optionLabel}>eventType</span>
|
|
182
|
+
<span class={styles().optionValue}>
|
|
183
|
+
{reg().options.eventType ?? 'keydown'}
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div class={styles().optionRow}>
|
|
187
|
+
<span class={styles().optionLabel}>preventDefault</span>
|
|
188
|
+
<span
|
|
189
|
+
class={
|
|
190
|
+
reg().options.preventDefault
|
|
191
|
+
? styles().optionValueTrue
|
|
192
|
+
: styles().optionValueFalse
|
|
193
|
+
}
|
|
194
|
+
>
|
|
195
|
+
{String(!!reg().options.preventDefault)}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div class={styles().optionRow}>
|
|
199
|
+
<span class={styles().optionLabel}>stopPropagation</span>
|
|
200
|
+
<span
|
|
201
|
+
class={
|
|
202
|
+
reg().options.stopPropagation
|
|
203
|
+
? styles().optionValueTrue
|
|
204
|
+
: styles().optionValueFalse
|
|
205
|
+
}
|
|
206
|
+
>
|
|
207
|
+
{String(!!reg().options.stopPropagation)}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div class={styles().optionRow}>
|
|
211
|
+
<span class={styles().optionLabel}>ignoreInputs</span>
|
|
212
|
+
<span
|
|
213
|
+
class={
|
|
214
|
+
reg().options.ignoreInputs !== false
|
|
215
|
+
? styles().optionValueTrue
|
|
216
|
+
: styles().optionValueFalse
|
|
217
|
+
}
|
|
218
|
+
>
|
|
219
|
+
{String(reg().options.ignoreInputs !== false)}
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
<div class={styles().optionRow}>
|
|
223
|
+
<span class={styles().optionLabel}>requireReset</span>
|
|
224
|
+
<span
|
|
225
|
+
class={
|
|
226
|
+
reg().options.requireReset
|
|
227
|
+
? styles().optionValueTrue
|
|
228
|
+
: styles().optionValueFalse
|
|
229
|
+
}
|
|
230
|
+
>
|
|
231
|
+
{String(!!reg().options.requireReset)}
|
|
232
|
+
</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div class={styles().optionRow}>
|
|
235
|
+
<span class={styles().optionLabel}>conflictBehavior</span>
|
|
236
|
+
<span class={styles().optionValue}>
|
|
237
|
+
{conflictBehavior()}
|
|
238
|
+
</span>
|
|
239
|
+
</div>
|
|
240
|
+
<div class={styles().optionRow}>
|
|
241
|
+
<span class={styles().optionLabel}>hasFired</span>
|
|
242
|
+
<span
|
|
243
|
+
class={
|
|
244
|
+
reg().hasFired
|
|
245
|
+
? styles().optionValueTrue
|
|
246
|
+
: styles().optionValueFalse
|
|
247
|
+
}
|
|
248
|
+
>
|
|
249
|
+
{String(reg().hasFired)}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Conflicts */}
|
|
256
|
+
<Show when={allConflicts().length > 0}>
|
|
257
|
+
<div class={styles().detailSection}>
|
|
258
|
+
<div class={styles().detailSectionHeader}>
|
|
259
|
+
Conflicts ({allConflicts().length})
|
|
260
|
+
</div>
|
|
261
|
+
<div class={styles().conflictList}>
|
|
262
|
+
<For each={targetConflicts()}>
|
|
263
|
+
{(conflict) => {
|
|
264
|
+
const itemStyle = () =>
|
|
265
|
+
getConflictItemStyle(conflictBehavior(), true)
|
|
266
|
+
const label = () =>
|
|
267
|
+
getConflictLabel(conflictBehavior(), true)
|
|
268
|
+
return (
|
|
269
|
+
<div class={styles()[itemStyle()]}>
|
|
270
|
+
<span>{label()}</span> {conflict.id}:{' '}
|
|
271
|
+
{formatForDisplay(conflict.hotkey)} (
|
|
272
|
+
{conflict.options.eventType ?? 'keydown'}) on{' '}
|
|
273
|
+
{getTargetDescription(conflict.target)}
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
}}
|
|
277
|
+
</For>
|
|
278
|
+
<For each={scopeConflicts()}>
|
|
279
|
+
{(conflict) => (
|
|
280
|
+
<div class={styles().conflictItemScope}>
|
|
281
|
+
<span>scope</span> {conflict.id}:{' '}
|
|
282
|
+
{formatForDisplay(conflict.hotkey)} (
|
|
283
|
+
{conflict.options.eventType ?? 'keydown'}) on{' '}
|
|
284
|
+
{getTargetDescription(conflict.target)}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</For>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
</Show>
|
|
291
|
+
</div>
|
|
292
|
+
</>
|
|
293
|
+
)
|
|
294
|
+
}}
|
|
295
|
+
</Show>
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js'
|
|
2
|
+
import { formatKeyForDebuggingDisplay } from '@tanstack/hotkeys'
|
|
3
|
+
import { useStyles } from '../styles/use-styles'
|
|
4
|
+
import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
|
|
5
|
+
|
|
6
|
+
export function HeldKeysBar() {
|
|
7
|
+
const styles = useStyles()
|
|
8
|
+
const state = useHotkeysDevtoolsState()
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div class={styles().heldKeysBar}>
|
|
12
|
+
<span class={styles().heldKeysBarHeader}>Held</span>
|
|
13
|
+
<div class={styles().heldKeysBarList}>
|
|
14
|
+
<Show
|
|
15
|
+
when={state.heldKeys().length > 0}
|
|
16
|
+
fallback={<span class={styles().noKeysHeld}>--</span>}
|
|
17
|
+
>
|
|
18
|
+
<For each={state.heldKeys()}>
|
|
19
|
+
{(key) => {
|
|
20
|
+
const code = () => state.heldCodes()[key]
|
|
21
|
+
const label = () => formatKeyForDebuggingDisplay(key)
|
|
22
|
+
const codeLabel = () => {
|
|
23
|
+
const c = code()
|
|
24
|
+
return c
|
|
25
|
+
? formatKeyForDebuggingDisplay(c, { source: 'code' })
|
|
26
|
+
: undefined
|
|
27
|
+
}
|
|
28
|
+
return (
|
|
29
|
+
<span class={styles().keyCap}>
|
|
30
|
+
<span>{label()}</span>
|
|
31
|
+
<Show when={codeLabel() && codeLabel() !== key}>
|
|
32
|
+
<span class={styles().keyCapCode}>{codeLabel()}</span>
|
|
33
|
+
</Show>
|
|
34
|
+
</span>
|
|
35
|
+
)
|
|
36
|
+
}}
|
|
37
|
+
</For>
|
|
38
|
+
</Show>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import { formatForDisplay } from '@tanstack/hotkeys'
|
|
4
|
+
import { useStyles } from '../styles/use-styles'
|
|
5
|
+
import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
|
|
6
|
+
import type { ConflictBehavior, HotkeyRegistration } from '@tanstack/hotkeys'
|
|
7
|
+
|
|
8
|
+
type HotkeyListProps = {
|
|
9
|
+
selectedId: () => string | null
|
|
10
|
+
setSelectedId: (id: string | null) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getTargetLabel(target: HTMLElement | Document | Window): string {
|
|
14
|
+
if (typeof document !== 'undefined' && target === document) {
|
|
15
|
+
return 'document'
|
|
16
|
+
}
|
|
17
|
+
if (typeof window !== 'undefined' && target === window) {
|
|
18
|
+
return 'window'
|
|
19
|
+
}
|
|
20
|
+
if (target instanceof HTMLElement) {
|
|
21
|
+
return target.tagName.toLowerCase()
|
|
22
|
+
}
|
|
23
|
+
return 'element'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getTargetTooltip(target: HTMLElement | Document | Window): string {
|
|
27
|
+
if (typeof document !== 'undefined' && target === document) {
|
|
28
|
+
return 'Listening on document'
|
|
29
|
+
}
|
|
30
|
+
if (typeof window !== 'undefined' && target === window) {
|
|
31
|
+
return 'Listening on window'
|
|
32
|
+
}
|
|
33
|
+
if (target instanceof HTMLElement) {
|
|
34
|
+
const tag = target.tagName.toLowerCase()
|
|
35
|
+
const parts: Array<string> = [tag]
|
|
36
|
+
if (target.id) {
|
|
37
|
+
parts.push(`id="${target.id}"`)
|
|
38
|
+
}
|
|
39
|
+
if (target.className) {
|
|
40
|
+
const classes = target.className.split(/\s+/).filter(Boolean).join(', ')
|
|
41
|
+
parts.push(`class="${classes}"`)
|
|
42
|
+
}
|
|
43
|
+
// Collect data- attributes
|
|
44
|
+
const dataAttrs = Array.from(target.attributes)
|
|
45
|
+
.filter((attr) => attr.name.startsWith('data-'))
|
|
46
|
+
.map((attr) => `${attr.name}="${attr.value}"`)
|
|
47
|
+
if (dataAttrs.length > 0) {
|
|
48
|
+
parts.push(...dataAttrs)
|
|
49
|
+
}
|
|
50
|
+
return `Listening on ${parts.join(' ')}`
|
|
51
|
+
}
|
|
52
|
+
return 'Listening on element'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findTargetConflicts(
|
|
56
|
+
registration: HotkeyRegistration,
|
|
57
|
+
all: Array<HotkeyRegistration>,
|
|
58
|
+
): Array<HotkeyRegistration> {
|
|
59
|
+
return all.filter(
|
|
60
|
+
(other) =>
|
|
61
|
+
other.id !== registration.id &&
|
|
62
|
+
other.hotkey === registration.hotkey &&
|
|
63
|
+
other.options.eventType === registration.options.eventType &&
|
|
64
|
+
other.target === registration.target,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findScopeConflicts(
|
|
69
|
+
registration: HotkeyRegistration,
|
|
70
|
+
all: Array<HotkeyRegistration>,
|
|
71
|
+
): Array<HotkeyRegistration> {
|
|
72
|
+
return all.filter(
|
|
73
|
+
(other) =>
|
|
74
|
+
other.id !== registration.id &&
|
|
75
|
+
other.hotkey === registration.hotkey &&
|
|
76
|
+
other.options.eventType === registration.options.eventType &&
|
|
77
|
+
other.target !== registration.target,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function HotkeyList(props: HotkeyListProps) {
|
|
82
|
+
const styles = useStyles()
|
|
83
|
+
const state = useHotkeysDevtoolsState()
|
|
84
|
+
|
|
85
|
+
const registrations = createMemo(() => state.registrations())
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
<div class={styles().panelHeader}>Hotkeys ({registrations().length})</div>
|
|
90
|
+
<div class={styles().hotkeyList}>
|
|
91
|
+
<For each={registrations()}>
|
|
92
|
+
{(reg) => {
|
|
93
|
+
const targetConflicts = () =>
|
|
94
|
+
findTargetConflicts(reg, registrations())
|
|
95
|
+
const scopeConflicts = () =>
|
|
96
|
+
findScopeConflicts(reg, registrations())
|
|
97
|
+
|
|
98
|
+
const hasTargetConflict = () => targetConflicts().length > 0
|
|
99
|
+
const hasScopeConflict = () => scopeConflicts().length > 0
|
|
100
|
+
|
|
101
|
+
const conflictBehavior = (): ConflictBehavior =>
|
|
102
|
+
reg.options.conflictBehavior ?? 'warn'
|
|
103
|
+
|
|
104
|
+
const targetConflictBadge = () => {
|
|
105
|
+
const behavior = conflictBehavior()
|
|
106
|
+
const c = targetConflicts()
|
|
107
|
+
if (behavior === 'allow') {
|
|
108
|
+
return {
|
|
109
|
+
style: 'badgeAllow' as const,
|
|
110
|
+
label: '~',
|
|
111
|
+
tooltip: `Allowed: ${c.length} other binding${c.length > 1 ? 's' : ''} on same key and target (conflictBehavior: allow)`,
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (behavior === 'error') {
|
|
115
|
+
return {
|
|
116
|
+
style: 'badgeError' as const,
|
|
117
|
+
label: '!',
|
|
118
|
+
tooltip: `Error: ${c.length} conflicting binding${c.length > 1 ? 's' : ''} on same key and target (conflictBehavior: error)`,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// 'warn' (default) or 'replace' (replacement already happened, but show warn-style if somehow present)
|
|
122
|
+
return {
|
|
123
|
+
style: 'badgeConflict' as const,
|
|
124
|
+
label: '!',
|
|
125
|
+
tooltip: `Warning: ${c.length} other binding${c.length > 1 ? 's' : ''} on same key and target`,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const scopeConflictTooltip = () => {
|
|
130
|
+
const c = scopeConflicts()
|
|
131
|
+
return `Info: ${c.length} binding${c.length > 1 ? 's' : ''} with same key on different target${c.length > 1 ? 's' : ''}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const enabled = () => reg.options.enabled !== false
|
|
135
|
+
|
|
136
|
+
// Look up trigger count reactively from the registrations list
|
|
137
|
+
// (reg is a stable object ref; the list re-derives on store change)
|
|
138
|
+
const triggerCount = () =>
|
|
139
|
+
registrations().find((r) => r.id === reg.id)?.triggerCount ??
|
|
140
|
+
reg.triggerCount
|
|
141
|
+
|
|
142
|
+
// Track trigger count changes for pulse animation
|
|
143
|
+
const [prevCount, setPrevCount] = createSignal(reg.triggerCount)
|
|
144
|
+
const [pulsing, setPulsing] = createSignal(false)
|
|
145
|
+
|
|
146
|
+
createEffect(
|
|
147
|
+
on(triggerCount, (current) => {
|
|
148
|
+
if (current > prevCount()) {
|
|
149
|
+
setPulsing(true)
|
|
150
|
+
setTimeout(() => setPulsing(false), 600)
|
|
151
|
+
}
|
|
152
|
+
setPrevCount(current)
|
|
153
|
+
}),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
class={clsx(
|
|
159
|
+
styles().hotkeyRow,
|
|
160
|
+
props.selectedId() === reg.id && styles().hotkeyRowSelected,
|
|
161
|
+
pulsing() && styles().hotkeyRowTriggered,
|
|
162
|
+
)}
|
|
163
|
+
onClick={() => props.setSelectedId(reg.id)}
|
|
164
|
+
>
|
|
165
|
+
<span class={styles().hotkeyLabel}>
|
|
166
|
+
{formatForDisplay(reg.hotkey)}
|
|
167
|
+
</span>
|
|
168
|
+
<Show when={triggerCount() > 0}>
|
|
169
|
+
<span class={styles().triggerCount}>x{triggerCount()}</span>
|
|
170
|
+
</Show>
|
|
171
|
+
<div class={styles().hotkeyBadges}>
|
|
172
|
+
{hasTargetConflict() && (
|
|
173
|
+
<span
|
|
174
|
+
class={clsx(
|
|
175
|
+
styles().badge,
|
|
176
|
+
styles()[targetConflictBadge().style],
|
|
177
|
+
styles().tooltip,
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{targetConflictBadge().label}
|
|
181
|
+
<span class={styles().tooltipText} data-tooltip>
|
|
182
|
+
{targetConflictBadge().tooltip}
|
|
183
|
+
</span>
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
{hasScopeConflict() && (
|
|
187
|
+
<span
|
|
188
|
+
class={clsx(
|
|
189
|
+
styles().badge,
|
|
190
|
+
styles().badgeInfo,
|
|
191
|
+
styles().tooltip,
|
|
192
|
+
)}
|
|
193
|
+
>
|
|
194
|
+
i
|
|
195
|
+
<span class={styles().tooltipText} data-tooltip>
|
|
196
|
+
{scopeConflictTooltip()}
|
|
197
|
+
</span>
|
|
198
|
+
</span>
|
|
199
|
+
)}
|
|
200
|
+
<span
|
|
201
|
+
class={clsx(
|
|
202
|
+
styles().badge,
|
|
203
|
+
enabled()
|
|
204
|
+
? styles().badgeEnabled
|
|
205
|
+
: styles().badgeDisabled,
|
|
206
|
+
styles().tooltip,
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
{enabled() ? 'on' : 'off'}
|
|
210
|
+
<span class={styles().tooltipText} data-tooltip>
|
|
211
|
+
{enabled() ? 'Hotkey is enabled' : 'Hotkey is disabled'}
|
|
212
|
+
</span>
|
|
213
|
+
</span>
|
|
214
|
+
<span
|
|
215
|
+
class={clsx(
|
|
216
|
+
styles().badge,
|
|
217
|
+
(reg.options.eventType ?? 'keydown') === 'keydown'
|
|
218
|
+
? styles().badgeKeydown
|
|
219
|
+
: styles().badgeKeyup,
|
|
220
|
+
styles().tooltip,
|
|
221
|
+
)}
|
|
222
|
+
>
|
|
223
|
+
{reg.options.eventType ?? 'keydown'}
|
|
224
|
+
<span class={styles().tooltipText} data-tooltip>
|
|
225
|
+
Fires on {reg.options.eventType ?? 'keydown'} event
|
|
226
|
+
</span>
|
|
227
|
+
</span>
|
|
228
|
+
<span
|
|
229
|
+
class={clsx(
|
|
230
|
+
styles().badge,
|
|
231
|
+
styles().badgeTarget,
|
|
232
|
+
styles().tooltip,
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
{getTargetLabel(reg.target)}
|
|
236
|
+
<span class={styles().tooltipText} data-tooltip>
|
|
237
|
+
{getTargetTooltip(reg.target)}
|
|
238
|
+
</span>
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}}
|
|
244
|
+
</For>
|
|
245
|
+
</div>
|
|
246
|
+
</>
|
|
247
|
+
)
|
|
248
|
+
}
|