refactor(platform): 统一控件 - 收敛下拉选择与按钮输入交互样式
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Button, Input, InputNumber, Segmented, Select, Slider } from "antd";
|
||||
import { Button, Input, InputNumber, Segmented, Slider } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { ChevronDown, Dice5, Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Sparkles, Video, Workflow as WorkflowIcon } from "lucide-react";
|
||||
@@ -17,6 +17,7 @@ import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
||||
import { workflowProviderPluginEnabled } from "@/lib/plugins/builtin/workflows";
|
||||
import { usePluginStore } from "@/stores/use-plugin-store";
|
||||
import type { CanvasGenerationMode, CanvasNodeData, CanvasNodeMetadata, CanvasVideoEditOperation, CanvasWorkspaceMode } from "@/types/canvas";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
type CanvasConfigNodePanelProps = {
|
||||
node: CanvasNodeData;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Dropdown, Input, InputNumber, Modal, Segmented, Select, Table } from "antd";
|
||||
import { Button, Dropdown, Input, InputNumber, Modal, Segmented, Table } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
StoryboardVideoInputMode,
|
||||
} from "@/types/canvas";
|
||||
import type { TaskStatus } from "@/services/api/task-center";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
const STORYBOARD_PROMPT_MIN_HEIGHT = 40;
|
||||
const STORYBOARD_PROMPT_MAX_HEIGHT = 116;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { App, Button, Input, Modal, Select, Spin } from "antd";
|
||||
import { App, Button, Input, Modal, Spin } from "antd";
|
||||
import { Copy, Link2, RefreshCw, Share2, Unlink } from "lucide-react";
|
||||
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { createCanvasShare, deleteCanvasShare, getCanvasShare, type CanvasShareStatus } from "@/services/api/canvas-share";
|
||||
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
export function CanvasShareModal({ projectId, open, onClose, beforeCreate }: { projectId: string; open: boolean; onClose: () => void; beforeCreate: () => Promise<boolean | void> }) {
|
||||
const { message, modal } = App.useApp();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Select } from "antd";
|
||||
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
const sizeOptions = ["auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "9:16"];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { App, Button, Input, InputNumber, Modal, Segmented, Select } from "antd";
|
||||
import { App, Button, Input, InputNumber, Modal, Segmented } from "antd";
|
||||
import { AudioLines, Check, ListVideo, Plus, Scissors, SkipBack, SkipForward, Trash2 } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@@ -15,6 +15,7 @@ import { resolveMediaUrl } from "@/services/file-storage";
|
||||
import { modelDisplayName, type AiConfig } from "@/stores/use-config-store";
|
||||
import { type CanvasConnection, type CanvasNodeData, type CanvasVideoEditOperation } from "@/types/canvas";
|
||||
import type { TimelineProject } from "@/types/timeline";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
export type CanvasVideoSegmentItem = CanvasTimelineSegmentItem;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { App, Button, Select } from "antd";
|
||||
import { App, Button } from "antd";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { FileText, Images, Plus, RefreshCw } from "lucide-react";
|
||||
import { CachedResourceImage } from "@/components/cached-resource-image";
|
||||
@@ -10,6 +10,7 @@ import { loadAssetLibraryPage } from "@/services/user-data-sync";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import type { Asset } from "@/stores/use-asset-store";
|
||||
import { assetPickerItemsToInsertPayloads, type InsertAssetPayload } from "./asset-picker-modal";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
const categories: AssetCategory[] = ["material", "character", "environment", "prop", "other"];
|
||||
export function CanvasWorkspaceAssetPanel({ onInsert, onManage, onProjectAssets }: { onInsert: (payloads: InsertAssetPayload[]) => Promise<unknown>; onManage: () => void; onProjectAssets?: () => void }) {
|
||||
|
||||
@@ -4,7 +4,7 @@ type FeedTab = ToolScope;
|
||||
import { memo, useEffect, useMemo, useRef, useState, type UIEvent } from "react";
|
||||
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Eye, Loader2, MoreHorizontal, Plus, Star, Trash2, Upload, Wrench, X } from "lucide-react";
|
||||
import { App, Button, Dropdown, Form, Input, Radio, Select, Space, type MenuProps } from "antd";
|
||||
import { App, Button, Dropdown, Form, Input, Radio, Space, type MenuProps } from "antd";
|
||||
|
||||
import { CanvasImagePreview } from "@/components/canvas/canvas-image-preview";
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
@@ -13,6 +13,7 @@ import { WorkspaceErrorState, WorkspaceState } from "@/components/layout/workspa
|
||||
import { uploadMediaFile } from "@/services/file-storage";
|
||||
import { uploadImage } from "@/services/image-storage";
|
||||
import { createTool, deleteTool, listTools, setToolFavorite, type ToolScope, type ToolSummary, type ToolType, type ToolVisibility } from "@/services/api/tools";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
type ToolSubTab = "style" | "effect" | "motion" | "nine_grid";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, Button, ColorPicker, Dropdown, Input, InputNumber, Select, Slider } from "antd";
|
||||
import { App, Button, ColorPicker, Dropdown, Input, InputNumber, Slider } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import type { MenuProps } from "antd";
|
||||
import { Box, BoxSelect, Camera, Circle, Cuboid, FileUp, Focus, Image as ImageIcon, LampDesk, Lightbulb, Plus, Redo2, RotateCcw, Save, Trash2, Undo2, UserRound, Video, X } from "lucide-react";
|
||||
@@ -30,6 +30,7 @@ import { useDirectorWorkbenchStore } from "@/stores/canvas/use-director-workbenc
|
||||
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
||||
import type { CanvasNodeData } from "@/types/canvas";
|
||||
import type { DirectorCamera, DirectorCameraMove, DirectorHumanoidBone, DirectorKeyframeDeleteTarget, DirectorKeyframeEasing, DirectorLight, DirectorObject, DirectorPose, DirectorQuat, DirectorRenderMode, DirectorRig, DirectorScene, DirectorSceneOutput, DirectorShot, DirectorShotSize, DirectorTransform, DirectorVec3 } from "@/types/director";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
export function CanvasDirectorWorkbench({ open, scene, imageNodes, onboardingScope, onClose, onChange, onApply, onDeleteImageNode, onFlush }: { open: boolean; scene: DirectorScene | null; imageNodes: CanvasNodeData[]; onboardingScope: string; onClose: () => void; onChange: (scene: DirectorScene) => void; onApply: (output: DirectorSceneOutput) => Promise<void>; onDeleteImageNode: (nodeId: string) => void; onFlush?: () => void | Promise<void> }) {
|
||||
const { message, modal } = App.useApp();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { Button, Input, InputNumber, Select } from "antd";
|
||||
import { Button, Input, InputNumber } from "antd";
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { AlertTriangle, Box, Check, FileImage, Link2, Plus, Save, SlidersHorizontal, Trash2 } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
import {
|
||||
createStyleProfileSnapshot,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { App, Button, Input, Segmented, Select } from "antd";
|
||||
import { App, Button, Input, Segmented } from "antd";
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import { Braces, Image, Layers3, Save, Sparkles } from "lucide-react";
|
||||
|
||||
import { StyleAssetBindingModal } from "@/components/canvas/style-asset-binding-modal";
|
||||
import { createStyleProfileSnapshot, styleProfileValidationMessage, type StyleProfileSnapshot } from "@/lib/canvas/style-profile";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
type EditorSection = "identity" | "prompt" | "execution";
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { App, Button, Form, Input, Select, Tag } from "antd";
|
||||
import { App, Button, Form, Input, Tag } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { Cloud, ShieldCheck } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { changesRequireOSSRetest, DEFAULT_OSS_PATH_PREFIX, getS3PresetHints, S3_PRESET_OPTIONS, type OSSConnectionTestResult, type OSSProvider, type S3Preset } from "@/lib/oss-settings";
|
||||
import { changesRequireOSSRetest, DEFAULT_OSS_PATH_PREFIX, getS3PresetHints, S3_PRESET_OPTIONS, validateOSSConnectionDraft, type OSSConnectionTestResult, type OSSProvider, type S3Preset } from "@/lib/oss-settings";
|
||||
import { getUserOSSSetting, testUserOSSConnection, updateUserOSSSetting, type UserOSSSetting } from "@/services/api/resources";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { StatusBadge } from "@/components/ui/base/badges";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
type OSSFormValues = {
|
||||
enabled?: boolean;
|
||||
@@ -104,6 +105,11 @@ export function UserOSSSettingsForm() {
|
||||
|
||||
const testConnection = async () => {
|
||||
const values = await form.validateFields();
|
||||
const validationError = validateOSSConnectionDraft(values);
|
||||
if (validationError) {
|
||||
message.error(validationError);
|
||||
return;
|
||||
}
|
||||
setTesting(true);
|
||||
try {
|
||||
const result = await testUserOSSConnection(connectionInput(values));
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Button, Select } from "antd";
|
||||
import { Button } from "antd";
|
||||
import { ChevronLeft, ChevronRight, ListFilter, RotateCcw } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
export function WorkspacePage({ children, className, grid = false, fluid = false, scroll = true }: { children: ReactNode; className?: string; grid?: boolean; fluid?: boolean; scroll?: boolean }) {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ImageSizePresetsEditor } from "./image-size-presets-editor";
|
||||
import { imageSizeConfigWithPresets, imageSizePresets } from "@/lib/image-size-presets";
|
||||
import { Input, InputNumber, Select } from "antd";
|
||||
import { Input, InputNumber } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { SegmentedControl } from "@/components/ui/base/segmented-control";
|
||||
import type { ReactNode } from "react";
|
||||
@@ -8,6 +8,7 @@ import type { ReactNode } from "react";
|
||||
import { defaultImageCapabilityConfig, defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ImageCapabilityConfig, type ModelCapabilityConfig, type TextCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import type { ModelProtocol } from "@/lib/model-protocols";
|
||||
import { VIDEO_RESOLUTION_CAPABILITY_OPTIONS } from "@/lib/video-generation-options";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
const ratioOptions = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"];
|
||||
const operationOptions = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ButtonHTMLAttributes } from "react";
|
||||
import { useState, type ButtonHTMLAttributes } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Loader2, type LucideIcon } from "lucide-react";
|
||||
|
||||
@@ -19,7 +19,7 @@ import { cn } from "@/lib/utils";
|
||||
* 明暗:全部颜色走 token;active 按压底用 bg-surface-active,明暗自动适配。
|
||||
*/
|
||||
export const iconButtonVariants = cva(
|
||||
["inline-flex shrink-0 items-center justify-center rounded-md", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45", "motion-safe:active:scale-[0.96]"],
|
||||
["inline-flex shrink-0 items-center justify-center rounded-md", "outline-none focus-visible:outline-none data-[input-modality=keyboard]:focus-visible:ring-2 data-[input-modality=keyboard]:focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45", "motion-safe:active:scale-[0.96]"],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -58,9 +58,31 @@ export type IconButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export function IconButton({ variant, size, icon: Icon, loading = false, active = false, className, type = "button", disabled, ...props }: IconButtonProps) {
|
||||
export function IconButton({ variant, size, icon: Icon, loading = false, active = false, className, type = "button", disabled, onPointerDown, onKeyDown, onBlur, ...props }: IconButtonProps) {
|
||||
const [inputModality, setInputModality] = useState<"pointer" | "keyboard">("pointer");
|
||||
return (
|
||||
<button data-slot="icon-button" type={type} disabled={disabled || loading} aria-busy={loading || undefined} data-active={active || undefined} className={cn(iconButtonVariants({ variant, size }), className)} {...props}>
|
||||
<button
|
||||
data-slot="icon-button"
|
||||
data-input-modality={inputModality}
|
||||
type={type}
|
||||
disabled={disabled || loading}
|
||||
aria-busy={loading || undefined}
|
||||
data-active={active || undefined}
|
||||
className={cn(iconButtonVariants({ variant, size }), className)}
|
||||
onPointerDown={(event) => {
|
||||
setInputModality("pointer");
|
||||
onPointerDown?.(event);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Tab" || event.key === "Enter" || event.key === " ") setInputModality("keyboard");
|
||||
onKeyDown?.(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setInputModality("pointer");
|
||||
onBlur?.(event);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{loading ? <Loader2 aria-hidden className="animate-spin" /> : <Icon aria-hidden />}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
import { useState, type ButtonHTMLAttributes, type ReactNode } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
@@ -21,7 +21,7 @@ import { cn } from "@/lib/utils";
|
||||
* 明暗:全部颜色走 token;active 用按压底语义 token,明暗自动适配。
|
||||
*/
|
||||
export const toolButtonVariants = cva(
|
||||
["inline-flex select-none items-center justify-center gap-1.5 rounded-md", "font-medium transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45"],
|
||||
["inline-flex select-none items-center justify-center gap-1.5 rounded-md px-3", "font-medium transition-colors", "outline-none focus-visible:outline-none data-[input-modality=keyboard]:focus-visible:ring-2 data-[input-modality=keyboard]:focus-visible:ring-ring", "disabled:pointer-events-none disabled:opacity-45"],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -74,12 +74,14 @@ export type ToolButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
label?: ReactNode;
|
||||
};
|
||||
|
||||
export function ToolButton({ variant, size, tone, icon, active = false, expands = false, loading = false, label, className, type = "button", disabled, ...props }: ToolButtonProps) {
|
||||
export function ToolButton({ variant, size, tone, icon, active = false, expands = false, loading = false, label, className, type = "button", disabled, onPointerDown, onKeyDown, onBlur, ...props }: ToolButtonProps) {
|
||||
const [inputModality, setInputModality] = useState<"pointer" | "keyboard">("pointer");
|
||||
const pressed = !expands && active;
|
||||
const expanded = expands && active;
|
||||
return (
|
||||
<button
|
||||
data-slot="tool-button"
|
||||
data-input-modality={inputModality}
|
||||
type={type}
|
||||
disabled={disabled || loading}
|
||||
aria-pressed={pressed || undefined}
|
||||
@@ -87,6 +89,18 @@ export function ToolButton({ variant, size, tone, icon, active = false, expands
|
||||
aria-busy={loading || undefined}
|
||||
data-active={active || undefined}
|
||||
className={cn(toolButtonVariants({ variant, size, tone, active }), className)}
|
||||
onPointerDown={(event) => {
|
||||
setInputModality("pointer");
|
||||
onPointerDown?.(event);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Tab" || event.key === "Enter" || event.key === " ") setInputModality("keyboard");
|
||||
onKeyDown?.(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setInputModality("pointer");
|
||||
onBlur?.(event);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{loading ? <Loader2 aria-hidden className="animate-spin" /> : icon}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { Select, type SelectOption, type SelectProps, type SelectSize } from "./select";
|
||||
export { Select, type SelectOption, type SelectProps, type SelectSize, type SelectAppearance } from "./select";
|
||||
|
||||
@@ -1,159 +1,70 @@
|
||||
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { Button as AriaButton, ListBox as AriaListBox, ListBoxItem as AriaListBoxItem, Popover as AriaPopover, Select as AriaSelect } from "react-aria-components";
|
||||
import { Check, ChevronDown, X } from "lucide-react";
|
||||
import { forwardRef, useState, type ReactElement, type ReactNode, type RefAttributes } from "react";
|
||||
import AntSelect, { type BaseOptionType, type DefaultOptionType, type RefSelectProps, type SelectProps as AntSelectProps } from "antd/es/select";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Select — 下拉选择(自研,react-aria-components 单选壳,无远程/多选)。
|
||||
* 影策唯一下拉选择组件。
|
||||
*
|
||||
* - API 对齐 AntD Select 的单选子集:value/onChange(options 值)/options/placeholder/
|
||||
* disabled/allowClear/size/className,可直接替换纯单选场景;可直接置于 antd Form.Item。
|
||||
* - 浮层定位、进出场、焦点与类型选择由 RAC 承担(ADR-0008:复杂控件以 RAC 为无障碍地基)。
|
||||
* - Popover isNonModal:避免 RAC modal 滚动锁把页面滚动条锁死(同 boardui 注释);
|
||||
* isDismissable 与 isNonModal 互斥,故外部 dismiss 手动处理(全局 pointerdown capture)。
|
||||
* - 样式只吃语义 token:trigger border-border/bg-background/hover:border-foreground/40、
|
||||
* popover bg-surface-strong + hairline、item hover:bg-surface-hover、选中 Check 前景色,无裸色。
|
||||
* - 范围外(本批不做,后续补):mode="multiple"/"tags"、showSearch(需 ComboBox)、
|
||||
* loading、labelInValue、maxCount、suffixIcon。
|
||||
* 对标:AntD Select(单选);boardui base/select。
|
||||
* 所有单选、多选、可搜索选择都复用 Ant Design 的同一实现;本文件只负责
|
||||
* 统一项目内的入口、尺寸别名、ariaLabel 兼容和公共 class,避免页面各自维护
|
||||
* 一套浮层和焦点样式。视觉规则集中在 app-theme.ts 与 globals.css。
|
||||
*/
|
||||
|
||||
export type SelectSize = "sm" | "md";
|
||||
export type SelectAppearance = "soft" | "pill" | "field";
|
||||
|
||||
export interface SelectOption<V extends string = string> {
|
||||
export type SelectOption<V extends string | number = string> = DefaultOptionType & {
|
||||
value: V;
|
||||
label: ReactNode;
|
||||
disabled?: boolean;
|
||||
/** 原生 title */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface SelectProps<V extends string = string> {
|
||||
value?: V;
|
||||
onChange?: (value: V) => void;
|
||||
options?: Array<SelectOption<V>>;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
allowClear?: boolean;
|
||||
size?: SelectSize;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TRIGGER_CLASS: Record<SelectSize, string> = {
|
||||
sm: "h-7 text-xs",
|
||||
md: "h-8 text-sm",
|
||||
};
|
||||
|
||||
export function Select<V extends string = string>({ value, onChange, options = [], placeholder, disabled = false, allowClear = false, size = "md", ariaLabel, className }: SelectProps<V>) {
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const popoverRef = useRef<HTMLDivElement | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [popoverWidth, setPopoverWidth] = useState<number | null>(null);
|
||||
export type SelectProps<ValueType = any, OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType> = Omit<AntSelectProps<ValueType, OptionType>, "size"> & {
|
||||
/** 兼容旧的基础 Select API;完整 AntD 尺寸仍可使用 small/middle/large。 */
|
||||
size?: SelectSize | "small" | "middle" | "large";
|
||||
/** 兼容基础 Select 的显式无障碍标签。 */
|
||||
ariaLabel?: string;
|
||||
/** 统一触发器外观;所有外观都无描边,差异只体现在圆角与内边距。 */
|
||||
appearance?: SelectAppearance;
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const measure = () => {
|
||||
const width = triggerRef.current?.offsetWidth;
|
||||
if (width) setPopoverWidth(width + 2);
|
||||
};
|
||||
measure();
|
||||
const observer = typeof ResizeObserver !== "undefined" ? new ResizeObserver(measure) : null;
|
||||
if (observer && triggerRef.current) observer.observe(triggerRef.current);
|
||||
return () => observer?.disconnect();
|
||||
}, [open]);
|
||||
type UnifiedSelect = (<ValueType = any, OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>(props: SelectProps<ValueType, OptionType> & RefAttributes<RefSelectProps>) => ReactElement) & { displayName?: string };
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) return;
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
const target = event.target as Node;
|
||||
const insideTrigger = triggerRef.current?.contains(target);
|
||||
const insidePopover = popoverRef.current?.contains(target);
|
||||
if (!insideTrigger && !insidePopover) setOpen(false);
|
||||
};
|
||||
window.addEventListener("pointerdown", onPointerDown, true);
|
||||
return () => window.removeEventListener("pointerdown", onPointerDown, true);
|
||||
}, [open]);
|
||||
|
||||
const selected = options.find((option) => option.value === value);
|
||||
const displayLabel = selected?.label ?? value;
|
||||
|
||||
const clear = () => {
|
||||
setOpen(false);
|
||||
onChange?.(undefined as unknown as V);
|
||||
};
|
||||
const sizeMap: Record<SelectSize, "small" | "middle"> = {
|
||||
sm: "small",
|
||||
md: "middle",
|
||||
};
|
||||
|
||||
export const Select = forwardRef<RefSelectProps, SelectProps>(function Select({ size, ariaLabel, appearance = "pill", className, variant, onMouseDown, onKeyDown, onFocus, onBlur, ...props }, ref) {
|
||||
const [inputModality, setInputModality] = useState<"unknown" | "pointer" | "keyboard">("unknown");
|
||||
const normalizedSize = size && size in sizeMap ? sizeMap[size as SelectSize] : size;
|
||||
return (
|
||||
<AriaSelect
|
||||
isDisabled={disabled}
|
||||
isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
selectedKey={value ?? undefined}
|
||||
onSelectionChange={(key) => {
|
||||
setOpen(false);
|
||||
if (key != null) onChange?.(key as V);
|
||||
}}
|
||||
<AntSelect
|
||||
ref={ref}
|
||||
{...props}
|
||||
variant={variant ?? "filled"}
|
||||
size={normalizedSize as AntSelectProps["size"]}
|
||||
aria-label={ariaLabel}
|
||||
className={cn("inline-flex w-full", className)}
|
||||
>
|
||||
<AriaButton
|
||||
ref={triggerRef}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center justify-between gap-1.5 rounded-md border border-border bg-background px-2.5 font-normal text-foreground outline-none transition-colors motion-reduce:transition-none",
|
||||
"hover:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
||||
"disabled:cursor-not-allowed disabled:opacity-55",
|
||||
open && "border-foreground/60",
|
||||
TRIGGER_CLASS[size],
|
||||
)}
|
||||
>
|
||||
<span className={cn("min-w-0 flex-1 truncate text-left", displayLabel === undefined && "text-muted-foreground")}>{displayLabel === undefined ? placeholder : displayLabel}</span>
|
||||
{allowClear && value !== undefined && !disabled && (
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label="清空选择"
|
||||
className="shrink-0 cursor-pointer rounded p-0.5 text-muted-foreground hover:bg-surface-hover hover:text-foreground"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
clear();
|
||||
}}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</span>
|
||||
)}
|
||||
<ChevronDown aria-hidden="true" className={cn("size-4 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none", open && "rotate-180")} />
|
||||
</AriaButton>
|
||||
<AriaPopover ref={popoverRef} isNonModal placement="bottom" offset={4} style={popoverWidth ? { width: popoverWidth } : undefined} className={cn("z-50 rounded-lg border border-border bg-surface-strong p-1 shadow-lg", "ra-pop-in")}>
|
||||
<AriaListBox aria-label={ariaLabel ?? placeholder ?? "选项"} className="max-h-60 overflow-auto outline-none">
|
||||
{options.map((option) => (
|
||||
<AriaListBoxItem
|
||||
key={option.value}
|
||||
id={option.value}
|
||||
textValue={typeof option.label === "string" ? option.label : option.value}
|
||||
isDisabled={option.disabled}
|
||||
className={({ isFocused, isSelected, isDisabled: itemDisabled }) =>
|
||||
cn(
|
||||
"flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors motion-reduce:transition-none",
|
||||
"focus-visible:bg-surface-hover",
|
||||
(isFocused || isSelected) && "bg-surface-hover",
|
||||
isSelected ? "font-medium text-foreground" : "text-muted-foreground",
|
||||
itemDisabled && "cursor-not-allowed opacity-50",
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
<span className={cn("min-w-0 flex-1 truncate")} title={option.title}>
|
||||
{option.label}
|
||||
</span>
|
||||
{isSelected && <Check className="size-3.5 shrink-0 text-foreground" />}
|
||||
</>
|
||||
)}
|
||||
</AriaListBoxItem>
|
||||
))}
|
||||
</AriaListBox>
|
||||
</AriaPopover>
|
||||
</AriaSelect>
|
||||
className={cn("app-unified-select", `app-unified-select--${appearance}`, className)}
|
||||
data-input-modality={inputModality}
|
||||
onMouseDown={(event) => {
|
||||
setInputModality("pointer");
|
||||
onMouseDown?.(event);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
setInputModality("keyboard");
|
||||
onKeyDown?.(event);
|
||||
}}
|
||||
onFocus={(event) => {
|
||||
setInputModality((current) => current === "unknown" ? "keyboard" : current);
|
||||
onFocus?.(event);
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setInputModality("unknown");
|
||||
onBlur?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}) as UnifiedSelect;
|
||||
|
||||
Select.displayName = "UnifiedSelect";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Rare UI Fluid Orb inspired launcher: isolated from canvas wheel/pointer gestures. */
|
||||
.fluid-orb {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, var(--fluid-orb-color, #7164f6) 18%, white);
|
||||
|
||||
@@ -81,12 +81,13 @@ float fbm(vec3 p) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / u_resolution.xy;
|
||||
vec2 p = uv * 2.0 - 1.0;
|
||||
p.x *= u_resolution.x / u_resolution.y;
|
||||
// Center on the pixel grid. Dividing by the canvas size and then
|
||||
// stretching x by the aspect ratio shifts the disc whenever the
|
||||
// backing store is not square (border box, DPR rounding).
|
||||
vec2 p = (gl_FragCoord.xy - u_resolution.xy * 0.5) / (0.5 * min(u_resolution.x, u_resolution.y));
|
||||
float r = length(p);
|
||||
if (r > 1.0) discard;
|
||||
float y = uv.y;
|
||||
float y = p.y * 0.5 + 0.5;
|
||||
float t = u_time * 0.18;
|
||||
float n1 = fbm(vec3(p * 1.28, t));
|
||||
float n2 = fbm(vec3(p * 2.35 + n1 * 0.55, t * 1.22));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Collapse, Input, InputNumber, Select, Tag } from "antd";
|
||||
import { Button, Collapse, Input, InputNumber, Tag } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ListFilter, ListPlus, Power, PowerOff } from "lucide-react";
|
||||
import type { WorkflowFieldMapping } from "@/stores/use-config-store";
|
||||
import { workflowFieldChoiceValues, workflowFieldConfigurationError, workflowFieldNumberBounds, workflowFieldPresetOptions } from "@/lib/model-capabilities";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
const sourceOptions = [
|
||||
{ label: "保留工作流默认值", value: "" },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Select } from "antd";
|
||||
import { Button } from "antd";
|
||||
import { IconButton } from "@/components/ui/base/buttons";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { Maximize, Power, PowerOff, ZoomIn, ZoomOut } from "lucide-react";
|
||||
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as
|
||||
|
||||
import { WorkflowFieldMappingEditor } from "@/components/workflow-field-mapping-editor";
|
||||
import type { WorkflowFieldMapping, WorkflowGraphPreview } from "@/stores/use-config-store";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
type WorkflowNode = {
|
||||
id: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, Button, Input, InputNumber, Progress, Select } from "antd";
|
||||
import { App, Button, Input, InputNumber, Progress } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { FileAudio, FileImage, Film, Grip, Play, RotateCcw, Square, Upload, WandSparkles } from "lucide-react";
|
||||
@@ -10,6 +10,7 @@ import { runBackendGenerationTask, type BackendGenerationResult } from "@/servic
|
||||
import { useConfigStore, type AiConfig, type RunningHubCapability, type WorkflowFieldMapping } from "@/stores/use-config-store";
|
||||
import type { ReferenceImage } from "@/types/image";
|
||||
import type { ReferenceAudio, ReferenceVideo } from "@/types/media";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
|
||||
type WorkflowProvider = "runninghub";
|
||||
type MediaKind = "image" | "video" | "audio";
|
||||
|
||||
Reference in New Issue
Block a user