项目文件夹

文件
2026-07-13 13:08:41 +08:00

308 行
9.7 KiB
TypeScript

import { LanguageType, PresentationConfig } from "../type";
import { useEffect, useRef, useState } from "react";
import { Check, ChevronUp, Languages } from "lucide-react";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import AdvanceSettings from "./AdvanceSettings";
import {
clampSlideCountValue,
MAX_NUMBER_OF_SLIDES,
} from "@/utils/presentationLimits";
// Types
interface ConfigurationSelectsProps {
config: PresentationConfig;
onConfigChange: (key: keyof PresentationConfig, value: any) => void;
}
type SlideOption =
| "5"
| "8"
| "9"
| "10"
| "11"
| "12"
| "13"
| "14"
| "15"
| "16"
| "17"
| "18"
| "19"
| "20";
// Constants
const SLIDE_OPTIONS: SlideOption[] = [
"5",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
];
/**
* Renders a select component for slide count
*/
const SlideCountSelect: React.FC<{
value: string | null;
onValueChange: (value: string) => void;
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ value, onValueChange, open, onOpenChange }) => {
const [customInput, setCustomInput] = useState(
value && !SLIDE_OPTIONS.includes(value as SlideOption) ? value : ""
);
const isSelectingPresetRef = useRef(false);
useEffect(() => {
if (value && !SLIDE_OPTIONS.includes(value as SlideOption)) {
setCustomInput(value);
} else {
setCustomInput("");
}
}, [value]);
useEffect(() => {
if (!open) {
isSelectingPresetRef.current = false;
}
}, [open]);
const sanitizeToPositiveInteger = (raw: string): string => {
return clampSlideCountValue(raw);
};
const applyCustomValue = () => {
const sanitized = sanitizeToPositiveInteger(customInput);
if (sanitized && Number(sanitized) > 0) {
onValueChange(sanitized);
onOpenChange(false);
}
};
const displayLabel = value ? `${value} slides` : "Auto slides";
return (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>
<button
role="combobox"
name="slides"
data-testid="slides-select"
aria-expanded={open}
className="flex h-[34px] items-center justify-between gap-2 overflow-hidden rounded-full bg-white px-3.5 font-syne font-medium text-[#191919] shadow-sm ring-1 ring-inset ring-slate-200 focus-visible:ring-[#5146E5]/30 min-[1800px]:h-10 min-[1800px]:px-4 min-[2200px]:h-11 min-[2200px]:px-5"
>
<svg
className="h-3.5 w-3.5 min-[1800px]:h-4 min-[1800px]:w-4 min-[2200px]:h-5 min-[2200px]:w-5"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
>
<path
d="M4.0835 12.25H9.91683"
stroke="black"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.6665 1.75H2.33317C1.68884 1.75 1.1665 2.27233 1.1665 2.91667V8.75C1.1665 9.39433 1.68884 9.91667 2.33317 9.91667H11.6665C12.3108 9.91667 12.8332 9.39433 12.8332 8.75V2.91667C12.8332 2.27233 12.3108 1.75 11.6665 1.75Z"
stroke="black"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className="flex flex-1 items-center gap-2.5">
<span className="text-xs font-medium min-[1800px]:text-sm min-[2200px]:text-base">{displayLabel}</span>
</span>
<ChevronUp className="ml-2 h-4 w-4 shrink-0 min-[1800px]:h-5 min-[1800px]:w-5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[140px] p-0 font-syne min-[1800px]:w-[160px] min-[2200px]:w-[180px]" align="end">
<div
className="sticky top-0 z-10 bg-white p-2 border-b"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-2">
<Input
inputMode="numeric"
pattern="[0-9]*"
max={MAX_NUMBER_OF_SLIDES}
value={customInput}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
onChange={(e) => {
const next = sanitizeToPositiveInteger(e.target.value);
setCustomInput(next);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
applyCustomValue();
}
}}
onBlur={() => {
if (!isSelectingPresetRef.current) {
applyCustomValue();
}
}}
placeholder="--"
className="h-8 w-16 px-2 text-sm min-[1800px]:h-9 min-[1800px]:w-20 min-[1800px]:text-base"
/>
<span className="text-sm font-medium min-[1800px]:text-base">slides</span>
</div>
</div>
<Command>
<CommandList>
<CommandGroup>
{SLIDE_OPTIONS.map((option) => (
<CommandItem
key={option}
value={`${option} slides`}
role="option"
onPointerDownCapture={() => {
isSelectingPresetRef.current = true;
}}
onMouseDownCapture={() => {
isSelectingPresetRef.current = true;
}}
onSelect={() => {
onValueChange(option);
setCustomInput("");
isSelectingPresetRef.current = false;
onOpenChange(false);
}}
className="font-syne text-sm font-medium min-[1800px]:text-base"
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option ? "opacity-100" : "opacity-0"
)}
/>
{option} slides
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
/**
* Renders a language selection component with search functionality
*/
const LanguageSelect: React.FC<{
value: string | null;
onValueChange: (value: string) => void;
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ value, onValueChange, open, onOpenChange }) => (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>
<button
role="combobox"
name="language"
data-testid="language-select"
aria-expanded={open}
className="flex h-[34px] max-w-[160px] items-center gap-2 overflow-hidden rounded-full bg-white px-3.5 font-syne font-semibold text-[#191919] shadow-sm ring-1 ring-inset ring-slate-200 min-[1800px]:h-10 min-[1800px]:max-w-[190px] min-[1800px]:px-4 min-[2200px]:h-11 min-[2200px]:max-w-[220px] min-[2200px]:px-5"
>
<Languages className="h-3.5 w-3.5 flex-shrink-0 min-[1800px]:h-4 min-[1800px]:w-4 min-[2200px]:h-5 min-[2200px]:w-5" />
<span className="flex-1 truncate text-xs font-medium min-[1800px]:text-sm min-[2200px]:text-base">
{value || "Select language"}
</span>
{/* <span className="w-[50%] text-left">
</span> */}
<ChevronUp className="ml-2 h-4 w-4 flex-shrink-0 min-[1800px]:h-5 min-[1800px]:w-5" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0 min-[1800px]:w-[340px] min-[2200px]:w-[380px]" align="end">
<Command>
<CommandInput
placeholder="Search language..."
className="font-instrument_sans"
/>
<CommandList>
<CommandEmpty>No language found.</CommandEmpty>
<CommandGroup>
{Object.values(LanguageType).map((language) => (
<CommandItem
key={language}
value={language}
role="option"
onSelect={(currentValue) => {
onValueChange(currentValue);
onOpenChange(false);
}}
className="font-instrument_sans"
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === language ? "opacity-100" : "opacity-0"
)}
/>
{language}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
export function ConfigurationSelects({
config,
onConfigChange,
}: ConfigurationSelectsProps) {
const [openSlides, setOpenSlides] = useState(false);
const [openLanguage, setOpenLanguage] = useState(false);
return (
<div className="order-1 flex flex-wrap items-center gap-4 min-[1800px]:gap-5">
<SlideCountSelect
value={config.slides}
onValueChange={(value) => onConfigChange("slides", value)}
open={openSlides}
onOpenChange={setOpenSlides}
/>
<LanguageSelect
value={config.language}
onValueChange={(value) => onConfigChange("language", value)}
open={openLanguage}
onOpenChange={setOpenLanguage}
/>
<AdvanceSettings config={config} onConfigChange={onConfigChange} />
</div>
);
}