summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2025-08-04 17:17:48 +0200
committerMaze Winther <mazewinther@gmail.com>2025-08-04 17:17:48 +0200
commit9ec5a022520aa3a2d1e18b4e3b4056f2ff5dcd35 (patch)
tree3fb53ec6ca8ead167aed3ba69c5f8b0dfd40b6ef /apps/web/src/components/editor
parent0386624ff5fd6d82d64d6b103ddc1a7e5045f45b (diff)
downloadopencut-9ec5a022520aa3a2d1e18b4e3b4056f2ff5dcd35.tar.gz
feat: layout guide for tiktok
Diffstat (limited to 'apps/web/src/components/editor')
-rw-r--r--apps/web/src/components/editor/layout-guide-overlay.tsx27
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx68
2 files changed, 93 insertions, 2 deletions
diff --git a/apps/web/src/components/editor/layout-guide-overlay.tsx b/apps/web/src/components/editor/layout-guide-overlay.tsx
new file mode 100644
index 00000000..ff4cde57
--- /dev/null
+++ b/apps/web/src/components/editor/layout-guide-overlay.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import { useEditorStore } from "@/stores/editor-store";
+import Image from "next/image";
+
+function TikTokGuide() {
+ return (
+ <div className="absolute inset-0 pointer-events-none">
+ <Image
+ src="/platform-guides/tiktok-blueprint.png"
+ alt="TikTok layout guide"
+ className="absolute inset-0 w-full h-full object-contain"
+ draggable={false}
+ fill
+ />
+ </div>
+ );
+}
+
+export function LayoutGuideOverlay() {
+ const { layoutGuide } = useEditorStore();
+
+ if (layoutGuide.platform === null) return null;
+ if (layoutGuide.platform === "tiktok") return <TikTokGuide />;
+
+ return null;
+}
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index b9c03382..f4cd2016 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -8,7 +8,14 @@ import { useEditorStore } from "@/stores/editor-store";
import { VideoPlayer } from "@/components/ui/video-player";
import { AudioPlayer } from "@/components/ui/audio-player";
import { Button } from "@/components/ui/button";
-import { Play, Pause, Expand, SkipBack, SkipForward } from "lucide-react";
+import {
+ Play,
+ Pause,
+ Expand,
+ SkipBack,
+ SkipForward,
+ LayoutGrid,
+} from "lucide-react";
import { useState, useRef, useEffect, useCallback } from "react";
import { cn } from "@/lib/utils";
import { formatTimeCode } from "@/lib/time";
@@ -16,6 +23,16 @@ import { EditableTimecode } from "@/components/ui/editable-timecode";
import { FONT_CLASS_MAP } from "@/lib/font-config";
import { useProjectStore } from "@/stores/project-store";
import { TextElementDragState } from "@/types/editor";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Checkbox } from "@/components/ui/checkbox";
+import { LayoutGuideOverlay } from "./layout-guide-overlay";
+import { Label } from "../ui/label";
+import { SocialsIcon } from "../icons";
+import { PLATFORM_LAYOUTS, type PlatformLayout } from "@/stores/editor-store";
interface ActiveElement {
element: TimelineElement;
@@ -496,6 +513,7 @@ export function PreviewPanel() {
renderElement(elementData, index)
)
)}
+ <LayoutGuideOverlay />
{activeProject?.backgroundType === "blur" &&
blurBackgroundElements.length === 0 &&
activeElements.length > 0 && (
@@ -758,6 +776,7 @@ function FullscreenPreview({
renderElement(elementData, index)
)
)}
+ <LayoutGuideOverlay />
{activeProject?.backgroundType === "blur" &&
blurBackgroundElements.length === 0 &&
activeElements.length > 0 && (
@@ -799,6 +818,7 @@ function PreviewToolbar({
getTotalDuration: () => number;
}) {
const { isPlaying } = usePlaybackStore();
+ const { layoutGuide, toggleLayoutGuide } = useEditorStore();
if (isExpanded) {
return (
@@ -818,9 +838,53 @@ function PreviewToolbar({
return (
<div
data-toolbar
- className="flex justify-between gap-2 px-1.5 pr-4 py-1.5 border border-border/50 w-auto absolute bottom-4 right-4 bg-black/20 rounded-full backdrop-blur-l text-white"
+ className="flex justify-end gap-2 h-auto pb-5 pr-5 w-full"
>
<div className="flex items-center gap-2">
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ variant="text"
+ size="icon"
+ className="h-auto p-0"
+ title="Toggle layout guide"
+ >
+ <SocialsIcon className="!size-6" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-80">
+ <div className="grid gap-4">
+ <div className="space-y-2">
+ <h4 className="font-medium leading-none">Layout guide</h4>
+ <p className="text-sm text-muted-foreground">
+ Show platform-specific layout guides to help align your
+ content with interface elements like profile pictures,
+ usernames, and interaction buttons.
+ </p>
+ </div>
+ <div className="grid gap-2">
+ <div className="flex items-center space-x-2">
+ <Checkbox
+ id="none"
+ checked={layoutGuide.platform === null}
+ onCheckedChange={() => toggleLayoutGuide(layoutGuide.platform || "tiktok")}
+ />
+ <Label htmlFor="none">None</Label>
+ </div>
+ {Object.entries(PLATFORM_LAYOUTS).map(([platform, label]) => (
+ <div key={platform} className="flex items-center space-x-2">
+ <Checkbox
+ id={platform}
+ checked={layoutGuide.platform === platform}
+ onCheckedChange={() => toggleLayoutGuide(platform as PlatformLayout)}
+ />
+ <Label htmlFor={platform}>{label}</Label>
+ </div>
+ ))}
+ </div>
+ </div>
+ </PopoverContent>
+ </Popover>
<Button
variant="text"
size="icon"