527 lines
26 KiB
TypeScript
527 lines
26 KiB
TypeScript
'use client';
|
||
|
||
import type { MutableRefObject, RefObject } from 'react';
|
||
import { getEnglishValue, useTranslation } from '../../../i18n/useTranslation';
|
||
import type { NamespaceCategory } from '../hooks/useNamespaceCategories';
|
||
|
||
type LanguageEntry = {
|
||
code: string;
|
||
name: string;
|
||
};
|
||
|
||
type Props = {
|
||
activeCategory: string;
|
||
setActiveCategory: (category: string) => void;
|
||
categoriesWithKnownNamespaces: NamespaceCategory[];
|
||
allTabStats: { total: number; translated: number; missing: number };
|
||
categoryTabStats: Record<string, { total: number; translated: number; missing: number }>;
|
||
globalTabStats: { total: number; translated: number; missing: number };
|
||
activeLang: string;
|
||
allLanguages: LanguageEntry[];
|
||
search: string;
|
||
setSearch: (value: string) => void;
|
||
autoScrollOnPanelOpen: boolean;
|
||
setAutoScrollOnPanelOpen: (value: boolean) => void;
|
||
autoScrollOnSave: boolean;
|
||
setAutoScrollOnSave: (value: boolean) => void;
|
||
newGlobalKeySelection: string;
|
||
setNewGlobalKeySelection: (value: string) => void;
|
||
availableGlobalKeyOptions: string[];
|
||
addGlobalKey: (key: string) => void;
|
||
globalFilteredKeys: string[];
|
||
removeGlobalKey: (key: string) => void;
|
||
getDisplayValue: (key: string) => string;
|
||
translations: Record<string, Record<string, string>>;
|
||
handleChange: (key: string, value: string) => void;
|
||
globalKeySet: Set<string>;
|
||
englishReferenceKeySet: Set<string>;
|
||
setEnglishReferenceForKey: (key: string, enabled: boolean) => void;
|
||
filteredNs: string[];
|
||
filteredGroups: Record<string, string[]>;
|
||
activeNamespacePanel: string | null;
|
||
setActiveNamespacePanel: (next: string | null | ((prev: string | null) => string | null)) => void;
|
||
namespaceTranslationStats: Record<string, { total: number; translated: number; missing: number }>;
|
||
openedNamespacePanelRef: RefObject<HTMLDivElement | null>;
|
||
openFromPanelClickRef: MutableRefObject<boolean>;
|
||
onBackToPanels: () => void;
|
||
onOpenCategoryManager: () => void;
|
||
};
|
||
|
||
export default function TranslationCoverageEditor({
|
||
activeCategory,
|
||
setActiveCategory,
|
||
categoriesWithKnownNamespaces,
|
||
allTabStats,
|
||
categoryTabStats,
|
||
globalTabStats,
|
||
activeLang,
|
||
allLanguages,
|
||
search,
|
||
setSearch,
|
||
autoScrollOnPanelOpen,
|
||
setAutoScrollOnPanelOpen,
|
||
autoScrollOnSave,
|
||
setAutoScrollOnSave,
|
||
newGlobalKeySelection,
|
||
setNewGlobalKeySelection,
|
||
availableGlobalKeyOptions,
|
||
addGlobalKey,
|
||
globalFilteredKeys,
|
||
removeGlobalKey,
|
||
getDisplayValue,
|
||
translations,
|
||
handleChange,
|
||
globalKeySet,
|
||
englishReferenceKeySet,
|
||
setEnglishReferenceForKey,
|
||
filteredNs,
|
||
filteredGroups,
|
||
activeNamespacePanel,
|
||
setActiveNamespacePanel,
|
||
namespaceTranslationStats,
|
||
openedNamespacePanelRef,
|
||
openFromPanelClickRef,
|
||
onBackToPanels,
|
||
onOpenCategoryManager,
|
||
}: Props) {
|
||
const { t } = useTranslation();
|
||
|
||
// Shared tab button renderer
|
||
const renderCategoryTab = (
|
||
key: string,
|
||
label: string,
|
||
stats: { total: number; translated: number; missing: number }
|
||
) => {
|
||
const isActive = activeCategory === key;
|
||
const hasMissing = activeLang !== 'en' && stats.missing > 0;
|
||
return (
|
||
<button
|
||
key={key}
|
||
onClick={() => setActiveCategory(key)}
|
||
className={`flex shrink-0 items-center gap-2 px-4 py-2.5 rounded-2xl text-sm font-medium transition whitespace-nowrap ${
|
||
isActive
|
||
? hasMissing
|
||
? 'bg-red-600 text-white shadow-[0_18px_40px_-24px_rgba(220,38,38,0.7)]'
|
||
: 'bg-slate-900 text-white shadow-[0_18px_40px_-24px_rgba(15,23,42,0.85)]'
|
||
: hasMissing
|
||
? 'border border-red-200 bg-red-50/80 text-red-700 hover:bg-red-100'
|
||
: 'bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 border border-transparent hover:border-slate-200'
|
||
}`}
|
||
>
|
||
<span>{label}</span>
|
||
{hasMissing ? (
|
||
<span className={`rounded-full px-2 py-0.5 text-[10px] leading-none font-semibold tabular-nums ${
|
||
isActive ? 'bg-white/20 text-white' : 'bg-red-100 text-red-600'
|
||
}`}>
|
||
{stats.missing}
|
||
</span>
|
||
) : (
|
||
<span className={`rounded-full px-2 py-0.5 text-[10px] leading-none font-medium tabular-nums ${
|
||
isActive ? 'bg-white/15 text-white/80' : 'bg-slate-100 text-slate-400'
|
||
}`}>
|
||
{stats.total}
|
||
</span>
|
||
)}
|
||
</button>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{/* ── Category tabs card ───────────────────────────────── */}
|
||
<div className="rounded-[28px] border border-white/80 bg-white/85 px-5 py-4 shadow-[0_22px_60px_-34px_rgba(15,23,42,0.28)] backdrop-blur">
|
||
<div className="flex items-center justify-between gap-3 flex-wrap mb-3">
|
||
<div>
|
||
<h2 className="text-sm font-semibold text-slate-950">{t('autofix.k5f978731')}</h2>
|
||
<p className="text-xs text-slate-500 mt-0.5">{t('autofix.kb7a30760')}</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onOpenCategoryManager}
|
||
className="rounded-2xl border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 shadow-sm hover:border-slate-300 hover:bg-slate-50 transition"
|
||
>
|
||
{t('autofix.kd6e42900')}
|
||
</button>
|
||
</div>
|
||
<div className="flex items-center gap-1.5 flex-wrap">
|
||
{renderCategoryTab('all', 'All', allTabStats)}
|
||
{renderCategoryTab('global', 'Global', globalTabStats)}
|
||
{categoriesWithKnownNamespaces.map((cat) =>
|
||
renderCategoryTab(cat.id, cat.label, categoryTabStats[cat.id] ?? { total: 0, translated: 0, missing: 0 })
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Search + options row ─────────────────────────────── */}
|
||
<div className="flex items-center gap-3 flex-wrap">
|
||
<div className="relative flex-1 min-w-[14rem] max-w-sm">
|
||
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" />
|
||
</svg>
|
||
<input
|
||
type="search"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
placeholder={t('autofix.kbf49d59b')}
|
||
className="w-full rounded-2xl border border-slate-200 bg-white/90 pl-9 pr-3 py-2 text-sm shadow-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900/20 focus:border-slate-300 transition"
|
||
/>
|
||
</div>
|
||
<div className="flex items-center gap-4 flex-wrap">
|
||
<label className="inline-flex items-center gap-2 text-sm text-slate-600 select-none">
|
||
<input
|
||
type="checkbox"
|
||
checked={autoScrollOnPanelOpen}
|
||
onChange={(e) => setAutoScrollOnPanelOpen(e.target.checked)}
|
||
className="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900/20"
|
||
/>
|
||
{t('autofix.kfd1e0089')}
|
||
</label>
|
||
<label className="inline-flex items-center gap-2 text-sm text-slate-600 select-none">
|
||
<input
|
||
type="checkbox"
|
||
checked={autoScrollOnSave}
|
||
onChange={(e) => setAutoScrollOnSave(e.target.checked)}
|
||
className="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900/20"
|
||
/>
|
||
{t('autofix.k23e95df1')}
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Content area ────────────────────────────────────── */}
|
||
<div className="space-y-3">
|
||
|
||
{/* Global keys table */}
|
||
{activeCategory === 'global' && (
|
||
<div className="rounded-[28px] border border-white/80 bg-white/85 overflow-hidden shadow-[0_22px_60px_-34px_rgba(15,23,42,0.28)] backdrop-blur">
|
||
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between gap-3 flex-wrap">
|
||
<div>
|
||
<span className="font-semibold text-slate-950">{t('autofix.k6cfeedd3')}</span>
|
||
<p className="text-xs text-slate-500 mt-0.5">{t('autofix.kad7d8c49')}</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<select
|
||
value={newGlobalKeySelection}
|
||
onChange={(e) => setNewGlobalKeySelection(e.target.value)}
|
||
className="w-72 max-w-full rounded-2xl border border-slate-200 bg-white px-3 py-1.5 text-xs shadow-sm focus:outline-none focus:ring-2 focus:ring-slate-900/20"
|
||
>
|
||
<option value="">{t('autofix.k47bce570')}</option>
|
||
{availableGlobalKeyOptions.map((key) => (
|
||
<option key={key} value={key}>{key}</option>
|
||
))}
|
||
</select>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
if (!newGlobalKeySelection) return;
|
||
addGlobalKey(newGlobalKeySelection);
|
||
setNewGlobalKeySelection('');
|
||
}}
|
||
className="rounded-2xl border border-slate-200 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition"
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{globalFilteredKeys.length > 0 ? (
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-slate-100 bg-slate-50/60">
|
||
<th className="px-5 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 w-1/3">Key</th>
|
||
{activeLang !== 'en' && (
|
||
<th className="px-5 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 w-1/3">English</th>
|
||
)}
|
||
<th className="px-5 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||
{allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{globalFilteredKeys.map((key) => {
|
||
const enVal = getEnglishValue(key);
|
||
const currentVal = getDisplayValue(key);
|
||
const hasOverride = (translations[activeLang]?.[key] ?? '') !== '';
|
||
|
||
return (
|
||
<tr key={key} className="border-b border-slate-50 last:border-0 hover:bg-slate-50/60 transition-colors">
|
||
<td className="px-5 py-2.5 font-mono text-xs text-slate-500 align-top pt-3">
|
||
<div className="flex items-start justify-between gap-2">
|
||
<span>{key}</span>
|
||
<button
|
||
type="button"
|
||
title={t('autofix.kc02b17c3')}
|
||
onClick={() => removeGlobalKey(key)}
|
||
className="shrink-0 rounded-xl border border-red-200 px-1.5 py-0.5 text-[10px] text-red-500 hover:bg-red-50 transition"
|
||
>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
</td>
|
||
{activeLang !== 'en' && (
|
||
<td className="px-5 py-2.5 text-slate-500 align-top pt-3 text-xs">{enVal}</td>
|
||
)}
|
||
<td className="px-5 py-2.5">
|
||
<div className="relative">
|
||
<textarea
|
||
rows={1}
|
||
value={activeLang === 'en' ? currentVal : (translations[activeLang]?.[key] ?? '')}
|
||
onChange={(e) => handleChange(key, e.target.value)}
|
||
placeholder={activeLang === 'en' ? '' : enVal}
|
||
className={`w-full rounded-xl border px-3 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-slate-900/20 transition ${
|
||
hasOverride && activeLang !== 'en'
|
||
? 'border-emerald-300 bg-emerald-50/60'
|
||
: 'border-slate-200 bg-white'
|
||
}`}
|
||
style={{ minHeight: '2.25rem', fieldSizing: 'content' } as React.CSSProperties}
|
||
onInput={(e) => {
|
||
const target = e.currentTarget;
|
||
target.style.height = 'auto';
|
||
target.style.height = `${target.scrollHeight}px`;
|
||
}}
|
||
/>
|
||
{hasOverride && activeLang !== 'en' && (
|
||
<button
|
||
type="button"
|
||
title={t('autofix.k644d9ea8')}
|
||
onClick={() => handleChange(key, '')}
|
||
className="absolute top-1.5 right-2 text-xs text-slate-400 hover:text-red-500 transition"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
) : (
|
||
<div className="p-10 text-center text-sm text-slate-400">{t('autofix.k0700b1f2')}</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Namespace grid + open panel */}
|
||
{activeCategory !== 'global' && filteredNs.length > 0 && (
|
||
<>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
|
||
{filteredNs.map((ns, idx) => {
|
||
const keys = filteredGroups[ns] ?? [];
|
||
const nsStats = namespaceTranslationStats[ns] ?? {
|
||
total: keys.length,
|
||
translated: keys.length,
|
||
missing: 0,
|
||
};
|
||
const hasMissing = activeLang !== 'en' && nsStats.missing > 0;
|
||
const isActive = activeNamespacePanel === ns;
|
||
const activeIdx = activeNamespacePanel ? filteredNs.indexOf(activeNamespacePanel) : -1;
|
||
const shiftClass = !activeNamespacePanel || isActive
|
||
? 'translate-x-0'
|
||
: idx < activeIdx ? '-translate-x-1' : 'translate-x-1';
|
||
|
||
return (
|
||
<button
|
||
key={ns}
|
||
type="button"
|
||
onClick={() => {
|
||
openFromPanelClickRef.current = true;
|
||
setActiveNamespacePanel((prev) => (prev === ns ? null : ns));
|
||
}}
|
||
className={`w-full rounded-[20px] border px-4 py-3 text-left transition-all duration-300 backdrop-blur ${shiftClass} ${
|
||
isActive
|
||
? hasMissing
|
||
? 'border-red-300 bg-red-50/90 shadow-[0_12px_30px_-18px_rgba(220,38,38,0.5)]'
|
||
: 'border-slate-300 bg-white/95 shadow-[0_12px_30px_-18px_rgba(15,23,42,0.4)]'
|
||
: hasMissing
|
||
? 'border-red-200 bg-red-50/60 hover:bg-red-50/90'
|
||
: 'border-white/80 bg-white/80 hover:bg-white/95 hover:border-slate-200'
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className={`text-sm font-semibold capitalize ${isActive ? 'text-slate-950' : 'text-slate-800'}`}>
|
||
{ns}
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-semibold ${
|
||
isActive
|
||
? hasMissing ? 'bg-red-500 text-white' : 'bg-slate-900 text-white'
|
||
: hasMissing ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-600'
|
||
}`}>
|
||
{nsStats.translated}/{nsStats.total}
|
||
</span>
|
||
{hasMissing && (
|
||
<span className={`rounded-full px-1.5 py-0.5 text-[10px] font-semibold ${
|
||
isActive ? 'bg-red-200 text-red-800' : 'bg-red-100 text-red-700'
|
||
}`}>
|
||
{nsStats.missing}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<p className="mt-1 text-[11px] text-slate-400">
|
||
{isActive ? t('autofix.k77d01d6a') : t('autofix.kcfb5fb54')}
|
||
</p>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{activeNamespacePanel && filteredGroups[activeNamespacePanel] && (
|
||
<div
|
||
ref={openedNamespacePanelRef}
|
||
className="rounded-[28px] border border-white/80 bg-white/90 overflow-hidden shadow-[0_24px_70px_-40px_rgba(15,23,42,0.3)] backdrop-blur transition-all duration-300"
|
||
>
|
||
{/* Panel header */}
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-100 bg-slate-50/60">
|
||
<span className="font-bold text-slate-950 capitalize text-base">{activeNamespacePanel}</span>
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="text-xs rounded-full border border-slate-200 bg-white px-2 py-0.5 text-slate-600 font-medium">
|
||
{(namespaceTranslationStats[activeNamespacePanel]?.translated ?? 0)}/{(namespaceTranslationStats[activeNamespacePanel]?.total ?? 0)}
|
||
</span>
|
||
{activeLang !== 'en' && (namespaceTranslationStats[activeNamespacePanel]?.missing ?? 0) > 0 && (
|
||
<span className="text-xs rounded-full border border-red-200 bg-red-50 px-2 py-0.5 text-red-700 font-medium">
|
||
{namespaceTranslationStats[activeNamespacePanel]?.missing} missing
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-slate-100 bg-slate-50/40">
|
||
<th className="px-5 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 w-1/3">Key</th>
|
||
{activeLang !== 'en' && (
|
||
<th className="px-5 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 w-1/3">English</th>
|
||
)}
|
||
<th className="px-5 py-2.5 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||
{allLanguages.find((l) => l.code === activeLang)?.name ?? activeLang}
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filteredGroups[activeNamespacePanel].map((key) => {
|
||
const enVal = getEnglishValue(key);
|
||
const currentVal = getDisplayValue(key);
|
||
const isGlobal = globalKeySet.has(key);
|
||
const isEnglishReference = englishReferenceKeySet.has(key);
|
||
const visibleValue = activeLang === 'en'
|
||
? currentVal
|
||
: (translations[activeLang]?.[key] ?? '');
|
||
const hasOverride = (translations[activeLang]?.[key] ?? '') !== '';
|
||
const isMissingInOpenedPanel =
|
||
activeLang !== 'en' &&
|
||
!isGlobal &&
|
||
!isEnglishReference && (
|
||
visibleValue.trim() === '' ||
|
||
visibleValue.trim() === enVal.trim()
|
||
);
|
||
|
||
return (
|
||
<tr key={key} className={`border-b border-slate-50 last:border-0 transition-colors ${
|
||
isMissingInOpenedPanel ? 'bg-red-50/50 hover:bg-red-50/80' : 'hover:bg-slate-50/60'
|
||
}`}>
|
||
<td className={`px-5 py-2.5 font-mono text-xs align-top pt-3 ${
|
||
isMissingInOpenedPanel ? 'text-red-700' : 'text-slate-500'
|
||
}`}>
|
||
<div className="space-y-2.5">
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="block">{key}</span>
|
||
{isGlobal && (
|
||
<span className="rounded-full border border-indigo-200 bg-indigo-50 px-1.5 py-0.5 text-[10px] font-semibold text-indigo-700">
|
||
Global
|
||
</span>
|
||
)}
|
||
{isEnglishReference && activeLang !== 'en' && (
|
||
<span className="rounded-full border border-sky-200 bg-sky-50 px-1.5 py-0.5 text-[10px] font-semibold text-sky-700">{t('autofix.k8de6d3df')}</span>
|
||
)}
|
||
</div>
|
||
<label className="flex items-center gap-1.5 text-[11px] font-sans text-slate-500 select-none">
|
||
<input
|
||
type="checkbox"
|
||
checked={isGlobal}
|
||
onChange={(e) => {
|
||
if (e.target.checked) { addGlobalKey(key); return; }
|
||
removeGlobalKey(key);
|
||
}}
|
||
className="h-3.5 w-3.5 rounded border-slate-300 text-slate-900 focus:ring-slate-900/20"
|
||
/>
|
||
{t('autofix.kb1cf599b')}
|
||
</label>
|
||
{activeLang !== 'en' && (
|
||
<label className="flex items-center gap-1.5 text-[11px] font-sans text-slate-500 select-none">
|
||
<input
|
||
type="checkbox"
|
||
checked={isEnglishReference}
|
||
onChange={(e) => setEnglishReferenceForKey(key, e.target.checked)}
|
||
className="h-3.5 w-3.5 rounded border-slate-300 text-slate-900 focus:ring-slate-900/20"
|
||
/>{t('autofix.k6d79b1df')}</label>
|
||
)}
|
||
</div>
|
||
</td>
|
||
{activeLang !== 'en' && (
|
||
<td className="px-5 py-2.5 text-slate-400 align-top pt-3 text-xs">{enVal}</td>
|
||
)}
|
||
<td className="px-5 py-2.5">
|
||
<div className="relative">
|
||
<textarea
|
||
rows={1}
|
||
value={visibleValue}
|
||
onChange={(e) => handleChange(key, e.target.value)}
|
||
placeholder={activeLang === 'en' ? '' : enVal}
|
||
className={`w-full rounded-xl border px-3 py-1.5 text-sm resize-none focus:outline-none focus:ring-2 focus:ring-slate-900/20 transition ${
|
||
isMissingInOpenedPanel
|
||
? 'border-red-300 bg-red-50/60'
|
||
: hasOverride && activeLang !== 'en'
|
||
? 'border-emerald-300 bg-emerald-50/60'
|
||
: 'border-slate-200 bg-white'
|
||
}`}
|
||
style={{ minHeight: '2.25rem', fieldSizing: 'content' } as React.CSSProperties}
|
||
onInput={(e) => {
|
||
const target = e.currentTarget;
|
||
target.style.height = 'auto';
|
||
target.style.height = `${target.scrollHeight}px`;
|
||
}}
|
||
/>
|
||
{hasOverride && activeLang !== 'en' && (
|
||
<button
|
||
type="button"
|
||
title={t('autofix.k644d9ea8')}
|
||
onClick={() => handleChange(key, '')}
|
||
className="absolute top-1.5 right-2 text-xs text-slate-400 hover:text-red-500 transition"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
|
||
<div className="border-t border-slate-100 px-5 py-3 bg-slate-50/40 flex justify-end">
|
||
<button
|
||
type="button"
|
||
onClick={onBackToPanels}
|
||
className="rounded-2xl border border-slate-200 bg-white px-4 py-1.5 text-xs font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition"
|
||
>
|
||
{t('autofix.k6aba2cb0')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{activeCategory !== 'global' && filteredNs.length === 0 && (
|
||
<div className="rounded-[28px] border border-white/80 bg-white/85 p-10 text-center text-sm text-slate-400 shadow-[0_22px_60px_-34px_rgba(15,23,42,0.18)] backdrop-blur">
|
||
{t('autofix.k6a892262')}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
);
|
||
} |