@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,210 @@
1
+ /**
2
+ * Field Component Renderer
3
+ *
4
+ * Renders individual input fields with labels, values, errors, and type-specific display
5
+ * across all 6 render tiers.
6
+ */
7
+ import type { UINode, RenderContext } from '../../core/types'
8
+ import { getProp } from '../utils'
9
+
10
+ const RESET = '\x1b[0m'
11
+ const DIM = '\x1b[2m'
12
+
13
+ // ============================================================================
14
+ // Types
15
+ // ============================================================================
16
+
17
+ interface FieldProps {
18
+ name: string
19
+ label?: string
20
+ type?: string
21
+ value?: unknown
22
+ placeholder?: string
23
+ required?: boolean
24
+ error?: string
25
+ valid?: boolean
26
+ disabled?: boolean
27
+ readonly?: boolean
28
+ helperText?: string
29
+ focused?: boolean
30
+ cursorPosition?: number
31
+ selectionStart?: number
32
+ selectionEnd?: number
33
+ }
34
+
35
+ // ============================================================================
36
+ // Rendering Helpers
37
+ // ============================================================================
38
+
39
+ function maskPassword(value: string, tier: string): string {
40
+ const maskChar = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '•' : '*'
41
+ return maskChar.repeat(value.length)
42
+ }
43
+
44
+ function renderCheckbox(checked: boolean, tier: string): string {
45
+ if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
46
+ return checked ? '[✓]' : '[ ]'
47
+ }
48
+ return checked ? '[x]' : '[ ]'
49
+ }
50
+
51
+ function renderValueWithSelection(
52
+ value: string,
53
+ selectionStart: number,
54
+ selectionEnd: number
55
+ ): string {
56
+ const before = value.slice(0, selectionStart)
57
+ const selected = value.slice(selectionStart, selectionEnd)
58
+ const after = value.slice(selectionEnd)
59
+ return `${before}\x1b[7m${selected}${RESET}${after}`
60
+ }
61
+
62
+ function renderValueWithCursor(value: string, cursorPosition: number): string {
63
+ const pos = Math.min(cursorPosition, value.length)
64
+ const before = value.slice(0, pos)
65
+ const after = value.slice(pos)
66
+ // Use reverse video for cursor position
67
+ return `${before}\x1b[7m \x1b[0m${after}`
68
+ }
69
+
70
+ // ============================================================================
71
+ // Main Renderer
72
+ // ============================================================================
73
+
74
+ export function renderField(node: UINode, ctx: RenderContext): string {
75
+ const { tier, theme } = ctx
76
+ const props = node.props ?? {}
77
+
78
+ const name = getProp<string>(props, 'name', '')
79
+ const label = getProp<string>(props, 'label', '')
80
+ const type = getProp<string>(props, 'type', 'text')
81
+ const value = props.value
82
+ const placeholder = getProp<string>(props, 'placeholder', '')
83
+ const required = getProp<boolean>(props, 'required', false)
84
+ const error = getProp<string>(props, 'error', '')
85
+ const valid = getProp<boolean>(props, 'valid', false)
86
+ const disabled = getProp<boolean>(props, 'disabled', false)
87
+ const helperText = getProp<string>(props, 'helperText', '')
88
+ const focused = getProp<boolean>(props, 'focused', false)
89
+ const cursorPosition = getProp<number | undefined>(props, 'cursorPosition', undefined)
90
+ const selectionStart = getProp<number | undefined>(props, 'selectionStart', undefined)
91
+ const selectionEnd = getProp<number | undefined>(props, 'selectionEnd', undefined)
92
+
93
+ const lines: string[] = []
94
+ const displayLabel = label || name
95
+
96
+ // Build label with required indicator
97
+ let labelStr = displayLabel
98
+ if (required) {
99
+ labelStr += ' *'
100
+ }
101
+
102
+ // Handle checkbox type
103
+ if (type === 'checkbox') {
104
+ const checked = !!value
105
+ const checkbox = renderCheckbox(checked, tier)
106
+ let line = `${checkbox} ${displayLabel}`
107
+ if (required) {
108
+ line += ' *'
109
+ }
110
+
111
+ if (tier === 'ansi' || tier === 'interactive') {
112
+ if (disabled) {
113
+ line = `${DIM}${line}${RESET}`
114
+ } else if (focused) {
115
+ line = `${theme.primary}${line}${RESET}`
116
+ } else if (valid) {
117
+ line = `${theme.success}${line}${RESET}`
118
+ }
119
+ }
120
+
121
+ lines.push(line)
122
+ } else {
123
+ // Regular field types
124
+ let displayValue = ''
125
+ let isPlaceholder = false
126
+
127
+ if (value === null || value === undefined) {
128
+ displayValue = placeholder
129
+ isPlaceholder = true
130
+ } else if (type === 'password' && typeof value === 'string') {
131
+ displayValue = maskPassword(value, tier)
132
+ } else if (type === 'textarea' && typeof value === 'string') {
133
+ displayValue = value // Keep newlines
134
+ } else {
135
+ displayValue = String(value)
136
+ }
137
+
138
+ // Interactive tier: handle selection and cursor
139
+ if (tier === 'interactive' && focused) {
140
+ const textValue = value === null || value === undefined ? '' : String(value)
141
+ if (typeof selectionStart === 'number' && typeof selectionEnd === 'number' && selectionStart !== selectionEnd) {
142
+ displayValue = renderValueWithSelection(textValue, selectionStart, selectionEnd)
143
+ } else if (typeof cursorPosition === 'number') {
144
+ displayValue = renderValueWithCursor(textValue, cursorPosition)
145
+ }
146
+ }
147
+
148
+ // Format based on tier
149
+ if (tier === 'markdown') {
150
+ const valueDisplay = displayValue || `_${placeholder}_`
151
+ lines.push(`**${labelStr}**: ${valueDisplay}`)
152
+ } else {
153
+ // Text/ASCII/Unicode/ANSI/Interactive
154
+ let line: string
155
+
156
+ if (tier === 'ansi' || tier === 'interactive') {
157
+ if (disabled) {
158
+ const val = displayValue || placeholder
159
+ line = `${DIM}${labelStr}: ${val}${RESET}`
160
+ } else if (error) {
161
+ line = `${theme.error}${labelStr}${RESET}: ${displayValue || `${theme.muted}${placeholder}${RESET}`}`
162
+ } else if (valid) {
163
+ line = `${theme.success}${labelStr}${RESET}: ${displayValue}`
164
+ } else if (focused) {
165
+ line = `${theme.primary}${labelStr}${RESET}: ${displayValue || `${theme.muted}${placeholder}${RESET}`}`
166
+ } else if (isPlaceholder && placeholder) {
167
+ line = `${labelStr}: ${theme.muted}${placeholder}${RESET}`
168
+ } else {
169
+ line = `${labelStr}: ${displayValue}`
170
+ }
171
+ } else {
172
+ line = `${labelStr}: ${displayValue || placeholder}`
173
+ }
174
+
175
+ lines.push(line)
176
+ }
177
+
178
+ // For textarea, value may have multiple lines
179
+ if (type === 'textarea' && typeof value === 'string' && value.includes('\n')) {
180
+ // Already handled in displayValue
181
+ }
182
+ }
183
+
184
+ // Helper text
185
+ if (helperText) {
186
+ if (tier === 'ansi' || tier === 'interactive') {
187
+ lines.push(` ${theme.muted}${helperText}${RESET}`)
188
+ } else {
189
+ lines.push(` ${helperText}`)
190
+ }
191
+ }
192
+
193
+ // Error message (rendered below value)
194
+ if (error) {
195
+ if (tier === 'ansi' || tier === 'interactive') {
196
+ lines.push(` ${theme.error}${error}${RESET}`)
197
+ } else if (tier === 'markdown') {
198
+ lines.push(` *${error}*`)
199
+ } else {
200
+ lines.push(` ${error}`)
201
+ }
202
+ }
203
+
204
+ // Interactive tier keyboard hints
205
+ if (tier === 'interactive' && focused) {
206
+ lines.push(` [Tab: Next | Enter: Confirm | Esc: Cancel]`)
207
+ }
208
+
209
+ return lines.join('\n')
210
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Form Component Renderer
3
+ *
4
+ * Renders form components with fields, validation, and submit buttons
5
+ * across all 6 render tiers.
6
+ */
7
+ import type { UINode, RenderContext } from '../../core/types'
8
+ import {
9
+ buildBox,
10
+ ASCII_BOX_CHARS,
11
+ UNICODE_SINGLE_BOX_CHARS,
12
+ getProp,
13
+ SPINNER_FRAMES,
14
+ } from '../utils'
15
+
16
+ const RESET = '\x1b[0m'
17
+ const DIM = '\x1b[2m'
18
+
19
+ // ============================================================================
20
+ // Types
21
+ // ============================================================================
22
+
23
+ interface FormField {
24
+ name: string
25
+ label?: string
26
+ type?: string
27
+ value?: unknown
28
+ placeholder?: string
29
+ required?: boolean
30
+ error?: string
31
+ valid?: boolean
32
+ disabled?: boolean
33
+ readonly?: boolean
34
+ helperText?: string
35
+ focused?: boolean
36
+ cursorPosition?: number
37
+ options?: Array<{ label: string; value: string }>
38
+ }
39
+
40
+ interface FormGroup {
41
+ title: string
42
+ fields: FormField[]
43
+ collapsible?: boolean
44
+ collapsed?: boolean
45
+ }
46
+
47
+ // ============================================================================
48
+ // Field Rendering Helpers
49
+ // ============================================================================
50
+
51
+ function maskPassword(value: string, tier: string): string {
52
+ const maskChar = tier === 'unicode' || tier === 'ansi' || tier === 'interactive' ? '•' : '*'
53
+ return maskChar.repeat(value.length)
54
+ }
55
+
56
+ function renderCheckbox(checked: boolean, tier: string): string {
57
+ if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
58
+ return checked ? '[✓]' : '[ ]'
59
+ }
60
+ return checked ? '[x]' : '[ ]'
61
+ }
62
+
63
+ function renderFieldValue(field: FormField, tier: string): string {
64
+ const { type, value, placeholder } = field
65
+
66
+ if (value === null || value === undefined || value === '') {
67
+ return placeholder || ''
68
+ }
69
+
70
+ if (type === 'password' && typeof value === 'string') {
71
+ return maskPassword(value, tier)
72
+ }
73
+
74
+ if (type === 'checkbox') {
75
+ return '' // Checkbox rendering handled separately
76
+ }
77
+
78
+ if (type === 'select' && field.options) {
79
+ const selected = field.options.find((opt) => opt.value === value)
80
+ return selected?.label || String(value)
81
+ }
82
+
83
+ return String(value)
84
+ }
85
+
86
+ function renderSingleField(
87
+ field: FormField,
88
+ ctx: RenderContext
89
+ ): string {
90
+ const { tier, theme } = ctx
91
+ const lines: string[] = []
92
+ const label = field.label || field.name
93
+
94
+ // Build label with required indicator
95
+ let labelStr = label
96
+ if (field.required) {
97
+ labelStr += ' *'
98
+ }
99
+
100
+ // Render checkbox differently
101
+ if (field.type === 'checkbox') {
102
+ const checkbox = renderCheckbox(!!field.value, tier)
103
+ let line = `${checkbox} ${label}`
104
+ if (field.required) {
105
+ line += ' *'
106
+ }
107
+ if (tier === 'ansi' || tier === 'interactive') {
108
+ if (field.disabled) {
109
+ line = `${DIM}${line}${RESET}`
110
+ } else if (field.focused) {
111
+ line = `${theme.primary}${line}${RESET}`
112
+ }
113
+ }
114
+ lines.push(line)
115
+ } else {
116
+ // Regular field
117
+ const value = renderFieldValue(field, tier)
118
+ let displayValue = value
119
+
120
+ // Interactive tier: show cursor
121
+ if (tier === 'interactive' && field.focused && typeof field.cursorPosition === 'number') {
122
+ const pos = field.cursorPosition
123
+ const before = displayValue.slice(0, pos)
124
+ const after = displayValue.slice(pos)
125
+ displayValue = `${before}\x1b[7m \x1b[0m${after}`
126
+ }
127
+
128
+ if (tier === 'markdown') {
129
+ // Markdown format
130
+ lines.push(`**${labelStr}**: ${displayValue || `_${field.placeholder || ''}_`}`)
131
+ } else {
132
+ // Text/ASCII/Unicode/ANSI format
133
+ let line = `${labelStr}: ${displayValue || field.placeholder || ''}`
134
+
135
+ if (tier === 'ansi' || tier === 'interactive') {
136
+ if (field.disabled) {
137
+ line = `${DIM}${labelStr}: ${displayValue || field.placeholder || ''}${RESET}`
138
+ } else if (field.focused) {
139
+ line = `${theme.primary}${labelStr}${RESET}: ${displayValue || `${theme.muted}${field.placeholder || ''}${RESET}`}`
140
+ } else if (field.valid) {
141
+ line = `${theme.success}${labelStr}${RESET}: ${displayValue}`
142
+ } else if (!displayValue && field.placeholder) {
143
+ line = `${labelStr}: ${theme.muted}${field.placeholder}${RESET}`
144
+ }
145
+ }
146
+
147
+ lines.push(line)
148
+ }
149
+ }
150
+
151
+ // Helper text
152
+ if (field.helperText) {
153
+ if (tier === 'ansi' || tier === 'interactive') {
154
+ lines.push(` ${theme.muted}${field.helperText}${RESET}`)
155
+ } else {
156
+ lines.push(` ${field.helperText}`)
157
+ }
158
+ }
159
+
160
+ // Error message
161
+ if (field.error) {
162
+ if (tier === 'ansi' || tier === 'interactive') {
163
+ lines.push(` ${theme.error}${field.error}${RESET}`)
164
+ } else if (tier === 'markdown') {
165
+ lines.push(` *${field.error}*`)
166
+ } else {
167
+ lines.push(` ${field.error}`)
168
+ }
169
+ }
170
+
171
+ return lines.join('\n')
172
+ }
173
+
174
+ // ============================================================================
175
+ // Main Renderer
176
+ // ============================================================================
177
+
178
+ export function renderForm(node: UINode, ctx: RenderContext): string {
179
+ const { tier, theme } = ctx
180
+ const props = node.props ?? {}
181
+
182
+ const fields = getProp<FormField[]>(props, 'fields', [])
183
+ const groups = getProp<FormGroup[]>(props, 'groups', [])
184
+ const title = getProp<string>(props, 'title', '')
185
+ const description = getProp<string>(props, 'description', '')
186
+ const error = getProp<string>(props, 'error', '')
187
+ const success = getProp<string>(props, 'success', '')
188
+ const loading = getProp<boolean>(props, 'loading', false)
189
+ const submitLabel = getProp<string>(props, 'submitLabel', '')
190
+ const cancelLabel = getProp<string>(props, 'cancelLabel', '')
191
+ const submitDisabled = getProp<boolean>(props, 'submitDisabled', false)
192
+ const submitHotkey = getProp<string>(props, 'submitHotkey', '')
193
+ const border = getProp<boolean>(props, 'border', false)
194
+
195
+ const lines: string[] = []
196
+
197
+ // Title
198
+ if (title) {
199
+ if (tier === 'markdown') {
200
+ lines.push(`## ${title}`)
201
+ } else {
202
+ lines.push(title)
203
+ }
204
+ }
205
+
206
+ // Description
207
+ if (description) {
208
+ lines.push(description)
209
+ lines.push('')
210
+ }
211
+
212
+ // Form-level error
213
+ if (error) {
214
+ if (tier === 'ansi' || tier === 'interactive') {
215
+ lines.push(`${theme.error}${error}${RESET}`)
216
+ } else {
217
+ lines.push(error)
218
+ }
219
+ lines.push('')
220
+ }
221
+
222
+ // Success message
223
+ if (success) {
224
+ if (tier === 'ansi' || tier === 'interactive') {
225
+ lines.push(`${theme.success}${success}${RESET}`)
226
+ } else {
227
+ lines.push(success)
228
+ }
229
+ lines.push('')
230
+ }
231
+
232
+ // Loading state
233
+ if (loading) {
234
+ const spinner = tier === 'unicode' || tier === 'ansi' || tier === 'interactive'
235
+ ? SPINNER_FRAMES[0]
236
+ : '...'
237
+ lines.push(`${spinner} Submitting...`)
238
+ }
239
+
240
+ // Render groups
241
+ for (const group of groups) {
242
+ if (group.collapsible && group.collapsed) {
243
+ // Just show title, hide fields
244
+ if (tier === 'ascii') {
245
+ lines.push(`+ ${group.title}`)
246
+ } else if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
247
+ lines.push(`▸ ${group.title}`)
248
+ } else {
249
+ lines.push(`[+] ${group.title}`)
250
+ }
251
+ continue
252
+ }
253
+
254
+ // Show group title
255
+ if (tier === 'ascii') {
256
+ lines.push(`+-- ${group.title} --+`)
257
+ } else if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
258
+ lines.push(`┌── ${group.title} ──┐`)
259
+ } else if (tier === 'markdown') {
260
+ lines.push(`### ${group.title}`)
261
+ } else {
262
+ lines.push(group.title)
263
+ }
264
+
265
+ // Render group fields
266
+ for (const field of group.fields) {
267
+ lines.push(renderSingleField(field, ctx))
268
+ }
269
+ lines.push('')
270
+ }
271
+
272
+ // Render standalone fields (not in groups)
273
+ for (const field of fields) {
274
+ lines.push(renderSingleField(field, ctx))
275
+ }
276
+
277
+ // Submit/Cancel buttons
278
+ if (submitLabel || cancelLabel) {
279
+ lines.push('')
280
+ const buttons: string[] = []
281
+
282
+ if (submitLabel) {
283
+ let btn = submitLabel
284
+ if (tier === 'interactive' && submitHotkey) {
285
+ btn += ` (${submitHotkey})`
286
+ }
287
+ if (submitDisabled && (tier === 'ansi' || tier === 'interactive')) {
288
+ btn = `${DIM}[${btn}]${RESET}`
289
+ } else if (tier === 'ascii' || tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
290
+ btn = `[${btn}]`
291
+ }
292
+ buttons.push(btn)
293
+ }
294
+
295
+ if (cancelLabel) {
296
+ let btn = cancelLabel
297
+ if (tier === 'ascii' || tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
298
+ btn = `[${btn}]`
299
+ }
300
+ buttons.push(btn)
301
+ }
302
+
303
+ lines.push(buttons.join(' '))
304
+ }
305
+
306
+ // Interactive hints
307
+ if (tier === 'interactive') {
308
+ lines.push('')
309
+ lines.push('Tab: Next field | Enter: Submit | ↑↓: Navigate')
310
+ }
311
+
312
+ // Build result
313
+ let result = lines.join('\n')
314
+
315
+ // Apply border if requested
316
+ if (border) {
317
+ const contentLines = result.split('\n')
318
+ const maxWidth = Math.max(...contentLines.map(l => l.length), 20)
319
+ const boxChars = tier === 'unicode' || tier === 'ansi' || tier === 'interactive'
320
+ ? UNICODE_SINGLE_BOX_CHARS
321
+ : ASCII_BOX_CHARS
322
+ const boxLines = buildBox(boxChars, contentLines, maxWidth + 4)
323
+ result = boxLines.join('\n')
324
+ }
325
+
326
+ return result
327
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @mdxui/terminal Component Renderers
3
+ *
4
+ * Individual component renderers for input components that work across
5
+ * all 6 render tiers (TEXT, MARKDOWN, ASCII, UNICODE, ANSI, INTERACTIVE).
6
+ *
7
+ * Each renderer function accepts a UINode and RenderContext and returns
8
+ * a string representation appropriate for the specified tier.
9
+ */
10
+
11
+ // Field Component - Individual input field renderer
12
+ export { renderField } from './field'
13
+
14
+ // Select Component - Dropdown/select input renderer
15
+ export { renderSelect } from './select'
16
+
17
+ // Form Component - Form container with fields, validation, and submit
18
+ export { renderForm } from './form'
19
+
20
+ // Search Component - Search input with suggestions and results
21
+ export { renderSearch } from './search'