@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,449 @@
1
+ /**
2
+ * Search Component Renderer
3
+ *
4
+ * Renders search input with suggestions, results, and filtering capabilities
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
+ const BOLD = '\x1b[1m'
19
+ const REVERSE = '\x1b[7m'
20
+
21
+ // ============================================================================
22
+ // Types
23
+ // ============================================================================
24
+
25
+ interface Suggestion {
26
+ label: string
27
+ value: string
28
+ description?: string
29
+ icon?: string
30
+ }
31
+
32
+ interface SuggestionGroup {
33
+ label: string
34
+ suggestions: Suggestion[]
35
+ }
36
+
37
+ interface SearchResult {
38
+ id: string
39
+ title: string
40
+ subtitle?: string
41
+ type?: string
42
+ }
43
+
44
+ interface Category {
45
+ label: string
46
+ count: number
47
+ }
48
+
49
+ interface QuickAction {
50
+ label: string
51
+ value: string
52
+ shortcut?: string
53
+ }
54
+
55
+ interface Command {
56
+ label: string
57
+ value: string
58
+ shortcut?: string
59
+ }
60
+
61
+ interface CommandGroup {
62
+ label: string
63
+ commands: Command[]
64
+ }
65
+
66
+ interface Scope {
67
+ label: string
68
+ value: string
69
+ }
70
+
71
+ // ============================================================================
72
+ // Rendering Helpers
73
+ // ============================================================================
74
+
75
+ function getSearchIcon(tier: string): string {
76
+ if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
77
+ return '🔍'
78
+ }
79
+ return 'Search:'
80
+ }
81
+
82
+ function highlightMatch(text: string, query: string, tier: string): string {
83
+ if (!query || !text) return text
84
+
85
+ const lowerText = text.toLowerCase()
86
+ const lowerQuery = query.toLowerCase()
87
+ const index = lowerText.indexOf(lowerQuery)
88
+
89
+ if (index === -1) return text
90
+
91
+ const before = text.slice(0, index)
92
+ const match = text.slice(index, index + query.length)
93
+ const after = text.slice(index + query.length)
94
+
95
+ if (tier === 'ansi' || tier === 'interactive') {
96
+ return `${before}${BOLD}${match}${RESET}${after}`
97
+ } else if (tier === 'markdown') {
98
+ return `${before}**${match}**${after}`
99
+ }
100
+
101
+ return text
102
+ }
103
+
104
+ function renderValueWithCursor(value: string, cursorPosition: number): string {
105
+ const pos = Math.min(cursorPosition, value.length)
106
+ const before = value.slice(0, pos)
107
+ const after = value.slice(pos)
108
+ return `${before}${REVERSE} ${RESET}${after}`
109
+ }
110
+
111
+ function renderValueWithSelection(
112
+ value: string,
113
+ selectionStart: number,
114
+ selectionEnd: number
115
+ ): string {
116
+ const before = value.slice(0, selectionStart)
117
+ const selected = value.slice(selectionStart, selectionEnd)
118
+ const after = value.slice(selectionEnd)
119
+ return `${before}${REVERSE}${selected}${RESET}${after}`
120
+ }
121
+
122
+ // ============================================================================
123
+ // Main Renderer
124
+ // ============================================================================
125
+
126
+ export function renderSearch(node: UINode, ctx: RenderContext): string {
127
+ const { tier, theme } = ctx
128
+ const props = node.props ?? {}
129
+
130
+ // Basic props
131
+ const value = getProp<string>(props, 'value', '')
132
+ const placeholder = getProp<string>(props, 'placeholder', '')
133
+ const label = getProp<string>(props, 'label', '')
134
+ const disabled = getProp<boolean>(props, 'disabled', false)
135
+ const loading = getProp<boolean>(props, 'loading', false)
136
+ const error = getProp<string>(props, 'error', '')
137
+ const border = getProp<boolean>(props, 'border', false)
138
+ const focused = getProp<boolean>(props, 'focused', false)
139
+ const cursorPosition = getProp<number | undefined>(props, 'cursorPosition', undefined)
140
+ const selectionStart = getProp<number | undefined>(props, 'selectionStart', undefined)
141
+ const selectionEnd = getProp<number | undefined>(props, 'selectionEnd', undefined)
142
+ const ariaLabel = getProp<string>(props, 'ariaLabel', '')
143
+
144
+ // Suggestions
145
+ const suggestions = getProp<Suggestion[] | null>(props, 'suggestions', null)
146
+ const suggestionGroups = getProp<SuggestionGroup[] | undefined>(props, 'suggestionGroups', undefined)
147
+ const showSuggestions = getProp<boolean>(props, 'showSuggestions', false)
148
+ const highlightedIndex = getProp<number>(props, 'highlightedIndex', -1)
149
+ const maxSuggestions = getProp<number>(props, 'maxSuggestions', 10)
150
+ const maxVisibleSuggestions = getProp<number>(props, 'maxVisibleSuggestions', 10)
151
+ const highlightMatches = getProp<boolean>(props, 'highlightMatches', false)
152
+
153
+ // Results
154
+ const results = getProp<SearchResult[] | undefined>(props, 'results', undefined)
155
+ const showResults = getProp<boolean>(props, 'showResults', false)
156
+ const totalResults = getProp<number | undefined>(props, 'totalResults', undefined)
157
+
158
+ // Categories
159
+ const categories = getProp<Category[] | undefined>(props, 'categories', undefined)
160
+
161
+ // Recent searches
162
+ const recentSearches = getProp<string[] | undefined>(props, 'recentSearches', undefined)
163
+ const showRecentSearches = getProp<boolean>(props, 'showRecentSearches', false)
164
+
165
+ // Quick actions
166
+ const quickActions = getProp<QuickAction[] | undefined>(props, 'quickActions', undefined)
167
+ const showQuickActions = getProp<boolean>(props, 'showQuickActions', false)
168
+
169
+ // Commands (command palette mode)
170
+ const commands = getProp<Command[] | undefined>(props, 'commands', undefined)
171
+ const filteredCommands = getProp<Command[] | undefined>(props, 'filteredCommands', undefined)
172
+ const commandGroups = getProp<CommandGroup[] | undefined>(props, 'commandGroups', undefined)
173
+ const showCommands = getProp<boolean>(props, 'showCommands', false)
174
+ const mode = getProp<string>(props, 'mode', 'inline')
175
+
176
+ // Voice search
177
+ const voiceEnabled = getProp<boolean>(props, 'voiceEnabled', false)
178
+ const voiceListening = getProp<boolean>(props, 'voiceListening', false)
179
+
180
+ // Scope
181
+ const scopeLabel = getProp<string>(props, 'scopeLabel', '')
182
+ const scopes = getProp<Scope[] | undefined>(props, 'scopes', undefined)
183
+ const showScopes = getProp<boolean>(props, 'showScopes', false)
184
+
185
+ // Typing/debounce
186
+ const isTyping = getProp<boolean>(props, 'isTyping', false)
187
+
188
+ const lines: string[] = []
189
+
190
+ // Label
191
+ if (label) {
192
+ lines.push(label)
193
+ }
194
+
195
+ // Scope indicator
196
+ if (scopeLabel) {
197
+ lines.push(`[${scopeLabel}]`)
198
+ }
199
+
200
+ // Scopes options
201
+ if (showScopes && scopes && scopes.length > 0) {
202
+ const scopeStrings = scopes.map(s => s.label)
203
+ lines.push(`Scopes: ${scopeStrings.join(' | ')}`)
204
+ }
205
+
206
+ // Search input line
207
+ const searchIcon = getSearchIcon(tier)
208
+ let displayValue = value
209
+
210
+ // Handle cursor and selection for interactive tier
211
+ if (tier === 'interactive' && focused) {
212
+ if (typeof selectionStart === 'number' && typeof selectionEnd === 'number' && selectionStart !== selectionEnd) {
213
+ displayValue = renderValueWithSelection(value, selectionStart, selectionEnd)
214
+ } else if (typeof cursorPosition === 'number') {
215
+ displayValue = renderValueWithCursor(value, cursorPosition)
216
+ }
217
+ }
218
+
219
+ let inputLine = `${searchIcon} ${displayValue || placeholder}`
220
+
221
+ // Voice indicator
222
+ if (voiceEnabled && tier === 'interactive') {
223
+ if (voiceListening) {
224
+ inputLine += ' 🎤 Listening...'
225
+ } else {
226
+ inputLine += ' [🎤 Voice]'
227
+ }
228
+ }
229
+
230
+ // Clear hint for interactive tier
231
+ if (tier === 'interactive' && value) {
232
+ inputLine += ' [Esc: Clear]'
233
+ }
234
+
235
+ // Submit hint for interactive tier
236
+ if (tier === 'interactive') {
237
+ inputLine += ' [Enter: Search]'
238
+ }
239
+
240
+ lines.push(inputLine)
241
+
242
+ // Typing indicator
243
+ if (isTyping && tier === 'interactive') {
244
+ lines.push('Typing...')
245
+ }
246
+
247
+ // Loading state
248
+ if (loading) {
249
+ const spinner = tier === 'unicode' || tier === 'ansi' || tier === 'interactive'
250
+ ? SPINNER_FRAMES[0]
251
+ : '...'
252
+ lines.push(`${spinner} Searching...`)
253
+ }
254
+
255
+ // Error message
256
+ if (error) {
257
+ if (tier === 'ansi' || tier === 'interactive') {
258
+ lines.push(`${theme.error}${error}${RESET}`)
259
+ } else {
260
+ lines.push(error)
261
+ }
262
+ }
263
+
264
+ // Categories/filters
265
+ if (categories && categories.length > 0 && showResults) {
266
+ const catStrings = categories.map(c => `${c.label} (${c.count})`)
267
+ lines.push(catStrings.join(' | '))
268
+ }
269
+
270
+ // Recent searches
271
+ if (showRecentSearches && recentSearches && recentSearches.length > 0) {
272
+ lines.push('')
273
+ lines.push('Recent Searches:')
274
+ for (const recent of recentSearches) {
275
+ lines.push(` ${recent}`)
276
+ }
277
+ lines.push(' [Clear recent searches]')
278
+ }
279
+
280
+ // Quick actions
281
+ if (showQuickActions && quickActions && quickActions.length > 0) {
282
+ lines.push('')
283
+ for (const action of quickActions) {
284
+ let actionLine = ` ${action.label}`
285
+ if (action.shortcut) {
286
+ actionLine += ` (${action.shortcut})`
287
+ }
288
+ lines.push(actionLine)
289
+ }
290
+ }
291
+
292
+ // Command palette mode
293
+ if (mode === 'command-palette' && showCommands) {
294
+ // Command groups
295
+ if (commandGroups && commandGroups.length > 0) {
296
+ for (const group of commandGroups) {
297
+ lines.push('')
298
+ lines.push(group.label)
299
+ for (const cmd of group.commands) {
300
+ let cmdLine = ` ${cmd.label}`
301
+ if (cmd.shortcut) {
302
+ cmdLine += ` (${cmd.shortcut})`
303
+ }
304
+ lines.push(cmdLine)
305
+ }
306
+ }
307
+ } else {
308
+ // Filtered or regular commands
309
+ const cmdsToShow = filteredCommands || commands || []
310
+ for (const cmd of cmdsToShow) {
311
+ let cmdLine = ` ${cmd.label}`
312
+ if (cmd.shortcut) {
313
+ cmdLine += ` (${cmd.shortcut})`
314
+ }
315
+ lines.push(cmdLine)
316
+ }
317
+ }
318
+ }
319
+
320
+ // Suggestions
321
+ if (showSuggestions) {
322
+ // Grouped suggestions
323
+ if (suggestionGroups && suggestionGroups.length > 0) {
324
+ for (const group of suggestionGroups) {
325
+ lines.push('')
326
+ lines.push(group.label)
327
+ for (const sug of group.suggestions) {
328
+ let sugLine = ` ${sug.label}`
329
+ if (sug.description) {
330
+ sugLine += ` - ${sug.description}`
331
+ }
332
+ lines.push(sugLine)
333
+ }
334
+ }
335
+ } else if (suggestions !== null) {
336
+ if (suggestions.length === 0) {
337
+ lines.push('No results found')
338
+ } else {
339
+ const sugLines: string[] = []
340
+ const limitedSuggestions = suggestions.slice(0, maxSuggestions)
341
+
342
+ limitedSuggestions.forEach((sug, index) => {
343
+ let sugLabel = sug.label
344
+
345
+ // Highlight match if enabled
346
+ if (highlightMatches && value) {
347
+ sugLabel = highlightMatch(sugLabel, value, tier)
348
+ }
349
+
350
+ let sugLine = ` ${sugLabel}`
351
+ if (sug.description) {
352
+ sugLine += ` - ${sug.description}`
353
+ }
354
+
355
+ // Highlight current selection
356
+ if (tier === 'interactive' && index === highlightedIndex) {
357
+ sugLine = `${theme.primary}> ${sugLabel}${RESET}`
358
+ if (sug.description) {
359
+ sugLine += ` - ${sug.description}`
360
+ }
361
+ }
362
+
363
+ sugLines.push(sugLine)
364
+ })
365
+
366
+ // Accessibility: show position indicator
367
+ if (tier === 'interactive' && highlightedIndex >= 0 && limitedSuggestions.length > 0) {
368
+ const position = highlightedIndex + 1
369
+ const total = limitedSuggestions.length
370
+ lines.push(`(${position} of ${total})`)
371
+ }
372
+
373
+ // Scroll indicator for long lists
374
+ if (tier === 'interactive' && suggestions.length > maxVisibleSuggestions) {
375
+ lines.push('↓ more...')
376
+ }
377
+
378
+ // Apply box for ascii/unicode tiers
379
+ if ((tier === 'ascii' || tier === 'unicode') && sugLines.length > 0) {
380
+ const boxChars = tier === 'unicode' ? UNICODE_SINGLE_BOX_CHARS : ASCII_BOX_CHARS
381
+ const maxWidth = Math.max(...sugLines.map(l => l.length), 10)
382
+ const boxLines = buildBox(boxChars, sugLines.map(l => l.trimStart()), maxWidth + 4)
383
+ lines.push(...boxLines)
384
+ } else {
385
+ lines.push(...sugLines)
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ // Results
392
+ if (showResults) {
393
+ // Total results count
394
+ if (totalResults !== undefined) {
395
+ lines.push(`${totalResults} results found`)
396
+ }
397
+
398
+ if (results !== undefined) {
399
+ if (results.length === 0) {
400
+ lines.push('No results found')
401
+ } else {
402
+ for (const result of results) {
403
+ let resultTitle = result.title
404
+
405
+ // Highlight match if enabled
406
+ if (highlightMatches && value) {
407
+ resultTitle = highlightMatch(resultTitle, value, tier)
408
+ }
409
+
410
+ let resultLine = ` ${resultTitle}`
411
+ if (result.subtitle) {
412
+ resultLine += ` - ${result.subtitle}`
413
+ }
414
+ if (result.type) {
415
+ resultLine += ` [${result.type}]`
416
+ }
417
+ lines.push(resultLine)
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ // Interactive keyboard hints
424
+ if (tier === 'interactive' && showSuggestions && suggestions && suggestions.length > 0) {
425
+ lines.push('')
426
+ lines.push('↑↓: Navigate | Enter: Select | Esc: Close | Tab: Next')
427
+ }
428
+
429
+ // Build result
430
+ let result = lines.join('\n')
431
+
432
+ // Apply border if requested
433
+ if (border) {
434
+ const contentLines = result.split('\n')
435
+ const maxWidth = Math.max(...contentLines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length), 20)
436
+ const boxChars = tier === 'unicode' || tier === 'ansi' || tier === 'interactive'
437
+ ? UNICODE_SINGLE_BOX_CHARS
438
+ : ASCII_BOX_CHARS
439
+ const boxLines = buildBox(boxChars, contentLines, maxWidth + 4)
440
+ result = boxLines.join('\n')
441
+ }
442
+
443
+ // Disabled state wrapper
444
+ if (disabled && (tier === 'ansi' || tier === 'interactive')) {
445
+ result = `${DIM}${result}${RESET}`
446
+ }
447
+
448
+ return result
449
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Select Component Renderer
3
+ *
4
+ * Renders dropdown/list selection with options and keyboard navigation
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 SelectOption {
24
+ label: string
25
+ value: string
26
+ disabled?: boolean
27
+ }
28
+
29
+ // ============================================================================
30
+ // Rendering Helpers
31
+ // ============================================================================
32
+
33
+ function renderDropdownIndicator(open: boolean, tier: string): string {
34
+ if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
35
+ return open ? '▾' : '▼'
36
+ }
37
+ return open ? 'v' : '>'
38
+ }
39
+
40
+ function renderOptionIndicator(
41
+ selected: boolean,
42
+ highlighted: boolean,
43
+ multiple: boolean,
44
+ tier: string
45
+ ): string {
46
+ if (multiple) {
47
+ // Checkbox style for multi-select
48
+ if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
49
+ return selected ? '☑' : '☐'
50
+ }
51
+ return selected ? '[x]' : '[ ]'
52
+ }
53
+
54
+ // Radio style for single select
55
+ if (selected) {
56
+ if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
57
+ return '●'
58
+ }
59
+ return '*'
60
+ }
61
+
62
+ if (highlighted) {
63
+ if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
64
+ return '>'
65
+ }
66
+ return '>'
67
+ }
68
+
69
+ return ' '
70
+ }
71
+
72
+ // ============================================================================
73
+ // Main Renderer
74
+ // ============================================================================
75
+
76
+ export function renderSelect(node: UINode, ctx: RenderContext): string {
77
+ const { tier, theme } = ctx
78
+ const props = node.props ?? {}
79
+
80
+ const options = getProp<SelectOption[]>(props, 'options', [])
81
+ const value = props.value
82
+ const label = getProp<string>(props, 'label', '')
83
+ const placeholder = getProp<string>(props, 'placeholder', '')
84
+ const required = getProp<boolean>(props, 'required', false)
85
+ const error = getProp<string>(props, 'error', '')
86
+ const valid = getProp<boolean>(props, 'valid', false)
87
+ const disabled = getProp<boolean>(props, 'disabled', false)
88
+ const loading = getProp<boolean>(props, 'loading', false)
89
+ const open = getProp<boolean>(props, 'open', true) // Default to showing options
90
+ const multiple = getProp<boolean>(props, 'multiple', false)
91
+ const highlightedIndex = getProp<number>(props, 'highlightedIndex', -1)
92
+ const searchable = getProp<boolean>(props, 'searchable', false)
93
+ const searchValue = getProp<string>(props, 'searchValue', '')
94
+ const filteredOptions = props.filteredOptions as SelectOption[] | undefined
95
+
96
+ const lines: string[] = []
97
+
98
+ // Label with required indicator
99
+ if (label) {
100
+ let labelStr = label
101
+ if (required) {
102
+ labelStr += ' *'
103
+ }
104
+ lines.push(labelStr)
105
+ }
106
+
107
+ // Loading state
108
+ if (loading) {
109
+ const spinner = tier === 'unicode' || tier === 'ansi' || tier === 'interactive'
110
+ ? SPINNER_FRAMES[0]
111
+ : '...'
112
+ lines.push(`${spinner} Loading...`)
113
+ return lines.join('\n')
114
+ }
115
+
116
+ // Current selection display (for collapsed state or always showing selected)
117
+ const selectedValues = Array.isArray(value) ? value : (value !== null && value !== undefined ? [value] : [])
118
+ const selectedOptions = options.filter((opt) => selectedValues.includes(opt.value))
119
+ const selectedLabels = selectedOptions.map((opt) => opt.label)
120
+
121
+ if (!open) {
122
+ // Collapsed state - show only selected value
123
+ const displayText = selectedLabels.length > 0
124
+ ? selectedLabels.join(', ')
125
+ : placeholder || 'Select...'
126
+ const indicator = renderDropdownIndicator(false, tier)
127
+
128
+ let line = `${displayText} ${indicator}`
129
+ if (tier === 'ansi' || tier === 'interactive') {
130
+ if (disabled) {
131
+ line = `${DIM}${line}${RESET}`
132
+ }
133
+ }
134
+ lines.push(line)
135
+ } else {
136
+ // Expanded state - show all options
137
+ if (searchable && (tier === 'interactive')) {
138
+ lines.push(`Search: ${searchValue || ''}`)
139
+ }
140
+
141
+ const dropdownIndicator = renderDropdownIndicator(true, tier)
142
+ const headerText = selectedLabels.length > 0
143
+ ? selectedLabels.join(', ')
144
+ : placeholder || 'Select...'
145
+ lines.push(`${headerText} ${dropdownIndicator}`)
146
+
147
+ // Use filtered options if available
148
+ const displayOptions = filteredOptions !== undefined ? filteredOptions : options
149
+
150
+ if (displayOptions.length === 0) {
151
+ lines.push(' No results found')
152
+ } else {
153
+ const optionLines: string[] = []
154
+
155
+ for (let i = 0; i < displayOptions.length; i++) {
156
+ const opt = displayOptions[i]
157
+ const isSelected = selectedValues.includes(opt.value)
158
+ const isHighlighted = i === highlightedIndex
159
+ const indicator = renderOptionIndicator(isSelected, isHighlighted, multiple, tier)
160
+
161
+ let optionLine = ` ${indicator} ${opt.label}`
162
+
163
+ if (tier === 'ansi' || tier === 'interactive') {
164
+ if (opt.disabled) {
165
+ optionLine = `${DIM} ${indicator} ${opt.label}${RESET}`
166
+ } else if (isSelected) {
167
+ optionLine = `${theme.primary} ${indicator} ${opt.label}${RESET}`
168
+ } else if (isHighlighted) {
169
+ optionLine = `${theme.secondary} ${indicator} ${opt.label}${RESET}`
170
+ }
171
+ }
172
+
173
+ optionLines.push(optionLine)
174
+ }
175
+
176
+ // Apply box if ascii/unicode
177
+ if (tier === 'ascii') {
178
+ const boxChars = ASCII_BOX_CHARS
179
+ const maxWidth = Math.max(...optionLines.map(l => l.length), 10)
180
+ const boxLines = buildBox(boxChars, optionLines.map(l => l.trimStart()), maxWidth + 4)
181
+ lines.push(...boxLines)
182
+ } else if (tier === 'unicode' || tier === 'ansi' || tier === 'interactive') {
183
+ const boxChars = UNICODE_SINGLE_BOX_CHARS
184
+ const maxWidth = Math.max(...optionLines.map(l => l.length), 10)
185
+ const boxLines = buildBox(boxChars, optionLines.map(l => l.trimStart()), maxWidth + 4)
186
+ lines.push(...boxLines)
187
+ } else {
188
+ lines.push(...optionLines)
189
+ }
190
+ }
191
+ }
192
+
193
+ // Error message
194
+ if (error) {
195
+ if (tier === 'ansi' || tier === 'interactive') {
196
+ lines.push(`${theme.error}${error}${RESET}`)
197
+ } else {
198
+ lines.push(error)
199
+ }
200
+ }
201
+
202
+ // Valid state indicator
203
+ if (valid && !error) {
204
+ if (tier === 'ansi' || tier === 'interactive') {
205
+ lines.push(`${theme.success}✓${RESET}`)
206
+ }
207
+ }
208
+
209
+ // Interactive keyboard hints
210
+ if (tier === 'interactive' && open) {
211
+ lines.push('')
212
+ lines.push('↑↓: Navigate | Enter: Select | Esc: Close')
213
+ }
214
+
215
+ // Disabled state wrapper
216
+ let result = lines.join('\n')
217
+ if (disabled && (tier === 'ansi' || tier === 'interactive')) {
218
+ result = `${DIM}${result}${RESET}`
219
+ }
220
+
221
+ return result
222
+ }