Storyboard Feature
Deploy / deploy (push) Failing after 1m53s

This commit is contained in:
twotalesanimation
2026-07-22 12:38:26 +02:00
parent 77a3161f0c
commit 3ae0a6e32f
3 changed files with 503 additions and 57 deletions
+7 -3
View File
@@ -12,11 +12,15 @@ export default async function DashboardLayout({
if (!session?.user) redirect("/login");
return (
<div className="flex h-screen bg-zinc-950 overflow-hidden">
<div className="flex h-screen bg-zinc-950 overflow-hidden print:block print:h-auto print:overflow-visible">
<div className="print:hidden">
<Sidebar />
<div className="flex-1 flex flex-col overflow-hidden">
</div>
<div className="flex-1 flex flex-col overflow-hidden print:block print:overflow-visible">
<div className="print:hidden">
<Header />
<main className="flex-1 overflow-auto">
</div>
<main className="flex-1 overflow-auto print:overflow-visible">
{children}
</main>
</div>
+448
View File
@@ -0,0 +1,448 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
// ─── Helpers ──────────────────────────────────────────────────────────────────
function esc(s: string | null | undefined): string {
if (!s) return "";
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
const STATUS_LABELS: Record<string, string> = {
WAITING: "Waiting",
IN_PROGRESS: "In Progress",
INTERNAL_REVIEW: "Internal Review",
READY_FOR_CLIENT: "Ready for Client",
CLIENT_REVIEW: "Client Review",
REVISIONS: "Revisions",
COMPLETE: "Complete",
};
// ─── Types ────────────────────────────────────────────────────────────────────
type Shot = {
id: string;
shotCode: string;
scene: string;
episode: string | null;
shotNumber: number;
description: string | null;
notes: string | null;
status: string;
thumbnailUrl: string | null;
frameStart: number | null;
frameEnd: number | null;
fps: number;
isKeyShot: boolean;
shotVersion: string;
shotGroup: { id: string; name: string } | null;
};
type Opts = {
showDesc: boolean;
showNotes: boolean;
showFrameRange: boolean;
showStatus: boolean;
showVersion: boolean;
};
type Group = { label: string; shots: Shot[] };
// ─── Shot card HTML (standard layout) ────────────────────────────────────────
function shotCardHtml(shot: Shot, opts: Opts): string {
const fc =
shot.frameStart != null && shot.frameEnd != null
? shot.frameEnd - shot.frameStart + 1
: null;
return `<div class="shot-card">
<div class="thumb-wrap">
${
shot.thumbnailUrl
? `<img src="${esc(shot.thumbnailUrl)}" alt="${esc(shot.shotCode)}" loading="eager" />`
: `<div class="no-thumb">No image</div>`
}
${shot.isKeyShot ? `<span class="key-badge">KEY</span>` : ""}
</div>
<div class="shot-info">
<div class="code-row">
<span class="shot-code">${esc(shot.shotCode)}</span>
${opts.showVersion ? `<span class="version">${esc(shot.shotVersion)}</span>` : ""}
</div>
${opts.showFrameRange && fc != null ? `<div class="frame-range">${shot.frameStart}${shot.frameEnd} &nbsp;(${fc}fr)</div>` : ""}
${opts.showDesc && shot.description ? `<div class="description">${esc(shot.description)}</div>` : ""}
${opts.showNotes && shot.notes ? `<div class="notes">${esc(shot.notes)}</div>` : ""}
${opts.showStatus ? `<div class="status-label">${esc(STATUS_LABELS[shot.status] ?? shot.status)}</div>` : ""}
</div>
</div>`;
}
// ─── Standard layout HTML ─────────────────────────────────────────────────────
function buildStandardHtml(
project: { name: string; code: string },
groups: Group[],
opts: Opts,
columns: number,
pageBreaks: boolean
): string {
const totalShots = groups.reduce((n, g) => n + g.shots.length, 0);
const groupsHtml = groups
.map((g, i) => {
const breakClass = pageBreaks && i > 0 ? " group-break" : "";
return `<div class="group-section${breakClass}">
${
g.label
? `<div class="group-header">
<div class="group-title">${esc(g.label)}</div>
<div class="group-meta">${esc(project.name)} &nbsp;·&nbsp; ${g.shots.length} shot${g.shots.length !== 1 ? "s" : ""}</div>
</div>`
: ""
}
<div class="grid cols-${columns}">
${g.shots.map((s) => shotCardHtml(s, opts)).join("\n ")}
</div>
</div>`;
})
.join("\n");
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>${esc(project.code)} — Storyboard</title>
<style>
@page { size: A4 portrait; margin: 14mm 12mm 12mm 12mm; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
background: #fff;
color: #111;
font-size: 10px;
}
/* ── Print bar (hidden on print) ── */
.print-bar {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
background: #18181b;
color: #fff;
padding: 10px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,.4);
}
.print-bar-info h1 { font-size: 14px; font-weight: 600; }
.print-bar-info p { font-size: 11px; color: #a1a1aa; margin-top: 1px; }
.print-btn {
background: #f59e0b; color: #000; border: none;
padding: 8px 22px; border-radius: 6px;
font-size: 13px; font-weight: 700; cursor: pointer;
white-space: nowrap; flex-shrink: 0;
}
.print-btn:hover { background: #d97706; }
@media print { .print-bar { display: none !important; } }
/* ── Content offset ── */
.content { padding-top: 56px; }
@media print { .content { padding-top: 0; } }
/* ── Group ── */
.group-break { break-before: page; page-break-before: always; }
.group-header { margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1.5px solid #d1d5db; }
.group-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .09em; color: #374151; }
.group-meta { font-size: 9px; color: #9ca3af; margin-top: 2px; }
.group-section { margin-bottom: 16px; }
/* ── Grid ── */
.grid { display: grid; gap: 8px; margin-bottom: 12px; }
.cols-2 { grid-template-columns: repeat(2, 1fr); }
.cols-3 { grid-template-columns: repeat(3, 1fr); }
.cols-4 { grid-template-columns: repeat(4, 1fr); }
/* ── Shot card ── */
.shot-card {
break-inside: avoid; page-break-inside: avoid;
border: 1px solid #e5e7eb; border-radius: 2px; overflow: hidden;
background: #fff;
}
.thumb-wrap {
position: relative; width: 100%;
aspect-ratio: 2.39 / 1; background: #f3f4f6; overflow: hidden;
}
.thumb-wrap img { width: 100%; height: 100%; object-fit: cover; display: block; }
.no-thumb {
display: flex; align-items: center; justify-content: center;
height: 100%; color: #d1d5db; font-size: 9px;
}
.key-badge {
position: absolute; top: 3px; right: 3px;
background: #f59e0b; color: #fff;
font-size: 7px; font-weight: 700; padding: 1px 4px; border-radius: 2px;
}
.shot-info { padding: 4px 6px 5px; }
.code-row { display: flex; align-items: baseline; justify-content: space-between; gap: 4px; }
.shot-code { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 9.5px; font-weight: 700; color: #111; }
.version { font-family: monospace; font-size: 8px; color: #9ca3af; }
.frame-range { font-size: 8px; color: #6b7280; margin-top: 1px; }
.description { font-size: 9px; color: #374151; line-height: 1.35; margin-top: 3px; }
.notes { font-size: 8px; color: #6b7280; font-style: italic; line-height: 1.3; margin-top: 2px; }
.status-label { font-size: 7.5px; color: #9ca3af; text-transform: uppercase; letter-spacing: .05em; margin-top: 2px; }
</style>
</head>
<body>
<div class="print-bar">
<div class="print-bar-info">
<h1>${esc(project.name)} — Storyboard</h1>
<p>${totalShots} shot${totalShots !== 1 ? "s" : ""} &nbsp;·&nbsp; ${columns} columns${groups[0]?.label ? ` &nbsp;·&nbsp; ${groups.length} group${groups.length !== 1 ? "s" : ""}` : ""}</p>
</div>
<button class="print-btn" onclick="window.print()">&#128424; Print / Save as PDF</button>
</div>
<div class="content">
${groupsHtml}
</div>
<script>
var imgs = Array.from(document.querySelectorAll('img'));
var done = 0;
function check() { if (++done >= imgs.length) setTimeout(function(){ window.print(); }, 200); }
if (!imgs.length) { setTimeout(function(){ window.print(); }, 200); }
else imgs.forEach(function(img) { if (img.complete) check(); else { img.onload = check; img.onerror = check; } });
</script>
</body>
</html>`;
}
// ─── Fullscreen layout HTML ────────────────────────────────────────────────────
function buildFullscreenHtml(
project: { name: string; code: string },
shots: Shot[],
opts: Opts
): string {
const pages = shots
.map((shot, idx) => {
const fc =
shot.frameStart != null && shot.frameEnd != null
? shot.frameEnd - shot.frameStart + 1
: null;
const isLast = idx === shots.length - 1;
const metaParts: string[] = [];
if (shot.episode) metaParts.push(`Ep&nbsp;${esc(shot.episode)}`);
metaParts.push(`Scene&nbsp;${esc(shot.scene)}`);
if (shot.shotGroup) metaParts.push(esc(shot.shotGroup.name));
if (opts.showFrameRange && fc != null) metaParts.push(`${shot.frameStart}${shot.frameEnd}&nbsp;(${fc}&nbsp;fr)`);
if (opts.showStatus) metaParts.push(esc(STATUS_LABELS[shot.status] ?? shot.status));
return `<div class="shot-page${isLast ? " last-page" : ""}">
<div class="image-area">
${
shot.thumbnailUrl
? `<img src="${esc(shot.thumbnailUrl)}" alt="${esc(shot.shotCode)}" loading="eager" />`
: `<div class="no-image">No image</div>`
}
<div class="watermark">${esc(project.code)}</div>
<div class="counter">${idx + 1}&nbsp;/&nbsp;${shots.length}</div>
</div>
<div class="info-bar">
<div class="shot-header">
<span class="shot-code-fs">${esc(shot.shotCode)}</span>
${shot.isKeyShot ? `<span class="key-label">&#9733; Key Shot</span>` : ""}
${opts.showVersion ? `<span class="version-fs">${esc(shot.shotVersion)}</span>` : ""}
</div>
${metaParts.length ? `<div class="meta-row">${metaParts.join("&ensp;·&ensp;")}</div>` : ""}
${opts.showDesc && shot.description ? `<div class="desc-fs">${esc(shot.description)}</div>` : ""}
${opts.showNotes && shot.notes ? `<div class="notes-fs">${esc(shot.notes)}</div>` : ""}
</div>
</div>`;
})
.join("\n");
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>${esc(project.code)} — Storyboard (Fullscreen)</title>
<style>
@page { size: A4 landscape; margin: 0; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #000; color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif; }
/* ── Print bar ── */
.print-bar {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
background: #18181b; color: #fff;
padding: 10px 20px;
display: flex; align-items: center; justify-content: space-between; gap: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,.6);
}
.print-bar-info h1 { font-size: 14px; font-weight: 600; }
.print-bar-info p { font-size: 11px; color: #a1a1aa; margin-top: 1px; }
.print-btn {
background: #f59e0b; color: #000; border: none;
padding: 8px 22px; border-radius: 6px;
font-size: 13px; font-weight: 700; cursor: pointer; white-space: nowrap;
}
.print-btn:hover { background: #d97706; }
@media print { .print-bar { display: none !important; } }
/* ── Shot page ── */
.shot-page {
display: flex; flex-direction: column;
height: 100vh;
break-after: page; page-break-after: always;
overflow: hidden;
background: #000;
}
.shot-page.last-page { break-after: auto; page-break-after: auto; }
@media print {
.shot-page { margin-top: 0; }
/* Offset for print bar gone on print — first page may have extra space in preview */
}
/* ── Image area ── */
.image-area {
flex: 1; overflow: hidden; background: #000; position: relative;
display: flex; align-items: center; justify-content: center;
min-height: 0;
}
.image-area img { width: 100%; height: 100%; object-fit: contain; display: block; }
.no-image { color: #333; font-size: 13px; }
.watermark {
position: absolute; top: 12px; left: 16px;
font-family: monospace; font-size: 9px; font-weight: 500;
color: rgba(255,255,255,.2); letter-spacing: .15em; text-transform: uppercase;
}
.counter {
position: absolute; top: 12px; right: 16px;
font-family: monospace; font-size: 9px;
color: rgba(255,255,255,.2);
}
/* ── Info bar ── */
.info-bar {
flex-shrink: 0;
background: #111; border-top: 1px solid #2a2a2a;
padding: 10px 24px;
display: flex; flex-direction: column; gap: 4px;
}
.shot-header { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.shot-code-fs { font-family: monospace; font-size: 20px; font-weight: 700; color: #fff; letter-spacing: -.01em; }
.key-label { font-size: 10px; color: #f59e0b; font-weight: 600; }
.version-fs { font-family: monospace; font-size: 11px; color: #6b7280; }
.meta-row { font-size: 10px; color: #6b7280; margin-top: 1px; }
.desc-fs { font-size: 12px; color: #d1d5db; line-height: 1.5; margin-top: 4px; }
.notes-fs { font-size: 10px; color: #6b7280; font-style: italic; margin-top: 2px; }
/* ── Preview offset ── */
.pages-wrap { padding-top: 52px; }
@media print { .pages-wrap { padding-top: 0; } }
</style>
</head>
<body>
<div class="print-bar">
<div class="print-bar-info">
<h1>${esc(project.name)} — Storyboard</h1>
<p>${shots.length} shot${shots.length !== 1 ? "s" : ""} &nbsp;·&nbsp; Fullscreen &nbsp;·&nbsp; A4 Landscape</p>
</div>
<button class="print-btn" onclick="window.print()">&#128424; Print / Save as PDF</button>
</div>
<div class="pages-wrap">
${pages}
</div>
<script>
var imgs = Array.from(document.querySelectorAll('img'));
var done = 0;
function check() { if (++done >= imgs.length) setTimeout(function(){ window.print(); }, 200); }
if (!imgs.length) { setTimeout(function(){ window.print(); }, 200); }
else imgs.forEach(function(img) { if (img.complete) check(); else { img.onload = check; img.onerror = check; } });
</script>
</body>
</html>`;
}
// ─── Route handler ────────────────────────────────────────────────────────────
export async function GET(req: NextRequest) {
const session = await auth();
if (!session?.user) return new NextResponse("Unauthorized", { status: 401 });
if (!["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session.user.role)) {
return new NextResponse("Forbidden", { status: 403 });
}
const p = req.nextUrl.searchParams;
const projectId = p.get("projectId");
if (!projectId) return new NextResponse("projectId required", { status: 400 });
const layout = p.get("layout") === "fullscreen" ? "fullscreen" : "standard";
const columns = Math.min(4, Math.max(2, parseInt(p.get("columns") ?? "3", 10)));
const groupBy = (p.get("groupBy") ?? "episode") as "episode" | "scene" | "group" | "none";
const epFilter = p.get("episodes")?.split(",").filter(Boolean) ?? [];
const grpFilter = p.get("groups")?.split(",").filter(Boolean) ?? [];
const opts: Opts = {
showDesc: p.get("desc") !== "0",
showNotes: p.get("notes") === "1",
showFrameRange: p.get("frameRange") !== "0",
showStatus: p.get("status") === "1",
showVersion: p.get("version") === "1",
};
const pageBreaks = p.get("pageBreaks") !== "0";
const onlyThumbs = p.get("onlyThumbs") === "1";
const project = await db.project.findUnique({
where: { id: projectId },
select: { name: true, code: true },
});
if (!project) return new NextResponse("Project not found", { status: 404 });
let shots = await db.shot.findMany({
where: { projectId },
select: {
id: true, shotCode: true, scene: true, episode: true, shotNumber: true,
description: true, notes: true, status: true, thumbnailUrl: true,
frameStart: true, frameEnd: true, fps: true, isKeyShot: true, shotVersion: true,
shotGroup: { select: { id: true, name: true } },
},
orderBy: [{ episode: "asc" }, { scene: "asc" }, { shotNumber: "asc" }],
});
if (onlyThumbs) shots = shots.filter((s) => !!s.thumbnailUrl);
if (epFilter.length) shots = shots.filter((s) => epFilter.includes(s.episode ?? ""));
if (grpFilter.length) shots = shots.filter((s) => grpFilter.includes(s.shotGroup?.id ?? ""));
// Group shots
const groups: Group[] = [];
if (groupBy === "none") {
groups.push({ label: "", shots });
} else {
const map = new Map<string, Shot[]>();
for (const shot of shots) {
let key = "";
if (groupBy === "episode") key = shot.episode ? `Episode ${shot.episode}` : "No Episode";
else if (groupBy === "scene") key = `Scene ${shot.scene}`;
else if (groupBy === "group") key = shot.shotGroup?.name ?? "No Group";
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(shot);
}
map.forEach((sh, label) => groups.push({ label, shots: sh }));
}
const html =
layout === "fullscreen"
? buildFullscreenHtml(project, groups.flatMap((g) => g.shots), opts)
: buildStandardHtml(project, groups, opts, columns, pageBreaks);
return new NextResponse(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
+40 -46
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useMemo, useCallback, useEffect } from "react";
import { useState, useMemo, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import {
LayoutGrid,
@@ -100,7 +100,7 @@ function ShotCard({ shot, opts }: { shot: Shot; opts: DisplayOptions }) {
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center w-full h-full">
<div className="flex items-center justify-center w-full h-full sb-no-thumb">
<Film className="h-6 w-6 text-zinc-700 print:text-zinc-400" />
</div>
)}
@@ -182,7 +182,7 @@ function StandardLayout({
{group.label && (
<div className="sb-avoid-break mb-4 print:mb-3">
<div className="flex items-center gap-3">
<div className="flex-1 h-px bg-zinc-700 print:bg-zinc-300" />
<div className="flex-1 h-px bg-zinc-700 print:bg-zinc-300 sb-group-header-line" />
<span className="text-xs font-bold uppercase tracking-widest text-zinc-400 print:text-zinc-600 px-2">
{group.label}
</span>
@@ -239,7 +239,7 @@ function FullscreenLayout({
)}
>
{/* Full-bleed image */}
<div className="relative flex-1 bg-zinc-900 print:bg-black overflow-hidden">
<div className="relative flex-1 bg-zinc-900 print:bg-black overflow-hidden sb-fs-image-area">
{shot.thumbnailUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
@@ -269,7 +269,7 @@ function FullscreenLayout({
</div>
{/* Info bar */}
<div className="bg-zinc-900 print:bg-zinc-950 border-t border-zinc-800 print:border-zinc-700 px-8 py-5 print:py-4 shrink-0">
<div className="bg-zinc-900 print:bg-zinc-950 border-t border-zinc-800 print:border-zinc-700 px-8 py-5 print:py-4 shrink-0 sb-fs-infobar">
<div className="flex items-start gap-8">
<div className="space-y-1 min-w-0 flex-1">
<div className="flex items-center gap-3 flex-wrap">
@@ -527,42 +527,32 @@ export function StoryboardGenerator({ projects }: Props) {
});
}, []);
// ── Print styles ────────────────────────────────────────────────────────────
const printStyles = useMemo(() => {
const isFullscreen = layout === "fullscreen";
return `
@media print {
.no-print { display: none !important; }
.print-area { display: block !important; }
body { background: ${isFullscreen ? "#000" : "#fff"} !important; margin: 0; }
@page {
size: ${isFullscreen ? "A4 landscape" : "A4 portrait"};
margin: ${isFullscreen ? "0" : "12mm 12mm 10mm 12mm"};
}
.sb-page-break { break-after: page; page-break-after: always; }
.sb-avoid-break { break-inside: avoid; page-break-inside: avoid; }
.sb-force-break { break-before: page; page-break-before: always; }
.print\\:bg-white { background: #fff !important; }
.print\\:bg-black { background: #000 !important; }
.print\\:text-black { color: #000 !important; }
.print\\:text-zinc-700 { color: #3f3f46 !important; }
.print\\:border-zinc-300 { border-color: #d4d4d8 !important; }
.print\\:h-screen { height: 100vh !important; }
.print\\:mb-0 { margin-bottom: 0 !important; }
.print\\:mt-0 { margin-top: 0 !important; }
.print\\:space-y-6 > * + * { margin-top: 1.5rem !important; }
.print\\:gap-2 { gap: 0.5rem !important; }
.print\\:py-4 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
}
`;
}, [layout]);
// ── PDF URL (opens in new tab → auto-triggers browser print dialog) ─────────────────────
const pdfUrl = useMemo(() => {
const params = new URLSearchParams({
projectId,
layout,
columns: String(columns),
groupBy,
desc: includeDescription ? "1" : "0",
notes: includeNotes ? "1" : "0",
frameRange: includeFrameRange ? "1" : "0",
status: includeStatus ? "1" : "0",
version: includeVersion ? "1" : "0",
pageBreaks: pageBreakBetweenGroups ? "1" : "0",
onlyThumbs: showOnlyWithThumbnails ? "1" : "0",
});
if (selectedEpisodes.size > 0) params.set("episodes", [...selectedEpisodes].join(","));
if (selectedGroups.size > 0) params.set("groups", [...selectedGroups].join(","));
return `/api/storyboard/pdf?${params}`;
}, [
projectId, layout, columns, groupBy,
includeDescription, includeNotes, includeFrameRange, includeStatus, includeVersion,
pageBreakBetweenGroups, showOnlyWithThumbnails, selectedEpisodes, selectedGroups,
]);
// ─── Render ──────────────────────────────────────────────────────────────────
return (
<>
{/* Print styles injected dynamically */}
<style dangerouslySetInnerHTML={{ __html: printStyles }} />
<div className="flex h-full overflow-hidden bg-zinc-950">
{/* ── Controls sidebar ── */}
<div className="no-print w-64 shrink-0 border-r border-zinc-800 overflow-y-auto flex flex-col bg-zinc-900">
@@ -742,17 +732,22 @@ export function StoryboardGenerator({ projects }: Props) {
<div className="shrink-0 p-4 border-t border-zinc-800 space-y-2">
{layout === "fullscreen" && (
<p className="text-[10px] text-zinc-600 text-center leading-tight">
Tip: choose Landscape in the print dialog for best results
Tip: choose Landscape in the print dialog
</p>
)}
<Button
className="w-full bg-amber-600 hover:bg-amber-500 text-white font-semibold"
onClick={() => window.print()}
disabled={filteredShots.length === 0}
>
{filteredShots.length > 0 ? (
<a href={pdfUrl} target="_blank" rel="noopener noreferrer" className="block">
<Button className="w-full bg-amber-600 hover:bg-amber-500 text-white font-semibold">
<Printer className="h-4 w-4 mr-2" />
Generate PDF
</Button>
</a>
) : (
<Button className="w-full bg-amber-600/40 text-white/40 font-semibold" disabled>
<Printer className="h-4 w-4 mr-2" />
Generate PDF
</Button>
)}
</div>
</div>
@@ -770,7 +765,7 @@ export function StoryboardGenerator({ projects }: Props) {
</div>
{/* Storyboard content */}
<div className="print-area p-6 print:p-0">
<div className={cn("print-area p-6 print:p-0", layout === "standard" ? "sb-standard-print" : "sb-fullscreen-print")}>
{isLoading ? (
<div className="flex items-center justify-center py-24 no-print">
<Loader2 className="h-8 w-8 animate-spin text-zinc-600" />
@@ -804,6 +799,5 @@ export function StoryboardGenerator({ projects }: Props) {
</div>
</div>
</div>
</>
);
}