fix(frontend): increase translation config legibility and integrate templates in glossary selector
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m31s

This commit is contained in:
2026-05-31 11:18:57 +02:00
parent 3f635e809e
commit 58d9d8a74c
3 changed files with 168 additions and 142 deletions

View File

@@ -7,6 +7,7 @@ import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
import { Switch } from '@/components/ui/switch';
import { SUPPORTED_LANGUAGES } from '../glossaries/types';
import { languages as API_LANGUAGES } from '@/lib/api';
interface GlossaryOption {
id: string;
@@ -45,7 +46,6 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
const [importingId, setImportingId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [showTemplates, setShowTemplates] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [filterByLang, setFilterByLang] = useState(sourceLang !== 'auto');
@@ -225,8 +225,14 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
}
};
const sourceFlag = SUPPORTED_LANGUAGES.find(l => l.code === sourceLang)?.flag ?? '';
const targetFlag = SUPPORTED_LANGUAGES.find(l => l.code === targetLang)?.flag ?? '';
const getFlag = useCallback((code: string) => {
return API_LANGUAGES.find(l => l.code === code)?.flag ??
SUPPORTED_LANGUAGES.find(l => l.code === code)?.flag ??
code.toUpperCase();
}, []);
const sourceFlag = useMemo(() => sourceLang === 'auto' ? '' : getFlag(sourceLang), [sourceLang, getFlag]);
const targetFlag = useMemo(() => getFlag(targetLang), [targetLang, getFlag]);
const filteredGlossaries = useMemo(() => {
if (!filterByLang || sourceLang === 'auto') {
@@ -235,7 +241,15 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
return glossaries.filter(g => g.source_language === sourceLang);
}, [glossaries, filterByLang, sourceLang]);
const filteredTemplates = useMemo(() => {
if (!filterByLang || sourceLang === 'auto') {
return templates;
}
return templates.filter(t => t.source_lang === sourceLang);
}, [templates, filterByLang, sourceLang]);
const selected = glossaries.find(g => g.id === glossaryId);
return (
<div
className="bg-brand-muted/30 dark:bg-white/[0.02] border border-black/[0.03] dark:border-white/[0.03] p-4 rounded-xl space-y-3 text-left"
@@ -243,11 +257,11 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<BookOpen size={12} className="text-brand-accent shrink-0" />
<span className="text-[9px] font-black uppercase tracking-[0.1em] text-brand-dark dark:text-white truncate">
<BookOpen size={14} className="text-brand-accent shrink-0" />
<span className="text-xs font-black uppercase tracking-[0.1em] text-brand-dark dark:text-white truncate">
{t('translate.glossary.title') || 'Glossaire & Terminologie'}
</span>
<span className="text-[7.5px] text-brand-dark/40 dark:text-white/40 shrink-0 font-medium font-mono">
<span className="text-[10px] text-brand-dark/40 dark:text-white/40 shrink-0 font-medium font-mono">
({sourceFlag || 'AUTO'}{targetFlag})
</span>
</div>
@@ -282,13 +296,13 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{mode === 'classic' ? (
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center">
<span className="text-[7.5px] font-black uppercase opacity-45 block">
<span className="text-xs font-black uppercase opacity-45 block">
Moteur neutre sans glossaire (IA uniquement)
</span>
</div>
) : !isPro ? (
<div className="p-2.5 rounded-lg bg-brand-dark/5 dark:bg-white/5 text-center">
<p className="text-[7.5px] text-brand-dark/50 dark:text-white/45 leading-relaxed font-light">
<p className="text-xs text-brand-dark/50 dark:text-white/45 leading-relaxed font-light">
{t('translate.glossary.proOnly') || 'Passez Pro pour appliquer vos glossaires terminologiques.'}
</p>
</div>
@@ -302,44 +316,44 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
disabled={disabled}
onClick={() => setIsOpen(!isOpen)}
className={cn(
"w-full bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 py-2 px-3 rounded-lg flex items-center justify-between shadow-sm transition-all",
"w-full bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 hover:border-black/10 dark:hover:border-white/10 py-2.5 px-3 rounded-lg flex items-center justify-between shadow-sm transition-all cursor-pointer",
isOpen && "border-brand-accent dark:border-brand-accent",
disabled && "opacity-50 cursor-not-allowed"
)}
>
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-[9px] font-black text-brand-dark dark:text-white tracking-tight block truncate">
<span className="text-xs font-bold text-brand-dark dark:text-white tracking-tight block truncate">
{selected ? selected.name : (isLoading ? "Chargement..." : "Sélectionner un glossaire...")}
</span>
<span className="text-[7px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
<span className="text-[10px] font-extrabold uppercase tracking-wider text-brand-dark/40 dark:text-white/40 block mt-0.5">
{selected
? `${SUPPORTED_LANGUAGES.find(l => l.code === selected.source_language)?.flag || '🌐'}${targetFlag}${selected.terms_count} termes`
: (filteredGlossaries.length > 0 ? "Aucun glossaire sélectionné" : "Aucun glossaire disponible")
? `${getFlag(selected.source_language)}${targetFlag}${selected.terms_count} termes`
: (filteredGlossaries.length > 0 || filteredTemplates.length > 0 ? "Sélectionnez un glossaire" : "Aucun glossaire disponible")
}
</span>
</div>
<ChevronDown size={11} className={cn("text-brand-dark/30 dark:text-white/30 shrink-0 transition-transform duration-200", isOpen && "rotate-180")} />
<ChevronDown size={14} className={cn("text-brand-dark/30 dark:text-white/30 shrink-0 transition-transform duration-200", isOpen && "rotate-180")} />
</button>
{/* Error message */}
{error && (
<p className="text-[7.5px] text-red-500 pl-1 mt-1 font-medium">{error}</p>
<p className="text-xs text-red-500 pl-1 mt-1 font-medium">{error}</p>
)}
{/* Selector Dropdown list */}
{isOpen && !disabled && (
<div className="absolute top-[102%] left-0 right-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-1.5 z-40 max-h-48 overflow-y-auto animate-fade-in">
<div className="absolute top-[102%] left-0 right-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-1.5 z-40 max-h-64 overflow-y-auto animate-fade-in animate-in fade-in zoom-in-95 duration-100">
{/* Filter toggle header */}
{sourceLang !== 'auto' && glossaries.length > 0 && (
<div className="px-2 py-1 border-b border-black/[0.03] dark:border-white/[0.03] flex justify-between items-center mb-1">
<span className="text-[8px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
{sourceLang !== 'auto' && (glossaries.length > 0 || templates.length > 0) && (
<div className="px-2 py-1.5 border-b border-black/[0.03] dark:border-white/[0.03] flex justify-between items-center mb-1">
<span className="text-[10px] font-black text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
Filtrer par langue ({sourceFlag})
</span>
<button
type="button"
onClick={() => setFilterByLang(!filterByLang)}
className={cn(
"text-[8px] font-bold px-1.5 py-0.5 rounded transition-colors",
"text-[10px] font-bold px-2 py-0.5 rounded transition-colors cursor-pointer",
filterByLang ? "bg-brand-accent/10 text-brand-accent" : "bg-brand-dark/10 dark:bg-white/10 text-brand-dark/50 dark:text-white/50"
)}
>
@@ -348,53 +362,118 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
</div>
)}
{filteredGlossaries.length > 0 ? (
filteredGlossaries.map(g => {
const flag = SUPPORTED_LANGUAGES.find(l => l.code === g.source_language)?.flag ?? '';
const isSelected = g.id === glossaryId;
return (
<button
key={g.id}
type="button"
onClick={() => {
onChange(isSelected ? null : g.id);
setIsOpen(false);
}}
className={cn(
"w-full text-left px-3 py-2 rounded-lg transition-colors flex items-center justify-between",
isSelected
? 'bg-brand-accent/5 text-brand-accent'
: 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70'
)}
>
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-[9px] font-black block leading-none truncate">{g.name}</span>
<span className="text-[7.5px] uppercase tracking-wider text-brand-dark/40 dark:text-white/40 font-bold block mt-1">
{flag || '🌐'} {targetFlag} {g.terms_count} termes
</span>
</div>
{isSelected && <Check size={10} className="text-brand-accent shrink-0" />}
</button>
);
})
) : (
{/* Custom Glossaries Section */}
{filteredGlossaries.length > 0 && (
<div className="mb-2">
<div className="px-2 py-1 text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest">
Mes Glossaires
</div>
{filteredGlossaries.map(g => {
const flag = getFlag(g.source_language);
const isSelected = g.id === glossaryId;
return (
<button
key={g.id}
type="button"
onClick={() => {
onChange(isSelected ? null : g.id);
setIsOpen(false);
}}
className={cn(
"w-full text-left px-2.5 py-2 rounded-lg transition-colors flex items-center justify-between cursor-pointer",
isSelected
? 'bg-brand-accent/5 text-brand-accent'
: 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70'
)}
>
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-xs font-bold block leading-none truncate">{g.name}</span>
<span className="text-[10px] uppercase tracking-wider text-brand-dark/40 dark:text-white/45 font-bold block mt-1">
{flag} {targetFlag} {g.terms_count} termes
</span>
</div>
{isSelected && <Check size={12} className="text-brand-accent shrink-0" />}
</button>
);
})}
</div>
)}
{/* Templates Section */}
{filteredTemplates.length > 0 && (
<div>
<div className="px-2 py-1 text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest border-t border-black/[0.03] dark:border-white/[0.03] pt-2 mt-1">
Modèles disponibles
</div>
<div className="grid grid-cols-1 gap-0.5">
{filteredTemplates.map(tmpl => {
const isImporting = importingId === tmpl.id;
const existingGlossary = glossaries.find(
g => g.name.toLowerCase().includes(tmpl.name.toLowerCase().split('/')[0].trim())
);
const isAlreadySelected = existingGlossary?.id === glossaryId;
const flag = getFlag(tmpl.source_lang);
const tFlag = getFlag(tmpl.target_lang);
return (
<button
key={tmpl.id}
type="button"
disabled={isImporting || disabled}
onClick={() => {
if (existingGlossary) {
onChange(isAlreadySelected ? null : existingGlossary.id);
setIsOpen(false);
} else {
handleImportTemplate(tmpl);
}
}}
className={cn(
"w-full text-left px-2.5 py-2 rounded-lg transition-colors flex items-center justify-between cursor-pointer",
isAlreadySelected
? 'bg-brand-accent/5 text-brand-accent'
: 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70'
)}
>
<div className="text-left min-w-0 flex-1 pr-2">
<span className="text-xs font-bold block leading-none truncate">
{isImporting ? 'Importation...' : tmpl.name.split('/')[0].trim()}
</span>
<span className="text-[10px] uppercase tracking-wider text-brand-dark/40 dark:text-white/45 font-bold block mt-1">
{flag} {tFlag} {tmpl.terms_count} termes {existingGlossary ? '(Importé)' : ''}
</span>
</div>
{isImporting ? (
<Loader2 size={12} className="animate-spin text-brand-accent shrink-0" />
) : isAlreadySelected ? (
<Check size={12} className="text-brand-accent shrink-0" />
) : null}
</button>
);
})}
</div>
</div>
)}
{/* Empty State */}
{filteredGlossaries.length === 0 && filteredTemplates.length === 0 && (
<div className="px-2.5 py-4 text-center">
<p className="text-[9px] text-brand-dark/45 dark:text-white/45 italic mb-3">
Aucun glossaire pour la langue {sourceFlag || sourceLang.toUpperCase()}.
<p className="text-xs text-brand-dark/45 dark:text-white/45 italic mb-3">
Aucun glossaire ni modèle disponible pour la langue {sourceFlag || sourceLang.toUpperCase()}.
</p>
<div className="flex flex-col gap-1.5">
{filterByLang && glossaries.length > 0 && (
{filterByLang && (glossaries.length > 0 || templates.length > 0) && (
<button
type="button"
onClick={() => setFilterByLang(false)}
className="w-full py-1.5 px-2 bg-brand-muted dark:bg-white/5 hover:bg-brand-muted/70 text-brand-dark dark:text-white rounded-lg text-[8px] font-bold uppercase tracking-wider transition-colors"
className="w-full py-2 px-2 bg-brand-muted dark:bg-white/5 hover:bg-brand-muted/70 text-brand-dark dark:text-white rounded-lg text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer"
>
Afficher tous les glossaires
</button>
)}
<a
href="/dashboard/glossaries"
className="w-full py-1.5 px-2 bg-brand-dark dark:bg-white text-white dark:text-brand-dark hover:opacity-90 rounded-lg text-[8px] font-bold uppercase tracking-wider block text-center transition-opacity"
className="w-full py-2 px-2 bg-brand-dark dark:bg-white text-white dark:text-brand-dark hover:opacity-90 rounded-lg text-xs font-bold uppercase tracking-wider block text-center transition-opacity cursor-pointer"
>
Créer un glossaire
</a>
@@ -409,43 +488,43 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{selected && (
<div className="bg-white/80 dark:bg-[#1a1a1a]/40 rounded-lg p-2.5 border border-black/[0.02] dark:border-white/[0.02] space-y-2">
<div className="flex justify-between items-center border-b border-black/[0.02] dark:border-white/[0.02] pb-1">
<span className="text-[7.5px] font-black uppercase tracking-wider text-brand-dark/30 dark:text-white/30 block">
<span className="text-xs font-bold uppercase tracking-wider text-brand-dark/30 dark:text-white/30 block">
Aperçu des correspondances actives :
</span>
<span className="text-[7px] font-bold text-brand-dark/40 dark:text-white/40">
<span className="text-xs font-bold text-brand-dark/40 dark:text-white/40">
{selectedGlossaryDetail?.terms?.length || selected.terms_count} au total
</span>
</div>
{isLoadingDetail ? (
<div className="flex items-center justify-center py-3 gap-1.5 text-[8px] text-brand-dark/40 dark:text-white/30 font-light">
<Loader2 size={10} className="animate-spin text-brand-accent" /> Chargement...
<div className="flex items-center justify-center py-3 gap-1.5 text-xs text-brand-dark/40 dark:text-white/30 font-light">
<Loader2 size={12} className="animate-spin text-brand-accent" /> Chargement...
</div>
) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? (
<div className="grid grid-cols-1 gap-1 max-h-[105px] overflow-y-auto pr-1">
<div className="grid grid-cols-1 gap-1 max-h-[120px] overflow-y-auto pr-1">
{selectedGlossaryDetail.terms.slice(0, 4).map((t: any, i: number) => (
<div key={t.id || i} className="flex justify-between items-center bg-brand-muted/30 dark:bg-white/5 px-2 py-1 rounded-md text-[8px]">
<span className="font-extrabold text-brand-dark/75 dark:text-white/70 truncate max-w-[220px]">
<div key={t.id || i} className="flex justify-between items-center bg-brand-muted/30 dark:bg-white/5 px-2 py-1.5 rounded-md text-xs">
<span className="font-semibold text-brand-dark/75 dark:text-white/70 truncate max-w-[220px]">
{t.source} {t.target}
</span>
<span className="inline-block w-1.5 h-1.5 bg-brand-accent rounded-full shrink-0" />
</div>
))}
{selectedGlossaryDetail.terms.length > 4 && (
<span className="text-[7px] text-brand-dark/40 dark:text-white/40 block text-right font-medium mt-1">
<span className="text-[10px] text-brand-dark/40 dark:text-white/40 block text-right font-medium mt-1">
+ {selectedGlossaryDetail.terms.length - 4} autres termes
</span>
)}
</div>
) : (
<p className="py-3 text-[8px] text-brand-dark/40 dark:text-white/30 italic text-center font-light">Aucun terme dans ce glossaire.</p>
<p className="py-3 text-xs text-brand-dark/40 dark:text-white/30 italic text-center font-light">Aucun terme dans ce glossaire.</p>
)}
</div>
)}
{/* Ultra-neat Quick Term Adder */}
{selected && (
<form onSubmit={handleAddTerm} className="flex gap-1">
<form onSubmit={handleAddTerm} className="flex gap-1.5">
<input
type="text"
required
@@ -453,7 +532,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
value={newSource}
onChange={(e) => setNewSource(e.target.value)}
disabled={isAddingTerm || disabled}
className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2 py-1 text-[8px] font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent"
className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent"
/>
<input
type="text"
@@ -462,80 +541,26 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
value={newTarget}
onChange={(e) => setNewTarget(e.target.value)}
disabled={isAddingTerm || disabled}
className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2 py-1 text-[8px] font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent"
className="flex-1 bg-white dark:bg-[#1a1a1a] border border-black/5 dark:border-white/5 rounded-lg px-2.5 py-1.5 text-xs font-semibold text-brand-dark dark:text-white placeholder:text-brand-dark/30 outline-none focus:border-brand-accent"
/>
<button
type="submit"
disabled={isAddingTerm || disabled || !newSource.trim() || !newTarget.trim()}
className="px-2.5 bg-brand-dark dark:bg-white text-white dark:text-brand-dark rounded-lg flex items-center justify-center disabled:opacity-35 transition-colors"
className="px-3 bg-brand-dark dark:bg-white text-white dark:text-brand-dark rounded-lg flex items-center justify-center disabled:opacity-35 transition-colors cursor-pointer"
title="Ajouter le terme"
>
{isAddingTerm ? (
<Loader2 size={10} className="animate-spin" />
<Loader2 size={12} className="animate-spin" />
) : (
<Plus size={10} />
<Plus size={14} />
)}
</button>
</form>
)}
{/* Interactive Accordion for Templates Generator */}
{templates.length > 0 && (
<div className="border-t border-black/[0.03] dark:border-white/[0.03] pt-2 mt-1">
<button
type="button"
onClick={() => setShowTemplates(!showTemplates)}
className="w-full flex items-center justify-between text-[8px] font-black uppercase tracking-wider text-brand-dark/40 dark:text-white/40 hover:text-brand-dark dark:hover:text-white transition-colors"
>
<span> Créer ou Charger depuis un Template</span>
<ChevronRight size={10} className={cn("transform transition-transform text-brand-accent", showTemplates ? 'rotate-95' : '')} />
</button>
{showTemplates && (
<div className="grid grid-cols-2 gap-1 mt-2 p-1 bg-white/40 dark:bg-black/10 rounded-lg animate-fade-in max-h-32 overflow-y-auto">
{templates.map(tmpl => {
const isImporting = importingId === tmpl.id;
const existingGlossary = glossaries.find(
g => g.name.toLowerCase().includes(tmpl.name.toLowerCase().split('/')[0].trim())
);
const isAlreadySelected = existingGlossary?.id === glossaryId;
return (
<button
key={tmpl.id}
type="button"
disabled={isImporting || disabled}
onClick={() => {
if (existingGlossary) {
onChange(isAlreadySelected ? null : existingGlossary.id);
} else {
handleImportTemplate(tmpl);
}
}}
className={cn(
"px-2 py-1 rounded text-left transition-all",
isAlreadySelected
? 'bg-brand-accent/10 border-l-2 border-brand-accent'
: 'hover:bg-brand-muted dark:hover:bg-white/5'
)}
>
<span className="text-[7.5px] font-extrabold text-brand-dark dark:text-white block truncate leading-none">
{isImporting ? 'Importation...' : tmpl.name.split('/')[0].trim()}
</span>
<span className="text-[6px] tracking-wider font-bold text-brand-dark/30 dark:text-white/30 block leading-tight uppercase">
{tmpl.terms_count} termes
</span>
</button>
);
})}
</div>
)}
</div>
)}
</div>
) : (
<div className="p-2.5 bg-brand-dark/5 dark:bg-white/5 rounded-lg text-center animate-fade-in">
<span className="text-[7.5px] font-black uppercase opacity-40">Moteur neutre sans glossaire appliqué</span>
<span className="text-xs font-black uppercase opacity-40">Moteur neutre sans glossaire appliqué</span>
</div>
)}
</div>

View File

@@ -66,28 +66,28 @@ function Combobox({
type="button"
onClick={() => setOpen(!open)}
className={cn(
'w-full py-2 px-3 bg-brand-muted/60 dark:bg-white/5 rounded-xl border text-[10px] font-bold uppercase tracking-wider text-brand-dark dark:text-white flex items-center justify-between hover:border-brand-accent/50 transition-all select-none cursor-pointer',
'w-full py-2.5 px-3.5 bg-brand-muted/60 dark:bg-white/5 rounded-xl border text-xs font-bold uppercase tracking-wider text-brand-dark dark:text-white flex items-center justify-between hover:border-brand-accent/50 transition-all select-none cursor-pointer',
open ? 'border-brand-accent/50' : 'border-brand-accent/20 dark:border-white/10'
)}
>
<span className="truncate">{label || placeholder}</span>
<ChevronDown className={cn('size-3 shrink-0 text-brand-accent transition-transform ms-2', open && 'rotate-180')} />
<ChevronDown className={cn('size-3.5 shrink-0 text-brand-accent transition-transform ms-2', open && 'rotate-180')} />
</button>
{open && (
<div className="absolute top-[102%] right-0 left-0 bg-white dark:bg-[#1a1a1a] border border-black/10 dark:border-white/10 rounded-xl shadow-2xl p-2 z-50 max-h-48 overflow-y-auto animate-fade-in">
<div className="border-b border-black/5 dark:border-white/5 px-2 py-1">
<div className="border-b border-black/5 dark:border-white/5 px-2 py-1.5">
<input
ref={inputRef}
type="text"
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
className="w-full bg-transparent px-1 py-1 text-[10px] outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white"
className="w-full bg-transparent px-1 py-1 text-xs outline-none placeholder:text-brand-dark/30 dark:placeholder:text-white/30 text-brand-dark dark:text-white"
/>
</div>
<div className="max-h-[160px] overflow-y-auto p-1 mt-1 space-y-0.5">
{filtered.length === 0 && (
<div className="px-3 py-3 text-center text-[9px] text-brand-dark/40 dark:text-white/40">No results</div>
<div className="px-3 py-3 text-center text-xs text-brand-dark/40 dark:text-white/40">No results</div>
)}
{filtered.map(lang => (
<button
@@ -95,14 +95,14 @@ function Combobox({
type="button"
onClick={() => { onChange(lang.code); setOpen(false); setQuery(''); }}
className={cn(
'flex w-full items-center justify-between rounded-lg px-2.5 py-1.5 text-[9px] font-bold uppercase tracking-wider transition-colors cursor-pointer',
'flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-xs font-bold uppercase tracking-wider transition-colors cursor-pointer',
value === lang.code
? 'bg-brand-accent/10 text-brand-accent'
: 'text-brand-dark/70 dark:text-white/70 hover:bg-brand-muted dark:hover:bg-white/5'
)}
>
<span className="truncate">{lang.name}</span>
{value === lang.code && <Check className="size-3 text-brand-accent shrink-0" />}
{value === lang.code && <Check className="size-3.5 text-brand-accent shrink-0" />}
</button>
))}
</div>
@@ -143,7 +143,7 @@ export default function LanguageSelector({
<div className="grid grid-cols-11 gap-2 items-center">
{/* Source */}
<div className="col-span-5 relative text-left">
<span className="text-[8px] font-bold text-brand-dark/30 dark:text-white/30 uppercase tracking-widest block mb-1">Source</span>
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">Source</span>
<Combobox
value={sourceLang}
options={languages}
@@ -174,7 +174,7 @@ export default function LanguageSelector({
{/* Target */}
<div className="col-span-5 relative text-left">
<span className="text-[8px] font-bold text-brand-dark/30 dark:text-white/30 uppercase tracking-widest block mb-1">Cible</span>
<span className="text-[10px] font-bold text-brand-dark/40 dark:text-white/40 uppercase tracking-widest block mb-1">Cible</span>
<Combobox
value={targetLang}
options={languages}

View File

@@ -142,7 +142,7 @@ export function ProviderSelector({
type="button"
onClick={() => onProviderChange(p.id)}
className={cn(
'p-2 py-1.5 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-center min-h-[44px]',
'p-2.5 py-2 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-center min-h-[48px]',
isSelected
? 'border-brand-accent bg-brand-accent/5 dark:bg-brand-accent/10'
: 'border-black/[0.05] dark:border-white/[0.05] bg-brand-muted/20 dark:bg-zinc-800/10 hover:bg-brand-muted/50 dark:hover:bg-zinc-800/20'
@@ -150,14 +150,14 @@ export function ProviderSelector({
>
<div className="flex items-center justify-between">
<span className={cn(
"text-[8px] font-black uppercase tracking-tight truncate flex-1",
"text-[9.5px] font-black uppercase tracking-tight truncate flex-1",
isSelected ? 'text-brand-accent' : 'text-brand-dark/40 dark:text-white/40'
)}>
Standard
</span>
{isSelected && <div className="w-1.5 h-1.5 rounded-full bg-brand-accent animate-pulse shrink-0 ml-1" />}
</div>
<span className="text-[10px] font-bold text-brand-dark dark:text-white block leading-tight truncate">
<span className="text-xs font-bold text-brand-dark dark:text-white block leading-tight truncate">
{label}
</span>
</button>
@@ -167,6 +167,7 @@ export function ProviderSelector({
const renderLlmCard = (p: AvailableProvider, locked: boolean) => {
const isSelected = provider === p.id;
const theme = LLM_THEMES[p.id] || DEFAULT_LLM_THEME;
const displayLabel = p.model || p.label.replace(/^Traduction\s+IA\s+/i, '');
return (
<button
@@ -175,7 +176,7 @@ export function ProviderSelector({
disabled={locked}
onClick={() => !locked && onProviderChange(p.id)}
className={cn(
'p-2 py-1.5 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-between min-h-[58px]',
'p-2 py-2 rounded-lg border text-left transition-all relative overflow-hidden flex flex-col justify-between min-h-[64px]',
isSelected
? 'border-brand-accent bg-brand-accent/5 dark:bg-brand-accent/10'
: locked
@@ -185,7 +186,7 @@ export function ProviderSelector({
>
<div className="flex items-center justify-between mb-0.5">
<span className={cn(
"text-[8px] font-black uppercase tracking-tight truncate flex-1",
"text-[9px] font-black uppercase tracking-tight truncate flex-1",
isSelected ? 'text-brand-accent' : 'text-brand-dark/40 dark:text-white/40'
)}>
{theme.badge}
@@ -193,10 +194,10 @@ export function ProviderSelector({
{isSelected && <div className="w-1.5 h-1.5 rounded-full bg-brand-accent animate-pulse shrink-0 ml-1" />}
{locked && <Lock className="size-2 text-brand-dark/40 dark:text-white/40 shrink-0 ml-1" />}
</div>
<span className="text-[10px] font-bold text-brand-dark dark:text-white block leading-none mb-0.5 truncate">
{p.label}
<span className="text-xs font-bold text-brand-dark dark:text-white block leading-none mb-0.5 truncate">
{displayLabel}
</span>
<span className="text-[7.5px] text-brand-dark/40 dark:text-white/45 uppercase font-bold block leading-none truncate">
<span className="text-[8.5px] text-brand-dark/40 dark:text-white/45 uppercase font-bold block leading-none truncate">
{theme.subBadge}
</span>
</button>