diff options
| author | Maze Winther <mazewinther@gmail.com> | 2025-07-30 11:47:43 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2025-07-30 11:47:43 +0200 |
| commit | 91c95b12d1d6b967511c0dc7dfa3fed00886b6e0 (patch) | |
| tree | ebb38ce34d7c48a81ca72633a2f5549f7ebee976 | |
| parent | 397421159bd06f08701efcec4f0e8003aebfb72f (diff) | |
| download | opencut-91c95b12d1d6b967511c0dc7dfa3fed00886b6e0.tar.gz | |
Merge pull request [#467](https://github.com/mazeincoding/AppCut/issues/467)
| -rw-r--r-- | apps/web/src/components/editor/properties-panel/text-properties.tsx | 205 |
1 files changed, 195 insertions, 10 deletions
diff --git a/apps/web/src/components/editor/properties-panel/text-properties.tsx b/apps/web/src/components/editor/properties-panel/text-properties.tsx index e147ea65..f8380ff4 100644 --- a/apps/web/src/components/editor/properties-panel/text-properties.tsx +++ b/apps/web/src/components/editor/properties-panel/text-properties.tsx @@ -5,6 +5,8 @@ import { TextElement } from "@/types/timeline"; import { useTimelineStore } from "@/stores/timeline-store"; import { Slider } from "@/components/ui/slider"; import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; import { PropertyItem, PropertyItemLabel, @@ -20,6 +22,70 @@ export function TextProperties({ }) { const { updateTextElement } = useTimelineStore(); + // Local state for input values to allow temporary empty/invalid states + const [fontSizeInput, setFontSizeInput] = useState( + element.fontSize.toString() + ); + const [opacityInput, setOpacityInput] = useState( + Math.round(element.opacity * 100).toString() + ); + + const parseAndValidateNumber = ( + value: string, + min: number, + max: number, + fallback: number + ): number => { + const parsed = parseInt(value, 10); + if (isNaN(parsed)) return fallback; + return Math.max(min, Math.min(max, parsed)); + }; + + const handleFontSizeChange = (value: string) => { + setFontSizeInput(value); + + if (value.trim() !== "") { + const fontSize = parseAndValidateNumber(value, 8, 300, element.fontSize); + updateTextElement(trackId, element.id, { fontSize }); + } + }; + + const handleFontSizeBlur = () => { + const fontSize = parseAndValidateNumber( + fontSizeInput, + 8, + 300, + element.fontSize + ); + setFontSizeInput(fontSize.toString()); + updateTextElement(trackId, element.id, { fontSize }); + }; + + const handleOpacityChange = (value: string) => { + setOpacityInput(value); + + if (value.trim() !== "") { + const opacityPercent = parseAndValidateNumber( + value, + 0, + 100, + Math.round(element.opacity * 100) + ); + updateTextElement(trackId, element.id, { opacity: opacityPercent / 100 }); + } + }; + + const handleOpacityBlur = () => { + const opacityPercent = parseAndValidateNumber( + opacityInput, + 0, + 100, + Math.round(element.opacity * 100) + ); + setOpacityInput(opacityPercent.toString()); + updateTextElement(trackId, element.id, { opacity: opacityPercent / 100 }); + }; + return ( <div className="space-y-6 p-5"> <Textarea @@ -42,27 +108,96 @@ export function TextProperties({ </PropertyItemValue> </PropertyItem> <PropertyItem direction="column"> + <PropertyItem direction="row"> + <PropertyItemLabel>Style</PropertyItemLabel> + <PropertyItemValue> + <div className="flex items-center gap-2"> + <Button + variant={element.fontWeight === "bold" ? "default" : "outline"} + size="sm" + onClick={() => + updateTextElement(trackId, element.id, { + 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={() => + updateTextElement(trackId, element.id, { + fontStyle: + element.fontStyle === "italic" ? "normal" : "italic", + }) + } + className="h-8 px-3 italic" + > + I + </Button> + <Button + variant={ + element.textDecoration === "underline" ? "default" : "outline" + } + size="sm" + onClick={() => + updateTextElement(trackId, element.id, { + 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={() => + updateTextElement(trackId, element.id, { + textDecoration: + element.textDecoration === "line-through" + ? "none" + : "line-through", + }) + } + className="h-8 px-3 line-through" + > + S + </Button> + </div> + </PropertyItemValue> + </PropertyItem> <PropertyItemLabel>Font size</PropertyItemLabel> <PropertyItemValue> <div className="flex items-center gap-2"> <Slider - defaultValue={[element.fontSize]} + value={[element.fontSize]} min={8} max={300} step={1} - onValueChange={([value]) => - updateTextElement(trackId, element.id, { fontSize: value }) - } + onValueChange={([value]) => { + updateTextElement(trackId, element.id, { fontSize: value }); + setFontSizeInput(value.toString()); + }} className="w-full" /> <Input type="number" - value={element.fontSize} - onChange={(e) => - updateTextElement(trackId, element.id, { - fontSize: parseInt(e.target.value), - }) - } + value={fontSizeInput} + min={8} + max={300} + onChange={(e) => handleFontSizeChange(e.target.value)} + onBlur={handleFontSizeBlur} className="w-12 !text-xs h-7 rounded-sm text-center [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none @@ -85,6 +220,56 @@ export function TextProperties({ /> </PropertyItemValue> </PropertyItem> + <PropertyItem direction="row"> + <PropertyItemLabel>Background</PropertyItemLabel> + <PropertyItemValue> + <Input + type="color" + value={ + element.backgroundColor === "transparent" + ? "#000000" + : element.backgroundColor || "#000000" + } + onChange={(e) => { + const backgroundColor = e.target.value; + updateTextElement(trackId, element.id, { backgroundColor }); + }} + className="w-full cursor-pointer rounded-full" + /> + </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]) => { + updateTextElement(trackId, element.id, { + opacity: value / 100, + }); + setOpacityInput(value.toString()); + }} + className="w-full" + /> + <Input + type="number" + value={opacityInput} + min={0} + max={100} + onChange={(e) => handleOpacityChange(e.target.value)} + onBlur={handleOpacityBlur} + className="w-12 !text-xs h-7 rounded-sm text-center + [appearance:textfield] + [&::-webkit-outer-spin-button]:appearance-none + [&::-webkit-inner-spin-button]:appearance-none" + /> + </div> + </PropertyItemValue> + </PropertyItem> </div> ); } |
