feat(ui): rewrite clone/design on clean shadcn, delete CloneDesign CSS + studio shell (fast mode) (#819)

Migrate the Clone / Voice-Design feature area to clean shadcn primitives +
Tailwind utilities, deleting the 338-line CloneDesignTab.css and trimming the
`studio-*` shell from index.css. Fast mode: palette kept, behavior intact, no
pixel-perfect reproduction.

Components rewritten on utilities (token utilities bg/border/text + standard
spacing), behavior preserved exactly:
- MicButton: mic-btn idle/recording/cleaning → utilities; pulse/spin reuse the
  global keyframes via `animate-[…]`.
- ScriptPanel: studio-column/studio-panel, the ⊕ Insert button + popover,
  coachmark close, and the script textarea → utilities.
- AudioMethodPanel: drop zone (clone-drop-zone padding override folded in),
  design-seed input, save-as-profile row → utilities.
- DesignMethodPanel: describe textarea, Starting-points scroll lane (mask edge
  fade), identity recipe line, category chip/select grid → utilities.
- ActionBar: production-override sliders row, language/steps controls, overrides
  disclosure, footer CTA → utilities.
- CloneDesignTab: clone-split-grid + voice column/panel → utilities; CSS import
  removed.

studio-* shell decision (grep-verified cross-file):
- `.studio-panel` — KEPT (dub: DubLeftColumn/RightColumn/Footer, IdleSkeleton).
  Clone usages migrated to inline utilities.
- `.studio-action-bar` — KEPT, relocated from the deleted CSS into index.css.
  WorkspaceHistory.css adds its `position: sticky` narrow-shell override (#476
  CTA-clip fix, guarded by workspaceHistoryReflow.test.js), so it stays a class.
  Its __row/__lang/__steps/__overrides children migrated to utilities.
- `.studio-column` — DELETED (only consumers were clone; now utilities).

Removed `.identity-line`/`.clone-insert-btn`/`.studio-action-bar__overrides`
from the shared focus-visible rule; the accent ring is now inline on each.

Verify: vite build ✓, oxlint 0, oxfmt clean, vitest 641 pass, bun
--frozen-lockfile ✓. Eyeballed both From-audio and By-design sub-views (incl.
production overrides) in chromium — 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 02:02:58 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent b940eb460f
commit c931d8f4f8
8 changed files with 98 additions and 397 deletions
+19 -16
View File
@@ -52,12 +52,12 @@ export default function ActionBar({
wasGeneratingRef,
}) {
return (
<div className="studio-action-bar clone-panel--overflow-visible">
<div className="studio-action-bar overflow-visible relative z-[10]">
{showOverrides && (
<div className="override-content">
<div className="grid [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))] gap-[6px] max-[500px]:grid-cols-2">
<div>
<div className="label-row label-row--spread">
<div className="label-row justify-between">
<span>CFG</span>
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
{cfg}
@@ -73,7 +73,7 @@ export default function ActionBar({
/>
</div>
<div>
<div className="label-row label-row--spread">
<div className="label-row justify-between">
<span>{t('clone.speed')}</span>
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
{speed}x
@@ -89,7 +89,7 @@ export default function ActionBar({
/>
</div>
<div>
<div className="label-row label-row--spread">
<div className="label-row justify-between">
<span>{t('clone.tshift')}</span>
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
{tShift}
@@ -105,7 +105,7 @@ export default function ActionBar({
/>
</div>
<div>
<div className="label-row label-row--spread">
<div className="label-row justify-between">
<span>{t('clone.pos_temp')}</span>
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
{posTemp}
@@ -121,7 +121,7 @@ export default function ActionBar({
/>
</div>
<div>
<div className="label-row label-row--spread">
<div className="label-row justify-between">
<span>{t('clone.class_temp')}</span>
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
{classTemp}
@@ -137,7 +137,7 @@ export default function ActionBar({
/>
</div>
<div>
<div className="label-row label-row--spread">
<div className="label-row justify-between">
<span>{t('clone.layer_pen')}</span>
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
{layerPenalty}
@@ -158,7 +158,7 @@ export default function ActionBar({
</div>
<input
type="text"
className="input-base clone-duration-input"
className="input-base text-[0.8rem]"
value={duration}
onChange={(e) => setDuration(e.target.value)}
placeholder={t('clone.auto')}
@@ -187,8 +187,8 @@ export default function ActionBar({
)}
{/* Controls row: language · steps · overrides disclosure */}
<div className="studio-action-bar__row">
<div className="studio-action-bar__lang">
<div className="flex items-center gap-[16px] min-w-0">
<div className="flex items-center gap-[6px] flex-[1_1_220px] min-w-[140px] [&>:last-child]:flex-1 [&>:last-child]:min-w-0">
<Globe size={12} className="label-icon" />
<SearchableSelect
value={language}
@@ -198,7 +198,10 @@ export default function ActionBar({
onChange={setLanguage}
/>
</div>
<label className="studio-action-bar__steps" title={t('clone.steps')}>
<label
className="flex items-center gap-[6px] flex-[1_1_160px] min-w-[120px] [&_input]:flex-1 [&_input]:min-w-[60px]"
title={t('clone.steps')}
>
<SlidersHorizontal size={12} className="label-icon" />
<input
type="range"
@@ -213,7 +216,7 @@ export default function ActionBar({
</label>
<button
type="button"
className="studio-action-bar__overrides"
className="inline-flex items-center gap-[4px] px-[10px] py-[4px] text-[0.7rem] text-[var(--chrome-fg-muted)] bg-transparent border border-[var(--chrome-border)] rounded-[var(--chrome-radius-pill)] cursor-pointer whitespace-nowrap flex-none transition-[color,border-color] duration-[var(--dur-fast)] hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)] focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]"
onClick={() => setShowOverrides(!showOverrides)}
aria-expanded={showOverrides}
>
@@ -229,7 +232,7 @@ export default function ActionBar({
block
onClick={playDemoOutput}
leading={<Play size={14} />}
className="clone-footer-cta"
className="mt-[6px]"
>
{demoAudioPlaying ? t('demo.stop_demo') : t('demo.hear_demo')}
</Button>
@@ -254,7 +257,7 @@ export default function ActionBar({
block
onClick={stopActivePlayback}
leading={<Square size={14} />}
className="clone-footer-cta"
className="mt-[6px]"
>
{t('clone.stop_playback')}
</Button>
@@ -265,7 +268,7 @@ export default function ActionBar({
loading={isGenerating}
onClick={handleGenerate}
leading={!isGenerating && <Play size={14} />}
className="clone-footer-cta"
className="mt-[6px]"
>
{isGenerating
? t('clone.synthesizing', { seconds: generationTime })
@@ -277,7 +280,7 @@ export default function ActionBar({
value={Math.min((generationTime / 8) * 100, 95)}
tone="brand"
size="sm"
className="clone-footer-cta"
className="mt-[6px]"
/>
)}
{/* 10x P4 a11y (spec §3): persistent polite live region — screen
@@ -48,10 +48,10 @@ export default function AudioMethodPanel({
/>
<label
htmlFor="audio-upload"
// Migrated `.file-drag` utilities. `is-dragging` stays as a JS-toggled
// state marker, matched via the `[&.is-dragging]:` variant. `clone-drop-zone`
// (out-of-scope `clone-` family, unlayered) still wins the padding override.
className="clone-drop-zone [border:1px_dashed_var(--chrome-border-strong)] rounded-[var(--chrome-radius-pill)] p-[10px] text-center cursor-pointer flex flex-col items-center gap-[4px] bg-transparent [transition:border-color_var(--dur-fast),background_var(--dur-fast)] hover:[border-color:var(--chrome-accent)] hover:bg-[var(--chrome-accent-bg)] [&.is-dragging]:[border-color:var(--chrome-accent)] [&.is-dragging]:bg-[var(--chrome-accent-bg)]"
// Migrated `.file-drag` + the old `.clone-drop-zone` padding override
// utilities (fast shadcn, CloneDesignTab.css deleted). `is-dragging` stays
// a JS-toggled state marker, matched via the `[&.is-dragging]:` variant.
className="flex-1 [border:1px_dashed_var(--chrome-border-strong)] rounded-[var(--chrome-radius-pill)] p-[6px] text-center cursor-pointer flex flex-col items-center gap-[4px] bg-transparent [transition:border-color_var(--dur-fast),background_var(--dur-fast)] hover:[border-color:var(--chrome-accent)] hover:bg-[var(--chrome-accent-bg)] [&.is-dragging]:[border-color:var(--chrome-accent)] [&.is-dragging]:bg-[var(--chrome-accent-bg)]"
onDragOver={(e) => {
e.preventDefault();
e.currentTarget.classList.add('is-dragging');
@@ -135,7 +135,7 @@ export default function AudioMethodPanel({
<div className="flex gap-[var(--space-3)] items-center">
<input
type="number"
className="input-base design-seed__input"
className="input-base w-[9rem] flex-none"
value={designSeed ?? ''}
placeholder={t('clone.seed_placeholder')}
onChange={(e) => {
@@ -188,7 +188,7 @@ export default function AudioMethodPanel({
{t('clone.save_as_profile')}
</Button>
) : (
<div className="clone-save-profile__row flex gap-[var(--space-3)] items-center">
<div className="flex gap-[var(--space-3)] items-center [&>:first-child]:flex-1">
<Input
size="sm"
placeholder={t('clone.profile_name')}
@@ -60,7 +60,7 @@ export default function DesignMethodPanel({
The placeholder explains itself; no extra header (10x §1.2). ── */}
<div className="mb-[8px]">
<textarea
className="input-base describe-voice-area"
className="input-base w-full resize-y min-h-[44px] mb-1"
rows={2}
placeholder={t('clone.describe_placeholder')}
value={describeText}
@@ -89,7 +89,7 @@ export default function DesignMethodPanel({
<div className="font-[var(--chrome-font-mono)] text-[0.62rem] uppercase tracking-[0.06em] text-[var(--chrome-fg-muted)] mb-[6px]">
{t('clone.starting_points', { defaultValue: 'Starting points' })}
</div>
<div className="personality-strip starting-points__strip">
<div className="flex flex-nowrap gap-[6px] mb-[10px] overflow-x-auto pb-[2px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [-webkit-mask-image:linear-gradient(90deg,transparent,#000_12px,#000_calc(100%-18px),transparent)] [mask-image:linear-gradient(90deg,transparent,#000_12px,#000_calc(100%-18px),transparent)]">
{chipPersonalities.map((p) => {
const Icon = PERSONALITY_ICONS[p.id] || FALLBACK_PERSONALITY_ICON;
return (
@@ -130,7 +130,7 @@ export default function DesignMethodPanel({
(first run) starts expanded. */}
<button
type="button"
className="identity-line"
className="flex items-center gap-[8px] w-full mt-[4px] mb-[8px] px-[10px] py-[6px] bg-[var(--chrome-hover-bg)] border border-[var(--chrome-border)] rounded-[8px] cursor-pointer text-left transition-[border-color] duration-[var(--dur-fast)] hover:border-[var(--chrome-border-strong)] focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]"
onClick={() => setIdentityOpen((o) => !o)}
aria-expanded={identityOpen}
>
@@ -152,11 +152,8 @@ export default function DesignMethodPanel({
return tl !== tKey ? tl : val;
};
return (
<div
key={key}
className={`clone-cat ${many ? 'clone-cat--select' : 'clone-cat--chips'}`}
>
<div className="label-row label-row--sm">
<div key={key} className={many ? 'min-w-0 max-[1100px]:col-[1/-1]' : 'col-[1/-1]'}>
<div className="label-row text-[0.7rem]">
{t(`clone.cat_${key}`)}
<span className="ml-[6px] text-[0.58rem] text-[var(--chrome-fg-muted)] font-medium">
{vdStates[key] === 'Auto'
@@ -234,7 +231,7 @@ export default function DesignMethodPanel({
{t('clone.save_design_as_profile', { defaultValue: 'Save design as profile' })}
</Button>
) : (
<div className="clone-save-profile__row flex gap-[var(--space-3)] items-center">
<div className="flex gap-[var(--space-3)] items-center [&>:first-child]:flex-1">
<Input
size="sm"
placeholder={t('clone.profile_name')}
+16 -4
View File
@@ -1,19 +1,31 @@
import { useTranslation } from 'react-i18next';
import { Sparkles, Square, Mic } from 'lucide-react';
// `.mic-btn` family migrated from CloneDesignTab.css → Tailwind utilities (fast
// shadcn). Same flat chrome pill; the recording pulse + cleaning spinner reuse
// the global `pulse` / `spin` keyframes (index.css) via arbitrary `animate-[…]`.
const MIC_BASE =
'flex flex-col items-center justify-center gap-[var(--space-2)] px-4 py-2 min-w-[70px] rounded-[var(--radius-xl)] text-[length:var(--text-xs)] font-semibold cursor-pointer transition-all duration-[var(--dur-base)] ease-[var(--ease-out)]';
const MIC_IDLE =
'bg-white/[0.03] border border-white/10 text-[var(--color-fg-muted)] hover:border-[var(--color-danger)] hover:text-[var(--color-danger)]';
const MIC_RECORDING =
'bg-[rgba(251,73,52,0.15)] border-2 border-[var(--color-danger)] text-[var(--color-danger)] animate-[pulse_1s_ease-in-out_infinite]';
const MIC_CLEANING =
'bg-[rgba(184,187,38,0.10)] border border-[rgba(184,187,38,0.20)] text-[#b8bb26] cursor-default';
export default function MicButton({ isCleaning, isRecording, recordingTime, onStart, onStop }) {
const { t } = useTranslation();
if (isCleaning) {
return (
<div className="mic-btn mic-btn--cleaning">
<Sparkles size={18} className="spinner" />
<div className={`${MIC_BASE} ${MIC_CLEANING}`}>
<Sparkles size={18} className="animate-[spin_1s_linear_infinite]" />
<span>{t('clone.cleaning')}</span>
</div>
);
}
if (isRecording) {
return (
<button type="button" onClick={onStop} className="mic-btn mic-btn--recording">
<button type="button" onClick={onStop} className={`${MIC_BASE} ${MIC_RECORDING}`}>
<Square size={18} fill="currentColor" />
<span>{recordingTime}s</span>
</button>
@@ -23,7 +35,7 @@ export default function MicButton({ isCleaning, isRecording, recordingTime, onSt
<button
type="button"
onClick={onStart}
className="mic-btn mic-btn--idle"
className={`${MIC_BASE} ${MIC_IDLE}`}
title={t('clone.record')}
>
<Mic size={18} />
+23 -10
View File
@@ -8,6 +8,13 @@ import { TAGS } from '../../utils/constants';
const TAG_BTN =
'border border-[var(--chrome-border)] bg-transparent text-[var(--chrome-fg-muted)] px-[9px] py-[3px] rounded-[var(--chrome-radius-pill)] font-[var(--chrome-font-mono)] font-medium text-[0.66rem] whitespace-nowrap cursor-pointer transition-colors duration-[120ms] hover:bg-[var(--chrome-hover-bg)] hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)]';
// Studio shell migrated from the `studio-*` classes + CloneDesignTab.css to
// utilities (fast shadcn). `.studio-panel` stays defined in index.css for the
// dub area; clone reproduces it inline so its bespoke restack (flat stack,
// overflow-visible insert popover) is self-contained.
const STUDIO_PANEL =
'flex flex-col min-h-0 bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-none py-[10px] px-[12px] max-[800px]:px-[10px] max-[600px]:px-[6px] max-[600px]:py-[8px]';
export default function ScriptPanel({
t,
defineMethod,
@@ -26,12 +33,11 @@ export default function ScriptPanel({
insertTag,
}) {
return (
<div className="studio-column">
<div className="flex flex-col gap-[6px] flex-none min-h-0 relative z-[2]">
{/* overflow-visible: the ⊕ Insert popover opens above the textarea and
must escape the panel's `overflow:auto` box instead of being clipped
into its scroll region (#481). */}
<div className="studio-panel clone-panel--overflow-visible">
<div className="label-row label-row--center">
must escape the panel's box instead of being clipped (#481). */}
<div className={`${STUDIO_PANEL} relative z-[10] overflow-visible`}>
<div className="label-row">
<Command className="label-icon" size={14} />{' '}
{t('clone.script', { defaultValue: 'Script' })}
</div>
@@ -49,7 +55,7 @@ export default function ScriptPanel({
<span className="flex-1">{t('demo.clone_coachmark')}</span>
<button
type="button"
className="clone-coachmark__close"
className="border-0 bg-transparent text-[var(--color-fg-muted)] cursor-pointer text-[16px] leading-none px-1 rounded-[4px] hover:bg-white/[0.08] hover:text-[var(--color-fg)]"
onClick={() => setShowDemoCoachmark(false)}
aria-label="Dismiss coach mark"
>
@@ -60,7 +66,7 @@ export default function ScriptPanel({
<div className="relative flex-1 flex flex-col min-h-0">
<textarea
ref={textAreaRef}
className="input-base clone-text-area"
className="input-base flex-[0_1_auto] resize-y min-h-[160px] mb-[6px]"
placeholder={
defineMethod === 'audio'
? t('clone.prompt_placeholder')
@@ -77,7 +83,11 @@ export default function ScriptPanel({
power feature (10x spec §1.4). */}
<button
type="button"
className={`clone-insert-btn ${insertOpen ? 'is-open' : ''}`}
className={`absolute right-[8px] bottom-[30px] inline-flex items-center gap-[4px] px-2 py-1 text-[0.66rem] bg-[var(--chrome-bg)] border rounded-[var(--chrome-radius-pill)] cursor-pointer transition-[color,border-color] duration-[var(--dur-fast)] focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px] ${
insertOpen
? 'text-[var(--chrome-fg)] border-[var(--chrome-border-strong)]'
: 'text-[var(--chrome-fg-muted)] border-[var(--chrome-border)] hover:text-[var(--chrome-fg)] hover:border-[var(--chrome-border-strong)]'
}`}
onClick={() => setInsertOpen((o) => !o)}
aria-expanded={insertOpen}
aria-label={t('clone.insert_token', { defaultValue: 'Insert expression token' })}
@@ -89,7 +99,10 @@ export default function ScriptPanel({
<div className="fixed inset-0 z-[19]" onClick={() => setInsertOpen(false)} />
)}
{insertOpen && (
<div className="clone-insert-pop" role="menu">
<div
className="absolute right-[8px] bottom-[60px] z-20 flex flex-wrap gap-1 max-w-[min(360px,calc(100vw-16px))] max-h-[min(280px,calc(100vh-120px))] overflow-y-auto overscroll-contain p-2 bg-[var(--chrome-bg)] border border-[var(--chrome-border-strong)] rounded-[10px] shadow-[0_8px_24px_rgba(0,0,0,0.45)]"
role="menu"
>
{TAGS.map((tag) => (
<button
key={tag}
@@ -104,7 +117,7 @@ export default function ScriptPanel({
</button>
))}
<button
className={`${TAG_BTN} clone-auto-extract-btn`}
className={`${TAG_BTN} !border-[#b8bb26] !text-[#b8bb26]`}
role="menuitem"
onClick={() => {
insertTag('[B EY1 S]');
+23 -8
View File
@@ -802,13 +802,10 @@ select:focus:not(:focus-visible) { outline: none; box-shadow: none; }
/* 10x P4 (spec §3 a11y gate): one consistent, fully-opaque ring on the
studio's 10x controls — shared rule, not per-component restyles. */
.identity-line:focus-visible,
.clone-insert-btn:focus-visible,
.personality-chip:focus-visible,
.chip:focus-visible,
.wh__chip:focus-visible,
.wf-player__btn:focus-visible,
.studio-action-bar__overrides:focus-visible {
.wf-player__btn:focus-visible {
outline: 2px solid var(--chrome-accent);
outline-offset: 1px;
}
@@ -1442,9 +1439,25 @@ div[role="dialog"].audio-trimmer {
.studio-panel::before { display: none; }
.studio-panel > * { position: relative; z-index: 1; }
.studio-column {
display: grid; grid-template-rows: minmax(0, 1fr) auto;
gap: 6px; min-height: 0;
/* `.studio-column` removed (fast shadcn): its only consumers were Clone's two
columns, now built from Tailwind utilities in CloneDesignTab.jsx +
ScriptPanel.jsx. The dub area uses `.studio-panel` directly, not a column
wrapper. */
/* Clone's pinned Synthesize action bar (ActionBar.jsx). Relocated here from the
deleted CloneDesignTab.css: WorkspaceHistory.css adds a `position: sticky`
override to `.studio-action-bar` in the narrow shell (the #476 CTA-clipping
fix, guarded by test/workspaceHistoryReflow.test.js), so this stays a shell
class rather than inline utilities. Its __row/__lang/__steps/__overrides
children migrated to Tailwind utilities at the usage site. */
.studio-action-bar {
flex: 0 0 auto;
border-top: 1px solid var(--chrome-border);
background: var(--chrome-bg);
padding: 8px 12px 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
/* ═══════════════════════════════════════════════════════════
@@ -1452,7 +1465,9 @@ div[role="dialog"].audio-trimmer {
════════════════════════════════════════════════════════════ */
/* DubTab split grid → single column is handled inline in DubTab.jsx /
IdleSkeleton.jsx via Tailwind max-[1000px]:grid-cols-1 utilities. */
IdleSkeleton.jsx via Tailwind max-[1000px]:grid-cols-1 utilities.
Clone's split is a single-column flex stack at every width (built from
Tailwind utilities in CloneDesignTab.jsx) — no narrow override needed. */
/* .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
-338
View File
@@ -1,338 +0,0 @@
/* CloneDesignTab — page-specific styles on top of design-system primitives. */
.clone-profile-delete {
position: absolute;
top: 2px;
right: 2px;
background: none;
border: none;
color: var(--color-danger);
cursor: pointer;
padding: 0;
opacity: 0.7;
transition: opacity var(--dur-fast) var(--ease-out);
}
.clone-profile-delete:hover { opacity: 1; }
/* .clone-profile-banner(+__label), .clone-save-profile(+__row base) → Tailwind utilities
in AudioMethodPanel.jsx + DesignMethodPanel.jsx. */
.clone-save-profile__row > :first-child { flex: 1; }
/* ── Record / Cleaning / Recording mic button ────────────────── */
.mic-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: 8px 16px;
min-width: 70px;
border-radius: var(--radius-xl);
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
cursor: pointer;
transition: all var(--dur-base) var(--ease-out);
}
.mic-btn--idle {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.10);
color: var(--color-fg-muted);
}
.mic-btn--idle:hover {
border-color: var(--color-danger);
color: var(--color-danger);
}
.mic-btn--recording {
background: rgba(251, 73, 52, 0.15);
border: 2px solid var(--color-danger);
color: var(--color-danger);
animation: pulse 1s ease-in-out infinite;
}
.mic-btn--cleaning {
background: rgba(184, 187, 38, 0.10);
border: 1px solid rgba(184, 187, 38, 0.20);
color: #b8bb26;
cursor: default;
}
.mic-btn .spinner { animation: spin 1s linear infinite; }
/* ── Page layout ─────────────────────────────────────────────── */
/* 10x console: scrollable definition content + a pinned action bar. */
/* .studio-def-col → Tailwind utilities in CloneDesignTab.jsx. */
.studio-action-bar {
flex: 0 0 auto;
border-top: 1px solid var(--chrome-border);
background: var(--chrome-bg);
padding: 8px 12px 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.studio-action-bar__row {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
.studio-action-bar__lang {
display: flex;
align-items: center;
gap: 6px;
flex: 1 1 220px;
min-width: 140px;
}
.studio-action-bar__lang > :last-child { flex: 1; min-width: 0; }
.studio-action-bar__steps {
display: flex;
align-items: center;
gap: 6px;
flex: 1 1 160px;
min-width: 120px;
}
.studio-action-bar__steps input[type='range'] { flex: 1; min-width: 60px; }
.studio-action-bar__overrides {
display: inline-flex;
align-items: center;
gap: 4px; /* 10x P4 8-pt audit: 5 → 4 */
padding: 4px 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;
white-space: nowrap;
flex: 0 0 auto;
transition: color var(--dur-fast), border-color var(--dur-fast);
}
.studio-action-bar__overrides:hover { color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
/* ── Script editor: ⊕ Insert token popover ───────────────────── */
/* .clone-script-wrap → Tailwind utilities in ScriptPanel.jsx. */
.clone-insert-btn {
position: absolute;
right: 8px;
/* Lifted off the textarea's bottom-right resize grip so the ⊕ button no
longer covers the drag handle (#481). The popover's `bottom` offset below
is anchored to this row, so they move together. */
bottom: 30px;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px; /* 10x P4 8-pt audit: 3px 9px → 4px 8px */
font-size: 0.66rem;
color: var(--chrome-fg-muted);
background: var(--chrome-bg);
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);
}
.clone-insert-btn:hover, .clone-insert-btn.is-open { color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
/* .clone-insert-backdrop → Tailwind utilities in ScriptPanel.jsx. */
.clone-insert-pop {
position: absolute;
right: 8px;
/* Opens upward, clearing the lifted ⊕ button (bottom:30px + its height). */
bottom: 60px;
z-index: 20;
display: flex;
flex-wrap: wrap;
gap: 4px;
/* Viewport-edge guard (#481): the 14-chip grid can't spill past the window
even when the script panel is narrow. The script panel's
`.clone-panel--overflow-visible` (applied in the JSX) stops the parent
`overflow:auto` from clipping it. */
max-width: min(360px, calc(100vw - 16px));
/* Vertical guard: the popover opens upward, and with 15 chips wrapping to
several rows it used to grow unbounded and clip off the TOP of the window
(the ⊕ button can sit high in a tall script panel). Cap the height to the
space above the button and scroll the overflow, so the whole list always
stays in view instead of shooting past the app's top edge. */
max-height: min(280px, calc(100vh - 120px));
overflow-y: auto;
overscroll-behavior: contain;
padding: 8px;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border-strong);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
/* ── Identity recipe line (collapsed category summary) ───────── */
.identity-line {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
margin: 4px 0 8px;
padding: 6px 10px;
background: var(--chrome-hover-bg, rgba(255, 255, 255, 0.03));
border: 1px solid var(--chrome-border);
border-radius: 8px;
cursor: pointer;
text-align: left;
transition: border-color var(--dur-fast);
}
.identity-line:hover { border-color: var(--chrome-border-strong); }
/* .identity-line__kicker / .identity-line__recipe → Tailwind utilities in DesignMethodPanel.jsx
(the .identity-line button base stays in CSS — it has an index.css :focus-visible rule). */
/* ── Starting points: ONE preset lane, edge-faded scroll ─────── */
/* .starting-points / .starting-points__label → Tailwind utilities in DesignMethodPanel.jsx
(the __strip scroll lane below stays in CSS — mask-image + scrollbar pseudo + descendant). */
.starting-points__strip {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
padding-bottom: 2px;
/* Overflow honesty (10x §3): clipping reads as "scrolls", never as a
cut glyph. */
-webkit-mask-image: linear-gradient(90deg, transparent, #000 12px, #000 calc(100% - 18px), transparent);
mask-image: linear-gradient(90deg, transparent, #000 12px, #000 calc(100% - 18px), transparent);
}
.starting-points__strip::-webkit-scrollbar { display: none; }
/* The personality chips' non-shrink sizing (was `.personality-chip { flex:0 0 auto }`)
moved onto the chips as the `flex-none` utility in DesignMethodPanel.jsx (P4). */
/* One definition column: Prompt stacked over Voice Source (spec §1 restack).
The old side-by-side split crammed both panels once the right-side
Voices/History column took its 340px — the drop zone, Record button and
Transcript/Style fields had no room and read as broken. */
.clone-split-grid {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
min-height: 0;
overflow-y: auto;
}
/* The base .studio-column is a grid with `minmax(0,1fr)` rows — that needs a
definite height and collapses to 0 (panels overlapping) inside this
auto-height scroll column. Re-flow the columns as natural-height stacks. */
.clone-split-grid > .studio-column {
display: flex;
flex-direction: column;
gap: 6px;
flex: 0 0 auto;
min-height: 0;
position: relative;
}
/* Natural stacking — forcing the script column to grow left a void between
SCRIPT and VOICE; the pinned action bar owns the bottom edge anyway. */
.clone-split-grid > .studio-column:first-child { flex: 0 0 auto; z-index: 2; }
.clone-split-grid > .studio-column:last-child { z-index: 1; }
.clone-panel--overflow-visible { overflow: visible; position: relative; z-index: 10; }
/* Label-row alignment variants for this page */
.label-row--center { align-items: center; }
.label-row--spread { justify-content: space-between; }
.label-row--flush { margin-bottom: 0; }
.label-row--sm { font-size: 0.7rem; margin-bottom: 4px; }
.label-row__kicker { margin-right: 6px; }
/* Text input + textarea tweaks */
.clone-text-area {
/* #595: the script field "couldn't be expanded". It was `flex: 1` inside a
flex column, so flex-grow recomputed its height on every reflow and
snapped the user's drag back — `resize: vertical` is silently ignored on a
flex-grown item in Chromium/WebView2 (Windows). Drop flex-grow (`flex: 0 1
auto`) so the textarea owns its height: it starts at min-height and the
corner grip now grows it reliably on every platform. The ⊕ Insert button is
lifted off the bottom-right so it doesn't cover the grip (#481). */
flex: 0 1 auto;
resize: vertical;
min-height: 160px;
margin-bottom: 6px;
}
.clone-auto-extract-btn { border-color: #b8bb26; color: #b8bb26; }
.clone-duration-input { font-size: 0.8rem; }
/* Saved-profile dropdown spacing */
.clone-profile-block { margin-bottom: 10px; }
.clone-profile-card { position: relative; }
/* File-drop + record-button row */
/* .clone-drop-row → Tailwind utilities in AudioMethodPanel.jsx (.clone-drop-zone stays:
it overrides the unlayered .file-drag padding, which a utility can't beat). */
.clone-drop-zone { padding: 6px; flex: 1; }
/* .clone-drop-filename / .grid-2--indent → Tailwind utilities in AudioMethodPanel.jsx. */
/* "Describe your voice" free-text block (issue #317) — base block margin + hint + feedback
→ Tailwind utilities in DesignMethodPanel.jsx (.describe-voice-area stays: it interacts
with the unlayered textarea.input-base reset). */
.describe-voice-area {
width: 100%;
resize: vertical;
min-height: 44px;
margin-bottom: 4px;
}
/* Category controls — compact two-column budget: chip groups (Gender/Age/
Pitch/Style) span the full width; the long dropdown facets (English accent,
Chinese dialect) sit side-by-side on one row instead of stacking. */
/* .clone-sliders-col → Tailwind utilities in DesignMethodPanel.jsx (the conditional
.clone-cat--chips / .clone-cat--select variants + their media query stay in CSS). */
.clone-cat--chips { grid-column: 1 / -1; }
.clone-cat--select { min-width: 0; }
@media (max-width: 1100px) {
.clone-cat--select { grid-column: 1 / -1; }
}
/* Insertable tag chips: one scrollable line instead of three wrapped rows. */
.clone-split-grid .tags-container {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: thin;
padding-bottom: 2px;
}
.clone-split-grid .tags-container::-webkit-scrollbar { height: 5px; }
.clone-split-grid .tags-container::-webkit-scrollbar-thumb {
background: var(--chrome-border-strong);
border-radius: 3px;
}
/* Tighter rhythm for the design stack. */
/* .describe-voice-block margin + .clone-slider-kicker → Tailwind utilities in DesignMethodPanel.jsx. */
.personality-strip { row-gap: 4px; }
/* Production-overrides column */
/* .clone-prod-col / .clone-prod-check → Tailwind utilities in ActionBar.jsx. */
.clone-icon-inline { vertical-align: middle; margin-right: 4px; }
/* Footer buttons */
.clone-footer-cta { margin-top: 6px; }
/* Demo coach-mark — shown once above the textarea on first visit to the
bundled demo profile. Auto-dismisses when the user types. */
/* .clone-coachmark(+__icon,+__msg) → Tailwind utilities in ScriptPanel.jsx
(the __close button + its :hover stay in CSS). */
.clone-coachmark__close {
border: 0;
background: transparent;
color: var(--color-fg-muted, #928374);
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 0 4px;
border-radius: 4px;
}
.clone-coachmark__close:hover {
background: rgba(255, 255, 255, 0.08);
color: var(--color-fg, currentColor);
}
/* "Hear demo" chip — shown under the play button when no TTS engine is
ready and the user is on the demo profile. Tells them they're hearing a
pre-rendered sample, not a live synthesis. */
/* .clone-hear-demo-chip → Tailwind utilities in ActionBar.jsx. */
/* #526: voice-design seed control — show + pin + re-roll.
.design-seed / .design-seed__row / .design-seed__keep → Tailwind utilities in
AudioMethodPanel.jsx (.design-seed__input stays: it overrides the unlayered
.input-base width, which a utility can't beat). */
.design-seed__input {
width: 9rem;
flex: 0 0 auto;
}
+5 -6
View File
@@ -13,7 +13,6 @@ import ScriptPanel from '../components/clone/ScriptPanel';
import AudioMethodPanel from '../components/clone/AudioMethodPanel';
import DesignMethodPanel from '../components/clone/DesignMethodPanel';
import ActionBar from '../components/clone/ActionBar';
import './CloneDesignTab.css';
export default function CloneDesignTab(props) {
const {
@@ -295,7 +294,7 @@ export default function CloneDesignTab(props) {
return (
<div className="flex-1 flex flex-col min-h-0 min-w-0">
<div className="clone-split-grid">
<div className="flex-1 flex flex-col gap-[6px] min-h-0 overflow-y-auto">
{/* ═══ SCRIPT — what should it say ═══ */}
<ScriptPanel
t={t}
@@ -316,10 +315,10 @@ export default function CloneDesignTab(props) {
/>
{/* ═══ VOICE — who says it ═══ */}
<div className="studio-column">
<div className="studio-panel">
<div className="label-row label-row--spread">
<span className="label-row label-row--flush">
<div className="flex flex-col gap-[6px] flex-none min-h-0 relative z-[1]">
<div className="flex flex-col min-h-0 overflow-auto bg-[var(--chrome-bg)] border border-[var(--chrome-border)] rounded-none py-[10px] px-[12px] max-[800px]:px-[10px] max-[600px]:px-[6px] max-[600px]:py-[8px]">
<div className="label-row justify-between">
<span className="label-row mb-0">
<Volume2 className="label-icon" size={14} />{' '}
{t('clone.voice_kicker', { defaultValue: 'Voice' })}
</span>