@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,501 @@
1
+ /**
2
+ * @mdxui/terminal Keyboard Simulator
3
+ *
4
+ * Provides utilities for simulating keyboard events in Storybook stories
5
+ * for testing interactive terminal components.
6
+ *
7
+ * @packageDocumentation
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import { createKeyboardSimulator, KeySequence } from '@mdxui/terminal/storybook'
12
+ *
13
+ * const simulator = createKeyboardSimulator({ renderer })
14
+ *
15
+ * // Single key press
16
+ * simulator.press('enter')
17
+ *
18
+ * // Key with modifiers
19
+ * simulator.press('s', { ctrl: true })
20
+ *
21
+ * // Predefined sequences
22
+ * simulator.sequence(KeySequence.NAVIGATE_DOWN_3)
23
+ * ```
24
+ */
25
+
26
+ import type { InteractiveRenderer } from '../renderers/interactive'
27
+
28
+ // ============================================================================
29
+ // Types
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Key modifiers for keyboard events
34
+ */
35
+ export interface KeyModifiers {
36
+ /** Ctrl key pressed */
37
+ ctrl?: boolean
38
+ /** Alt/Option key pressed */
39
+ alt?: boolean
40
+ /** Shift key pressed */
41
+ shift?: boolean
42
+ /** Meta/Command key pressed */
43
+ meta?: boolean
44
+ }
45
+
46
+ /**
47
+ * Single key press event
48
+ */
49
+ export interface KeyPress {
50
+ /** Key name (e.g., 'enter', 'tab', 'a', 'escape') */
51
+ key: string
52
+ /** Key modifiers */
53
+ modifiers?: KeyModifiers
54
+ /** Delay in ms before this key press (default: 0) */
55
+ delay?: number
56
+ }
57
+
58
+ /**
59
+ * Configuration for the keyboard simulator
60
+ */
61
+ export interface KeyboardSimulatorConfig {
62
+ /** Interactive renderer instance */
63
+ renderer: InteractiveRenderer
64
+ /** Default delay between key presses in ms (default: 0) */
65
+ defaultDelay?: number
66
+ /** Callback when key is pressed */
67
+ onKeyPress?: (key: string, modifiers: KeyModifiers) => void
68
+ /** Callback when sequence completes */
69
+ onSequenceComplete?: (sequence: KeyPress[]) => void
70
+ }
71
+
72
+ /**
73
+ * Keyboard simulator instance
74
+ */
75
+ export interface KeyboardSimulator {
76
+ /** Press a single key */
77
+ press(key: string, modifiers?: KeyModifiers): void
78
+ /** Press a key and return a promise that resolves after the default delay */
79
+ pressAsync(key: string, modifiers?: KeyModifiers): Promise<void>
80
+ /** Execute a sequence of key presses */
81
+ sequence(keys: KeyPress[]): void
82
+ /** Execute a sequence of key presses with delays */
83
+ sequenceAsync(keys: KeyPress[]): Promise<void>
84
+ /** Type a string character by character */
85
+ type(text: string): void
86
+ /** Type a string character by character with delays */
87
+ typeAsync(text: string, charDelay?: number): Promise<void>
88
+ /** Reset simulator state */
89
+ reset(): void
90
+ /** Get list of pressed keys (for assertions) */
91
+ getPressedKeys(): string[]
92
+ /** Clear pressed keys history */
93
+ clearHistory(): void
94
+ }
95
+
96
+ // ============================================================================
97
+ // Predefined Key Sequences
98
+ // ============================================================================
99
+
100
+ /**
101
+ * Common key sequences for testing navigation patterns
102
+ */
103
+ export const KeySequence = {
104
+ /** Navigate down one item */
105
+ NAVIGATE_DOWN: [{ key: 'down' }] as KeyPress[],
106
+
107
+ /** Navigate down three items */
108
+ NAVIGATE_DOWN_3: [
109
+ { key: 'down' },
110
+ { key: 'down' },
111
+ { key: 'down' },
112
+ ] as KeyPress[],
113
+
114
+ /** Navigate up one item */
115
+ NAVIGATE_UP: [{ key: 'up' }] as KeyPress[],
116
+
117
+ /** Navigate up three items */
118
+ NAVIGATE_UP_3: [
119
+ { key: 'up' },
120
+ { key: 'up' },
121
+ { key: 'up' },
122
+ ] as KeyPress[],
123
+
124
+ /** Tab forward through focusables */
125
+ TAB_FORWARD: [{ key: 'tab' }] as KeyPress[],
126
+
127
+ /** Tab forward three times */
128
+ TAB_FORWARD_3: [
129
+ { key: 'tab' },
130
+ { key: 'tab' },
131
+ { key: 'tab' },
132
+ ] as KeyPress[],
133
+
134
+ /** Tab backward through focusables */
135
+ TAB_BACKWARD: [{ key: 'tab', modifiers: { shift: true } }] as KeyPress[],
136
+
137
+ /** Tab backward three times */
138
+ TAB_BACKWARD_3: [
139
+ { key: 'tab', modifiers: { shift: true } },
140
+ { key: 'tab', modifiers: { shift: true } },
141
+ { key: 'tab', modifiers: { shift: true } },
142
+ ] as KeyPress[],
143
+
144
+ /** Select/activate focused element */
145
+ SELECT: [{ key: 'enter' }] as KeyPress[],
146
+
147
+ /** Toggle focused element (e.g., checkbox) */
148
+ TOGGLE: [{ key: 'space' }] as KeyPress[],
149
+
150
+ /** Cancel/escape current action */
151
+ CANCEL: [{ key: 'escape' }] as KeyPress[],
152
+
153
+ /** Navigate to first item (vim: gg) */
154
+ GOTO_FIRST: [{ key: 'g' }, { key: 'g' }] as KeyPress[],
155
+
156
+ /** Navigate to last item (vim: G) */
157
+ GOTO_LAST: [{ key: 'G', modifiers: { shift: true } }] as KeyPress[],
158
+
159
+ /** Open search mode (vim: /) */
160
+ SEARCH: [{ key: '/' }] as KeyPress[],
161
+
162
+ /** Navigate down and select */
163
+ NAVIGATE_DOWN_SELECT: [{ key: 'down' }, { key: 'enter' }] as KeyPress[],
164
+
165
+ /** Navigate to next item using j (vim) */
166
+ VIM_DOWN: [{ key: 'j' }] as KeyPress[],
167
+
168
+ /** Navigate to previous item using k (vim) */
169
+ VIM_UP: [{ key: 'k' }] as KeyPress[],
170
+
171
+ /** Navigate left using h (vim) */
172
+ VIM_LEFT: [{ key: 'h' }] as KeyPress[],
173
+
174
+ /** Navigate right using l (vim) */
175
+ VIM_RIGHT: [{ key: 'l' }] as KeyPress[],
176
+
177
+ /** Delete sequence (vim: dd) */
178
+ VIM_DELETE: [{ key: 'd' }, { key: 'd' }] as KeyPress[],
179
+
180
+ /** Home key - jump to beginning */
181
+ HOME: [{ key: 'home' }] as KeyPress[],
182
+
183
+ /** End key - jump to end */
184
+ END: [{ key: 'end' }] as KeyPress[],
185
+
186
+ /** Page down */
187
+ PAGE_DOWN: [{ key: 'pagedown' }] as KeyPress[],
188
+
189
+ /** Page up */
190
+ PAGE_UP: [{ key: 'pageup' }] as KeyPress[],
191
+
192
+ /** Ctrl+C - cancel/interrupt */
193
+ INTERRUPT: [{ key: 'c', modifiers: { ctrl: true } }] as KeyPress[],
194
+
195
+ /** Ctrl+S - save */
196
+ SAVE: [{ key: 's', modifiers: { ctrl: true } }] as KeyPress[],
197
+ } as const
198
+
199
+ // ============================================================================
200
+ // Implementation
201
+ // ============================================================================
202
+
203
+ /**
204
+ * Convert key and modifiers to binding string format
205
+ */
206
+ function toBindingString(key: string, modifiers?: KeyModifiers): string {
207
+ const parts: string[] = []
208
+
209
+ if (modifiers?.ctrl) parts.push('ctrl')
210
+ if (modifiers?.alt) parts.push('alt')
211
+ if (modifiers?.shift) parts.push('shift')
212
+ if (modifiers?.meta) parts.push('meta')
213
+
214
+ parts.push(key.toLowerCase())
215
+
216
+ return parts.join('+')
217
+ }
218
+
219
+ /**
220
+ * Sleep utility for async operations
221
+ */
222
+ function sleep(ms: number): Promise<void> {
223
+ return new Promise((resolve) => setTimeout(resolve, ms))
224
+ }
225
+
226
+ /**
227
+ * Creates a keyboard simulator for testing interactive terminal components.
228
+ *
229
+ * The simulator wraps the interactive renderer's `emitKey` method and provides
230
+ * higher-level abstractions for common keyboard testing patterns.
231
+ *
232
+ * @param config - Simulator configuration
233
+ * @returns Keyboard simulator instance
234
+ *
235
+ * @example
236
+ * ```tsx
237
+ * const renderer = await createInteractiveRenderer({ vimBindings: true })
238
+ * const simulator = createKeyboardSimulator({ renderer })
239
+ *
240
+ * // Register some focusable elements
241
+ * renderer.registerFocusable('item-1', { tabIndex: 0 })
242
+ * renderer.registerFocusable('item-2', { tabIndex: 0 })
243
+ * renderer.registerFocusable('item-3', { tabIndex: 0 })
244
+ *
245
+ * // Focus first item
246
+ * renderer.focusById('item-1')
247
+ *
248
+ * // Navigate down using simulator
249
+ * simulator.press('j') // vim down
250
+ * expect(renderer.getFocusedId()).toBe('item-2')
251
+ *
252
+ * // Or use predefined sequences
253
+ * simulator.sequence(KeySequence.NAVIGATE_DOWN)
254
+ * expect(renderer.getFocusedId()).toBe('item-3')
255
+ * ```
256
+ */
257
+ export function createKeyboardSimulator(
258
+ config: KeyboardSimulatorConfig
259
+ ): KeyboardSimulator {
260
+ const { renderer, defaultDelay = 0, onKeyPress, onSequenceComplete } = config
261
+
262
+ // Track pressed keys for assertions
263
+ const pressedKeys: string[] = []
264
+
265
+ const simulator: KeyboardSimulator = {
266
+ press(key: string, modifiers?: KeyModifiers): void {
267
+ const bindingString = toBindingString(key, modifiers)
268
+ pressedKeys.push(bindingString)
269
+ onKeyPress?.(key, modifiers ?? {})
270
+ renderer.emitKey(bindingString)
271
+ },
272
+
273
+ async pressAsync(key: string, modifiers?: KeyModifiers): Promise<void> {
274
+ this.press(key, modifiers)
275
+ if (defaultDelay > 0) {
276
+ await sleep(defaultDelay)
277
+ }
278
+ },
279
+
280
+ sequence(keys: KeyPress[]): void {
281
+ for (const keyPress of keys) {
282
+ this.press(keyPress.key, keyPress.modifiers)
283
+ }
284
+ onSequenceComplete?.(keys)
285
+ },
286
+
287
+ async sequenceAsync(keys: KeyPress[]): Promise<void> {
288
+ for (const keyPress of keys) {
289
+ const delay = keyPress.delay ?? defaultDelay
290
+ if (delay > 0) {
291
+ await sleep(delay)
292
+ }
293
+ this.press(keyPress.key, keyPress.modifiers)
294
+ }
295
+ onSequenceComplete?.(keys)
296
+ },
297
+
298
+ type(text: string): void {
299
+ for (const char of text) {
300
+ // Handle special characters
301
+ if (char === '\n') {
302
+ this.press('enter')
303
+ } else if (char === '\t') {
304
+ this.press('tab')
305
+ } else if (char === ' ') {
306
+ this.press('space')
307
+ } else {
308
+ // Check if uppercase (implies shift)
309
+ const isUpperCase = char === char.toUpperCase() && char !== char.toLowerCase()
310
+ this.press(char, isUpperCase ? { shift: true } : undefined)
311
+ }
312
+ }
313
+ },
314
+
315
+ async typeAsync(text: string, charDelay = 50): Promise<void> {
316
+ for (const char of text) {
317
+ if (charDelay > 0) {
318
+ await sleep(charDelay)
319
+ }
320
+ if (char === '\n') {
321
+ this.press('enter')
322
+ } else if (char === '\t') {
323
+ this.press('tab')
324
+ } else if (char === ' ') {
325
+ this.press('space')
326
+ } else {
327
+ const isUpperCase = char === char.toUpperCase() && char !== char.toLowerCase()
328
+ this.press(char, isUpperCase ? { shift: true } : undefined)
329
+ }
330
+ }
331
+ },
332
+
333
+ reset(): void {
334
+ this.clearHistory()
335
+ },
336
+
337
+ getPressedKeys(): string[] {
338
+ return [...pressedKeys]
339
+ },
340
+
341
+ clearHistory(): void {
342
+ pressedKeys.length = 0
343
+ },
344
+ }
345
+
346
+ return simulator
347
+ }
348
+
349
+ // ============================================================================
350
+ // Assertion Helpers
351
+ // ============================================================================
352
+
353
+ /**
354
+ * Assert that a specific key was pressed
355
+ */
356
+ export function expectKeyPressed(
357
+ simulator: KeyboardSimulator,
358
+ key: string,
359
+ modifiers?: KeyModifiers
360
+ ): void {
361
+ const bindingString = toBindingString(key, modifiers)
362
+ const pressedKeys = simulator.getPressedKeys()
363
+
364
+ if (!pressedKeys.includes(bindingString)) {
365
+ throw new Error(
366
+ `Expected key "${bindingString}" to be pressed, but it was not. Pressed keys: [${pressedKeys.join(', ')}]`
367
+ )
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Assert that a sequence of keys was pressed in order
373
+ */
374
+ export function expectSequencePressed(
375
+ simulator: KeyboardSimulator,
376
+ sequence: KeyPress[]
377
+ ): void {
378
+ const pressedKeys = simulator.getPressedKeys()
379
+ const expectedKeys = sequence.map((kp) => toBindingString(kp.key, kp.modifiers))
380
+
381
+ // Find the sequence in pressed keys
382
+ let found = false
383
+ for (let i = 0; i <= pressedKeys.length - expectedKeys.length; i++) {
384
+ let match = true
385
+ for (let j = 0; j < expectedKeys.length; j++) {
386
+ if (pressedKeys[i + j] !== expectedKeys[j]) {
387
+ match = false
388
+ break
389
+ }
390
+ }
391
+ if (match) {
392
+ found = true
393
+ break
394
+ }
395
+ }
396
+
397
+ if (!found) {
398
+ throw new Error(
399
+ `Expected sequence [${expectedKeys.join(', ')}] to be pressed in order, but it was not. Pressed keys: [${pressedKeys.join(', ')}]`
400
+ )
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Assert that no keys were pressed
406
+ */
407
+ export function expectNoKeysPressed(simulator: KeyboardSimulator): void {
408
+ const pressedKeys = simulator.getPressedKeys()
409
+
410
+ if (pressedKeys.length > 0) {
411
+ throw new Error(
412
+ `Expected no keys to be pressed, but found: [${pressedKeys.join(', ')}]`
413
+ )
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Assert total number of key presses
419
+ */
420
+ export function expectKeyPressCount(
421
+ simulator: KeyboardSimulator,
422
+ count: number
423
+ ): void {
424
+ const pressedKeys = simulator.getPressedKeys()
425
+
426
+ if (pressedKeys.length !== count) {
427
+ throw new Error(
428
+ `Expected ${count} key press(es), but found ${pressedKeys.length}. Pressed keys: [${pressedKeys.join(', ')}]`
429
+ )
430
+ }
431
+ }
432
+
433
+ // ============================================================================
434
+ // Browser/DOM Key Event Integration (for Storybook)
435
+ // ============================================================================
436
+
437
+ /**
438
+ * Map DOM KeyboardEvent to our key format
439
+ */
440
+ export function mapDOMKeyEvent(event: KeyboardEvent): KeyPress {
441
+ // Normalize key names
442
+ let key = event.key.toLowerCase()
443
+
444
+ // Map special keys
445
+ const keyMap: Record<string, string> = {
446
+ arrowup: 'up',
447
+ arrowdown: 'down',
448
+ arrowleft: 'left',
449
+ arrowright: 'right',
450
+ ' ': 'space',
451
+ return: 'enter',
452
+ }
453
+
454
+ key = keyMap[key] ?? key
455
+
456
+ return {
457
+ key,
458
+ modifiers: {
459
+ ctrl: event.ctrlKey,
460
+ alt: event.altKey,
461
+ shift: event.shiftKey,
462
+ meta: event.metaKey,
463
+ },
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Create a keyboard event handler for DOM events that forwards to simulator
469
+ */
470
+ export function createDOMKeyHandler(
471
+ simulator: KeyboardSimulator,
472
+ options?: {
473
+ /** Prevent default browser behavior */
474
+ preventDefault?: boolean
475
+ /** Stop event propagation */
476
+ stopPropagation?: boolean
477
+ /** Keys to ignore (don't forward to simulator) */
478
+ ignoreKeys?: string[]
479
+ }
480
+ ): (event: KeyboardEvent) => void {
481
+ const { preventDefault = true, stopPropagation = true, ignoreKeys = [] } = options ?? {}
482
+
483
+ return (event: KeyboardEvent) => {
484
+ const keyPress = mapDOMKeyEvent(event)
485
+
486
+ // Check if key should be ignored
487
+ if (ignoreKeys.includes(keyPress.key)) {
488
+ return
489
+ }
490
+
491
+ if (preventDefault) {
492
+ event.preventDefault()
493
+ }
494
+
495
+ if (stopPropagation) {
496
+ event.stopPropagation()
497
+ }
498
+
499
+ simulator.press(keyPress.key, keyPress.modifiers)
500
+ }
501
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * ANSI Escape Code Constants
3
+ *
4
+ * Standard ANSI escape sequences for terminal text styling.
5
+ * These codes work in all terminals that support ANSI escape codes.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ /**
11
+ * ANSI escape code constants for terminal styling.
12
+ *
13
+ * Includes:
14
+ * - Reset code to clear all styles
15
+ * - Text styling (bold, dim, italic, underline, strikethrough)
16
+ * - Standard 8 foreground colors (black through white)
17
+ * - Bright variants of all 8 foreground colors
18
+ * - Standard 8 background colors
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * // Apply bold cyan text
23
+ * console.log(`${ANSI.bold}${ANSI.cyan}Hello${ANSI.reset}`)
24
+ *
25
+ * // Use with styled helper
26
+ * console.log(styled('Error!', ANSI.bold, ANSI.red))
27
+ * ```
28
+ */
29
+ export const ANSI = {
30
+ // Reset
31
+ reset: '\x1b[0m',
32
+
33
+ // Styles
34
+ bold: '\x1b[1m',
35
+ dim: '\x1b[2m',
36
+ italic: '\x1b[3m',
37
+ underline: '\x1b[4m',
38
+ inverse: '\x1b[7m',
39
+ strikethrough: '\x1b[9m',
40
+
41
+ // Foreground colors (basic)
42
+ black: '\x1b[30m',
43
+ red: '\x1b[31m',
44
+ green: '\x1b[32m',
45
+ yellow: '\x1b[33m',
46
+ blue: '\x1b[34m',
47
+ magenta: '\x1b[35m',
48
+ cyan: '\x1b[36m',
49
+ white: '\x1b[37m',
50
+
51
+ // Bright foreground colors
52
+ brightBlack: '\x1b[90m',
53
+ brightRed: '\x1b[91m',
54
+ brightGreen: '\x1b[92m',
55
+ brightYellow: '\x1b[93m',
56
+ brightBlue: '\x1b[94m',
57
+ brightMagenta: '\x1b[95m',
58
+ brightCyan: '\x1b[96m',
59
+ brightWhite: '\x1b[97m',
60
+
61
+ // Background colors
62
+ bgBlack: '\x1b[40m',
63
+ bgRed: '\x1b[41m',
64
+ bgGreen: '\x1b[42m',
65
+ bgYellow: '\x1b[43m',
66
+ bgBlue: '\x1b[44m',
67
+ bgMagenta: '\x1b[45m',
68
+ bgCyan: '\x1b[46m',
69
+ bgWhite: '\x1b[47m',
70
+
71
+ // Bright background colors
72
+ bgBrightBlack: '\x1b[100m',
73
+ bgBrightRed: '\x1b[101m',
74
+ bgBrightGreen: '\x1b[102m',
75
+ bgBrightYellow: '\x1b[103m',
76
+ bgBrightBlue: '\x1b[104m',
77
+ bgBrightMagenta: '\x1b[105m',
78
+ bgBrightCyan: '\x1b[106m',
79
+ bgBrightWhite: '\x1b[107m',
80
+ } as const
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Box Drawing Characters
3
+ *
4
+ * Unicode box-drawing character sets for terminal UI borders
5
+ * and utility functions for drawing boxes.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ /**
11
+ * Unicode box-drawing character sets for terminal UI borders.
12
+ *
13
+ * Provides three styles:
14
+ * - `single` - Standard single-line borders (┌─┐│└┘)
15
+ * - `double` - Double-line borders (╔═╗║╚╝)
16
+ * - `rounded` - Single-line with rounded corners (╭─╮│╰╯)
17
+ *
18
+ * Each set includes:
19
+ * - Corner characters (topLeft, topRight, bottomLeft, bottomRight)
20
+ * - Line characters (horizontal, vertical)
21
+ * - T-junction characters (teeLeft, teeRight, teeTop, teeBottom)
22
+ * - Cross character (cross)
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * const chars = boxChars.single
27
+ * const top = chars.topLeft + chars.horizontal.repeat(10) + chars.topRight
28
+ * // Outputs: ┌──────────┐
29
+ * ```
30
+ */
31
+ export const boxChars = {
32
+ single: {
33
+ topLeft: '┌',
34
+ topRight: '┐',
35
+ bottomLeft: '└',
36
+ bottomRight: '┘',
37
+ horizontal: '─',
38
+ vertical: '│',
39
+ teeLeft: '├',
40
+ teeRight: '┤',
41
+ teeTop: '┬',
42
+ teeBottom: '┴',
43
+ cross: '┼',
44
+ },
45
+ double: {
46
+ topLeft: '╔',
47
+ topRight: '╗',
48
+ bottomLeft: '╚',
49
+ bottomRight: '╝',
50
+ horizontal: '═',
51
+ vertical: '║',
52
+ teeLeft: '╠',
53
+ teeRight: '╣',
54
+ teeTop: '╦',
55
+ teeBottom: '╩',
56
+ cross: '╬',
57
+ },
58
+ rounded: {
59
+ topLeft: '╭',
60
+ topRight: '╮',
61
+ bottomLeft: '╰',
62
+ bottomRight: '╯',
63
+ horizontal: '─',
64
+ vertical: '│',
65
+ teeLeft: '├',
66
+ teeRight: '┤',
67
+ teeTop: '┬',
68
+ teeBottom: '┴',
69
+ cross: '┼',
70
+ },
71
+ } as const
72
+
73
+ /**
74
+ * Available box drawing styles.
75
+ * - `'single'` - Standard single-line borders
76
+ * - `'double'` - Double-line borders
77
+ * - `'rounded'` - Single-line with rounded corners
78
+ */
79
+ export type BoxStyle = keyof typeof boxChars
80
+
81
+ /**
82
+ * Draws a box with specified style and dimensions.
83
+ *
84
+ * Creates an array of strings representing each line of the box.
85
+ * The box is hollow (empty inside) and can be used as a container
86
+ * for content in terminal UIs.
87
+ *
88
+ * @param width - Total width of the box including borders
89
+ * @param height - Total height of the box including borders
90
+ * @param style - Box style ('single', 'double', or 'rounded')
91
+ * @returns Array of strings, one per line of the box
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * const box = drawBox(20, 5, 'rounded')
96
+ * box.forEach(line => console.log(line))
97
+ * // ╭──────────────────╮
98
+ * // │ │
99
+ * // │ │
100
+ * // │ │
101
+ * // ╰──────────────────╯
102
+ * ```
103
+ */
104
+ export function drawBox(
105
+ width: number,
106
+ height: number,
107
+ style: BoxStyle = 'single'
108
+ ): string[] {
109
+ // Handle edge cases for dimensions
110
+ if (width < 2) width = 2
111
+ if (height < 2) height = 2
112
+
113
+ // Ensure integers
114
+ width = Math.floor(width)
115
+ height = Math.floor(height)
116
+
117
+ const chars = boxChars[style]
118
+ const lines: string[] = []
119
+
120
+ // Top border
121
+ lines.push(chars.topLeft + chars.horizontal.repeat(Math.max(0, width - 2)) + chars.topRight)
122
+
123
+ // Middle rows
124
+ for (let i = 0; i < height - 2; i++) {
125
+ lines.push(chars.vertical + ' '.repeat(Math.max(0, width - 2)) + chars.vertical)
126
+ }
127
+
128
+ // Bottom border
129
+ lines.push(chars.bottomLeft + chars.horizontal.repeat(Math.max(0, width - 2)) + chars.bottomRight)
130
+
131
+ return lines
132
+ }