feat: add collapsible sections to StepGenerate component with reusable CollapsibleSection

This commit is contained in:
Ender 2025-10-24 20:51:11 +02:00
parent 4afb21c38b
commit b3418e3c96
3 changed files with 65 additions and 21 deletions

View File

@ -0,0 +1,44 @@
import { Box, Collapse, IconButton, Stack, Typography, type SxProps, type Theme } from '@mui/material';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { useState, type PropsWithChildren } from 'react';
export default function CollapsibleSection({ title, defaultCollapsed = true, right, sx, children }: PropsWithChildren<{
title: string;
defaultCollapsed?: boolean;
right?: React.ReactNode;
sx?: SxProps<Theme>;
}>) {
const [open, setOpen] = useState(!defaultCollapsed);
return (
<Box sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1, overflow: 'hidden', ...sx }}>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
sx={{ px: 1.25, py: 0.75, cursor: 'pointer', bgcolor: 'background.paper' }}
onClick={() => setOpen(v => !v)}
>
<Stack direction="row" alignItems="center" spacing={1}>
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
setOpen(v => !v);
}}
sx={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 150ms ease' }}
>
<ExpandMore fontSize="small" />
</IconButton>
<Typography variant="subtitle2">{title}</Typography>
</Stack>
{right}
</Stack>
<Collapse in={open} timeout="auto" unmountOnExit={false}>
<Box sx={{ p: 1.25 }}>
{children}
</Box>
</Collapse>
</Box>
);
}

View File

@ -8,19 +8,16 @@ export default function SelectedImages({
onRemove: (key: string) => void; onRemove: (key: string) => void;
}) { }) {
return ( return (
<Box> <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 1 }}>
<Typography variant="subtitle2" sx={{ mb: 1 }}>Selected Images</Typography> {imageKeys.map((k) => (
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: 1 }}> <Box key={k} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, textAlign: 'center', bgcolor: '#fafafa' }}>
{imageKeys.map((k) => ( <img src={`/api/media/obj?key=${encodeURIComponent(k)}`} alt={k.split('/').slice(-1)[0]} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'contain' }} />
<Box key={k} sx={{ p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1, textAlign: 'center', bgcolor: '#fafafa' }}> <Button size="small" color="error" variant="text" onClick={() => onRemove(k)} sx={{ mt: 0.5 }}>Remove</Button>
<img src={`/api/media/obj?key=${encodeURIComponent(k)}`} alt={k.split('/').slice(-1)[0]} style={{ maxWidth: '100%', maxHeight: 100, objectFit: 'contain' }} /> </Box>
<Button size="small" color="error" variant="text" onClick={() => onRemove(k)} sx={{ mt: 0.5 }}>Remove</Button> ))}
</Box> {imageKeys.length === 0 && (
))} <Typography variant="body2" sx={{ color: 'text.secondary' }}>(No images selected)</Typography>
{imageKeys.length === 0 && ( )}
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No images selected)</Typography>
)}
</Box>
</Box> </Box>
); );
} }

View File

@ -1,6 +1,7 @@
import { Box, Stack, TextField, Typography } from '@mui/material'; import { Box, Stack, TextField, Typography } from '@mui/material';
import MediaLibrary from '../MediaLibrary'; import MediaLibrary from '../MediaLibrary';
import SelectedImages from './SelectedImages'; import SelectedImages from './SelectedImages';
import CollapsibleSection from './CollapsibleSection';
import type { Clip } from './StepAssets'; import type { Clip } from './StepAssets';
export default function StepGenerate({ export default function StepGenerate({
@ -24,8 +25,7 @@ export default function StepGenerate({
</Typography> </Typography>
{/* Audio transcriptions in order */} {/* Audio transcriptions in order */}
<Box> <CollapsibleSection title="Audio Transcriptions">
<Typography variant="subtitle2" sx={{ mb: 1 }}>Audio Transcriptions</Typography>
<Stack spacing={1}> <Stack spacing={1}>
{[...postClips] {[...postClips]
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
@ -39,17 +39,20 @@ export default function StepGenerate({
<Typography variant="body2" sx={{ color: 'text.secondary' }}>(No audio clips)</Typography> <Typography variant="body2" sx={{ color: 'text.secondary' }}>(No audio clips)</Typography>
)} )}
</Stack> </Stack>
</Box> </CollapsibleSection>
{/* Selected images */} {/* Selected images */}
<SelectedImages imageKeys={genImageKeys} onRemove={onToggleGenImage} /> <CollapsibleSection title="Selected Images">
<SelectedImages imageKeys={genImageKeys} onRemove={onToggleGenImage} />
</CollapsibleSection>
{/* Media library */} {/* Media library */}
<MediaLibrary selectionMode selectedKeys={genImageKeys} onToggleSelect={onToggleGenImage} /> <CollapsibleSection title="Media Library">
<MediaLibrary selectionMode selectedKeys={genImageKeys} onToggleSelect={onToggleGenImage} />
</CollapsibleSection>
{/* Prompt */} {/* Prompt */}
<Box> <CollapsibleSection title="AI Prompt">
<Typography variant="subtitle2" sx={{ mb: 1 }}>AI Prompt</Typography>
<TextField <TextField
label="Instructions + context for AI generation" label="Instructions + context for AI generation"
value={promptText} value={promptText}
@ -58,7 +61,7 @@ export default function StepGenerate({
multiline multiline
minRows={4} minRows={4}
/> />
</Box> </CollapsibleSection>
</Box> </Box>
); );
} }