@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.
- package/dist/components/FormFieldRenderer.cjs +145 -50
- package/dist/components/FormFieldRenderer.cjs.map +1 -1
- package/dist/components/FormFieldRenderer.js +145 -50
- package/dist/components/FormFieldRenderer.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FormFieldRenderer.tsx +119 -38
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
</
|
|
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 [
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
×
|
|
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={
|
|
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-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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">✓</span>
|
|
531
|
+
</Show>
|
|
532
|
+
</button>
|
|
533
|
+
)
|
|
534
|
+
}}
|
|
454
535
|
</For>
|
|
455
536
|
</div>
|
|
456
537
|
</Show>
|