@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,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'
|