@tanstack/hotkeys-devtools 0.7.4 → 0.8.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.
@@ -1,5 +1,6 @@
1
1
  import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
2
2
  import clsx from 'clsx'
3
+ import { matchSorter } from 'match-sorter'
3
4
  import { formatForDisplay } from '@tanstack/hotkeys'
4
5
  import { useStyles } from '../styles/use-styles'
5
6
  import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
@@ -113,6 +114,17 @@ function findSequenceScopeConflicts(
113
114
  )
114
115
  }
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
+
116
128
  function SequenceListRow(props: {
117
129
  reg: SequenceRegistrationView
118
130
  getSequences: () => Array<SequenceRegistrationView>
@@ -195,32 +207,46 @@ function SequenceListRow(props: {
195
207
  }),
196
208
  )
197
209
 
210
+ const metaName = () => props.reg.options.meta?.name
211
+ const rowTooltip = () => buildRowTooltip(props.reg.options.meta)
212
+
198
213
  return (
199
214
  <div
200
215
  class={clsx(
201
216
  styles().hotkeyRow,
202
217
  props.selectedId() === props.reg.id && styles().hotkeyRowSelected,
203
218
  pulsing() && styles().hotkeyRowTriggered,
219
+ rowTooltip() && styles().tooltip,
204
220
  )}
205
221
  onClick={() => props.setSelectedId(props.reg.id)}
206
222
  >
223
+ <Show when={rowTooltip()}>
224
+ <span class={styles().rowTooltipText} data-tooltip>
225
+ {rowTooltip()}
226
+ </span>
227
+ </Show>
207
228
  <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)}
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>
220
243
  </span>
221
- </span>
222
- )}
223
- </For>
244
+ )}
245
+ </For>
246
+ </span>
247
+ <Show when={metaName()}>
248
+ <span class={styles().hotkeyLabelName}>{metaName()}</span>
249
+ </Show>
224
250
  </span>
225
251
  <Show when={triggerCount() > 0}>
226
252
  <span class={styles().triggerCount}>x{triggerCount()}</span>
@@ -293,14 +319,62 @@ export function HotkeyList(props: HotkeyListProps) {
293
319
  const styles = useStyles()
294
320
  const state = useHotkeysDevtoolsState()
295
321
 
322
+ const [searchQuery, setSearchQuery] = createSignal('')
323
+
296
324
  const registrations = createMemo(() => state.registrations())
297
325
  const sequenceRegistrations = createMemo(() => state.sequenceRegistrations())
298
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
+
299
353
  return (
300
354
  <>
301
- <div class={styles().panelHeader}>Hotkeys ({registrations().length})</div>
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>
302
376
  <div class={styles().hotkeyList}>
303
- <For each={registrations()}>
377
+ <For each={filteredRegistrations()}>
304
378
  {(reg) => {
305
379
  const targetConflicts = () =>
306
380
  findTargetConflicts(reg, registrations())
@@ -365,17 +439,31 @@ export function HotkeyList(props: HotkeyListProps) {
365
439
  }),
366
440
  )
367
441
 
442
+ const metaName = () => reg.options.meta?.name
443
+ const rowTooltip = () => buildRowTooltip(reg.options.meta)
444
+
368
445
  return (
369
446
  <div
370
447
  class={clsx(
371
448
  styles().hotkeyRow,
372
449
  props.selectedId() === reg.id && styles().hotkeyRowSelected,
373
450
  pulsing() && styles().hotkeyRowTriggered,
451
+ rowTooltip() && styles().tooltip,
374
452
  )}
375
453
  onClick={() => props.setSelectedId(reg.id)}
376
454
  >
455
+ <Show when={rowTooltip()}>
456
+ <span class={styles().rowTooltipText} data-tooltip>
457
+ {rowTooltip()}
458
+ </span>
459
+ </Show>
377
460
  <span class={styles().hotkeyLabel}>
378
- {formatForDisplay(reg.hotkey)}
461
+ <span class={styles().hotkeyLabelKeys}>
462
+ {formatForDisplay(reg.hotkey)}
463
+ </span>
464
+ <Show when={metaName()}>
465
+ <span class={styles().hotkeyLabelName}>{metaName()}</span>
466
+ </Show>
379
467
  </span>
380
468
  <Show when={triggerCount() > 0}>
381
469
  <span class={styles().triggerCount}>x{triggerCount()}</span>
@@ -457,10 +545,10 @@ export function HotkeyList(props: HotkeyListProps) {
457
545
  </div>
458
546
 
459
547
  <div class={styles().panelHeader}>
460
- Sequences ({sequenceRegistrations().length})
548
+ Sequences ({filteredSequenceRegistrations().length})
461
549
  </div>
462
550
  <div class={styles().hotkeyList}>
463
- <For each={sequenceRegistrations()}>
551
+ <For each={filteredSequenceRegistrations()}>
464
552
  {(reg) => (
465
553
  <SequenceListRow
466
554
  reg={reg}
@@ -173,6 +173,55 @@ const stylesFactory = (theme: 'light' | 'dark') => {
173
173
  min-width: 0;
174
174
  overflow: hidden;
175
175
  `,
176
+ searchContainer: css`
177
+ position: relative;
178
+ padding: ${size[2]};
179
+ padding-bottom: 0;
180
+ flex-shrink: 0;
181
+ `,
182
+ searchInput: css`
183
+ width: 100%;
184
+ padding: ${size[1.5]} ${size[2]};
185
+ padding-right: ${size[7]};
186
+ font-size: ${fontSize.xs};
187
+ font-family: ${fontFamily.sans};
188
+ color: ${t(colors.gray[900], colors.gray[100])};
189
+ background: ${t(colors.gray[200], colors.darkGray[700])};
190
+ border: 1px solid ${t(colors.gray[300], colors.darkGray[600])};
191
+ border-radius: ${border.radius.md};
192
+ outline: none;
193
+ box-sizing: border-box;
194
+
195
+ &::placeholder {
196
+ color: ${t(colors.gray[500], colors.gray[500])};
197
+ }
198
+
199
+ &:focus {
200
+ border-color: ${t(colors.blue[500], colors.blue[400])};
201
+ box-shadow: 0 0 0 1px
202
+ ${t(colors.blue[500] + alpha[30], colors.blue[400] + alpha[30])};
203
+ }
204
+ `,
205
+ searchClear: css`
206
+ position: absolute;
207
+ right: ${size[3]};
208
+ top: 50%;
209
+ transform: translateY(-50%);
210
+ background: none;
211
+ border: none;
212
+ color: ${t(colors.gray[500], colors.gray[400])};
213
+ font-size: ${fontSize.md};
214
+ cursor: pointer;
215
+ padding: ${size[1]};
216
+ line-height: 1;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+
221
+ &:hover {
222
+ color: ${t(colors.gray[700], colors.gray[200])};
223
+ }
224
+ `,
176
225
  panelHeader: css`
177
226
  font-size: ${fontSize.md};
178
227
  font-weight: ${font.weight.bold};
@@ -225,13 +274,28 @@ const stylesFactory = (theme: 'light' | 'dark') => {
225
274
  }
226
275
  `,
227
276
  hotkeyLabel: css`
228
- font-family: ${fontFamily.mono};
229
277
  font-size: ${fontSize.xs};
230
278
  color: ${t(colors.gray[900], colors.gray[100])};
231
279
  flex: 1;
232
280
  overflow: hidden;
233
281
  text-overflow: ellipsis;
234
282
  white-space: nowrap;
283
+ display: flex;
284
+ align-items: center;
285
+ gap: ${size[1.5]};
286
+ min-width: 0;
287
+ `,
288
+ hotkeyLabelKeys: css`
289
+ font-family: ${fontFamily.mono};
290
+ flex-shrink: 0;
291
+ `,
292
+ hotkeyLabelName: css`
293
+ font-family: ${fontFamily.sans};
294
+ color: ${t(colors.gray[500], colors.gray[400])};
295
+ overflow: hidden;
296
+ text-overflow: ellipsis;
297
+ white-space: nowrap;
298
+ min-width: 0;
235
299
  `,
236
300
  sequenceStepMatched: css`
237
301
  font-weight: ${font.weight.bold};
@@ -580,6 +644,29 @@ const stylesFactory = (theme: 'light' | 'dark') => {
580
644
  z-index: ${tokens.zIndices.tooltip};
581
645
  margin-top: ${size[1]};
582
646
  `,
647
+ rowTooltipText: css`
648
+ position: absolute;
649
+ bottom: 100%;
650
+ left: ${size[2]};
651
+ padding: ${size[1]} ${size[1.5]};
652
+ background: ${t(colors.gray[800], colors.darkGray[500])};
653
+ color: ${t(colors.white, colors.gray[100])};
654
+ font-size: ${fontSize['2xs']};
655
+ font-family: ${fontFamily.sans};
656
+ border-radius: ${border.radius.sm};
657
+ white-space: nowrap;
658
+ pointer-events: none;
659
+ opacity: 0;
660
+ visibility: hidden;
661
+ transition:
662
+ opacity 0.15s,
663
+ visibility 0.15s;
664
+ z-index: ${tokens.zIndices.tooltip};
665
+ margin-bottom: ${size[1]};
666
+ max-width: 300px;
667
+ overflow: hidden;
668
+ text-overflow: ellipsis;
669
+ `,
583
670
  triggerCount: css`
584
671
  font-family: ${fontFamily.mono};
585
672
  font-size: ${fontSize['2xs']};