import { type ReactNode, useState } from "react"; import { LoaderCircleIcon } from "lucide-react"; import { isMobile } from "react-device-detect"; import { type PopoverItem } from "@storyteller/ui-popover"; import { TabSelector } from "@storyteller/ui-tab-selector"; import { TruchetPattern } from "@storyteller/ui-vfx"; import Seo from "../../components/seo"; import { useIsMobile } from "../ui/use-mobile"; import { GalleryAutoplayToggle, GallerySelectToggle, GalleryViewToggle, } from "./GalleryViewToggle"; import { MobileCreateTabsProvider, type MobileCreateTab, } from "./mobile-create-tabs"; interface CreateMediaPageShellProps { // SEO title: string; description: string; // Auth state — `authChecked` gates the initial spinner so we don't flash // logged-out chrome while the session resolves. Pages stay viewable for // logged-out users; the signup CTA modal is triggered at generate time. authChecked: boolean; // Content hasContent: boolean; // Video page only: playing/still toggle for video preview thumbnails, // rendered next to the grid/list toggle on the mobile History tab. showAutoplayToggle?: boolean; // Multi-select + batch download toggle, rendered in the same mobile // History-tab cluster (the desktop toggle lives in the TopBar). showSelectToggle?: boolean; emptyStateTitle: string; emptyStateSubtitle: string; // Optional CTA rendered under the empty-state subtitle (e.g. a signup button // for logged-out visitors). Omitted for logged-in users. emptyStateCta?: ReactNode; bottomOffset: number; // Model selector modelItems: PopoverItem[]; onModelChange: (item: PopoverItem) => void; // Glow orb overrides (optional - defaults provided) glowOrbs?: ReactNode; // Children slots gridContent: ReactNode; promptBox: ReactNode; // Mobile-only form rendered on the "Generate" tab. When provided and the // viewport is mobile, the shell swaps the floating prompt box for a tabbed // Generate / History layout. promptForm?: ReactNode; modals: ReactNode; } const MOBILE_TABS = [ { id: "generate", label: "Generate" }, { id: "history", label: "History" }, ]; export function CreateMediaPageShell({ title, description, authChecked, hasContent, showAutoplayToggle = false, showSelectToggle = false, emptyStateTitle, emptyStateSubtitle, emptyStateCta, bottomOffset, glowOrbs, gridContent, promptBox, promptForm, modals, }: CreateMediaPageShellProps) { const viewportIsMobile = useIsMobile(); const [mobileTab, setMobileTab] = useState("generate"); if (!authChecked) { return (
); } // Mobile: split into Generate (form) / History (gallery) tabs so the prompt // box no longer overlaps results. Desktop keeps the floating prompt box. if (viewportIsMobile && promptForm) { return ( setMobileTab("history"), }} >
{/* Flex (not absolute) keeps the toggle cluster from overlapping the tabs — the tabs stay centered via the equal flex-1 sides and just shift left if the cluster outgrows its half. */}
setMobileTab(id as MobileCreateTab)} className="w-auto shrink-0" />
{mobileTab === "history" && hasContent && ( <> {showSelectToggle && } {showAutoplayToggle && } )}
{mobileTab === "generate" ? ( promptForm ) : hasContent ? (
{gridContent}
) : (
{emptyStateTitle} Your generations will appear here.
)}
{modals} ); } return (
{/* Decorative background, empty state only. Not rendered on mobile devices: the truchet uses mask-image, a full-screen GPU layer that iOS Safari must re-rasterize when an overlay (mobile menu, modal) composites over it, causing multi-second hangs. (This is distinct from backdrop-filter.) We gate on the actual device (react-device-detect's user-agent check) rather than a viewport breakpoint so the layers never mount on a real phone/tablet, and a narrow desktop window still gets the decoration. */} {!hasContent && !isMobile && ( <> {glowOrbs}
)}
{!hasContent && (

{emptyStateTitle}

{emptyStateSubtitle} {emptyStateCta &&
{emptyStateCta}
}
)} {hasContent && (
{gridContent}
)} {/* Bottom fade behind the floating prompt box so it stays legible over the feed. Sits under the prompt box (z-30). */} {hasContent && (
)} {promptBox}
{modals}
); }