summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/web/src/components/editor/media-panel/index.tsx2
-rw-r--r--apps/web/src/components/editor/media-panel/store.ts8
-rw-r--r--apps/web/src/components/editor/media-panel/views/settings.tsx246
-rw-r--r--apps/web/src/components/editor/properties-panel/property-item.tsx10
-rw-r--r--apps/web/src/components/icons.tsx22
-rw-r--r--apps/web/src/components/ui/select.tsx4
-rw-r--r--apps/web/src/components/ui/tabs.tsx4
7 files changed, 288 insertions, 8 deletions
diff --git a/apps/web/src/components/editor/media-panel/index.tsx b/apps/web/src/components/editor/media-panel/index.tsx
index a5cd6686..4d80b642 100644
--- a/apps/web/src/components/editor/media-panel/index.tsx
+++ b/apps/web/src/components/editor/media-panel/index.tsx
@@ -6,6 +6,7 @@ import { useMediaPanelStore, Tab } from "./store";
import { TextView } from "./views/text";
import { AudioView } from "./views/audio";
import { Separator } from "@/components/ui/separator";
+import { SettingsView } from "./views/settings";
export function MediaPanel() {
const { activeTab } = useMediaPanelStore();
@@ -44,6 +45,7 @@ export function MediaPanel() {
Adjustment view coming soon...
</div>
),
+ settings: <SettingsView />,
};
return (
diff --git a/apps/web/src/components/editor/media-panel/store.ts b/apps/web/src/components/editor/media-panel/store.ts
index c661f51e..577417a9 100644
--- a/apps/web/src/components/editor/media-panel/store.ts
+++ b/apps/web/src/components/editor/media-panel/store.ts
@@ -9,6 +9,7 @@ import {
SlidersHorizontalIcon,
LucideIcon,
TypeIcon,
+ SettingsIcon,
} from "lucide-react";
import { create } from "zustand";
@@ -21,7 +22,8 @@ export type Tab =
| "transitions"
| "captions"
| "filters"
- | "adjustment";
+ | "adjustment"
+ | "settings";
export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = {
media: {
@@ -60,6 +62,10 @@ export const tabs: { [key in Tab]: { icon: LucideIcon; label: string } } = {
icon: SlidersHorizontalIcon,
label: "Adjustment",
},
+ settings: {
+ icon: SettingsIcon,
+ label: "Settings",
+ },
};
interface MediaPanelStore {
diff --git a/apps/web/src/components/editor/media-panel/views/settings.tsx b/apps/web/src/components/editor/media-panel/views/settings.tsx
new file mode 100644
index 00000000..54387dd7
--- /dev/null
+++ b/apps/web/src/components/editor/media-panel/views/settings.tsx
@@ -0,0 +1,246 @@
+"use client";
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ PropertyItem,
+ PropertyItemLabel,
+ PropertyItemValue,
+} from "../../properties-panel/property-item";
+import { FPS_PRESETS } from "@/constants/timeline-constants";
+import { useProjectStore } from "@/stores/project-store";
+import { useEditorStore } from "@/stores/editor-store";
+import { useAspectRatio } from "@/hooks/use-aspect-ratio";
+import Image from "next/image";
+import { cn } from "@/lib/utils";
+import { colors } from "@/data/colors";
+import { PipetteIcon } from "lucide-react";
+import { useMemo, memo, useCallback } from "react";
+
+export function SettingsView() {
+ return <ProjectSettingsTabs />;
+}
+
+function ProjectSettingsTabs() {
+ return (
+ <div className="h-full flex flex-col">
+ <Tabs defaultValue="project-info" className="flex flex-col h-full">
+ <div className="px-3 pt-4 pb-0">
+ <TabsList>
+ <TabsTrigger value="project-info">Project info</TabsTrigger>
+ <TabsTrigger value="background">Background</TabsTrigger>
+ </TabsList>
+ </div>
+ <Separator className="my-4" />
+ <ScrollArea className="flex-1">
+ <TabsContent value="project-info" className="p-5 pt-0 mt-0">
+ <ProjectInfoView />
+ </TabsContent>
+ <TabsContent value="background" className="p-4 pt-0">
+ <BackgroundView />
+ </TabsContent>
+ </ScrollArea>
+ </Tabs>
+ </div>
+ );
+}
+
+function ProjectInfoView() {
+ const { activeProject, updateProjectFps } = useProjectStore();
+ const { canvasSize, canvasPresets, setCanvasSize } = useEditorStore();
+ const { getDisplayName } = useAspectRatio();
+
+ const handleAspectRatioChange = (value: string) => {
+ const preset = canvasPresets.find((p) => p.name === value);
+ if (preset) {
+ setCanvasSize({ width: preset.width, height: preset.height });
+ }
+ };
+
+ const handleFpsChange = (value: string) => {
+ const fps = parseFloat(value);
+ updateProjectFps(fps);
+ };
+
+ return (
+ <div className="flex flex-col gap-4">
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Name</PropertyItemLabel>
+ <PropertyItemValue>
+ {activeProject?.name || "Untitled project"}
+ </PropertyItemValue>
+ </PropertyItem>
+
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Aspect ratio</PropertyItemLabel>
+ <PropertyItemValue>
+ <Select
+ value={getDisplayName()}
+ onValueChange={handleAspectRatioChange}
+ >
+ <SelectTrigger className="bg-panel-accent">
+ <SelectValue placeholder="Select an aspect ratio" />
+ </SelectTrigger>
+ <SelectContent>
+ {canvasPresets.map((preset) => (
+ <SelectItem key={preset.name} value={preset.name}>
+ {preset.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </PropertyItemValue>
+ </PropertyItem>
+
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Frame rate</PropertyItemLabel>
+ <PropertyItemValue>
+ <Select
+ value={(activeProject?.fps || 30).toString()}
+ onValueChange={handleFpsChange}
+ >
+ <SelectTrigger className="bg-panel-accent">
+ <SelectValue placeholder="Select a frame rate" />
+ </SelectTrigger>
+ <SelectContent>
+ {FPS_PRESETS.map((preset) => (
+ <SelectItem key={preset.value} value={preset.value}>
+ {preset.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </PropertyItemValue>
+ </PropertyItem>
+ </div>
+ );
+}
+
+const BlurPreview = memo(
+ ({
+ blur,
+ isSelected,
+ onSelect,
+ }: {
+ blur: { label: string; value: number };
+ isSelected: boolean;
+ onSelect: () => void;
+ }) => (
+ <div
+ className={cn(
+ "w-full aspect-square rounded-sm cursor-pointer hover:outline-2 hover:outline-primary relative overflow-hidden",
+ isSelected && "outline-2 outline-primary"
+ )}
+ onClick={onSelect}
+ >
+ <Image
+ src="https://images.unsplash.com/photo-1501785888041-af3ef285b470?q=80&w=1470&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
+ alt={`Blur preview ${blur.label}`}
+ fill
+ className="object-cover"
+ style={{ filter: `blur(${blur.value}px)` }}
+ priority
+ />
+ <div className="absolute bottom-1 left-1 right-1 text-center">
+ <span className="text-xs text-white bg-black/50 px-1 rounded">
+ {blur.label}
+ </span>
+ </div>
+ </div>
+ )
+);
+
+BlurPreview.displayName = "BlurPreview";
+
+function BackgroundView() {
+ const { activeProject, updateBackgroundType } = useProjectStore();
+
+ const blurLevels = useMemo(
+ () => [
+ { label: "Light", value: 4 },
+ { label: "Medium", value: 8 },
+ { label: "Heavy", value: 18 },
+ ],
+ []
+ );
+
+ const handleBlurSelect = useCallback(
+ async (blurIntensity: number) => {
+ await updateBackgroundType("blur", { blurIntensity });
+ },
+ [updateBackgroundType]
+ );
+
+ const handleColorSelect = useCallback(
+ async (color: string) => {
+ await updateBackgroundType("color", { backgroundColor: color });
+ },
+ [updateBackgroundType]
+ );
+
+ const currentBlurIntensity = activeProject?.blurIntensity || 8;
+ const isBlurBackground = activeProject?.backgroundType === "blur";
+ const currentBackgroundColor = activeProject?.backgroundColor || "#000000";
+ const isColorBackground = activeProject?.backgroundType === "color";
+
+ const blurPreviews = useMemo(
+ () =>
+ blurLevels.map((blur) => (
+ <BlurPreview
+ key={blur.value}
+ blur={blur}
+ isSelected={isBlurBackground && currentBlurIntensity === blur.value}
+ onSelect={() => handleBlurSelect(blur.value)}
+ />
+ )),
+ [blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect]
+ );
+
+ const colorPreviews = useMemo(
+ () =>
+ colors.map((color) => (
+ <div
+ key={color}
+ className={cn(
+ "w-full aspect-square rounded-sm cursor-pointer hover:border-2 hover:border-primary",
+ isColorBackground &&
+ color === currentBackgroundColor &&
+ "border-2 border-primary"
+ )}
+ style={{ backgroundColor: color }}
+ onClick={() => handleColorSelect(color)}
+ />
+ )),
+ [isColorBackground, currentBackgroundColor, handleColorSelect]
+ );
+
+ return (
+ <div className="flex flex-col gap-4">
+ <PropertyItem direction="column" className="gap-2">
+ <PropertyItemLabel>Blur</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="grid grid-cols-4 gap-2 w-full">{blurPreviews}</div>
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column" className="gap-2">
+ <PropertyItemLabel>Color</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="grid grid-cols-4 gap-2 w-full">
+ <div className="w-full aspect-square rounded-sm cursor-pointer border border-foreground/15 hover:border-primary flex items-center justify-center">
+ <PipetteIcon className="size-4" />
+ </div>
+ {colorPreviews}
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ </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
index 804ffb39..86a8d378 100644
--- a/apps/web/src/components/editor/properties-panel/property-item.tsx
+++ b/apps/web/src/components/editor/properties-panel/property-item.tsx
@@ -17,7 +17,7 @@ export function PropertyItem({
"flex gap-2",
direction === "row"
? "items-center justify-between gap-6"
- : "flex-col gap-1",
+ : "flex-col gap-1.5",
className
)}
>
@@ -33,7 +33,11 @@ export function PropertyItemLabel({
children: React.ReactNode;
className?: string;
}) {
- return <label className={cn("text-xs", className)}>{children}</label>;
+ return (
+ <label className={cn("text-xs text-muted-foreground", className)}>
+ {children}
+ </label>
+ );
}
export function PropertyItemValue({
@@ -43,5 +47,5 @@ export function PropertyItemValue({
children: React.ReactNode;
className?: string;
}) {
- return <div className={cn("flex-1", className)}>{children}</div>;
+ return <div className={cn("flex-1 text-sm", className)}>{children}</div>;
}
diff --git a/apps/web/src/components/icons.tsx b/apps/web/src/components/icons.tsx
index 43f953e8..065a123d 100644
--- a/apps/web/src/components/icons.tsx
+++ b/apps/web/src/components/icons.tsx
@@ -163,3 +163,25 @@ export function DataBuddyIcon({
</svg>
);
}
+
+export function SquareSlashIcon({
+ className,
+ size = 24,
+}: {
+ className?: string;
+ size?: number;
+}) {
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width={size}
+ height={size}
+ className={className}
+ fill="none"
+ viewBox="0 0 256 256"
+ >
+ <rect width="18" height="18" x="3" y="3" rx="2" />
+ <line x1="9" x2="15" y1="15" y2="9" />
+ </svg>
+ );
+}
diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx
index ce4f340d..53ca27cb 100644
--- a/apps/web/src/components/ui/select.tsx
+++ b/apps/web/src/components/ui/select.tsx
@@ -35,14 +35,14 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
- "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+ "flex gap-1 h-7 cursor-pointer w-auto items-center justify-between whitespace-nowrap rounded-md bg-accent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
- <ChevronDown className="h-4 w-4 opacity-50" />
+ <ChevronDown className="size-3 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx
index e5384daf..ea628959 100644
--- a/apps/web/src/components/ui/tabs.tsx
+++ b/apps/web/src/components/ui/tabs.tsx
@@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
- "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
+ "inline-flex h-auto items-center justify-center gap-2 rounded-lg bg-transparent p-0 text-muted-foreground",
className
)}
{...props}
@@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
+ "inline-flex items-center cursor-pointer justify-center whitespace-nowrap rounded-lg px-3 py-1 text-sm font-medium ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-card data-[state=active]:text-foreground",
className
)}
{...props}