summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/preview-panel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor/preview-panel.tsx')
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx111
1 files changed, 83 insertions, 28 deletions
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx
index b9c03382..3873ebca 100644
--- a/apps/web/src/components/editor/preview-panel.tsx
+++ b/apps/web/src/components/editor/preview-panel.tsx
@@ -14,8 +14,18 @@ import { cn } from "@/lib/utils";
import { formatTimeCode } from "@/lib/time";
import { EditableTimecode } from "@/components/ui/editable-timecode";
import { FONT_CLASS_MAP } from "@/lib/font-config";
-import { useProjectStore } from "@/stores/project-store";
+import { DEFAULT_CANVAS_SIZE, 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;
@@ -26,8 +36,8 @@ interface ActiveElement {
export function PreviewPanel() {
const { tracks, getTotalDuration, updateTextElement } = useTimelineStore();
const { mediaItems } = useMediaStore();
- const { currentTime, toggle, setCurrentTime, isPlaying } = usePlaybackStore();
- const { canvasSize } = useEditorStore();
+ const { currentTime, toggle, setCurrentTime } = usePlaybackStore();
+ const { activeProject } = useProjectStore();
const previewRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [previewDimensions, setPreviewDimensions] = useState({
@@ -35,7 +45,8 @@ export function PreviewPanel() {
height: 0,
});
const [isExpanded, setIsExpanded] = useState(false);
- const { activeProject } = useProjectStore();
+
+ const canvasSize = activeProject?.canvasSize || DEFAULT_CANVAS_SIZE;
const [dragState, setDragState] = useState<TextElementDragState>({
isDragging: false,
elementId: null,
@@ -223,7 +234,8 @@ export function PreviewPanel() {
const getActiveElements = (): ActiveElement[] => {
const activeElements: ActiveElement[] = [];
- tracks.forEach((track) => {
+ // Iterate tracks from bottom to top so topmost track renders last (on top)
+ [...tracks].reverse().forEach((track) => {
track.elements.forEach((element) => {
if (element.hidden) return;
const elementStart = element.startTime;
@@ -300,6 +312,7 @@ export function PreviewPanel() {
trimEnd={element.trimEnd}
clipDuration={element.duration}
className="w-full h-full object-cover"
+ trackMuted={true}
/>
</div>
);
@@ -343,7 +356,7 @@ export function PreviewPanel() {
return (
<div
key={element.id}
- className="absolute flex items-center justify-center cursor-grab"
+ className="absolute cursor-grab"
onMouseDown={(e) =>
handleTextMouseDown(e, element, elementData.track.id)
}
@@ -364,7 +377,7 @@ export function PreviewPanel() {
canvasSize.height) *
100
}%`,
- transform: `translate(-50%, -50%) rotate(${element.rotation}deg) scale(${scaleRatio})`,
+ transform: `translate(-50%, -50%) rotate(${element.rotation}deg)`,
opacity: element.opacity,
zIndex: 100 + index, // Text elements on top
}}
@@ -372,16 +385,16 @@ export function PreviewPanel() {
<div
className={fontClassName}
style={{
- fontSize: `${element.fontSize}px`,
+ fontSize: `${element.fontSize * scaleRatio}px`,
color: element.color,
backgroundColor: element.backgroundColor,
textAlign: element.textAlign,
fontWeight: element.fontWeight,
fontStyle: element.fontStyle,
textDecoration: element.textDecoration,
- padding: "4px 8px",
- borderRadius: "2px",
- whiteSpace: "pre-wrap",
+ padding: `${4 * scaleRatio}px ${8 * scaleRatio}px`,
+ borderRadius: `${2 * scaleRatio}px`,
+ whiteSpace: "nowrap",
// Fallback for system fonts that don't have classes
...(fontClassName === "" && { fontFamily: element.fontFamily }),
}}
@@ -423,6 +436,7 @@ export function PreviewPanel() {
trimStart={element.trimStart}
trimEnd={element.trimEnd}
clipDuration={element.duration}
+ trackMuted={element.muted || elementData.track.muted}
/>
</div>
);
@@ -448,14 +462,18 @@ export function PreviewPanel() {
// Audio elements (no visual representation)
if (mediaItem.type === "audio") {
return (
- <div key={element.id} className="absolute inset-0">
+ <div
+ key={element.id}
+ className="absolute inset-0"
+ style={{ pointerEvents: "none" }}
+ >
<AudioPlayer
src={mediaItem.url!}
clipStartTime={element.startTime}
trimStart={element.trimStart}
trimEnd={element.trimEnd}
clipDuration={element.duration}
- trackMuted={elementData.track.muted}
+ trackMuted={element.muted || elementData.track.muted}
/>
</div>
);
@@ -496,13 +514,7 @@ export function PreviewPanel() {
renderElement(elementData, index)
)
)}
- {activeProject?.backgroundType === "blur" &&
- blurBackgroundElements.length === 0 &&
- activeElements.length > 0 && (
- <div className="absolute bottom-2 left-2 right-2 bg-black/70 text-white text-xs p-2 rounded">
- Add a video or image to use blur background
- </div>
- )}
+ <LayoutGuideOverlay />
</div>
) : null}
@@ -758,13 +770,7 @@ function FullscreenPreview({
renderElement(elementData, index)
)
)}
- {activeProject?.backgroundType === "blur" &&
- blurBackgroundElements.length === 0 &&
- activeElements.length > 0 && (
- <div className="absolute bottom-2 left-2 right-2 bg-black/70 text-white text-xs p-2 rounded">
- Add a video or image to use blur background
- </div>
- )}
+ <LayoutGuideOverlay />
</div>
</div>
<div className="p-4 bg-background">
@@ -799,6 +805,7 @@ function PreviewToolbar({
getTotalDuration: () => number;
}) {
const { isPlaying } = usePlaybackStore();
+ const { layoutGuide, toggleLayoutGuide } = useEditorStore();
if (isExpanded) {
return (
@@ -818,7 +825,7 @@ 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 pt-4 w-full"
>
<div className="flex items-center gap-2">
<Button
@@ -834,6 +841,54 @@ function PreviewToolbar({
<Play className="h-3 w-3" />
)}
</Button>
+ <Popover>
+ <PopoverTrigger asChild>
+ <Button
+ variant="text"
+ size="icon"
+ className="h-auto p-0 mr-1"
+ 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"