feat: layout guide for tiktok

This commit is contained in:
Maze Winther
2025-08-04 17:17:48 +02:00
parent 0386624ff5
commit 9ec5a02252
5 changed files with 155 additions and 2 deletions
@@ -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;
}
@@ -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"