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:
co-authored by
mergetest
Claude Opus 4.8
parent
b940eb460f
commit
c931d8f4f8
@@ -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')}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user