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:
co-authored by
mergetest
Claude Opus 4.8
parent
bafe9677d9
commit
b033b8e9da
@@ -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:{' '}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.0–0: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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user