summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/properties
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-02-08 22:21:17 +0100
committerMaze Winther <mazewinther@gmail.com>2026-02-08 22:21:17 +0100
commite50a817b97b87baa0cd8fdde0fdb5d607143c1c3 (patch)
tree8f33d19153be9a6d9fe81b1a7e7a54179ab88feb /apps/web/src/components/editor/panels/properties
parent484ce47867b1c92c83e4cfab9fa8371bb6e68e12 (diff)
downloadopencut-e50a817b97b87baa0cd8fdde0fdb5d607143c1c3.tar.gz
add transform + few fixes
feat: transform feat: batch command fix: slider undo fix: timeline zoom perf refactor: make updateElement support multiple
Diffstat (limited to 'apps/web/src/components/editor/panels/properties')
-rw-r--r--apps/web/src/components/editor/panels/properties/text-properties.tsx268
1 files changed, 190 insertions, 78 deletions
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 43c1eb26..35230c75 100644
--- a/apps/web/src/components/editor/panels/properties/text-properties.tsx
+++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx
@@ -50,6 +50,8 @@ export function TextProperties({
);
const lastSelectedColor = useRef(DEFAULT_COLOR);
+ const initialFontSizeRef = useRef<number | null>(null);
+ const initialOpacityRef = useRef<number | null>(null);
const handleFontSizeChange = ({ value }: { value: string }) => {
setFontSizeInput(value);
@@ -59,10 +61,8 @@ export function TextProperties({
const fontSize = Number.isNaN(parsed)
? element.fontSize
: clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE });
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { fontSize },
+ editor.timeline.updateElements({
+ updates: [{ trackId, elementId: element.id, updates: { fontSize } }],
});
}
};
@@ -73,10 +73,8 @@ export function TextProperties({
? element.fontSize
: clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE });
setFontSizeInput(fontSize.toString());
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { fontSize },
+ editor.timeline.updateElements({
+ updates: [{ trackId, elementId: element.id, updates: { fontSize } }],
});
};
@@ -88,10 +86,14 @@ export function TextProperties({
const opacityPercent = Number.isNaN(parsed)
? Math.round(element.opacity * 100)
: clamp({ value: parsed, min: 0, max: 100 });
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { opacity: opacityPercent / 100 },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { opacity: opacityPercent / 100 },
+ },
+ ],
});
}
};
@@ -102,10 +104,14 @@ export function TextProperties({
? Math.round(element.opacity * 100)
: clamp({ value: parsed, min: 0, max: 100 });
setOpacityInput(opacityPercent.toString());
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { opacity: opacityPercent / 100 },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { opacity: opacityPercent / 100 },
+ },
+ ],
});
};
@@ -113,10 +119,14 @@ export function TextProperties({
if (color !== "transparent") {
lastSelectedColor.current = color;
}
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { backgroundColor: color },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: color },
+ },
+ ],
});
};
@@ -126,10 +136,14 @@ export function TextProperties({
isTransparent: boolean;
}) => {
const newColor = isTransparent ? "transparent" : lastSelectedColor.current;
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { backgroundColor: newColor },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: newColor },
+ },
+ ],
});
};
@@ -154,10 +168,14 @@ export function TextProperties({
defaultValue={element.content}
className="bg-panel-accent min-h-20"
onChange={(e) =>
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { content: e.target.value },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { content: e.target.value },
+ },
+ ],
})
}
/>
@@ -167,10 +185,14 @@ export function TextProperties({
<FontPicker
defaultValue={element.fontFamily}
onValueChange={(value: FontFamily) =>
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { fontFamily: value },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { fontFamily: value },
+ },
+ ],
})
}
/>
@@ -186,13 +208,19 @@ export function TextProperties({
}
size="sm"
onClick={() =>
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: {
- fontWeight:
- element.fontWeight === "bold" ? "normal" : "bold",
- },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: {
+ fontWeight:
+ element.fontWeight === "bold"
+ ? "normal"
+ : "bold",
+ },
+ },
+ ],
})
}
className="h-8 px-3 font-bold"
@@ -205,15 +233,19 @@ export function TextProperties({
}
size="sm"
onClick={() =>
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: {
- fontStyle:
- element.fontStyle === "italic"
- ? "normal"
- : "italic",
- },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: {
+ fontStyle:
+ element.fontStyle === "italic"
+ ? "normal"
+ : "italic",
+ },
+ },
+ ],
})
}
className="h-8 px-3 italic"
@@ -228,15 +260,19 @@ export function TextProperties({
}
size="sm"
onClick={() =>
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: {
- textDecoration:
- element.textDecoration === "underline"
- ? "none"
- : "underline",
- },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: {
+ textDecoration:
+ element.textDecoration === "underline"
+ ? "none"
+ : "underline",
+ },
+ },
+ ],
})
}
className="h-8 px-3 underline"
@@ -251,15 +287,19 @@ export function TextProperties({
}
size="sm"
onClick={() =>
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: {
- textDecoration:
- element.textDecoration === "line-through"
- ? "none"
- : "line-through",
- },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: {
+ textDecoration:
+ element.textDecoration === "line-through"
+ ? "none"
+ : "line-through",
+ },
+ },
+ ],
})
}
className="h-8 px-3 line-through"
@@ -279,13 +319,48 @@ export function TextProperties({
max={MAX_FONT_SIZE}
step={1}
onValueChange={([value]) => {
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { fontSize: value },
+ if (initialFontSizeRef.current === null) {
+ initialFontSizeRef.current = element.fontSize;
+ }
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { fontSize: value },
+ },
+ ],
+ pushHistory: false,
});
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
@@ -310,10 +385,14 @@ export function TextProperties({
string: (element.color || "FFFFFF").replace("#", ""),
})}
onChange={(color) => {
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { color: `#${color}` },
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { color: `#${color}` },
+ },
+ ],
});
}}
containerRef={containerRef}
@@ -330,13 +409,46 @@ export function TextProperties({
max={100}
step={1}
onValueChange={([value]) => {
- editor.timeline.updateElement({
- trackId,
- elementId: element.id,
- updates: { opacity: value / 100 },
+ if (initialOpacityRef.current === null) {
+ initialOpacityRef.current = element.opacity;
+ }
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { opacity: value / 100 },
+ },
+ ],
+ 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