From 6ab9f25658d9b6a9dd6af5f01af088a70922b7ae Mon Sep 17 00:00:00 2001 From: pratiyankkumar Date: Sun, 27 Jul 2025 10:27:40 +0530 Subject: [PATCH 1/2] feat: add text styling options (bold, italic, underline, strikethrough) and opacity control in text properties panel --- .../properties-panel/text-properties.tsx | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) 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..3dccc226 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,7 @@ 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 { PropertyItem, PropertyItemLabel, @@ -42,6 +43,61 @@ export function TextProperties({ + + Style + +
+ + + + +
+
+
Font size
@@ -85,6 +141,50 @@ export function TextProperties({ /> + + Background + + { + const backgroundColor = e.target.value; + updateTextElement(trackId, element.id, { backgroundColor }); + }} + className="w-full cursor-pointer rounded-full" + /> + + + + Opacity + +
+ + updateTextElement(trackId, element.id, { opacity: value / 100 }) + } + className="w-full" + /> + + updateTextElement(trackId, element.id, { + opacity: parseInt(e.target.value) / 100, + }) + } + 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" + /> +
+
+
); -} +} \ No newline at end of file From 609e1159771912017c072510108088628a5447b4 Mon Sep 17 00:00:00 2001 From: pratiyankkumar Date: Sun, 27 Jul 2025 12:58:44 +0530 Subject: [PATCH 2/2] fix: sync between slider and input and add input validation --- .../properties-panel/text-properties.tsx | 83 ++++++++++++++----- 1 file changed, 62 insertions(+), 21 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 3dccc226..285bfc41 100644 --- a/apps/web/src/components/editor/properties-panel/text-properties.tsx +++ b/apps/web/src/components/editor/properties-panel/text-properties.tsx @@ -6,6 +6,7 @@ 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 +21,46 @@ export function TextProperties({ trackId: string; }) { 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 (
@@ -93,7 +134,7 @@ export function TextProperties({ } className="h-8 px-3 line-through" > - S + S
@@ -102,23 +143,23 @@ export function TextProperties({
- updateTextElement(trackId, element.id, { fontSize: value }) - } + onValueChange={([value]) => { + updateTextElement(trackId, element.id, { fontSize: value }); + setFontSizeInput(value.toString()); + }} className="w-full" /> - 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 @@ -160,23 +201,23 @@ export function TextProperties({
- updateTextElement(trackId, element.id, { opacity: value / 100 }) - } + onValueChange={([value]) => { + updateTextElement(trackId, element.id, { opacity: value / 100 }); + setOpacityInput(value.toString()); + }} className="w-full" /> - updateTextElement(trackId, element.id, { - opacity: parseInt(e.target.value) / 100, - }) - } + 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