/** * GeneratingPanel — reusable enterprise-grade waiting overlay * used across Interactive surfaces. * * Mirrors the pattern Animate uses on its video-card processing * overlay (see Animate.tsx — bg-black/50 backdrop-blur-sm, * Loader2 size=32 animate-spin, text-white/90 sm font-medium * label, optional pill under it for extra context) so the two * features feel like siblings under the HomePilot brand. * * Two modes: * - `variant="overlay"` fills its positioned parent (use inside * a ``relative`` container; the form beneath stays in the DOM * and the overlay sits above it). * - `variant="panel"` renders a self-contained block for places * that don't have a host to overlay. * * Two progress styles (mixable): * - `steps[]` — vertical checklist; promotes each stage * "pending → running → done" as activeStep * advances. Best for a small fixed set of * phases (plan → graph → ready). * - `progress` + `progressLabel` — linear gradient progress bar * with text below. Best for long-running * batch ops (render scene 5 / 12). * * ``spinnerSize="large"`` switches the 32px Loader2 for a 64px * dual-ring spinner (outer static ring + rotating inner arc), * matching the visual weight of a full-screen creation modal. * ``iconOverride`` swaps the central Sparkles for per-phase icons * (e.g. ImageIcon while rendering stills, Film while rendering * clips) so the user sees what phase we're in at a glance. */ import React from "react"; import { Check, Loader2, Sparkles } from "lucide-react"; export interface GeneratingStep { label: string; } export interface GeneratingPanelProps { /** Big heading above the spinner, e.g. "Generating project". */ title: string; /** One-line hint below the title. Optional. */ description?: React.ReactNode; /** Variant picks full-parent overlay vs self-contained block. */ variant?: "overlay" | "panel"; /** Ordered stages the caller walks through. */ steps?: GeneratingStep[]; /** Which step is currently running. Earlier ones render as done; * later ones render as pending. Default 0. */ activeStep?: number; /** 0–100 linear progress. Renders the gradient bar + label * under the title. Omit for spinner-only indeterminate mode. */ progress?: number; /** Small text under the progress bar (e.g. "5 / 12 scenes"). */ progressLabel?: React.ReactNode; /** Tint color for the spinner + running-step indicator. Accepts * a Tailwind arbitrary-value color class (e.g. "text-[#3ea6ff]" * or "text-purple-400"). Default matches Animate (purple). */ accentClassName?: string; /** "small" = 32px Loader2 (default, legacy look). * "large" = 64px dual-ring spinner (Studio-style modal). */ spinnerSize?: "small" | "large"; /** Replace the decorative Sparkles with a phase-specific icon * (ImageIcon, Film, Gamepad2…). Sits centered inside the * dual-ring when spinnerSize='large', or next to the 32px * loader when 'small'. */ iconOverride?: React.ReactNode; /** Footer pill copy. Default: "AI is working — this usually * takes a few seconds." Pass null to hide the pill entirely. */ footerHint?: React.ReactNode | null; } export function GeneratingPanel({ title, description, variant = "overlay", steps, activeStep = 0, progress, progressLabel, accentClassName = "text-purple-400", spinnerSize = "small", iconOverride, footerHint, }: GeneratingPanelProps) { const clamped = typeof progress === "number" ? Math.max(0, Math.min(100, progress)) : undefined; const hasProgress = clamped !== undefined; const spinner = spinnerSize === "large" ? : ; const body = (
{spinner}
{title}
{description && (
{description}
)} {hasProgress && (
{progressLabel && (
{progressLabel}
)}
)} {steps && steps.length > 0 && (
    {steps.map((s, i) => { const done = i < activeStep; const running = i === activeStep; return (
  1. {s.label}
  2. ); })}
)} {footerHint !== null && (
{footerHint ?? "AI is working — this usually takes a few seconds."}
)}
); if (variant === "overlay") { return (
{body}
); } return (
{body}
); } // ── Spinner variants ──────────────────────────────────────────── function SmallSpinner({ accentClassName, icon, }: { accentClassName: string; icon?: React.ReactNode }) { return (
{icon ?? }
); } function LargeSpinner({ accentClassName, icon, }: { accentClassName: string; icon?: React.ReactNode }) { // Studio-inspired dual-ring: a faint static outer ring plus a // bright rotating arc on top, with the phase icon planted dead // center. Keeps the visual weight enterprise-y for long waits // without depending on any Studio code. return (
{/* Static base ring — signals "work in progress" even when the animated layer is occluded. */}
{/* Rotating arc. Using the built-in animate-spin on a partial border rather than a keyframe gradient so the look stays crisp at any screen DPR. */}
{/* Center phase icon. */}
{icon ?? }
); } /** * Derive a Tailwind border-color class from an accent text-color * class so the dual-ring picks up whatever brand tint the caller * passed. Falls back to white/20 + white for unknown prefixes so * the component still renders sensibly with custom colors. */ function accentToBorder(textClass: string, opacitySuffix: string): string { // "text-purple-400" → "border-purple-400", "text-[#3ea6ff]" → // "border-[#3ea6ff]". We keep opacity as a raw suffix so callers // can pass an empty string for the solid ring + "/20" for the // faint base. Tailwind JIT will emit both forms safely. const colorPart = textClass.replace(/^text-/, ""); if (!colorPart) return `border-white${opacitySuffix || ""}`; return `border-${colorPart}${opacitySuffix}`; } function StepIndicator({ state, accentClassName, }: { state: "pending" | "running" | "done"; accentClassName: string; }) { if (state === "done") { return ( ); } if (state === "running") { return ( ); } return ( ); }