@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,748 @@
1
+ /**
2
+ * Focus Management System
3
+ *
4
+ * React hooks and context for managing focus state across terminal UI components.
5
+ * Provides focus tracking, navigation, and keyboard integration.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import React, { useState, useCallback, useRef, useEffect, useContext, createContext, type ReactNode, type ReactElement } from 'react'
11
+ import { createFocusId, type FocusId } from '../types'
12
+ import type { KeyModifiers } from './manager'
13
+
14
+ // ============================================================================
15
+ // useKeyboard Hook
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Options for the useKeyboard hook.
20
+ */
21
+ export interface UseKeyboardOptions {
22
+ /** Whether keyboard handling is enabled (default: true) */
23
+ enabled?: boolean
24
+ /** Priority for keyboard event handling (higher wins) */
25
+ priority?: number
26
+ }
27
+
28
+ /**
29
+ * Return value from the useKeyboard hook.
30
+ */
31
+ export interface UseKeyboardResult {
32
+ /** Whether keyboard handling is currently enabled */
33
+ enabled: boolean
34
+ /** Current priority level */
35
+ priority: number
36
+ /** Enable keyboard handling */
37
+ enable: () => void
38
+ /** Disable keyboard handling */
39
+ disable: () => void
40
+ /** Cleanup function (called on unmount) */
41
+ cleanup: () => void
42
+ }
43
+
44
+ /**
45
+ * React hook for handling keyboard input with modifier key support.
46
+ *
47
+ * Provides a handler that receives key presses along with modifier state.
48
+ * Useful for building custom keyboard interaction patterns.
49
+ *
50
+ * @param handler - Callback receiving key name and modifier state
51
+ * @param options - Configuration options
52
+ * @returns Controls for enabling/disabling keyboard handling
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * function MyComponent() {
57
+ * const keyboard = useKeyboard((key, modifiers) => {
58
+ * if (modifiers.ctrl && key === 'c') {
59
+ * handleCopy()
60
+ * }
61
+ * })
62
+ *
63
+ * return <div>...</div>
64
+ * }
65
+ * ```
66
+ */
67
+ export function useKeyboard(
68
+ handler: (key: string, modifiers: KeyModifiers) => void,
69
+ options?: UseKeyboardOptions
70
+ ): UseKeyboardResult {
71
+ const [enabled, setEnabled] = useState(options?.enabled !== false)
72
+ const priority = options?.priority ?? 0
73
+ const handlerRef = useRef(handler)
74
+ handlerRef.current = handler
75
+
76
+ const cleanup = useCallback(() => {
77
+ // Cleanup function for when hook unmounts
78
+ }, [])
79
+
80
+ useEffect(() => {
81
+ // Setup keyboard event listeners when in a real environment
82
+ // This is a placeholder for the actual implementation
83
+ return cleanup
84
+ }, [cleanup])
85
+
86
+ return {
87
+ enabled,
88
+ priority,
89
+ enable: () => setEnabled(true),
90
+ disable: () => setEnabled(false),
91
+ cleanup,
92
+ }
93
+ }
94
+
95
+ // ============================================================================
96
+ // useFocus Hook
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Options for the useFocus hook.
101
+ */
102
+ export interface UseFocusOptions {
103
+ /** Custom ID for the focusable element */
104
+ id?: string
105
+ /** Whether to focus on mount (default: false) */
106
+ autoFocus?: boolean
107
+ /** Tab order index */
108
+ tabIndex?: number
109
+ /** Callback when element receives focus */
110
+ onFocus?: () => void
111
+ /** Callback when element loses focus */
112
+ onBlur?: () => void
113
+ }
114
+
115
+ /**
116
+ * Interface for a focusable element returned by useFocus.
117
+ */
118
+ export interface FocusableElement {
119
+ /** Unique ID for this focusable element */
120
+ id: FocusId
121
+ /** Whether this element currently has focus */
122
+ focused: boolean
123
+ /** Tab order index */
124
+ tabIndex: number
125
+ /** Programmatically focus this element */
126
+ focus: () => void
127
+ /** Programmatically blur this element */
128
+ blur: () => void
129
+ /** Move focus to the next focusable element */
130
+ focusNext: () => void
131
+ /** Move focus to the previous focusable element */
132
+ focusPrev: () => void
133
+ /** Move focus to the first focusable element */
134
+ focusFirst: () => void
135
+ /** Move focus to the last focusable element */
136
+ focusLast: () => void
137
+ }
138
+
139
+ /**
140
+ * React hook for focus management in terminal UIs.
141
+ *
142
+ * Provides focus state and navigation helpers for building
143
+ * accessible keyboard-navigable interfaces.
144
+ *
145
+ * @param options - Configuration options
146
+ * @returns Focus state and control methods
147
+ *
148
+ * @example
149
+ * ```tsx
150
+ * function MenuItem({ label }: { label: string }) {
151
+ * const { focused, focus, blur } = useFocus({
152
+ * onFocus: () => console.log('focused'),
153
+ * onBlur: () => console.log('blurred')
154
+ * })
155
+ *
156
+ * return (
157
+ * <Text color={focused ? 'primary' : undefined}>
158
+ * {focused ? '> ' : ' '}{label}
159
+ * </Text>
160
+ * )
161
+ * }
162
+ * ```
163
+ */
164
+ export function useFocus(options?: UseFocusOptions): FocusableElement {
165
+ const idRef = useRef(createFocusId(options?.id))
166
+ const [localFocused, setLocalFocused] = useState(options?.autoFocus ?? false)
167
+ const tabIndex = options?.tabIndex ?? 0
168
+ const context = useContext(FocusContext)
169
+ const id = idRef.current
170
+
171
+ // Register with FocusProvider on mount, unregister on unmount
172
+ useEffect(() => {
173
+ if (context?.registerFocusable) {
174
+ context.registerFocusable(id, tabIndex)
175
+ return () => context.unregisterFocusable?.(id)
176
+ }
177
+ }, [id, tabIndex, context])
178
+
179
+ // Determine focused state: prefer context if available, else use local state
180
+ const focused = context ? context.focusedId === id : localFocused
181
+
182
+ const focus = useCallback(() => {
183
+ if (context) {
184
+ context.focusById(id)
185
+ } else {
186
+ setLocalFocused(true)
187
+ }
188
+ options?.onFocus?.()
189
+ }, [context, id, options])
190
+
191
+ const blur = useCallback(() => {
192
+ // In context mode, we don't have a direct "blur" - focus moves to another element
193
+ // In local mode, we set local focused to false
194
+ if (!context) {
195
+ setLocalFocused(false)
196
+ }
197
+ options?.onBlur?.()
198
+ }, [context, options])
199
+
200
+ const focusNext = useCallback(() => {
201
+ context?.focusNext()
202
+ }, [context])
203
+
204
+ const focusPrev = useCallback(() => {
205
+ context?.focusPrev()
206
+ }, [context])
207
+
208
+ const focusFirst = useCallback(() => {
209
+ context?.focusFirst()
210
+ }, [context])
211
+
212
+ const focusLast = useCallback(() => {
213
+ context?.focusLast()
214
+ }, [context])
215
+
216
+ return {
217
+ id,
218
+ focused,
219
+ tabIndex,
220
+ focus,
221
+ blur,
222
+ focusNext,
223
+ focusPrev,
224
+ focusFirst,
225
+ focusLast,
226
+ }
227
+ }
228
+
229
+ // ============================================================================
230
+ // useFocusManager Hook
231
+ // ============================================================================
232
+
233
+ /**
234
+ * State and controls from the focus manager context.
235
+ */
236
+ export interface FocusManagerState {
237
+ /** IDs of all registered focusable elements */
238
+ focusableIds: FocusId[]
239
+ /** ID of the currently focused element, or null */
240
+ focusedId: FocusId | null
241
+ /** Whether focus is trapped within a container */
242
+ isTrapped: boolean
243
+ /** Focus a specific element by ID */
244
+ focusById: (id: FocusId) => void
245
+ /** Move focus to the next element */
246
+ focusNext: () => void
247
+ /** Move focus to the previous element */
248
+ focusPrev: () => void
249
+ /** Move focus to the first element */
250
+ focusFirst: () => void
251
+ /** Move focus to the last element */
252
+ focusLast: () => void
253
+ /** Register a focusable element with the provider */
254
+ registerFocusable: (id: FocusId, tabIndex: number) => void
255
+ /** Unregister a focusable element from the provider */
256
+ unregisterFocusable: (id: FocusId) => void
257
+ }
258
+
259
+ /**
260
+ * React context for focus manager state.
261
+ */
262
+ export const FocusContext = createContext<FocusManagerState | null>(null)
263
+
264
+ /**
265
+ * React hook to access the focus manager context.
266
+ *
267
+ * Must be used within a FocusProvider. Returns a default
268
+ * no-op implementation if used outside a provider.
269
+ *
270
+ * @returns Focus manager state and controls
271
+ *
272
+ * @example
273
+ * ```tsx
274
+ * function NavigationHelp() {
275
+ * const { focusFirst, focusLast } = useFocusManager()
276
+ *
277
+ * return (
278
+ * <Box>
279
+ * <Button onPress={focusFirst}>Go to start</Button>
280
+ * <Button onPress={focusLast}>Go to end</Button>
281
+ * </Box>
282
+ * )
283
+ * }
284
+ * ```
285
+ */
286
+ export function useFocusManager(): FocusManagerState {
287
+ const context = useContext(FocusContext)
288
+
289
+ // Return context if available, otherwise return default no-op implementation
290
+ if (context) {
291
+ return context
292
+ }
293
+
294
+ // Default implementation when not inside a FocusProvider
295
+ return {
296
+ focusableIds: [],
297
+ focusedId: null,
298
+ isTrapped: false,
299
+ focusById: () => {},
300
+ focusNext: () => {},
301
+ focusPrev: () => {},
302
+ focusFirst: () => {},
303
+ focusLast: () => {},
304
+ registerFocusable: () => {},
305
+ unregisterFocusable: () => {},
306
+ }
307
+ }
308
+
309
+ // ============================================================================
310
+ // useNavigableList Hook
311
+ // ============================================================================
312
+
313
+ /**
314
+ * Options for the useNavigableList hook.
315
+ */
316
+ export interface UseNavigableListOptions<T> {
317
+ /** Array of items to navigate */
318
+ items: T[]
319
+ /** Initial selected index (default: 0) */
320
+ initialIndex?: number
321
+ /** Wrap around at list boundaries (default: false) */
322
+ wrap?: boolean
323
+ /** Enable keyboard navigation (default: false) */
324
+ useKeyboard?: boolean
325
+ /** Callback when an item is selected */
326
+ onSelect?: (item: T, index: number) => void
327
+ }
328
+
329
+ /**
330
+ * Return value from the useNavigableList hook.
331
+ */
332
+ export interface UseNavigableListResult<T> {
333
+ /** Current selected index */
334
+ currentIndex: number
335
+ /** Current selected item */
336
+ currentItem: T | undefined
337
+ /** Whether wrapping is enabled */
338
+ wrap: boolean
339
+ /** Whether keyboard navigation is enabled */
340
+ keyboardEnabled: boolean
341
+ /** Move selection up (decrease index) */
342
+ moveUp: () => void
343
+ /** Move selection down (increase index) */
344
+ moveDown: () => void
345
+ /** Move selection to the first item */
346
+ moveToFirst: () => void
347
+ /** Move selection to the last item */
348
+ moveToLast: () => void
349
+ /** Set the selection to a specific index */
350
+ setIndex: (index: number) => void
351
+ }
352
+
353
+ /**
354
+ * React hook for keyboard-navigable lists.
355
+ *
356
+ * Manages selection state and provides navigation methods
357
+ * for building interactive list interfaces.
358
+ *
359
+ * @param options - Configuration options
360
+ * @returns Selection state and navigation controls
361
+ *
362
+ * @example
363
+ * ```tsx
364
+ * function Menu() {
365
+ * const items = ['File', 'Edit', 'View', 'Help']
366
+ * const { currentIndex, moveUp, moveDown } = useNavigableList({
367
+ * items,
368
+ * wrap: true,
369
+ * onSelect: (item) => console.log('Selected:', item)
370
+ * })
371
+ *
372
+ * return (
373
+ * <List items={items} selectedIndex={currentIndex} />
374
+ * )
375
+ * }
376
+ * ```
377
+ */
378
+ export function useNavigableList<T>(options: UseNavigableListOptions<T>): UseNavigableListResult<T> {
379
+ const { items, initialIndex = 0, wrap = false, useKeyboard: keyboardEnabled = false } = options
380
+ const [currentIndex, setCurrentIndex] = useState(initialIndex)
381
+
382
+ const moveUp = useCallback(() => {
383
+ setCurrentIndex((prev) => {
384
+ if (prev <= 0) {
385
+ return wrap ? items.length - 1 : 0
386
+ }
387
+ return prev - 1
388
+ })
389
+ }, [items.length, wrap])
390
+
391
+ const moveDown = useCallback(() => {
392
+ setCurrentIndex((prev) => {
393
+ if (prev >= items.length - 1) {
394
+ return wrap ? 0 : items.length - 1
395
+ }
396
+ return prev + 1
397
+ })
398
+ }, [items.length, wrap])
399
+
400
+ const moveToFirst = useCallback(() => {
401
+ setCurrentIndex(0)
402
+ }, [])
403
+
404
+ const moveToLast = useCallback(() => {
405
+ setCurrentIndex(items.length - 1)
406
+ }, [items.length])
407
+
408
+ const setIndex = useCallback(
409
+ (index: number) => {
410
+ if (index >= 0 && index < items.length) {
411
+ setCurrentIndex(index)
412
+ }
413
+ },
414
+ [items.length]
415
+ )
416
+
417
+ return {
418
+ currentIndex,
419
+ currentItem: items[currentIndex],
420
+ wrap,
421
+ keyboardEnabled,
422
+ moveUp,
423
+ moveDown,
424
+ moveToFirst,
425
+ moveToLast,
426
+ setIndex,
427
+ }
428
+ }
429
+
430
+ // ============================================================================
431
+ // useNavigableGrid Hook
432
+ // ============================================================================
433
+
434
+ /**
435
+ * Options for the useNavigableGrid hook.
436
+ */
437
+ export interface UseNavigableGridOptions {
438
+ /** Number of rows in the grid */
439
+ rows: number
440
+ /** Number of columns in the grid */
441
+ cols: number
442
+ /** Initial row (default: 0) */
443
+ initialRow?: number
444
+ /** Initial column (default: 0) */
445
+ initialCol?: number
446
+ /** Wrap horizontally at column boundaries (default: false) */
447
+ wrapHorizontal?: boolean
448
+ /** Wrap vertically at row boundaries (default: false) */
449
+ wrapVertical?: boolean
450
+ /** Enable keyboard navigation (default: false) */
451
+ useKeyboard?: boolean
452
+ }
453
+
454
+ /**
455
+ * Return value from the useNavigableGrid hook.
456
+ */
457
+ export interface UseNavigableGridResult {
458
+ /** Current row position */
459
+ row: number
460
+ /** Current column position */
461
+ col: number
462
+ /** Whether horizontal wrapping is enabled */
463
+ wrapHorizontal: boolean
464
+ /** Whether vertical wrapping is enabled */
465
+ wrapVertical: boolean
466
+ /** Whether keyboard navigation is enabled */
467
+ keyboardEnabled: boolean
468
+ /** Move up one row */
469
+ moveUp: () => void
470
+ /** Move down one row */
471
+ moveDown: () => void
472
+ /** Move left one column */
473
+ moveLeft: () => void
474
+ /** Move right one column */
475
+ moveRight: () => void
476
+ /** Move to a specific cell */
477
+ moveToCell: (row: number, col: number) => void
478
+ }
479
+
480
+ /**
481
+ * React hook for keyboard-navigable grids.
482
+ *
483
+ * Manages 2D position state and provides navigation methods
484
+ * for building grid-based interactive interfaces.
485
+ *
486
+ * @param options - Configuration options
487
+ * @returns Position state and navigation controls
488
+ *
489
+ * @example
490
+ * ```tsx
491
+ * function Calendar() {
492
+ * const { row, col, moveUp, moveDown, moveLeft, moveRight } = useNavigableGrid({
493
+ * rows: 5,
494
+ * cols: 7,
495
+ * wrapHorizontal: true
496
+ * })
497
+ *
498
+ * // Render 5x7 calendar grid with selection at [row, col]
499
+ * }
500
+ * ```
501
+ */
502
+ export function useNavigableGrid(options: UseNavigableGridOptions): UseNavigableGridResult {
503
+ const {
504
+ rows,
505
+ cols,
506
+ initialRow = 0,
507
+ initialCol = 0,
508
+ wrapHorizontal = false,
509
+ wrapVertical = false,
510
+ useKeyboard: keyboardEnabled = false,
511
+ } = options
512
+
513
+ const [row, setRow] = useState(initialRow)
514
+ const [col, setCol] = useState(initialCol)
515
+
516
+ const moveUp = useCallback(() => {
517
+ setRow((prev) => {
518
+ if (prev <= 0) {
519
+ return wrapVertical ? rows - 1 : 0
520
+ }
521
+ return prev - 1
522
+ })
523
+ }, [rows, wrapVertical])
524
+
525
+ const moveDown = useCallback(() => {
526
+ setRow((prev) => {
527
+ if (prev >= rows - 1) {
528
+ return wrapVertical ? 0 : rows - 1
529
+ }
530
+ return prev + 1
531
+ })
532
+ }, [rows, wrapVertical])
533
+
534
+ const moveLeft = useCallback(() => {
535
+ setCol((prev) => {
536
+ if (prev <= 0) {
537
+ return wrapHorizontal ? cols - 1 : 0
538
+ }
539
+ return prev - 1
540
+ })
541
+ }, [cols, wrapHorizontal])
542
+
543
+ const moveRight = useCallback(() => {
544
+ setCol((prev) => {
545
+ if (prev >= cols - 1) {
546
+ return wrapHorizontal ? 0 : cols - 1
547
+ }
548
+ return prev + 1
549
+ })
550
+ }, [cols, wrapHorizontal])
551
+
552
+ const moveToCell = useCallback(
553
+ (newRow: number, newCol: number) => {
554
+ if (newRow >= 0 && newRow < rows) {
555
+ setRow(newRow)
556
+ }
557
+ if (newCol >= 0 && newCol < cols) {
558
+ setCol(newCol)
559
+ }
560
+ },
561
+ [rows, cols]
562
+ )
563
+
564
+ return {
565
+ row,
566
+ col,
567
+ wrapHorizontal,
568
+ wrapVertical,
569
+ keyboardEnabled,
570
+ moveUp,
571
+ moveDown,
572
+ moveLeft,
573
+ moveRight,
574
+ moveToCell,
575
+ }
576
+ }
577
+
578
+ // ============================================================================
579
+ // FocusProvider Component
580
+ // ============================================================================
581
+
582
+ /**
583
+ * Props for the FocusProvider component.
584
+ */
585
+ export interface FocusProviderProps {
586
+ /** Child components */
587
+ children?: ReactNode
588
+ /** Trap focus within this provider (default: false) */
589
+ trap?: boolean
590
+ /** Wrap focus when reaching boundaries (default: false) */
591
+ wrapFocus?: boolean
592
+ /** ID of element to focus initially */
593
+ initialFocus?: FocusId
594
+ /** Focus first element on mount (default: false) */
595
+ focusFirstOnMount?: boolean
596
+ /** Restore focus to previous element when provider unmounts */
597
+ restoreFocus?: boolean
598
+ /** Group name for nested focus management */
599
+ group?: string
600
+ }
601
+
602
+ /**
603
+ * Focus provider component for managing focus state across components.
604
+ *
605
+ * Provides focus management context to child components, enabling
606
+ * coordinated focus navigation within a container.
607
+ *
608
+ * **Features:**
609
+ * - Focus tracking across registered focusable elements
610
+ * - Focus trapping for modals and dialogs
611
+ * - Wrap-around navigation at boundaries
612
+ * - Initial focus and focus restoration support
613
+ * - Named focus groups for scoped management
614
+ *
615
+ * @param props - Configuration and children
616
+ * @returns React element with focus context
617
+ *
618
+ * @example
619
+ * ```tsx
620
+ * function Dialog() {
621
+ * return (
622
+ * <FocusProvider trap focusFirstOnMount>
623
+ * <Input label="Name" />
624
+ * <Input label="Email" />
625
+ * <Button>Submit</Button>
626
+ * </FocusProvider>
627
+ * )
628
+ * }
629
+ * ```
630
+ */
631
+ export function FocusProvider(props: FocusProviderProps): ReactElement {
632
+ const { children, trap = false, wrapFocus = false, initialFocus, focusFirstOnMount = false, restoreFocus = false, group } = props
633
+
634
+ // Track registered focusable elements
635
+ const [focusableIds, setFocusableIds] = useState<FocusId[]>([])
636
+ const [focusedId, setFocusedId] = useState<FocusId | null>(initialFocus ?? null)
637
+ const previousFocusRef = useRef<FocusId | null>(null)
638
+ // Track tabIndex for each focusable element for sorting
639
+ const tabIndexMapRef = useRef<Map<FocusId, number>>(new Map())
640
+
641
+ // Focus first element on mount if configured
642
+ useEffect(() => {
643
+ if (focusFirstOnMount && focusableIds.length > 0 && !focusedId) {
644
+ setFocusedId(focusableIds[0])
645
+ }
646
+ }, [focusFirstOnMount, focusableIds, focusedId])
647
+
648
+ // Restore focus on unmount if configured
649
+ useEffect(() => {
650
+ if (restoreFocus) {
651
+ previousFocusRef.current = focusedId
652
+ }
653
+ return () => {
654
+ if (restoreFocus && previousFocusRef.current) {
655
+ // Focus restoration would happen here in a real DOM environment
656
+ }
657
+ }
658
+ }, [restoreFocus, focusedId])
659
+
660
+ const focusById = useCallback((id: FocusId) => {
661
+ if (focusableIds.includes(id)) {
662
+ setFocusedId(id)
663
+ }
664
+ }, [focusableIds])
665
+
666
+ const focusNext = useCallback(() => {
667
+ if (focusableIds.length === 0) return
668
+ const currentIndex = focusedId ? focusableIds.indexOf(focusedId) : -1
669
+ let nextIndex = currentIndex + 1
670
+ if (nextIndex >= focusableIds.length) {
671
+ nextIndex = wrapFocus ? 0 : focusableIds.length - 1
672
+ }
673
+ setFocusedId(focusableIds[nextIndex])
674
+ }, [focusableIds, focusedId, wrapFocus])
675
+
676
+ const focusPrev = useCallback(() => {
677
+ if (focusableIds.length === 0) return
678
+ const currentIndex = focusedId ? focusableIds.indexOf(focusedId) : focusableIds.length
679
+ let prevIndex = currentIndex - 1
680
+ if (prevIndex < 0) {
681
+ prevIndex = wrapFocus ? focusableIds.length - 1 : 0
682
+ }
683
+ setFocusedId(focusableIds[prevIndex])
684
+ }, [focusableIds, focusedId, wrapFocus])
685
+
686
+ const focusFirst = useCallback(() => {
687
+ if (focusableIds.length > 0) {
688
+ setFocusedId(focusableIds[0])
689
+ }
690
+ }, [focusableIds])
691
+
692
+ const focusLast = useCallback(() => {
693
+ if (focusableIds.length > 0) {
694
+ setFocusedId(focusableIds[focusableIds.length - 1])
695
+ }
696
+ }, [focusableIds])
697
+
698
+ const registerFocusable = useCallback((id: FocusId, tabIndex: number) => {
699
+ tabIndexMapRef.current.set(id, tabIndex)
700
+ setFocusableIds(prev => {
701
+ if (prev.includes(id)) return prev
702
+ const newIds = [...prev, id]
703
+ // Sort by tabIndex (lower values first)
704
+ newIds.sort((a, b) => {
705
+ const tabA = tabIndexMapRef.current.get(a) ?? 0
706
+ const tabB = tabIndexMapRef.current.get(b) ?? 0
707
+ return tabA - tabB
708
+ })
709
+ return newIds
710
+ })
711
+ }, [])
712
+
713
+ const unregisterFocusable = useCallback((id: FocusId) => {
714
+ tabIndexMapRef.current.delete(id)
715
+ setFocusableIds(prev => prev.filter(fid => fid !== id))
716
+ }, [])
717
+
718
+ const contextValue: FocusManagerState = {
719
+ focusableIds,
720
+ focusedId,
721
+ isTrapped: trap,
722
+ focusById,
723
+ focusNext,
724
+ focusPrev,
725
+ focusFirst,
726
+ focusLast,
727
+ registerFocusable,
728
+ unregisterFocusable,
729
+ }
730
+
731
+ // Render a div wrapper that passes through all props for test compatibility
732
+ // and provides focus context to children
733
+ return React.createElement(
734
+ FocusContext.Provider,
735
+ { value: contextValue },
736
+ React.createElement(
737
+ 'div',
738
+ {
739
+ 'data-focus-provider': true,
740
+ 'data-focus-group': group,
741
+ 'data-focus-trap': trap,
742
+ ...props,
743
+ children: undefined, // Remove children from div props to avoid duplication
744
+ },
745
+ children
746
+ )
747
+ )
748
+ }