@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,101 @@
1
+ /**
2
+ * @mdxui/terminal Renderers
3
+ *
4
+ * Multi-tier rendering architecture for Universal Terminal UI.
5
+ * Each tier provides progressively richer output capabilities:
6
+ *
7
+ * - TEXT: Plain text without formatting (tier 1)
8
+ * - MARKDOWN: Markdown syntax for AI agents (tier 2)
9
+ * - ASCII: ASCII art and basic drawing characters (tier 3)
10
+ * - UNICODE: Unicode box drawing and symbols (tier 4)
11
+ * - ANSI: Full ANSI escape sequences for colors/styles (tier 5)
12
+ * - INTERACTIVE: Full interactive terminal UI with input handling (tier 6)
13
+ */
14
+
15
+ // TEXT tier renderer
16
+ export { renderText } from './text'
17
+ export type { TextRenderOptions } from './text'
18
+
19
+ // MARKDOWN tier renderer
20
+ export { renderMarkdown } from './markdown'
21
+ export type { MarkdownRenderOptions } from './markdown'
22
+
23
+ // ASCII tier renderer
24
+ export { renderASCII } from './ascii'
25
+
26
+ // UNICODE tier renderer
27
+ export { renderUnicode } from './unicode'
28
+
29
+ // ANSI tier renderer
30
+ export { renderANSI } from './ansi'
31
+
32
+ // INTERACTIVE tier renderer
33
+ export { renderInteractive, createInteractiveRenderer } from './interactive'
34
+ export type {
35
+ InteractiveRendererConfig,
36
+ InteractiveRenderer,
37
+ FocusableOptions,
38
+ ClickableOptions,
39
+ InputOptions,
40
+ ComponentOptions,
41
+ } from './interactive'
42
+
43
+ // ANSI to CSS conversion utilities for web rendering
44
+ export {
45
+ ansiToCSS,
46
+ ansiToHTML,
47
+ parseAnsiToSpans,
48
+ spanToInlineStyle,
49
+ } from './ansi-css'
50
+ export type {
51
+ CSSStyleProperties,
52
+ StyledSpan,
53
+ ANSIToCSSResult,
54
+ } from './ansi-css'
55
+
56
+ // Common utilities used across renderers
57
+ export {
58
+ getIndentStr,
59
+ getTextWidth,
60
+ padText,
61
+ wrapText,
62
+ sanitizeToASCII,
63
+ boxDrawingToASCII,
64
+ buildBox,
65
+ extractStringArray,
66
+ extractHeaders,
67
+ extractRowValues,
68
+ joinParts,
69
+ getProp,
70
+ calculateColumnWidths,
71
+ buildTableSeparator,
72
+ buildTableRow,
73
+ matchPath,
74
+ normalizePath,
75
+ matchPathPattern,
76
+ generateBreadcrumbSegments,
77
+ formatSegmentLabel,
78
+ findActiveItemByPath,
79
+ findActiveItemInSections,
80
+ joinPath,
81
+ extractPathParams,
82
+ createCallbackRouterAdapter,
83
+ ASCII_BOX_CHARS,
84
+ ASCII_DOUBLE_BOX_CHARS,
85
+ UNICODE_SINGLE_BOX_CHARS,
86
+ UNICODE_DOUBLE_BOX_CHARS,
87
+ UNICODE_ROUNDED_BOX_CHARS,
88
+ getASCIIBoxChars,
89
+ getUnicodeBoxChars,
90
+ UNICODE_SYMBOLS,
91
+ SPINNER_FRAMES,
92
+ DEFAULT_THEME_TOKENS,
93
+ DEFAULT_RENDER_CONTEXT,
94
+ } from './utils'
95
+ export type {
96
+ BoxChars,
97
+ TextAlign,
98
+ RendererRegistry,
99
+ RouterAdapter,
100
+ RouteMatchMode,
101
+ } from './utils'
@@ -0,0 +1,622 @@
1
+ /**
2
+ * @mdxui/terminal Component Handlers
3
+ *
4
+ * Handles per-component rendering and state management
5
+ * for the interactive renderer.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type {
11
+ ComponentOptions,
12
+ ComponentManagerState,
13
+ FocusManagerState,
14
+ InputManagerState,
15
+ UINode,
16
+ InteractiveRenderer,
17
+ } from './types'
18
+ import { registerFocusable, unregisterFocusable } from './focus-manager'
19
+ import { handleInputKey } from './input-handler'
20
+
21
+ /**
22
+ * Creates the component manager state
23
+ */
24
+ export function createComponentManagerState(): ComponentManagerState {
25
+ return {
26
+ components: new Map<string, ComponentOptions>(),
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Register a component
32
+ */
33
+ export function registerComponent(
34
+ componentState: ComponentManagerState,
35
+ focusState: FocusManagerState,
36
+ id: string,
37
+ options: ComponentOptions,
38
+ destroyed?: boolean
39
+ ): void {
40
+ if (destroyed) return
41
+ componentState.components.set(id, { ...options })
42
+
43
+ // Register as focusable if applicable
44
+ if (['select', 'checkbox', 'radiogroup', 'slider', 'tree', 'scrollview'].includes(options.type)) {
45
+ registerFocusable(focusState, id, { tabIndex: 0 }, destroyed)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Unregister a component
51
+ */
52
+ export function unregisterComponent(
53
+ componentState: ComponentManagerState,
54
+ focusState: FocusManagerState,
55
+ id: string,
56
+ destroyed?: boolean
57
+ ): void {
58
+ if (destroyed) return
59
+ componentState.components.delete(id)
60
+ unregisterFocusable(focusState, id, destroyed)
61
+ }
62
+
63
+ /**
64
+ * Get a component
65
+ */
66
+ export function getComponent(
67
+ componentState: ComponentManagerState,
68
+ id: string,
69
+ destroyed?: boolean
70
+ ): ComponentOptions | undefined {
71
+ if (destroyed) return undefined
72
+ return componentState.components.get(id)
73
+ }
74
+
75
+ /**
76
+ * Update a component
77
+ */
78
+ export function updateComponent(
79
+ componentState: ComponentManagerState,
80
+ id: string,
81
+ updates: Partial<ComponentOptions>,
82
+ destroyed?: boolean
83
+ ): void {
84
+ if (destroyed) return
85
+ const existing = componentState.components.get(id)
86
+ if (existing) {
87
+ Object.assign(existing, updates)
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get visible tree nodes
93
+ */
94
+ export function getVisibleTreeNodes(
95
+ nodes: Array<{ id: string; children?: string[]; expanded?: boolean; parent?: string }>
96
+ ): typeof nodes {
97
+ const result: typeof nodes = []
98
+ const rootNodes = nodes.filter(n => !n.parent)
99
+
100
+ const addNode = (node: (typeof nodes)[0]) => {
101
+ result.push(node)
102
+ if (node.expanded && node.children) {
103
+ for (const childId of node.children) {
104
+ const child = nodes.find(n => n.id === childId)
105
+ if (child) addNode(child)
106
+ }
107
+ }
108
+ }
109
+
110
+ for (const root of rootNodes) {
111
+ addNode(root)
112
+ }
113
+
114
+ return result
115
+ }
116
+
117
+ /**
118
+ * Handle component key press
119
+ */
120
+ export function handleComponentKeyInternal(
121
+ id: string,
122
+ component: ComponentOptions,
123
+ key: string
124
+ ): boolean {
125
+ const {
126
+ type,
127
+ onChange,
128
+ onSelect,
129
+ onToggle,
130
+ options,
131
+ selectedIndex,
132
+ checked,
133
+ value,
134
+ min,
135
+ max,
136
+ step,
137
+ searchable,
138
+ onScroll,
139
+ nodes,
140
+ selectedId,
141
+ } = component as ComponentOptions & {
142
+ onChange?: (index: number, value?: string) => void
143
+ onSelect?: (index: number | string, value?: string) => void
144
+ onToggle?: (id: string, expanded: boolean) => void
145
+ options?: string[]
146
+ selectedIndex?: number
147
+ checked?: boolean
148
+ value?: number
149
+ min?: number
150
+ max?: number
151
+ step?: number
152
+ searchable?: boolean
153
+ onScroll?: (event: { scrollY: number }) => void
154
+ nodes?: Array<{ id: string; label: string; children?: string[]; expanded?: boolean; parent?: string }>
155
+ selectedId?: string
156
+ }
157
+
158
+ if (type === 'select') {
159
+ const opts = options as string[]
160
+ const idx = selectedIndex as number
161
+
162
+ if (key === 'down' || key === 'j') {
163
+ const newIdx = Math.min(idx + 1, opts.length - 1)
164
+ component.selectedIndex = newIdx
165
+ onChange?.(newIdx, opts[newIdx])
166
+ return true
167
+ }
168
+ if (key === 'up' || key === 'k') {
169
+ const newIdx = Math.max(idx - 1, 0)
170
+ component.selectedIndex = newIdx
171
+ onChange?.(newIdx, opts[newIdx])
172
+ return true
173
+ }
174
+ if (key === 'enter') {
175
+ // If onSelect is provided, call it
176
+ if (onSelect) {
177
+ onSelect(idx, opts[idx])
178
+ }
179
+ // Toggle open state for dropdown behavior
180
+ component.isOpen = !component.isOpen
181
+ return true
182
+ }
183
+ // Type-ahead search
184
+ if (searchable && key.length === 1) {
185
+ const matchIdx = opts.findIndex(o => o.toLowerCase().startsWith(key.toLowerCase()))
186
+ if (matchIdx !== -1) {
187
+ component.selectedIndex = matchIdx
188
+ onChange?.(matchIdx, opts[matchIdx])
189
+ }
190
+ return true
191
+ }
192
+ return false
193
+ }
194
+
195
+ if (type === 'checkbox') {
196
+ if (key === 'space' || key === 'enter') {
197
+ const newChecked = !checked
198
+ component.checked = newChecked
199
+ ;(component.onChange as (checked: boolean) => void)?.(newChecked)
200
+ return true
201
+ }
202
+ return false
203
+ }
204
+
205
+ if (type === 'radiogroup') {
206
+ const opts = options as string[]
207
+ const idx = selectedIndex as number
208
+
209
+ if (key === 'down' || key === 'up') {
210
+ const newIdx = key === 'down' ? Math.min(idx + 1, opts.length - 1) : Math.max(idx - 1, 0)
211
+ component.selectedIndex = newIdx
212
+ ;(component.onChange as (index: number) => void)?.(newIdx)
213
+ return true
214
+ }
215
+ if (key === 'space') {
216
+ onSelect?.(idx, opts[idx])
217
+ return true
218
+ }
219
+ return false
220
+ }
221
+
222
+ if (type === 'slider') {
223
+ const v = value as number
224
+ const minV = min as number
225
+ const maxV = max as number
226
+ const stepV = step ?? 1
227
+
228
+ if (key === 'right' || key === 'up') {
229
+ const newV = Math.min(v + stepV, maxV)
230
+ component.value = newV
231
+ ;(component.onChange as (value: number) => void)?.(newV)
232
+ return true
233
+ }
234
+ if (key === 'left' || key === 'down') {
235
+ const newV = Math.max(v - stepV, minV)
236
+ component.value = newV
237
+ ;(component.onChange as (value: number) => void)?.(newV)
238
+ return true
239
+ }
240
+ if (key === 'home') {
241
+ component.value = minV
242
+ ;(component.onChange as (value: number) => void)?.(minV)
243
+ return true
244
+ }
245
+ if (key === 'end') {
246
+ component.value = maxV
247
+ ;(component.onChange as (value: number) => void)?.(maxV)
248
+ return true
249
+ }
250
+ return false
251
+ }
252
+
253
+ if (type === 'tree') {
254
+ const nodeList = nodes as Array<{
255
+ id: string
256
+ label: string
257
+ children?: string[]
258
+ expanded?: boolean
259
+ parent?: string
260
+ }>
261
+ const currentId = selectedId as string
262
+ const currentNode = nodeList.find(n => n.id === currentId)
263
+
264
+ if (key === 'enter' && currentNode?.children) {
265
+ const newExpanded = !currentNode.expanded
266
+ currentNode.expanded = newExpanded
267
+ onToggle?.(currentId, newExpanded)
268
+ return true
269
+ }
270
+ if (key === 'down') {
271
+ // Find next visible node
272
+ const visibleNodes = getVisibleTreeNodes(nodeList)
273
+ const currentIndex = visibleNodes.findIndex(n => n.id === currentId)
274
+ if (currentIndex < visibleNodes.length - 1) {
275
+ const nextNode = visibleNodes[currentIndex + 1]
276
+ component.selectedId = nextNode.id
277
+ ;(component.onSelect as (id: string) => void)?.(nextNode.id)
278
+ }
279
+ return true
280
+ }
281
+ return false
282
+ }
283
+
284
+ if (type === 'scrollview') {
285
+ const currentScrollY = (component.scrollY as number) ?? 0
286
+ const viewportHeight = (component.viewportHeight as number) ?? 20
287
+
288
+ if (key === 'down') {
289
+ const newScrollY = currentScrollY + 1
290
+ component.scrollY = newScrollY
291
+ onScroll?.({ scrollY: newScrollY })
292
+ return true
293
+ }
294
+ if (key === 'up') {
295
+ const newScrollY = Math.max(0, currentScrollY - 1)
296
+ component.scrollY = newScrollY
297
+ onScroll?.({ scrollY: newScrollY })
298
+ return true
299
+ }
300
+ if (key === 'pagedown') {
301
+ const newScrollY = currentScrollY + viewportHeight
302
+ component.scrollY = newScrollY
303
+ onScroll?.({ scrollY: newScrollY })
304
+ return true
305
+ }
306
+ if (key === 'pageup') {
307
+ const newScrollY = Math.max(0, currentScrollY - viewportHeight)
308
+ component.scrollY = newScrollY
309
+ onScroll?.({ scrollY: newScrollY })
310
+ return true
311
+ }
312
+ return false
313
+ }
314
+
315
+ return false
316
+ }
317
+
318
+ /**
319
+ * Handle key for focused component
320
+ */
321
+ export function handleComponentKey(
322
+ focusedId: string | null,
323
+ inputState: InputManagerState,
324
+ componentState: ComponentManagerState,
325
+ focusState: FocusManagerState,
326
+ key: string
327
+ ): boolean {
328
+ if (!focusedId) return false
329
+
330
+ // Check if focused element is an input
331
+ const input = inputState.inputs.get(focusedId)
332
+ if (input) {
333
+ return handleInputKey(focusedId, input, key)
334
+ }
335
+
336
+ // Check if focused element is a component
337
+ const component = componentState.components.get(focusedId)
338
+ if (component) {
339
+ return handleComponentKeyInternal(focusedId, component, key)
340
+ }
341
+
342
+ // Check focusable handlers
343
+ const focusable = focusState.focusables.get(focusedId)
344
+ if (focusable) {
345
+ if (key === 'enter' && focusable.options.onActivate) {
346
+ focusable.options.onActivate({ id: focusedId })
347
+ return true
348
+ }
349
+ if (key === 'space' && focusable.options.onToggle) {
350
+ focusable.options.onToggle({ id: focusedId })
351
+ return true
352
+ }
353
+ }
354
+
355
+ return false
356
+ }
357
+
358
+ /**
359
+ * Clear component state on destroy
360
+ */
361
+ export function clearComponentState(componentState: ComponentManagerState): void {
362
+ componentState.components.clear()
363
+ }
364
+
365
+ /**
366
+ * Registers a UINode with the interactive renderer (for component state management).
367
+ * This is used internally for focus management and event handling.
368
+ */
369
+ export function registerInteractiveNode(
370
+ node: UINode,
371
+ renderer: InteractiveRenderer,
372
+ options?: { group?: string }
373
+ ): void {
374
+ const { type, props } = node
375
+ const id = props.id as string | undefined
376
+ const group = options?.group
377
+
378
+ if (!id) return
379
+
380
+ // Handle different node types
381
+ switch (type) {
382
+ case 'button': {
383
+ const { onClick, disabled } = props as { onClick?: () => void; disabled?: boolean }
384
+ renderer.registerFocusable(id, {
385
+ tabIndex: 0,
386
+ disabled,
387
+ group,
388
+ onActivate: onClick ? () => !disabled && onClick() : undefined,
389
+ onToggle: onClick ? () => !disabled && onClick() : undefined,
390
+ })
391
+ break
392
+ }
393
+
394
+ case 'input': {
395
+ const { placeholder, value, type: inputType } = props as {
396
+ placeholder?: string
397
+ value?: string
398
+ type?: string
399
+ }
400
+ renderer.registerComponent(id, {
401
+ type: 'input',
402
+ placeholder,
403
+ value: value ?? '',
404
+ displayValue: inputType === 'password' && value ? '*'.repeat(value.length) : value,
405
+ })
406
+ renderer.registerFocusable(id, { tabIndex: 0, group })
407
+ break
408
+ }
409
+
410
+ case 'select': {
411
+ const { options: selectOptions } = props as { options?: Array<{ value: string; label: string }> }
412
+ renderer.registerComponent(id, {
413
+ type: 'select',
414
+ options: selectOptions ?? [],
415
+ selectedIndex: 0,
416
+ isOpen: false,
417
+ })
418
+ renderer.registerFocusable(id, { tabIndex: 0, group })
419
+ break
420
+ }
421
+
422
+ case 'dialog': {
423
+ const { open, onClose, children } = props as {
424
+ open?: boolean
425
+ onClose?: () => void
426
+ children?: UINode[]
427
+ }
428
+ if (open) {
429
+ // Push focus trap
430
+ renderer.pushFocusTrap(id)
431
+
432
+ // Register cancel handler
433
+ if (onClose) {
434
+ renderer.onCancel(onClose)
435
+ }
436
+
437
+ // Render children with this dialog's group
438
+ if (children) {
439
+ for (const child of children) {
440
+ registerInteractiveNode(child, renderer, { group: id })
441
+ }
442
+ }
443
+ }
444
+ break
445
+ }
446
+
447
+ case 'table': {
448
+ const { rows, onRowSelect, onCellSelect, cellNavigation, columns, data } = props as {
449
+ rows?: Array<{ id: string; cells: string[] }>
450
+ onRowSelect?: (id: string) => void
451
+ onCellSelect?: (rowId: string, cellIndex: number) => void
452
+ cellNavigation?: boolean
453
+ columns?: Array<{ key: string; header: string }>
454
+ data?: Array<Record<string, unknown>>
455
+ }
456
+
457
+ // Get data from node.data (TDD) or props.data
458
+ const nodeData = node.data as Array<Record<string, unknown>> | undefined
459
+ const tableData = nodeData ?? data
460
+
461
+ let currentRowIndex = 0
462
+ let currentCellIndex = 0
463
+
464
+ renderer.registerComponent(id, {
465
+ type: 'table',
466
+ rows: rows ?? [],
467
+ columns: columns ?? [],
468
+ data: tableData ?? [],
469
+ selectedRowIndex: 0,
470
+ selectedCellIndex: 0,
471
+ cellNavigation,
472
+ onRowSelect,
473
+ onCellSelect,
474
+ })
475
+
476
+ renderer.registerFocusable(id, {
477
+ tabIndex: 0,
478
+ onActivate: () => {
479
+ if (rows && rows[currentRowIndex]) {
480
+ onRowSelect?.(rows[currentRowIndex].id)
481
+ }
482
+ },
483
+ })
484
+
485
+ // Custom key handling for table
486
+ renderer.onKeyPress(
487
+ 'down',
488
+ () => {
489
+ if (renderer.getFocusedId() !== id || !rows) return false
490
+ if (currentRowIndex < rows.length - 1) {
491
+ currentRowIndex++
492
+ onRowSelect?.(rows[currentRowIndex].id)
493
+ }
494
+ return true
495
+ },
496
+ { priority: 10 }
497
+ )
498
+
499
+ renderer.onKeyPress(
500
+ 'right',
501
+ () => {
502
+ if (renderer.getFocusedId() !== id || !rows || !cellNavigation) return false
503
+ const row = rows[currentRowIndex]
504
+ if (row && currentCellIndex < row.cells.length - 1) {
505
+ currentCellIndex++
506
+ onCellSelect?.(row.id, currentCellIndex)
507
+ }
508
+ return true
509
+ },
510
+ { priority: 10 }
511
+ )
512
+ break
513
+ }
514
+
515
+ case 'progress': {
516
+ const { value, max } = props as { value?: number; max?: number }
517
+ renderer.registerComponent(id, {
518
+ type: 'progress',
519
+ value: value ?? 0,
520
+ max: max ?? 100,
521
+ })
522
+ break
523
+ }
524
+
525
+ case 'spinner': {
526
+ renderer.registerComponent(id, {
527
+ type: 'spinner',
528
+ isAnimating: true,
529
+ })
530
+ break
531
+ }
532
+
533
+ case 'scrollview': {
534
+ const { contentHeight, viewportHeight, onScroll, scrollY } = props as {
535
+ contentHeight?: number
536
+ viewportHeight?: number
537
+ onScroll?: (event: { scrollY: number }) => void
538
+ scrollY?: number
539
+ }
540
+
541
+ renderer.registerComponent(id, {
542
+ type: 'scrollview',
543
+ contentHeight: contentHeight ?? 100,
544
+ viewportHeight: viewportHeight ?? 20,
545
+ scrollY: scrollY ?? 0,
546
+ onScroll,
547
+ })
548
+ renderer.registerFocusable(id, { tabIndex: 0 })
549
+ break
550
+ }
551
+
552
+ case 'list': {
553
+ const { items, numbered, taskList, children } = props as {
554
+ items?: Array<string | { text: string; checked?: boolean }>
555
+ numbered?: boolean
556
+ taskList?: boolean
557
+ children?: UINode[]
558
+ }
559
+
560
+ renderer.registerComponent(id, {
561
+ type: 'list',
562
+ items: items ?? [],
563
+ numbered: numbered ?? false,
564
+ taskList: taskList ?? false,
565
+ children,
566
+ })
567
+ break
568
+ }
569
+
570
+ case 'card': {
571
+ const { title, subtitle, badge, titleAction, pairs, actions } = props as {
572
+ title?: string
573
+ subtitle?: string
574
+ badge?: { content: string; variant?: string }
575
+ titleAction?: { label: string; action?: string }
576
+ pairs?: Array<{ key: string; value: unknown }>
577
+ actions?: Array<{ label: string; action?: string }>
578
+ }
579
+
580
+ renderer.registerComponent(id, {
581
+ type: 'card',
582
+ title,
583
+ subtitle,
584
+ badge,
585
+ titleAction,
586
+ pairs,
587
+ actions,
588
+ })
589
+ break
590
+ }
591
+
592
+ case 'metrics': {
593
+ const { metrics } = props as {
594
+ metrics?: Array<{ label: string; value: unknown; format?: string; unit?: string; trend?: string }>
595
+ }
596
+
597
+ renderer.registerComponent(id, {
598
+ type: 'metrics',
599
+ metrics: metrics ?? [],
600
+ })
601
+ break
602
+ }
603
+
604
+ case 'metric': {
605
+ const { label, value, format, unit } = props as {
606
+ label?: string
607
+ value?: unknown
608
+ format?: string
609
+ unit?: string
610
+ }
611
+
612
+ renderer.registerComponent(id, {
613
+ type: 'metric',
614
+ label,
615
+ value,
616
+ format,
617
+ unit,
618
+ })
619
+ break
620
+ }
621
+ }
622
+ }