@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.
@@ -1,4 +1,10 @@
1
- import { createMemo, createSignal, onCleanup, onMount } from 'solid-js'
1
+ import {
2
+ createEffect,
3
+ createMemo,
4
+ createSignal,
5
+ onCleanup,
6
+ onMount,
7
+ } from 'solid-js'
2
8
  import { Header, HeaderLogo, MainPanel } from '@tanstack/devtools-ui'
3
9
  import { useStyles } from '../styles/use-styles'
4
10
  import { useHotkeysDevtoolsState } from '../HotkeysContextProvider'
@@ -6,12 +12,33 @@ import { HeldKeysBar } from './HeldKeysTopbar'
6
12
  import { HotkeyList } from './HotkeyList'
7
13
  import { DetailsPanel } from './DetailsPanel'
8
14
 
15
+ function readResizeObserverBlockSize(entry: ResizeObserverEntry): number {
16
+ const [box] = entry.borderBoxSize
17
+ if (box !== undefined) return box.blockSize
18
+ return entry.contentRect.height
19
+ }
20
+
21
+ /** TanStack devtools plugin pane; percentage heights break without this. */
22
+ function resolvePluginHost(el: HTMLElement): HTMLElement | null {
23
+ const found = el.closest('[id^="plugin-container-"]')
24
+ if (found instanceof HTMLElement) return found
25
+ let n: HTMLElement | null = el
26
+ for (let i = 0; i < 3 && n; i++) {
27
+ n = n.parentElement
28
+ }
29
+ return n
30
+ }
31
+
9
32
  export function Shell() {
10
33
  const styles = useStyles()
11
34
  const state = useHotkeysDevtoolsState()
12
35
  const [selectedId, setSelectedId] = createSignal<string | null>(null)
13
36
  const [leftPanelWidth, setLeftPanelWidth] = createSignal(300)
14
37
  const [isDragging, setIsDragging] = createSignal(false)
38
+ const [shellRootEl, setShellRootEl] = createSignal<HTMLElement | null>(null)
39
+ const [slotHeightPx, setSlotHeightPx] = createSignal<number | undefined>(
40
+ undefined,
41
+ )
15
42
 
16
43
  const selectedRegistration = createMemo(() => {
17
44
  const id = selectedId()
@@ -60,45 +87,85 @@ export function Shell() {
60
87
  document.removeEventListener('mouseup', handleMouseUp)
61
88
  })
62
89
 
90
+ createEffect(() => {
91
+ const el = shellRootEl()
92
+ if (!el || typeof ResizeObserver === 'undefined') return
93
+ const pluginHost = resolvePluginHost(el)
94
+ if (!pluginHost) return
95
+
96
+ const applyHeight = (entry?: ResizeObserverEntry) => {
97
+ const clientH = pluginHost.clientHeight
98
+ const fromObserver =
99
+ entry !== undefined ? readResizeObserverBlockSize(entry) : 0
100
+ const h = clientH > 0 ? clientH : fromObserver
101
+ if (h > 0) setSlotHeightPx(Math.round(h))
102
+ }
103
+
104
+ applyHeight()
105
+ const ro = new ResizeObserver((entries) => {
106
+ applyHeight(entries[0])
107
+ })
108
+ ro.observe(pluginHost)
109
+ onCleanup(() => ro.disconnect())
110
+ })
111
+
63
112
  return (
64
- <MainPanel>
65
- <Header>
66
- <HeaderLogo
67
- flavor={{
68
- light: 'oklch(51.4% 0.222 16.935)',
69
- dark: 'oklch(58.6% 0.253 17.585)',
70
- }}
71
- >
72
- TanStack Hotkeys
73
- </HeaderLogo>
74
- </Header>
75
-
76
- <div class={styles().mainContainer}>
77
- <HeldKeysBar />
78
-
79
- <div class={styles().panelsContainer}>
80
- <div
81
- class={styles().leftPanel}
82
- style={{
83
- width: `${leftPanelWidth()}px`,
84
- 'min-width': '150px',
85
- 'max-width': '800px',
113
+ <div
114
+ ref={(el) => setShellRootEl(el)}
115
+ class={styles().shellRoot}
116
+ style={{
117
+ ...(slotHeightPx() !== undefined
118
+ ? {
119
+ height: `${slotHeightPx()}px`,
120
+ 'max-height': `${slotHeightPx()}px`,
121
+ }
122
+ : {}),
123
+ }}
124
+ >
125
+ <MainPanel class={styles().mainPanelShell}>
126
+ <Header>
127
+ <HeaderLogo
128
+ flavor={{
129
+ light: 'oklch(51.4% 0.222 16.935)',
130
+ dark: 'oklch(58.6% 0.253 17.585)',
86
131
  }}
87
132
  >
88
- <HotkeyList selectedId={selectedId} setSelectedId={setSelectedId} />
89
- </div>
133
+ TanStack Hotkeys
134
+ </HeaderLogo>
135
+ </Header>
136
+
137
+ <div class={styles().mainContainer}>
138
+ <HeldKeysBar />
139
+
140
+ <div class={styles().panelsContainer}>
141
+ <div
142
+ class={styles().leftPanel}
143
+ style={{
144
+ width: `${leftPanelWidth()}px`,
145
+ 'min-width': '150px',
146
+ 'max-width': '800px',
147
+ }}
148
+ >
149
+ <div class={styles().leftPanelScroll}>
150
+ <HotkeyList
151
+ selectedId={selectedId}
152
+ setSelectedId={setSelectedId}
153
+ />
154
+ </div>
155
+ </div>
90
156
 
91
- <div
92
- class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
93
- onMouseDown={handleMouseDown}
94
- />
157
+ <div
158
+ class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
159
+ onMouseDown={handleMouseDown}
160
+ />
95
161
 
96
- <div class={styles().rightPanel} style={{ flex: 1 }}>
97
- <div class={styles().panelHeader}>Details</div>
98
- <DetailsPanel selectedRegistration={selectedRegistration} />
162
+ <div class={styles().rightPanel} style={{ flex: 1 }}>
163
+ <div class={styles().panelHeader}>Details</div>
164
+ <DetailsPanel selectedRegistration={selectedRegistration} />
165
+ </div>
99
166
  </div>
100
167
  </div>
101
- </div>
102
- </MainPanel>
168
+ </MainPanel>
169
+ </div>
103
170
  )
104
171
  }
@@ -0,0 +1,29 @@
1
+ import { DEFAULT_SEQUENCE_TIMEOUT } from '@tanstack/hotkeys'
2
+ import type { SequenceRegistrationView } from '@tanstack/hotkeys'
3
+
4
+ /**
5
+ * Matched step count for UI, accounting for inter-key timeout even if the
6
+ * manager has not processed another event yet.
7
+ */
8
+ export function effectiveSequenceMatchedSteps(
9
+ reg: SequenceRegistrationView,
10
+ now: number,
11
+ ): number {
12
+ const timeout = reg.options.timeout ?? DEFAULT_SEQUENCE_TIMEOUT
13
+ if (
14
+ reg.matchedStepCount <= 0 ||
15
+ reg.partialMatchLastKeyTime <= 0 ||
16
+ now - reg.partialMatchLastKeyTime > timeout
17
+ ) {
18
+ return 0
19
+ }
20
+ return reg.matchedStepCount
21
+ }
22
+
23
+ /** Whether the devtools should run a short interval to refresh sequence progress UI. */
24
+ export function needsSequenceProgressClock(
25
+ regs: Array<SequenceRegistrationView>,
26
+ now: number,
27
+ ): boolean {
28
+ return regs.some((reg) => effectiveSequenceMatchedSteps(reg, now) > 0)
29
+ }
@@ -10,9 +10,30 @@ const stylesFactory = (theme: 'light' | 'dark') => {
10
10
  const t = (light: string, dark: string) => (theme === 'light' ? light : dark)
11
11
 
12
12
  return {
13
+ /** Full-height flex child so MainPanel gets a bounded height from the portal */
14
+ shellRoot: css`
15
+ display: flex;
16
+ flex-direction: column;
17
+ min-height: 0;
18
+ height: 100%;
19
+ flex: 1;
20
+ overflow: hidden;
21
+ `,
22
+ /** Overrides devtools-ui MainPanel (overflow-y: auto) so inner columns scroll independently */
23
+ mainPanelShell: css`
24
+ display: flex;
25
+ flex-direction: column;
26
+ flex: 1 1 0%;
27
+ min-height: 0;
28
+ height: auto;
29
+ overflow: hidden !important;
30
+ `,
13
31
  mainContainer: css`
14
32
  display: flex;
15
33
  flex-direction: column;
34
+ flex: 1 1 0%;
35
+ min-height: 0;
36
+ overflow: hidden;
16
37
  padding: ${size[2]};
17
38
  padding-top: 0;
18
39
  margin-top: ${size[2]};
@@ -47,6 +68,10 @@ const stylesFactory = (theme: 'light' | 'dark') => {
47
68
  `,
48
69
  panelsContainer: css`
49
70
  display: flex;
71
+ flex: 1 1 0%;
72
+ min-height: 0;
73
+ overflow: hidden;
74
+ align-items: stretch;
50
75
  `,
51
76
  dragHandle: css`
52
77
  width: 8px;
@@ -129,6 +154,13 @@ const stylesFactory = (theme: 'light' | 'dark') => {
129
154
  display: flex;
130
155
  flex-direction: column;
131
156
  flex-shrink: 0;
157
+ min-height: 0;
158
+ min-width: 0;
159
+ `,
160
+ leftPanelScroll: css`
161
+ flex: 1;
162
+ min-height: 0;
163
+ overflow-y: auto;
132
164
  `,
133
165
  rightPanel: css`
134
166
  background: ${t(colors.gray[100], colors.darkGray[800])};
@@ -137,6 +169,9 @@ const stylesFactory = (theme: 'light' | 'dark') => {
137
169
  display: flex;
138
170
  flex-direction: column;
139
171
  flex: 1;
172
+ min-height: 0;
173
+ min-width: 0;
174
+ overflow: hidden;
140
175
  `,
141
176
  panelHeader: css`
142
177
  font-size: ${fontSize.md};
@@ -198,6 +233,10 @@ const stylesFactory = (theme: 'light' | 'dark') => {
198
233
  text-overflow: ellipsis;
199
234
  white-space: nowrap;
200
235
  `,
236
+ sequenceStepMatched: css`
237
+ font-weight: ${font.weight.bold};
238
+ color: ${t(colors.blue[700], colors.blue[300])};
239
+ `,
201
240
  hotkeyBadges: css`
202
241
  display: flex;
203
242
  gap: ${size[1]};
@@ -250,6 +289,11 @@ const stylesFactory = (theme: 'light' | 'dark') => {
250
289
  background: ${t(colors.blue[100], colors.blue[900] + alpha[30])};
251
290
  `,
252
291
  stateDetails: css`
292
+ display: flex;
293
+ flex-direction: column;
294
+ flex: 1;
295
+ min-height: 0;
296
+ overflow-y: auto;
253
297
  padding: ${size[2]};
254
298
  `,
255
299
  stateHeader: css`
@@ -379,6 +423,11 @@ const stylesFactory = (theme: 'light' | 'dark') => {
379
423
  text-transform: capitalize;
380
424
  white-space: nowrap;
381
425
  `,
426
+ keyCapLargeInProgress: css`
427
+ border-color: ${t(colors.blue[500], colors.blue[400])};
428
+ color: ${t(colors.blue[900], colors.blue[200])};
429
+ background: ${t(colors.blue[50], colors.darkGray[500])};
430
+ `,
382
431
  conflictList: css`
383
432
  display: flex;
384
433
  flex-direction: column;