统一create输入

This commit is contained in:
北枳 2025-10-17 20:32:06 +08:00
parent 236051a91c
commit 1c2421e2b6
13 changed files with 823 additions and 1 deletions

10
app/create/page.tsx Normal file
View File

@ -0,0 +1,10 @@
import CreateVideo from '@/components/pages/create-video/CreateVideo';
import { DashboardLayout } from '@/components/layout/dashboard-layout';
export default function CreatePage() {
return (
<DashboardLayout>
<CreateVideo />
</DashboardLayout>
);
}

View File

@ -1,4 +1,4 @@
import { BookHeart, Gift } from "lucide-react"; import { BookHeart, Gift, Plus } from "lucide-react";
interface NavigationItem { interface NavigationItem {
name: string; name: string;
@ -17,6 +17,7 @@ export const navigationItems: Navigations[] = [
items: [ items: [
{ name: 'My Portfolio', href: '/movies', icon: BookHeart }, { name: 'My Portfolio', href: '/movies', icon: BookHeart },
{ name: 'Share', href: '/share', icon: Gift }, { name: 'Share', href: '/share', icon: Gift },
{ name: 'Create', href: '/create', icon: Plus },
], ],
} }
]; ];

View File

@ -0,0 +1,145 @@
"use client";
import {
GlobalOutlined,
ClockCircleOutlined,
DownOutlined,
MobileOutlined,
DesktopOutlined,
} from '@ant-design/icons';
import { WandSparkles, RectangleHorizontal, RectangleVertical } from 'lucide-react';
import { Dropdown, Menu, Tooltip } from 'antd';
import { LanguageOptions, VideoDurationOptions } from './config-options';
import type { ConfigOptions, LanguageValue, VideoDurationValue } from './config-options';
interface ConfigPanelProps {
/** Current configuration options */
configOptions: ConfigOptions;
/** Handler for configuration changes */
onConfigChange: <K extends keyof ConfigOptions>(key: K, value: ConfigOptions[K]) => void;
/** Whether it's mobile device */
isMobile?: boolean;
/** Whether it's desktop device */
isDesktop?: boolean;
}
/**
* Configuration panel component for video creation settings.
* Includes language, auto script, duration, and aspect ratio selectors.
* @param {ConfigOptions} configOptions - current configuration
* @param {Function} onConfigChange - handler for config changes
* @param {boolean} isMobile - whether it's mobile device
* @param {boolean} isDesktop - whether it's desktop device
* @returns {JSX.Element}
*/
export const ConfigPanel = ({
configOptions,
onConfigChange,
isMobile = false,
isDesktop = true,
}: ConfigPanelProps) => {
/** Language dropdown menu */
const languageMenu = (
<Menu
className="bg-[#1a1a1a] border border-white/10"
onClick={({ key }) => onConfigChange('language', key as LanguageValue)}
items={LanguageOptions.map((option) => ({
key: option.value,
label: <span className="text-gray-300">{option.label}</span>,
}))}
/>
);
/** Duration dropdown menu */
const durationMenu = (
<Menu
className="bg-[#1a1a1a] border border-white/10"
onClick={({ key }) => onConfigChange('videoDuration', key as VideoDurationValue)}
items={VideoDurationOptions.map((option) => ({
key: option.value,
label: <span className="text-gray-300">{option.label}</span>,
}))}
/>
);
const currentLanguage = LanguageOptions.find((option) => option.value === configOptions.language);
const currentDuration = configOptions.videoDuration === 'unlimited' ? 'auto' : configOptions.videoDuration;
return (
<div data-alt="config-panel" className="flex items-center gap-2">
{/* Language selector */}
<Dropdown overlay={languageMenu} trigger={['click']}>
<button
data-alt="config-language"
className="h-8 px-2 rounded-full border border-white/20 bg-transparent text-gray-300 hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center gap-2 hover:text-cyan-400"
>
<GlobalOutlined className="text-base" />
<span className="text-sm">{currentLanguage?.code}</span>
<DownOutlined className="text-xs" />
</button>
</Dropdown>
{/* Auto script toggle */}
<Tooltip title="AI Story Copilot" placement="top">
<button
data-alt="config-expansion-mode"
className={`h-8 px-2 rounded-full border transition-all duration-200 flex items-center gap-2 text-sm ${
configOptions.videoDuration === '8s'
? 'opacity-40 cursor-not-allowed border-white/20 bg-transparent text-gray-400'
: configOptions.expansion_mode
? 'border-cyan-400 bg-cyan-400/10 text-cyan-400 hover:bg-cyan-400/20'
: 'border-white/20 bg-transparent text-gray-300 hover:bg-white/5 hover:border-cyan-400/60 hover:text-cyan-400'
}`}
disabled={configOptions.videoDuration === '8s'}
onClick={() => {
if (configOptions.videoDuration !== '8s') {
onConfigChange('expansion_mode', !configOptions.expansion_mode);
}
}}
>
<WandSparkles className="w-4 h-4" />
<span>AutoScript</span>
</button>
</Tooltip>
{/* Duration selector */}
<Dropdown overlay={durationMenu} trigger={['click']}>
<button
data-alt="config-video-duration"
className="h-8 px-2 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center gap-2 text-gray-300 hover:text-cyan-400"
>
<ClockCircleOutlined className="text-base" />
<span className="text-sm capitalize">{currentDuration}</span>
<DownOutlined className="text-xs" />
</button>
</Dropdown>
{/* Aspect ratio toggles */}
<div data-alt="aspect-ratio-controls" className="h-8 px-2 py-1 flex items-center gap-1 rounded-full border border-white/20 hover:border-cyan-400/60">
<button
data-alt="portrait-button"
className={`w-8 h-6 rounded-full transition-all duration-200 flex items-center justify-center ${
configOptions.aspect_ratio === 'VIDEO_ASPECT_RATIO_PORTRAIT'
? 'bg-white/10 text-cyan-400 shadow-sm'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
onClick={() => onConfigChange('aspect_ratio', 'VIDEO_ASPECT_RATIO_PORTRAIT')}
>
<RectangleVertical className="w-4 h-4" />
</button>
<button
data-alt="landscape-button"
className={`w-8 h-6 rounded-full transition-all duration-200 flex items-center justify-center ${
configOptions.aspect_ratio === 'VIDEO_ASPECT_RATIO_LANDSCAPE'
? 'bg-white/10 text-cyan-400 shadow-sm'
: 'bg-transparent text-gray-400 hover:text-gray-300'
}`}
onClick={() => onConfigChange('aspect_ratio', 'VIDEO_ASPECT_RATIO_LANDSCAPE')}
>
<RectangleHorizontal className="w-4 h-4" />
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,77 @@
# CreateInput Component
Video creation input form with configuration options.
## File Structure
```
CreateInput/
├── VideoCreationForm.tsx # Main form component
├── ConfigPanel.tsx # Configuration panel with all options
├── config-options.ts # Configuration options and types
├── index.ts # Module exports
└── README.md # This file
```
## Components
### VideoCreationForm
Main form component for video creation, includes:
- Photo upload (character, scene, prop)
- Text input area
- Configuration options
- Create button
### ConfigPanel
Configuration panel with unified circular button style:
- Language selector (14 languages)
- Auto Script toggle (AI Story Copilot)
- Duration selector (8s, 1min, 2min, auto)
- Aspect ratio selector (landscape/portrait)
All buttons follow the same design pattern:
- Circular buttons with `rounded-full`
- Border: `border-white/20`
- Hover: `hover:bg-white/5 hover:border-cyan-400/60`
- Active state: `border-cyan-400 bg-cyan-400/10`
- Cyan color theme for interactions
## Configuration Options
Defined in `config-options.ts`:
```typescript
interface ConfigOptions {
language: LanguageValue; // Default: 'english'
expansion_mode: boolean; // Default: false
videoDuration: VideoDurationValue; // Default: 'unlimited'
aspect_ratio: AspectRatioValue; // Default: 'VIDEO_ASPECT_RATIO_LANDSCAPE'
}
```
## Usage
```tsx
import { VideoCreationForm } from '@/components/pages/create-video/CreateInput';
export default function CreatePage() {
return <VideoCreationForm />;
}
```
## Design System
All configuration buttons follow a unified design:
- **Base style**: `rounded-full border border-white/20 bg-transparent`
- **Hover**: `hover:bg-white/5 hover:border-cyan-400/60 hover:text-cyan-400`
- **Active**: `border-cyan-400 bg-cyan-400/10 text-cyan-400`
- **Transition**: `transition-all duration-200`
## Notes
- Configuration options are synchronized with ChatInputBox component
- All text and labels are in English as per project guidelines
- Uses Tailwind CSS 3.x for all styling
- Uses Ant Design icons for consistency with existing UI
- Responsive design with mobile/desktop support

View File

@ -0,0 +1,193 @@
"use client";
import { useState, useRef } from 'react';
import { PhotoPreviewSection } from '../PhotoPreview';
import type { PhotoItem, PhotoType } from '../PhotoPreview/types';
import {
PlusOutlined,
UserOutlined,
CameraOutlined,
BulbOutlined,
ArrowRightOutlined,
} from '@ant-design/icons';
import { Dropdown, Menu } from 'antd';
import { ConfigPanel } from './ConfigPanel';
import { defaultConfig } from './config-options';
import type { ConfigOptions } from './config-options';
import { useDeviceType } from '@/hooks/useDeviceType';
export default function VideoCreationForm() {
const [photos, setPhotos] = useState<PhotoItem[]>([]);
const [inputText, setInputText] = useState('');
const [configOptions, setConfigOptions] = useState<ConfigOptions>(defaultConfig);
const { isMobile, isDesktop } = useDeviceType();
const characterInputRef = useRef<HTMLInputElement>(null);
const sceneInputRef = useRef<HTMLInputElement>(null);
const propInputRef = useRef<HTMLInputElement>(null);
/** Handle photo deletion */
const handleDeletePhoto = (index: number) => {
setPhotos(prevPhotos => prevPhotos.filter((_, i) => i !== index));
};
/** Handle file upload */
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>, type: PhotoType) => {
const files = event.target.files;
if (!files || files.length === 0) return;
const newPhotos: PhotoItem[] = Array.from(files).map((file, index) => ({
url: URL.createObjectURL(file),
type,
id: `${type}-${Date.now()}-${index}`,
}));
setPhotos(prevPhotos => [...prevPhotos, ...newPhotos]);
event.target.value = '';
};
/** Handle configuration change */
const handleConfigChange = <K extends keyof ConfigOptions>(
key: K,
value: ConfigOptions[K]
) => {
setConfigOptions(prev => ({ ...prev, [key]: value }));
};
/** Handle video creation */
const handleCreate = () => {
console.log({
text: inputText,
photos,
config: configOptions,
});
// TODO: Implement video creation logic
};
return (
<div data-alt="video-creation-form" className="w-full h-full flex flex-col">
{/* Main Content Area with Border */}
<div
data-alt="content-container"
className="flex-1 border border-white/10 rounded-3xl bg-gradient-to-br from-[#1a1a1a]/50 to-[#0a0a0a]/50 backdrop-blur-sm overflow-hidden flex flex-col"
>
{/* Photo Preview Section - Top */}
{photos.length > 0 && (
<div data-alt="photo-preview-wrapper" className="px-6 pt-6">
<PhotoPreviewSection
photos={photos}
onDelete={handleDeletePhoto}
/>
</div>
)}
{/* Text Input Area - Middle */}
<div data-alt="text-input-wrapper" className="flex-1 px-4 py-4">
<textarea
data-alt="main-text-input"
className="w-full h-full bg-transparent text-gray-300 text-base placeholder-gray-500 resize-none outline-none border-none"
placeholder="Share a topic, idea, or instructions with Video Agent to produce a full avatar video"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
/>
</div>
{/* Control Panel - Bottom */}
<div data-alt="control-panel" className="px-4 py-4 flex items-center justify-between gap-2">
{/* Left Side - Upload and Options */}
<div data-alt="left-controls" className="flex items-center gap-2">
{/* Upload Button with Dropdown */}
<Dropdown
overlay={
<Menu className="bg-[#1a1a1a] border border-white/10">
<Menu.Item
key="character"
icon={<UserOutlined className="text-cyan-400" />}
onClick={() => characterInputRef.current?.click()}
>
<span className="text-gray-300">Character</span>
</Menu.Item>
<Menu.Item
key="scene"
icon={<CameraOutlined className="text-cyan-400" />}
onClick={() => sceneInputRef.current?.click()}
>
<span className="text-gray-300">Scene</span>
</Menu.Item>
<Menu.Item
key="prop"
icon={<BulbOutlined className="text-cyan-400" />}
onClick={() => propInputRef.current?.click()}
>
<span className="text-gray-300">Prop</span>
</Menu.Item>
</Menu>
}
trigger={['click']}
>
<button
data-alt="upload-button"
className="w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400"
>
<PlusOutlined className="text-base font-bold" />
</button>
</Dropdown>
{/* Hidden file inputs */}
<input
ref={characterInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => handleFileUpload(e, 'character')}
/>
<input
ref={sceneInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => handleFileUpload(e, 'scene')}
/>
<input
ref={propInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => handleFileUpload(e, 'prop')}
/>
{/* Mention Button */}
<button
data-alt="mention-button"
className="w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400"
>
<span className="text-base font-bold">@</span>
</button>
{/* Configuration Panel */}
<ConfigPanel
configOptions={configOptions}
onConfigChange={handleConfigChange}
isMobile={isMobile}
isDesktop={isDesktop}
/>
</div>
{/* Right Side - Create Button */}
<button
data-alt="create-button"
className="w-8 h-8 rounded-full bg-black hover:bg-gray-900 border border-white/20 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-white shadow-lg hover:shadow-cyan-400/20"
onClick={handleCreate}
>
<ArrowRightOutlined className="text-base font-bold" />
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,51 @@
/** Language configuration options */
export const LanguageOptions = [
{ value: "english", label: "English", isVip: false, code: 'EN' },
{ value: "chinese", label: "Chinese", isVip: false, code: 'ZH' },
{ value: "japanese", label: "Japanese", isVip: false, code: 'JA' },
{ value: "spanish", label: "Spanish", isVip: false, code: 'ES' },
{ value: "portuguese", label: "Portuguese", isVip: false, code: 'PT' },
{ value: "hindi", label: "Hindi", isVip: false, code: 'HI' },
{ value: "korean", label: "Korean", isVip: false, code: 'KO' },
{ value: "arabic", label: "Arabic", isVip: false, code: 'AR' },
{ value: "russian", label: "Russian", isVip: false, code: 'RU' },
{ value: "thai", label: "Thai", isVip: false, code: 'TH' },
{ value: "french", label: "French", isVip: false, code: 'FR' },
{ value: "german", label: "German", isVip: false, code: 'DE' },
{ value: "vietnamese", label: "Vietnamese", isVip: false, code: 'VI' },
{ value: "indonesian", label: "Indonesian", isVip: false, code: 'ID' }
];
/** Video duration options */
export const VideoDurationOptions = [
{ value: "8s", label: "8s" },
{ value: "1min", label: "1min" },
{ value: "2min", label: "2min" },
{ value: "unlimited", label: "auto" },
];
/** Language type */
export type LanguageValue = typeof LanguageOptions[number]['value'];
/** Video duration type */
export type VideoDurationValue = typeof VideoDurationOptions[number]['value'];
/** Aspect ratio type */
export type AspectRatioValue = 'VIDEO_ASPECT_RATIO_LANDSCAPE' | 'VIDEO_ASPECT_RATIO_PORTRAIT';
/** Configuration options interface */
export interface ConfigOptions {
language: LanguageValue;
expansion_mode: boolean;
videoDuration: VideoDurationValue;
aspect_ratio: AspectRatioValue;
}
/** Default configuration */
export const defaultConfig: ConfigOptions = {
language: 'english',
expansion_mode: false,
videoDuration: 'unlimited',
aspect_ratio: 'VIDEO_ASPECT_RATIO_LANDSCAPE',
};

View File

@ -0,0 +1,4 @@
export { default as VideoCreationForm } from './VideoCreationForm';
export { ConfigPanel } from './ConfigPanel';
export * from './config-options';

View File

@ -0,0 +1,17 @@
"use client";
import { VideoCreationForm } from './CreateInput';
export default function CreateVideo() {
return (
<div data-alt="create-video-page" className="w-full">
<div className='p-1 text-center text-2xl font-bold tracking-tight sm:text-3xl'>Transform any idea into a compelling video</div>
<div className='mx-auto w-full max-w-[600px] px-3 py-2 text-center text-xs text-gray-400 sm:px-16 sm:text-sm'>Generate professional videos from simple prompts. Browse community creations for inspiration, or start fresh with your own vision</div>
<div className='py-2'>
<div className='space-y-4 mx-auto w-full max-w-[900px] px-3 sm:px-16'>
<VideoCreationForm />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,147 @@
"use client";
import { useState } from 'react';
import { Image } from 'antd';
import { CloseOutlined, UserOutlined, CameraOutlined, BulbOutlined } from '@ant-design/icons';
import type { PhotoPreviewSectionProps, PhotoType } from './types';
import './styles.css';
/**
* Photo Preview Section Component
* Displays a horizontal list of photos with type indicators and delete functionality
* @param photos - Array of photos with type information
* @param onDelete - Callback when a photo is deleted
* @param className - Additional CSS classes
*/
export default function PhotoPreviewSection({
photos = [],
onDelete,
className = '',
}: PhotoPreviewSectionProps) {
const [previewVisible, setPreviewVisible] = useState(false);
const [previewImage, setPreviewImage] = useState('');
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
/**
* Get icon for photo type
*/
const getTypeIcon = (type: PhotoType) => {
switch (type) {
case 'character':
return <UserOutlined className="text-sm" />;
case 'scene':
return <CameraOutlined className="text-sm" />;
case 'prop':
return <BulbOutlined className="text-sm" />;
default:
return null;
}
};
/**
* Get label for photo type
*/
const getTypeLabel = (type: PhotoType) => {
switch (type) {
case 'character':
return 'Character';
case 'scene':
return 'Scene';
case 'prop':
return 'Prop';
default:
return '';
}
};
/**
* Handle photo preview
*/
const handlePreview = (url: string) => {
setPreviewImage(url);
setPreviewVisible(true);
};
/**
* Handle photo deletion
*/
const handleDelete = (index: number) => {
onDelete?.(index);
};
/** Don't render if no photos */
if (photos.length === 0) {
return null;
}
return (
<div data-alt="photo-preview-section" className={`w-full ${className}`}>
{/* Photo List Container with Horizontal Scroll */}
<div
data-alt="photo-list-container"
className="flex gap-2 overflow-x-auto overflow-y-hidden photo-list-scrollbar pb-1"
>
{photos.map((photo, index) => (
<div
key={photo.id || `photo-${index}`}
data-alt="photo-item"
className="relative flex-shrink-0 w-16 h-16 rounded-md overflow-visible border border-white/10 hover:border-cyan-400/60 transition-all duration-200 cursor-pointer group bg-black/20"
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
>
{/* Photo Image */}
<div
className="w-full h-full rounded-md overflow-hidden"
onClick={() => handlePreview(photo.url)}
>
<img
src={photo.url}
alt={`${getTypeLabel(photo.type)} ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
{/* Type Icon - Bottom Left Corner */}
<div
data-alt="type-icon"
className="absolute bottom-1 left-1 w-5 h-5 bg-black/60 backdrop-blur-sm text-cyan-400 rounded-sm flex items-center justify-center z-[5]"
>
{getTypeIcon(photo.type)}
</div>
{/* Delete Button - Top Right Corner */}
{hoveredIndex === index && (
<button
data-alt="delete-button"
className="absolute top-1 right-1 p-1 bg-[#636364] opacity-80 hover:opacity-100 text-white rounded-full flex items-center justify-center transition-all duration-200 shadow-lg hover:scale-110 z-10"
onClick={(e) => {
e.stopPropagation();
handleDelete(index);
}}
>
<CloseOutlined className="text-[10px]" />
</button>
)}
</div>
))}
</div>
{/* Preview Modal - Using absolute positioning to avoid layout space */}
<div style={{ position: 'absolute', width: 0, height: 0, overflow: 'hidden', opacity: 0, pointerEvents: 'none' }}>
<Image
width={0}
height={0}
src={previewImage}
preview={{
visible: previewVisible,
src: previewImage,
onVisibleChange: (visible) => {
setPreviewVisible(visible);
},
}}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,113 @@
# PhotoPreviewSection Component
Compact photo preview component with type indicators for characters, scenes, and props.
## Features
**Type Indicators** - Icons in bottom-left corner show photo type
**Compact Design** - 64x64px photos with horizontal scroll
**Smart Visibility** - Auto-hides when no photos
**Preview Support** - Click to view full-size image
**Delete Functionality** - X button appears on hover
**Dark Theme** - Fully styled for dark UI
## Usage
```tsx
import { PhotoPreviewSection } from './PhotoPreview';
import type { PhotoItem } from './PhotoPreview';
function MyComponent() {
const [photos, setPhotos] = useState<PhotoItem[]>([
{ url: 'photo1.jpg', type: 'character', id: '1' },
{ url: 'photo2.jpg', type: 'scene', id: '2' },
{ url: 'photo3.jpg', type: 'prop', id: '3' },
]);
const handleDelete = (index: number) => {
setPhotos(prev => prev.filter((_, i) => i !== index));
};
return (
<PhotoPreviewSection
photos={photos}
onDelete={handleDelete}
/>
);
}
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `photos` | `PhotoItem[]` | `[]` | Array of photos with type information |
| `onDelete` | `(index: number) => void` | optional | Callback when a photo is deleted |
| `className` | `string` | `''` | Additional CSS classes |
## Photo Types
### PhotoItem Interface
```typescript
interface PhotoItem {
url: string; // Photo URL
type: PhotoType; // 'character' | 'scene' | 'prop'
id?: string; // Optional unique identifier
}
```
### Type Icons
- **Character** (👤): `UserOutlined` - Cyan icon in bottom-left
- **Scene** (📷): `CameraOutlined` - Cyan icon in bottom-left
- **Prop** (💡): `BulbOutlined` - Cyan icon in bottom-left
## Design Specifications
- **Photo Size**: 64x64px (w-16 h-16)
- **Gap**: 8px between items
- **Border Radius**: 6px (rounded-md)
- **Type Icon**: 20x20px square, bottom-left corner with backdrop blur
- **Delete Button**: Circular gray button, top-right corner (hover only)
- **Scrollbar**: 4px height, appears on hover
## Interactions
- **Click Photo**: Opens full-screen preview modal
- **Hover Photo**: Shows delete button at top-right corner
- **Click Delete**: Triggers `onDelete` callback with photo index
- **Type Icon**: Always visible in bottom-left corner
## Visual Layout
```
┌───────────────────────────────────┐
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ │ │ │ │ │ │
│ │ [img]│ │ [img]│ │ [img]│ │
│ │ 👤 │ │ 📷 │ │ 💡 │ │
│ └──────┘ └──────┘ └──────┘ │
│ Char Scene Prop │
└───────────────────────────────────┘
```
- Top-right: Delete button (X) - appears on hover
- Bottom-left: Type icon - always visible
## Styling
The component uses Tailwind CSS with custom dark theme colors:
- **Primary Color**: `cyan-400` for type icons
- **Text Colors**: White with varying opacity
- **Borders**: White with 10% opacity, hover to cyan-400 with 60% opacity
- **Backgrounds**: Black/White with low opacity + backdrop blur
- **Delete Button**: Gray (`#636364`) with 80% opacity
## Notes
- Component returns `null` when `photos` array is empty
- Upload functionality should be implemented externally
- Each photo should have a unique `id` for optimal React key handling
- Preview uses antd's Image component for full-screen viewing

View File

@ -0,0 +1,6 @@
/** Export PhotoPreviewSection component */
export { default as PhotoPreviewSection } from './PhotoPreviewSection';
/** Export types */
export type { PhotoPreviewSectionProps, PhotoItem, PhotoType } from './types';

View File

@ -0,0 +1,33 @@
/** Custom styles for Photo Preview Section */
/** Override antd Upload component styles for compact mode */
.photo-upload-button-compact .ant-upload.ant-upload-select {
width: 4rem !important;
height: 4rem !important;
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
border: none !important;
}
.photo-upload-button-compact .ant-upload-wrapper {
display: inline-block;
}
/** Custom scrollbar for photo list */
.photo-list-scrollbar::-webkit-scrollbar {
height: 4px;
}
.photo-list-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.photo-list-scrollbar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.photo-list-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}

View File

@ -0,0 +1,25 @@
import type { ReactNode } from 'react';
/** Photo type enum */
export type PhotoType = 'character' | 'scene' | 'prop';
/** Photo item with type information */
export interface PhotoItem {
/** Photo URL */
url: string;
/** Photo type: character, scene, or prop */
type: PhotoType;
/** Optional unique identifier */
id?: string;
}
/** Props for PhotoPreviewSection component */
export interface PhotoPreviewSectionProps {
/** List of photos with type information */
photos: PhotoItem[];
/** Callback when a photo is deleted */
onDelete?: (index: number) => void;
/** Custom class name */
className?: string;
}