返回 JoyAI-Echo
AspectRatioPicker.tsx
根目录 / echo_longvideo / Director_Agent / webui / src / components / thread / AspectRatioPicker.tsx
1 import { ChevronDown, Ratio } from "lucide-react";
2
3 import {
4 DropdownMenu,
5 DropdownMenuContent,
6 DropdownMenuItem,
7 DropdownMenuTrigger,
8 } from "@/components/ui/dropdown-menu";
9 import { cn } from "@/lib/utils";
10
11 export type VideoSize = { width: number; height: number };
12
13 export const VIDEO_SIZE_PRESETS = [
14 { label: "16:9", width: 1280, height: 736 },
15 { label: "1:1", width: 736, height: 736 },
16 { label: "9:16", width: 736, height: 1280 },
17 ] as const;
18
19 export const DEFAULT_VIDEO_SIZE: VideoSize = VIDEO_SIZE_PRESETS[0];
20
21 interface AspectRatioPickerProps {
22 value: VideoSize;
23 onChange: (value: VideoSize) => void;
24 disabled?: boolean;
25 size?: "hero" | "thread";
26 }
27
28 export function AspectRatioPicker({
29 value,
30 onChange,
31 disabled,
32 size = "thread",
33 }: AspectRatioPickerProps) {
34 const isHero = size === "hero";
35 const selected =
36 VIDEO_SIZE_PRESETS.find(
37 (preset) =>
38 preset.width === value.width && preset.height === value.height,
39 ) ?? VIDEO_SIZE_PRESETS[0];
40
41 return (
42 <DropdownMenu>
43 <DropdownMenuTrigger asChild disabled={disabled}>
44 <span
45 aria-label="Select aspect ratio"
46 className={cn(
47 "cursor-pointer",
48 "group inline-flex min-w-0 items-center gap-1.5 rounded-full border px-2.5 py-1",
49 "border-foreground/10 bg-foreground/[0.035] font-medium text-foreground/80",
50 "whitespace-nowrap transition-colors hover:border-foreground/15 hover:bg-foreground/[0.04]",
51 "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/20",
52 "disabled:cursor-not-allowed disabled:opacity-60",
53 "data-[state=open]:border-foreground/15 data-[state=open]:bg-foreground/[0.04]",
54 isHero ? "text-[11px]" : "text-[12px]",
55 )}
56 >
57 <Ratio className="h-3 w-3" aria-hidden />
58 <span>{selected.label}</span>
59 <ChevronDown
60 className={cn(
61 "h-2.5 w-2.5 opacity-60 transition-transform duration-200",
62 "group-data-[state=open]:rotate-180",
63 )}
64 aria-hidden
65 />
66 </span>
67 </DropdownMenuTrigger>
68 <DropdownMenuContent
69 side="top"
70 align="start"
71 sideOffset={6}
72 className="min-w-[var(--radix-dropdown-menu-trigger-width)] rounded-xl p-1"
73 >
74 {VIDEO_SIZE_PRESETS.map((preset) => {
75 const isSelected =
76 preset.width === value.width && preset.height === value.height;
77 return (
78 <DropdownMenuItem
79 key={preset.label}
80 onSelect={() =>
81 onChange({ width: preset.width, height: preset.height })
82 }
83 title={`${preset.width} × ${preset.height}`}
84 className={cn(
85 "cursor-pointer",
86 "justify-center rounded-lg px-3 py-1.5 font-mono text-[12px]",
87 isSelected &&
88 "bg-foreground/[0.06] font-medium text-foreground/85",
89 )}
90 >
91 {preset.label}
92 </DropdownMenuItem>
93 );
94 })}
95 </DropdownMenuContent>
96 </DropdownMenu>
97 );
98 }
99
99 lines Plain Text