@mdxui/terminal 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,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'