summaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-07-30 11:47:43 +0200
committerMaze Winther <mazewinther@gmail.com>2025-07-30 11:47:43 +0200
commit91c95b12d1d6b967511c0dc7dfa3fed00886b6e0 (patch)
treeebb38ce34d7c48a81ca72633a2f5549f7ebee976 /apps/web/src
parent397421159bd06f08701efcec4f0e8003aebfb72f (diff)
downloadopencut-91c95b12d1d6b967511c0dc7dfa3fed00886b6e0.tar.gz
Merge pull request [#467](https://github.com/mazeincoding/AppCut/issues/467)
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/components/editor/properties-panel/text-properties.tsx205
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>
);
}