@mdxui/terminal 2.0.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 (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,1791 @@
1
+ /**
2
+ * @mdxui/terminal Keyboard Navigation Tests
3
+ *
4
+ * TDD RED Phase: These tests define the contract for keyboard navigation
5
+ * in terminal UIs. All tests should FAIL initially because the APIs
6
+ * aren't implemented yet.
7
+ *
8
+ * Requirements tested:
9
+ * - j/k movement (Vim-style vertical navigation)
10
+ * - h/l movement (Vim-style horizontal navigation)
11
+ * - Enter/Space selection and activation
12
+ * - Tab/Shift+Tab focus cycling
13
+ * - Escape cancel/back navigation
14
+ * - Custom key bindings
15
+ * - Focus management (tracking, trapping)
16
+ */
17
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
18
+ import React from 'react'
19
+
20
+ // Mock React for testing components without full React reconciler
21
+ vi.mock('react', async () => {
22
+ const actual = await vi.importActual('react')
23
+ return {
24
+ ...actual,
25
+ createElement: vi.fn((type, props, ...children) => ({
26
+ type,
27
+ props: { ...props, children },
28
+ })),
29
+ useState: vi.fn((initial) => [initial, vi.fn()]),
30
+ useEffect: vi.fn(),
31
+ useCallback: vi.fn((fn) => fn),
32
+ useRef: vi.fn((initial) => ({ current: initial })),
33
+ useContext: vi.fn(),
34
+ createContext: vi.fn((defaultValue) => ({
35
+ Provider: vi.fn(),
36
+ Consumer: vi.fn(),
37
+ _currentValue: defaultValue,
38
+ })),
39
+ }
40
+ })
41
+
42
+ // ============================================================================
43
+ // createKeyboardManager Tests
44
+ // ============================================================================
45
+
46
+ describe('createKeyboardManager', () => {
47
+ describe('basic creation', () => {
48
+ it('creates a keyboard manager with bindings', async () => {
49
+ const { createKeyboardManager } = await import('@mdxui/terminal')
50
+
51
+ const manager = createKeyboardManager({
52
+ bindings: {
53
+ j: 'move-down',
54
+ k: 'move-up',
55
+ enter: 'select',
56
+ escape: 'back',
57
+ },
58
+ })
59
+
60
+ expect(manager).toBeDefined()
61
+ expect(typeof manager.handleKey).toBe('function')
62
+ })
63
+
64
+ it('returns actions for bound keys', async () => {
65
+ const { createKeyboardManager } = await import('@mdxui/terminal')
66
+
67
+ const manager = createKeyboardManager({
68
+ bindings: {
69
+ j: 'move-down',
70
+ k: 'move-up',
71
+ },
72
+ })
73
+
74
+ expect(manager.getAction('j')).toBe('move-down')
75
+ expect(manager.getAction('k')).toBe('move-up')
76
+ })
77
+
78
+ it('returns undefined for unbound keys', async () => {
79
+ const { createKeyboardManager } = await import('@mdxui/terminal')
80
+
81
+ const manager = createKeyboardManager({
82
+ bindings: {
83
+ j: 'move-down',
84
+ },
85
+ })
86
+
87
+ expect(manager.getAction('x')).toBeUndefined()
88
+ })
89
+ })
90
+
91
+ describe('modifier key support', () => {
92
+ it('handles ctrl modifier', async () => {
93
+ const { createKeyboardManager } = await import('@mdxui/terminal')
94
+
95
+ const manager = createKeyboardManager({
96
+ bindings: {
97
+ 'ctrl+c': 'cancel',
98
+ 'ctrl+s': 'save',
99
+ },
100
+ })
101
+
102
+ expect(manager.getAction('ctrl+c')).toBe('cancel')
103
+ expect(manager.getAction('ctrl+s')).toBe('save')
104
+ })
105
+
106
+ it('handles alt modifier', async () => {
107
+ const { createKeyboardManager } = await import('@mdxui/terminal')
108
+
109
+ const manager = createKeyboardManager({
110
+ bindings: {
111
+ 'alt+h': 'help',
112
+ },
113
+ })
114
+
115
+ expect(manager.getAction('alt+h')).toBe('help')
116
+ })
117
+
118
+ it('handles shift modifier', async () => {
119
+ const { createKeyboardManager } = await import('@mdxui/terminal')
120
+
121
+ const manager = createKeyboardManager({
122
+ bindings: {
123
+ 'shift+tab': 'focus-prev',
124
+ K: 'move-up-fast', // Capital K = shift+k
125
+ },
126
+ })
127
+
128
+ expect(manager.getAction('shift+tab')).toBe('focus-prev')
129
+ expect(manager.getAction('K')).toBe('move-up-fast')
130
+ })
131
+
132
+ it('handles combined modifiers', async () => {
133
+ const { createKeyboardManager } = await import('@mdxui/terminal')
134
+
135
+ const manager = createKeyboardManager({
136
+ bindings: {
137
+ 'ctrl+shift+s': 'save-as',
138
+ 'ctrl+alt+delete': 'force-quit',
139
+ },
140
+ })
141
+
142
+ expect(manager.getAction('ctrl+shift+s')).toBe('save-as')
143
+ })
144
+ })
145
+
146
+ describe('vim-style bindings', () => {
147
+ it('supports standard vim movement (h/j/k/l)', async () => {
148
+ const { createKeyboardManager, VIM_BINDINGS } = await import('@mdxui/terminal')
149
+
150
+ const manager = createKeyboardManager({
151
+ bindings: VIM_BINDINGS,
152
+ })
153
+
154
+ expect(manager.getAction('h')).toBe('move-left')
155
+ expect(manager.getAction('j')).toBe('move-down')
156
+ expect(manager.getAction('k')).toBe('move-up')
157
+ expect(manager.getAction('l')).toBe('move-right')
158
+ })
159
+
160
+ it('supports vim navigation bindings', async () => {
161
+ const { createKeyboardManager, VIM_BINDINGS } = await import('@mdxui/terminal')
162
+
163
+ const manager = createKeyboardManager({
164
+ bindings: VIM_BINDINGS,
165
+ })
166
+
167
+ expect(manager.getAction('gg')).toBe('move-first')
168
+ expect(manager.getAction('G')).toBe('move-last')
169
+ expect(manager.getAction('/')).toBe('search')
170
+ })
171
+
172
+ it('supports vim action bindings', async () => {
173
+ const { createKeyboardManager, VIM_BINDINGS } = await import('@mdxui/terminal')
174
+
175
+ const manager = createKeyboardManager({
176
+ bindings: VIM_BINDINGS,
177
+ })
178
+
179
+ expect(manager.getAction('enter')).toBe('select')
180
+ expect(manager.getAction('escape')).toBe('back')
181
+ expect(manager.getAction('q')).toBe('quit')
182
+ })
183
+ })
184
+
185
+ describe('handleKey callback', () => {
186
+ it('calls action handler when key matches', async () => {
187
+ const { createKeyboardManager } = await import('@mdxui/terminal')
188
+ const onAction = vi.fn()
189
+
190
+ const manager = createKeyboardManager({
191
+ bindings: { j: 'move-down' },
192
+ onAction,
193
+ })
194
+
195
+ manager.handleKey('j')
196
+ expect(onAction).toHaveBeenCalledWith('move-down', { key: 'j' })
197
+ })
198
+
199
+ it('does not call handler for unbound keys', async () => {
200
+ const { createKeyboardManager } = await import('@mdxui/terminal')
201
+ const onAction = vi.fn()
202
+
203
+ const manager = createKeyboardManager({
204
+ bindings: { j: 'move-down' },
205
+ onAction,
206
+ })
207
+
208
+ manager.handleKey('x')
209
+ expect(onAction).not.toHaveBeenCalled()
210
+ })
211
+
212
+ it('returns true when key was handled', async () => {
213
+ const { createKeyboardManager } = await import('@mdxui/terminal')
214
+
215
+ const manager = createKeyboardManager({
216
+ bindings: { j: 'move-down' },
217
+ })
218
+
219
+ expect(manager.handleKey('j')).toBe(true)
220
+ })
221
+
222
+ it('returns false when key was not handled', async () => {
223
+ const { createKeyboardManager } = await import('@mdxui/terminal')
224
+
225
+ const manager = createKeyboardManager({
226
+ bindings: { j: 'move-down' },
227
+ })
228
+
229
+ expect(manager.handleKey('x')).toBe(false)
230
+ })
231
+ })
232
+
233
+ describe('binding management', () => {
234
+ it('allows adding bindings dynamically', async () => {
235
+ const { createKeyboardManager } = await import('@mdxui/terminal')
236
+
237
+ const manager = createKeyboardManager({
238
+ bindings: { j: 'move-down' },
239
+ })
240
+
241
+ manager.addBinding('x', 'delete')
242
+ expect(manager.getAction('x')).toBe('delete')
243
+ })
244
+
245
+ it('allows removing bindings', async () => {
246
+ const { createKeyboardManager } = await import('@mdxui/terminal')
247
+
248
+ const manager = createKeyboardManager({
249
+ bindings: { j: 'move-down', k: 'move-up' },
250
+ })
251
+
252
+ manager.removeBinding('j')
253
+ expect(manager.getAction('j')).toBeUndefined()
254
+ expect(manager.getAction('k')).toBe('move-up')
255
+ })
256
+
257
+ it('allows replacing all bindings', async () => {
258
+ const { createKeyboardManager } = await import('@mdxui/terminal')
259
+
260
+ const manager = createKeyboardManager({
261
+ bindings: { j: 'move-down' },
262
+ })
263
+
264
+ manager.setBindings({ x: 'delete' })
265
+ expect(manager.getAction('j')).toBeUndefined()
266
+ expect(manager.getAction('x')).toBe('delete')
267
+ })
268
+
269
+ it('returns all current bindings', async () => {
270
+ const { createKeyboardManager } = await import('@mdxui/terminal')
271
+
272
+ const bindings = { j: 'move-down', k: 'move-up' }
273
+ const manager = createKeyboardManager({ bindings })
274
+
275
+ expect(manager.getBindings()).toEqual(bindings)
276
+ })
277
+ })
278
+
279
+ describe('enabled state', () => {
280
+ it('can be disabled', async () => {
281
+ const { createKeyboardManager } = await import('@mdxui/terminal')
282
+ const onAction = vi.fn()
283
+
284
+ const manager = createKeyboardManager({
285
+ bindings: { j: 'move-down' },
286
+ onAction,
287
+ enabled: false,
288
+ })
289
+
290
+ manager.handleKey('j')
291
+ expect(onAction).not.toHaveBeenCalled()
292
+ })
293
+
294
+ it('can toggle enabled state', async () => {
295
+ const { createKeyboardManager } = await import('@mdxui/terminal')
296
+ const onAction = vi.fn()
297
+
298
+ const manager = createKeyboardManager({
299
+ bindings: { j: 'move-down' },
300
+ onAction,
301
+ })
302
+
303
+ manager.disable()
304
+ manager.handleKey('j')
305
+ expect(onAction).not.toHaveBeenCalled()
306
+
307
+ manager.enable()
308
+ manager.handleKey('j')
309
+ expect(onAction).toHaveBeenCalledWith('move-down', { key: 'j' })
310
+ })
311
+
312
+ it('reports enabled status', async () => {
313
+ const { createKeyboardManager } = await import('@mdxui/terminal')
314
+
315
+ const manager = createKeyboardManager({
316
+ bindings: {},
317
+ enabled: false,
318
+ })
319
+
320
+ expect(manager.isEnabled()).toBe(false)
321
+ manager.enable()
322
+ expect(manager.isEnabled()).toBe(true)
323
+ })
324
+ })
325
+
326
+ describe('key sequence support', () => {
327
+ it('handles multi-key sequences like gg', async () => {
328
+ const { createKeyboardManager } = await import('@mdxui/terminal')
329
+ const onAction = vi.fn()
330
+
331
+ const manager = createKeyboardManager({
332
+ bindings: {
333
+ gg: 'move-first',
334
+ G: 'move-last',
335
+ },
336
+ onAction,
337
+ })
338
+
339
+ manager.handleKey('g')
340
+ expect(onAction).not.toHaveBeenCalled() // Waiting for second key
341
+
342
+ manager.handleKey('g')
343
+ expect(onAction).toHaveBeenCalledWith('move-first', { key: 'gg' })
344
+ })
345
+
346
+ it('times out pending sequence', async () => {
347
+ const { createKeyboardManager } = await import('@mdxui/terminal')
348
+ const onAction = vi.fn()
349
+
350
+ const manager = createKeyboardManager({
351
+ bindings: {
352
+ gg: 'move-first',
353
+ g: 'go', // Fallback if only single g
354
+ },
355
+ onAction,
356
+ sequenceTimeout: 500,
357
+ })
358
+
359
+ manager.handleKey('g')
360
+ // Simulate timeout
361
+ await new Promise((r) => setTimeout(r, 600))
362
+
363
+ expect(onAction).toHaveBeenCalledWith('go', { key: 'g' })
364
+ })
365
+
366
+ it('cancels pending sequence on escape', async () => {
367
+ const { createKeyboardManager } = await import('@mdxui/terminal')
368
+
369
+ const manager = createKeyboardManager({
370
+ bindings: {
371
+ gg: 'move-first',
372
+ },
373
+ })
374
+
375
+ manager.handleKey('g')
376
+ expect(manager.getPendingSequence()).toBe('g')
377
+
378
+ manager.handleKey('escape')
379
+ expect(manager.getPendingSequence()).toBe('')
380
+ })
381
+ })
382
+
383
+ describe('destroy method', () => {
384
+ it('provides destroy function', async () => {
385
+ const { createKeyboardManager } = await import('@mdxui/terminal')
386
+
387
+ const manager = createKeyboardManager({
388
+ bindings: { j: 'move-down' },
389
+ })
390
+
391
+ expect(typeof manager.destroy).toBe('function')
392
+ })
393
+
394
+ it('clears pending sequence on destroy', async () => {
395
+ const { createKeyboardManager } = await import('@mdxui/terminal')
396
+
397
+ const manager = createKeyboardManager({
398
+ bindings: {
399
+ gg: 'move-first',
400
+ },
401
+ })
402
+
403
+ manager.handleKey('g')
404
+ expect(manager.getPendingSequence()).toBe('g')
405
+
406
+ manager.destroy()
407
+ expect(manager.getPendingSequence()).toBe('')
408
+ })
409
+
410
+ it('clears sequence timer on destroy to prevent memory leak', async () => {
411
+ const { createKeyboardManager } = await import('@mdxui/terminal')
412
+ const onAction = vi.fn()
413
+
414
+ const manager = createKeyboardManager({
415
+ bindings: {
416
+ gg: 'move-first',
417
+ g: 'go',
418
+ },
419
+ onAction,
420
+ sequenceTimeout: 100,
421
+ })
422
+
423
+ // Start a sequence which creates a timer
424
+ // When 'g' is pressed and 'gg' exists, it waits for potential sequence completion
425
+ manager.handleKey('g')
426
+
427
+ // Immediately destroy before timeout fires
428
+ manager.destroy()
429
+
430
+ // Now verify destruction worked by checking onAction was not called yet
431
+ // (if the timer fired, it would have called onAction with 'go')
432
+ const callCountBeforeWait = onAction.mock.calls.length
433
+
434
+ // Wait for what would have been the timeout
435
+ await new Promise((r) => setTimeout(r, 200))
436
+
437
+ // The timeout handler should NOT have been called because destroy cleared it
438
+ // Without destroy, the timeout would have triggered 'go' action for 'g'
439
+ expect(onAction.mock.calls.length).toBe(callCountBeforeWait)
440
+ })
441
+
442
+ it('can be called multiple times safely', async () => {
443
+ const { createKeyboardManager } = await import('@mdxui/terminal')
444
+
445
+ const manager = createKeyboardManager({
446
+ bindings: { gg: 'move-first' },
447
+ })
448
+
449
+ manager.handleKey('g')
450
+
451
+ // Should not throw when called multiple times
452
+ expect(() => {
453
+ manager.destroy()
454
+ manager.destroy()
455
+ manager.destroy()
456
+ }).not.toThrow()
457
+ })
458
+ })
459
+ })
460
+
461
+ // ============================================================================
462
+ // useKeyboard Hook Tests
463
+ // ============================================================================
464
+
465
+ describe('useKeyboard hook', () => {
466
+ beforeEach(() => {
467
+ vi.clearAllMocks()
468
+ })
469
+
470
+ it('registers keyboard handler', async () => {
471
+ const { useKeyboard } = await import('@mdxui/terminal')
472
+ const handler = vi.fn()
473
+
474
+ const result = useKeyboard(handler)
475
+
476
+ expect(result).toBeDefined()
477
+ expect(result.enabled).toBe(true)
478
+ })
479
+
480
+ it('provides enable/disable controls', async () => {
481
+ const { useKeyboard } = await import('@mdxui/terminal')
482
+ const handler = vi.fn()
483
+
484
+ const result = useKeyboard(handler)
485
+
486
+ expect(typeof result.enable).toBe('function')
487
+ expect(typeof result.disable).toBe('function')
488
+ })
489
+
490
+ it('respects enabled option', async () => {
491
+ const { useKeyboard } = await import('@mdxui/terminal')
492
+ const handler = vi.fn()
493
+
494
+ const result = useKeyboard(handler, { enabled: false })
495
+
496
+ expect(result.enabled).toBe(false)
497
+ })
498
+
499
+ it('provides key information in handler', async () => {
500
+ const { useKeyboard } = await import('@mdxui/terminal')
501
+
502
+ // The handler should receive key and modifier info
503
+ type KeyHandler = (
504
+ key: string,
505
+ modifiers: { ctrl: boolean; alt: boolean; shift: boolean; meta: boolean }
506
+ ) => void
507
+
508
+ const handler: KeyHandler = vi.fn()
509
+ useKeyboard(handler)
510
+
511
+ // Handler signature is tested; actual calls depend on terminal events
512
+ expect(handler).toBeDefined()
513
+ })
514
+
515
+ it('supports priority for layered handlers', async () => {
516
+ const { useKeyboard } = await import('@mdxui/terminal')
517
+ const handler = vi.fn()
518
+
519
+ // Higher priority handlers should receive events first
520
+ const result = useKeyboard(handler, { priority: 10 })
521
+
522
+ expect(result.priority).toBe(10)
523
+ })
524
+
525
+ it('cleans up on unmount', async () => {
526
+ const { useKeyboard } = await import('@mdxui/terminal')
527
+ const handler = vi.fn()
528
+
529
+ const result = useKeyboard(handler)
530
+
531
+ // Should have cleanup function
532
+ expect(typeof result.cleanup).toBe('function')
533
+ })
534
+ })
535
+
536
+ // ============================================================================
537
+ // useFocus Hook Tests
538
+ // ============================================================================
539
+
540
+ describe('useFocus hook', () => {
541
+ beforeEach(() => {
542
+ vi.clearAllMocks()
543
+ })
544
+
545
+ describe('basic focus state', () => {
546
+ it('returns focused state', async () => {
547
+ const { useFocus } = await import('@mdxui/terminal')
548
+
549
+ const result = useFocus()
550
+
551
+ expect(typeof result.focused).toBe('boolean')
552
+ })
553
+
554
+ it('provides focus function', async () => {
555
+ const { useFocus } = await import('@mdxui/terminal')
556
+
557
+ const result = useFocus()
558
+
559
+ expect(typeof result.focus).toBe('function')
560
+ })
561
+
562
+ it('provides blur function', async () => {
563
+ const { useFocus } = await import('@mdxui/terminal')
564
+
565
+ const result = useFocus()
566
+
567
+ expect(typeof result.blur).toBe('function')
568
+ })
569
+ })
570
+
571
+ describe('focus navigation', () => {
572
+ it('provides focusNext function', async () => {
573
+ const { useFocus } = await import('@mdxui/terminal')
574
+
575
+ const result = useFocus()
576
+
577
+ expect(typeof result.focusNext).toBe('function')
578
+ })
579
+
580
+ it('provides focusPrev function', async () => {
581
+ const { useFocus } = await import('@mdxui/terminal')
582
+
583
+ const result = useFocus()
584
+
585
+ expect(typeof result.focusPrev).toBe('function')
586
+ })
587
+
588
+ it('provides focusFirst function', async () => {
589
+ const { useFocus } = await import('@mdxui/terminal')
590
+
591
+ const result = useFocus()
592
+
593
+ expect(typeof result.focusFirst).toBe('function')
594
+ })
595
+
596
+ it('provides focusLast function', async () => {
597
+ const { useFocus } = await import('@mdxui/terminal')
598
+
599
+ const result = useFocus()
600
+
601
+ expect(typeof result.focusLast).toBe('function')
602
+ })
603
+ })
604
+
605
+ describe('focus identification', () => {
606
+ it('accepts id parameter', async () => {
607
+ const { useFocus } = await import('@mdxui/terminal')
608
+
609
+ const result = useFocus({ id: 'my-element' })
610
+
611
+ expect(result.id).toBe('my-element')
612
+ })
613
+
614
+ it('generates id if not provided', async () => {
615
+ const { useFocus } = await import('@mdxui/terminal')
616
+
617
+ const result = useFocus()
618
+
619
+ expect(typeof result.id).toBe('string')
620
+ expect(result.id.length).toBeGreaterThan(0)
621
+ })
622
+ })
623
+
624
+ describe('auto focus', () => {
625
+ it('supports autoFocus option', async () => {
626
+ const { useFocus } = await import('@mdxui/terminal')
627
+
628
+ const result = useFocus({ autoFocus: true })
629
+
630
+ // With autoFocus, component should request focus on mount
631
+ expect(result.focused).toBe(true)
632
+ })
633
+ })
634
+
635
+ describe('tab index', () => {
636
+ it('returns tabIndex for ordering', async () => {
637
+ const { useFocus } = await import('@mdxui/terminal')
638
+
639
+ const result = useFocus({ tabIndex: 5 })
640
+
641
+ expect(result.tabIndex).toBe(5)
642
+ })
643
+
644
+ it('defaults tabIndex to 0', async () => {
645
+ const { useFocus } = await import('@mdxui/terminal')
646
+
647
+ const result = useFocus()
648
+
649
+ expect(result.tabIndex).toBe(0)
650
+ })
651
+
652
+ it('negative tabIndex removes from tab order', async () => {
653
+ const { useFocus } = await import('@mdxui/terminal')
654
+
655
+ const result = useFocus({ tabIndex: -1 })
656
+
657
+ expect(result.tabIndex).toBe(-1)
658
+ })
659
+ })
660
+
661
+ describe('focus callbacks', () => {
662
+ it('calls onFocus when focused', async () => {
663
+ const { useFocus } = await import('@mdxui/terminal')
664
+ const onFocus = vi.fn()
665
+
666
+ useFocus({ onFocus })
667
+
668
+ // onFocus should be registered
669
+ expect(onFocus).toBeDefined()
670
+ })
671
+
672
+ it('calls onBlur when blurred', async () => {
673
+ const { useFocus } = await import('@mdxui/terminal')
674
+ const onBlur = vi.fn()
675
+
676
+ useFocus({ onBlur })
677
+
678
+ // onBlur should be registered
679
+ expect(onBlur).toBeDefined()
680
+ })
681
+ })
682
+ })
683
+
684
+ // ============================================================================
685
+ // FocusProvider Tests
686
+ // ============================================================================
687
+
688
+ describe('FocusProvider', () => {
689
+ beforeEach(() => {
690
+ vi.clearAllMocks()
691
+ })
692
+
693
+ describe('basic rendering', () => {
694
+ it('renders children', async () => {
695
+ const { FocusProvider } = await import('@mdxui/terminal')
696
+
697
+ const element = FocusProvider({ children: 'Content' })
698
+
699
+ expect(element).toBeDefined()
700
+ })
701
+ })
702
+
703
+ describe('focus trapping', () => {
704
+ it('supports trap prop for modal focus', async () => {
705
+ const { FocusProvider } = await import('@mdxui/terminal')
706
+
707
+ const element = FocusProvider({ trap: true, children: 'Modal content' })
708
+
709
+ // FocusProvider wraps in Context.Provider, check inner div's data-focus-trap
710
+ const divElement = element.props?.children
711
+ expect(divElement?.props?.['data-focus-trap']).toBe(true)
712
+ })
713
+
714
+ it('wraps Tab focus to first element when at end', async () => {
715
+ const { FocusProvider } = await import('@mdxui/terminal')
716
+
717
+ const element = FocusProvider({
718
+ trap: true,
719
+ wrapFocus: true,
720
+ children: 'Content',
721
+ })
722
+
723
+ // wrapFocus is passed to div props and used internally for navigation
724
+ const divElement = element.props?.children
725
+ expect(divElement?.props?.wrapFocus).toBe(true)
726
+ })
727
+
728
+ it('wraps Shift+Tab focus to last element when at start', async () => {
729
+ const { FocusProvider } = await import('@mdxui/terminal')
730
+
731
+ // Same as above - wrapFocus handles both directions
732
+ const element = FocusProvider({
733
+ trap: true,
734
+ wrapFocus: true,
735
+ children: 'Content',
736
+ })
737
+
738
+ const divElement = element.props?.children
739
+ expect(divElement?.props?.wrapFocus).toBe(true)
740
+ })
741
+ })
742
+
743
+ describe('initial focus', () => {
744
+ it('supports initialFocus id', async () => {
745
+ const { FocusProvider } = await import('@mdxui/terminal')
746
+
747
+ const element = FocusProvider({
748
+ initialFocus: 'my-input',
749
+ children: 'Content',
750
+ })
751
+
752
+ // initialFocus is passed through to div props
753
+ const divElement = element.props?.children
754
+ expect(divElement?.props?.initialFocus).toBe('my-input')
755
+ })
756
+
757
+ it('focuses first focusable by default', async () => {
758
+ const { FocusProvider } = await import('@mdxui/terminal')
759
+
760
+ const element = FocusProvider({
761
+ focusFirstOnMount: true,
762
+ children: 'Content',
763
+ })
764
+
765
+ // focusFirstOnMount is passed through to div props
766
+ const divElement = element.props?.children
767
+ expect(divElement?.props?.focusFirstOnMount).toBe(true)
768
+ })
769
+ })
770
+
771
+ describe('focus restoration', () => {
772
+ it('restores focus on unmount', async () => {
773
+ const { FocusProvider } = await import('@mdxui/terminal')
774
+
775
+ const element = FocusProvider({
776
+ restoreFocus: true,
777
+ children: 'Content',
778
+ })
779
+
780
+ // restoreFocus is passed through to div props
781
+ const divElement = element.props?.children
782
+ expect(divElement?.props?.restoreFocus).toBe(true)
783
+ })
784
+ })
785
+
786
+ describe('focus group', () => {
787
+ it('supports group name for scoped focus', async () => {
788
+ const { FocusProvider } = await import('@mdxui/terminal')
789
+
790
+ const element = FocusProvider({
791
+ group: 'sidebar',
792
+ children: 'Content',
793
+ })
794
+
795
+ // group is exposed via data-focus-group attribute
796
+ const divElement = element.props?.children
797
+ expect(divElement?.props?.['data-focus-group']).toBe('sidebar')
798
+ })
799
+ })
800
+ })
801
+
802
+ // ============================================================================
803
+ // useFocusManager Hook Tests
804
+ // ============================================================================
805
+
806
+ describe('useFocusManager hook', () => {
807
+ it('provides access to focus context', async () => {
808
+ const { useFocusManager } = await import('@mdxui/terminal')
809
+
810
+ const result = useFocusManager()
811
+
812
+ expect(result).toBeDefined()
813
+ })
814
+
815
+ it('tracks all focusable elements', async () => {
816
+ const { useFocusManager } = await import('@mdxui/terminal')
817
+
818
+ const result = useFocusManager()
819
+
820
+ expect(Array.isArray(result.focusableIds)).toBe(true)
821
+ })
822
+
823
+ it('tracks currently focused element', async () => {
824
+ const { useFocusManager } = await import('@mdxui/terminal')
825
+
826
+ const result = useFocusManager()
827
+
828
+ // Could be null if nothing focused, or string id
829
+ expect(result.focusedId === null || typeof result.focusedId === 'string').toBe(true)
830
+ })
831
+
832
+ it('provides focusById function', async () => {
833
+ const { useFocusManager } = await import('@mdxui/terminal')
834
+
835
+ const result = useFocusManager()
836
+
837
+ expect(typeof result.focusById).toBe('function')
838
+ })
839
+
840
+ it('provides focus navigation functions', async () => {
841
+ const { useFocusManager } = await import('@mdxui/terminal')
842
+
843
+ const result = useFocusManager()
844
+
845
+ expect(typeof result.focusNext).toBe('function')
846
+ expect(typeof result.focusPrev).toBe('function')
847
+ expect(typeof result.focusFirst).toBe('function')
848
+ expect(typeof result.focusLast).toBe('function')
849
+ })
850
+
851
+ it('provides trap state', async () => {
852
+ const { useFocusManager } = await import('@mdxui/terminal')
853
+
854
+ const result = useFocusManager()
855
+
856
+ expect(typeof result.isTrapped).toBe('boolean')
857
+ })
858
+ })
859
+
860
+ // ============================================================================
861
+ // Vim-style Navigation Integration Tests
862
+ // ============================================================================
863
+
864
+ describe('vim-style navigation', () => {
865
+ describe('j/k vertical movement', () => {
866
+ it('j moves selection down', async () => {
867
+ const { createKeyboardManager } = await import('@mdxui/terminal')
868
+ const onAction = vi.fn()
869
+
870
+ const manager = createKeyboardManager({
871
+ bindings: { j: 'move-down', k: 'move-up' },
872
+ onAction,
873
+ })
874
+
875
+ manager.handleKey('j')
876
+ expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
877
+ })
878
+
879
+ it('k moves selection up', async () => {
880
+ const { createKeyboardManager } = await import('@mdxui/terminal')
881
+ const onAction = vi.fn()
882
+
883
+ const manager = createKeyboardManager({
884
+ bindings: { j: 'move-down', k: 'move-up' },
885
+ onAction,
886
+ })
887
+
888
+ manager.handleKey('k')
889
+ expect(onAction).toHaveBeenCalledWith('move-up', expect.any(Object))
890
+ })
891
+
892
+ it('J (shift+j) for faster movement', async () => {
893
+ const { createKeyboardManager } = await import('@mdxui/terminal')
894
+ const onAction = vi.fn()
895
+
896
+ const manager = createKeyboardManager({
897
+ bindings: { J: 'move-down-page', K: 'move-up-page' },
898
+ onAction,
899
+ })
900
+
901
+ manager.handleKey('J')
902
+ expect(onAction).toHaveBeenCalledWith('move-down-page', expect.any(Object))
903
+ })
904
+ })
905
+
906
+ describe('h/l horizontal movement', () => {
907
+ it('h moves left', async () => {
908
+ const { createKeyboardManager } = await import('@mdxui/terminal')
909
+ const onAction = vi.fn()
910
+
911
+ const manager = createKeyboardManager({
912
+ bindings: { h: 'move-left', l: 'move-right' },
913
+ onAction,
914
+ })
915
+
916
+ manager.handleKey('h')
917
+ expect(onAction).toHaveBeenCalledWith('move-left', expect.any(Object))
918
+ })
919
+
920
+ it('l moves right', async () => {
921
+ const { createKeyboardManager } = await import('@mdxui/terminal')
922
+ const onAction = vi.fn()
923
+
924
+ const manager = createKeyboardManager({
925
+ bindings: { h: 'move-left', l: 'move-right' },
926
+ onAction,
927
+ })
928
+
929
+ manager.handleKey('l')
930
+ expect(onAction).toHaveBeenCalledWith('move-right', expect.any(Object))
931
+ })
932
+ })
933
+
934
+ describe('arrow key alternatives', () => {
935
+ it('arrow keys work alongside vim keys', async () => {
936
+ const { createKeyboardManager, ARROW_BINDINGS } = await import('@mdxui/terminal')
937
+ const onAction = vi.fn()
938
+
939
+ const manager = createKeyboardManager({
940
+ bindings: ARROW_BINDINGS,
941
+ onAction,
942
+ })
943
+
944
+ manager.handleKey('up')
945
+ expect(onAction).toHaveBeenCalledWith('move-up', expect.any(Object))
946
+
947
+ manager.handleKey('down')
948
+ expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
949
+
950
+ manager.handleKey('left')
951
+ expect(onAction).toHaveBeenCalledWith('move-left', expect.any(Object))
952
+
953
+ manager.handleKey('right')
954
+ expect(onAction).toHaveBeenCalledWith('move-right', expect.any(Object))
955
+ })
956
+ })
957
+ })
958
+
959
+ // ============================================================================
960
+ // Selection & Activation Tests
961
+ // ============================================================================
962
+
963
+ describe('selection and activation', () => {
964
+ describe('enter key', () => {
965
+ it('triggers select action', async () => {
966
+ const { createKeyboardManager } = await import('@mdxui/terminal')
967
+ const onAction = vi.fn()
968
+
969
+ const manager = createKeyboardManager({
970
+ bindings: { enter: 'select', return: 'select' },
971
+ onAction,
972
+ })
973
+
974
+ manager.handleKey('enter')
975
+ expect(onAction).toHaveBeenCalledWith('select', expect.any(Object))
976
+ })
977
+ })
978
+
979
+ describe('space key', () => {
980
+ it('triggers toggle action', async () => {
981
+ const { createKeyboardManager } = await import('@mdxui/terminal')
982
+ const onAction = vi.fn()
983
+
984
+ const manager = createKeyboardManager({
985
+ bindings: { space: 'toggle', ' ': 'toggle' },
986
+ onAction,
987
+ })
988
+
989
+ manager.handleKey('space')
990
+ expect(onAction).toHaveBeenCalledWith('toggle', expect.any(Object))
991
+ })
992
+ })
993
+
994
+ describe('escape key', () => {
995
+ it('triggers back/cancel action', async () => {
996
+ const { createKeyboardManager } = await import('@mdxui/terminal')
997
+ const onAction = vi.fn()
998
+
999
+ const manager = createKeyboardManager({
1000
+ bindings: { escape: 'back' },
1001
+ onAction,
1002
+ })
1003
+
1004
+ manager.handleKey('escape')
1005
+ expect(onAction).toHaveBeenCalledWith('back', expect.any(Object))
1006
+ })
1007
+ })
1008
+ })
1009
+
1010
+ // ============================================================================
1011
+ // Tab Navigation Tests
1012
+ // ============================================================================
1013
+
1014
+ describe('tab navigation', () => {
1015
+ describe('Tab key', () => {
1016
+ it('Tab triggers focus-next action', async () => {
1017
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1018
+ const onAction = vi.fn()
1019
+
1020
+ const manager = createKeyboardManager({
1021
+ bindings: { tab: 'focus-next', 'shift+tab': 'focus-prev' },
1022
+ onAction,
1023
+ })
1024
+
1025
+ manager.handleKey('tab')
1026
+ expect(onAction).toHaveBeenCalledWith('focus-next', expect.any(Object))
1027
+ })
1028
+ })
1029
+
1030
+ describe('Shift+Tab', () => {
1031
+ it('Shift+Tab triggers focus-prev action', async () => {
1032
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1033
+ const onAction = vi.fn()
1034
+
1035
+ const manager = createKeyboardManager({
1036
+ bindings: { tab: 'focus-next', 'shift+tab': 'focus-prev' },
1037
+ onAction,
1038
+ })
1039
+
1040
+ manager.handleKey('shift+tab')
1041
+ expect(onAction).toHaveBeenCalledWith('focus-prev', expect.any(Object))
1042
+ })
1043
+ })
1044
+ })
1045
+
1046
+ // ============================================================================
1047
+ // Custom Bindings Tests
1048
+ // ============================================================================
1049
+
1050
+ describe('custom bindings', () => {
1051
+ it('allows custom action names', async () => {
1052
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1053
+ const onAction = vi.fn()
1054
+
1055
+ const manager = createKeyboardManager({
1056
+ bindings: {
1057
+ d: 'delete-item',
1058
+ r: 'rename-item',
1059
+ n: 'new-item',
1060
+ },
1061
+ onAction,
1062
+ })
1063
+
1064
+ manager.handleKey('d')
1065
+ expect(onAction).toHaveBeenCalledWith('delete-item', expect.any(Object))
1066
+ })
1067
+
1068
+ it('supports function as action', async () => {
1069
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1070
+ const customAction = vi.fn()
1071
+
1072
+ const manager = createKeyboardManager({
1073
+ bindings: {
1074
+ x: customAction,
1075
+ },
1076
+ })
1077
+
1078
+ manager.handleKey('x')
1079
+ expect(customAction).toHaveBeenCalled()
1080
+ })
1081
+
1082
+ it('supports context-aware bindings', async () => {
1083
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1084
+ const onAction = vi.fn()
1085
+
1086
+ const manager = createKeyboardManager({
1087
+ bindings: { enter: 'select' },
1088
+ onAction,
1089
+ context: { mode: 'list' },
1090
+ })
1091
+
1092
+ manager.handleKey('enter')
1093
+ expect(onAction).toHaveBeenCalledWith('select', expect.objectContaining({
1094
+ context: { mode: 'list' },
1095
+ }))
1096
+ })
1097
+
1098
+ it('allows updating context', async () => {
1099
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1100
+ const onAction = vi.fn()
1101
+
1102
+ const manager = createKeyboardManager({
1103
+ bindings: { enter: 'select' },
1104
+ onAction,
1105
+ context: { mode: 'list' },
1106
+ })
1107
+
1108
+ manager.setContext({ mode: 'edit' })
1109
+ manager.handleKey('enter')
1110
+
1111
+ expect(onAction).toHaveBeenCalledWith('select', expect.objectContaining({
1112
+ context: { mode: 'edit' },
1113
+ }))
1114
+ })
1115
+ })
1116
+
1117
+ // ============================================================================
1118
+ // Preset Bindings Tests
1119
+ // ============================================================================
1120
+
1121
+ describe('preset bindings', () => {
1122
+ it('exports VIM_BINDINGS preset', async () => {
1123
+ const { VIM_BINDINGS } = await import('@mdxui/terminal')
1124
+
1125
+ expect(VIM_BINDINGS).toBeDefined()
1126
+ expect(VIM_BINDINGS.j).toBe('move-down')
1127
+ expect(VIM_BINDINGS.k).toBe('move-up')
1128
+ expect(VIM_BINDINGS.h).toBe('move-left')
1129
+ expect(VIM_BINDINGS.l).toBe('move-right')
1130
+ })
1131
+
1132
+ it('exports ARROW_BINDINGS preset', async () => {
1133
+ const { ARROW_BINDINGS } = await import('@mdxui/terminal')
1134
+
1135
+ expect(ARROW_BINDINGS).toBeDefined()
1136
+ expect(ARROW_BINDINGS.up).toBe('move-up')
1137
+ expect(ARROW_BINDINGS.down).toBe('move-down')
1138
+ expect(ARROW_BINDINGS.left).toBe('move-left')
1139
+ expect(ARROW_BINDINGS.right).toBe('move-right')
1140
+ })
1141
+
1142
+ it('exports COMMON_BINDINGS preset', async () => {
1143
+ const { COMMON_BINDINGS } = await import('@mdxui/terminal')
1144
+
1145
+ expect(COMMON_BINDINGS).toBeDefined()
1146
+ expect(COMMON_BINDINGS.enter).toBe('select')
1147
+ expect(COMMON_BINDINGS.escape).toBe('back')
1148
+ expect(COMMON_BINDINGS.tab).toBe('focus-next')
1149
+ expect(COMMON_BINDINGS['shift+tab']).toBe('focus-prev')
1150
+ })
1151
+
1152
+ it('allows merging presets', async () => {
1153
+ const { createKeyboardManager, VIM_BINDINGS, COMMON_BINDINGS } = await import('@mdxui/terminal')
1154
+
1155
+ const manager = createKeyboardManager({
1156
+ bindings: { ...VIM_BINDINGS, ...COMMON_BINDINGS },
1157
+ })
1158
+
1159
+ // Should have both vim and common bindings
1160
+ expect(manager.getAction('j')).toBe('move-down')
1161
+ expect(manager.getAction('enter')).toBe('select')
1162
+ })
1163
+ })
1164
+
1165
+ // ============================================================================
1166
+ // Focus Manager Context Tests
1167
+ // ============================================================================
1168
+
1169
+ describe('FocusManagerContext', () => {
1170
+ it('exports FocusContext', async () => {
1171
+ const { FocusContext } = await import('@mdxui/terminal')
1172
+
1173
+ expect(FocusContext).toBeDefined()
1174
+ })
1175
+
1176
+ it('provides default value when not in provider', async () => {
1177
+ const { useFocusManager } = await import('@mdxui/terminal')
1178
+
1179
+ const result = useFocusManager()
1180
+
1181
+ // Should have default no-op implementations
1182
+ expect(result.focusableIds).toEqual([])
1183
+ expect(result.focusedId).toBeNull()
1184
+ })
1185
+ })
1186
+
1187
+ // ============================================================================
1188
+ // useNavigableList Hook Tests
1189
+ // ============================================================================
1190
+
1191
+ describe('useNavigableList hook', () => {
1192
+ it('provides current index', async () => {
1193
+ const { useNavigableList } = await import('@mdxui/terminal')
1194
+
1195
+ const result = useNavigableList({
1196
+ items: ['a', 'b', 'c'],
1197
+ })
1198
+
1199
+ expect(typeof result.currentIndex).toBe('number')
1200
+ expect(result.currentIndex).toBe(0) // Default to first item
1201
+ })
1202
+
1203
+ it('provides navigation functions', async () => {
1204
+ const { useNavigableList } = await import('@mdxui/terminal')
1205
+
1206
+ const result = useNavigableList({
1207
+ items: ['a', 'b', 'c'],
1208
+ })
1209
+
1210
+ expect(typeof result.moveUp).toBe('function')
1211
+ expect(typeof result.moveDown).toBe('function')
1212
+ expect(typeof result.moveToFirst).toBe('function')
1213
+ expect(typeof result.moveToLast).toBe('function')
1214
+ })
1215
+
1216
+ it('provides current item', async () => {
1217
+ const { useNavigableList } = await import('@mdxui/terminal')
1218
+
1219
+ const result = useNavigableList({
1220
+ items: ['a', 'b', 'c'],
1221
+ initialIndex: 1,
1222
+ })
1223
+
1224
+ expect(result.currentItem).toBe('b')
1225
+ })
1226
+
1227
+ it('supports wrap option', async () => {
1228
+ const { useNavigableList } = await import('@mdxui/terminal')
1229
+
1230
+ const result = useNavigableList({
1231
+ items: ['a', 'b', 'c'],
1232
+ wrap: true,
1233
+ })
1234
+
1235
+ expect(result.wrap).toBe(true)
1236
+ })
1237
+
1238
+ it('supports keyboard bindings', async () => {
1239
+ const { useNavigableList } = await import('@mdxui/terminal')
1240
+
1241
+ const result = useNavigableList({
1242
+ items: ['a', 'b', 'c'],
1243
+ useKeyboard: true,
1244
+ })
1245
+
1246
+ expect(result.keyboardEnabled).toBe(true)
1247
+ })
1248
+
1249
+ it('provides setIndex function', async () => {
1250
+ const { useNavigableList } = await import('@mdxui/terminal')
1251
+
1252
+ const result = useNavigableList({
1253
+ items: ['a', 'b', 'c'],
1254
+ })
1255
+
1256
+ expect(typeof result.setIndex).toBe('function')
1257
+ })
1258
+ })
1259
+
1260
+ // ============================================================================
1261
+ // useNavigableGrid Hook Tests
1262
+ // ============================================================================
1263
+
1264
+ describe('useNavigableGrid hook', () => {
1265
+ it('provides current position', async () => {
1266
+ const { useNavigableGrid } = await import('@mdxui/terminal')
1267
+
1268
+ const result = useNavigableGrid({
1269
+ rows: 3,
1270
+ cols: 4,
1271
+ })
1272
+
1273
+ expect(typeof result.row).toBe('number')
1274
+ expect(typeof result.col).toBe('number')
1275
+ expect(result.row).toBe(0)
1276
+ expect(result.col).toBe(0)
1277
+ })
1278
+
1279
+ it('provides navigation functions for all directions', async () => {
1280
+ const { useNavigableGrid } = await import('@mdxui/terminal')
1281
+
1282
+ const result = useNavigableGrid({
1283
+ rows: 3,
1284
+ cols: 4,
1285
+ })
1286
+
1287
+ expect(typeof result.moveUp).toBe('function')
1288
+ expect(typeof result.moveDown).toBe('function')
1289
+ expect(typeof result.moveLeft).toBe('function')
1290
+ expect(typeof result.moveRight).toBe('function')
1291
+ })
1292
+
1293
+ it('provides moveToCell function', async () => {
1294
+ const { useNavigableGrid } = await import('@mdxui/terminal')
1295
+
1296
+ const result = useNavigableGrid({
1297
+ rows: 3,
1298
+ cols: 4,
1299
+ })
1300
+
1301
+ expect(typeof result.moveToCell).toBe('function')
1302
+ })
1303
+
1304
+ it('supports wrap in both dimensions', async () => {
1305
+ const { useNavigableGrid } = await import('@mdxui/terminal')
1306
+
1307
+ const result = useNavigableGrid({
1308
+ rows: 3,
1309
+ cols: 4,
1310
+ wrapHorizontal: true,
1311
+ wrapVertical: true,
1312
+ })
1313
+
1314
+ expect(result.wrapHorizontal).toBe(true)
1315
+ expect(result.wrapVertical).toBe(true)
1316
+ })
1317
+
1318
+ it('supports keyboard bindings', async () => {
1319
+ const { useNavigableGrid } = await import('@mdxui/terminal')
1320
+
1321
+ const result = useNavigableGrid({
1322
+ rows: 3,
1323
+ cols: 4,
1324
+ useKeyboard: true,
1325
+ })
1326
+
1327
+ expect(result.keyboardEnabled).toBe(true)
1328
+ })
1329
+ })
1330
+
1331
+ // ============================================================================
1332
+ // Type Exports Tests
1333
+ // ============================================================================
1334
+
1335
+ describe('type exports', () => {
1336
+ it('exports KeyBinding type', async () => {
1337
+ // This test verifies types are exported - checking via usage
1338
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1339
+
1340
+ // TypeScript would fail compilation if KeyBinding isn't exported correctly
1341
+ const manager = createKeyboardManager({
1342
+ bindings: { j: 'move-down' },
1343
+ })
1344
+
1345
+ expect(manager).toBeDefined()
1346
+ })
1347
+
1348
+ it('exports KeyboardAction type', async () => {
1349
+ const { createKeyboardManager } = await import('@mdxui/terminal')
1350
+
1351
+ // KeyboardAction should support string and function
1352
+ const manager = createKeyboardManager({
1353
+ bindings: {
1354
+ a: 'action-name',
1355
+ b: () => {},
1356
+ },
1357
+ })
1358
+
1359
+ expect(manager).toBeDefined()
1360
+ })
1361
+
1362
+ it('exports KeyModifiers type', async () => {
1363
+ const { useKeyboard } = await import('@mdxui/terminal')
1364
+
1365
+ // KeyModifiers is used in handler parameter
1366
+ useKeyboard((_key, modifiers) => {
1367
+ // TypeScript checks modifiers has ctrl, alt, shift, meta
1368
+ const _: boolean = modifiers.ctrl
1369
+ const __: boolean = modifiers.alt
1370
+ const ___: boolean = modifiers.shift
1371
+ })
1372
+ })
1373
+
1374
+ it('exports FocusableElement type', async () => {
1375
+ const { useFocus } = await import('@mdxui/terminal')
1376
+
1377
+ // FocusableElement is the return type of useFocus
1378
+ const element = useFocus({ id: 'test' })
1379
+
1380
+ expect(element.id).toBeDefined()
1381
+ expect(typeof element.focused).toBe('boolean')
1382
+ })
1383
+ })
1384
+
1385
+ // ============================================================================
1386
+ // normalizeReadlineKey Tests
1387
+ // ============================================================================
1388
+
1389
+ describe('normalizeReadlineKey', () => {
1390
+ it('normalizes single character input', async () => {
1391
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1392
+
1393
+ const result = normalizeReadlineKey('a', undefined)
1394
+
1395
+ expect(result.name).toBe('a')
1396
+ expect(result.ctrl).toBe(false)
1397
+ expect(result.alt).toBe(false)
1398
+ expect(result.shift).toBe(false)
1399
+ expect(result.meta).toBe(false)
1400
+ })
1401
+
1402
+ it('detects shift for uppercase letters', async () => {
1403
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1404
+
1405
+ const result = normalizeReadlineKey('A', undefined)
1406
+
1407
+ expect(result.name).toBe('A')
1408
+ expect(result.shift).toBe(true)
1409
+ })
1410
+
1411
+ it('normalizes return to enter', async () => {
1412
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1413
+
1414
+ const result = normalizeReadlineKey('\r', { name: 'return' })
1415
+
1416
+ expect(result.name).toBe('enter')
1417
+ })
1418
+
1419
+ it('preserves ctrl modifier from readline key', async () => {
1420
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1421
+
1422
+ const result = normalizeReadlineKey('\x03', { name: 'c', ctrl: true })
1423
+
1424
+ expect(result.name).toBe('c')
1425
+ expect(result.ctrl).toBe(true)
1426
+ })
1427
+
1428
+ it('preserves shift modifier from readline key', async () => {
1429
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1430
+
1431
+ const result = normalizeReadlineKey('\t', { name: 'tab', shift: true })
1432
+
1433
+ expect(result.name).toBe('tab')
1434
+ expect(result.shift).toBe(true)
1435
+ })
1436
+
1437
+ it('normalizes arrow keys', async () => {
1438
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1439
+
1440
+ expect(normalizeReadlineKey('\x1b[A', { name: 'up' }).name).toBe('up')
1441
+ expect(normalizeReadlineKey('\x1b[B', { name: 'down' }).name).toBe('down')
1442
+ expect(normalizeReadlineKey('\x1b[C', { name: 'right' }).name).toBe('right')
1443
+ expect(normalizeReadlineKey('\x1b[D', { name: 'left' }).name).toBe('left')
1444
+ })
1445
+
1446
+ it('normalizes escape key', async () => {
1447
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1448
+
1449
+ const result = normalizeReadlineKey('\x1b', { name: 'escape' })
1450
+
1451
+ expect(result.name).toBe('escape')
1452
+ })
1453
+
1454
+ it('handles unknown keys by lowercasing', async () => {
1455
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1456
+
1457
+ const result = normalizeReadlineKey('x', { name: 'CUSTOMKEY' })
1458
+
1459
+ expect(result.name).toBe('customkey')
1460
+ })
1461
+ })
1462
+
1463
+ // ============================================================================
1464
+ // keyToBindingString Tests
1465
+ // ============================================================================
1466
+
1467
+ describe('keyToBindingString', () => {
1468
+ it('returns plain key name for no modifiers', async () => {
1469
+ const { keyToBindingString } = await import('@mdxui/terminal')
1470
+
1471
+ const result = keyToBindingString({
1472
+ name: 'a',
1473
+ ctrl: false,
1474
+ alt: false,
1475
+ shift: false,
1476
+ meta: false,
1477
+ })
1478
+
1479
+ expect(result).toBe('a')
1480
+ })
1481
+
1482
+ it('adds ctrl modifier', async () => {
1483
+ const { keyToBindingString } = await import('@mdxui/terminal')
1484
+
1485
+ const result = keyToBindingString({
1486
+ name: 'c',
1487
+ ctrl: true,
1488
+ alt: false,
1489
+ shift: false,
1490
+ meta: false,
1491
+ })
1492
+
1493
+ expect(result).toBe('ctrl+c')
1494
+ })
1495
+
1496
+ it('adds shift modifier', async () => {
1497
+ const { keyToBindingString } = await import('@mdxui/terminal')
1498
+
1499
+ const result = keyToBindingString({
1500
+ name: 'tab',
1501
+ ctrl: false,
1502
+ alt: false,
1503
+ shift: true,
1504
+ meta: false,
1505
+ })
1506
+
1507
+ expect(result).toBe('shift+tab')
1508
+ })
1509
+
1510
+ it('combines multiple modifiers in order', async () => {
1511
+ const { keyToBindingString } = await import('@mdxui/terminal')
1512
+
1513
+ const result = keyToBindingString({
1514
+ name: 's',
1515
+ ctrl: true,
1516
+ alt: false,
1517
+ shift: true,
1518
+ meta: false,
1519
+ })
1520
+
1521
+ expect(result).toBe('ctrl+shift+s')
1522
+ })
1523
+
1524
+ it('includes all four modifiers when present', async () => {
1525
+ const { keyToBindingString } = await import('@mdxui/terminal')
1526
+
1527
+ const result = keyToBindingString({
1528
+ name: 'a',
1529
+ ctrl: true,
1530
+ alt: true,
1531
+ shift: true,
1532
+ meta: true,
1533
+ })
1534
+
1535
+ expect(result).toBe('ctrl+alt+shift+meta+a')
1536
+ })
1537
+ })
1538
+
1539
+ // ============================================================================
1540
+ // normalizeOpenTUIKey Tests
1541
+ // ============================================================================
1542
+
1543
+ describe('normalizeOpenTUIKey', () => {
1544
+ it('normalizes basic key', async () => {
1545
+ const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
1546
+
1547
+ const result = normalizeOpenTUIKey({
1548
+ name: 'a',
1549
+ ctrl: false,
1550
+ meta: false,
1551
+ shift: false,
1552
+ option: false,
1553
+ sequence: 'a',
1554
+ })
1555
+
1556
+ expect(result.name).toBe('a')
1557
+ expect(result.ctrl).toBe(false)
1558
+ expect(result.alt).toBe(false)
1559
+ expect(result.shift).toBe(false)
1560
+ expect(result.meta).toBe(false)
1561
+ })
1562
+
1563
+ it('maps option to alt', async () => {
1564
+ const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
1565
+
1566
+ const result = normalizeOpenTUIKey({
1567
+ name: 'h',
1568
+ ctrl: false,
1569
+ meta: false,
1570
+ shift: false,
1571
+ option: true,
1572
+ sequence: 'h',
1573
+ })
1574
+
1575
+ expect(result.alt).toBe(true)
1576
+ })
1577
+
1578
+ it('normalizes return to enter', async () => {
1579
+ const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
1580
+
1581
+ const result = normalizeOpenTUIKey({
1582
+ name: 'return',
1583
+ ctrl: false,
1584
+ meta: false,
1585
+ shift: false,
1586
+ option: false,
1587
+ sequence: '\r',
1588
+ })
1589
+
1590
+ expect(result.name).toBe('enter')
1591
+ })
1592
+
1593
+ it('preserves all modifier keys', async () => {
1594
+ const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
1595
+
1596
+ const result = normalizeOpenTUIKey({
1597
+ name: 's',
1598
+ ctrl: true,
1599
+ meta: true,
1600
+ shift: true,
1601
+ option: true,
1602
+ sequence: 's',
1603
+ })
1604
+
1605
+ expect(result.ctrl).toBe(true)
1606
+ expect(result.meta).toBe(true)
1607
+ expect(result.shift).toBe(true)
1608
+ expect(result.alt).toBe(true)
1609
+ })
1610
+ })
1611
+
1612
+ // ============================================================================
1613
+ // createOpenTUIKeyHandler Tests
1614
+ // ============================================================================
1615
+
1616
+ describe('createOpenTUIKeyHandler', () => {
1617
+ it('creates a handler function', async () => {
1618
+ const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
1619
+
1620
+ const manager = createKeyboardManager({ bindings: {} })
1621
+ const handler = createOpenTUIKeyHandler(manager)
1622
+
1623
+ expect(typeof handler).toBe('function')
1624
+ })
1625
+
1626
+ it('passes key events to manager', async () => {
1627
+ const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
1628
+ const onAction = vi.fn()
1629
+
1630
+ const manager = createKeyboardManager({
1631
+ bindings: { j: 'move-down' },
1632
+ onAction,
1633
+ })
1634
+ const handler = createOpenTUIKeyHandler(manager)
1635
+
1636
+ handler({
1637
+ name: 'j',
1638
+ ctrl: false,
1639
+ meta: false,
1640
+ shift: false,
1641
+ option: false,
1642
+ sequence: 'j',
1643
+ })
1644
+
1645
+ expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
1646
+ })
1647
+
1648
+ it('skips release events', async () => {
1649
+ const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
1650
+ const onAction = vi.fn()
1651
+
1652
+ const manager = createKeyboardManager({
1653
+ bindings: { j: 'move-down' },
1654
+ onAction,
1655
+ })
1656
+ const handler = createOpenTUIKeyHandler(manager)
1657
+
1658
+ handler({
1659
+ name: 'j',
1660
+ ctrl: false,
1661
+ meta: false,
1662
+ shift: false,
1663
+ option: false,
1664
+ sequence: 'j',
1665
+ eventType: 'release',
1666
+ })
1667
+
1668
+ expect(onAction).not.toHaveBeenCalled()
1669
+ })
1670
+
1671
+ it('handles modifier keys', async () => {
1672
+ const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
1673
+ const onAction = vi.fn()
1674
+
1675
+ const manager = createKeyboardManager({
1676
+ bindings: { 'ctrl+c': 'cancel' },
1677
+ onAction,
1678
+ })
1679
+ const handler = createOpenTUIKeyHandler(manager)
1680
+
1681
+ handler({
1682
+ name: 'c',
1683
+ ctrl: true,
1684
+ meta: false,
1685
+ shift: false,
1686
+ option: false,
1687
+ sequence: '\x03',
1688
+ })
1689
+
1690
+ expect(onAction).toHaveBeenCalledWith('cancel', expect.any(Object))
1691
+ })
1692
+ })
1693
+
1694
+ // ============================================================================
1695
+ // attachKeyboardManager Tests
1696
+ // ============================================================================
1697
+
1698
+ describe('attachKeyboardManager', () => {
1699
+ it('exports attachKeyboardManager function', async () => {
1700
+ const { attachKeyboardManager } = await import('@mdxui/terminal')
1701
+
1702
+ expect(typeof attachKeyboardManager).toBe('function')
1703
+ })
1704
+
1705
+ it('throws error for non-TTY stdin', async () => {
1706
+ const { createKeyboardManager, attachKeyboardManager } = await import('@mdxui/terminal')
1707
+
1708
+ const manager = createKeyboardManager({ bindings: {} })
1709
+
1710
+ // Create a mock non-TTY stream
1711
+ const mockStream = {
1712
+ isTTY: false,
1713
+ } as NodeJS.ReadStream
1714
+
1715
+ expect(() => attachKeyboardManager(manager, { input: mockStream })).toThrow('requires a TTY stdin')
1716
+ })
1717
+ })
1718
+
1719
+ // ============================================================================
1720
+ // Type Exports for Terminal Input
1721
+ // ============================================================================
1722
+
1723
+ describe('terminal input type exports', () => {
1724
+ it('exports NormalizedKey type', async () => {
1725
+ // Type check - if this compiles, the type is exported
1726
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1727
+
1728
+ const key = normalizeReadlineKey('a', undefined)
1729
+
1730
+ // Verify structure matches NormalizedKey
1731
+ expect('name' in key).toBe(true)
1732
+ expect('ctrl' in key).toBe(true)
1733
+ expect('alt' in key).toBe(true)
1734
+ expect('shift' in key).toBe(true)
1735
+ expect('meta' in key).toBe(true)
1736
+ })
1737
+
1738
+ it('exports ReadlineKey type', async () => {
1739
+ const { normalizeReadlineKey } = await import('@mdxui/terminal')
1740
+
1741
+ // ReadlineKey is accepted as parameter type
1742
+ const key: { name: string; ctrl: boolean } = { name: 'a', ctrl: false }
1743
+ const result = normalizeReadlineKey('a', key)
1744
+
1745
+ expect(result).toBeDefined()
1746
+ })
1747
+
1748
+ it('exports OpenTUIKeyEvent type', async () => {
1749
+ const { createOpenTUIKeyHandler, createKeyboardManager } = await import('@mdxui/terminal')
1750
+
1751
+ const manager = createKeyboardManager({ bindings: {} })
1752
+ const handler = createOpenTUIKeyHandler(manager)
1753
+
1754
+ // OpenTUIKeyEvent structure is accepted
1755
+ handler({
1756
+ name: 'a',
1757
+ ctrl: false,
1758
+ meta: false,
1759
+ shift: false,
1760
+ option: false,
1761
+ sequence: 'a',
1762
+ })
1763
+ })
1764
+
1765
+ it('exports AttachKeyboardOptions type', async () => {
1766
+ // Type check - if this compiles with the options, the type works
1767
+ const { attachKeyboardManager, createKeyboardManager } = await import('@mdxui/terminal')
1768
+
1769
+ const manager = createKeyboardManager({ bindings: {} })
1770
+
1771
+ // This should type-check even though we can't run it without a TTY
1772
+ const options = { exitOnCtrlC: false }
1773
+
1774
+ // Just verify the options shape is correct
1775
+ expect(options.exitOnCtrlC).toBe(false)
1776
+ })
1777
+
1778
+ it('exports DetachKeyboard type', async () => {
1779
+ const { createOpenTUIKeyHandler, createKeyboardManager } = await import('@mdxui/terminal')
1780
+
1781
+ // DetachKeyboard is a function type
1782
+ // We can't actually test attachKeyboardManager without a TTY,
1783
+ // but we can verify the return type concept works
1784
+ const manager = createKeyboardManager({ bindings: {} })
1785
+
1786
+ // A DetachKeyboard function is just () => void
1787
+ const detach: () => void = () => {}
1788
+
1789
+ expect(typeof detach).toBe('function')
1790
+ })
1791
+ })