import { useEffect, useMemo, useRef, useState } from "react"; import { Check, Copy, RotateCcw } from "lucide-react"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "../ui/accordion"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; import { Slider } from "../ui/slider"; import { Switch } from "../ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; import { Textarea } from "../ui/textarea"; import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group"; import { copyTextToClipboard } from "../../lib/clipboard"; import { cn } from "../../lib/cn"; import { DEFAULT_LOOK_PRESET_ID, ENVIRONMENT_PRESETS, LOOK_FLOOR_MODES, LOOK_PRESETS, cloneLookPresetSettings, getLookPresetIdForSettings } from "../../lib/lookSettings"; import FileSheet from "./FileSheet"; const CUSTOM_PRESET_VALUE = "__custom"; const BACKGROUND_MODE_OPTIONS = [ { value: "solid", label: "Solid" }, { value: "linear", label: "Linear" }, { value: "radial", label: "Radial" }, { value: "transparent", label: "Transparent" } ]; const FLOOR_MODE_OPTIONS = [ { value: LOOK_FLOOR_MODES.STAGE, label: "Stage" }, { value: LOOK_FLOOR_MODES.GRID, label: "Grid" }, { value: LOOK_FLOOR_MODES.NONE, label: "None" } ]; const CAD_WORKSPACE_SCENE_TONE_OPTIONS = [ { value: "light", label: "Light" }, { value: "dark", label: "Dark" } ]; const PRIMARY_LIGHT_OPTIONS = [ { value: "directional", label: "Directional" }, { value: "spot", label: "Spot" }, { value: "point", label: "Point" } ]; const controlRowClasses = "space-y-1.5 px-3 py-2"; const fieldLabelClasses = "block text-xs font-medium text-muted-foreground"; const subsectionHeadingClasses = "px-3 pt-2 pb-1 text-xs font-medium text-muted-foreground"; const valueBadgeClasses = "rounded-md bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"; const compactButtonClasses = "h-8 px-2 text-xs"; const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}){1,2}$/; const COLOR_COMMIT_DELAY_MS = 180; const SLIDER_COMMIT_DELAY_MS = 120; function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function formatNumber(value, digits = 2) { const numericValue = Number(value); if (!Number.isFinite(numericValue)) { return "0"; } return numericValue.toFixed(digits); } function Field({ label, value, children, className }) { return (
{label} {value != null ? ( {value} ) : null}
{children}
); } function Section({ title, value, children }) { return ( {title} {children} ); } function Subsection({ title, children }) { return (
{title}
{children}
); } function SwitchRow({ label, checked, onChange }) { return (
{label}
); } function SliderInput({ value, min, max, step = 0.01, onChange }) { const numericValue = Number.isFinite(Number(value)) ? Number(value) : min; const [draftValue, setDraftValue] = useState(numericValue); const commitTimerRef = useRef(null); useEffect(() => { setDraftValue(numericValue); }, [numericValue]); useEffect(() => () => { if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); } }, []); const resolveNextValue = (nextValue) => { const numericNextValue = Number(nextValue); return Number.isFinite(numericNextValue) ? clamp(numericNextValue, min, max) : numericValue; }; const commitValue = (nextValue) => { const resolvedNextValue = resolveNextValue(nextValue); if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); } if (Math.abs(resolvedNextValue - numericValue) > 1e-9) { onChange(resolvedNextValue); } }; const scheduleCommitValue = (nextValue) => { const resolvedNextValue = resolveNextValue(nextValue); setDraftValue(resolvedNextValue); if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); } commitTimerRef.current = setTimeout(() => { commitValue(resolvedNextValue); }, SLIDER_COMMIT_DELAY_MS); }; return ( scheduleCommitValue(nextValue[0] ?? draftValue)} onValueCommit={(nextValue) => commitValue(nextValue[0] ?? draftValue)} className="h-8" /> ); } function ColorInput({ value, onChange }) { const normalizedValue = String(value || "#ffffff"); const [draftValue, setDraftValue] = useState(normalizedValue); const commitTimerRef = useRef(null); useEffect(() => { setDraftValue(normalizedValue); }, [normalizedValue]); useEffect(() => () => { if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); } }, []); const commitValue = (nextValue) => { const normalizedNextValue = String(nextValue || normalizedValue).toLowerCase(); if (!HEX_COLOR_PATTERN.test(normalizedNextValue)) { return; } if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); } if (normalizedNextValue !== normalizedValue.toLowerCase()) { onChange(normalizedNextValue); } }; const scheduleCommitValue = (nextValue) => { const normalizedNextValue = String(nextValue || normalizedValue).toLowerCase(); setDraftValue(normalizedNextValue); if (!HEX_COLOR_PATTERN.test(normalizedNextValue)) { return; } if (commitTimerRef.current) { clearTimeout(commitTimerRef.current); } commitTimerRef.current = setTimeout(() => { commitValue(normalizedNextValue); }, COLOR_COMMIT_DELAY_MS); }; return ( ); } function SegmentedControl({ value, onChange, options }) { const templateColumns = `repeat(${Math.max(options.length, 1)}, minmax(0, 1fr))`; return ( { if (!nextValue) { return; } onChange(nextValue); }} className="grid h-8 w-full min-w-0" style={{ gridTemplateColumns: templateColumns }} > {options.map((option) => ( {option.label} ))} ); } function SelectInput({ value, onChange, options }) { return ( ); } function PresetSwatch({ preset = null }) { if (!preset) { return (