feat: 新增 EnhancedSelect 组件,为 A2UI Dropdown 添加 Combobox 模式

- 创建 enhanced-select 组件(搜索、分组、描述、键盘导航、Glassmorphism 样式)
- 扩展 DropdownComponentSchema 支持 searchable/clearable/size/label/error 等字段
- A2UIDropdown 渲染器自动检测增强特性,按需切换标准 Select 与 Combobox 模式
- 所有新字段 optional,完全向后兼容
This commit is contained in:
catlog22
2026-02-08 15:21:22 +08:00
parent cafc6d06fe
commit f27e52a7a6
6 changed files with 536 additions and 16 deletions

View File

@@ -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(),
}),
});

View File

@@ -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}>