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:
co-authored by
mergetest
Claude Opus 4.8
parent
3c17d19b7e
commit
d88e57675d
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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); }
|
||||
@@ -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.',
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user