@seed-ship/mcp-ui-solid 2.6.2 → 2.6.3

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.
@@ -275,6 +275,7 @@ const MultiSelectField: Component<{
275
275
  baseClass: string
276
276
  }> = (props) => {
277
277
  const [open, setOpen] = createSignal(false)
278
+ const [filter, setFilter] = createSignal('')
278
279
 
279
280
  const toggle = (val: string) => {
280
281
  const current = props.value || []
@@ -292,6 +293,14 @@ const MultiSelectField: Component<{
292
293
  const getLabel = (val: string) =>
293
294
  props.field.options?.find((o) => o.value === val)?.label || val
294
295
 
296
+ const filteredOptions = () => {
297
+ const q = filter().toLowerCase()
298
+ if (!q) return props.field.options || []
299
+ return (props.field.options || []).filter(
300
+ (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q)
301
+ )
302
+ }
303
+
295
304
  return (
296
305
  <div class="relative">
297
306
  {/* Selected chips */}
@@ -318,7 +327,7 @@ const MultiSelectField: Component<{
318
327
  {/* Trigger button */}
319
328
  <button
320
329
  type="button"
321
- onClick={() => setOpen(!open())}
330
+ onClick={() => { setOpen(!open()); if (!open()) setFilter('') }}
322
331
  disabled={props.disabled}
323
332
  class={`${props.baseClass} text-left flex items-center justify-between`}
324
333
  >
@@ -332,22 +341,41 @@ const MultiSelectField: Component<{
332
341
  </svg>
333
342
  </button>
334
343
 
335
- {/* Dropdown */}
344
+ {/* Dropdown with filter */}
336
345
  <Show when={open()}>
337
- <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">
338
- <For each={props.field.options}>
339
- {(option) => (
340
- <label class="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer text-sm">
341
- <input
342
- type="checkbox"
343
- checked={(props.value || []).includes(option.value)}
344
- onChange={() => toggle(option.value)}
345
- class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
346
- />
347
- <span class="text-gray-900 dark:text-white">{option.label}</span>
348
- </label>
349
- )}
350
- </For>
346
+ <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 overflow-hidden">
347
+ {/* Search filter */}
348
+ <Show when={(props.field.options?.length || 0) > 10}>
349
+ <div class="p-2 border-b border-gray-200 dark:border-gray-600">
350
+ <input
351
+ type="text"
352
+ value={filter()}
353
+ onInput={(e) => setFilter(e.currentTarget.value)}
354
+ placeholder="Filter..."
355
+ class="w-full px-2 py-1 text-sm border border-gray-200 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:border-blue-400 outline-none"
356
+ autofocus
357
+ />
358
+ </div>
359
+ </Show>
360
+ {/* Options list */}
361
+ <div class="max-h-72 overflow-y-auto">
362
+ <For each={filteredOptions()}>
363
+ {(option) => (
364
+ <label class="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer text-sm">
365
+ <input
366
+ type="checkbox"
367
+ checked={(props.value || []).includes(option.value)}
368
+ onChange={() => toggle(option.value)}
369
+ class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600"
370
+ />
371
+ <span class="text-gray-900 dark:text-white">{option.label}</span>
372
+ </label>
373
+ )}
374
+ </For>
375
+ <Show when={filteredOptions().length === 0}>
376
+ <p class="px-3 py-2 text-sm text-gray-400">No matches</p>
377
+ </Show>
378
+ </div>
351
379
  </div>
352
380
  </Show>
353
381
  </div>
@@ -358,17 +386,19 @@ const MultiSelectField: Component<{
358
386
 
359
387
  const AutocompleteField: Component<{
360
388
  field: FormFieldParams
361
- value: string
362
- onChange: (value: string) => void
389
+ value: string | string[]
390
+ onChange: (value: string | string[]) => void
363
391
  disabled?: boolean
364
392
  baseClass: string
365
393
  }> = (props) => {
366
394
  const [query, setQuery] = createSignal('')
367
395
  const [suggestions, setSuggestions] = createSignal<Array<{ label: string; value: string }>>([])
368
396
  const [isOpen, setIsOpen] = createSignal(false)
369
- const [selectedLabel, setSelectedLabel] = createSignal('')
397
+ const [selectedLabels, setSelectedLabels] = createSignal<Map<string, string>>(new Map())
370
398
  let debounceTimer: ReturnType<typeof setTimeout> | null = null
371
399
 
400
+ const isMultiple = () => props.field.multiple === true
401
+ const selectedValues = () => isMultiple() ? (Array.isArray(props.value) ? props.value : []) : []
372
402
  const minChars = () => props.field.minChars ?? 2
373
403
  const debounceMs = () => props.field.debounceMs ?? 300
374
404
 
@@ -402,8 +432,9 @@ const AutocompleteField: Component<{
402
432
 
403
433
  const handleInput = (value: string) => {
404
434
  setQuery(value)
405
- setSelectedLabel('')
406
- props.onChange('')
435
+ if (!isMultiple()) {
436
+ props.onChange('')
437
+ }
407
438
 
408
439
  if (debounceTimer) clearTimeout(debounceTimer)
409
440
  if (value.length < minChars()) {
@@ -415,42 +446,92 @@ const AutocompleteField: Component<{
415
446
  }
416
447
 
417
448
  const selectSuggestion = (item: { label: string; value: string }) => {
418
- props.onChange(item.value)
419
- setSelectedLabel(item.label)
420
- setQuery(item.label)
421
- setIsOpen(false)
422
- setSuggestions([])
449
+ if (isMultiple()) {
450
+ const current = selectedValues()
451
+ if (!current.includes(item.value)) {
452
+ props.onChange([...current, item.value])
453
+ setSelectedLabels((prev) => new Map(prev).set(item.value, item.label))
454
+ }
455
+ setQuery('')
456
+ setSuggestions([])
457
+ setIsOpen(false)
458
+ } else {
459
+ props.onChange(item.value)
460
+ setSelectedLabels((prev) => new Map(prev).set(item.value, item.label))
461
+ setQuery(item.label)
462
+ setIsOpen(false)
463
+ setSuggestions([])
464
+ }
465
+ }
466
+
467
+ const removeChip = (val: string) => {
468
+ props.onChange(selectedValues().filter((v) => v !== val))
469
+ setSelectedLabels((prev) => { const m = new Map(prev); m.delete(val); return m })
423
470
  }
424
471
 
472
+ const getLabel = (val: string) => selectedLabels().get(val) || val
473
+
425
474
  onCleanup(() => { if (debounceTimer) clearTimeout(debounceTimer) })
426
475
 
427
476
  return (
428
477
  <div class="relative">
478
+ {/* Multi chips */}
479
+ <Show when={isMultiple() && selectedValues().length > 0}>
480
+ <div class="flex flex-wrap gap-1 mb-1">
481
+ <For each={selectedValues()}>
482
+ {(val) => (
483
+ <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">
484
+ {getLabel(val)}
485
+ <button
486
+ type="button"
487
+ onClick={() => removeChip(val)}
488
+ class="hover:text-blue-900 dark:hover:text-blue-100"
489
+ aria-label={`Remove ${getLabel(val)}`}
490
+ >
491
+ &times;
492
+ </button>
493
+ </span>
494
+ )}
495
+ </For>
496
+ </div>
497
+ </Show>
498
+
429
499
  <input
430
500
  type="text"
431
501
  value={query()}
432
502
  onInput={(e) => handleInput(e.currentTarget.value)}
433
503
  onFocus={() => { if (suggestions().length) setIsOpen(true) }}
434
504
  onBlur={() => setTimeout(() => setIsOpen(false), 200)}
435
- placeholder={props.field.placeholder}
505
+ placeholder={isMultiple() && selectedValues().length
506
+ ? 'Add more...'
507
+ : props.field.placeholder}
436
508
  disabled={props.disabled}
437
509
  class={props.baseClass}
438
510
  autocomplete="off"
439
511
  />
440
512
 
441
513
  <Show when={isOpen() && suggestions().length > 0}>
442
- <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">
514
+ <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-72 overflow-y-auto">
443
515
  <For each={suggestions()}>
444
- {(item) => (
445
- <button
446
- type="button"
447
- onMouseDown={(e) => e.preventDefault()}
448
- onClick={() => selectSuggestion(item)}
449
- 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"
450
- >
451
- {item.label}
452
- </button>
453
- )}
516
+ {(item) => {
517
+ const isSelected = () => isMultiple() && selectedValues().includes(item.value)
518
+ return (
519
+ <button
520
+ type="button"
521
+ onMouseDown={(e) => e.preventDefault()}
522
+ onClick={() => selectSuggestion(item)}
523
+ class={`w-full text-left px-3 py-2 text-sm hover:bg-blue-50 dark:hover:bg-blue-900/20 ${
524
+ isSelected() ? 'text-blue-600 dark:text-blue-400 bg-blue-50/50 dark:bg-blue-900/10' : 'text-gray-900 dark:text-white'
525
+ }`}
526
+ disabled={isSelected()}
527
+ >
528
+ {item.label}
529
+ <Show when={isSelected()}>
530
+ <span class="ml-2 text-xs">&#10003;</span>
531
+ </Show>
532
+ </button>
533
+ )
534
+ }}
454
535
  </For>
455
536
  </div>
456
537
  </Show>