@seed-ship/mcp-ui-solid 2.5.2 → 2.6.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/dist/types.d.cts CHANGED
@@ -217,7 +217,7 @@ export interface FormFieldOption {
217
217
  /**
218
218
  * Form field type
219
219
  */
220
- export type FormFieldType = 'text' | 'email' | 'password' | 'number' | 'date' | 'textarea' | 'select' | 'checkbox' | 'radio';
220
+ export type FormFieldType = 'text' | 'email' | 'password' | 'number' | 'date' | 'textarea' | 'select' | 'checkbox' | 'radio' | 'autocomplete';
221
221
  /**
222
222
  * Operators for conditional field display
223
223
  */
@@ -251,6 +251,22 @@ export interface FormFieldParams {
251
251
  minDate?: string;
252
252
  maxDate?: string;
253
253
  options?: FormFieldOption[];
254
+ /** Enable multi-select (returns array of values) */
255
+ multiple?: boolean;
256
+ /** API URL for autocomplete suggestions */
257
+ apiUrl?: string;
258
+ /** Query parameter name (e.g. 'nom', 'q') */
259
+ searchParam?: string;
260
+ /** Field in API response to display */
261
+ labelField?: string;
262
+ /** Field in API response to use as value */
263
+ valueField?: string;
264
+ /** Extra query parameters */
265
+ extraParams?: Record<string, string>;
266
+ /** Min characters before triggering (default: 2) */
267
+ minChars?: number;
268
+ /** Debounce delay in ms (default: 300) */
269
+ debounceMs?: number;
254
270
  checkboxLabel?: string;
255
271
  rows?: number;
256
272
  showWhen?: ShowWhenCondition;
package/dist/types.d.ts CHANGED
@@ -217,7 +217,7 @@ export interface FormFieldOption {
217
217
  /**
218
218
  * Form field type
219
219
  */
220
- export type FormFieldType = 'text' | 'email' | 'password' | 'number' | 'date' | 'textarea' | 'select' | 'checkbox' | 'radio';
220
+ export type FormFieldType = 'text' | 'email' | 'password' | 'number' | 'date' | 'textarea' | 'select' | 'checkbox' | 'radio' | 'autocomplete';
221
221
  /**
222
222
  * Operators for conditional field display
223
223
  */
@@ -251,6 +251,22 @@ export interface FormFieldParams {
251
251
  minDate?: string;
252
252
  maxDate?: string;
253
253
  options?: FormFieldOption[];
254
+ /** Enable multi-select (returns array of values) */
255
+ multiple?: boolean;
256
+ /** API URL for autocomplete suggestions */
257
+ apiUrl?: string;
258
+ /** Query parameter name (e.g. 'nom', 'q') */
259
+ searchParam?: string;
260
+ /** Field in API response to display */
261
+ labelField?: string;
262
+ /** Field in API response to use as value */
263
+ valueField?: string;
264
+ /** Extra query parameters */
265
+ extraParams?: Record<string, string>;
266
+ /** Min characters before triggering (default: 2) */
267
+ minChars?: number;
268
+ /** Debounce delay in ms (default: 300) */
269
+ debounceMs?: number;
254
270
  checkboxLabel?: string;
255
271
  rows?: number;
256
272
  showWhen?: ShowWhenCondition;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-ship/mcp-ui-solid",
3
- "version": "2.5.2",
3
+ "version": "2.6.0",
4
4
  "description": "SolidJS components for rendering MCP-generated UI resources",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -259,6 +259,61 @@ describe('ChatPrompt', () => {
259
259
  })
260
260
  })
261
261
 
262
+ // ─── dismissLabel ────────────────────────────────────
263
+
264
+ describe('dismissLabel', () => {
265
+ it('shows X icon by default (no dismissLabel)', () => {
266
+ const config: ChatPromptConfig = {
267
+ type: 'choice',
268
+ title: 'Test',
269
+ config: { options: [{ value: 'a', label: 'A' }] },
270
+ }
271
+ const { getByLabelText } = render(() => (
272
+ <ChatPrompt config={config} onSubmit={() => {}} />
273
+ ))
274
+
275
+ expect(getByLabelText('Dismiss')).toBeDefined()
276
+ expect(getByLabelText('Dismiss').querySelector('svg')).not.toBeNull()
277
+ })
278
+
279
+ it('shows text button when dismissLabel is provided', () => {
280
+ const config: ChatPromptConfig = {
281
+ type: 'choice',
282
+ title: 'Test',
283
+ config: { options: [{ value: 'a', label: 'A' }] },
284
+ }
285
+ const { getByText, getByLabelText } = render(() => (
286
+ <ChatPrompt config={config} onSubmit={() => {}} dismissLabel="Send as-is" />
287
+ ))
288
+
289
+ expect(getByText('Send as-is')).toBeDefined()
290
+ expect(getByLabelText('Send as-is')).toBeDefined()
291
+ })
292
+
293
+ it('calls onDismiss + onSubmit when dismissLabel button clicked', () => {
294
+ const onSubmit = vi.fn()
295
+ const onDismiss = vi.fn()
296
+ const config: ChatPromptConfig = {
297
+ type: 'choice',
298
+ title: 'Test',
299
+ config: { options: [{ value: 'a', label: 'A' }] },
300
+ }
301
+ const { getByText } = render(() => (
302
+ <ChatPrompt config={config} onSubmit={onSubmit} onDismiss={onDismiss} dismissLabel="Envoyer directement" />
303
+ ))
304
+
305
+ fireEvent.click(getByText('Envoyer directement'))
306
+
307
+ expect(onDismiss).toHaveBeenCalled()
308
+ expect(onSubmit).toHaveBeenCalledWith({
309
+ type: 'choice',
310
+ value: '',
311
+ label: '',
312
+ dismissed: true,
313
+ })
314
+ })
315
+ })
316
+
262
317
  // ─── Null guard (F1) ──────────────────────────────────
263
318
 
264
319
  describe('null guard', () => {
@@ -16,14 +16,18 @@ import type {
16
16
  ConfirmPromptConfig,
17
17
  FormPromptConfig,
18
18
  } from '../types/chat-bus'
19
+ import { FormFieldRenderer } from './FormFieldRenderer'
20
+ import type { FormFieldParams } from '../types'
19
21
 
20
22
  export interface ChatPromptProps {
21
23
  /** Prompt configuration */
22
24
  config: ChatPromptConfig
23
25
  /** Called when user responds */
24
26
  onSubmit: (response: ChatPromptResponse) => void
25
- /** Called when user dismisses */
27
+ /** Called when user dismisses (e.g. "send as-is") */
26
28
  onDismiss?: () => void
29
+ /** Label for the dismiss button (replaces X icon). Default: shows X icon. */
30
+ dismissLabel?: string
27
31
  }
28
32
 
29
33
  /**
@@ -57,12 +61,19 @@ export const ChatPrompt: Component<ChatPromptProps> = (props) => {
57
61
  props.onDismiss?.()
58
62
  props.onSubmit({ type: props.config.type, value: '', label: '', dismissed: true })
59
63
  }}
60
- class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
61
- aria-label="Dismiss"
64
+ class={props.dismissLabel
65
+ ? 'px-3 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors'
66
+ : 'p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors'
67
+ }
68
+ aria-label={props.dismissLabel || 'Dismiss'}
62
69
  >
63
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
64
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
65
- </svg>
70
+ <Show when={props.dismissLabel} fallback={
71
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
72
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
73
+ </svg>
74
+ }>
75
+ {props.dismissLabel}
76
+ </Show>
66
77
  </button>
67
78
  </div>
68
79
 
@@ -176,25 +187,24 @@ const ConfirmBody: Component<{
176
187
  )
177
188
  }
178
189
 
179
- // ─── Form ────────────────────────────────────────────────────
190
+ // ─── Form (delegates to FormFieldRenderer for all field types) ───
180
191
 
181
192
  const FormBody: Component<{
182
193
  config: FormPromptConfig
183
194
  onSubmit: (data: Record<string, unknown>, label: string) => void
184
195
  }> = (props) => {
185
- const [formData, setFormData] = createSignal<Record<string, string>>({})
196
+ const [formData, setFormData] = createSignal<Record<string, any>>({})
186
197
 
187
- const updateField = (name: string, value: string) => {
198
+ const updateField = (name: string, value: any) => {
188
199
  setFormData((prev) => ({ ...prev, [name]: value }))
189
200
  }
190
201
 
191
202
  const handleSubmit = (e: Event) => {
192
203
  e.preventDefault()
193
204
  const data = formData()
194
- // Build a human-readable label from the form values
195
205
  const label = Object.entries(data)
196
- .filter(([, v]) => v)
197
- .map(([k, v]) => `${k}: ${v}`)
206
+ .filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0))
207
+ .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : v}`)
198
208
  .join(', ')
199
209
  props.onSubmit(data, label || 'Form submitted')
200
210
  }
@@ -203,53 +213,24 @@ const FormBody: Component<{
203
213
  const data = formData()
204
214
  return (props.config.fields || [])
205
215
  .filter((f) => f.required)
206
- .every((f) => data[f.name]?.trim())
216
+ .every((f) => {
217
+ const val = data[f.name]
218
+ if (Array.isArray(val)) return val.length > 0
219
+ if (typeof val === 'boolean') return true
220
+ return val !== undefined && val !== ''
221
+ })
207
222
  }
208
223
 
209
224
  return (
210
225
  <form onSubmit={handleSubmit} class="flex flex-col gap-3">
211
226
  <For each={props.config.fields}>
212
227
  {(field) => (
213
- <div>
214
- <label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">
215
- {field.label}
216
- <Show when={field.required}>
217
- <span class="text-red-500 ml-0.5">*</span>
218
- </Show>
219
- </label>
220
- <Switch>
221
- <Match when={field.type === 'textarea'}>
222
- <textarea
223
- value={formData()[field.name] || ''}
224
- onInput={(e) => updateField(field.name, e.currentTarget.value)}
225
- placeholder={field.placeholder}
226
- rows={3}
227
- class="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 focus:ring-1 focus:ring-blue-400 outline-none transition-colors"
228
- />
229
- </Match>
230
- <Match when={field.type === 'select'}>
231
- <select
232
- value={formData()[field.name] || ''}
233
- onChange={(e) => updateField(field.name, e.currentTarget.value)}
234
- class="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 focus:ring-1 focus:ring-blue-400 outline-none transition-colors"
235
- >
236
- <option value="">{field.placeholder || 'Select...'}</option>
237
- <For each={field.options}>
238
- {(opt) => <option value={opt.value}>{opt.label}</option>}
239
- </For>
240
- </select>
241
- </Match>
242
- <Match when={true}>
243
- <input
244
- type={field.type === 'number' ? 'number' : 'text'}
245
- value={formData()[field.name] || ''}
246
- onInput={(e) => updateField(field.name, e.currentTarget.value)}
247
- placeholder={field.placeholder}
248
- class="w-full px-3 py-2 text-sm rounded-lg border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 focus:ring-1 focus:ring-blue-400 outline-none transition-colors"
249
- />
250
- </Match>
251
- </Switch>
252
- </div>
228
+ <FormFieldRenderer
229
+ field={field as FormFieldParams}
230
+ value={formData()[field.name]}
231
+ onChange={(val) => updateField(field.name, val)}
232
+ formData={formData}
233
+ />
253
234
  )}
254
235
  </For>
255
236
  <div class="flex justify-end">
@@ -4,7 +4,7 @@
4
4
  * Sprint 2: Conditional field visibility (showWhen)
5
5
  */
6
6
 
7
- import { Component, Show, For, Switch, Match, Accessor } from 'solid-js'
7
+ import { Component, Show, For, Switch, Match, Accessor, createSignal, createEffect, onCleanup } from 'solid-js'
8
8
  import type { FormFieldParams } from '../types'
9
9
  import { useConditionalField } from '../hooks/useConditionalField'
10
10
 
@@ -133,8 +133,8 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
133
133
  />
134
134
  </Match>
135
135
 
136
- {/* Select */}
137
- <Match when={props.field.type === 'select'}>
136
+ {/* Select (single) */}
137
+ <Match when={props.field.type === 'select' && !props.field.multiple}>
138
138
  <select
139
139
  id={fieldId()}
140
140
  name={props.field.name}
@@ -182,6 +182,28 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
182
182
  </label>
183
183
  </Match>
184
184
 
185
+ {/* Multi-Select with chips */}
186
+ <Match when={props.field.type === 'select' && props.field.multiple}>
187
+ <MultiSelectField
188
+ field={props.field}
189
+ value={props.value || []}
190
+ onChange={props.onChange}
191
+ disabled={props.disabled}
192
+ baseClass={baseInputClass()}
193
+ />
194
+ </Match>
195
+
196
+ {/* Autocomplete with API fetch */}
197
+ <Match when={props.field.type === 'autocomplete'}>
198
+ <AutocompleteField
199
+ field={props.field}
200
+ value={props.value || ''}
201
+ onChange={props.onChange}
202
+ disabled={props.disabled}
203
+ baseClass={baseInputClass()}
204
+ />
205
+ </Match>
206
+
185
207
  {/* Radio Group */}
186
208
  <Match when={props.field.type === 'radio'}>
187
209
  <div
@@ -227,3 +249,196 @@ export const FormFieldRenderer: Component<FormFieldRendererProps> = (props) => {
227
249
  </Show>
228
250
  )
229
251
  }
252
+
253
+ // ─── Multi-Select with Chips ─────────────────────────────────
254
+
255
+ const MultiSelectField: Component<{
256
+ field: FormFieldParams
257
+ value: string[]
258
+ onChange: (value: string[]) => void
259
+ disabled?: boolean
260
+ baseClass: string
261
+ }> = (props) => {
262
+ const [open, setOpen] = createSignal(false)
263
+
264
+ const toggle = (val: string) => {
265
+ const current = props.value || []
266
+ if (current.includes(val)) {
267
+ props.onChange(current.filter((v) => v !== val))
268
+ } else {
269
+ props.onChange([...current, val])
270
+ }
271
+ }
272
+
273
+ const removeChip = (val: string) => {
274
+ props.onChange((props.value || []).filter((v) => v !== val))
275
+ }
276
+
277
+ const getLabel = (val: string) =>
278
+ props.field.options?.find((o) => o.value === val)?.label || val
279
+
280
+ return (
281
+ <div class="relative">
282
+ {/* Selected chips */}
283
+ <Show when={props.value.length > 0}>
284
+ <div class="flex flex-wrap gap-1 mb-1">
285
+ <For each={props.value}>
286
+ {(val) => (
287
+ <span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full">
288
+ {getLabel(val)}
289
+ <button
290
+ type="button"
291
+ onClick={() => removeChip(val)}
292
+ class="hover:text-blue-900 dark:hover:text-blue-100"
293
+ aria-label={`Remove ${getLabel(val)}`}
294
+ >
295
+ &times;
296
+ </button>
297
+ </span>
298
+ )}
299
+ </For>
300
+ </div>
301
+ </Show>
302
+
303
+ {/* Trigger button */}
304
+ <button
305
+ type="button"
306
+ onClick={() => setOpen(!open())}
307
+ disabled={props.disabled}
308
+ class={`${props.baseClass} text-left flex items-center justify-between`}
309
+ >
310
+ <span class={props.value.length ? 'text-gray-900 dark:text-white' : 'text-gray-400'}>
311
+ {props.value.length
312
+ ? `${props.value.length} selected`
313
+ : props.field.placeholder || 'Select...'}
314
+ </span>
315
+ <svg class="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
316
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
317
+ </svg>
318
+ </button>
319
+
320
+ {/* Dropdown */}
321
+ <Show when={open()}>
322
+ <div class="absolute z-20 mt-1 w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg max-h-48 overflow-auto">
323
+ <For each={props.field.options}>
324
+ {(option) => (
325
+ <label class="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer text-sm">
326
+ <input
327
+ type="checkbox"
328
+ checked={(props.value || []).includes(option.value)}
329
+ onChange={() => toggle(option.value)}
330
+ class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
331
+ />
332
+ <span class="text-gray-900 dark:text-white">{option.label}</span>
333
+ </label>
334
+ )}
335
+ </For>
336
+ </div>
337
+ </Show>
338
+ </div>
339
+ )
340
+ }
341
+
342
+ // ─── Autocomplete with API fetch ─────────────────────────────
343
+
344
+ const AutocompleteField: Component<{
345
+ field: FormFieldParams
346
+ value: string
347
+ onChange: (value: string) => void
348
+ disabled?: boolean
349
+ baseClass: string
350
+ }> = (props) => {
351
+ const [query, setQuery] = createSignal('')
352
+ const [suggestions, setSuggestions] = createSignal<Array<{ label: string; value: string }>>([])
353
+ const [isOpen, setIsOpen] = createSignal(false)
354
+ const [selectedLabel, setSelectedLabel] = createSignal('')
355
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null
356
+
357
+ const minChars = () => props.field.minChars ?? 2
358
+ const debounceMs = () => props.field.debounceMs ?? 300
359
+
360
+ const fetchSuggestions = async (q: string) => {
361
+ if (!props.field.apiUrl || !props.field.searchParam) return
362
+
363
+ try {
364
+ const params = new URLSearchParams({ [props.field.searchParam]: q })
365
+ if (props.field.extraParams) {
366
+ for (const [k, v] of Object.entries(props.field.extraParams)) {
367
+ params.set(k, v)
368
+ }
369
+ }
370
+ const res = await fetch(`${props.field.apiUrl}?${params}`)
371
+ if (!res.ok) return
372
+
373
+ const data = await res.json()
374
+ const items = Array.isArray(data) ? data : data.results || data.features || []
375
+ const labelField = props.field.labelField || 'label'
376
+ const valueField = props.field.valueField || 'value'
377
+
378
+ setSuggestions(items.slice(0, 10).map((item: any) => ({
379
+ label: item[labelField] || String(item),
380
+ value: String(item[valueField] || item[labelField] || item),
381
+ })))
382
+ setIsOpen(true)
383
+ } catch {
384
+ setSuggestions([])
385
+ }
386
+ }
387
+
388
+ const handleInput = (value: string) => {
389
+ setQuery(value)
390
+ setSelectedLabel('')
391
+ props.onChange('')
392
+
393
+ if (debounceTimer) clearTimeout(debounceTimer)
394
+ if (value.length < minChars()) {
395
+ setSuggestions([])
396
+ setIsOpen(false)
397
+ return
398
+ }
399
+ debounceTimer = setTimeout(() => fetchSuggestions(value), debounceMs())
400
+ }
401
+
402
+ const selectSuggestion = (item: { label: string; value: string }) => {
403
+ props.onChange(item.value)
404
+ setSelectedLabel(item.label)
405
+ setQuery(item.label)
406
+ setIsOpen(false)
407
+ setSuggestions([])
408
+ }
409
+
410
+ onCleanup(() => { if (debounceTimer) clearTimeout(debounceTimer) })
411
+
412
+ return (
413
+ <div class="relative">
414
+ <input
415
+ type="text"
416
+ value={query()}
417
+ onInput={(e) => handleInput(e.currentTarget.value)}
418
+ onFocus={() => { if (suggestions().length) setIsOpen(true) }}
419
+ onBlur={() => setTimeout(() => setIsOpen(false), 200)}
420
+ placeholder={props.field.placeholder}
421
+ disabled={props.disabled}
422
+ class={props.baseClass}
423
+ autocomplete="off"
424
+ />
425
+
426
+ <Show when={isOpen() && suggestions().length > 0}>
427
+ <div class="absolute z-20 mt-1 w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md shadow-lg max-h-48 overflow-auto">
428
+ <For each={suggestions()}>
429
+ {(item) => (
430
+ <button
431
+ type="button"
432
+ onMouseDown={(e) => e.preventDefault()}
433
+ onClick={() => selectSuggestion(item)}
434
+ class="w-full text-left px-3 py-2 text-sm hover:bg-blue-50 dark:hover:bg-blue-900/20 text-gray-900 dark:text-white"
435
+ >
436
+ {item.label}
437
+ </button>
438
+ )}
439
+ </For>
440
+ </div>
441
+ </Show>
442
+ </div>
443
+ )
444
+ }
@@ -197,10 +197,30 @@ export interface FormPromptConfig {
197
197
  fields: Array<{
198
198
  name: string
199
199
  label: string
200
- type: 'text' | 'number' | 'select' | 'textarea'
200
+ type: 'text' | 'number' | 'select' | 'textarea' | 'checkbox' | 'radio' | 'date' | 'email' | 'autocomplete'
201
201
  required?: boolean
202
202
  placeholder?: string
203
203
  options?: Array<{ label: string; value: string }>
204
+ /** Enable multi-select (returns array) */
205
+ multiple?: boolean
206
+ /** Autocomplete: API URL */
207
+ apiUrl?: string
208
+ /** Autocomplete: query param name */
209
+ searchParam?: string
210
+ /** Autocomplete: field in response for display */
211
+ labelField?: string
212
+ /** Autocomplete: field in response for value */
213
+ valueField?: string
214
+ /** Autocomplete: extra query params */
215
+ extraParams?: Record<string, string>
216
+ /** Autocomplete: min chars before trigger (default: 2) */
217
+ minChars?: number
218
+ /** Autocomplete: debounce ms (default: 300) */
219
+ debounceMs?: number
220
+ /** Checkbox label text */
221
+ checkboxLabel?: string
222
+ /** Help text below field */
223
+ helpText?: string
204
224
  }>
205
225
  submitLabel?: string
206
226
  }
@@ -267,6 +267,7 @@ export type FormFieldType =
267
267
  | 'select'
268
268
  | 'checkbox'
269
269
  | 'radio'
270
+ | 'autocomplete'
270
271
 
271
272
  /**
272
273
  * Operators for conditional field display
@@ -324,6 +325,24 @@ export interface FormFieldParams {
324
325
 
325
326
  // Select/Radio specific
326
327
  options?: FormFieldOption[]
328
+ /** Enable multi-select (returns array of values) */
329
+ multiple?: boolean
330
+
331
+ // Autocomplete specific
332
+ /** API URL for autocomplete suggestions */
333
+ apiUrl?: string
334
+ /** Query parameter name (e.g. 'nom', 'q') */
335
+ searchParam?: string
336
+ /** Field in API response to display */
337
+ labelField?: string
338
+ /** Field in API response to use as value */
339
+ valueField?: string
340
+ /** Extra query parameters */
341
+ extraParams?: Record<string, string>
342
+ /** Min characters before triggering (default: 2) */
343
+ minChars?: number
344
+ /** Debounce delay in ms (default: 300) */
345
+ debounceMs?: number
327
346
 
328
347
  // Checkbox specific
329
348
  checkboxLabel?: string