feat(ui): convert VoiceGallery + CloneDesign page CSS to Tailwind utilities (partial) (#792)

Mechanically convert the safe, low-risk page CSS of the Voice Gallery and
Clone/Design pages to Tailwind v4 utilities, removing each converted rule from
the page CSS so there is a single source of truth.

Scope (conservative, partial — complex rules left as CSS):
- VoiceGallery.css: pure flex/grid containers + text/ellipsis spans converted
  (voice-gallery, gallery-header, header-top, gallery-sub, gallery-search,
  search-row base, search-results-panel, panel-header, results-list, result-*,
  content-header, content-title base, count-badge, voice-list base, voice-info/
  name/meta/actions base, arch-head/title, archetype-name/sub/chips, arch-foot,
  archetype-section, load-more, import-explainer, community-explainer,
  submit-actions).
- CloneDesignTab.css: studio-def-col, clone-script-wrap, clone-insert-backdrop,
  clone-prod-col/check, clone-hear-demo-chip, clone-drop-row, clone-drop-filename,
  grid-2--indent, describe-voice-block margin / hint / feedback, starting-points
  (+__label), clone-sliders-col, clone-slider-kicker, identity-line__kicker/recipe,
  design-seed(+__row/__keep), clone-coachmark(+__icon/__msg), clone-profile-banner
  (+__label), clone-save-profile(+__row base).

Rules followed:
- No reliance on Tailwind preflight: borders use arbitrary [border:...]; only
  @theme tokens map to named utilities (bg-bg-elev-2, text-fg, text-success,
  rounded-lg/md), everything else (--chrome-*/--space-*/--text-* + literal px)
  stays exact via arbitrary var()/px.
- Left in CSS: keyframes, ::before/::after, :has/combinators, masks, scrollbar
  pseudo, !important, media queries, hover/border-heavy buttons & chips, and any
  rule overriding an unlayered base (.file-drag/.input-base/.studio-panel) that a
  @layer utility can't beat.
- Retained class names that anchor kept descendant selectors
  (search-row, clone-save-profile__row).

Verified: oxlint 0, oxfmt --check clean, vite build OK, 641 vitest pass,
bun install --frozen-lockfile unchanged.

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-06-30 18:15:14 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent 3c17d19b7e
commit d88e57675d
12 changed files with 136 additions and 353 deletions
+6 -4
View File
@@ -152,8 +152,8 @@ export default function ActionBar({
placeholder={t('clone.auto')}
/>
</div>
<div className="clone-prod-col">
<label className="clone-prod-check">
<div className="flex flex-col gap-[6px]">
<label className="text-[0.75rem] flex items-center gap-[6px] cursor-pointer">
<input
type="checkbox"
checked={denoise}
@@ -161,7 +161,7 @@ export default function ActionBar({
/>{' '}
{t('clone.denoise')}
</label>
<label className="clone-prod-check">
<label className="text-[0.75rem] flex items-center gap-[6px] cursor-pointer">
<input
type="checkbox"
checked={postprocess}
@@ -219,7 +219,9 @@ export default function ActionBar({
>
{demoAudioPlaying ? t('demo.stop_demo') : t('demo.hear_demo')}
</Button>
<div className="clone-hear-demo-chip">{t('demo.prerendered_chip')}</div>
<div className="mt-[6px] px-[8px] py-[4px] text-[10px] text-center text-fg-muted bg-white/[0.03] rounded-md [border:1px_dashed_rgba(255,255,255,0.08)]">
{t('demo.prerendered_chip')}
</div>
<audio
ref={demoAudioRef}
onEnded={() => {
@@ -34,7 +34,7 @@ export default function AudioMethodPanel({
{/* Saved voices now live in the right-side WorkspaceVoices panel. */}
{!selectedProfile && (
<div className="clone-drop-row">
<div className="flex gap-[8px] items-stretch">
<input
type="file"
accept="audio/*,.mp3,.wav,.m4a,.flac,.ogg"
@@ -69,11 +69,7 @@ export default function AudioMethodPanel({
>
<UploadCloud color="#a89984" size={18} />
<p>
{refAudio ? (
<span className="clone-drop-filename">{refAudio.name}</span>
) : (
t('clone.drop_audio')
)}
{refAudio ? <span className="text-fg">{refAudio.name}</span> : t('clone.drop_audio')}
</p>
</label>
@@ -88,8 +84,8 @@ export default function AudioMethodPanel({
)}
{selectedProfile && (
<div className="clone-profile-banner">
<span className="clone-profile-banner__label">
<div className="p-[var(--space-4)] bg-[rgba(142,192,124,0.08)] [border:1px_solid_rgba(142,192,124,0.2)] rounded-lg text-[var(--text-md)] mb-[var(--space-4)] flex items-center gap-[var(--space-4)]">
<span className="text-success flex-1">
{t('clone.using_profile', {
name: profiles.find((p) => p.id === selectedProfile)?.name,
})}
@@ -105,7 +101,7 @@ export default function AudioMethodPanel({
</div>
)}
<div className="grid-2 grid-2--indent">
<div className="grid-2 mt-[6px]">
<div>
<div className="label-row">{t('clone.transcript')}</div>
<input
@@ -131,9 +127,9 @@ export default function AudioMethodPanel({
{/* #526: voice-design seed — show + pin + re-roll so tweaks can
stay on the same base timbre. Design mode only. */}
{defineMethod === 'design' && (
<div className="design-seed">
<div className="mt-[var(--space-3)]">
<div className="label-row">{t('clone.seed_label')}</div>
<div className="design-seed__row">
<div className="flex gap-[var(--space-3)] items-center">
<input
type="number"
className="input-base design-seed__input"
@@ -164,7 +160,7 @@ export default function AudioMethodPanel({
>
{t('clone.seed_reroll')}
</Button>
<label className="design-seed__keep">
<label className="inline-flex items-center gap-[6px] text-[0.85em] text-fg-muted cursor-pointer select-none whitespace-nowrap">
<input
type="checkbox"
checked={keepSeed}
@@ -178,7 +174,7 @@ export default function AudioMethodPanel({
{/* Save as profile */}
{refAudio && !selectedProfile && (
<div className="clone-save-profile">
<div className="mt-[var(--space-4)]">
{!showSaveProfile ? (
<Button
variant="subtle"
@@ -189,7 +185,7 @@ export default function AudioMethodPanel({
{t('clone.save_as_profile')}
</Button>
) : (
<div className="clone-save-profile__row">
<div className="clone-save-profile__row flex gap-[var(--space-3)] items-center">
<Input
size="sm"
placeholder={t('clone.profile_name')}
@@ -38,7 +38,7 @@ export default function DesignMethodPanel({
<div>
{/* ── Describe your voice (#317) — free text drives the controls.
The placeholder explains itself; no extra header (10x §1.2). ── */}
<div className="describe-voice-block">
<div className="mb-[8px]">
<textarea
className="input-base describe-voice-area"
rows={2}
@@ -47,24 +47,26 @@ export default function DesignMethodPanel({
onChange={onDescribeChange}
/>
{describeText.trim() && !describeMatchedAny && (
<div className="describe-voice-feedback" role="status">
<div className="text-[0.65rem] text-[#d79921] mb-[2px]" role="status">
{t('clone.describe_no_match')}
</div>
)}
{describeMatchedAny && describeUnmatched.length > 0 && (
<div className="describe-voice-feedback" role="status">
<div className="text-[0.65rem] text-[#d79921] mb-[2px]" role="status">
{t('clone.describe_unmatched', { items: describeUnmatched.join(', ') })}
</div>
)}
<div className="describe-voice-hint">{t('clone.describe_hint')}</div>
<div className="text-[0.62rem] text-[var(--chrome-fg-muted)]">
{t('clone.describe_hint')}
</div>
</div>
{/* ONE preset system (10x §1.3): personalities + the old PROMPT
presets share a single scrollable "Starting points" lane —
both set vdStates + instruct; two widgets for one slot was
the confusion. */}
<div className="starting-points">
<div className="starting-points__label">
<div className="mt-[8px] mr-0 mb-[12px] ml-0">
<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">
@@ -112,14 +114,16 @@ export default function DesignMethodPanel({
onClick={() => setIdentityOpen((o) => !o)}
aria-expanded={identityOpen}
>
<span className="identity-line__kicker">
<span className="font-[var(--chrome-font-mono)] text-[0.62rem] uppercase tracking-[0.06em] text-[var(--chrome-fg-muted)] flex-none">
{t('clone.identity', { defaultValue: 'Identity' })}
</span>
<span className="identity-line__recipe">{identityRecipe}</span>
<span className="flex-1 min-w-0 text-[0.74rem] text-[var(--chrome-fg)] truncate">
{identityRecipe}
</span>
{identityOpen ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</button>
{identityOpen && (
<div className="clone-sliders-col">
<div className="grid grid-cols-[1fr_1fr] gap-x-[12px] gap-y-[8px]">
{Object.entries(CATEGORIES).map(([key, options]) => {
const many = options.length > 6;
const optLabel = (val) => {
@@ -134,7 +138,7 @@ export default function DesignMethodPanel({
>
<div className="label-row label-row--sm">
{t(`clone.cat_${key}`)}
<span className="clone-slider-kicker">
<span className="ml-[6px] text-[0.58rem] text-[var(--chrome-fg-muted)] font-medium">
{vdStates[key] === 'Auto'
? t('clone.auto_kicker')
: `· ${optLabel(vdStates[key])}`}
@@ -195,7 +199,7 @@ export default function DesignMethodPanel({
{/* Save the current design as a reusable profile (0005): the
backend renders a deterministic identity sample (seed 42)
and stores the slider picks for later re-editing. */}
<div className="clone-save-profile">
<div className="mt-[var(--space-4)]">
{!showSaveProfile ? (
<Button
variant="subtle"
@@ -206,7 +210,7 @@ export default function DesignMethodPanel({
{t('clone.save_design_as_profile', { defaultValue: 'Save design as profile' })}
</Button>
) : (
<div className="clone-save-profile__row">
<div className="clone-save-profile__row flex gap-[var(--space-3)] items-center">
<Input
size="sm"
placeholder={t('clone.profile_name')}
@@ -35,9 +35,12 @@ export default function ScriptPanel({
<DemoPresetGrid presets={demoPresets} onUse={applyDemoPreset} />
)}
{showDemoCoachmark && defineMethod === 'audio' && selectedProfile === DEMO_PROFILE_ID && (
<div className="clone-coachmark" role="note">
<span className="clone-coachmark__icon">💡</span>
<span className="clone-coachmark__msg">{t('demo.clone_coachmark')}</span>
<div
className="flex items-center gap-[8px] px-[10px] py-[6px] mb-[8px] rounded-[8px] bg-[rgba(243,165,182,0.08)] [border:1px_solid_rgba(243,165,182,0.25)] text-[11px] text-fg"
role="note"
>
<span className="text-[14px] leading-none">💡</span>
<span className="flex-1">{t('demo.clone_coachmark')}</span>
<button
type="button"
className="clone-coachmark__close"
@@ -48,7 +51,7 @@ export default function ScriptPanel({
</button>
</div>
)}
<div className="clone-script-wrap">
<div className="relative flex-1 flex flex-col min-h-0">
<textarea
ref={textAreaRef}
className="input-base clone-text-area"
@@ -77,7 +80,7 @@ export default function ScriptPanel({
<ChevronDown size={10} />
</button>
{insertOpen && (
<div className="clone-insert-backdrop" onClick={() => setInsertOpen(false)} />
<div className="fixed inset-0 z-[19]" onClick={() => setInsertOpen(false)} />
)}
{insertOpen && (
<div className="clone-insert-pop" role="menu">
@@ -37,11 +37,17 @@ export default function ArchetypeCard({
className={`archetype-card ${viewMode} ${isPlaying ? 'playing' : ''}`}
style={{ '--card-accent': color }}
>
<div className="arch-head">
<div className="flex items-center gap-[10px]">
<ArchetypeAvatar item={a} />
<div className="arch-title">
<div className="archetype-name">{a.name}</div>
{sub && <div className="archetype-sub">{sub}</div>}
<div className="flex-1 min-w-0">
<div className="text-[0.84rem] font-semibold text-[var(--text-primary)] truncate">
{a.name}
</div>
{sub && (
<div className="text-[0.68rem] text-[var(--text-secondary)] mt-[2px] truncate">
{sub}
</div>
)}
</div>
<button
className={`fav-btn ${isFavorite ? 'on' : ''}`}
@@ -54,7 +60,7 @@ export default function ArchetypeCard({
{/* Always render the chip row (even when empty) so every card shares the
same height and the action rows align across the grid. */}
<div className="archetype-chips">
<div className="flex flex-wrap items-center gap-[5px] min-h-[21px]">
{accentLabel && (
<span className="facet-chip with-flag">
<AccentFlag accent={a.facets.accent} lang={a.language} size={14} />
@@ -68,7 +74,7 @@ export default function ArchetypeCard({
)}
</div>
<div className="arch-foot">
<div className="flex items-center gap-[6px] mt-auto">
<button
className="preview-btn"
onClick={() => onPreview(a)}
@@ -206,9 +206,9 @@ export default function ArchetypesZone({
</div>
{showFeatured && (
<section className="archetype-section">
<div className="content-header">
<div className="content-title">
<section className="mb-[14px]">
<div className="flex justify-between items-center pb-[8px] shrink-0">
<div className="text-[0.85rem] font-medium">
{t('archetypes.featured', { defaultValue: 'Featured' })}
</div>
</div>
@@ -220,11 +220,13 @@ export default function ArchetypesZone({
</section>
)}
<section className="archetype-section">
<div className="content-header">
<div className="content-title">
<section className="mb-[14px]">
<div className="flex justify-between items-center pb-[8px] shrink-0">
<div className="text-[0.85rem] font-medium">
{t('archetypes.browse_all', { defaultValue: 'Browse all' })}
<span className="count-badge">{total}</span>
<span className="ml-[6px] px-[7px] py-[1px] rounded-[10px] bg-bg-elev-2 text-[var(--text-secondary)] text-[0.65rem] font-normal">
{total}
</span>
</div>
</div>
{browseQ.isLoading ? (
@@ -244,7 +246,7 @@ export default function ArchetypesZone({
</div>
)}
{offset + BROWSE_PAGE < total && !favOnly && (
<div className="load-more">
<div className="flex justify-center py-[12px]">
<Button
variant="ghost"
onClick={() => setOffset(offset + BROWSE_PAGE)}
@@ -31,14 +31,14 @@ export default function CommunityZone({
return (
<div className="gallery-content gallery-scroll">
<div className="import-explainer community-explainer">
<div className="shrink-0 px-[10px] py-[8px] mb-[8px] bg-bg-elev-2 rounded-[8px] text-[0.72rem] text-[var(--text-secondary)] leading-[1.4] flex items-center justify-between gap-[12px] flex-wrap">
<span>
{t('gallery.community_explainer', {
defaultValue:
'Designed presets and recorded voices shared by the community, loaded from the omnivoice-gallery.',
})}
</span>
<div className="submit-actions">
<div className="flex gap-[6px] shrink-0">
<button className="submit-btn" onClick={() => submit('preset')}>
<Send size={13} /> {t('gallery.submit_preset', { defaultValue: 'Submit a preset' })}
</button>
+23 -17
View File
@@ -215,15 +215,15 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
return (
<div className="gallery-content">
<div className="import-explainer">
<div className="shrink-0 px-[10px] py-[8px] mb-[8px] bg-bg-elev-2 rounded-[8px] text-[0.72rem] text-[var(--text-secondary)] leading-[1.4]">
{t('gallery.import_explainer', {
defaultValue:
'Paste a URL you have the rights to (or upload a file), trim the part you need, and save it as a voice. You are responsible for the licensing of anything you import.',
})}
</div>
<div className="gallery-search">
<div className="search-row">
<div className="shrink-0 flex flex-col gap-[10px]">
<div className="search-row flex gap-[6px]">
<Input
placeholder={t('gallery.import_placeholder', {
defaultValue: 'Paste a video/audio URL, or type to search…',
@@ -280,8 +280,8 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
</div>
{results.length > 0 && (
<div className="search-results-panel">
<div className="panel-header">
<div className="shrink-0 bg-bg-elev-2 rounded-[8px] max-h-[180px] overflow-hidden flex flex-col">
<div className="flex justify-between items-center px-[10px] py-[8px] bg-bg-elev-1 text-[0.75rem] font-medium shrink-0">
<span>
{t('gallery.search_results', {
defaultValue: '{{count}} results',
@@ -292,12 +292,14 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
<X size={14} />
</button>
</div>
<div className="results-list">
<div className="overflow-y-auto flex-1">
{results.map((r, i) => (
<div key={i} className="result-row">
<div className="result-info">
<span className="result-title">{r.title}</span>
<span className="result-meta">{r.duration || '?'}s</span>
<div className="flex-1 min-w-0 flex flex-col gap-[2px]">
<span className="text-[0.75rem] truncate">{r.title}</span>
<span className="text-[0.65rem] text-[var(--text-secondary)]">
{r.duration || '?'}s
</span>
</div>
<Button size="sm" onClick={() => handleDownload(r)} disabled={isDownloading}>
<Download size={12} /> {t('gallery.import', { defaultValue: 'Import' })}
@@ -308,10 +310,12 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
</div>
)}
<div className="content-header">
<div className="content-title">
<div className="flex justify-between items-center pb-[8px] shrink-0">
<div className="text-[0.85rem] font-medium">
{t('gallery.my_imports', { defaultValue: 'My Imports' })}
<span className="count-badge">{voices.length}</span>
<span className="ml-[6px] px-[7px] py-[1px] rounded-[10px] bg-bg-elev-2 text-[var(--text-secondary)] text-[0.65rem] font-normal">
{voices.length}
</span>
</div>
</div>
@@ -326,7 +330,7 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
})}
</div>
) : (
<div className="voice-list">
<div className="flex flex-col gap-[4px] overflow-y-auto flex-1 pr-[4px]">
{voices.map((v) => (
<div key={v.id} className="voice-card">
<button className="voice-play" onClick={() => onPlayGallery(v)}>
@@ -338,11 +342,13 @@ export default function ImportsZone({ t, playingId, loadingPreviewId, onPlayGall
<Play size={16} />
)}
</button>
<div className="voice-info">
<span className="voice-name">{v.name}</span>
<span className="voice-meta">{Math.round(v.duration || 0)}s</span>
<div className="flex-1 min-w-0 flex flex-col gap-[1px]">
<span className="text-[0.8rem] font-medium truncate">{v.name}</span>
<span className="flex items-center gap-[3px] text-[0.65rem] text-[var(--text-secondary)]">
{Math.round(v.duration || 0)}s
</span>
</div>
<div className="voice-actions">
<div className="flex gap-[3px]">
<button
className="action-btn"
onClick={() => handleTrimClick(v)}
+26 -147
View File
@@ -14,25 +14,8 @@
}
.clone-profile-delete:hover { opacity: 1; }
.clone-profile-banner {
padding: var(--space-4);
background: rgba(142, 192, 124, 0.08);
border: 1px solid rgba(142, 192, 124, 0.2);
border-radius: var(--radius-lg);
font-size: var(--text-md);
margin-bottom: var(--space-4);
display: flex;
align-items: center;
gap: var(--space-4);
}
.clone-profile-banner__label { color: var(--color-success); flex: 1; }
.clone-save-profile { margin-top: var(--space-4); }
.clone-save-profile__row {
display: flex;
gap: var(--space-3);
align-items: center;
}
/* .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 ────────────────── */
@@ -75,13 +58,7 @@
/* ── Page layout ─────────────────────────────────────────────── */
/* 10x console: scrollable definition content + a pinned action bar. */
.studio-def-col {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0;
}
/* .studio-def-col → Tailwind utilities in CloneDesignTab.jsx. */
.studio-action-bar {
flex: 0 0 auto;
@@ -132,7 +109,7 @@
.studio-action-bar__overrides:hover { color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
/* ── Script editor: ⊕ Insert token popover ───────────────────── */
.clone-script-wrap { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 0; }
/* .clone-script-wrap → Tailwind utilities in ScriptPanel.jsx. */
.clone-insert-btn {
position: absolute;
right: 8px;
@@ -153,7 +130,7 @@
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 { position: fixed; inset: 0; z-index: 19; }
/* .clone-insert-backdrop → Tailwind utilities in ScriptPanel.jsx. */
.clone-insert-pop {
position: absolute;
right: 8px;
@@ -199,36 +176,12 @@
transition: border-color var(--dur-fast);
}
.identity-line:hover { border-color: var(--chrome-border-strong); }
.identity-line__kicker {
font-family: var(--chrome-font-mono, var(--font-mono));
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.06em;
/* 10x P4 contrast: readable kicker — dim (3.91:1 default theme) → muted. */
color: var(--chrome-fg-muted, #a89984);
flex: 0 0 auto;
}
.identity-line__recipe {
flex: 1;
min-width: 0;
font-size: 0.74rem;
color: var(--chrome-fg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* .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 { margin: 8px 0 12px; }
.starting-points__label {
font-family: var(--chrome-font-mono, var(--font-mono));
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.06em;
/* 10x P4 contrast: readable kicker — dim (3.91:1 default theme) → muted. */
color: var(--chrome-fg-muted, #a89984);
margin-bottom: 6px;
}
/* .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;
@@ -300,46 +253,26 @@
.clone-profile-card { position: relative; }
/* File-drop + record-button row */
.clone-drop-row {
display: flex;
gap: 8px;
align-items: stretch;
}
/* .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 { color: var(--color-fg); }
/* .clone-drop-filename / .grid-2--indent → Tailwind utilities in AudioMethodPanel.jsx. */
/* Grid-2 margin */
.grid-2--indent { margin-top: 6px; }
/* "Describe your voice" free-text block (issue #317) */
.describe-voice-block {
margin-bottom: 12px;
}
/* "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;
}
.describe-voice-hint {
font-size: 0.62rem;
/* 10x P4 contrast: readable hint text — dim (3.91:1 default theme) → muted. */
color: var(--chrome-fg-muted);
}
.describe-voice-feedback {
font-size: 0.65rem;
color: #d79921;
margin-bottom: 2px;
}
/* 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 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 12px; /* 10x P4 8-pt audit: 7 → 8 */
}
/* .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) {
@@ -360,29 +293,11 @@
}
/* Tighter rhythm for the design stack. */
.describe-voice-block { margin-bottom: 8px; }
/* .describe-voice-block margin + .clone-slider-kicker → Tailwind utilities in DesignMethodPanel.jsx. */
.personality-strip { row-gap: 4px; }
.clone-slider-kicker {
margin-left: 6px;
font-size: 0.58rem;
/* 10x P4 contrast: shows the selected value — readable label, dim → muted. */
color: var(--chrome-fg-muted);
font-weight: 500;
}
/* Production-overrides column */
.clone-prod-col {
display: flex;
flex-direction: column;
gap: 6px;
}
.clone-prod-check {
font-size: 0.75rem;
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
/* .clone-prod-col / .clone-prod-check → Tailwind utilities in ActionBar.jsx. */
.clone-icon-inline { vertical-align: middle; margin-right: 4px; }
/* Footer buttons */
@@ -390,20 +305,8 @@
/* Demo coach-mark — shown once above the textarea on first visit to the
bundled demo profile. Auto-dismisses when the user types. */
.clone-coachmark {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
margin-bottom: 8px;
border-radius: 8px;
background: rgba(243, 165, 182, 0.08);
border: 1px solid rgba(243, 165, 182, 0.25);
font-size: 11px;
color: var(--color-fg, currentColor);
}
.clone-coachmark__icon { font-size: 14px; line-height: 1; }
.clone-coachmark__msg { flex: 1; }
/* .clone-coachmark(+__icon,+__msg) → Tailwind utilities in ScriptPanel.jsx
(the __close button + its :hover stay in CSS). */
.clone-coachmark__close {
border: 0;
background: transparent;
@@ -422,37 +325,13 @@
/* "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 {
margin-top: 6px;
padding: 4px 8px;
font-size: 10px;
text-align: center;
color: var(--color-fg-muted, #928374);
background: rgba(255, 255, 255, 0.03);
border-radius: 4px;
border: 1px dashed rgba(255, 255, 255, 0.08);
}
/* .clone-hear-demo-chip → Tailwind utilities in ActionBar.jsx. */
/* #526: voice-design seed control — show + pin + re-roll. */
.design-seed {
margin-top: var(--space-3, 12px);
}
.design-seed__row {
display: flex;
gap: var(--space-3, 12px);
align-items: center;
}
/* #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;
}
.design-seed__keep {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.85em;
color: var(--color-fg-muted, #928374);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
+1 -1
View File
@@ -294,7 +294,7 @@ export default function CloneDesignTab(props) {
};
return (
<div className="studio-def-col">
<div className="flex-1 flex flex-col min-h-0 min-w-0">
<div className="clone-split-grid">
{/* ═══ SCRIPT — what should it say ═══ */}
<ScriptPanel
+18 -133
View File
@@ -1,21 +1,4 @@
.voice-gallery {
display: flex;
flex-direction: column;
gap: 12px;
padding: 12px;
height: 100%;
overflow: hidden;
}
.gallery-header {
flex-shrink: 0;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
}
/* .voice-gallery / .gallery-header / .header-top → Tailwind utilities in VoiceGallery.jsx. */
.header-text h2 {
font-size: 1.1rem;
@@ -24,17 +7,7 @@
color: var(--text-primary);
}
.gallery-search {
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.search-row {
display: flex;
gap: 6px;
}
/* .gallery-search / .search-row base → Tailwind utilities in ImportsZone.jsx. */
.search-row .input-wrapper {
flex: 1;
@@ -75,26 +48,7 @@
color: #1b1b1b;
}
.search-results-panel {
flex-shrink: 0;
background: var(--color-bg-elev-2);
border-radius: 8px;
max-height: 180px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background: var(--color-bg-elev-1);
font-size: 0.75rem;
font-weight: 500;
flex-shrink: 0;
}
/* .search-results-panel / .panel-header → Tailwind utilities in ImportsZone.jsx. */
.close-btn {
background: none;
@@ -104,10 +58,7 @@
padding: 2px;
}
.results-list {
overflow-y: auto;
flex: 1;
}
/* .results-list → Tailwind utilities in ImportsZone.jsx. */
.result-row {
display: flex;
@@ -122,25 +73,7 @@
border: none;
}
.result-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.result-title {
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.result-meta {
font-size: 0.65rem;
color: var(--text-secondary);
}
/* .result-info / .result-title / .result-meta → Tailwind utilities in ImportsZone.jsx. */
.gallery-content {
flex: 1;
@@ -150,18 +83,7 @@
overflow: hidden;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 8px;
flex-shrink: 0;
}
.content-title {
font-size: 0.85rem;
font-weight: 500;
}
/* .content-header / .content-title base → Tailwind utilities in ArchetypesZone.jsx + ImportsZone.jsx. */
.content-title .count {
font-weight: 400;
@@ -240,14 +162,8 @@
font-size: 0.8rem;
}
.voice-list {
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
flex: 1;
padding-right: 4px;
}
/* .voice-list base → Tailwind utilities in ImportsZone.jsx (the .grid variant + its
descendant overrides below stay in CSS). */
.voice-list.grid {
display: grid;
@@ -289,34 +205,8 @@
color: #fff;
}
.voice-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.voice-name {
font-size: 0.8rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.voice-meta {
display: flex;
align-items: center;
gap: 3px;
font-size: 0.65rem;
color: var(--text-secondary);
}
.voice-actions {
display: flex;
gap: 3px;
}
/* .voice-info / .voice-name / .voice-meta / .voice-actions base → Tailwind utilities
in ImportsZone.jsx (the .voice-list.grid descendant overrides below stay in CSS). */
.action-btn {
display: flex;
@@ -365,7 +255,7 @@
}
/* ── Voice Gallery: zone toggle + header ──────────────────────────────── */
.gallery-sub { margin: 2px 0 0; font-size: 0.72rem; color: var(--text-secondary); }
/* .gallery-sub → Tailwind utilities in VoiceGallery.jsx. */
.zone-toggle { display: inline-flex; gap: 2px; background: var(--chrome-hover-bg); border: 1px solid var(--chrome-border); border-radius: 8px; padding: 2px; }
.zone-tab { display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px; border: none; background: transparent; color: var(--chrome-fg-muted); border-radius: 6px; font-size: 0.75rem; font-weight: 500; cursor: pointer; transition: background var(--dur-fast), color var(--dur-fast); }
.zone-tab:hover { color: var(--chrome-fg); }
@@ -450,8 +340,7 @@
/* ── Archetype sections + grid ────────────────────────────────────────── */
.gallery-content.gallery-scroll { overflow-y: auto; }
.archetype-section { margin-bottom: 14px; }
.count-badge { margin-left: 6px; padding: 1px 7px; border-radius: 10px; background: var(--color-bg-elev-2); color: var(--text-secondary); font-size: 0.65rem; font-weight: 400; }
/* .archetype-section / .count-badge → Tailwind utilities in ArchetypesZone.jsx + ImportsZone.jsx. */
.archetype-grid.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); gap: 10px; }
.archetype-grid.list { display: flex; flex-direction: column; gap: 6px; }
@@ -471,14 +360,12 @@
.archetype-card.playing { border-color: var(--card-accent, var(--accent)); box-shadow: 0 0 0 1px var(--card-accent, var(--accent)), 0 6px 22px rgba(0, 0, 0, 0.4); }
/* Header: avatar tile + title + favorite */
.arch-head { display: flex; align-items: center; gap: 10px; }
/* .arch-head → Tailwind utilities in ArchetypeCard.jsx. */
.arch-avatar { position: relative; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border: 1px solid transparent; border-radius: 11px; }
.arch-avatar-flag { position: absolute; right: -4px; bottom: -4px; display: flex; line-height: 0; border-radius: 3px; overflow: hidden; box-shadow: 0 0 0 2px var(--bg-tertiary, #1d2021); }
.accent-flag { display: block; border-radius: 2px; }
.flag-globe { color: var(--text-secondary); }
.arch-title { flex: 1; min-width: 0; }
.archetype-name { font-size: 0.84rem; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.archetype-sub { font-size: 0.68rem; color: var(--text-secondary); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* .arch-title / .archetype-name / .archetype-sub → Tailwind utilities in ArchetypeCard.jsx. */
.fav-btn { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; background: transparent; color: var(--text-secondary); border-radius: 7px; cursor: pointer; transition: color 0.15s ease, background 0.15s ease; }
.fav-btn:hover { color: #fabd2f; background: rgba(255, 255, 255, 0.05); }
@@ -488,12 +375,12 @@
voice has no accent/whisper chip) with a reserved min-height so every card
shares the same vertical rhythm and the action rows line up across the grid
instead of floating at ragged heights. */
.archetype-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-height: 21px; }
/* .archetype-chips → Tailwind utilities in ArchetypeCard.jsx. */
.facet-chip { display: inline-flex; align-items: center; gap: 5px; padding: 2px 8px; border-radius: 7px; background: rgba(255, 255, 255, 0.05); color: var(--text-secondary); font-size: 0.64rem; line-height: 1.6; }
.facet-chip.with-flag { padding-left: 5px; }
/* Footer actions */
.arch-foot { display: flex; align-items: center; gap: 6px; margin-top: auto; }
/* .arch-foot → Tailwind utilities in ArchetypeCard.jsx. */
.preview-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 11px; border: 1px solid rgba(255, 255, 255, 0.09); background: rgba(255, 255, 255, 0.03); color: var(--text-primary); border-radius: 8px; font-size: 0.7rem; cursor: pointer; transition: border-color 0.15s ease, color 0.15s ease; }
.preview-btn:hover { border-color: var(--card-accent, var(--accent)); color: var(--card-accent, var(--accent)); }
/* Primary action — tonal by default (a low-alpha wash of the card accent with
@@ -534,9 +421,7 @@
/* Category chips: lucide icon + label */
.use-case-chips .category-chip svg { flex-shrink: 0; }
.load-more { display: flex; justify-content: center; padding: 12px 0; }
.import-explainer { flex-shrink: 0; padding: 8px 10px; margin-bottom: 8px; background: var(--color-bg-elev-2); border-radius: 8px; font-size: 0.72rem; color: var(--text-secondary); line-height: 1.4; }
.community-explainer { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.submit-actions { display: flex; gap: 6px; flex-shrink: 0; }
/* .load-more / .import-explainer / .community-explainer / .submit-actions
→ Tailwind utilities in ArchetypesZone.jsx, CommunityZone.jsx + ImportsZone.jsx. */
.submit-btn { display: inline-flex; align-items: center; gap: 5px; padding: 6px 10px; border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.03); color: var(--text-primary); border-radius: 8px; font-size: 0.7rem; cursor: pointer; transition: border-color 0.15s ease, color 0.15s ease; }
.submit-btn:hover { border-color: var(--accent); color: var(--accent); }
+4 -4
View File
@@ -147,12 +147,12 @@ export default function VoiceGallery() {
};
return (
<div className="voice-gallery">
<div className="gallery-header">
<div className="header-top">
<div className="flex flex-col gap-[12px] p-[12px] h-full overflow-hidden">
<div className="shrink-0">
<div className="flex justify-between items-center">
<div className="header-text">
<h2>{t('gallery.title', { defaultValue: 'OmniVoice Gallery' })}</h2>
<p className="gallery-sub">
<p className="mt-[2px] mr-0 mb-0 ml-0 text-[0.72rem] text-[var(--text-secondary)]">
{t('gallery.subtitle', {
defaultValue: 'Hundreds of ready-made designed voices — pick one and go.',
})}