feat: move language card into top row

This commit is contained in:
Song367 2026-03-21 14:06:30 +08:00
parent 7ac7c4b216
commit ea353c963c
2 changed files with 90 additions and 74 deletions

View File

@ -67,6 +67,15 @@ describe('UploadScreen', () => {
expect(screen.getByRole('heading', { name: 'Language & Dubbing' })).toBeInTheDocument();
});
it('renders upload, mode, and language cards as first-row regions', () => {
renderUploadScreen();
expect(screen.getByTestId('upload-dropzone-card')).toBeInTheDocument();
expect(screen.getByTestId('mode-card')).toBeInTheDocument();
expect(screen.getByTestId('language-card')).toBeInTheDocument();
expect(screen.getByTestId('subtitle-defaults-card')).toBeInTheDocument();
});
it('shows all supported tts languages in a compact always-visible grid', () => {
renderUploadScreen();
@ -84,6 +93,13 @@ describe('UploadScreen', () => {
expect(screen.queryByText('DEF')).not.toBeInTheDocument();
});
it('keeps the language card controls intact after moving into the first row', () => {
renderUploadScreen();
expect(screen.getByTestId('language-card')).toContainElement(screen.getByTestId('tts-language-grid'));
expect(screen.getByRole('button', { name: 'Generate Translated Video' })).toBeInTheDocument();
});
it('shows a fixed English subtitle language and the supported TTS languages', () => {
renderUploadScreen();

View File

@ -34,7 +34,6 @@ export default function UploadScreen({
const [subtitleDefaults, setSubtitleDefaults] = useState<SubtitleDefaults>(DEFAULT_SUBTITLE_DEFAULTS);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const activeModeLabel = mode === 'editing' ? m.upload.editingMode : m.upload.simpleMode;
const activeModeDescription = mode === 'editing' ? m.upload.editingModeDesc : m.upload.simpleModeDesc;
const settingsCardClass =
'app-surface rounded-[24px] border border-white/70 px-4 py-4 shadow-[0_18px_60px_-34px_rgba(15,23,42,0.3)] sm:px-5';
@ -61,13 +60,13 @@ export default function UploadScreen({
return (
<div
data-testid="upload-workbench"
className="grid items-start gap-4 lg:gap-5 xl:grid-cols-[minmax(0,1.45fr)_minmax(300px,380px)]"
className="grid items-start gap-4 lg:gap-5 xl:grid-cols-[minmax(0,1.55fr)_minmax(240px,0.72fr)_minmax(300px,0.95fr)]"
>
<section
data-testid="upload-dropzone-card"
className="app-surface min-w-0 rounded-[28px] border border-white/70 px-4 py-4 shadow-[0_24px_80px_-40px_rgba(15,23,42,0.35)] sm:px-5 sm:py-5 lg:px-6 lg:py-5"
className="app-surface min-w-0 rounded-[28px] border border-white/70 px-4 py-4 shadow-[0_24px_80px_-40px_rgba(15,23,42,0.35)] sm:px-5 sm:py-5 lg:px-6 lg:py-5 xl:row-span-2"
>
<div className="mb-4 flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="mb-4 flex flex-col gap-3">
<div className="min-w-0 space-y-2">
<h2 className="text-xl font-semibold tracking-tight text-slate-950 sm:text-2xl">
{m.upload.uploadPanelTitle}
@ -76,14 +75,6 @@ export default function UploadScreen({
{m.upload.uploadPanelDescription}
</p>
</div>
<div className="rounded-2xl border border-sky-100 bg-sky-50/80 px-3.5 py-2.5 text-left shadow-sm shadow-sky-100/50">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-sky-600">
{m.upload.modeWorkflowTitle}
</p>
<p className="mt-2 text-sm font-semibold text-slate-900">{activeModeLabel}</p>
<p className="mt-1 max-w-xs text-xs leading-5 text-slate-600">{activeModeDescription}</p>
</div>
</div>
<div className="relative overflow-hidden rounded-[24px] border border-slate-200/80 bg-[linear-gradient(180deg,rgba(248,250,252,0.95)_0%,rgba(226,232,240,0.88)_100%)] p-2.5 sm:p-3">
@ -122,17 +113,20 @@ export default function UploadScreen({
</div>
</section>
<aside
<div
data-testid="upload-settings-column"
className="min-w-0 space-y-4"
className="min-w-0 grid gap-4 xl:contents"
>
<section className={settingsCardClass}>
<section
data-testid="mode-card"
className={`${settingsCardClass} xl:col-start-2 xl:row-start-1`}
>
<div className="mb-3">
<h2 className="text-base font-semibold text-slate-950">{m.upload.modeWorkflowTitle}</h2>
<p className="mt-1 text-sm leading-5 text-slate-600">{m.upload.modeWorkflowDescription}</p>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div className="grid gap-2">
<button
type="button"
aria-pressed={mode === 'editing'}
@ -177,7 +171,69 @@ export default function UploadScreen({
</div>
</section>
<section className={settingsCardClass}>
<section
data-testid="language-card"
className={`${settingsCardClass} flex flex-col xl:col-start-3 xl:row-start-1`}
>
<div className="mb-3">
<h2 className="text-base font-semibold text-slate-950">{m.upload.languageDubbingTitle}</h2>
<p className="mt-1 text-sm leading-5 text-slate-600">{m.upload.languageDubbingDescription}</p>
</div>
<div className="mb-3 rounded-2xl border border-slate-200 bg-white/80 p-3">
<h3 className="font-semibold text-slate-900">{m.upload.subtitleLanguage}</h3>
<p className="mt-1 text-xs text-slate-500">{m.upload.subtitleLanguageHint}</p>
<div className="mt-3 inline-flex items-center rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-medium text-slate-700">
{m.upload.languages.en}
</div>
</div>
<div className="mb-3">
<h3 className="font-semibold text-slate-900">{m.upload.ttsLanguage}</h3>
<p className="mt-1 text-xs text-slate-500">{m.upload.ttsLanguageHint}</p>
</div>
<div
data-testid="tts-language-grid"
className="grid grid-cols-2 gap-2"
>
{LANGUAGES.map((lang) => (
<button
key={lang.code}
type="button"
className={`rounded-2xl border px-3 py-2 text-left text-sm font-medium transition-colors ${
selectedTtsLanguage === lang.code
? 'border-emerald-500 bg-emerald-50 text-emerald-700'
: lang.code === 'zh' || lang.code === 'yue'
? 'border-orange-100 bg-orange-50 text-orange-600 hover:border-orange-200'
: 'border-slate-200 bg-white/80 text-slate-700 hover:border-sky-200 hover:text-sky-700'
}`}
onClick={() => setSelectedTtsLanguage(lang.code)}
>
{m.upload.languages[lang.code as keyof typeof m.upload.languages]}
</button>
))}
</div>
<button
className={`mt-4 w-full rounded-2xl px-4 py-3 text-sm font-semibold transition-colors ${
tempFile
? 'bg-[#16a34a] text-white shadow-lg shadow-emerald-200 hover:bg-[#15803d]'
: 'cursor-not-allowed bg-slate-200 text-slate-400'
}`}
onClick={() => {
if (tempFile) setShowTrimModal(true);
}}
disabled={!tempFile}
>
{m.upload.generateTranslatedVideo}
</button>
</section>
<section
data-testid="subtitle-defaults-card"
className={`${settingsCardClass} xl:col-start-2 xl:col-span-2 xl:row-start-2`}
>
<div className="mb-3 flex items-start justify-between gap-4">
<div>
<h2 className="text-base font-semibold text-slate-950">{m.upload.subtitleDefaults}</h2>
@ -265,63 +321,7 @@ export default function UploadScreen({
</div>
</div>
</section>
<section className={`${settingsCardClass} flex flex-col`}>
<div className="mb-3">
<h2 className="text-base font-semibold text-slate-950">{m.upload.languageDubbingTitle}</h2>
<p className="mt-1 text-sm leading-5 text-slate-600">{m.upload.languageDubbingDescription}</p>
</div>
<div className="mb-3 rounded-2xl border border-slate-200 bg-white/80 p-3">
<h3 className="font-semibold text-slate-900">{m.upload.subtitleLanguage}</h3>
<p className="mt-1 text-xs text-slate-500">{m.upload.subtitleLanguageHint}</p>
<div className="mt-3 inline-flex items-center rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm font-medium text-slate-700">
{m.upload.languages.en}
</div>
</div>
<div className="mb-3">
<h3 className="font-semibold text-slate-900">{m.upload.ttsLanguage}</h3>
<p className="mt-1 text-xs text-slate-500">{m.upload.ttsLanguageHint}</p>
</div>
<div
data-testid="tts-language-grid"
className="grid grid-cols-2 gap-2"
>
{LANGUAGES.map((lang) => (
<button
key={lang.code}
type="button"
className={`rounded-2xl border px-3 py-2 text-left text-sm font-medium transition-colors ${
selectedTtsLanguage === lang.code
? 'border-emerald-500 bg-emerald-50 text-emerald-700'
: lang.code === 'zh' || lang.code === 'yue'
? 'border-orange-100 bg-orange-50 text-orange-600 hover:border-orange-200'
: 'border-slate-200 bg-white/80 text-slate-700 hover:border-sky-200 hover:text-sky-700'
}`}
onClick={() => setSelectedTtsLanguage(lang.code)}
>
{m.upload.languages[lang.code as keyof typeof m.upload.languages]}
</button>
))}
</div>
<button
className={`mt-4 w-full rounded-2xl px-4 py-3 text-sm font-semibold transition-colors ${
tempFile
? 'bg-[#16a34a] text-white shadow-lg shadow-emerald-200 hover:bg-[#15803d]'
: 'cursor-not-allowed bg-slate-200 text-slate-400'
}`}
onClick={() => {
if (tempFile) setShowTrimModal(true);
}}
disabled={!tempFile}
>
{m.upload.generateTranslatedVideo}
</button>
</section>
</aside>
</div>
{showTrimModal && tempFile && (
<TrimModal