@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.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- 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
|
+
}
|