@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,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Form Converters
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for converting between React Hook Form state
|
|
5
|
+
* and terminal UINode props format.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
FormValues,
|
|
12
|
+
FieldConfig,
|
|
13
|
+
FieldGroup,
|
|
14
|
+
FormState,
|
|
15
|
+
RHFFormState,
|
|
16
|
+
RHFFormConfig,
|
|
17
|
+
SelectState,
|
|
18
|
+
SelectOption,
|
|
19
|
+
SearchState,
|
|
20
|
+
FieldValue,
|
|
21
|
+
} from './types'
|
|
22
|
+
import type { UINode } from '../core/types'
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Form State Converters
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts RHF form state and field configurations to terminal FormState.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* This function bridges React Hook Form's state representation with
|
|
33
|
+
* the terminal form renderer's expected props format.
|
|
34
|
+
*
|
|
35
|
+
* @param rhfState - RHF form state from useForm hook
|
|
36
|
+
* @param fields - Array of field configurations
|
|
37
|
+
* @param options - Additional form options
|
|
38
|
+
* @returns FormState compatible with terminal Form renderer
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const { formState, register, getValues } = useForm<LoginForm>()
|
|
43
|
+
*
|
|
44
|
+
* const terminalFormState = convertRHFToFormState(
|
|
45
|
+
* formState,
|
|
46
|
+
* [
|
|
47
|
+
* { name: 'email', label: 'Email', type: 'email', required: true },
|
|
48
|
+
* { name: 'password', label: 'Password', type: 'password', required: true },
|
|
49
|
+
* ],
|
|
50
|
+
* {
|
|
51
|
+
* title: 'Login',
|
|
52
|
+
* submitLabel: 'Sign In',
|
|
53
|
+
* values: getValues(),
|
|
54
|
+
* }
|
|
55
|
+
* )
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function convertRHFToFormState<T extends FormValues>(
|
|
59
|
+
rhfState: RHFFormState,
|
|
60
|
+
fields: FieldConfig[],
|
|
61
|
+
options?: {
|
|
62
|
+
title?: string
|
|
63
|
+
description?: string
|
|
64
|
+
submitLabel?: string
|
|
65
|
+
cancelLabel?: string
|
|
66
|
+
submitHotkey?: string
|
|
67
|
+
border?: boolean
|
|
68
|
+
groups?: FieldGroup[]
|
|
69
|
+
values?: T
|
|
70
|
+
}
|
|
71
|
+
): FormState<T> {
|
|
72
|
+
// Merge field configs with RHF errors and touched state
|
|
73
|
+
const enhancedFields = fields.map((field) => {
|
|
74
|
+
const error = rhfState.errors[field.name]
|
|
75
|
+
const isTouched = rhfState.touchedFields[field.name]
|
|
76
|
+
const isDirty = rhfState.dirtyFields[field.name]
|
|
77
|
+
const value = options?.values?.[field.name as keyof T] as FieldValue | undefined
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
...field,
|
|
81
|
+
value: value ?? field.value,
|
|
82
|
+
error: error?.message ?? field.error,
|
|
83
|
+
valid: isTouched && !error && field.required ? true : field.valid,
|
|
84
|
+
// Mark as valid if touched without error and was required
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Process groups if provided
|
|
89
|
+
const enhancedGroups = options?.groups?.map((group) => ({
|
|
90
|
+
...group,
|
|
91
|
+
fields: group.fields.map((field) => {
|
|
92
|
+
const error = rhfState.errors[field.name]
|
|
93
|
+
const isTouched = rhfState.touchedFields[field.name]
|
|
94
|
+
const value = options?.values?.[field.name as keyof T] as FieldValue | undefined
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...field,
|
|
98
|
+
value: value ?? field.value,
|
|
99
|
+
error: error?.message ?? field.error,
|
|
100
|
+
valid: isTouched && !error && field.required ? true : field.valid,
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
title: options?.title,
|
|
107
|
+
description: options?.description,
|
|
108
|
+
fields: enhancedFields,
|
|
109
|
+
groups: enhancedGroups,
|
|
110
|
+
loading: rhfState.isSubmitting,
|
|
111
|
+
submitLabel: options?.submitLabel,
|
|
112
|
+
cancelLabel: options?.cancelLabel,
|
|
113
|
+
submitDisabled: !rhfState.isValid || rhfState.isSubmitting,
|
|
114
|
+
submitHotkey: options?.submitHotkey,
|
|
115
|
+
border: options?.border,
|
|
116
|
+
values: options?.values,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Converts field configurations to UINode format for Form renderer.
|
|
122
|
+
*
|
|
123
|
+
* @param formState - Terminal FormState configuration
|
|
124
|
+
* @returns UINode for Form component
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```tsx
|
|
128
|
+
* const formNode = formStateToUINode({
|
|
129
|
+
* title: 'Contact Us',
|
|
130
|
+
* fields: [
|
|
131
|
+
* { name: 'email', label: 'Email', type: 'email' },
|
|
132
|
+
* { name: 'message', label: 'Message', type: 'textarea' },
|
|
133
|
+
* ],
|
|
134
|
+
* submitLabel: 'Send',
|
|
135
|
+
* })
|
|
136
|
+
*
|
|
137
|
+
* const rendered = renderForm(formNode, ctx)
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function formStateToUINode<T extends FormValues>(
|
|
141
|
+
formState: FormState<T>
|
|
142
|
+
): UINode {
|
|
143
|
+
return {
|
|
144
|
+
type: 'form',
|
|
145
|
+
props: {
|
|
146
|
+
title: formState.title,
|
|
147
|
+
description: formState.description,
|
|
148
|
+
fields: formState.fields,
|
|
149
|
+
groups: formState.groups,
|
|
150
|
+
error: formState.error,
|
|
151
|
+
success: formState.success,
|
|
152
|
+
loading: formState.loading,
|
|
153
|
+
submitLabel: formState.submitLabel,
|
|
154
|
+
cancelLabel: formState.cancelLabel,
|
|
155
|
+
submitDisabled: formState.submitDisabled,
|
|
156
|
+
submitHotkey: formState.submitHotkey,
|
|
157
|
+
border: formState.border,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Field Converters
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Converts a single field configuration to UINode format.
|
|
168
|
+
*
|
|
169
|
+
* @param field - Field configuration
|
|
170
|
+
* @returns UINode for Field component
|
|
171
|
+
*/
|
|
172
|
+
export function fieldConfigToUINode(field: FieldConfig): UINode {
|
|
173
|
+
return {
|
|
174
|
+
type: 'field',
|
|
175
|
+
props: {
|
|
176
|
+
name: field.name,
|
|
177
|
+
label: field.label,
|
|
178
|
+
type: field.type,
|
|
179
|
+
value: field.value,
|
|
180
|
+
placeholder: field.placeholder,
|
|
181
|
+
required: field.required,
|
|
182
|
+
error: field.error,
|
|
183
|
+
valid: field.valid,
|
|
184
|
+
disabled: field.disabled,
|
|
185
|
+
readonly: field.readonly,
|
|
186
|
+
helperText: field.helperText,
|
|
187
|
+
focused: field.focused,
|
|
188
|
+
cursorPosition: field.cursorPosition,
|
|
189
|
+
selectionStart: field.selectionStart,
|
|
190
|
+
selectionEnd: field.selectionEnd,
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Creates a field configuration from RHF register result.
|
|
197
|
+
*
|
|
198
|
+
* @param name - Field name
|
|
199
|
+
* @param registerResult - Result from RHF register function
|
|
200
|
+
* @param options - Additional field options
|
|
201
|
+
* @returns FieldConfig for terminal rendering
|
|
202
|
+
*/
|
|
203
|
+
export function createFieldFromRegister(
|
|
204
|
+
name: string,
|
|
205
|
+
registerResult: { name: string },
|
|
206
|
+
options?: Omit<FieldConfig, 'name'>
|
|
207
|
+
): FieldConfig {
|
|
208
|
+
return {
|
|
209
|
+
name: registerResult.name,
|
|
210
|
+
label: options?.label ?? name.charAt(0).toUpperCase() + name.slice(1),
|
|
211
|
+
type: options?.type ?? 'text',
|
|
212
|
+
placeholder: options?.placeholder,
|
|
213
|
+
required: options?.required,
|
|
214
|
+
helperText: options?.helperText,
|
|
215
|
+
disabled: options?.disabled,
|
|
216
|
+
readonly: options?.readonly,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Select Converters
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Converts select state to UINode format.
|
|
226
|
+
*
|
|
227
|
+
* @param selectState - Select component state
|
|
228
|
+
* @returns UINode for Select component
|
|
229
|
+
*/
|
|
230
|
+
export function selectStateToUINode<T = string>(
|
|
231
|
+
selectState: SelectState<T>
|
|
232
|
+
): UINode {
|
|
233
|
+
return {
|
|
234
|
+
type: 'select',
|
|
235
|
+
props: {
|
|
236
|
+
options: selectState.options,
|
|
237
|
+
value: selectState.value,
|
|
238
|
+
label: selectState.label,
|
|
239
|
+
placeholder: selectState.placeholder,
|
|
240
|
+
required: selectState.required,
|
|
241
|
+
error: selectState.error,
|
|
242
|
+
valid: selectState.valid,
|
|
243
|
+
disabled: selectState.disabled,
|
|
244
|
+
open: selectState.open ?? true,
|
|
245
|
+
multiple: selectState.multiple,
|
|
246
|
+
highlightedIndex: selectState.highlightedIndex ?? -1,
|
|
247
|
+
searchable: selectState.searchable,
|
|
248
|
+
searchValue: selectState.searchValue,
|
|
249
|
+
filteredOptions: selectState.filteredOptions,
|
|
250
|
+
loading: selectState.loading,
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Creates a select field configuration for RHF Controller integration.
|
|
257
|
+
*
|
|
258
|
+
* @param name - Field name
|
|
259
|
+
* @param options - Select options
|
|
260
|
+
* @param fieldOptions - Additional field options
|
|
261
|
+
* @returns SelectState for terminal rendering
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```tsx
|
|
265
|
+
* // With RHF Controller
|
|
266
|
+
* <Controller
|
|
267
|
+
* name="country"
|
|
268
|
+
* control={control}
|
|
269
|
+
* render={({ field, fieldState }) => {
|
|
270
|
+
* const selectState = createSelectForController(
|
|
271
|
+
* 'country',
|
|
272
|
+
* [
|
|
273
|
+
* { label: 'United States', value: 'us' },
|
|
274
|
+
* { label: 'Canada', value: 'ca' },
|
|
275
|
+
* ],
|
|
276
|
+
* {
|
|
277
|
+
* value: field.value,
|
|
278
|
+
* error: fieldState.error?.message,
|
|
279
|
+
* }
|
|
280
|
+
* )
|
|
281
|
+
* const node = selectStateToUINode(selectState)
|
|
282
|
+
* return renderSelect(node, ctx)
|
|
283
|
+
* }}
|
|
284
|
+
* />
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export function createSelectForController<T = string>(
|
|
288
|
+
name: string,
|
|
289
|
+
options: SelectOption<T>[],
|
|
290
|
+
fieldOptions?: {
|
|
291
|
+
value?: T | T[]
|
|
292
|
+
error?: string
|
|
293
|
+
label?: string
|
|
294
|
+
placeholder?: string
|
|
295
|
+
required?: boolean
|
|
296
|
+
disabled?: boolean
|
|
297
|
+
multiple?: boolean
|
|
298
|
+
searchable?: boolean
|
|
299
|
+
}
|
|
300
|
+
): SelectState<T> {
|
|
301
|
+
return {
|
|
302
|
+
options,
|
|
303
|
+
value: fieldOptions?.value,
|
|
304
|
+
label: fieldOptions?.label ?? name.charAt(0).toUpperCase() + name.slice(1),
|
|
305
|
+
placeholder: fieldOptions?.placeholder ?? `Select ${name}...`,
|
|
306
|
+
required: fieldOptions?.required,
|
|
307
|
+
error: fieldOptions?.error,
|
|
308
|
+
disabled: fieldOptions?.disabled,
|
|
309
|
+
multiple: fieldOptions?.multiple,
|
|
310
|
+
searchable: fieldOptions?.searchable,
|
|
311
|
+
open: true,
|
|
312
|
+
highlightedIndex: -1,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// Search Converters
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Converts search state to UINode format.
|
|
322
|
+
*
|
|
323
|
+
* @param searchState - Search component state
|
|
324
|
+
* @returns UINode for Search component
|
|
325
|
+
*/
|
|
326
|
+
export function searchStateToUINode(searchState: SearchState): UINode {
|
|
327
|
+
return {
|
|
328
|
+
type: 'search',
|
|
329
|
+
props: {
|
|
330
|
+
value: searchState.value,
|
|
331
|
+
placeholder: searchState.placeholder,
|
|
332
|
+
label: searchState.label,
|
|
333
|
+
disabled: searchState.disabled,
|
|
334
|
+
loading: searchState.loading,
|
|
335
|
+
error: searchState.error,
|
|
336
|
+
focused: searchState.focused,
|
|
337
|
+
cursorPosition: searchState.cursorPosition,
|
|
338
|
+
suggestions: searchState.suggestions,
|
|
339
|
+
showSuggestions: searchState.showSuggestions,
|
|
340
|
+
highlightedIndex: searchState.highlightedIndex ?? -1,
|
|
341
|
+
maxSuggestions: searchState.maxSuggestions ?? 10,
|
|
342
|
+
highlightMatches: searchState.highlightMatches,
|
|
343
|
+
isTyping: searchState.isTyping,
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Creates a debounced search state handler.
|
|
350
|
+
*
|
|
351
|
+
* @param onSearch - Callback when search should be executed
|
|
352
|
+
* @param delay - Debounce delay in milliseconds (default: 300)
|
|
353
|
+
* @returns Object with handlers for search input
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```tsx
|
|
357
|
+
* const { handleChange, handleFocus, handleBlur, state } = createDebouncedSearch(
|
|
358
|
+
* async (query) => {
|
|
359
|
+
* const results = await fetchSuggestions(query)
|
|
360
|
+
* return results.map(r => ({ label: r.name, value: r.id }))
|
|
361
|
+
* }
|
|
362
|
+
* )
|
|
363
|
+
*
|
|
364
|
+
* // Use with RHF Controller
|
|
365
|
+
* <Controller
|
|
366
|
+
* name="search"
|
|
367
|
+
* control={control}
|
|
368
|
+
* render={({ field }) => {
|
|
369
|
+
* const searchNode = searchStateToUINode({
|
|
370
|
+
* value: field.value,
|
|
371
|
+
* ...state,
|
|
372
|
+
* })
|
|
373
|
+
* return renderSearch(searchNode, ctx)
|
|
374
|
+
* }}
|
|
375
|
+
* />
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
export function createDebouncedSearch(
|
|
379
|
+
onSearch: (query: string) => Promise<{ label: string; value: string }[]> | { label: string; value: string }[],
|
|
380
|
+
delay: number = 300
|
|
381
|
+
): {
|
|
382
|
+
handleChange: (value: string) => void
|
|
383
|
+
handleFocus: () => void
|
|
384
|
+
handleBlur: () => void
|
|
385
|
+
getState: () => Pick<SearchState, 'suggestions' | 'showSuggestions' | 'loading' | 'isTyping'>
|
|
386
|
+
} {
|
|
387
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
|
388
|
+
let currentState: Pick<SearchState, 'suggestions' | 'showSuggestions' | 'loading' | 'isTyping'> = {
|
|
389
|
+
suggestions: null,
|
|
390
|
+
showSuggestions: false,
|
|
391
|
+
loading: false,
|
|
392
|
+
isTyping: false,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const handleChange = (value: string) => {
|
|
396
|
+
// Clear existing timeout
|
|
397
|
+
if (timeoutId) {
|
|
398
|
+
clearTimeout(timeoutId)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Set typing state
|
|
402
|
+
currentState = {
|
|
403
|
+
...currentState,
|
|
404
|
+
isTyping: true,
|
|
405
|
+
showSuggestions: true,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Debounce the search
|
|
409
|
+
timeoutId = setTimeout(async () => {
|
|
410
|
+
currentState = {
|
|
411
|
+
...currentState,
|
|
412
|
+
isTyping: false,
|
|
413
|
+
loading: true,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
const results = await onSearch(value)
|
|
418
|
+
currentState = {
|
|
419
|
+
...currentState,
|
|
420
|
+
suggestions: results,
|
|
421
|
+
loading: false,
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
currentState = {
|
|
425
|
+
...currentState,
|
|
426
|
+
suggestions: [],
|
|
427
|
+
loading: false,
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}, delay)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const handleFocus = () => {
|
|
434
|
+
currentState = {
|
|
435
|
+
...currentState,
|
|
436
|
+
showSuggestions: true,
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const handleBlur = () => {
|
|
441
|
+
// Delay hiding to allow click on suggestion
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
currentState = {
|
|
444
|
+
...currentState,
|
|
445
|
+
showSuggestions: false,
|
|
446
|
+
}
|
|
447
|
+
}, 200)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const getState = () => ({ ...currentState })
|
|
451
|
+
|
|
452
|
+
return { handleChange, handleFocus, handleBlur, getState }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// Validation Helpers
|
|
457
|
+
// ============================================================================
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Extracts field errors from RHF form state in a format suitable for rendering.
|
|
461
|
+
*
|
|
462
|
+
* @param errors - RHF errors object
|
|
463
|
+
* @param fieldNames - Optional list of field names to extract
|
|
464
|
+
* @returns Record of field names to error messages
|
|
465
|
+
*/
|
|
466
|
+
export function extractFieldErrors(
|
|
467
|
+
errors: Record<string, { message?: string }>,
|
|
468
|
+
fieldNames?: string[]
|
|
469
|
+
): Record<string, string> {
|
|
470
|
+
const result: Record<string, string> = {}
|
|
471
|
+
|
|
472
|
+
const names = fieldNames ?? Object.keys(errors)
|
|
473
|
+
for (const name of names) {
|
|
474
|
+
const error = errors[name]
|
|
475
|
+
if (error?.message) {
|
|
476
|
+
result[name] = error.message
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return result
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Checks if a form has any errors.
|
|
485
|
+
*
|
|
486
|
+
* @param errors - RHF errors object
|
|
487
|
+
* @returns True if there are any errors
|
|
488
|
+
*/
|
|
489
|
+
export function hasFormErrors(errors: Record<string, unknown>): boolean {
|
|
490
|
+
return Object.keys(errors).length > 0
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Gets the first field with an error.
|
|
495
|
+
*
|
|
496
|
+
* @param errors - RHF errors object
|
|
497
|
+
* @param fieldOrder - Optional field order to check
|
|
498
|
+
* @returns Name of first field with error, or undefined
|
|
499
|
+
*/
|
|
500
|
+
export function getFirstErrorField(
|
|
501
|
+
errors: Record<string, unknown>,
|
|
502
|
+
fieldOrder?: string[]
|
|
503
|
+
): string | undefined {
|
|
504
|
+
if (fieldOrder) {
|
|
505
|
+
for (const name of fieldOrder) {
|
|
506
|
+
if (errors[name]) {
|
|
507
|
+
return name
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return Object.keys(errors)[0]
|
|
512
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Forms Module
|
|
3
|
+
*
|
|
4
|
+
* React Hook Form integration for terminal form components.
|
|
5
|
+
*
|
|
6
|
+
* This module provides:
|
|
7
|
+
* - TypeScript types for form values and field configurations
|
|
8
|
+
* - Zod schemas for validation (compatible with @hookform/resolvers/zod)
|
|
9
|
+
* - Converters between RHF state and terminal UINode format
|
|
10
|
+
* - Helper utilities for form state management
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* import { useForm } from 'react-hook-form'
|
|
17
|
+
* import { zodResolver } from '@hookform/resolvers/zod'
|
|
18
|
+
* import {
|
|
19
|
+
* createLoginSchema,
|
|
20
|
+
* convertRHFToFormState,
|
|
21
|
+
* formStateToUINode,
|
|
22
|
+
* } from '@mdxui/terminal/forms'
|
|
23
|
+
* import { renderForm } from '@mdxui/terminal'
|
|
24
|
+
*
|
|
25
|
+
* // Create form with Zod validation
|
|
26
|
+
* const loginSchema = createLoginSchema({ rememberMe: true })
|
|
27
|
+
* type LoginForm = z.infer<typeof loginSchema>
|
|
28
|
+
*
|
|
29
|
+
* const { register, formState, getValues, handleSubmit } = useForm<LoginForm>({
|
|
30
|
+
* resolver: zodResolver(loginSchema),
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* // Convert to terminal-renderable format
|
|
34
|
+
* const terminalState = convertRHFToFormState(
|
|
35
|
+
* formState,
|
|
36
|
+
* [
|
|
37
|
+
* { name: 'email', label: 'Email', type: 'email', required: true },
|
|
38
|
+
* { name: 'password', label: 'Password', type: 'password', required: true },
|
|
39
|
+
* { name: 'rememberMe', label: 'Remember me', type: 'checkbox' },
|
|
40
|
+
* ],
|
|
41
|
+
* {
|
|
42
|
+
* title: 'Sign In',
|
|
43
|
+
* submitLabel: 'Login',
|
|
44
|
+
* values: getValues(),
|
|
45
|
+
* }
|
|
46
|
+
* )
|
|
47
|
+
*
|
|
48
|
+
* // Render to terminal
|
|
49
|
+
* const formNode = formStateToUINode(terminalState)
|
|
50
|
+
* const output = renderForm(formNode, ctx)
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
// Types
|
|
55
|
+
export type {
|
|
56
|
+
FieldValue,
|
|
57
|
+
FormValues,
|
|
58
|
+
FieldConfig,
|
|
59
|
+
FieldGroup,
|
|
60
|
+
FormState,
|
|
61
|
+
RegisterFn,
|
|
62
|
+
RHFFormState,
|
|
63
|
+
RHFFormConfig,
|
|
64
|
+
SelectOption,
|
|
65
|
+
SelectState,
|
|
66
|
+
SearchSuggestion,
|
|
67
|
+
SearchState,
|
|
68
|
+
InferFormValues,
|
|
69
|
+
FormStateConverter,
|
|
70
|
+
FieldRegistration,
|
|
71
|
+
} from './types'
|
|
72
|
+
|
|
73
|
+
// Schemas
|
|
74
|
+
export {
|
|
75
|
+
// Base schemas
|
|
76
|
+
FieldValueSchema,
|
|
77
|
+
SelectOptionSchema,
|
|
78
|
+
SearchSuggestionSchema,
|
|
79
|
+
FieldTypeSchema,
|
|
80
|
+
FieldConfigSchema,
|
|
81
|
+
FieldGroupSchema,
|
|
82
|
+
FormStateSchema,
|
|
83
|
+
SelectStateSchema,
|
|
84
|
+
SearchStateSchema,
|
|
85
|
+
|
|
86
|
+
// Common validation schemas
|
|
87
|
+
EmailSchema,
|
|
88
|
+
PasswordSchema,
|
|
89
|
+
SimplePasswordSchema,
|
|
90
|
+
UsernameSchema,
|
|
91
|
+
UrlSchema,
|
|
92
|
+
PhoneSchema,
|
|
93
|
+
|
|
94
|
+
// Schema builders
|
|
95
|
+
createLoginSchema,
|
|
96
|
+
createRegistrationSchema,
|
|
97
|
+
createContactSchema,
|
|
98
|
+
createProfileSchema,
|
|
99
|
+
createSettingsSchema,
|
|
100
|
+
} from './schemas'
|
|
101
|
+
|
|
102
|
+
// Re-export schema-derived types (alternative import path)
|
|
103
|
+
export type {
|
|
104
|
+
LoginFormValues,
|
|
105
|
+
RegistrationFormValues,
|
|
106
|
+
ContactFormValues,
|
|
107
|
+
ProfileFormValues,
|
|
108
|
+
SettingsFormValues,
|
|
109
|
+
} from './schemas'
|
|
110
|
+
|
|
111
|
+
// Converters
|
|
112
|
+
export {
|
|
113
|
+
// Form converters
|
|
114
|
+
convertRHFToFormState,
|
|
115
|
+
formStateToUINode,
|
|
116
|
+
|
|
117
|
+
// Field converters
|
|
118
|
+
fieldConfigToUINode,
|
|
119
|
+
createFieldFromRegister,
|
|
120
|
+
|
|
121
|
+
// Select converters
|
|
122
|
+
selectStateToUINode,
|
|
123
|
+
createSelectForController,
|
|
124
|
+
|
|
125
|
+
// Search converters
|
|
126
|
+
searchStateToUINode,
|
|
127
|
+
createDebouncedSearch,
|
|
128
|
+
|
|
129
|
+
// Validation helpers
|
|
130
|
+
extractFieldErrors,
|
|
131
|
+
hasFormErrors,
|
|
132
|
+
getFirstErrorField,
|
|
133
|
+
} from './converters'
|