@seed-ship/mcp-ui-solid 2.0.0 → 2.1.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 (140) hide show
  1. package/README.md +50 -1
  2. package/dist/components/AutocompleteDropdown.cjs +201 -0
  3. package/dist/components/AutocompleteDropdown.cjs.map +1 -0
  4. package/dist/components/AutocompleteDropdown.d.ts +71 -0
  5. package/dist/components/AutocompleteDropdown.d.ts.map +1 -0
  6. package/dist/components/AutocompleteDropdown.js +201 -0
  7. package/dist/components/AutocompleteDropdown.js.map +1 -0
  8. package/dist/components/AutocompleteFormField.cjs +289 -0
  9. package/dist/components/AutocompleteFormField.cjs.map +1 -0
  10. package/dist/components/AutocompleteFormField.d.ts +52 -0
  11. package/dist/components/AutocompleteFormField.d.ts.map +1 -0
  12. package/dist/components/AutocompleteFormField.js +289 -0
  13. package/dist/components/AutocompleteFormField.js.map +1 -0
  14. package/dist/components/DraggableGridItem.cjs +133 -0
  15. package/dist/components/DraggableGridItem.cjs.map +1 -0
  16. package/dist/components/DraggableGridItem.d.ts +95 -0
  17. package/dist/components/DraggableGridItem.d.ts.map +1 -0
  18. package/dist/components/DraggableGridItem.js +133 -0
  19. package/dist/components/DraggableGridItem.js.map +1 -0
  20. package/dist/components/EditableUIResourceRenderer.cjs +203 -0
  21. package/dist/components/EditableUIResourceRenderer.cjs.map +1 -0
  22. package/dist/components/EditableUIResourceRenderer.d.ts +43 -0
  23. package/dist/components/EditableUIResourceRenderer.d.ts.map +1 -0
  24. package/dist/components/EditableUIResourceRenderer.js +203 -0
  25. package/dist/components/EditableUIResourceRenderer.js.map +1 -0
  26. package/dist/components/GhostText.cjs +105 -0
  27. package/dist/components/GhostText.cjs.map +1 -0
  28. package/dist/components/GhostText.d.ts +113 -0
  29. package/dist/components/GhostText.d.ts.map +1 -0
  30. package/dist/components/GhostText.js +105 -0
  31. package/dist/components/GhostText.js.map +1 -0
  32. package/dist/components/ResizeHandle.cjs +173 -0
  33. package/dist/components/ResizeHandle.cjs.map +1 -0
  34. package/dist/components/ResizeHandle.d.ts +50 -0
  35. package/dist/components/ResizeHandle.d.ts.map +1 -0
  36. package/dist/components/ResizeHandle.js +173 -0
  37. package/dist/components/ResizeHandle.js.map +1 -0
  38. package/dist/context/AutocompleteContext.cjs +158 -0
  39. package/dist/context/AutocompleteContext.cjs.map +1 -0
  40. package/dist/context/AutocompleteContext.d.ts +77 -0
  41. package/dist/context/AutocompleteContext.d.ts.map +1 -0
  42. package/dist/context/AutocompleteContext.js +158 -0
  43. package/dist/context/AutocompleteContext.js.map +1 -0
  44. package/dist/hooks/index.d.ts +6 -0
  45. package/dist/hooks/index.d.ts.map +1 -1
  46. package/dist/hooks/useAutocomplete.cjs +234 -0
  47. package/dist/hooks/useAutocomplete.cjs.map +1 -0
  48. package/dist/hooks/useAutocomplete.d.ts +119 -0
  49. package/dist/hooks/useAutocomplete.d.ts.map +1 -0
  50. package/dist/hooks/useAutocomplete.js +234 -0
  51. package/dist/hooks/useAutocomplete.js.map +1 -0
  52. package/dist/hooks/useDragDrop.cjs +170 -0
  53. package/dist/hooks/useDragDrop.cjs.map +1 -0
  54. package/dist/hooks/useDragDrop.d.ts +100 -0
  55. package/dist/hooks/useDragDrop.d.ts.map +1 -0
  56. package/dist/hooks/useDragDrop.js +170 -0
  57. package/dist/hooks/useDragDrop.js.map +1 -0
  58. package/dist/hooks/useResize.cjs +209 -0
  59. package/dist/hooks/useResize.cjs.map +1 -0
  60. package/dist/hooks/useResize.d.ts +87 -0
  61. package/dist/hooks/useResize.d.ts.map +1 -0
  62. package/dist/hooks/useResize.js +209 -0
  63. package/dist/hooks/useResize.js.map +1 -0
  64. package/dist/hooks.cjs +6 -0
  65. package/dist/hooks.cjs.map +1 -1
  66. package/dist/hooks.d.cts +6 -0
  67. package/dist/hooks.d.ts +6 -0
  68. package/dist/hooks.js +6 -0
  69. package/dist/hooks.js.map +1 -1
  70. package/dist/index.cjs +29 -0
  71. package/dist/index.cjs.map +1 -1
  72. package/dist/index.d.cts +18 -3
  73. package/dist/index.d.ts +18 -3
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +29 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/plugins/duckdb.cjs +192 -0
  78. package/dist/plugins/duckdb.cjs.map +1 -0
  79. package/dist/plugins/duckdb.d.ts +20 -0
  80. package/dist/plugins/duckdb.d.ts.map +1 -0
  81. package/dist/plugins/duckdb.js +170 -0
  82. package/dist/plugins/duckdb.js.map +1 -0
  83. package/dist/plugins/groq.cjs +97 -0
  84. package/dist/plugins/groq.cjs.map +1 -0
  85. package/dist/plugins/groq.d.ts +13 -0
  86. package/dist/plugins/groq.d.ts.map +1 -0
  87. package/dist/plugins/groq.js +97 -0
  88. package/dist/plugins/groq.js.map +1 -0
  89. package/dist/plugins/index.d.ts +10 -0
  90. package/dist/plugins/index.d.ts.map +1 -0
  91. package/dist/plugins/rest.cjs +92 -0
  92. package/dist/plugins/rest.cjs.map +1 -0
  93. package/dist/plugins/rest.d.ts +13 -0
  94. package/dist/plugins/rest.d.ts.map +1 -0
  95. package/dist/plugins/rest.js +92 -0
  96. package/dist/plugins/rest.js.map +1 -0
  97. package/dist/plugins/supabase.cjs +79 -0
  98. package/dist/plugins/supabase.cjs.map +1 -0
  99. package/dist/plugins/supabase.d.ts +13 -0
  100. package/dist/plugins/supabase.d.ts.map +1 -0
  101. package/dist/plugins/supabase.js +79 -0
  102. package/dist/plugins/supabase.js.map +1 -0
  103. package/dist/services/validation.cjs +40 -1
  104. package/dist/services/validation.cjs.map +1 -1
  105. package/dist/services/validation.d.ts.map +1 -1
  106. package/dist/services/validation.js +40 -1
  107. package/dist/services/validation.js.map +1 -1
  108. package/dist/types/index.d.ts +430 -0
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types.d.cts +430 -0
  111. package/dist/types.d.ts +430 -0
  112. package/package.json +16 -1
  113. package/src/components/AutocompleteDropdown.tsx +329 -0
  114. package/src/components/AutocompleteFormField.tsx +288 -0
  115. package/src/components/DraggableGridItem.tsx +274 -0
  116. package/src/components/EditableUIResourceRenderer.tsx +273 -0
  117. package/src/components/GhostText.tsx +262 -0
  118. package/src/components/ResizeHandle.tsx +262 -0
  119. package/src/context/AutocompleteContext.tsx +317 -0
  120. package/src/hooks/index.ts +23 -0
  121. package/src/hooks/useAutocomplete.test.ts +334 -0
  122. package/src/hooks/useAutocomplete.ts +466 -0
  123. package/src/hooks/useDragDrop.test.ts +355 -0
  124. package/src/hooks/useDragDrop.ts +379 -0
  125. package/src/hooks/useResize.test.ts +313 -0
  126. package/src/hooks/useResize.ts +372 -0
  127. package/src/index.ts +71 -0
  128. package/src/plugins/duckdb.ts +269 -0
  129. package/src/plugins/groq.ts +137 -0
  130. package/src/plugins/index.ts +14 -0
  131. package/src/plugins/rest.ts +147 -0
  132. package/src/plugins/supabase.ts +120 -0
  133. package/src/services/validation.ts +46 -0
  134. package/src/styles/autocomplete.css +356 -0
  135. package/src/styles/drag-drop.css +297 -0
  136. package/src/styles/index.css +7 -0
  137. package/src/types/index.ts +529 -0
  138. package/src/vite-env.d.ts +18 -0
  139. package/tsconfig.tsbuildinfo +1 -1
  140. package/vite.config.ts +2 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * AutocompleteDropdown Component
3
+ * Displays data-driven dropdown suggestions
4
+ *
5
+ * Sprint Autocomplete Feature
6
+ */
7
+
8
+ import { Component, For, Show, createMemo, JSX } from 'solid-js'
9
+ import type { AutocompleteOption } from '../types'
10
+
11
+ /**
12
+ * Props for AutocompleteDropdown
13
+ */
14
+ export interface AutocompleteDropdownProps {
15
+ /**
16
+ * Options to display
17
+ */
18
+ options: AutocompleteOption[]
19
+
20
+ /**
21
+ * Currently selected index
22
+ */
23
+ selectedIndex: number
24
+
25
+ /**
26
+ * Whether dropdown is visible
27
+ */
28
+ isOpen: boolean
29
+
30
+ /**
31
+ * Callback when option is selected
32
+ */
33
+ onSelect: (option: AutocompleteOption) => void
34
+
35
+ /**
36
+ * Callback when option is hovered
37
+ */
38
+ onHover?: (index: number) => void
39
+
40
+ /**
41
+ * Whether loading
42
+ */
43
+ isLoading?: boolean
44
+
45
+ /**
46
+ * Custom class
47
+ */
48
+ class?: string
49
+
50
+ /**
51
+ * Max height
52
+ */
53
+ maxHeight?: string
54
+
55
+ /**
56
+ * Empty state message
57
+ */
58
+ emptyMessage?: string
59
+
60
+ /**
61
+ * Loading message
62
+ */
63
+ loadingMessage?: string
64
+
65
+ /**
66
+ * Highlight matching text in options
67
+ */
68
+ highlightMatch?: string
69
+
70
+ /**
71
+ * Position (default: 'bottom')
72
+ */
73
+ position?: 'top' | 'bottom'
74
+
75
+ /**
76
+ * Custom option renderer
77
+ */
78
+ renderOption?: (option: AutocompleteOption, isSelected: boolean) => JSX.Element
79
+ }
80
+
81
+ /**
82
+ * Highlight matching text
83
+ */
84
+ function highlightText(text: string, match?: string): JSX.Element {
85
+ if (!match || !text) {
86
+ return <>{text}</>
87
+ }
88
+
89
+ const lowerText = text.toLowerCase()
90
+ const lowerMatch = match.toLowerCase()
91
+ const startIndex = lowerText.indexOf(lowerMatch)
92
+
93
+ if (startIndex === -1) {
94
+ return <>{text}</>
95
+ }
96
+
97
+ const before = text.slice(0, startIndex)
98
+ const matched = text.slice(startIndex, startIndex + match.length)
99
+ const after = text.slice(startIndex + match.length)
100
+
101
+ return (
102
+ <>
103
+ {before}
104
+ <strong class="mcp-autocomplete-highlight">{matched}</strong>
105
+ {after}
106
+ </>
107
+ )
108
+ }
109
+
110
+ /**
111
+ * Default option renderer
112
+ */
113
+ const DefaultOptionRenderer: Component<{
114
+ option: AutocompleteOption
115
+ isSelected: boolean
116
+ highlightMatch?: string
117
+ }> = (props) => {
118
+ const displayLabel = createMemo(() =>
119
+ props.option.label || props.option.value
120
+ )
121
+
122
+ return (
123
+ <div class="mcp-autocomplete-option-content">
124
+ <Show when={props.option.icon}>
125
+ <span class="mcp-autocomplete-option-icon">{props.option.icon}</span>
126
+ </Show>
127
+ <div class="mcp-autocomplete-option-text">
128
+ <span class="mcp-autocomplete-option-label">
129
+ {highlightText(displayLabel(), props.highlightMatch)}
130
+ </span>
131
+ <Show when={props.option.description}>
132
+ <span class="mcp-autocomplete-option-description">
133
+ {props.option.description}
134
+ </span>
135
+ </Show>
136
+ </div>
137
+ </div>
138
+ )
139
+ }
140
+
141
+ /**
142
+ * AutocompleteDropdown Component
143
+ */
144
+ export const AutocompleteDropdown: Component<AutocompleteDropdownProps> = (props) => {
145
+ const positionStyles = createMemo((): JSX.CSSProperties => {
146
+ if (props.position === 'top') {
147
+ return {
148
+ bottom: '100%',
149
+ 'margin-bottom': '4px'
150
+ }
151
+ }
152
+ return {
153
+ top: '100%',
154
+ 'margin-top': '4px'
155
+ }
156
+ })
157
+
158
+ const containerStyles = createMemo((): JSX.CSSProperties => ({
159
+ position: 'absolute',
160
+ left: '0',
161
+ right: '0',
162
+ 'z-index': '50',
163
+ 'background-color': '#ffffff',
164
+ border: '1px solid #e5e7eb',
165
+ 'border-radius': '6px',
166
+ 'box-shadow': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
167
+ 'max-height': props.maxHeight || '240px',
168
+ overflow: 'auto',
169
+ ...positionStyles()
170
+ }))
171
+
172
+ return (
173
+ <Show when={props.isOpen}>
174
+ <div
175
+ class={`mcp-autocomplete-dropdown ${props.class || ''}`}
176
+ style={containerStyles()}
177
+ role="listbox"
178
+ aria-label="Suggestions"
179
+ >
180
+ {/* Loading state */}
181
+ <Show when={props.isLoading}>
182
+ <div
183
+ class="mcp-autocomplete-loading"
184
+ style={{
185
+ padding: '12px 16px',
186
+ color: '#6b7280',
187
+ 'font-size': '0.875rem',
188
+ display: 'flex',
189
+ 'align-items': 'center',
190
+ gap: '8px'
191
+ }}
192
+ >
193
+ <span
194
+ style={{
195
+ display: 'inline-block',
196
+ width: '14px',
197
+ height: '14px',
198
+ border: '2px solid #e5e7eb',
199
+ 'border-top-color': '#3b82f6',
200
+ 'border-radius': '50%',
201
+ animation: 'mcp-spin 0.6s linear infinite'
202
+ }}
203
+ />
204
+ {props.loadingMessage || 'Loading...'}
205
+ </div>
206
+ </Show>
207
+
208
+ {/* Empty state */}
209
+ <Show when={!props.isLoading && props.options.length === 0}>
210
+ <div
211
+ class="mcp-autocomplete-empty"
212
+ style={{
213
+ padding: '12px 16px',
214
+ color: '#9ca3af',
215
+ 'font-size': '0.875rem',
216
+ 'text-align': 'center'
217
+ }}
218
+ >
219
+ {props.emptyMessage || 'No suggestions found'}
220
+ </div>
221
+ </Show>
222
+
223
+ {/* Options list */}
224
+ <Show when={!props.isLoading && props.options.length > 0}>
225
+ <ul
226
+ class="mcp-autocomplete-options"
227
+ style={{ margin: '0', padding: '4px 0', 'list-style': 'none' }}
228
+ >
229
+ <For each={props.options}>
230
+ {(option, index) => {
231
+ const isSelected = () => index() === props.selectedIndex
232
+ const isDisabled = () => option.disabled
233
+
234
+ return (
235
+ <li
236
+ role="option"
237
+ aria-selected={isSelected()}
238
+ aria-disabled={isDisabled()}
239
+ class={`mcp-autocomplete-option ${isSelected() ? 'mcp-autocomplete-option-selected' : ''} ${isDisabled() ? 'mcp-autocomplete-option-disabled' : ''}`}
240
+ style={{
241
+ padding: '8px 16px',
242
+ cursor: isDisabled() ? 'not-allowed' : 'pointer',
243
+ 'background-color': isSelected() ? '#eff6ff' : 'transparent',
244
+ color: isDisabled() ? '#9ca3af' : '#374151',
245
+ 'font-size': '0.875rem',
246
+ transition: 'background-color 150ms ease'
247
+ }}
248
+ onClick={() => {
249
+ if (!isDisabled()) {
250
+ props.onSelect(option)
251
+ }
252
+ }}
253
+ onMouseEnter={() => {
254
+ if (!isDisabled()) {
255
+ props.onHover?.(index())
256
+ }
257
+ }}
258
+ >
259
+ <Show
260
+ when={props.renderOption}
261
+ fallback={
262
+ <DefaultOptionRenderer
263
+ option={option}
264
+ isSelected={isSelected()}
265
+ highlightMatch={props.highlightMatch}
266
+ />
267
+ }
268
+ >
269
+ {props.renderOption!(option, isSelected())}
270
+ </Show>
271
+ </li>
272
+ )
273
+ }}
274
+ </For>
275
+ </ul>
276
+ </Show>
277
+
278
+ {/* Footer hint */}
279
+ <Show when={!props.isLoading && props.options.length > 0}>
280
+ <div
281
+ class="mcp-autocomplete-footer"
282
+ style={{
283
+ padding: '6px 12px',
284
+ 'border-top': '1px solid #e5e7eb',
285
+ 'background-color': '#f9fafb',
286
+ 'font-size': '0.75rem',
287
+ color: '#6b7280'
288
+ }}
289
+ >
290
+ <kbd style={{
291
+ 'background-color': '#e5e7eb',
292
+ padding: '1px 4px',
293
+ 'border-radius': '2px',
294
+ 'font-family': 'inherit',
295
+ 'font-size': '0.7rem'
296
+ }}>↑</kbd>
297
+ {' '}
298
+ <kbd style={{
299
+ 'background-color': '#e5e7eb',
300
+ padding: '1px 4px',
301
+ 'border-radius': '2px',
302
+ 'font-family': 'inherit',
303
+ 'font-size': '0.7rem'
304
+ }}>↓</kbd>
305
+ {' to navigate, '}
306
+ <kbd style={{
307
+ 'background-color': '#e5e7eb',
308
+ padding: '1px 4px',
309
+ 'border-radius': '2px',
310
+ 'font-family': 'inherit',
311
+ 'font-size': '0.7rem'
312
+ }}>Enter</kbd>
313
+ {' to select, '}
314
+ <kbd style={{
315
+ 'background-color': '#e5e7eb',
316
+ padding: '1px 4px',
317
+ 'border-radius': '2px',
318
+ 'font-family': 'inherit',
319
+ 'font-size': '0.7rem'
320
+ }}>Esc</kbd>
321
+ {' to dismiss'}
322
+ </div>
323
+ </Show>
324
+ </div>
325
+ </Show>
326
+ )
327
+ }
328
+
329
+ export default AutocompleteDropdown
@@ -0,0 +1,288 @@
1
+ /**
2
+ * AutocompleteFormField Component
3
+ * Form field with integrated autocomplete support
4
+ *
5
+ * Sprint Autocomplete Feature
6
+ */
7
+
8
+ import { Component, Show, createSignal, createMemo, Accessor, createEffect, on } from 'solid-js'
9
+ import type { FormFieldParams, FieldAutocompleteConfig, AutocompleteContext } from '../types'
10
+ import { useConditionalField } from '../hooks/useConditionalField'
11
+ import { useAutocomplete } from '../hooks/useAutocomplete'
12
+ import { useAutocompleteContextSafe } from '../context/AutocompleteContext'
13
+ import { GhostText } from './GhostText'
14
+ import { AutocompleteDropdown } from './AutocompleteDropdown'
15
+
16
+ /**
17
+ * Extended FormFieldParams with autocomplete config
18
+ */
19
+ export interface AutocompleteFormFieldParams extends FormFieldParams {
20
+ /**
21
+ * Autocomplete configuration for this field
22
+ */
23
+ autocomplete?: FieldAutocompleteConfig
24
+ }
25
+
26
+ /**
27
+ * Props for AutocompleteFormField
28
+ */
29
+ export interface AutocompleteFormFieldProps {
30
+ /**
31
+ * Field configuration
32
+ */
33
+ field: AutocompleteFormFieldParams
34
+
35
+ /**
36
+ * Current field value
37
+ */
38
+ value: any
39
+
40
+ /**
41
+ * Error message
42
+ */
43
+ error?: string
44
+
45
+ /**
46
+ * Change handler
47
+ */
48
+ onChange: (value: any) => void
49
+
50
+ /**
51
+ * Whether field is disabled
52
+ */
53
+ disabled?: boolean
54
+
55
+ /**
56
+ * Form data accessor for conditional visibility and context
57
+ */
58
+ formData?: Accessor<Record<string, any>>
59
+ }
60
+
61
+ /**
62
+ * AutocompleteFormField Component
63
+ */
64
+ export const AutocompleteFormField: Component<AutocompleteFormFieldProps> = (props) => {
65
+ // Check if autocomplete context is available
66
+ const autocompleteCtx = useAutocompleteContextSafe()
67
+
68
+ // Conditional visibility
69
+ const { isVisible } = useConditionalField({
70
+ condition: props.field.showWhen,
71
+ formData: props.formData || (() => ({}))
72
+ })
73
+
74
+ // Local input value for autocomplete (may differ during suggestion)
75
+ const [localValue, setLocalValue] = createSignal(String(props.value || ''))
76
+
77
+ // Sync external value changes
78
+ createEffect(on(() => props.value, (newValue) => {
79
+ setLocalValue(String(newValue || ''))
80
+ }))
81
+
82
+ // Build autocomplete context
83
+ const autocompleteContext = createMemo((): AutocompleteContext | undefined => {
84
+ const formData = props.formData?.()
85
+ const config = props.field.autocomplete
86
+
87
+ if (!config?.contextFields?.length && !formData) {
88
+ return { fieldName: props.field.name }
89
+ }
90
+
91
+ const contextData: Record<string, any> = {}
92
+ if (config?.contextFields && formData) {
93
+ config.contextFields.forEach(field => {
94
+ if (formData[field] !== undefined) {
95
+ contextData[field] = formData[field]
96
+ }
97
+ })
98
+ }
99
+
100
+ return {
101
+ fieldName: props.field.name,
102
+ formData: contextData
103
+ }
104
+ })
105
+
106
+ // Initialize autocomplete hook
107
+ const autocomplete = useAutocomplete({
108
+ inputValue: localValue,
109
+ pluginId: props.field.autocomplete?.plugin,
110
+ fieldConfig: props.field.autocomplete,
111
+ context: () => autocompleteContext() || { fieldName: props.field.name },
112
+ enabled: !!(props.field.autocomplete?.enabled && autocompleteCtx),
113
+ minChars: props.field.autocomplete?.minChars,
114
+ debounceMs: props.field.autocomplete?.debounceMs,
115
+ onInputChange: (value) => {
116
+ setLocalValue(value)
117
+ props.onChange(value)
118
+ }
119
+ })
120
+
121
+ // Handle input change
122
+ const handleInput = (value: string) => {
123
+ setLocalValue(value)
124
+ props.onChange(value)
125
+ }
126
+
127
+ // Handle key down
128
+ const handleKeyDown = (e: KeyboardEvent) => {
129
+ if (autocomplete.handleKeyDown(e)) {
130
+ return
131
+ }
132
+ }
133
+
134
+ // Base input class
135
+ const baseInputClass = () => `
136
+ w-full px-3 py-2 border rounded-md
137
+ focus:ring-2 focus:ring-blue-500 focus:border-blue-500
138
+ disabled:bg-gray-100 disabled:cursor-not-allowed
139
+ ${props.error
140
+ ? 'border-red-500 focus:ring-red-500'
141
+ : 'border-gray-300 dark:border-gray-600'}
142
+ dark:bg-gray-700 dark:text-white
143
+ `
144
+
145
+ const fieldId = () => `field-${props.field.name}`
146
+ const errorId = () => `${props.field.name}-error`
147
+
148
+ // Check if field supports autocomplete (text-based fields only)
149
+ const supportsAutocomplete = createMemo(() =>
150
+ ['text', 'email'].includes(props.field.type)
151
+ )
152
+
153
+ // Whether to show autocomplete features
154
+ const showAutocomplete = createMemo(() =>
155
+ supportsAutocomplete() &&
156
+ props.field.autocomplete?.enabled &&
157
+ autocompleteCtx !== undefined
158
+ )
159
+
160
+ return (
161
+ <Show when={isVisible()}>
162
+ <div class="space-y-1">
163
+ {/* Label */}
164
+ <Show when={props.field.label}>
165
+ <label
166
+ for={fieldId()}
167
+ class="block text-sm font-medium text-gray-700 dark:text-gray-300"
168
+ >
169
+ {props.field.label}
170
+ <Show when={props.field.required}>
171
+ <span class="text-red-500 ml-1" aria-hidden="true">*</span>
172
+ </Show>
173
+ </label>
174
+ </Show>
175
+
176
+ {/* Input with autocomplete */}
177
+ <div class="relative">
178
+ <Show
179
+ when={showAutocomplete()}
180
+ fallback={
181
+ /* Standard input without autocomplete */
182
+ <input
183
+ id={fieldId()}
184
+ type={props.field.type}
185
+ name={props.field.name}
186
+ value={props.value || ''}
187
+ onInput={(e) => props.onChange(e.currentTarget.value)}
188
+ placeholder={props.field.placeholder}
189
+ disabled={props.disabled}
190
+ required={props.field.required}
191
+ minLength={props.field.minLength}
192
+ maxLength={props.field.maxLength}
193
+ pattern={props.field.pattern}
194
+ aria-invalid={!!props.error}
195
+ aria-describedby={props.error ? errorId() : undefined}
196
+ class={baseInputClass()}
197
+ />
198
+ }
199
+ >
200
+ {/* Autocomplete-enabled input */}
201
+ <div class="relative">
202
+ {/* Ghost text overlay (for completion type) */}
203
+ <Show when={autocomplete.resultType() === 'completion'}>
204
+ <GhostText
205
+ inputValue={localValue()}
206
+ ghostText={autocomplete.ghostText()}
207
+ visible={autocomplete.isOpen()}
208
+ hintText={autocomplete.ghostText() ? 'Tab to accept' : undefined}
209
+ isLoading={autocomplete.isLoading()}
210
+ />
211
+ </Show>
212
+
213
+ <input
214
+ id={fieldId()}
215
+ type={props.field.type}
216
+ name={props.field.name}
217
+ value={localValue()}
218
+ onInput={(e) => handleInput(e.currentTarget.value)}
219
+ onKeyDown={handleKeyDown}
220
+ onBlur={() => {
221
+ // Delay dismiss to allow click on dropdown
222
+ setTimeout(() => autocomplete.dismiss(), 150)
223
+ }}
224
+ onFocus={() => {
225
+ if (localValue().length >= (props.field.autocomplete?.minChars || 1)) {
226
+ autocomplete.open()
227
+ }
228
+ }}
229
+ placeholder={props.field.placeholder}
230
+ disabled={props.disabled}
231
+ required={props.field.required}
232
+ minLength={props.field.minLength}
233
+ maxLength={props.field.maxLength}
234
+ pattern={props.field.pattern}
235
+ aria-invalid={!!props.error}
236
+ aria-describedby={props.error ? errorId() : undefined}
237
+ aria-autocomplete="list"
238
+ aria-expanded={autocomplete.isOpen()}
239
+ aria-haspopup="listbox"
240
+ class={`${baseInputClass()} ${autocomplete.resultType() === 'completion' ? 'bg-transparent' : ''}`}
241
+ style={{
242
+ position: 'relative',
243
+ 'z-index': '2'
244
+ }}
245
+ autocomplete="off"
246
+ />
247
+
248
+ {/* Dropdown (for options type) */}
249
+ <Show when={autocomplete.resultType() === 'options'}>
250
+ <AutocompleteDropdown
251
+ options={autocomplete.options()}
252
+ selectedIndex={autocomplete.selectedIndex()}
253
+ isOpen={autocomplete.isOpen()}
254
+ isLoading={autocomplete.isLoading()}
255
+ onSelect={(option) => {
256
+ handleInput(option.value)
257
+ autocomplete.dismiss()
258
+ }}
259
+ onHover={(_index) => {
260
+ // Could add hover selection here
261
+ }}
262
+ highlightMatch={localValue()}
263
+ loadingMessage={props.field.autocomplete?.loadingPlaceholder}
264
+ />
265
+ </Show>
266
+ </div>
267
+ </Show>
268
+ </div>
269
+
270
+ {/* Help text */}
271
+ <Show when={props.field.helpText && !props.error}>
272
+ <p class="text-xs text-gray-500 dark:text-gray-400">
273
+ {props.field.helpText}
274
+ </p>
275
+ </Show>
276
+
277
+ {/* Error message */}
278
+ <Show when={props.error}>
279
+ <p id={errorId()} role="alert" class="text-xs text-red-600 dark:text-red-400">
280
+ {props.error}
281
+ </p>
282
+ </Show>
283
+ </div>
284
+ </Show>
285
+ )
286
+ }
287
+
288
+ export default AutocompleteFormField