summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/properties-panel
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /apps/web/src/components/editor/properties-panel
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/components/editor/properties-panel')
-rw-r--r--apps/web/src/components/editor/properties-panel/audio-properties.tsx5
-rw-r--r--apps/web/src/components/editor/properties-panel/index.tsx70
-rw-r--r--apps/web/src/components/editor/properties-panel/media-properties.tsx5
-rw-r--r--apps/web/src/components/editor/properties-panel/property-item.tsx86
-rw-r--r--apps/web/src/components/editor/properties-panel/text-properties.tsx370
5 files changed, 0 insertions, 536 deletions
diff --git a/apps/web/src/components/editor/properties-panel/audio-properties.tsx b/apps/web/src/components/editor/properties-panel/audio-properties.tsx
deleted file mode 100644
index 8d69e36a..00000000
--- a/apps/web/src/components/editor/properties-panel/audio-properties.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MediaElement } from "@/types/timeline";
-
-export function AudioProperties({ element }: { element: MediaElement }) {
- return <div className="space-y-4 p-5">Audio properties</div>;
-}
diff --git a/apps/web/src/components/editor/properties-panel/index.tsx b/apps/web/src/components/editor/properties-panel/index.tsx
deleted file mode 100644
index 150f9512..00000000
--- a/apps/web/src/components/editor/properties-panel/index.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-"use client";
-
-import { useMediaStore } from "@/stores/media-store";
-import { useTimelineStore } from "@/stores/timeline-store";
-import { ScrollArea } from "../../ui/scroll-area";
-import { AudioProperties } from "./audio-properties";
-import { MediaProperties } from "./media-properties";
-import { TextProperties } from "./text-properties";
-import { SquareSlashIcon } from "lucide-react";
-
-export function PropertiesPanel() {
- const { selectedElements, tracks } = useTimelineStore();
- const { mediaFiles } = useMediaStore();
-
- return (
- <>
- {selectedElements.length > 0 ? (
- <ScrollArea className="h-full bg-panel rounded-sm">
- {selectedElements.map(({ trackId, elementId }) => {
- const track = tracks.find((t) => t.id === trackId);
- const element = track?.elements.find((e) => e.id === elementId);
-
- if (element?.type === "text") {
- return (
- <div key={elementId}>
- <TextProperties element={element} trackId={trackId} />
- </div>
- );
- }
- if (element?.type === "media") {
- const mediaFile = mediaFiles.find(
- (file) => file.id === element.mediaId
- );
-
- if (mediaFile?.type === "audio") {
- return <AudioProperties key={elementId} element={element} />;
- }
-
- return (
- <div key={elementId}>
- <MediaProperties element={element} />
- </div>
- );
- }
- return null;
- })}
- </ScrollArea>
- ) : (
- <EmptyView />
- )}
- </>
- );
-}
-
-function EmptyView() {
- return (
- <div className="bg-panel h-full p-4 flex flex-col items-center justify-center gap-3">
- <SquareSlashIcon
- className="w-10 h-10 text-muted-foreground"
- strokeWidth={1.5}
- />
- <div className="flex flex-col gap-2 text-center">
- <p className="text-lg font-medium">It’s empty here</p>
- <p className="text-sm text-muted-foreground text-balance">
- Click an element on the timeline to edit its properties
- </p>
- </div>
- </div>
- );
-}
diff --git a/apps/web/src/components/editor/properties-panel/media-properties.tsx b/apps/web/src/components/editor/properties-panel/media-properties.tsx
deleted file mode 100644
index 4ea28bb2..00000000
--- a/apps/web/src/components/editor/properties-panel/media-properties.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { MediaElement } from "@/types/timeline";
-
-export function MediaProperties({ element }: { element: MediaElement }) {
- return <div className="space-y-4 p-5">Media properties</div>;
-}
diff --git a/apps/web/src/components/editor/properties-panel/property-item.tsx b/apps/web/src/components/editor/properties-panel/property-item.tsx
deleted file mode 100644
index f7d5dbda..00000000
--- a/apps/web/src/components/editor/properties-panel/property-item.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { cn } from "@/lib/utils";
-import { ChevronDown } from "lucide-react";
-import { useState } from "react";
-
-interface PropertyItemProps {
- direction?: "row" | "column";
- children: React.ReactNode;
- className?: string;
-}
-
-export function PropertyItem({
- direction = "row",
- children,
- className,
-}: PropertyItemProps) {
- return (
- <div
- className={cn(
- "flex gap-2",
- direction === "row"
- ? "items-center justify-between gap-6"
- : "flex-col gap-1.5",
- className
- )}
- >
- {children}
- </div>
- );
-}
-
-export function PropertyItemLabel({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return (
- <label className={cn("text-xs text-muted-foreground", className)}>
- {children}
- </label>
- );
-}
-
-export function PropertyItemValue({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return <div className={cn("flex-1 text-sm", className)}>{children}</div>;
-}
-
-interface PropertyGroupProps {
- title: string;
- children: React.ReactNode;
- defaultExpanded?: boolean;
- className?: string;
- titleClassName?: string;
-}
-
-export function PropertyGroup({
- title,
- children,
- defaultExpanded = true,
- className,
- titleClassName,
-}: PropertyGroupProps) {
- const [isExpanded, setIsExpanded] = useState(defaultExpanded);
-
- return (
- <PropertyItem direction="column" className={cn("gap-3", className)}>
- <div
- className="flex items-center gap-1.5 cursor-pointer"
- onClick={() => setIsExpanded(!isExpanded)}
- >
- <PropertyItemLabel className={cn("cursor-pointer", titleClassName)}>
- {title}
- </PropertyItemLabel>
- <ChevronDown className={cn("size-3", !isExpanded && "-rotate-90")} />
- </div>
- {isExpanded && <PropertyItemValue>{children}</PropertyItemValue>}
- </PropertyItem>
- );
-}
diff --git a/apps/web/src/components/editor/properties-panel/text-properties.tsx b/apps/web/src/components/editor/properties-panel/text-properties.tsx
deleted file mode 100644
index deaee157..00000000
--- a/apps/web/src/components/editor/properties-panel/text-properties.tsx
+++ /dev/null
@@ -1,370 +0,0 @@
-import { Textarea } from "@/components/ui/textarea";
-import { FontPicker } from "@/components/ui/font-picker";
-import { FontFamily } from "@/constants/font-constants";
-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, useRef } from "react";
-import { PanelBaseView } from "@/components/editor/panel-base-view";
-import {
- TEXT_PROPERTIES_TABS,
- isTextPropertiesTab,
- useTextPropertiesStore,
-} from "@/stores/text-properties-store";
-import {
- PropertyItem,
- PropertyItemLabel,
- PropertyItemValue,
-} from "./property-item";
-import { ColorPicker } from "@/components/ui/color-picker";
-import { cn, uppercase } from "@/lib/utils";
-import { Grid2x2 } from "lucide-react";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-
-export function TextProperties({
- element,
- trackId,
-}: {
- element: TextElement;
- trackId: string;
-}) {
- const { updateTextElement } = useTimelineStore();
- const { activeTab, setActiveTab } = useTextPropertiesStore();
- const containerRef = useRef<HTMLDivElement>(null);
- // 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()
- );
-
- // Track the last selected color for toggling
- const lastSelectedColor = useRef("#000000");
-
- 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 });
- };
-
- // Update last selected color when a new color is picked
- const handleColorChange = (color: string) => {
- if (color !== "transparent") {
- lastSelectedColor.current = color;
- }
- updateTextElement(trackId, element.id, { backgroundColor: color });
- };
-
- // Toggle between transparent and last selected color
- const handleTransparentToggle = (isTransparent: boolean) => {
- const newColor = isTransparent ? "transparent" : lastSelectedColor.current;
- updateTextElement(trackId, element.id, { backgroundColor: newColor });
- };
-
- 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="min-h-18 resize-none bg-panel-accent"
- onChange={(e) =>
- updateTextElement(trackId, element.id, {
- content: e.target.value,
- })
- }
- />
- <PropertyItem direction="column">
- <PropertyItemLabel>Font</PropertyItemLabel>
- <PropertyItemValue>
- <FontPicker
- defaultValue={element.fontFamily}
- onValueChange={(value: FontFamily) =>
- updateTextElement(trackId, element.id, {
- 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={() =>
- 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>
- <PropertyItem direction="column">
- <PropertyItemLabel>Font size</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <Slider
- value={[element.fontSize]}
- min={8}
- max={300}
- step={1}
- onValueChange={([value]) => {
- updateTextElement(trackId, element.id, {
- fontSize: value,
- });
- setFontSizeInput(value.toString());
- }}
- className="w-full"
- />
- <Input
- type="number"
- value={fontSizeInput}
- min={8}
- max={300}
- onChange={(e) => handleFontSizeChange(e.target.value)}
- onBlur={handleFontSizeBlur}
- className="w-12 px-2 !text-xs h-7 rounded-sm text-center bg-panel-accent
- [appearance:textfield]
- [&::-webkit-outer-spin-button]:appearance-none
- [&::-webkit-inner-spin-button]:appearance-none"
- />
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Color</PropertyItemLabel>
- <PropertyItemValue>
- <ColorPicker
- value={uppercase(
- (element.color || "FFFFFF").replace("#", "")
- )}
- onChange={(color) => {
- updateTextElement(trackId, element.id, {
- color: `#${color}`,
- });
- }}
- 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]) => {
- 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 bg-panel-accent
- [appearance:textfield]
- [&::-webkit-outer-spin-button]:appearance-none
- [&::-webkit-inner-spin-button]:appearance-none"
- />
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Background</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <ColorPicker
- value={uppercase(
- element.backgroundColor === "transparent"
- ? lastSelectedColor.current.replace("#", "")
- : (element.backgroundColor || "#000000").replace(
- "#",
- ""
- )
- )}
- onChange={(color) => handleColorChange(`#${color}`)}
- containerRef={containerRef}
- className={
- element.backgroundColor === "transparent"
- ? "opacity-50 pointer-events-none"
- : ""
- }
- />
-
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="outline"
- size="icon"
- onClick={() =>
- handleTransparentToggle(
- element.backgroundColor !== "transparent"
- )
- }
- className="size-9 rounded-full bg-panel-accent p-0 overflow-hidden"
- >
- <Grid2x2
- className={cn(
- "text-foreground",
- element.backgroundColor === "transparent" &&
- "text-primary"
- )}
- />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Transparent background</TooltipContent>
- </Tooltip>
- </div>
- </PropertyItemValue>
- </PropertyItem>
- </div>
- ),
- }))}
- />
- );
-}