import { Textarea } from "@/components/ui/textarea"; import { FontPicker } from "@/components/ui/font-picker"; import type { FontFamily } from "@/constants/font-constants"; import type { TextElement } from "@/types/timeline"; import { Slider } from "@/components/ui/slider"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { useReducer, useRef } from "react"; import { PanelBaseView } from "@/components/editor/panels/panel-base-view"; import { PropertyGroup, PropertyItem, PropertyItemLabel, PropertyItemValue, } from "./property-item"; import { ColorPicker } from "@/components/ui/color-picker"; import { uppercase } from "@/utils/string"; import { clamp } from "@/utils/math"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_COLOR } from "@/constants/project-constants"; import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/constants/text-constants"; export function TextProperties({ element, trackId, }: { element: TextElement; trackId: string; }) { const editor = useEditor(); const containerRef = useRef(null); const [, forceRender] = useReducer((x: number) => x + 1, 0); const isEditingFontSize = useRef(false); const isEditingOpacity = useRef(false); const isEditingContent = useRef(false); const fontSizeDraft = useRef(""); const opacityDraft = useRef(""); const contentDraft = useRef(""); const fontSizeDisplay = isEditingFontSize.current ? fontSizeDraft.current : element.fontSize.toString(); const opacityDisplay = isEditingOpacity.current ? opacityDraft.current : Math.round(element.opacity * 100).toString(); const contentDisplay = isEditingContent.current ? contentDraft.current : element.content; const lastSelectedColor = useRef(DEFAULT_COLOR); const initialFontSizeRef = useRef(null); const initialOpacityRef = useRef(null); const initialContentRef = useRef(null); const initialColorRef = useRef(null); const initialBgColorRef = useRef(null); const handleFontSizeChange = ({ value }: { value: string }) => { fontSizeDraft.current = value; forceRender(); if (value.trim() !== "") { if (initialFontSizeRef.current === null) { initialFontSizeRef.current = element.fontSize; } const parsed = parseInt(value, 10); const fontSize = Number.isNaN(parsed) ? element.fontSize : clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE }); editor.timeline.updateElements({ updates: [{ trackId, elementId: element.id, updates: { fontSize } }], pushHistory: false, }); } }; const handleFontSizeBlur = () => { if (initialFontSizeRef.current !== null) { const parsed = parseInt(fontSizeDraft.current, 10); const fontSize = Number.isNaN(parsed) ? element.fontSize : clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE }); editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { fontSize: initialFontSizeRef.current }, }, ], pushHistory: false, }); editor.timeline.updateElements({ updates: [{ trackId, elementId: element.id, updates: { fontSize } }], pushHistory: true, }); initialFontSizeRef.current = null; } isEditingFontSize.current = false; fontSizeDraft.current = ""; forceRender(); }; const handleOpacityChange = ({ value }: { value: string }) => { opacityDraft.current = value; forceRender(); if (value.trim() !== "") { if (initialOpacityRef.current === null) { initialOpacityRef.current = element.opacity; } const parsed = parseInt(value, 10); const opacityPercent = Number.isNaN(parsed) ? Math.round(element.opacity * 100) : clamp({ value: parsed, min: 0, max: 100 }); editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { opacity: opacityPercent / 100 }, }, ], pushHistory: false, }); } }; const handleOpacityBlur = () => { if (initialOpacityRef.current !== null) { const parsed = parseInt(opacityDraft.current, 10); const opacityPercent = Number.isNaN(parsed) ? Math.round(element.opacity * 100) : clamp({ value: parsed, min: 0, max: 100 }); editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { opacity: initialOpacityRef.current }, }, ], pushHistory: false, }); editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { opacity: opacityPercent / 100 }, }, ], pushHistory: true, }); initialOpacityRef.current = null; } isEditingOpacity.current = false; opacityDraft.current = ""; forceRender(); }; const handleColorChange = ({ color }: { color: string }) => { if (color !== "transparent") { lastSelectedColor.current = color; } if (initialBgColorRef.current === null) { initialBgColorRef.current = element.backgroundColor; } if (initialBgColorRef.current !== null) { editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { backgroundColor: color }, }, ], pushHistory: false, }); } else { editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { backgroundColor: color }, }, ], }); } }; const handleColorChangeEnd = ({ color }: { color: string }) => { if (initialBgColorRef.current !== null) { editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { backgroundColor: initialBgColorRef.current }, }, ], pushHistory: false, }); editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { backgroundColor: `#${color}` }, }, ], pushHistory: true, }); initialBgColorRef.current = null; } }; return (