summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/assets/views/settings.tsx
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/panels/assets/views/settings.tsx
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'apps/web/src/components/editor/panels/assets/views/settings.tsx')
-rw-r--r--apps/web/src/components/editor/panels/assets/views/settings.tsx394
1 files changed, 394 insertions, 0 deletions
diff --git a/apps/web/src/components/editor/panels/assets/views/settings.tsx b/apps/web/src/components/editor/panels/assets/views/settings.tsx
new file mode 100644
index 00000000..b8c54510
--- /dev/null
+++ b/apps/web/src/components/editor/panels/assets/views/settings.tsx
@@ -0,0 +1,394 @@
+"use client";
+
+import Image from "next/image";
+import { memo, useCallback, useMemo } from "react";
+import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import {
+ BLUR_INTENSITY_PRESETS,
+ DEFAULT_BLUR_INTENSITY,
+ DEFAULT_COLOR,
+ FPS_PRESETS,
+} from "@/constants/project-constants";
+import { patternCraftGradients } from "@/data/colors/pattern-craft";
+import { colors } from "@/data/colors/solid";
+import { syntaxUIGradients } from "@/data/colors/syntax-ui";
+import { useEditor } from "@/hooks/use-editor";
+import { useEditorStore } from "@/stores/editor-store";
+import type { TProject } from "@/types/project";
+import { dimensionToAspectRatio } from "@/utils/geometry";
+import { cn } from "@/utils/ui";
+import {
+ PropertyGroup,
+ PropertyItem,
+ PropertyItemLabel,
+ PropertyItemValue,
+} from "@/components/editor/panels/properties/property-item";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { DropperIcon } from "@hugeicons/core-free-icons";
+
+export function SettingsView() {
+ return <ProjectSettingsTabs />;
+}
+
+function ProjectSettingsTabs() {
+ return (
+ <BaseView
+ defaultTab="project-info"
+ tabs={[
+ {
+ value: "project-info",
+ label: "Project info",
+ content: (
+ <div className="p-5">
+ <ProjectInfoView />
+ </div>
+ ),
+ },
+ {
+ value: "background",
+ label: "Background",
+ content: (
+ <div className="flex h-full flex-col justify-between">
+ <div className="flex-1 p-5">
+ <BackgroundView />
+ </div>
+ {/* <div className="bg-panel/85 sticky -bottom-0 flex flex-col backdrop-blur-lg">
+ <Separator />
+ <Button className="text-muted-foreground hover:text-foreground/85 h-auto w-fit !bg-transparent p-5 py-4 text-xs shadow-none">
+ Custom background
+ <PlusIcon />
+ </Button>
+ </div> */}
+
+ {/* another ui */}
+ {/* <div className="flex flex-col justify-center items-center pb-5 sticky bottom-0">
+ <Button className="w-fit h-auto gap-1.5 px-3.5 py-1.5 bg-foreground hover:bg-foreground/85 text-background rounded-full">
+ <span className="text-sm">Custom</span>
+ <PlusIcon className="" />
+ </Button>
+ </div> */}
+ </div>
+ ),
+ },
+ ]}
+ className="flex h-full flex-col justify-between p-0"
+ />
+ );
+}
+
+function getCurrentCanvasSize({ activeProject }: { activeProject: TProject }) {
+ const { canvasSize } = activeProject.settings;
+
+ return {
+ width: canvasSize.width,
+ height: canvasSize.height,
+ };
+}
+
+function ProjectInfoView() {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const { canvasPresets } = useEditorStore();
+
+ const findPresetIndexByAspectRatio = ({
+ presets,
+ targetAspectRatio,
+ }: {
+ presets: Array<{ width: number; height: number }>;
+ targetAspectRatio: string;
+ }) => {
+ for (let index = 0; index < presets.length; index++) {
+ const preset = presets[index];
+ const presetAspectRatio = dimensionToAspectRatio({
+ width: preset.width,
+ height: preset.height,
+ });
+ if (presetAspectRatio === targetAspectRatio) {
+ return index;
+ }
+ }
+ return -1;
+ };
+
+ const currentCanvasSize = getCurrentCanvasSize({ activeProject });
+ const currentAspectRatio = dimensionToAspectRatio(currentCanvasSize);
+ const originalCanvasSize = activeProject.settings.originalCanvasSize ?? null;
+ const presetIndex = findPresetIndexByAspectRatio({
+ presets: canvasPresets,
+ targetAspectRatio: currentAspectRatio,
+ });
+ const originalPresetValue = "original";
+ const selectedPresetValue =
+ presetIndex !== -1 ? presetIndex.toString() : originalPresetValue;
+
+ const handleAspectRatioChange = ({ value }: { value: string }) => {
+ if (value === originalPresetValue) {
+ const canvasSize = originalCanvasSize ?? currentCanvasSize;
+ editor.project.updateSettings({
+ settings: { canvasSize },
+ });
+ return;
+ }
+ const index = parseInt(value, 10);
+ const preset = canvasPresets[index];
+ if (preset) {
+ editor.project.updateSettings({ settings: { canvasSize: preset } });
+ }
+ };
+
+ const handleFpsChange = (value: string) => {
+ const fps = parseFloat(value);
+ editor.project.updateSettings({ settings: { fps } });
+ };
+
+ return (
+ <div className="flex flex-col gap-4">
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Name</PropertyItemLabel>
+ <PropertyItemValue>{activeProject.metadata.name}</PropertyItemValue>
+ </PropertyItem>
+
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Aspect ratio</PropertyItemLabel>
+ <PropertyItemValue>
+ <Select
+ value={selectedPresetValue}
+ onValueChange={(value) => handleAspectRatioChange({ value })}
+ >
+ <SelectTrigger className="bg-panel-accent">
+ <SelectValue placeholder="Select an aspect ratio" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value={originalPresetValue}>Original</SelectItem>
+ {canvasPresets.map((preset, index) => {
+ const label = dimensionToAspectRatio({
+ width: preset.width,
+ height: preset.height,
+ });
+ return (
+ <SelectItem key={label} value={index.toString()}>
+ {label}
+ </SelectItem>
+ );
+ })}
+ </SelectContent>
+ </Select>
+ </PropertyItemValue>
+ </PropertyItem>
+
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Frame rate</PropertyItemLabel>
+ <PropertyItemValue>
+ <Select
+ value={activeProject.settings.fps.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;
+ }) => (
+ <button
+ className={cn(
+ "border-foreground/15 hover:border-primary relative aspect-square w-full cursor-pointer overflow-hidden rounded-sm border",
+ isSelected && "border-primary border-2",
+ )}
+ onClick={onSelect}
+ type="button"
+ aria-label={`Select ${blur.label} blur`}
+ >
+ <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)` }}
+ loading="eager"
+ />
+ <div className="absolute right-1 bottom-1 left-1 text-center">
+ <span className="rounded bg-black/50 px-1 text-xs text-white">
+ {blur.label}
+ </span>
+ </div>
+ </button>
+ ),
+);
+
+BlurPreview.displayName = "BlurPreview";
+
+const BackgroundPreviews = memo(
+ ({
+ backgrounds,
+ currentBackgroundColor,
+ isColorBackground,
+ handleColorSelect,
+ useBackgroundColor = false,
+ }: {
+ backgrounds: string[];
+ currentBackgroundColor: string;
+ isColorBackground: boolean;
+ handleColorSelect: ({ bg }: { bg: string }) => void;
+ useBackgroundColor?: boolean;
+ }) => {
+ return useMemo(
+ () =>
+ backgrounds.map((bg, index) => (
+ <button
+ key={`${index}-${bg}`}
+ className={cn(
+ "border-foreground/15 hover:border-primary aspect-square w-full cursor-pointer rounded-sm border",
+ isColorBackground &&
+ bg === currentBackgroundColor &&
+ "border-primary border-2",
+ )}
+ style={
+ useBackgroundColor
+ ? { backgroundColor: bg }
+ : {
+ background: bg,
+ backgroundSize: "cover",
+ backgroundPosition: "center",
+ backgroundRepeat: "no-repeat",
+ }
+ }
+ onClick={() => handleColorSelect({ bg })}
+ type="button"
+ aria-label={`Select background ${useBackgroundColor ? bg : index + 1}`}
+ />
+ )),
+ [
+ backgrounds,
+ isColorBackground,
+ currentBackgroundColor,
+ handleColorSelect,
+ useBackgroundColor,
+ ],
+ );
+ },
+);
+
+BackgroundPreviews.displayName = "BackgroundPreviews";
+
+function BackgroundView() {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const blurLevels = useMemo(() => BLUR_INTENSITY_PRESETS, []);
+
+ const handleBlurSelect = useCallback(
+ async ({ blurIntensity }: { blurIntensity: number }) => {
+ await editor.project.updateSettings({
+ settings: { background: { type: "blur", blurIntensity } },
+ });
+ },
+ [editor.project],
+ );
+
+ const handleColorSelect = useCallback(
+ async ({ color }: { color: string }) => {
+ await editor.project.updateSettings({
+ settings: { background: { type: "color", color } },
+ });
+ },
+ [editor.project],
+ );
+
+ const currentBlurIntensity =
+ activeProject.settings.background.type === "blur"
+ ? activeProject.settings.background.blurIntensity
+ : DEFAULT_BLUR_INTENSITY;
+
+ const currentBackgroundColor =
+ activeProject.settings.background.type === "color"
+ ? activeProject.settings.background.color
+ : DEFAULT_COLOR;
+
+ const isBlurBackground = activeProject.settings.background.type === "blur";
+ const isColorBackground = activeProject.settings.background.type === "color";
+
+ const blurPreviews = useMemo(
+ () =>
+ blurLevels.map((blur) => (
+ <BlurPreview
+ key={blur.value}
+ blur={blur}
+ isSelected={isBlurBackground && currentBlurIntensity === blur.value}
+ onSelect={() => handleBlurSelect({ blurIntensity: blur.value })}
+ />
+ )),
+ [blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect],
+ );
+
+ return (
+ <div className="flex h-full flex-col gap-4">
+ <PropertyGroup title="Blur" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">{blurPreviews}</div>
+ </PropertyGroup>
+
+ <PropertyGroup title="Colors" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">
+ <div className="border-foreground/15 hover:border-primary flex aspect-square w-full cursor-pointer items-center justify-center rounded-sm border">
+ <HugeiconsIcon icon={DropperIcon} className="size-4" />
+ </div>
+ <BackgroundPreviews
+ backgrounds={colors}
+ currentBackgroundColor={currentBackgroundColor}
+ isColorBackground={isColorBackground}
+ handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
+ useBackgroundColor={true}
+ />
+ </div>
+ </PropertyGroup>
+
+ <PropertyGroup title="Pattern craft" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">
+ <BackgroundPreviews
+ backgrounds={patternCraftGradients}
+ currentBackgroundColor={currentBackgroundColor}
+ isColorBackground={isColorBackground}
+ handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
+ />
+ </div>
+ </PropertyGroup>
+
+ <PropertyGroup title="Syntax UI" defaultExpanded={false}>
+ <div className="grid w-full grid-cols-4 gap-2">
+ <BackgroundPreviews
+ backgrounds={syntaxUIGradients}
+ currentBackgroundColor={currentBackgroundColor}
+ isColorBackground={isColorBackground}
+ handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
+ />
+ </div>
+ </PropertyGroup>
+ </div>
+ );
+}