diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-02-10 16:28:21 +0100 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-02-10 16:28:21 +0100 |
| commit | fca99d6126c31fbb18ed9f1034cee6f940b040e8 (patch) | |
| tree | 11500ff0fcc4a933650e3e91f1ea00fd315c4d0c /apps/web/src/components/editor/panels/properties | |
| parent | 33a8705f0d967891402c61dd3192e6bad24921ef (diff) | |
| download | opencut-fca99d6126c31fbb18ed9f1034cee6f940b040e8.tar.gz | |
refactor
Diffstat (limited to 'apps/web/src/components/editor/panels/properties')
4 files changed, 599 insertions, 430 deletions
diff --git a/apps/web/src/components/editor/panels/properties/empty-view.tsx b/apps/web/src/components/editor/panels/properties/empty-view.tsx new file mode 100644 index 00000000..8ccf07ee --- /dev/null +++ b/apps/web/src/components/editor/panels/properties/empty-view.tsx @@ -0,0 +1,20 @@ +import { HugeiconsIcon } from "@hugeicons/react"; +import { Settings05Icon } from "@hugeicons/core-free-icons"; + +export function EmptyView() { + return ( + <div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4"> + <HugeiconsIcon + icon={Settings05Icon} + className="text-muted-foreground/75 size-10" + strokeWidth={1} + /> + <div className="flex flex-col gap-2 text-center"> + <p className="text-lg font-medium ">It's empty here</p> + <p className="text-muted-foreground text-sm text-balance"> + Click an element on the timeline to edit its properties + </p> + </div> + </div> + ); +}
\ No newline at end of file diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx index 3a552b93..05ad5396 100644 --- a/apps/web/src/components/editor/panels/properties/index.tsx +++ b/apps/web/src/components/editor/panels/properties/index.tsx @@ -4,8 +4,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { AudioProperties } from "./audio-properties"; import { VideoProperties } from "./video-properties"; import { TextProperties } from "./text-properties"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { Settings05Icon } from "@hugeicons/core-free-icons"; +import { EmptyView } from "./empty-view"; import { useEditor } from "@/hooks/use-editor"; import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; @@ -18,9 +17,9 @@ export function PropertiesPanel() { }); return ( - <> + <div className="panel bg-background h-full rounded-sm border overflow-hidden"> {selectedElements.length > 0 ? ( - <ScrollArea className="bg-panel h-full rounded-sm"> + <ScrollArea className="h-full"> {elementsWithTracks.map(({ track, element }) => { if (element.type === "text") { return ( @@ -45,24 +44,6 @@ export function PropertiesPanel() { ) : ( <EmptyView /> )} - </> - ); -} - -function EmptyView() { - return ( - <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4"> - <HugeiconsIcon - icon={Settings05Icon} - className="text-muted-foreground/75 size-10" - strokeWidth={1} - /> - <div className="flex flex-col gap-2 text-center"> - <p className="text-lg font-medium">It's empty here</p> - <p className="text-muted-foreground text-sm text-balance"> - Click an element on the timeline to edit its properties - </p> - </div> </div> ); } diff --git a/apps/web/src/components/editor/panels/properties/property-item.tsx b/apps/web/src/components/editor/panels/properties/property-item.tsx index 6d2fe444..a682097a 100644 --- a/apps/web/src/components/editor/panels/properties/property-item.tsx +++ b/apps/web/src/components/editor/panels/properties/property-item.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { cn } from "@/utils/ui"; import { HugeiconsIcon } from "@hugeicons/react"; -import { ArrowDownIcon } from "@hugeicons/core-free-icons"; +import { MinusSignIcon, PlusSignIcon } from "@hugeicons/core-free-icons"; interface PropertyItemProps { direction?: "row" | "column"; @@ -57,35 +57,76 @@ interface PropertyGroupProps { title: string; children: React.ReactNode; defaultExpanded?: boolean; + collapsible?: boolean; className?: string; - titleClassName?: string; + hasBorderTop?: boolean; + hasBorderBottom?: boolean; } export function PropertyGroup({ title, children, defaultExpanded = true, + collapsible = true, className, - titleClassName, + hasBorderTop = true, + hasBorderBottom = true, }: PropertyGroupProps) { const [isExpanded, setIsExpanded] = useState(defaultExpanded); return ( - <PropertyItem direction="column" className={cn("gap-3", className)}> - <button - type="button" - className="flex items-center gap-1.5 cursor-pointer" - onClick={() => setIsExpanded(!isExpanded)} - > - <PropertyItemLabel className={cn(titleClassName)}> - {title} - </PropertyItemLabel> - <HugeiconsIcon - icon={ArrowDownIcon} - className={cn("size-3", !isExpanded && "-rotate-90")} - /> - </button> - {isExpanded && <PropertyItemValue>{children}</PropertyItemValue>} - </PropertyItem> + <div + className={cn( + "flex flex-col", + hasBorderTop && "border-t", + hasBorderBottom && "last:border-b", + className, + )} + > + {collapsible ? ( + <button + type="button" + className="flex items-center justify-between p-3.5 cursor-pointer" + onClick={() => setIsExpanded(!isExpanded)} + > + <PropertyGroupTitle isExpanded={isExpanded}> + {title} + </PropertyGroupTitle> + <HugeiconsIcon + icon={isExpanded ? MinusSignIcon : PlusSignIcon} + className={cn( + "size-3", + isExpanded ? "text-foreground" : "text-muted-foreground", + )} + /> + </button> + ) : ( + <div className="flex items-center justify-between p-4"> + <PropertyGroupTitle isExpanded>{title}</PropertyGroupTitle> + </div> + )} + {(collapsible ? isExpanded : true) && ( + <div className="p-3 pt-0">{children}</div> + )} + </div> + ); +} + +function PropertyGroupTitle({ + children, + isExpanded = false, +}: { + children: React.ReactNode; + isExpanded?: boolean; +}) { + return ( + <span + className={cn( + "text-xs font-medium", + isExpanded ? "text-foreground" : "text-muted-foreground", + )} + > + {children} + </span> ); } 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<HTMLDivElement>(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<number | null>(null); const initialOpacityRef = useRef<number | null>(null); + const initialContentRef = useRef<string | null>(null); + const initialColorRef = useRef<string | null>(null); + const initialBgColorRef = useRef<string | null>(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 ( - <PanelBaseView - defaultTab="transform" - value={activeTab} - onValueChange={(v) => { - if (isTextPropertiesTab(v)) setActiveTab(v); - }} - ref={containerRef} - tabs={TEXT_PROPERTIES_TABS.map((t) => ({ - value: t.value, - label: t.label, - content: - t.value === "transform" ? ( - <div className="space-y-6"></div> - ) : ( - <div className="space-y-6"> - <Textarea - placeholder="Name" - defaultValue={element.content} - className="bg-panel-accent min-h-20" - onChange={(e) => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { content: e.target.value }, - }, - ], - }) - } - /> - <PropertyItem direction="column"> - <PropertyItemLabel>Font</PropertyItemLabel> - <PropertyItemValue> - <FontPicker - defaultValue={element.fontFamily} - onValueChange={(value: FontFamily) => + <div className="flex h-full flex-col" ref={containerRef}> + <PanelBaseView className="p-0"> + <PropertyGroup title="Content" hasBorderTop={false} collapsible={false}> + <Textarea + placeholder="Name" + value={contentDisplay} + className="bg-accent min-h-20" + onFocus={() => { + isEditingContent.current = true; + contentDraft.current = element.content; + initialContentRef.current = element.content; + forceRender(); + }} + onChange={(event) => { + contentDraft.current = event.target.value; + forceRender(); + if (initialContentRef.current === null) { + initialContentRef.current = element.content; + } + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { content: event.target.value }, + }, + ], + pushHistory: false, + }); + }} + onBlur={() => { + if (initialContentRef.current !== null) { + const finalContent = contentDraft.current; + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { content: initialContentRef.current }, + }, + ], + pushHistory: false, + }); + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { content: finalContent }, + }, + ], + pushHistory: true, + }); + initialContentRef.current = null; + } + isEditingContent.current = false; + contentDraft.current = ""; + forceRender(); + }} + /> + </PropertyGroup> + <PropertyGroup title="Typography" collapsible={false}> + <div className="space-y-6"> + <PropertyItem direction="column"> + <PropertyItemLabel>Font</PropertyItemLabel> + <PropertyItemValue> + <FontPicker + defaultValue={element.fontFamily} + onValueChange={(value: FontFamily) => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { fontFamily: value }, + }, + ], + }) + } + /> + </PropertyItemValue> + </PropertyItem> + <PropertyItem direction="column"> + <PropertyItemLabel>Style</PropertyItemLabel> + <PropertyItemValue> + <div className="flex items-center gap-2"> + <Button + variant={ + element.fontWeight === "bold" ? "default" : "outline" + } + size="sm" + onClick={() => editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, - updates: { fontFamily: value }, + updates: { + fontWeight: + element.fontWeight === "bold" + ? "normal" + : "bold", + }, }, ], }) } - /> - </PropertyItemValue> - </PropertyItem> - <PropertyItem direction="column"> - <PropertyItemLabel>Style</PropertyItemLabel> - <PropertyItemValue> - <div className="flex items-center gap-2"> - <Button - variant={ - element.fontWeight === "bold" ? "default" : "outline" - } - size="sm" - onClick={() => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - fontWeight: - element.fontWeight === "bold" - ? "normal" - : "bold", - }, + className="h-8 px-3 font-bold" + > + B + </Button> + <Button + variant={ + element.fontStyle === "italic" ? "default" : "outline" + } + size="sm" + onClick={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + fontStyle: + element.fontStyle === "italic" + ? "normal" + : "italic", }, - ], - }) - } - className="h-8 px-3 font-bold" - > - B - </Button> - <Button - variant={ - element.fontStyle === "italic" ? "default" : "outline" - } - size="sm" - onClick={() => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - fontStyle: - element.fontStyle === "italic" - ? "normal" - : "italic", - }, + }, + ], + }) + } + className="h-8 px-3 italic" + > + I + </Button> + <Button + variant={ + element.textDecoration === "underline" + ? "default" + : "outline" + } + size="sm" + onClick={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + textDecoration: + element.textDecoration === "underline" + ? "none" + : "underline", }, - ], - }) - } - className="h-8 px-3 italic" - > - I - </Button> - <Button - variant={ - element.textDecoration === "underline" - ? "default" - : "outline" - } - size="sm" - onClick={() => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - textDecoration: - element.textDecoration === "underline" - ? "none" - : "underline", - }, + }, + ], + }) + } + className="h-8 px-3 underline" + > + U + </Button> + <Button + variant={ + element.textDecoration === "line-through" + ? "default" + : "outline" + } + size="sm" + onClick={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + textDecoration: + element.textDecoration === "line-through" + ? "none" + : "line-through", }, - ], - }) - } - className="h-8 px-3 underline" - > - U - </Button> - <Button - variant={ - element.textDecoration === "line-through" - ? "default" - : "outline" + }, + ], + }) + } + className="h-8 px-3 line-through" + > + S + </Button> + </div> + </PropertyItemValue> + </PropertyItem> + <PropertyItem direction="column"> + <PropertyItemLabel>Font size</PropertyItemLabel> + <PropertyItemValue> + <div className="flex items-center gap-2"> + <Slider + value={[element.fontSize]} + min={MIN_FONT_SIZE} + max={MAX_FONT_SIZE} + step={1} + onValueChange={([value]) => { + if (initialFontSizeRef.current === null) { + initialFontSizeRef.current = element.fontSize; } - size="sm" - onClick={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { fontSize: value }, + }, + ], + pushHistory: false, + }); + }} + onValueCommit={([value]) => { + if (initialFontSizeRef.current !== null) { editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, updates: { - textDecoration: - element.textDecoration === "line-through" - ? "none" - : "line-through", + fontSize: initialFontSizeRef.current, }, }, ], - }) - } - className="h-8 px-3 line-through" - > - S - </Button> - </div> - </PropertyItemValue> - </PropertyItem> - <PropertyItem direction="column"> - <PropertyItemLabel>Font size</PropertyItemLabel> - <PropertyItemValue> - <div className="flex items-center gap-2"> - <Slider - value={[element.fontSize]} - min={MIN_FONT_SIZE} - max={MAX_FONT_SIZE} - step={1} - onValueChange={([value]) => { - if (initialFontSizeRef.current === null) { - initialFontSizeRef.current = element.fontSize; - } + pushHistory: false, + }); editor.timeline.updateElements({ updates: [ { @@ -330,61 +455,48 @@ export function TextProperties({ updates: { fontSize: value }, }, ], - pushHistory: false, + pushHistory: true, }); - setFontSizeInput(value.toString()); - }} - onValueCommit={([value]) => { - if (initialFontSizeRef.current !== null) { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - fontSize: initialFontSizeRef.current, - }, - }, - ], - pushHistory: false, - }); - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { fontSize: value }, - }, - ], - pushHistory: true, - }); - initialFontSizeRef.current = null; - } - }} - className="w-full" - /> - <Input - type="number" - value={fontSizeInput} - min={MIN_FONT_SIZE} - max={MAX_FONT_SIZE} - onChange={(e) => - handleFontSizeChange({ value: e.target.value }) + initialFontSizeRef.current = null; } - onBlur={handleFontSizeBlur} - className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm px-2 text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" - /> - </div> - </PropertyItemValue> - </PropertyItem> - <PropertyItem direction="column"> - <PropertyItemLabel>Color</PropertyItemLabel> - <PropertyItemValue> - <ColorPicker - value={uppercase({ - string: (element.color || "FFFFFF").replace("#", ""), - })} - onChange={(color) => { + }} + className="w-full" + /> + <Input + type="number" + value={fontSizeDisplay} + min={MIN_FONT_SIZE} + max={MAX_FONT_SIZE} + onFocus={() => { + isEditingFontSize.current = true; + fontSizeDraft.current = element.fontSize.toString(); + forceRender(); + }} + onChange={(e) => + handleFontSizeChange({ value: e.target.value }) + } + onBlur={handleFontSizeBlur} + className="bg-accent h-7 w-12 [appearance:textfield] rounded-sm px-2 text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + /> + </div> + </PropertyItemValue> + </PropertyItem> + </div> + </PropertyGroup> + <PropertyGroup title="Appearance" collapsible={false}> + <div className="space-y-6"> + <PropertyItem direction="column"> + <PropertyItemLabel>Color</PropertyItemLabel> + <PropertyItemValue> + <ColorPicker + value={uppercase({ + string: (element.color || "FFFFFF").replace("#", ""), + })} + onChange={(color) => { + if (initialColorRef.current === null) { + initialColorRef.current = element.color || "#FFFFFF"; + } + if (initialColorRef.current !== null) { editor.timeline.updateElements({ updates: [ { @@ -393,131 +505,146 @@ export function TextProperties({ updates: { color: `#${color}` }, }, ], + pushHistory: false, + }); + } else { + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { color: `#${color}` }, + }, + ], + }); + } + }} + onChangeEnd={(color) => { + if (initialColorRef.current !== null) { + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { color: initialColorRef.current }, + }, + ], + pushHistory: false, + }); + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { color: `#${color}` }, + }, + ], + pushHistory: true, + }); + initialColorRef.current = null; + } + }} + containerRef={containerRef} + /> + </PropertyItemValue> + </PropertyItem> + <PropertyItem direction="column"> + <PropertyItemLabel>Opacity</PropertyItemLabel> + <PropertyItemValue> + <div className="flex items-center gap-2"> + <Slider + value={[element.opacity * 100]} + min={0} + max={100} + step={1} + onValueChange={([value]) => { + if (initialOpacityRef.current === null) { + initialOpacityRef.current = element.opacity; + } + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { opacity: value / 100 }, + }, + ], + pushHistory: false, }); }} - containerRef={containerRef} - /> - </PropertyItemValue> - </PropertyItem> - <PropertyItem direction="column"> - <PropertyItemLabel>Opacity</PropertyItemLabel> - <PropertyItemValue> - <div className="flex items-center gap-2"> - <Slider - value={[element.opacity * 100]} - min={0} - max={100} - step={1} - onValueChange={([value]) => { - if (initialOpacityRef.current === null) { - initialOpacityRef.current = element.opacity; - } + onValueCommit={([value]) => { + if (initialOpacityRef.current !== null) { editor.timeline.updateElements({ updates: [ { trackId, elementId: element.id, - updates: { opacity: value / 100 }, + updates: { opacity: initialOpacityRef.current }, }, ], pushHistory: false, }); - setOpacityInput(value.toString()); - }} - onValueCommit={([value]) => { - if (initialOpacityRef.current !== null) { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { opacity: initialOpacityRef.current }, - }, - ], - pushHistory: false, - }); - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { opacity: value / 100 }, - }, - ], - pushHistory: true, - }); - initialOpacityRef.current = null; - } - }} - className="w-full" - /> - <Input - type="number" - value={opacityInput} - min={0} - max={100} - onChange={(e) => - handleOpacityChange({ value: e.target.value }) - } - onBlur={handleOpacityBlur} - className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" - /> - </div> - </PropertyItemValue> - </PropertyItem> - <PropertyItem direction="column"> - <PropertyItemLabel>Background</PropertyItemLabel> - <PropertyItemValue> - <div className="flex items-center gap-2"> - <ColorPicker - value={capitalizeFirstLetter({ - string: - element.backgroundColor === "transparent" - ? lastSelectedColor.current.replace("#", "") - : element.backgroundColor.replace("#", ""), - })} - onChange={(color) => - handleColorChange({ color: `#${color}` }) - } - containerRef={containerRef} - className={ - element.backgroundColor === "transparent" - ? "pointer-events-none opacity-50" - : "" + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { opacity: value / 100 }, + }, + ], + pushHistory: true, + }); + initialOpacityRef.current = null; } - /> - - <Tooltip> - <TooltipTrigger asChild> - <Button - variant="outline" - size="icon" - onClick={() => - handleTransparentToggle({ - isTransparent: - element.backgroundColor !== "transparent", - }) - } - className="bg-panel-accent size-9 overflow-hidden rounded-full p-0" - > - <HugeiconsIcon - icon={LayoutGridIcon} - className={cn( - "text-foreground", - element.backgroundColor === "transparent" && - "text-primary", - )} - /> - </Button> - </TooltipTrigger> - <TooltipContent>Transparent background</TooltipContent> - </Tooltip> - </div> - </PropertyItemValue> - </PropertyItem> - </div> - ), - }))} - /> + }} + className="w-full" + /> + <Input + type="number" + value={opacityDisplay} + min={0} + max={100} + onFocus={() => { + isEditingOpacity.current = true; + opacityDraft.current = Math.round( + element.opacity * 100, + ).toString(); + forceRender(); + }} + onChange={(e) => + handleOpacityChange({ value: e.target.value }) + } + onBlur={handleOpacityBlur} + className="bg-accent h-7 w-12 [appearance:textfield] rounded-sm text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + /> + </div> + </PropertyItemValue> + </PropertyItem> + <PropertyItem direction="column"> + <PropertyItemLabel>Background</PropertyItemLabel> + <PropertyItemValue> + <ColorPicker + value={ + element.backgroundColor === "transparent" + ? lastSelectedColor.current.replace("#", "") + : element.backgroundColor.replace("#", "") + } + onChange={(color) => + handleColorChange({ color: `#${color}` }) + } + onChangeEnd={(color) => handleColorChangeEnd({ color })} + containerRef={containerRef} + className={ + element.backgroundColor === "transparent" + ? "pointer-events-none opacity-50" + : "" + } + /> + </PropertyItemValue> + </PropertyItem> + </div> + </PropertyGroup> + </PanelBaseView> + </div> ); } |
