feat(ui): rewrite dub studio on clean shadcn, delete dub CSS (fast mode) (#818)

FAST-mode shadcn migration of the Dub Studio feature area. The dub
components now style with Tailwind utilities on the OmniVoice palette
tokens (bg/text/border via chrome-* + space/text vars) plus the src/ui
shadcn primitives (Button/Badge/Progress/Segmented/Table), and the
800-line page stylesheet is gone.

What changed
- Deleted frontend/src/pages/DubTab.css (800 lines). The irreducible
  pieces that can't be utilities — keyframe motion (stepper spin,
  idle-drop pulse, skeleton shimmer), ::before stepper connectors, and a
  handful of rules that must override other *global* design-system
  classes (.studio-panel / .label-row / .override-toggle / .segment-del)
  — moved to a small co-located frontend/src/components/dub/dub.css.
- Converted the dub-* presentational classes to inline utilities across
  DubFooter (footer panel, export-track chips, compression warn),
  DubLeftColumn (generating overlay, cast strip, the whole translation
  settings bar + fields), DubRightColumn (output-options rows, transcript
  body, glossary chip, bulk-select row), IdleSkeleton (speakers input,
  ingest opt-in, landing advanced, ghost footer + buttons), and
  TranscribeOverlay (stats row).
- Rewrote FooterBtn off the global .btn-primary / .dub-footer-btn
  subsystem onto a Tailwind tone map (idle/danger/green/pink/amber/…),
  preserving the flat tinted-outline look.
- Removed the dub-* fragments from src/index.css (tabular-nums group,
  focus-visible group, and the dub-split-grid / dub-settings-bar /
  dub-footer-btns responsive media queries — now inline max-[…] utils).
  Kept .btn-primary base (still used by ErrorBoundary) and all shared
  design-system classes.

Left intact (reported): the segment-* subsystem (DubSegmentTable.jsx/css,
DubSegmentRow.jsx/css, segment-* in index.css). It's the lowest-risk
option for the core, most test-covered segment table; ModelsTable and
EngineCompatibilityMatrix were verified NOT to consume segment-* (they
use models-*/engine-matrix-*), so nothing else breaks.

Behavior preserved exactly — every onClick/state/prop/hook untouched.
Verified: vite build ✓, oxlint 0 ✓, oxfmt --check clean ✓,
vitest 641/641 ✓, bun install --frozen-lockfile ✓. Eyeballed the idle
dropzone and the loaded skeleton (stepper, settings bar, skeleton
segment table, footer) in Chromium — palette + layout coherent.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-07-01 01:24:30 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent bafe9677d9
commit b033b8e9da
10 changed files with 470 additions and 943 deletions
+31 -10
View File
@@ -2,6 +2,15 @@ import { Check, AlertCircle } from 'lucide-react';
import { Badge } from '../../ui';
import DubFailureNotice from './DubFailureNotice';
// Export-track toggle chips: flat pill outline, tinted by on/off/success state.
const TRACK_LABEL =
'inline-flex items-center gap-[4px] px-[8px] py-[2px] border border-transparent rounded-[var(--chrome-radius-pill)] cursor-pointer transition-colors';
const TRACK_ON =
'text-[var(--chrome-fg)] border-[var(--chrome-border-strong)] bg-[var(--chrome-hover-bg)]';
const TRACK_OFF = 'text-[var(--chrome-fg-dim)]';
const TRACK_ON_SUCCESS =
'text-[var(--chrome-severity-ok)] border-[color-mix(in_srgb,var(--chrome-severity-ok)_45%,transparent)] bg-[color-mix(in_srgb,var(--chrome-severity-ok)_10%,transparent)]';
export default function DubFooter({
t,
dubStep,
@@ -15,7 +24,7 @@ export default function DubFooter({
translateQuality,
}) {
return (
<div className="studio-panel dub-footer-panel">
<div className="px-[var(--space-3)] py-[4px] shrink-0 bg-[var(--chrome-bg)] border border-[var(--chrome-border)]">
{dubStep === 'done' && (
<div className="mb-[var(--space-2)]">
<Badge tone="success">
@@ -45,24 +54,33 @@ export default function DubFooter({
)}
{/* Output options + Timing moved to the top of the right (transcript) section. */}
{dubTracks.length > 0 && (
<div className="dub-tracks-row">
<span className="dub-tracks-row__title">{t('dub.export_tracks')}</span>
<label className={exportTracks['original'] !== false ? 'is-on' : 'is-off'}>
<div className="flex items-center gap-[var(--space-2)] mb-[2px] px-[var(--space-3)] py-[3px] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] font-[family-name:var(--font-sans)] bg-[var(--chrome-bg)] rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)] flex-wrap">
<span className="font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] tracking-[var(--chrome-label-track)] uppercase text-[var(--chrome-fg-muted)] font-semibold">
{t('dub.export_tracks')}
</span>
<label
className={`${TRACK_LABEL} ${exportTracks['original'] !== false ? TRACK_ON : TRACK_OFF}`}
>
<input
type="checkbox"
className="accent-[var(--chrome-accent)]"
checked={exportTracks['original'] !== false}
onChange={(e) => setExportTracks((prev) => ({ ...prev, original: e.target.checked }))}
/>
<span>{t('dub.original_track')}</span>
</label>
{dubTracks.map((t) => (
<label key={t} className={exportTracks[t] !== false ? 'is-on is-success' : 'is-off'}>
<label
key={t}
className={`${TRACK_LABEL} ${exportTracks[t] !== false ? TRACK_ON_SUCCESS : TRACK_OFF}`}
>
<input
type="checkbox"
className="accent-[var(--chrome-accent)]"
checked={exportTracks[t] !== false}
onChange={(e) => setExportTracks((prev) => ({ ...prev, [t]: e.target.checked }))}
/>
<span className="code">{t}</span>
<span className="uppercase tracking-[0.04em]">{t}</span>
</label>
))}
</div>
@@ -81,10 +99,13 @@ export default function DubFooter({
if (pctHot < 10) return null;
const worst = hot.reduce((a, b) => (a.rate_ratio > b.rate_ratio ? a : b));
return (
<div className="dub-compression-warn" role="status">
<span className="dub-compression-warn__icon"></span>
<span className="dub-compression-warn__body">
<strong>
<div
className="flex items-start gap-[8px] px-[10px] py-[6px] my-[4px] bg-[color-mix(in_srgb,#fabd2f_12%,transparent)] border border-[color-mix(in_srgb,#fabd2f_35%,transparent)] border-l-2 border-l-[#fabd2f] rounded-[var(--chrome-radius-pill)] text-[0.72rem] text-[var(--chrome-fg)] leading-[1.35]"
role="status"
>
<span className="text-[#fabd2f] text-[0.9rem] leading-none shrink-0"></span>
<span className="flex-1">
<strong className="text-[#fabd2f] font-semibold">
{hot.length} of {dubSegments.length}
</strong>{' '}
segments need {'>'}1.3× compression (worst:{' '}
+67 -40
View File
@@ -18,6 +18,21 @@ import { POPULAR_LANGS, PRESETS } from '../../utils/constants';
import { dialectOptionsFor, dialectLabel, dialectMatchesLang } from '../../api/dialects';
import toast from 'react-hot-toast';
// ── Translation-settings bar utility class clusters ──────────────────────
const SETTINGS_SUMMARY =
'flex items-center gap-[var(--space-2)] px-[var(--space-3)] py-[3px] mb-[3px] bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] font-[family-name:var(--font-sans)] text-[0.66rem] text-[var(--chrome-fg-muted)]';
const SUMMARY_TRIGGER =
'inline-flex items-center gap-[5px] flex-1 min-w-0 bg-transparent border-none text-fg-muted cursor-pointer py-[2px] px-0 [font:inherit] text-left';
const SETTINGS_BAR =
'flex flex-col gap-[3px] max-[900px]:gap-[6px] mb-[4px] px-[8px] py-[4px] bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)]';
const FIELD = 'flex flex-col gap-[1px] min-w-0';
const FIELD_RESP = 'max-[960px]:basis-full max-[960px]:min-w-0';
const FIELD_LABEL =
'label-row !text-[0.58rem] !text-fg-muted !m-0 whitespace-nowrap overflow-hidden text-ellipsis';
const FIELD_INPUT = 'input-base !w-full !text-[0.65rem] !px-[5px] !py-[3px]';
const ENGINE_CHIP =
'ml-[6px] px-[6px] py-[1px] text-[0.55rem] leading-[1.4] bg-[rgba(211,134,155,0.14)] border border-[rgba(211,134,155,0.35)] text-[#d3869b] rounded-[999px] whitespace-nowrap transition-colors';
export default function DubLeftColumn({
hasDubbedTrack,
t,
@@ -125,15 +140,15 @@ export default function DubLeftColumn({
disabled={dubStep === 'generating' || dubStep === 'stopping'}
overlayContent={
dubStep === 'generating' || dubStep === 'stopping' ? (
<div className="dub-gen-overlay">
<div className="dub-gen-overlay__head">
<div className="flex flex-col items-center gap-[6px] w-full p-[10px] backdrop-blur-[2px]">
<div className="flex items-center gap-[6px]">
{dubStep === 'stopping' ? (
<Loader className="spinner" size={14} color="#a89984" />
) : (
<Sparkles className="spinner" size={14} color="#d3869b" />
)}
<span
className={`dub-gen-overlay__title ${dubStep === 'stopping' ? 'is-stopping' : ''}`}
className={`font-semibold text-[0.75rem] [font-variant-numeric:tabular-nums] tracking-[0.01em] ${dubStep === 'stopping' ? 'text-fg-muted' : 'text-fg'}`}
>
{dubStep === 'stopping'
? t('dub.stopping')
@@ -142,7 +157,7 @@ export default function DubLeftColumn({
</div>
{dubStep === 'generating' && (
<>
<div className="dub-gen-overlay__stats">
<div className="flex gap-[var(--space-4)] text-[0.65rem] text-fg-muted [font-variant-numeric:tabular-nums]">
<span>
{fmtDur(genElapsed)} {t('dub.elapsed')}
</span>
@@ -152,7 +167,7 @@ export default function DubLeftColumn({
</span>
)}
</div>
<div className="dub-gen-overlay__bar">
<div className="w-[80%] max-w-[240px] my-[1px]">
<Progress
value={
dubProgress.total ? (dubProgress.current / dubProgress.total) * 100 : 0
@@ -162,7 +177,7 @@ export default function DubLeftColumn({
/>
</div>
{dubProgress.text && (
<span className="dub-gen-overlay__text">{dubProgress.text}</span>
<span className="text-[0.62rem] text-fg-muted">{dubProgress.text}</span>
)}
</>
)}
@@ -177,9 +192,12 @@ export default function DubLeftColumn({
dropdown. It's also pre-selected on the segments so "new
language = same speaker's voice" works by default. */}
{dubSegments.some((s) => s.speaker_id) && (
<div className="dub-cast">
<div className="dub-cast__row">
<span className="dub-cast__kicker" title={t('dub.cast_title')}>
<div className="mt-[2px] px-[var(--space-3)] py-[3px] bg-[var(--chrome-bg)] rounded-[var(--chrome-radius-pill)] border border-[var(--chrome-border)]">
<div className="flex gap-[var(--space-2)] items-center flex-wrap">
<span
className="font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] text-[var(--chrome-fg-muted)] tracking-[var(--chrome-label-track)] uppercase font-semibold"
title={t('dub.cast_title')}
>
{t('dub.cast')}
</span>
{[...new Set(dubSegments.map((s) => s.speaker_id).filter(Boolean))].map((spk) => {
@@ -187,7 +205,9 @@ export default function DubLeftColumn({
const clone = speakerClones[spk];
return (
<div key={spk} className="dub-cast__pair">
<span className="dub-cast__label">{spk}:</span>
<span className="font-[family-name:var(--chrome-font-mono)] text-[0.62rem] text-[var(--chrome-fg)]">
{spk}:
</span>
<select
className="input-base dub-cast__select"
value={dubSegments.find((s) => s.speaker_id === spk)?.profile_id || ''}
@@ -234,21 +254,22 @@ export default function DubLeftColumn({
{/* Translation settings — collapsed or expanded */}
{!settingsOpen && (
<div className="dub-settings-summary">
<div className={SETTINGS_SUMMARY}>
<button
type="button"
className="dub-settings-summary__trigger"
className={SUMMARY_TRIGGER}
onClick={() => setSettingsOpen(true)}
title={t('dub.edit_settings')}
>
<ChevronDown size={10} />
<span>
<strong>{dubLang}</strong> · {dubLangCode} · {translateQuality} ·{' '}
<strong className="text-[var(--chrome-fg)] font-semibold">{dubLang}</strong> ·{' '}
{dubLangCode} · {translateQuality} ·{' '}
<span style={{ color: activeEngineUnavailable ? '#fb4934' : '#b8bb26' }}></span>{' '}
{translateProvider}
</span>
{dubInstruct && (
<span className="dub-settings-summary__style">
<span className="text-[var(--chrome-fg-dim)] italic ml-[var(--space-2)]">
{t('dub.style_label_prefix')}
{dubInstruct}
</span>
@@ -281,22 +302,22 @@ export default function DubLeftColumn({
</div>
)}
{settingsOpen && (
<div className="dub-settings-bar">
<div className="dub-settings-bar__fields">
<div className={SETTINGS_BAR}>
<div className="flex flex-wrap gap-x-[6px] gap-y-[4px] items-end">
<button
type="button"
className="dub-settings-summary__trigger dub-settings-close"
className={`${SUMMARY_TRIGGER} flex-[0_0_auto] !px-[4px] self-center`}
onClick={() => setSettingsOpen(false)}
title={t('dub.collapse_settings')}
>
<ChevronUp size={10} />
</button>
<div className="dub-settings-field dub-settings-field--lang">
<div className="label-row">
<div className={`${FIELD} flex-[1_1_100px] min-w-[70px] ${FIELD_RESP}`}>
<div className={FIELD_LABEL}>
<Globe className="label-icon" size={9} /> {t('dub.language')}
</div>
<select
className="input-base dub-cast__select"
className={FIELD_INPUT}
value={dubLang}
onChange={(e) => {
const lang = e.target.value;
@@ -328,10 +349,10 @@ export default function DubLeftColumn({
</optgroup>
</select>
</div>
<div className="dub-settings-field dub-settings-field--iso">
<div className="label-row">{t('dub.iso_code')}</div>
<div className={`${FIELD} flex-[0_1_72px] min-w-[52px] ${FIELD_RESP}`}>
<div className={FIELD_LABEL}>{t('dub.iso_code')}</div>
<select
className="input-base dub-cast__select"
className={FIELD_INPUT}
value={dubLangCode}
onChange={(e) => {
const code = e.target.value;
@@ -350,12 +371,12 @@ export default function DubLeftColumn({
languages with curated variants; region names come from
Intl.DisplayNames so they localize with the UI for free. */}
{dialectOptionsFor(dubLangCode).length > 0 && (
<div className="dub-settings-field dub-settings-field--dialect">
<div className="label-row" title={t('dub.dialect_title')}>
<div className={`${FIELD} flex-[0_1_110px] min-w-[80px] ${FIELD_RESP}`}>
<div className={FIELD_LABEL} title={t('dub.dialect_title')}>
{t('dub.dialect_label')}
</div>
<select
className="input-base dub-cast__select"
className={FIELD_INPUT}
value={dialectMatchesLang(dubDialect, dubLangCode) ? dubDialect : ''}
onChange={(e) => setDubDialect(e.target.value)}
>
@@ -368,13 +389,13 @@ export default function DubLeftColumn({
</select>
</div>
)}
<div className="dub-settings-field dub-settings-field--engine">
<div className="label-row">
<div className={`${FIELD} flex-[1.4_1_130px] min-w-[90px] ${FIELD_RESP}`}>
<div className={FIELD_LABEL}>
{t('dub.engine_label')}
{activeEngineUnavailable && !enginesSandboxed && (
<button
type="button"
className="dub-engine-install-chip"
className={`${ENGINE_CHIP} cursor-pointer hover:bg-[rgba(211,134,155,0.22)] disabled:opacity-55 disabled:cursor-default disabled:italic`}
onClick={() => handleInstallEngine(translateProvider)}
disabled={engineInstalling === translateProvider}
title={t('dub.install_engine')}
@@ -386,7 +407,7 @@ export default function DubLeftColumn({
)}
{activeEngineUnavailable && enginesSandboxed && (
<span
className="dub-engine-install-chip dub-engine-install-chip--disabled"
className={`${ENGINE_CHIP} opacity-55 cursor-default italic`}
title={t('dub.install_disabled_title')}
>
{t('dub.needs_dev_install')}
@@ -394,7 +415,7 @@ export default function DubLeftColumn({
)}
</div>
<select
className="input-base dub-engine-select"
className={FIELD_INPUT}
value={translateProvider}
onChange={(e) => setTranslateProvider(e.target.value)}
>
@@ -407,11 +428,12 @@ export default function DubLeftColumn({
))}
</select>
</div>
<div className="dub-settings-field dub-settings-field--quality">
<div className="label-row" title={t('dub.quality_title')}>
<div className={`${FIELD} flex-[0_1_auto] min-w-[80px] ${FIELD_RESP}`}>
<div className={FIELD_LABEL} title={t('dub.quality_title')}>
{t('dub.quality_label')}
</div>
<Segmented
className="w-full"
size="sm"
value={translateQuality}
onChange={(v) => {
@@ -436,22 +458,27 @@ export default function DubLeftColumn({
]}
/>
</div>
<div className="dub-settings-field dub-settings-field--style">
<div className="label-row">
<div className={`${FIELD} flex-[1_1_90px] min-w-[64px] ${FIELD_RESP}`}>
<div className={FIELD_LABEL}>
<UserSquare2 className="label-icon" size={9} /> {t('dub.style')}{' '}
<span className="dub-settings-field__hint">{t('dub.optional')}</span>
<span className="text-[0.52rem] text-fg-subtle italic ml-[2px]">
{t('dub.optional')}
</span>
</div>
<input
className="input-base input-base--xs"
className={FIELD_INPUT}
placeholder={t('dub.style_placeholder')}
value={dubInstruct}
onChange={(e) => setDubInstruct(e.target.value)}
/>
</div>
<div className="dub-settings-field dub-settings-field--multi">
<label className="dub-multi-toggle">
<div
className={`${FIELD} basis-full pt-[3px] border-t border-[var(--chrome-border)] mt-[1px]`}
>
<label className="flex items-center gap-[6px] text-[0.65rem] text-[var(--chrome-fg-muted)] cursor-pointer mb-[2px]">
<input
type="checkbox"
className="accent-[var(--chrome-accent)] cursor-pointer"
checked={multiLangMode}
onChange={(e) => setMultiLangMode(e.target.checked)}
/>
@@ -466,7 +493,7 @@ export default function DubLeftColumn({
)}
</div>
</div>
<div className="dub-settings-bar__actions">
<div className="flex justify-end gap-[6px] flex-wrap">
<Button
variant="subtle"
size="sm"
+36 -24
View File
@@ -9,6 +9,16 @@ const DubSegmentTable = lazy(() => import('../DubSegmentTable'));
const LazyFallback = () => <div className="p-[12px] text-[#6b6657] text-[0.7rem]">Loading</div>;
// ── Output-options + bulk-select utility clusters ────────────────────────
const OUT_ROW =
'flex items-center gap-[var(--space-3)] mb-[2px] px-[var(--space-2)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] font-[family-name:var(--font-sans)] flex-wrap';
const OUT_LABEL =
'flex items-center gap-[var(--space-2)] cursor-pointer hover:text-[var(--chrome-fg)]';
const OUT_TITLE =
'font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] tracking-[var(--chrome-label-track)] uppercase text-[var(--chrome-fg-muted)] font-semibold';
const CHK = 'accent-[var(--chrome-accent)]';
const BULK_SELECT = 'input-base !text-[0.62rem] !px-[4px] !py-[2px]';
export default function DubRightColumn({
t,
preserveBg,
@@ -63,37 +73,40 @@ export default function DubRightColumn({
return (
<div className="studio-panel dub-panel-col">
{/* Output options + timing — moved to the top of the right section. */}
<div className="dub-right-outputs">
<div className="dub-outputs-row">
<span className="dub-outputs-title-strong">{t('dub.output_options')}</span>
<label>
<div>
<div className={OUT_ROW}>
<span className={OUT_TITLE}>{t('dub.output_options')}</span>
<label className={OUT_LABEL}>
<input
type="checkbox"
className={CHK}
checked={preserveBg}
onChange={(e) => setPreserveBg(e.target.checked)}
/>{' '}
{t('dub.mix_bg_audio')}
</label>
<label title={t('dub.dual_subs_title')}>
<label className={OUT_LABEL} title={t('dub.dual_subs_title')}>
<input
type="checkbox"
className={CHK}
checked={!!dualSubs}
onChange={(e) => setDualSubs(e.target.checked)}
/>{' '}
{t('dub.dual_subs')}
</label>
<label title={t('dub.burn_subs_title')}>
<label className={OUT_LABEL} title={t('dub.burn_subs_title')}>
<input
type="checkbox"
className={CHK}
checked={!!burnSubs}
onChange={(e) => setBurnSubs(e.target.checked)}
/>{' '}
{t('dub.burn_subs')}
</label>
<label>
<label className={OUT_LABEL}>
{t('dub.default_track')}
<select
className="input-base dub-outputs-default"
className="input-base !text-[0.6rem] !px-[4px] !py-[2px] !w-[120px]"
value={defaultTrack}
onChange={(e) => setDefaultTrack(e.target.value)}
>
@@ -112,10 +125,10 @@ export default function DubRightColumn({
</label>
</div>
<div
className="dub-outputs-row"
className={OUT_ROW}
title="Timing strategy — how the dub reconciles natural-rate TTS with the original timeline."
>
<span className="dub-outputs-title-strong">Timing:</span>
<span className={OUT_TITLE}>Timing:</span>
<Segmented
value={timingStrategy}
onChange={setTimingStrategy}
@@ -149,17 +162,21 @@ export default function DubRightColumn({
</div>
{dubTranscript && (
<div className="dub-transcript-toggle-wrap">
<div className="mb-[4px]">
<div
className="override-toggle dub-transcript-toggle__inner"
onClick={() => setShowTranscript(!showTranscript)}
>
<span>
<FileText size={10} className="dub-inline-icon" /> {t('dub.transcript')}
<FileText size={10} className="align-middle mr-[3px]" /> {t('dub.transcript')}
</span>
{showTranscript ? <ChevronUp size={10} /> : <ChevronDown size={10} />}
</div>
{showTranscript && <div className="dub-transcript-body">{dubTranscript}</div>}
{showTranscript && (
<div className="bg-[var(--chrome-bg)] border border-[var(--chrome-border)] border-t-0 rounded-b-[var(--chrome-radius-pill)] p-[var(--space-3)] text-[length:var(--text-xs)] text-[var(--chrome-fg-muted)] leading-[1.5] max-h-[80px] overflow-y-auto">
{dubTranscript}
</div>
)}
</div>
)}
@@ -168,7 +185,7 @@ export default function DubRightColumn({
{dubJobId && !glossaryVisible && (
<button
type="button"
className="dub-glossary-chip"
className="inline-flex items-center px-[var(--space-3)] py-[3px] mb-[4px] font-[family-name:var(--chrome-font-mono)] text-[length:var(--chrome-label-size)] tracking-[var(--chrome-label-track)] uppercase text-[var(--chrome-fg-muted)] bg-transparent border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] cursor-pointer transition-colors hover:bg-[var(--chrome-hover-bg)] hover:border-[var(--chrome-border-strong)] hover:text-[var(--chrome-fg)]"
onClick={() => {
setGlossaryOpen(true);
setGlossaryHidden(false);
@@ -179,7 +196,7 @@ export default function DubRightColumn({
</button>
)}
{dubJobId && glossaryVisible && (
<div className="dub-glossary-wrap">
<div className="mb-[4px]">
<GlossaryPanel
projectId={dubJobId}
sourceLang={dubLangCode && dubLang ? dubLang.slice(0, 2).toLowerCase() || 'en' : 'en'}
@@ -199,12 +216,12 @@ export default function DubRightColumn({
thing per-speaker (and handles the multi-speaker case cleanly). */}
{selectedSegIds.size > 0 && (
<div className="dub-bulk-row dub-bulk-row--select">
<div className="flex items-center gap-[var(--space-3)] px-[6px] py-[3px] rounded-[var(--radius-md)] mb-[var(--space-2)] text-[length:var(--text-xs)] bg-[rgba(211,134,155,0.08)] border border-[rgba(211,134,155,0.25)]">
<span className="text-brand font-bold whitespace-nowrap">
{t('dub.selected_count', { count: selectedSegIds.size })}
</span>
<select
className="input-base dub-bulk-select dub-bulk-select--voice"
className={`${BULK_SELECT} min-w-[72px] flex-[1_1_100px]`}
value=""
onChange={(e) => {
const v = e.target.value;
@@ -250,7 +267,7 @@ export default function DubRightColumn({
)}
</select>
<select
className="input-base dub-bulk-select dub-bulk-select--lang"
className={`${BULK_SELECT} !w-auto min-w-[64px] flex-[0_1_90px]`}
value=""
onChange={(e) => {
if (e.target.value === '__def__') bulkApplyToSelected({ target_lang: null });
@@ -268,12 +285,7 @@ export default function DubRightColumn({
<Button variant="danger" size="sm" onClick={bulkDeleteSelected}>
{t('dub.delete_selected')}
</Button>
<Button
variant="ghost"
size="sm"
onClick={clearSegSelection}
className="dub-bulk-row__clear"
>
<Button variant="ghost" size="sm" onClick={clearSegSelection} className="ml-auto">
{t('dub.clear_selection')}
</Button>
</div>
+31 -13
View File
@@ -1,23 +1,41 @@
import React from 'react';
/**
* FooterBtn — the gradient-per-tone download button family in the action footer.
* Uses the legacy .btn-primary as the shape/hover base, just picks a tone class.
* forwardRef so <Menu> can wire its triggerRef to the underlying button
* without this the Export menu can't compute coords and never opens.
* FooterBtn — the tinted-outline action button family used in the dub header /
* footer action strip. Flat chrome surfaces: color = accent (text + border +
* faint fill), not a gradient. Pure Tailwind utilities on the palette tokens
* no global .btn-primary / .dub-footer-btn CSS. forwardRef so <Menu> can wire
* its triggerRef to the underlying button (the Export menu needs it to open).
*/
const BASE =
'inline-flex items-center justify-center gap-[5px] flex-1 mt-0 ' +
'font-[family-name:var(--font-sans)] tracking-[0.02em] normal-case ' +
'bg-transparent border rounded-[var(--chrome-radius-pill)] shadow-none transition-colors ' +
'disabled:opacity-45 disabled:cursor-not-allowed';
const TONES = {
idle: 'text-[var(--chrome-fg-muted)] border-[var(--chrome-border)] hover:bg-[var(--chrome-hover-bg)]',
stopping:
'text-[var(--chrome-fg-muted)] border-[var(--chrome-border)] hover:bg-[var(--chrome-hover-bg)]',
danger:
'text-[var(--chrome-severity-err)] border-[color-mix(in_srgb,var(--chrome-severity-err)_45%,transparent)] bg-[color-mix(in_srgb,var(--chrome-severity-err)_10%,transparent)] hover:bg-[color-mix(in_srgb,var(--chrome-severity-err)_18%,transparent)]',
green:
'text-[var(--chrome-severity-ok)] border-[color-mix(in_srgb,var(--chrome-severity-ok)_45%,transparent)] bg-[color-mix(in_srgb,var(--chrome-severity-ok)_10%,transparent)] hover:bg-[color-mix(in_srgb,var(--chrome-severity-ok)_18%,transparent)]',
pink: 'text-[var(--chrome-accent)] border-[var(--chrome-accent-border)] bg-[var(--chrome-accent-bg)] hover:bg-[color-mix(in_srgb,var(--chrome-accent)_20%,transparent)]',
blue: 'text-[#83a598] border-[color-mix(in_srgb,#83a598_45%,transparent)] bg-[color-mix(in_srgb,#83a598_10%,transparent)]',
lime: 'text-[#b8bb26] border-[color-mix(in_srgb,#b8bb26_45%,transparent)] bg-[color-mix(in_srgb,#b8bb26_10%,transparent)]',
amber:
'text-[var(--chrome-severity-warn)] border-[color-mix(in_srgb,var(--chrome-severity-warn)_45%,transparent)] bg-[color-mix(in_srgb,var(--chrome-severity-warn)_10%,transparent)]',
orange:
'text-[#fe8019] border-[color-mix(in_srgb,#fe8019_45%,transparent)] bg-[color-mix(in_srgb,#fe8019_10%,transparent)]',
};
const FooterBtn = React.forwardRef(function FooterBtn(
{ tone = 'idle', sm = false, disabled, onClick, icon, label, ...rest },
{ tone = 'idle', sm = false, disabled, onClick, icon, label, className = '', ...rest },
ref,
) {
const cls = [
'btn-primary',
'dub-footer-btn',
sm && 'dub-footer-btn--sm',
`dub-footer-btn--${tone}`,
]
.filter(Boolean)
.join(' ');
const size = sm ? 'px-[6px] py-[3px] text-[0.62rem]' : 'px-[8px] py-[5px] text-[0.72rem]';
const cls = [BASE, size, TONES[tone] || TONES.idle, className].filter(Boolean).join(' ');
return (
<button ref={ref} className={cls} disabled={disabled} onClick={onClick} {...rest}>
{icon} {label}
+29 -17
View File
@@ -26,6 +26,9 @@ import PrepOverlay from './PrepOverlay';
import TranscribeOverlay from './TranscribeOverlay';
import { LANG_CODES } from '../../utils/languages';
const SPEAKERS_INPUT =
'w-[52px] ml-[4px] px-[6px] py-[4px] rounded-[6px] border border-[var(--border,#3c3836)] bg-[var(--input-bg,#282828)] text-inherit text-[12px]';
export default function IdleSkeleton({
t,
dubVideoFile,
@@ -154,7 +157,11 @@ export default function IdleSkeleton({
{/* SPLIT LAYOUT skeleton */}
<div
className={`dub-split-grid ${dubVideoFile ? 'grid grid-cols-2 gap-[6px] flex-1 min-h-0 overflow-hidden' : 'grid grid-cols-1 gap-[6px] flex-1 min-h-0'}`}
className={
dubVideoFile
? 'grid grid-cols-2 max-[1000px]:grid-cols-1 max-[1000px]:grid-rows-[auto_1fr] gap-[6px] flex-1 min-h-0 overflow-hidden'
: 'grid grid-cols-1 gap-[6px] flex-1 min-h-0'
}
>
{/* LEFT */}
<div className="studio-panel dub-panel-col">
@@ -219,7 +226,7 @@ export default function IdleSkeleton({
min={1}
max={20}
step={1}
className="dub-speakers-input"
className={SPEAKERS_INPUT}
placeholder={t('dub.num_speakers_auto')}
value={dubNumSpeakers ?? ''}
disabled={dubStep === 'uploading' || dubStep === 'transcribing'}
@@ -229,8 +236,9 @@ export default function IdleSkeleton({
}}
/>
</label>
<button
className="btn-primary dub-change-row__cta"
<Button
variant="primary"
className="flex-1"
onClick={handleDubUpload}
disabled={dubStep === 'uploading' || dubStep === 'transcribing'}
>
@@ -243,7 +251,7 @@ export default function IdleSkeleton({
<Sparkles size={14} /> {t('dub.upload_transcribe')}
</>
)}
</button>
</Button>
</div>
</>
) : dubStep === 'uploading' ? (
@@ -335,7 +343,7 @@ export default function IdleSkeleton({
</button>
</div>
<label
className="dub-ingest-sub-opt"
className="flex items-center gap-[6px] mt-[6px] px-[6px] py-[4px] text-[0.62rem] text-fg-muted cursor-pointer rounded-[4px] bg-[rgba(255,255,255,0.02)] hover:text-fg hover:bg-[rgba(255,255,255,0.05)]"
title={t('dub.pull_captions_title')}
onClick={(e) => {
e.stopPropagation();
@@ -343,6 +351,7 @@ export default function IdleSkeleton({
>
<input
type="checkbox"
className="m-0 accent-[#d3869b]"
checked={fetchYtSubs}
onChange={(e) => setFetchYtSubs(e.target.checked)}
onClick={(e) => e.stopPropagation()}
@@ -361,7 +370,7 @@ export default function IdleSkeleton({
{t('dub.target_language', { defaultValue: 'Dub into' })}
</span>
<select
className="input-base input-base--xs"
className="input-base text-[0.65rem]"
value={dubLangCode}
onChange={(e) => {
const lc = LANG_CODES.find((l) => l.code === e.target.value);
@@ -378,7 +387,7 @@ export default function IdleSkeleton({
</label>
<button
type="button"
className="dub-landing-opts__adv"
className="inline-flex items-center gap-[5px] px-[10px] py-[5px] text-[0.7rem] text-[var(--chrome-fg-muted)] bg-transparent border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill,999px)] cursor-pointer transition-colors hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)]"
onClick={() => setLandingAdvOpen((o) => !o)}
aria-expanded={landingAdvOpen}
>
@@ -398,7 +407,7 @@ export default function IdleSkeleton({
min={1}
max={20}
step={1}
className="input-base input-base--xs dub-speakers-input"
className={SPEAKERS_INPUT}
placeholder={t('dub.num_speakers_auto')}
value={dubNumSpeakers ?? ''}
onChange={(e) => {
@@ -411,7 +420,7 @@ export default function IdleSkeleton({
<UserSquare2 size={12} /> {t('dub.style')}
<input
type="text"
className="input-base input-base--xs"
className="input-base text-[0.65rem]"
placeholder={t('dub.style_placeholder')}
value={dubInstruct}
onChange={(e) => setDubInstruct(e.target.value)}
@@ -471,13 +480,13 @@ export default function IdleSkeleton({
<div className="label-row">
<Globe className="label-icon" size={9} /> {t('dub.language')}
</div>
<select className="input-base input-base--xs" disabled>
<select className="input-base text-[0.65rem]" disabled>
<option>{t('dub.auto')}</option>
</select>
</div>
<div className="flex-1 min-w-[80px]">
<div className="label-row">{t('dub.iso_code')}</div>
<select className="input-base input-base--xs" disabled>
<select className="input-base text-[0.65rem]" disabled>
<option>en {t('dub.original_audio')}</option>
</select>
</div>
@@ -486,7 +495,7 @@ export default function IdleSkeleton({
<UserSquare2 className="label-icon" size={9} /> {t('dub.style')}
</div>
<input
className="input-base input-base--xs"
className="input-base text-[0.65rem]"
disabled
placeholder={t('dub.style_placeholder')}
/>
@@ -544,12 +553,15 @@ export default function IdleSkeleton({
{/* Ghost footer — only once a file is in play; the bare landing stays
clean. Generate is the lone primary, exports demoted to one menu. */}
{dubVideoFile && (
<div className="studio-panel dub-ghost-footer">
<div className="studio-panel px-[8px] py-[4px] shrink-0">
<div className="flex gap-[4px]">
<button className="btn-primary dub-skel-gen-btn" disabled>
<Button variant="primary" className="flex-1 opacity-40" disabled>
<Play size={11} /> {t('dub.generate_dub')}
</button>
<button className="dub-skel-gen-btn dub-skel-gen-btn--secondary" disabled>
</Button>
<button
className="inline-flex items-center gap-[5px] bg-transparent border border-[var(--chrome-border)] text-[var(--chrome-fg-muted)] rounded-[8px] flex-[0_0_auto] px-[8px] py-[4px] text-[0.7rem] opacity-40"
disabled
>
<Download size={11} /> {t('dub.export_btn', { defaultValue: 'Export' })}{' '}
<ChevronDown size={10} />
</button>
@@ -16,7 +16,7 @@ function TranscribeOverlay({ elapsed, duration, onAbort }) {
<Loader className="spinner" size={18} color="#d3869b" />
<span className="text-fg font-medium text-[var(--text-lg)]">{t('dub.transcribing')}</span>
</div>
<div className="dub-trans-overlay__stats">
<div className="flex gap-[var(--space-6)] text-[length:var(--text-md)] text-fg-muted [font-variant-numeric:tabular-nums_slashed-zero]">
<span>
{mm}:{ss} {t('dub.elapsed')}
</span>
+269
View File
@@ -0,0 +1,269 @@
/* Dub Studio — irreducible CSS that can't live as Tailwind utilities:
keyframe-driven motion (stepper spin, idle-drop pulse, skeleton shimmer),
::before connectors, and a few rules that must override other *global*
design-system classes (.studio-panel / .label-row / .override-toggle /
.segment-del) where a utility would lose on cascade. Everything else in the
dub feature is plain Tailwind utilities on the components. */
/* ── Title strip — coexists with the global .label-row it sits on ── */
.dub-head__title {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: 0;
min-width: 0;
flex: 1;
}
/* ── Column flex that fills height — overrides .studio-panel overflow ── */
.dub-panel-col {
margin-bottom: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Pipeline stepper (top spine) ─────────────────────────────────────── */
.dub-stepper {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 2px 0;
padding: 8px 12px;
flex: 0 0 auto;
border-bottom: 1px solid var(--chrome-border);
}
.dub-stepper__step {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
font-weight: 500;
white-space: nowrap;
color: var(--chrome-fg-dim, #665c54);
}
.dub-stepper__step:not(:first-child)::before {
content: '';
width: 24px;
height: 2px;
margin: 0 8px;
border-radius: 1px;
background: var(--chrome-border-strong, rgba(255, 255, 255, 0.12));
}
.dub-stepper__step.is-reached:not(:first-child)::before {
background: var(--chrome-accent, #d3869b);
}
.dub-stepper__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
flex: 0 0 auto;
border-radius: 50%;
border: 1.5px solid var(--chrome-border-strong, rgba(255, 255, 255, 0.15));
color: var(--chrome-fg-dim, #665c54);
}
.dub-stepper__step.is-done .dub-stepper__icon {
border-color: var(--chrome-accent, #d3869b);
color: var(--chrome-accent, #d3869b);
background: color-mix(in srgb, var(--chrome-accent, #d3869b) 16%, transparent);
}
.dub-stepper__step.is-active .dub-stepper__icon {
border-color: var(--chrome-accent, #d3869b);
color: var(--chrome-accent, #d3869b);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--chrome-accent, #d3869b) 14%, transparent);
}
.dub-stepper__step.is-done .dub-stepper__label,
.dub-stepper__step.is-active .dub-stepper__label {
color: var(--chrome-fg);
}
.dub-stepper__spin { animation: dub-stepper-spin 1s linear infinite; }
@keyframes dub-stepper-spin { to { transform: rotate(360deg); } }
/* ── Idle / drop-zone ─────────────────────────────────────────────────── */
.dub-idle-drop {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
border: 2px dashed rgba(255, 255, 255, 0.06);
border-radius: 8px;
transition: background 0.3s, border-color 0.3s;
margin: 2px;
background: transparent;
}
@keyframes dub-pulse {
0%, 100% { border-color: rgba(211, 134, 155, 0.3); background: rgba(211, 134, 155, 0.04); }
50% { border-color: rgba(211, 134, 155, 0.6); background: rgba(211, 134, 155, 0.08); }
}
.dub-idle-drop.is-dragging {
border-color: #d3869b;
background: rgba(211, 134, 155, 0.05);
animation: dub-pulse 1.2s ease-in-out infinite;
}
.dub-idle-drop__puck {
width: 60px; height: 60px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: rgba(211, 134, 155, 0.06);
border: 1px solid rgba(211, 134, 155, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.dub-idle-drop:hover .dub-idle-drop__puck {
transform: scale(1.08);
box-shadow: 0 0 24px rgba(211, 134, 155, 0.15);
}
.dub-ingest-row__cta {
padding: 3px 10px;
border-radius: 4px;
font-size: 0.7rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
color: #665c54;
cursor: default;
}
.dub-ingest-row__cta.is-ready {
background: rgba(211, 134, 155, 0.15);
border-color: rgba(211, 134, 155, 0.3);
color: var(--color-brand);
cursor: pointer;
}
/* ── Landing options — descendants override global element styling ── */
.dub-landing-opts__lang select { max-width: 220px; }
.dub-landing-adv__field--grow { flex: 1; min-width: 200px; }
.dub-landing-adv__field--grow input { flex: 1; }
/* ── Transcript toggle — sits on the global .override-toggle ── */
.dub-transcript-toggle__inner {
margin-top: 0;
padding: 3px var(--space-3);
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
color: var(--chrome-fg-muted);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
}
.dub-transcript-toggle__inner:hover { color: var(--chrome-fg); }
.dub-skel-transcript-toggle__inner { margin-top: 0; padding: 2px 6px; font-size: 0.65rem; opacity: 0.3; cursor: default; }
/* ── Prep / Transcribe overlays ───────────────────────────────────────── */
.dub-prep-overlay--large {
flex: 1;
justify-content: center;
gap: var(--space-6);
padding: var(--space-5);
margin: var(--space-2);
border: 2px dashed rgba(211, 134, 155, 0.2);
border-radius: var(--radius-xl);
background: rgba(211, 134, 155, 0.03);
}
.dub-prep-bar {
width: min(240px, 85vw);
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
overflow: hidden;
}
.dub-prep-overlay--large .dub-prep-bar { width: 320px; height: 5px; }
.dub-prep-bar__fill {
height: 100%;
background: linear-gradient(90deg, rgba(211, 134, 155, 0.75), rgba(211, 134, 155, 1));
border-radius: inherit;
transition: width 220ms ease-out;
}
.dub-prep-chip {
padding: 1px 6px;
border-radius: var(--radius-xs);
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
color: var(--color-fg-subtle);
}
.dub-prep-chip.is-active {
background: rgba(211, 134, 155, 0.20);
border-color: rgba(211, 134, 155, 0.30);
color: var(--color-brand);
}
.dub-prep-chip.is-active.is-cached {
background: rgba(142, 192, 124, 0.20);
border-color: rgba(142, 192, 124, 0.30);
color: var(--color-success);
}
.dub-prep-chips--lg .dub-prep-chip { padding: 2px 8px; border-radius: var(--radius-sm); font-weight: var(--weight-regular); }
.dub-prep-chips--lg .dub-prep-chip.is-active { font-weight: var(--weight-semibold); }
/* ── Cast strip — --muted overrides descend onto __label/__kicker ── */
.dub-cast__pair { display: flex; align-items: center; gap: 3px; padding: 1px 5px; border-radius: var(--radius-sm); transition: background 0.2s ease; }
.dub-cast__pair:hover { background: rgba(255, 255, 255, 0.03); }
.dub-cast__select { width: 95px; padding: 1px 3px; font-size: 0.60rem; }
.dub-cast--muted { background: transparent; border-color: var(--chrome-border); }
.dub-cast--muted .dub-cast__label,
.dub-cast--muted .dub-cast__kicker { color: var(--chrome-fg-dim); }
/* ── Multi-language preview switcher ──────────────────────────────────── */
.dub-lang-switch {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 6px 0 8px;
}
.dub-lang-pill {
padding: 5px 14px;
font-size: 0.72rem;
font-weight: 600;
color: var(--chrome-fg-muted);
background: var(--chrome-hover-bg, rgba(255, 255, 255, 0.05));
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill, 999px);
cursor: pointer;
transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.dub-lang-pill:hover {
color: var(--chrome-fg);
border-color: var(--chrome-border-strong);
}
.dub-lang-pill.is-active {
color: #1b1b1b;
background: var(--chrome-fg, #ebdbb2);
border-color: var(--chrome-fg, #ebdbb2);
}
.dub-lang-pill:focus-visible {
outline: 2px solid var(--chrome-accent);
outline-offset: 1px;
}
/* ── Skeleton segment rows (shimmer bars) ─────────────────────────────── */
.dub-skel-table { flex: 1; max-height: none; overflow-y: auto; min-height: 0; }
.dub-skel-row { gap: 6px; }
.dub-skel-bar {
height: 11px;
border-radius: 3px;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.09) 37%,
rgba(255, 255, 255, 0.04) 63%);
background-size: 400% 100%;
animation: dub-skel-shimmer 1.4s ease-in-out infinite;
}
.dub-skel-cell-acts__icon { opacity: 0.3; }
@keyframes dub-skel-shimmer {
0% { background-position: 100% 0; }
100% { background-position: 0 0; }
}
/* ── Reduced motion: shimmer/pulse die; stepper spinner holds a frame ── */
@media (prefers-reduced-motion: reduce) {
.dub-skel-bar,
.dub-idle-drop.is-dragging,
.dub-stepper__spin {
animation: none;
}
}
+4 -36
View File
@@ -216,8 +216,6 @@ h1, h2, h3, h4 {
.segment-time,
.segment-table code,
.ui-table-toolbar__meta,
.dub-gen-overlay__stats,
.dub-trans-overlay__stats,
.logs-footer__count,
[data-tabular] {
font-variant-numeric: tabular-nums slashed-zero;
@@ -809,7 +807,6 @@ select:focus:not(:focus-visible) { outline: none; box-shadow: none; }
.personality-chip:focus-visible,
.chip:focus-visible,
.wh__chip:focus-visible,
.dub-lang-pill:focus-visible,
.wf-player__btn:focus-visible,
.studio-action-bar__overrides:focus-visible {
outline: 2px solid var(--chrome-accent);
@@ -1465,15 +1462,8 @@ div[role="dialog"].audio-trimmer {
RESPONSIVE — make every component adapt to viewport width
════════════════════════════════════════════════════════════ */
/* ── DubTab + CloneDesignTab split grids → single column ── */
@media (max-width: 1000px) {
.dub-split-grid {
grid-template-columns: 1fr !important;
grid-template-rows: auto 1fr !important;
}
/* .clone-split-grid is single-column at every width now (spec §1 restack —
see CloneDesignTab.css); no narrow-viewport override needed. */
}
/* DubTab split grid → single column is handled inline in DubTab.jsx /
IdleSkeleton.jsx via Tailwind max-[1000px]:grid-cols-1 utilities. */
/* .grid-2 / .grid-4 responsive collapse → Tailwind max-[700px]/max-[500px]
variants inline at the usage sites (P4). .grid-3 was already dead (no base
@@ -1502,30 +1492,8 @@ div[role="dialog"].audio-trimmer {
.studio-panel { padding: 8px 6px; }
}
/* ── DubTab settings bar wrap ── */
@media (max-width: 900px) {
.dub-settings-bar {
flex-direction: column !important;
gap: 6px !important;
}
.dub-settings-bar > div { min-width: 0 !important; width: 100%; }
}
/* ── DubTab footer buttons — wrap on narrow ── */
@media (max-width: 800px) {
.dub-footer-btns {
flex-wrap: wrap !important;
}
.dub-footer-btns .btn-primary {
min-width: 80px;
flex: 1 1 calc(50% - 4px) !important;
}
}
@media (max-width: 500px) {
.dub-footer-btns .btn-primary {
flex: 1 1 100% !important;
}
}
/* DubTab settings-bar wrap is handled inline in DubLeftColumn.jsx via Tailwind
max-[900px]:gap utilities (the bar is already a flex-column at every width). */
/* ── Segment table — tighter columns ── */
@media (max-width: 800px) {
-800
View File
@@ -1,800 +0,0 @@
/* DubTab — page-specific styles layered over the design system. */
/* ── Header strip ────────────────────────────────────────────── */
/* Top-bar — anchors to the top of the workspace, claims the full width
above the video + segment-table split. Drop the card chrome so it reads
as a header strip, not a floating panel. */
/* .dub-head strip + filename/meta/project/actions/primary → Tailwind utilities
(DubHeader.jsx, IdleSkeleton.jsx). .dub-head__title stays: it coexists with
the unlayered global .label-row rule (which it overrides), so a utility would
lose to it. */
.dub-head__title { display: flex; align-items: center; gap: var(--space-3); margin-bottom: 0; min-width: 0; flex: 1; }
/* ── Preview toggle wrapper (above waveform) ─────────────────── */
.dub-preview-toggle {
display: flex;
gap: var(--space-2);
align-items: center;
padding: 4px var(--space-3);
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
margin-bottom: var(--space-2);
flex-shrink: 0;
}
.dub-preview-toggle__kicker {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
color: var(--chrome-fg-muted);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
margin-right: var(--space-2);
}
.dub-preview-toggle__hint {
font-family: var(--font-sans);
font-size: var(--chrome-label-size);
color: var(--chrome-fg-dim);
margin-left: auto;
}
/* ── Prep stage chips (upload/preparation flow) ──────────────── */
/* .dub-prep-chips container → utilities (PrepOverlay.jsx). The --lg modifier and
the .dub-prep-chip items below stay (state/modifier combinators). */
.dub-prep-chip {
padding: 1px 6px;
border-radius: var(--radius-xs);
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
color: var(--color-fg-subtle);
}
.dub-prep-chip.is-active {
background: rgba(211, 134, 155, 0.20);
border-color: rgba(211, 134, 155, 0.30);
color: var(--color-brand);
}
.dub-prep-chip.is-active.is-cached {
background: rgba(142, 192, 124, 0.20);
border-color: rgba(142, 192, 124, 0.30);
color: var(--color-success);
}
.dub-prep-chips--lg .dub-prep-chip { padding: 2px 8px; border-radius: var(--radius-sm); font-weight: var(--weight-regular); }
.dub-prep-chips--lg .dub-prep-chip.is-active { font-weight: var(--weight-semibold); }
/* ── Prep / Transcribe overlays ──────────────────────────────── */
/* .dub-prep-overlay base + __title/__note/__detail → utilities (PrepOverlay.jsx).
The --large modifier (and its descendant .dub-prep-bar override) stays. */
.dub-prep-overlay--large {
flex: 1;
justify-content: center;
gap: var(--space-6);
padding: var(--space-5);
margin: var(--space-2);
border: 2px dashed rgba(211, 134, 155, 0.2);
border-radius: var(--radius-xl);
background: rgba(211, 134, 155, 0.03);
}
.dub-prep-bar {
width: min(240px, 85vw);
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
overflow: hidden;
}
.dub-prep-overlay--large .dub-prep-bar { width: 320px; height: 5px; }
.dub-prep-bar__fill {
height: 100%;
background: linear-gradient(90deg, rgba(211, 134, 155, 0.75), rgba(211, 134, 155, 1));
border-radius: inherit;
transition: width 220ms ease-out;
}
/* .dub-trans-overlay base + __head/__title/__bar → utilities (TranscribeOverlay.jsx).
__stats stays: it's targeted by the unlayered global tabular-nums rule in index.css. */
.dub-trans-overlay__stats {
display: flex;
gap: var(--space-6);
font-size: var(--text-md);
color: var(--color-fg-muted);
}
/* ── Bulk selection + Apply-voice rows ───────────────────────── */
.dub-bulk-row {
display: flex;
align-items: center;
gap: var(--space-3);
padding: 3px 6px;
border-radius: var(--radius-md);
margin-bottom: var(--space-2);
font-size: var(--text-xs);
}
.dub-bulk-row--apply { background: rgba(142, 192, 124, 0.06); border: 1px solid rgba(142, 192, 124, 0.12); }
.dub-bulk-row--select { background: rgba(211, 134, 155, 0.08); border: 1px solid rgba(211, 134, 155, 0.25); }
.dub-bulk-row__label-success { color: var(--color-success); font-weight: var(--weight-bold); white-space: nowrap; }
/* .dub-bulk-row__label-brand → utilities (DubRightColumn.jsx). */
/* ── Footer status banner ────────────────────────────────────── */
/* .dub-footer-banner → utilities (DubFooter.jsx, IdleSkeleton.jsx). */
.dub-footer-banner__badge { display: inline-flex; }
/* ── Output options row (checkboxes + default track) ─────────── */
.dub-outputs-row {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: 2px;
padding: 0 var(--space-2);
font-size: var(--text-xs);
color: var(--chrome-fg-muted);
font-family: var(--font-sans);
flex-wrap: wrap;
}
.dub-outputs-row label { display: flex; align-items: center; gap: var(--space-2); cursor: pointer; }
.dub-outputs-row label:hover { color: var(--chrome-fg); }
.dub-outputs-row input[type="checkbox"] { accent-color: var(--chrome-accent); }
/* ── Export track checkbox row ───────────────────────────────── */
.dub-tracks-row {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: 2px;
padding: 3px var(--space-3);
font-size: var(--text-xs);
color: var(--chrome-fg-muted);
font-family: var(--font-sans);
background: var(--chrome-bg);
border-radius: var(--chrome-radius-pill);
border: 1px solid var(--chrome-border);
flex-wrap: wrap;
}
.dub-tracks-row__title {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
color: var(--chrome-fg-muted);
font-weight: 600;
}
.dub-tracks-row label {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border: 1px solid transparent;
border-radius: var(--chrome-radius-pill);
cursor: pointer;
transition: border-color var(--dur-fast), background var(--dur-fast), color var(--dur-fast);
}
.dub-tracks-row label input { accent-color: var(--chrome-accent); }
.dub-tracks-row label.is-on.is-success {
color: var(--chrome-severity-ok);
border-color: color-mix(in srgb, var(--chrome-severity-ok) 45%, transparent);
background: color-mix(in srgb, var(--chrome-severity-ok) 10%, transparent);
}
.dub-tracks-row label.is-off { color: var(--chrome-fg-dim); }
.dub-tracks-row label.is-on { color: var(--chrome-fg); border-color: var(--chrome-border-strong); background: var(--chrome-hover-bg); }
.dub-tracks-row label span.code { text-transform: uppercase; letter-spacing: 0.04em; }
/* Pre-generation compression-ratio warning surfaced above Generate Dub. */
.dub-compression-warn {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 10px;
margin: 4px 0;
background: color-mix(in srgb, #fabd2f 12%, transparent);
border: 1px solid color-mix(in srgb, #fabd2f 35%, transparent);
border-left: 2px solid #fabd2f;
border-radius: var(--chrome-radius-pill);
font-size: 0.72rem;
color: var(--chrome-fg);
line-height: 1.35;
}
.dub-compression-warn__icon {
color: #fabd2f;
font-size: 0.9rem;
line-height: 1;
flex-shrink: 0;
}
.dub-compression-warn__body { flex: 1; }
.dub-compression-warn strong { color: #fabd2f; font-weight: var(--weight-semibold); }
/* ── Utility: column flex that fills remaining height ────────── */
/* .dub-col + .dub-split-1/.dub-split-2 → utilities (DubTab.jsx, IdleSkeleton.jsx).
.dub-panel-col stays: it sits on .studio-panel (unlayered) and overrides its
overflow, so a utility would lose. */
.dub-panel-col { margin-bottom: 0; overflow: hidden; display: flex; flex-direction: column; }
/* ── Idle / drop-zone skeleton ───────────────────────────────── */
.dub-idle-drop {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
border: 2px dashed rgba(255, 255, 255, 0.06);
border-radius: 8px;
transition: background 0.3s, border-color 0.3s;
margin: 2px;
background: transparent;
}
@keyframes dub-pulse {
0%, 100% { border-color: rgba(211, 134, 155, 0.3); background: rgba(211, 134, 155, 0.04); }
50% { border-color: rgba(211, 134, 155, 0.6); background: rgba(211, 134, 155, 0.08); }
}
.dub-idle-drop.is-dragging {
border-color: #d3869b;
background: rgba(211, 134, 155, 0.05);
animation: dub-pulse 1.2s ease-in-out infinite;
}
.dub-idle-drop__puck {
width: 60px; height: 60px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: rgba(211, 134, 155, 0.06);
border: 1px solid rgba(211, 134, 155, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.dub-idle-drop:hover .dub-idle-drop__puck {
transform: scale(1.08);
box-shadow: 0 0 24px rgba(211, 134, 155, 0.15);
}
/* .dub-idle-drop__lines/__title/__sub + .dub-ingest-row/__input → utilities
(IdleSkeleton.jsx). .dub-idle-drop base/__puck stay (pulse keyframes + hover);
.dub-ingest-row__cta stays (.is-ready modifier interplay). */
.dub-ingest-row__cta {
padding: 3px 10px;
border-radius: 4px;
font-size: 0.7rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
color: #665c54;
cursor: default;
}
.dub-ingest-row__cta.is-ready {
background: rgba(211, 134, 155, 0.15);
border-color: rgba(211, 134, 155, 0.3);
color: var(--color-brand);
cursor: pointer;
}
/* .dub-idle-upload-label + .dub-hidden-file → utilities (IdleSkeleton.jsx). */
/* .dub-change-row + .dub-speakers-hint → utilities (IdleSkeleton.jsx).
.dub-change-row__cta stays: it sits on .btn-primary (unlayered) and overrides
its margin-top, so a utility would lose. */
.dub-change-row__cta { flex: 1; margin-top: 0; }
.dub-speakers-input { width: 52px; margin-left: 4px; padding: 4px 6px; border-radius: 6px; border: 1px solid var(--border, #3c3836); background: var(--input-bg, #282828); color: inherit; font-size: 12px; }
/* Compact disabled inputs in the idle skeleton */
.input-base--xs { font-size: 0.65rem; }
/* .dub-skel-translate-btn + .dub-skel-transcript-toggle + .dub-inline-icon →
utilities (IdleSkeleton.jsx). __inner stays: it sits on .override-toggle
(unlayered) and overrides its margin/padding, so a utility would lose. */
.dub-skel-transcript-toggle__inner { margin-top: 0; padding: 2px 6px; font-size: 0.65rem; opacity: 0.3; cursor: default; }
.dub-ghost-footer { padding: 4px 8px; flex-shrink: 0; }
/* Settings bar (right-pane) — expanded translation settings.
Two-row layout: fields on top (CSS grid so nothing truncates),
primary/secondary actions flush-right underneath. Wraps into a
container card so it reads as a distinct cluster from the bulk-select
and segment table that follow it. */
.dub-settings-bar {
display: flex;
flex-direction: column;
gap: 3px;
margin-bottom: 4px;
padding: 4px 8px;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
}
.dub-settings-bar__fields {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
align-items: end;
}
.dub-settings-bar__actions {
display: flex;
justify-content: flex-end;
gap: 6px;
flex-wrap: wrap;
}
.dub-settings-field {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.dub-settings-field .label-row {
font-size: 0.58rem;
color: var(--color-fg-muted);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dub-settings-field .input-base,
.dub-settings-field .dub-cast__select,
.dub-settings-field .dub-engine-select,
.dub-settings-field .input-base--xs {
width: 100%;
font-size: 0.65rem;
padding: 3px 5px;
}
.dub-settings-field__hint {
font-size: 0.52rem;
color: var(--color-fg-subtle);
font-style: italic;
margin-left: 2px;
}
/* Size fields so they share the row proportionally */
.dub-settings-field--lang { flex: 1 1 100px; min-width: 70px; }
.dub-settings-field--iso { flex: 0 1 72px; min-width: 52px; }
.dub-settings-field--dialect { flex: 0 1 110px; min-width: 80px; }
.dub-settings-field--engine { flex: 1.4 1 130px; min-width: 90px; }
.dub-settings-field--quality { flex: 0 1 auto; min-width: 80px; }
.dub-settings-field--quality > *:last-child { width: 100%; }
.dub-settings-field--style { flex: 1 1 90px; min-width: 64px; }
@media (max-width: 960px) {
.dub-settings-bar__fields { flex-direction: column; }
.dub-settings-field--lang,
.dub-settings-field--iso,
.dub-settings-field--dialect,
.dub-settings-field--engine,
.dub-settings-field--quality,
.dub-settings-field--style { flex-basis: 100%; min-width: 0; }
}
/* Multi-lang mode toggle + picker — inline in actions row */
.dub-settings-field--multi {
flex-basis: 100%;
padding-top: 3px;
border-top: 1px solid var(--chrome-border);
margin-top: 1px;
}
.dub-multi-toggle {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.65rem;
color: var(--chrome-fg-muted);
cursor: pointer;
margin-bottom: 2px;
}
.dub-multi-toggle input { accent-color: var(--chrome-accent); cursor: pointer; }
/* Collapsed-summary strip that replaces the full grid once language/engine/
quality are set. Keeps the primary Translate All + Clean Up CTAs inline. */
.dub-settings-summary {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 3px var(--space-3);
margin-bottom: 3px;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
font-family: var(--font-sans);
font-size: 0.66rem;
color: var(--chrome-fg-muted);
}
.dub-settings-summary__trigger {
display: inline-flex;
align-items: center;
gap: 5px;
flex: 1;
min-width: 0;
background: none;
border: none;
color: var(--color-fg-muted);
cursor: pointer;
padding: 2px 0;
font: inherit;
text-align: left;
}
.dub-settings-summary__trigger strong { color: var(--chrome-fg); font-weight: 600; }
.dub-settings-summary__style {
color: var(--chrome-fg-dim);
font-style: italic;
margin-left: var(--space-2);
}
.dub-settings-close {
flex: 0 0 auto;
padding: 0 4px;
align-self: center;
}
.dub-engine-select { font-size: 0.65rem; padding: 3px 5px; min-width: 0; }
/* Small opt-in below the URL ingest row: toggle YouTube caption pulling. */
.dub-ingest-sub-opt {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
padding: 4px 6px;
font-size: 0.62rem;
color: var(--color-fg-muted);
cursor: pointer;
border-radius: 4px;
background: rgba(255, 255, 255, 0.02);
}
.dub-ingest-sub-opt:hover { color: var(--color-fg); background: rgba(255, 255, 255, 0.05); }
.dub-ingest-sub-opt input[type="checkbox"] { margin: 0; accent-color: #d3869b; }
/* Inline "install engine" chip next to the Engine label. Tight + brand-tinted
so it reads as an affordance, not a badge. Disappears after a successful
install (refreshEngines re-renders). */
.dub-engine-install-chip {
margin-left: 6px;
padding: 1px 6px;
font-size: 0.55rem;
line-height: 1.4;
background: rgba(211, 134, 155, 0.14);
border: 1px solid rgba(211, 134, 155, 0.35);
color: #d3869b;
border-radius: 999px;
cursor: pointer;
white-space: nowrap;
transition: background var(--dur-fast) var(--ease-out);
}
.dub-engine-install-chip:hover { background: rgba(211, 134, 155, 0.22); }
.dub-engine-install-chip:disabled,
.dub-engine-install-chip--disabled {
opacity: 0.55;
cursor: default;
font-style: italic;
}
/* Transcript collapsible + content */
.dub-transcript-toggle-wrap { margin-bottom: 4px; }
.dub-transcript-toggle__inner {
margin-top: 0;
padding: 3px var(--space-3);
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
color: var(--chrome-fg-muted);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
}
.dub-transcript-toggle__inner:hover { color: var(--chrome-fg); }
.dub-transcript-body {
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-top: none;
border-radius: 0 0 var(--chrome-radius-pill) var(--chrome-radius-pill);
padding: var(--space-3);
font-size: var(--text-xs);
color: var(--chrome-fg-muted);
line-height: 1.5;
max-height: 80px;
overflow-y: auto;
}
/* Glossary wrapper */
.dub-glossary-wrap { margin-bottom: 4px; }
.dub-glossary-chip {
display: inline-flex;
align-items: center;
padding: 3px var(--space-3);
margin-bottom: 4px;
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
color: var(--chrome-fg-muted);
background: transparent;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
cursor: pointer;
transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
}
.dub-glossary-chip:hover {
background: var(--chrome-hover-bg);
border-color: var(--chrome-border-strong);
color: var(--chrome-fg);
}
/* Footer layout — flatten the warm studio-panel backdrop so the chrome
action strip below has somewhere neutral to sit. Keep the content
padding tight. */
.dub-footer-panel {
padding: 4px var(--space-3);
flex-shrink: 0;
background: var(--chrome-bg) !important;
border: 1px solid var(--chrome-border) !important;
border-radius: 0 !important;
}
.dub-footer-panel::before { display: none !important; }
.dub-footer-btns { display: flex; gap: 3px; }
.dub-footer-btns--spaced { margin-top: 3px; }
/* Output options row value column */
.dub-outputs-title-strong {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
color: var(--chrome-fg-muted);
font-weight: 600;
}
/* Bulk-select row controls */
.dub-bulk-select { font-size: 0.62rem; padding: 2px 4px; }
.dub-bulk-select--flex { flex: 1; }
.dub-bulk-select--voice { min-width: 72px; flex: 1 1 100px; }
.dub-bulk-select--lang { width: auto; min-width: 64px; flex: 0 1 90px; }
.dub-bulk-row__clear { margin-left: auto; }
/* Footer banner gapping */
/* .dub-footer-banner__badge-gap → utilities (DubFooter.jsx). */
/* .dub-lazy-fallback → utilities (DubRightColumn.jsx). */
/* ── Landing options — the one pre-upload decision + Advanced disclosure ── */
/* .dub-landing-opts + __lang/__label → utilities (IdleSkeleton.jsx). __lang
keeps its class for the descendant select rule below; __adv stays (:hover). */
.dub-landing-opts__lang select { max-width: 220px; }
.dub-landing-opts__adv {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
font-size: 0.7rem;
color: var(--chrome-fg-muted);
background: transparent;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill, 999px);
cursor: pointer;
transition: color var(--dur-fast), border-color var(--dur-fast);
}
.dub-landing-opts__adv:hover { color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
/* .dub-landing-adv + __field base → utilities (IdleSkeleton.jsx). __field keeps
its class for the --grow modifier + descendant input rule below. */
.dub-landing-adv__field--grow { flex: 1; min-width: 200px; }
.dub-landing-adv__field--grow input { flex: 1; }
/* Demoted ghost export button (single Export ▾ beside the primary Generate). */
.dub-skel-gen-btn--secondary {
display: inline-flex;
align-items: center;
gap: 5px;
background: transparent;
border: 1px solid var(--chrome-border);
color: var(--chrome-fg-muted);
border-radius: 8px;
flex: 0 0 auto;
}
/* ── Multi-language preview switcher (tap a language to hear that dub) ── */
.dub-lang-switch {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 6px 0 8px;
}
.dub-lang-pill {
padding: 5px 14px;
font-size: 0.72rem;
font-weight: 600;
color: var(--chrome-fg-muted);
background: var(--chrome-hover-bg, rgba(255, 255, 255, 0.05));
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill, 999px);
cursor: pointer;
transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.dub-lang-pill:hover {
color: var(--chrome-fg);
border-color: var(--chrome-border-strong);
}
.dub-lang-pill.is-active {
color: #1b1b1b;
background: var(--chrome-fg, #ebdbb2);
border-color: var(--chrome-fg, #ebdbb2);
}
/* ── Pipeline stepper (top spine) ──────────────────────────────────────
Upload → Prepare → Transcribe → Edit → Generate → Export. Done steps and
the connectors behind them take the accent; the active step gets a ring;
pending steps are dim. */
.dub-stepper {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 2px 0;
padding: 8px 12px;
flex: 0 0 auto;
border-bottom: 1px solid var(--chrome-border);
}
.dub-stepper__step {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
font-weight: 500;
white-space: nowrap;
color: var(--chrome-fg-dim, #665c54);
}
.dub-stepper__step:not(:first-child)::before {
content: '';
width: 24px;
height: 2px;
margin: 0 8px;
border-radius: 1px;
background: var(--chrome-border-strong, rgba(255, 255, 255, 0.12));
}
.dub-stepper__step.is-reached:not(:first-child)::before {
background: var(--chrome-accent, #d3869b);
}
.dub-stepper__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
flex: 0 0 auto;
border-radius: 50%;
border: 1.5px solid var(--chrome-border-strong, rgba(255, 255, 255, 0.15));
color: var(--chrome-fg-dim, #665c54);
}
.dub-stepper__step.is-done .dub-stepper__icon {
border-color: var(--chrome-accent, #d3869b);
color: var(--chrome-accent, #d3869b);
background: color-mix(in srgb, var(--chrome-accent, #d3869b) 16%, transparent);
}
.dub-stepper__step.is-active .dub-stepper__icon {
border-color: var(--chrome-accent, #d3869b);
color: var(--chrome-accent, #d3869b);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--chrome-accent, #d3869b) 14%, transparent);
}
.dub-stepper__step.is-done .dub-stepper__label,
.dub-stepper__step.is-active .dub-stepper__label {
color: var(--chrome-fg);
}
.dub-stepper__spin { animation: dub-stepper-spin 1s linear infinite; }
@keyframes dub-stepper-spin { to { transform: rotate(360deg); } }
/* ── Skeleton segment rows (idle / pre-transcription state) ─────────────
Shimmer bars, not literal placeholder text — the old "0:00.00:00.0 /
Speaker 1 / Default" wrapped in their narrow cells and read as broken
data. Bars have a fixed height so nothing wraps or collides. */
.dub-skel-table { flex: 1; max-height: none; overflow-y: auto; min-height: 0; }
.dub-skel-row { gap: 6px; }
.dub-skel-bar {
height: 11px;
border-radius: 3px;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.09) 37%,
rgba(255, 255, 255, 0.04) 63%);
background-size: 400% 100%;
animation: dub-skel-shimmer 1.4s ease-in-out infinite;
}
/* .dub-skel-cell-* and -header-* cells + .dub-skel-hint → utilities
(IdleSkeleton.jsx). __icon stays: it sits on .segment-del (unlayered) and
overrides its opacity, so a utility would lose. */
.dub-skel-cell-acts__icon { opacity: 0.3; }
@keyframes dub-skel-shimmer {
0% { background-position: 100% 0; }
100% { background-position: 0 0; }
}
/* Disabled generation controls shown in idle skeleton */
.dub-skel-gen-btn { margin-top: 0; flex: 1; padding: 4px 8px; font-size: 0.7rem; opacity: 0.4; }
/* .dub-skel-gen-row + .dub-skel-settings + .dub-skel-field/--sm → utilities
(IdleSkeleton.jsx). .dub-skel-gen-btn stays (sits on .btn-primary, unlayered). */
/* ── Generating overlay (on top of waveform) ─────────────────── */
.dub-gen-overlay { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 100%; padding: 10px; backdrop-filter: blur(2px); }
.dub-gen-overlay__head { display: flex; align-items: center; gap: 6px; }
.dub-gen-overlay__title { font-weight: 600; font-size: 0.75rem; color: var(--color-fg); font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
.dub-gen-overlay__title.is-stopping { color: var(--color-fg-muted); }
.dub-gen-overlay__bar { width: 80%; max-width: 240px; margin: 1px 0; }
.dub-gen-overlay__text { font-size: 0.62rem; color: var(--color-fg-muted); }
.dub-gen-overlay__stats { display: flex; gap: var(--space-4); font-size: 0.65rem; color: var(--color-fg-muted); font-variant-numeric: tabular-nums; }
/* ── Cast / speaker-voices strip below waveform ──────────────── */
/* .dub-cast base + __row + __kicker/__label base + --muted__chip → utilities
(IdleSkeleton.jsx). .dub-cast / __kicker / __label keep their classes so the
.dub-cast--muted (and its descendant) overrides below still apply; __pair
(:hover) and __select (real cast, not the skeleton) stay. */
.dub-cast__pair { display: flex; align-items: center; gap: 3px; padding: 1px 5px; border-radius: var(--radius-sm); transition: background 0.2s ease; }
.dub-cast__pair:hover { background: rgba(255, 255, 255, 0.03); }
.dub-cast__select { width: 95px; padding: 1px 3px; font-size: 0.60rem; }
/* Muted placeholder cast strip shown in the idle skeleton */
.dub-cast--muted { background: transparent; border-color: var(--chrome-border); }
.dub-cast--muted .dub-cast__label,
.dub-cast--muted .dub-cast__kicker { color: var(--chrome-fg-dim); }
/* ── Output options row (footer) ─────────────────────────────── */
.dub-outputs-title { font-weight: 600; color: var(--color-fg); }
.dub-outputs-default { font-size: 0.6rem; padding: 2px 4px; width: 120px; }
/* ── Footer download buttons ─────────────────────────────────── */
/* Flat chrome surfaces — color = accent (text+border), not a gradient fill.
Matches the status-bar chrome so the footer row reads as one strip instead
of a jelly of pastel pills. Tones tint via color-mix so a single formula
handles danger/ok/brand/etc consistently. */
.dub-footer-btn {
margin-top: 0 !important;
flex: 1;
padding: 5px 8px !important;
font-family: var(--font-sans) !important;
font-size: 0.72rem !important;
letter-spacing: 0.02em !important;
text-transform: none !important;
background: transparent !important;
border: 1px solid var(--chrome-border-strong) !important;
color: var(--chrome-fg) !important;
border-radius: var(--chrome-radius-pill) !important;
box-shadow: none !important;
transition: background var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast) !important;
}
.dub-footer-btn:hover:not(:disabled) { background: var(--chrome-hover-bg) !important; }
.dub-footer-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.dub-footer-btn--sm { padding: 3px 6px !important; font-size: 0.62rem !important; }
.dub-footer-btn--idle { color: var(--chrome-fg-muted) !important; border-color: var(--chrome-border) !important; }
.dub-footer-btn--stopping { color: var(--chrome-fg-muted) !important; border-color: var(--chrome-border) !important; }
.dub-footer-btn--danger {
color: var(--chrome-severity-err) !important;
border-color: color-mix(in srgb, var(--chrome-severity-err) 45%, transparent) !important;
background: color-mix(in srgb, var(--chrome-severity-err) 10%, transparent) !important;
}
.dub-footer-btn--danger:hover:not(:disabled) { background: color-mix(in srgb, var(--chrome-severity-err) 18%, transparent) !important; }
.dub-footer-btn--green {
color: var(--chrome-severity-ok) !important;
border-color: color-mix(in srgb, var(--chrome-severity-ok) 45%, transparent) !important;
background: color-mix(in srgb, var(--chrome-severity-ok) 10%, transparent) !important;
}
.dub-footer-btn--green:hover:not(:disabled) { background: color-mix(in srgb, var(--chrome-severity-ok) 18%, transparent) !important; }
.dub-footer-btn--pink {
color: var(--chrome-accent) !important;
border-color: var(--chrome-accent-border) !important;
background: var(--chrome-accent-bg) !important;
}
.dub-footer-btn--pink:hover:not(:disabled) { background: color-mix(in srgb, var(--chrome-accent) 20%, transparent) !important; }
.dub-footer-btn--blue {
color: #83a598 !important;
border-color: color-mix(in srgb, #83a598 45%, transparent) !important;
background: color-mix(in srgb, #83a598 10%, transparent) !important;
}
.dub-footer-btn--lime {
color: #b8bb26 !important;
border-color: color-mix(in srgb, #b8bb26 45%, transparent) !important;
background: color-mix(in srgb, #b8bb26 10%, transparent) !important;
}
.dub-footer-btn--amber {
color: var(--chrome-severity-warn) !important;
border-color: color-mix(in srgb, var(--chrome-severity-warn) 45%, transparent) !important;
background: color-mix(in srgb, var(--chrome-severity-warn) 10%, transparent) !important;
}
.dub-footer-btn--orange {
color: #fe8019 !important;
border-color: color-mix(in srgb, #fe8019 45%, transparent) !important;
background: color-mix(in srgb, #fe8019 10%, transparent) !important;
}
/* plan-04 (#131): actionable failure detail beneath the error badge.
.dub-failure-notice + __hint/__actions → Tailwind utilities (DubFailureNotice.jsx). */
/* ── Reduced motion (10x P4, spec §3): shimmer/pulse die; the stepper
spinner holds a static frame. ───────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.dub-skel-bar,
.dub-idle-drop.is-dragging,
.dub-stepper__spin {
animation: none;
}
}
+2 -2
View File
@@ -14,7 +14,7 @@ import DubHeader from '../components/dub/DubHeader';
import DubLeftColumn from '../components/dub/DubLeftColumn';
import DubRightColumn from '../components/dub/DubRightColumn';
import DubFooter from '../components/dub/DubFooter';
import './DubTab.css';
import '../components/dub/dub.css';
export default function DubTab(props) {
const { t, i18n } = useTranslation();
@@ -478,7 +478,7 @@ export default function DubTab(props) {
handleDubQc={handleDubQc}
setExportOpen={setExportOpen}
/>
<div className="dub-split-grid grid grid-cols-2 gap-[6px] flex-1 min-h-0 overflow-hidden">
<div className="grid grid-cols-2 max-[1000px]:grid-cols-1 max-[1000px]:grid-rows-[auto_1fr] gap-[6px] flex-1 min-h-0 overflow-hidden">
<DubLeftColumn
hasDubbedTrack={hasDubbedTrack}
t={t}