refactor(platform): 统一控件 - 收敛下拉选择与按钮输入交互样式

This commit is contained in:
ddcat666
2026-09-25 00:01:09 +08:00
parent c0d223f357
commit 8a691f76c9
61 changed files with 661 additions and 237 deletions

View File

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

View File

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

View File

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

View File

@@ -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"];

View File

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

View File

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

View File

@@ -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";

View File

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

View File

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

View File

@@ -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";

View File

@@ -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));

View File

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

View File

@@ -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 = [

View File

@@ -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>
);

View File

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

View File

@@ -1 +1 @@
export { Select, type SelectOption, type SelectProps, type SelectSize } from "./select";
export { Select, type SelectOption, type SelectProps, type SelectSize, type SelectAppearance } from "./select";

View File

@@ -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";

View File

@@ -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);

View File

@@ -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));

View File

@@ -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: "" },

View File

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

View File

@@ -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";