From fca99d6126c31fbb18ed9f1034cee6f940b040e8 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 10 Feb 2026 16:28:21 +0100 Subject: refactor --- .../editor/panels/properties/text-properties.tsx | 905 ++++++++++++--------- 1 file changed, 516 insertions(+), 389 deletions(-) (limited to 'apps/web/src/components/editor/panels/properties/text-properties.tsx') diff --git a/apps/web/src/components/editor/panels/properties/text-properties.tsx b/apps/web/src/components/editor/panels/properties/text-properties.tsx index 35230c75..22e50e4e 100644 --- a/apps/web/src/components/editor/panels/properties/text-properties.tsx +++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx @@ -5,29 +5,17 @@ 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 { useState, useRef } from "react"; +import { useReducer, useRef } from "react"; import { PanelBaseView } from "@/components/editor/panels/panel-base-view"; import { - TEXT_PROPERTIES_TABS, - isTextPropertiesTab, - useTextPropertiesStore, -} from "@/stores/text-properties-store"; -import { + PropertyGroup, PropertyItem, PropertyItemLabel, PropertyItemValue, } from "./property-item"; import { ColorPicker } from "@/components/ui/color-picker"; -import { cn } from "@/utils/ui"; -import { capitalizeFirstLetter, uppercase } from "@/utils/string"; +import { uppercase } from "@/utils/string"; import { clamp } from "@/utils/math"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { LayoutGridIcon } from "@hugeicons/core-free-icons"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_COLOR } from "@/constants/project-constants"; import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/constants/text-constants"; @@ -40,48 +28,86 @@ export function TextProperties({ trackId: string; }) { const editor = useEditor(); - const { activeTab, setActiveTab } = useTextPropertiesStore(); const containerRef = useRef(null); - const [fontSizeInput, setFontSizeInput] = useState( - element.fontSize.toString(), - ); - const [opacityInput, setOpacityInput] = useState( - Math.round(element.opacity * 100).toString(), - ); + 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 }) => { - setFontSizeInput(value); + 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 = () => { - const parsed = parseInt(fontSizeInput, 10); - const fontSize = Number.isNaN(parsed) - ? element.fontSize - : clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE }); - setFontSizeInput(fontSize.toString()); - editor.timeline.updateElements({ - updates: [{ trackId, elementId: element.id, updates: { fontSize } }], - }); + 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 }) => { - setOpacityInput(value); + 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) @@ -94,234 +120,333 @@ export function TextProperties({ updates: { opacity: opacityPercent / 100 }, }, ], + pushHistory: false, }); } }; const handleOpacityBlur = () => { - const parsed = parseInt(opacityInput, 10); - const opacityPercent = Number.isNaN(parsed) - ? Math.round(element.opacity * 100) - : clamp({ value: parsed, min: 0, max: 100 }); - setOpacityInput(opacityPercent.toString()); - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { opacity: opacityPercent / 100 }, - }, - ], - }); + 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; } - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { backgroundColor: 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 handleTransparentToggle = ({ - isTransparent, - }: { - isTransparent: boolean; - }) => { - const newColor = isTransparent ? "transparent" : lastSelectedColor.current; - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { backgroundColor: newColor }, - }, - ], - }); + 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 ( - { - if (isTextPropertiesTab(v)) setActiveTab(v); - }} - ref={containerRef} - tabs={TEXT_PROPERTIES_TABS.map((t) => ({ - value: t.value, - label: t.label, - content: - t.value === "transform" ? ( -
- ) : ( -
-