mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: 新增 EnhancedSelect 组件,为 A2UI Dropdown 添加 Combobox 模式
- 创建 enhanced-select 组件(搜索、分组、描述、键盘导航、Glassmorphism 样式) - 扩展 DropdownComponentSchema 支持 searchable/clearable/size/label/error 等字段 - A2UIDropdown 渲染器自动检测增强特性,按需切换标准 Select 与 Combobox 模式 - 所有新字段 optional,完全向后兼容
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
// ========================================
|
||||
// Enhanced Select Component
|
||||
// ========================================
|
||||
// Glassmorphism-styled Combobox with search, groups, descriptions,
|
||||
// form integration (label/required/error), and keyboard navigation.
|
||||
// Built on native DOM + Radix-like patterns (no extra deps).
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { ChevronDown, Search, X, Check } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { triggerVariants, optionItemVariants } from './enhanced-select-variants';
|
||||
import type { EnhancedSelectProps, EnhancedSelectOption } from './types';
|
||||
|
||||
// ========== Grouped + Filtered Options ==========
|
||||
|
||||
interface GroupedOptions {
|
||||
[group: string]: EnhancedSelectOption[];
|
||||
}
|
||||
|
||||
function groupAndFilter(
|
||||
options: EnhancedSelectOption[],
|
||||
search: string,
|
||||
): { grouped: GroupedOptions; total: number } {
|
||||
const filtered = search
|
||||
? options.filter(
|
||||
(opt) =>
|
||||
opt.label.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(opt.description && opt.description.toLowerCase().includes(search.toLowerCase()))
|
||||
)
|
||||
: options;
|
||||
|
||||
const grouped: GroupedOptions = {};
|
||||
for (const opt of filtered) {
|
||||
const key = opt.group || '';
|
||||
if (!grouped[key]) grouped[key] = [];
|
||||
grouped[key].push(opt);
|
||||
}
|
||||
|
||||
return { grouped, total: filtered.length };
|
||||
}
|
||||
|
||||
function highlightMatch(text: string, search: string): React.ReactNode {
|
||||
if (!search) return text;
|
||||
const idx = text.toLowerCase().indexOf(search.toLowerCase());
|
||||
if (idx === -1) return text;
|
||||
return (
|
||||
<>
|
||||
{text.slice(0, idx)}
|
||||
<mark className="bg-primary/20 text-foreground rounded-sm px-0.5">{text.slice(idx, idx + search.length)}</mark>
|
||||
{text.slice(idx + search.length)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Main Component ==========
|
||||
|
||||
export function EnhancedSelect({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Select...',
|
||||
searchable = false,
|
||||
clearable = false,
|
||||
size = 'default',
|
||||
label,
|
||||
required,
|
||||
error,
|
||||
disabled,
|
||||
className,
|
||||
}: EnhancedSelectProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const triggerId = useRef(`enhanced-select-${Math.random().toString(36).slice(2, 8)}`).current;
|
||||
|
||||
// Resolve display label
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
const displayValue = selectedOption?.label || '';
|
||||
|
||||
// Group and filter
|
||||
const { grouped, total } = useMemo(() => groupAndFilter(options, search), [options, search]);
|
||||
|
||||
// Flat list of visible options for keyboard nav
|
||||
const flatVisible = useMemo(() => {
|
||||
const result: EnhancedSelectOption[] = [];
|
||||
const sortedGroups = Object.keys(grouped).sort((a, b) => {
|
||||
if (a === '') return -1;
|
||||
if (b === '') return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
for (const g of sortedGroups) {
|
||||
for (const opt of grouped[g]) {
|
||||
result.push(opt);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}, [grouped]);
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
setHighlightIndex(-1);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [open]);
|
||||
|
||||
// Focus search input on open
|
||||
useEffect(() => {
|
||||
if (open && searchable) {
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}
|
||||
}, [open, searchable]);
|
||||
|
||||
// Scroll highlighted option into view
|
||||
useEffect(() => {
|
||||
if (highlightIndex >= 0 && listRef.current) {
|
||||
const el = listRef.current.querySelector(`[data-index="${highlightIndex}"]`);
|
||||
el?.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}, [highlightIndex]);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
if (disabled) return;
|
||||
setOpen(true);
|
||||
setHighlightIndex(-1);
|
||||
}, [disabled]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(opt: EnhancedSelectOption) => {
|
||||
if (opt.disabled) return;
|
||||
onChange(opt.value);
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
setHighlightIndex(-1);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleClear = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onChange('');
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (!open) {
|
||||
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
handleOpen();
|
||||
}
|
||||
if ((e.key === 'Backspace' || e.key === 'Delete') && clearable && value) {
|
||||
e.preventDefault();
|
||||
onChange('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
setOpen(false);
|
||||
setSearch('');
|
||||
setHighlightIndex(-1);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setHighlightIndex((prev) => {
|
||||
let next = prev + 1;
|
||||
while (next < flatVisible.length && flatVisible[next].disabled) next++;
|
||||
return next < flatVisible.length ? next : prev;
|
||||
});
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setHighlightIndex((prev) => {
|
||||
let next = prev - 1;
|
||||
while (next >= 0 && flatVisible[next].disabled) next--;
|
||||
return next >= 0 ? next : prev;
|
||||
});
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (highlightIndex >= 0 && highlightIndex < flatVisible.length) {
|
||||
handleSelect(flatVisible[highlightIndex]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
[open, handleOpen, flatVisible, highlightIndex, handleSelect, clearable, value, onChange]
|
||||
);
|
||||
|
||||
// Derive state variant
|
||||
const stateVariant = disabled ? 'disabled' as const : error ? 'error' as const : 'normal' as const;
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-1.5', className)}>
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<label htmlFor={triggerId} className="text-sm font-medium leading-none">
|
||||
{label}
|
||||
{required && <span className="text-destructive ml-0.5">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* Select Container */}
|
||||
<div ref={containerRef} className="relative" onKeyDown={handleKeyDown}>
|
||||
{/* Trigger */}
|
||||
<button
|
||||
id={triggerId}
|
||||
type="button"
|
||||
onClick={() => (open ? setOpen(false) : handleOpen())}
|
||||
disabled={disabled}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
aria-invalid={!!error}
|
||||
aria-required={required}
|
||||
data-state={open ? 'open' : 'closed'}
|
||||
className={cn(triggerVariants({ size, state: stateVariant }))}
|
||||
>
|
||||
<span className={cn('truncate', !displayValue && 'text-muted-foreground')}>
|
||||
{displayValue || placeholder}
|
||||
</span>
|
||||
<div className="flex items-center shrink-0">
|
||||
{clearable && value && !disabled && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
onClick={handleClear}
|
||||
className="p-0.5 rounded-full hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
)}
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Panel */}
|
||||
{open && (
|
||||
<div
|
||||
className="absolute z-50 mt-1 w-full rounded-lg shadow-lg bg-card/90 backdrop-blur-md border border-primary/20 animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-200"
|
||||
role="listbox"
|
||||
aria-label={label || placeholder}
|
||||
>
|
||||
{/* Search Input */}
|
||||
{searchable && (
|
||||
<div className="flex items-center px-3 py-2 border-b border-border/50">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50 text-muted-foreground" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
setHighlightIndex(-1);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
className="flex-1 h-7 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
||||
aria-label="Search options"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSearch('');
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
className="p-0.5 rounded-full hover:bg-muted text-muted-foreground"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Options List */}
|
||||
<div ref={listRef} className="max-h-64 overflow-y-auto p-1">
|
||||
{total === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
{search ? 'No matching options' : 'No options available'}
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
let globalIdx = 0;
|
||||
const sortedGroups = Object.keys(grouped).sort((a, b) => {
|
||||
if (a === '') return -1;
|
||||
if (b === '') return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
return sortedGroups.map((group) => (
|
||||
<div key={group || '__ungrouped'}>
|
||||
{group && (
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{group}
|
||||
</div>
|
||||
)}
|
||||
{grouped[group].map((opt) => {
|
||||
const idx = globalIdx++;
|
||||
const isSelected = opt.value === value;
|
||||
const isHighlighted = idx === highlightIndex;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={opt.value}
|
||||
data-index={idx}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
aria-disabled={opt.disabled}
|
||||
onClick={() => handleSelect(opt)}
|
||||
className={cn(
|
||||
optionItemVariants({ isSelected, isDisabled: !!opt.disabled }),
|
||||
isHighlighted && !isSelected && 'bg-accent/50',
|
||||
'pl-3',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full min-w-0">
|
||||
{/* Icon */}
|
||||
{opt.icon && (
|
||||
<span className="shrink-0 text-muted-foreground">{opt.icon}</span>
|
||||
)}
|
||||
{/* Content */}
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="truncate text-sm">
|
||||
{highlightMatch(opt.label, search)}
|
||||
</span>
|
||||
{opt.description && (
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{highlightMatch(opt.description, search)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Check mark */}
|
||||
{isSelected && (
|
||||
<Check className="h-4 w-4 shrink-0 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
));
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// ========================================
|
||||
// Enhanced Select - CVA Variant Definitions
|
||||
// ========================================
|
||||
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
export const triggerVariants = cva(
|
||||
'inline-flex w-full items-center justify-between rounded-lg border bg-background/80 backdrop-blur-sm text-sm ring-offset-background transition-all duration-200 [&>svg]:transition-transform [&>svg]:duration-200',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-8 px-2 text-xs gap-1',
|
||||
default: 'h-10 px-3 gap-2',
|
||||
lg: 'h-12 px-4 text-base gap-2',
|
||||
},
|
||||
variant: {
|
||||
default: 'border-input hover:bg-primary/10',
|
||||
ghost: 'border-transparent hover:bg-primary/10',
|
||||
outline: 'border-2 border-muted-foreground/30 hover:bg-primary/10',
|
||||
},
|
||||
state: {
|
||||
normal: 'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2',
|
||||
error: 'border-destructive focus-within:ring-2 focus-within:ring-destructive/50 focus-within:ring-offset-2',
|
||||
disabled: 'opacity-50 cursor-not-allowed pointer-events-none',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'default',
|
||||
variant: 'default',
|
||||
state: 'normal',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const optionItemVariants = cva(
|
||||
'relative flex w-full cursor-pointer select-none items-start rounded-md px-2 py-1.5 text-sm outline-none transition-colors duration-150',
|
||||
{
|
||||
variants: {
|
||||
isSelected: {
|
||||
true: "bg-primary/20 before:absolute before:left-0 before:top-0 before:h-full before:w-1 before:bg-primary before:content-[''] before:rounded-l-md",
|
||||
false: 'hover:bg-primary/15',
|
||||
},
|
||||
isDisabled: {
|
||||
true: 'pointer-events-none opacity-50',
|
||||
false: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
isSelected: false,
|
||||
isDisabled: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
7
ccw/frontend/src/components/ui/enhanced-select/index.ts
Normal file
7
ccw/frontend/src/components/ui/enhanced-select/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// ========================================
|
||||
// Enhanced Select - Barrel Export
|
||||
// ========================================
|
||||
|
||||
export { EnhancedSelect } from './EnhancedSelect';
|
||||
export type { EnhancedSelectProps, EnhancedSelectOption } from './types';
|
||||
export { triggerVariants, optionItemVariants } from './enhanced-select-variants';
|
||||
27
ccw/frontend/src/components/ui/enhanced-select/types.ts
Normal file
27
ccw/frontend/src/components/ui/enhanced-select/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// ========================================
|
||||
// Enhanced Select - Internal Types
|
||||
// ========================================
|
||||
|
||||
export interface EnhancedSelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export interface EnhancedSelectProps {
|
||||
options: EnhancedSelectOption[];
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
searchable?: boolean;
|
||||
clearable?: boolean;
|
||||
size?: 'sm' | 'default' | 'lg';
|
||||
label?: string;
|
||||
required?: boolean;
|
||||
error?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
@@ -62,16 +62,31 @@ export const ButtonComponentSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
/** Dropdown/Select component */
|
||||
/** Dropdown option schema (enhanced with description, icon, disabled, group) */
|
||||
export const DropdownOptionSchema = z.object({
|
||||
label: TextContentSchema,
|
||||
value: z.string(),
|
||||
description: TextContentSchema.optional(),
|
||||
icon: z.string().optional(),
|
||||
disabled: BooleanContentSchema.optional(),
|
||||
group: z.string().optional(),
|
||||
});
|
||||
|
||||
/** Dropdown/Select component (enhanced with search, form integration, styling) */
|
||||
export const DropdownComponentSchema = z.object({
|
||||
Dropdown: z.object({
|
||||
options: z.array(z.object({
|
||||
label: TextContentSchema,
|
||||
value: z.string(),
|
||||
})),
|
||||
options: z.array(DropdownOptionSchema),
|
||||
selectedValue: TextContentSchema.optional(),
|
||||
onChange: ActionSchema,
|
||||
placeholder: z.string().optional(),
|
||||
// Enhanced features
|
||||
searchable: z.boolean().optional(),
|
||||
clearable: z.boolean().optional(),
|
||||
size: z.enum(['sm', 'default', 'lg']).optional(),
|
||||
// Form integration
|
||||
label: TextContentSchema.optional(),
|
||||
required: z.boolean().optional(),
|
||||
error: TextContentSchema.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// ========================================
|
||||
// A2UI Dropdown Component Renderer
|
||||
// ========================================
|
||||
// Maps A2UI Dropdown component to shadcn/ui Select
|
||||
// Renders as EnhancedSelect (Combobox) when searchable,
|
||||
// or standard shadcn/ui Select otherwise.
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -11,25 +12,31 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/Select';
|
||||
import { EnhancedSelect } from '@/components/ui/enhanced-select';
|
||||
import type { EnhancedSelectOption } from '@/components/ui/enhanced-select';
|
||||
import type { ComponentRenderer } from '../../core/A2UIComponentRegistry';
|
||||
import { resolveTextContent } from '../A2UIRenderer';
|
||||
import { resolveTextContent, resolveLiteralOrBinding } from '../A2UIRenderer';
|
||||
import type { DropdownComponent } from '../../core/A2UITypes';
|
||||
|
||||
interface A2UIDropdownProps {
|
||||
component: DropdownComponent;
|
||||
state: Record<string, unknown>;
|
||||
onAction: (actionId: string, params: Record<string, unknown>) => void | Promise<void>;
|
||||
resolveBinding: (binding: { path: string }) => unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* A2UI Dropdown Component Renderer
|
||||
* Using shadcn/ui Select with options array mapping to SelectItem
|
||||
* Auto-selects rendering mode based on schema fields:
|
||||
* - searchable/description/icon/group → EnhancedSelect (Combobox)
|
||||
* - basic options only → shadcn/ui Select
|
||||
*/
|
||||
export const A2UIDropdown: ComponentRenderer = ({ component, state, onAction, resolveBinding }) => {
|
||||
const dropdownComp = component as DropdownComponent;
|
||||
const { Dropdown: dropdownConfig } = dropdownComp;
|
||||
|
||||
// Detect if enhanced features are requested
|
||||
const hasEnhancedFeatures = dropdownConfig.searchable ||
|
||||
dropdownConfig.clearable ||
|
||||
dropdownConfig.label ||
|
||||
dropdownConfig.error ||
|
||||
dropdownConfig.size ||
|
||||
dropdownConfig.options.some((opt: any) => opt.description || opt.icon || opt.group || opt.disabled);
|
||||
|
||||
// Resolve initial selected value from binding
|
||||
const getInitialValue = (): string => {
|
||||
if (!dropdownConfig.selectedValue) return '';
|
||||
@@ -56,13 +63,55 @@ export const A2UIDropdown: ComponentRenderer = ({ component, state, onAction, re
|
||||
});
|
||||
}, [dropdownConfig.onChange, onAction]);
|
||||
|
||||
// Resolve enhanced options (always computed to satisfy hooks rules)
|
||||
const resolvedOptions: EnhancedSelectOption[] = useMemo(() =>
|
||||
dropdownConfig.options.map((opt: any) => ({
|
||||
label: resolveTextContent(opt.label, resolveBinding),
|
||||
value: opt.value,
|
||||
description: opt.description ? resolveTextContent(opt.description, resolveBinding) : undefined,
|
||||
icon: opt.icon || undefined,
|
||||
disabled: opt.disabled
|
||||
? Boolean(resolveLiteralOrBinding(opt.disabled, resolveBinding))
|
||||
: false,
|
||||
group: opt.group || undefined,
|
||||
})),
|
||||
[dropdownConfig.options, resolveBinding]
|
||||
);
|
||||
|
||||
const resolvedLabel = dropdownConfig.label
|
||||
? resolveTextContent(dropdownConfig.label, resolveBinding)
|
||||
: undefined;
|
||||
|
||||
const resolvedError = dropdownConfig.error
|
||||
? resolveTextContent(dropdownConfig.error, resolveBinding)
|
||||
: undefined;
|
||||
|
||||
// ========== Enhanced Mode (Combobox) ==========
|
||||
if (hasEnhancedFeatures) {
|
||||
return (
|
||||
<EnhancedSelect
|
||||
options={resolvedOptions}
|
||||
value={selectedValue}
|
||||
onChange={handleChange}
|
||||
placeholder={dropdownConfig.placeholder}
|
||||
searchable={dropdownConfig.searchable || false}
|
||||
clearable={dropdownConfig.clearable || false}
|
||||
size={dropdownConfig.size}
|
||||
label={resolvedLabel}
|
||||
required={dropdownConfig.required || false}
|
||||
error={resolvedError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Standard Mode (Radix Select) ==========
|
||||
return (
|
||||
<Select value={selectedValue} onValueChange={handleChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={dropdownConfig.placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{dropdownConfig.options.map((option) => {
|
||||
{dropdownConfig.options.map((option: any) => {
|
||||
const label = resolveTextContent(option.label, resolveBinding);
|
||||
return (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
|
||||
Reference in New Issue
Block a user