Files
2026-09-14 23:35:23 +08:00

399 lines
20 KiB
TypeScript

import React, { useState } from 'react';
import {
MapPin,
Users,
MessageCircle,
Activity,
Eye,
Copy,
Check,
Sparkles,
Camera
} from 'lucide-react';
import { SCENES } from '../data/storyData';
import { soundManager } from '../utils/audio';
interface StoryboardViewProps {
isBedtimeMode: boolean;
selectedSceneId?: number;
}
export const StoryboardView: React.FC<StoryboardViewProps> = ({
isBedtimeMode,
selectedSceneId,
}) => {
const [filterAct, setFilterAct] = useState<'all' | 'beginning' | 'middle' | 'end'>('all');
const [activeSceneId, setActiveSceneId] = useState<number>(selectedSceneId || 1);
const [copiedText, setCopiedText] = useState<string | null>(null);
const filteredScenes = filterAct === 'all'
? SCENES
: SCENES.filter(s => s.act === filterAct);
const currentScene = SCENES.find(s => s.id === activeSceneId) || SCENES[0];
const handleCopyPrompt = (text: string) => {
soundManager.playPop();
navigator.clipboard.writeText(text);
setCopiedText(text);
setTimeout(() => setCopiedText(null), 2500);
};
return (
<div className={`min-h-[calc(100vh-7rem)] py-8 px-4 sm:px-6 lg:px-8 transition-colors duration-300 ${
isBedtimeMode
? 'bg-gradient-to-b from-indigo-950 via-slate-900 to-indigo-950 text-indigo-100'
: 'bg-[#faf6ee] text-stone-900'
}`}>
<div className="max-w-7xl mx-auto space-y-8">
{/* Header & Concept Pitch */}
<div className={`p-6 sm:p-8 rounded-3xl border shadow-sm ${
isBedtimeMode
? 'bg-slate-900/90 border-indigo-800'
: 'bg-white border-amber-200/90'
}`}>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider bg-amber-500/10 text-amber-700 dark:text-amber-400 border border-amber-300/40">
Visual Narrative Architecture
</span>
<span className="text-xs text-stone-500 dark:text-indigo-300">
5 Key Visual Beats (Act I, II, III)
</span>
</div>
<h2 className="text-2xl sm:text-3xl font-heading font-bold text-amber-950 dark:text-white">
Complete Storyboard & Scene Bible
</h2>
<p className="mt-1 text-sm sm:text-base text-stone-600 dark:text-indigo-200 max-w-3xl leading-relaxed">
Every frame pairs narrative intent with rigorous art direction: environmental settings, natural dialogue, physical actions, camera perspectives, and exact color palettes.
</p>
</div>
{/* Act Filter Tabs */}
<div className="flex items-center gap-1.5 bg-stone-100 dark:bg-indigo-950 p-1.5 rounded-2xl border border-stone-200 dark:border-indigo-800 shrink-0">
<button
onClick={() => { soundManager.playPop(); setFilterAct('all'); }}
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
filterAct === 'all'
? 'bg-amber-500 text-white shadow-sm'
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
}`}
>
All (5)
</button>
<button
onClick={() => { soundManager.playPop(); setFilterAct('beginning'); }}
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
filterAct === 'beginning'
? 'bg-amber-500 text-white shadow-sm'
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
}`}
>
Act I: Beginning
</button>
<button
onClick={() => { soundManager.playPop(); setFilterAct('middle'); }}
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
filterAct === 'middle'
? 'bg-amber-500 text-white shadow-sm'
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
}`}
>
Act II: Middle
</button>
<button
onClick={() => { soundManager.playPop(); setFilterAct('end'); }}
className={`px-3 py-1.5 rounded-xl text-xs font-bold transition-all ${
filterAct === 'end'
? 'bg-amber-500 text-white shadow-sm'
: 'text-stone-600 dark:text-indigo-300 hover:bg-stone-200/60 dark:hover:bg-indigo-900'
}`}
>
Act III: End
</button>
</div>
</div>
</div>
{/* Storyboard Navigation Thumbnails */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-3">
{filteredScenes.map((sc) => {
const isSelected = activeSceneId === sc.id;
return (
<div
key={sc.id}
onClick={() => {
soundManager.playPop();
setActiveSceneId(sc.id);
}}
className={`p-3 rounded-2xl cursor-pointer border transition-all text-left group ${
isSelected
? 'bg-amber-100/90 dark:bg-indigo-900 border-amber-400 dark:border-indigo-600 shadow-md ring-2 ring-amber-400/40'
: 'bg-white dark:bg-slate-900/80 border-stone-200 dark:border-indigo-900 hover:bg-amber-50/60 dark:hover:bg-indigo-900/50'
}`}
>
<div className="relative aspect-[16/10] rounded-xl overflow-hidden mb-2.5">
<img
src={sc.image}
alt={sc.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
<span className="absolute top-1.5 left-1.5 bg-black/60 text-white text-[10px] font-bold px-2 py-0.5 rounded-md backdrop-blur-sm">
Scene {sc.id}
</span>
<span className={`absolute bottom-1.5 right-1.5 text-[9px] font-bold uppercase px-1.5 py-0.2 rounded-md ${
sc.act === 'beginning' ? 'bg-amber-500 text-white' : sc.act === 'middle' ? 'bg-sky-500 text-white' : 'bg-purple-500 text-white'
}`}>
{sc.act}
</span>
</div>
<h4 className="font-heading font-bold text-xs line-clamp-1 text-stone-900 dark:text-white">
{sc.title}
</h4>
<p className="text-[11px] text-stone-500 dark:text-indigo-300 line-clamp-1">
{sc.subtitle}
</p>
</div>
);
})}
</div>
{/* Detailed Scene Storyboard Inspector */}
<div className={`rounded-3xl border overflow-hidden shadow-xl ${
isBedtimeMode
? 'bg-slate-900 border-indigo-800'
: 'bg-white border-amber-200/90'
}`}>
{/* Top Bar with Scene Header */}
<div className="p-6 bg-gradient-to-r from-amber-500/10 via-rose-500/10 to-transparent border-b border-stone-200 dark:border-indigo-800 flex flex-wrap items-center justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<span className={`px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wider ${
currentScene.act === 'beginning'
? 'bg-amber-100 text-amber-800 border border-amber-300'
: currentScene.act === 'middle'
? 'bg-sky-100 text-sky-800 border border-sky-300'
: 'bg-purple-100 text-purple-800 border border-purple-300'
}`}>
Act: {currentScene.act.toUpperCase()}
</span>
<span className="text-xs font-semibold text-stone-500 dark:text-indigo-300">
Scene #{currentScene.id} of {SCENES.length}
</span>
</div>
<h3 className="text-2xl sm:text-3xl font-heading font-bold text-stone-900 dark:text-white">
{currentScene.title}
</h3>
<p className="text-sm font-semibold text-amber-700 dark:text-amber-400">
"{currentScene.subtitle}"
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-stone-500 dark:text-indigo-300">Key Palette:</span>
<div className="flex items-center gap-1.5">
{currentScene.artNotes.palette.map((color, idx) => (
<div
key={idx}
style={{ backgroundColor: color }}
className="w-6 h-6 rounded-full border-2 border-white dark:border-slate-800 shadow-sm"
title={color}
/>
))}
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 divide-y lg:divide-y-0 lg:divide-x divide-stone-200 dark:divide-indigo-800">
{/* Visual Frame & Illustration Prompt Column (5 cols) */}
<div className="lg:col-span-5 p-6 space-y-6">
{/* Scene Frame Rendering */}
<div className="relative rounded-2xl overflow-hidden shadow-lg border border-stone-200 dark:border-indigo-700 aspect-[4/3] group">
<img
src={currentScene.image}
alt={currentScene.imageAlt}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent pointer-events-none" />
<span className="absolute bottom-3 left-3 text-white text-xs font-bold drop-shadow-md">
Frame #{currentScene.id} Preview
</span>
</div>
{/* Illustration Prompt Box */}
<div className="p-4 rounded-2xl bg-amber-50/70 dark:bg-indigo-950/60 border border-amber-200 dark:border-indigo-800 space-y-2.5">
<div className="flex items-center justify-between">
<span className="text-xs font-heading font-bold uppercase tracking-wider text-amber-800 dark:text-amber-300 flex items-center gap-1.5">
<Sparkles className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400" />
Illustration Prompt (AI / Artist Bible)
</span>
<button
onClick={() => handleCopyPrompt(currentScene.illustrationPrompt)}
className="flex items-center gap-1 text-[11px] font-bold text-amber-700 dark:text-amber-300 hover:text-amber-900 bg-white/80 dark:bg-indigo-900/80 px-2 py-1 rounded-lg border border-amber-200 dark:border-indigo-700 transition-colors"
>
{copiedText === currentScene.illustrationPrompt ? (
<>
<Check className="w-3 h-3 text-emerald-600" />
<span>Copied!</span>
</>
) : (
<>
<Copy className="w-3 h-3" />
<span>Copy Prompt</span>
</>
)}
</button>
</div>
<p className="text-xs sm:text-sm font-reading leading-relaxed italic text-stone-700 dark:text-stone-300">
"{currentScene.illustrationPrompt}"
</p>
</div>
{/* Art Direction Specs */}
<div className="space-y-3">
<h5 className="font-heading font-bold text-xs uppercase tracking-wider text-stone-500 dark:text-indigo-400 flex items-center gap-1.5">
<Camera className="w-3.5 h-3.5" />
Visual Direction Parameters
</h5>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
Camera Perspective:
</span>
<p className="text-stone-700 dark:text-stone-300 leading-snug">
{currentScene.artNotes.cameraAngle}
</p>
</div>
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
Lighting & Atmosphere:
</span>
<p className="text-stone-700 dark:text-stone-300 leading-snug">
{currentScene.artNotes.lighting}
</p>
</div>
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
Focal Point:
</span>
<p className="text-stone-700 dark:text-stone-300 leading-snug">
{currentScene.artNotes.focalPoint}
</p>
</div>
<div className="p-3 rounded-xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800">
<span className="font-bold text-amber-800 dark:text-amber-300 block mb-1">
Character Expressions:
</span>
<p className="text-stone-700 dark:text-stone-300 leading-snug">
{currentScene.artNotes.expressions}
</p>
</div>
</div>
</div>
</div>
{/* Script & Stage Direction Storyboard Column (7 cols) */}
<div className="lg:col-span-7 p-6 space-y-6">
{/* Setting */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
<MapPin className="w-4 h-4 text-rose-500" />
<span>Scene Setting & Environment</span>
</div>
<div className="p-4 rounded-2xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800 text-sm leading-relaxed text-stone-800 dark:text-stone-200 font-reading">
{currentScene.setting}
</div>
</div>
{/* Characters Present */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
<Users className="w-4 h-4 text-sky-500" />
<span>Characters on Stage</span>
</div>
<div className="flex flex-wrap gap-2">
{currentScene.characters.map((charName) => (
<span
key={charName}
className="px-3 py-1 rounded-xl bg-amber-100 dark:bg-indigo-900 border border-amber-200 dark:border-indigo-700 text-xs font-bold text-amber-900 dark:text-amber-300 flex items-center gap-1.5"
>
<span>{charName === 'Pip' ? '🦔' : charName === 'Caelum' ? '☁️' : charName === 'Barnaby' ? '🐻' : '🐦'}</span>
<span>{charName}</span>
</span>
))}
</div>
</div>
{/* Script & Dialogue */}
<div className="space-y-3">
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
<MessageCircle className="w-4 h-4 text-amber-500" />
<span>Spoken Dialogue & Tone</span>
</div>
<div className="space-y-3">
{currentScene.dialogue.map((dlg, i) => (
<div
key={i}
className="p-4 rounded-2xl bg-amber-50/50 dark:bg-indigo-950/60 border border-amber-200/60 dark:border-indigo-800 flex flex-col sm:flex-row sm:items-baseline justify-between gap-2"
>
<div className="space-y-1">
<span className="font-heading font-bold text-sm text-amber-900 dark:text-amber-300">
{dlg.speaker}:
</span>
<p className="text-base font-reading text-stone-900 dark:text-white italic">
"{dlg.text}"
</p>
</div>
<span className="shrink-0 text-xs font-semibold px-2.5 py-1 rounded-lg bg-white dark:bg-indigo-900 text-stone-600 dark:text-indigo-300 border border-stone-200 dark:border-indigo-700 self-start sm:self-auto">
🎭 Tone: {dlg.tone}
</span>
</div>
))}
</div>
</div>
{/* Key Physical Actions & Choreography */}
<div className="space-y-3">
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-stone-500 dark:text-indigo-400">
<Activity className="w-4 h-4 text-emerald-500" />
<span>Key Physical Actions (Beat by Beat)</span>
</div>
<div className="p-4 rounded-2xl bg-stone-50 dark:bg-indigo-950/40 border border-stone-200 dark:border-indigo-800 space-y-2.5">
{currentScene.keyActions.map((action, i) => (
<div key={i} className="flex items-start gap-3 text-sm text-stone-800 dark:text-stone-200">
<span className="w-5 h-5 rounded-full bg-amber-200 dark:bg-indigo-800 text-amber-900 dark:text-amber-200 font-bold text-xs flex items-center justify-center shrink-0 mt-0.5">
{i + 1}
</span>
<p className="font-reading leading-relaxed">
{action}
</p>
</div>
))}
</div>
</div>
{/* Director's & Illustrator's Intent */}
<div className="p-4 rounded-2xl bg-gradient-to-r from-amber-500/10 via-rose-500/10 to-indigo-500/10 border border-amber-300/60 dark:border-indigo-700 space-y-1.5">
<div className="flex items-center gap-2 text-xs font-heading font-bold uppercase tracking-wider text-amber-900 dark:text-amber-300">
<Eye className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<span>Director's Core Intention & Emotional Beat</span>
</div>
<p className="text-xs sm:text-sm font-reading text-stone-800 dark:text-stone-200 leading-relaxed italic">
"{currentScene.directorNotes}"
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};