fix(admin-ui): QA report sweep — modal/table layout, i18n, sidebar polish

Восемь дефектов из QA-отчёта одним MR.

#1 — Records-table теряла первую колонку после закрытия Create-modal.
Корень: lg:grid-cols-[220px_1fr] без minmax(0,…) — 1fr имел implicit
min-content (=max-content), позволяя table вырасти шире viewport.
После закрытия Radix Dialog снималось body padding-right (~15px), grid
пересчитывался и клиппил колонку под overflow-hidden card.
Fix: grid-cols-[220px_minmax(0,1fr)] + убран overflow-hidden с card
(Table primitive имеет свой overflow-x-auto).

#2 — Esc закрывал modal с задержкой (визуал/DOM рассинхрон).
duration-200 на slow CPU ощущался как несколько кадров между state=closed
и unmount. Снижено до duration-150 — совпадает с tw-animate-css дефолтами.

#3 — «+ Создать» обрезалось до «+ Соз» на ~1075px на detail-странице.
EditorTabBar: 6 tabs + 3 кнопки actions в одном flex row без shrink-0
давили друг друга. Решение: tabs в overflow-x-auto wrapper с min-w-0,
actions в shrink-0 контейнере — tabs скроллятся, actions всегда видны
полностью. scrollbar скрыт визуально (height-консистентность).

#4 — Обрезка названий в records-table при PUBLIC+INTERNAL.
Добавлен div.overflow-x-auto вокруг table + min-w-[760px] на table +
min-w-[180px] max-w-[320px] truncate на name cell. Catalog table также
получает min-w-[220px] на name column.

#5 — Заголовки колонок и кнопка «Граф» не локализованы на EN.
defaultValue:'Название' inline в коде падал на ru-text при EN locale.
Вынесены в i18n: dict.col.title/id/bundle/records/updated/outgoingFk/
incomingFk + dict.list.graph для обеих локалей.

#6 — Modal backdrop почти невидим.
bg-black/40 + 1px blur → bg-black/55 + 2px blur. Заметно темнее при
сохранении читабельности фона, фокус на modal.

#7 — Неестественная подсказка в форме «Создать»:
«Имя оставьте уникальное; версия сбросится на 1.0.0» →
«Укажите уникальное имя — версия сбросится на 1.0.0».

#8 — «Главная» в сайдбаре сбивала active state.
Клик на «Главную» вёл на `/` → redirect на /dictionaries → подсветка
уезжала на «Справочники». Logo сверху уже даёт home affordance, поэтому
duplicate nav item удалён (desktop + mobile).
This commit is contained in:
Zimin A.N.
2026-05-17 15:23:01 +03:00
parent b56fcc41a4
commit 239cecd242
5 changed files with 101 additions and 44 deletions
@@ -800,8 +800,14 @@ function DictionaryDetail() {
Info panel — sticky слева с dict metadata + relations.
На <lg viewport schedule переключается в stacked (info above content). */}
{/* Grid items-stretch (default) — InfoPanel + main panel равной высоты.
* gap-3 — closer panels per user feedback ("ближе расположены"). */}
<div className="grid grid-cols-1 lg:grid-cols-[220px_1fr] gap-3 mt-2">
* gap-3 — closer panels per user feedback ("ближе расположены").
* QA bug #1: 1fr → minmax(0,1fr) — без minmax(0,…) grid track имеет
* implicit min-content (auto), что позволяет records table вырасти
* шире viewport. После закрытия Radix Dialog снимается body padding-
* right (~15px), 1fr пересчитывается и ломает layout (исчезает
* первая колонка под overflow-hidden card). minmax(0,1fr) фиксирует
* нижнюю границу 0 → overflow-x-auto в Table primitive работает. */}
<div className="grid grid-cols-1 lg:grid-cols-[220px_minmax(0,1fr)] gap-3 mt-2">
{detailQuery.data && (
<EditorInfoPanel
detail={detailQuery.data}
@@ -835,8 +841,12 @@ function DictionaryDetail() {
{/* Main editor panel — tab bar + toolbar + tab content в bordered card
* per redesign prototype. Visual containment отделяет editor от
* page chrome. */}
<div className="rounded-lg border border-line bg-surface overflow-hidden">
* page chrome.
* QA bug #1: overflow-hidden убран — клиппил records table при ширине
* контента больше колонки. Table primitive сам имеет overflow-x-auto;
* tabs ниже получают свой scroll контейнер. Card rounding продолжает
* работать через border-radius на самом div'е. */}
<div className="rounded-lg border border-line bg-surface">
{/* Editor tabs per handoff Stage 3.4 + redesign:
* - counts в labels (Записи 127, Связи 6, Поля 12)
@@ -1189,7 +1199,7 @@ function DictionaryDetail() {
) : null}
</div>
</TableCell>
<TableCell hideBelow="md">
<TableCell hideBelow="md" className="min-w-[180px] max-w-[320px] truncate">
{recordDisplayName(
r.data,
detailQuery.data?.defaultLocale ?? 'ru-RU',
@@ -1981,32 +1991,42 @@ function EditorTabBar({
{ id: 'events', label: t('editor.tab.events', { defaultValue: 'События' }) },
{ id: 'history', label: t('editor.tab.history', { defaultValue: 'История' }) },
]
// QA bug #3: на 1075px tabs (6 шт.) + actions (3 кнопки) переполняли flex
// row → "+ Создать" обрезалось до "+ Соз". Решение: actions выносим в
// отдельный shrink-0 контейнер, tabs кладём в overflow-x-auto wrapper
// с min-w-0 → tabs скроллятся горизонтально если cramped, actions всегда
// видны полностью. whitespace-nowrap на tab buttons чтобы текст не
// переносился в кружок счётчика.
return (
<div className="flex items-center gap-0 border-b border-line w-full">
{tabs.map((tab) => {
const isActive = active === tab.id
const count = counts?.[tab.id]
return (
<button
key={tab.id}
type="button"
onClick={() => onChange(tab.id)}
aria-pressed={isActive}
className={[
'px-3.5 h-10 text-body transition-colors -mb-px border-b-2 inline-flex items-center gap-1.5',
isActive
? 'border-accent text-ink font-semibold'
: 'border-transparent text-ink-2 hover:text-ink',
].join(' ')}
>
{tab.label}
{typeof count === 'number' && count > 0 && (
<span className="text-mono text-mute">{count}</span>
)}
</button>
)
})}
{actions && <div className="ml-auto flex items-center gap-2 pl-2">{actions}</div>}
<div className="flex items-center border-b border-line w-full">
<div className="flex items-center gap-0 overflow-x-auto flex-1 min-w-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{tabs.map((tab) => {
const isActive = active === tab.id
const count = counts?.[tab.id]
return (
<button
key={tab.id}
type="button"
onClick={() => onChange(tab.id)}
aria-pressed={isActive}
className={[
'px-3.5 h-10 text-body transition-colors -mb-px border-b-2 inline-flex items-center gap-1.5 shrink-0 whitespace-nowrap',
isActive
? 'border-accent text-ink font-semibold'
: 'border-transparent text-ink-2 hover:text-ink',
].join(' ')}
>
{tab.label}
{typeof count === 'number' && count > 0 && (
<span className="text-mono text-mute">{count}</span>
)}
</button>
)
})}
</div>
{actions && (
<div className="flex items-center gap-2 pl-2 pr-2 shrink-0">{actions}</div>
)}
</div>
)
}
@@ -483,9 +483,12 @@ function DictionaryListTable({ rows }: { rows: DictionaryDefinition[] }) {
sort.key === key ? (sort.dir === 'asc' ? ' ↑' : ' ↓') : ''
return (
// No outer rounded/border wrapper — outer panel handles that (toolbar + table
// в одной card). Just table with white header per user feedback.
<table className="w-full bg-surface">
// QA bug #4: оборачиваем в overflow-x-auto wrapper чтобы при PUBLIC+INTERNAL
// с extra columns table не клипал name'ы под parent overflow-hidden card.
// table:min-w[760px] — нижняя граница ширины (sum min-content всех колонок),
// ниже неё включается horizontal scroll.
<div className="w-full overflow-x-auto">
<table className="w-full min-w-[760px] bg-surface">
<thead className="bg-surface border-b border-line">
<tr>
<SortableHeader onClick={() => toggleSort('title')} active={sort.key === 'title'}>
@@ -530,6 +533,7 @@ function DictionaryListTable({ rows }: { rows: DictionaryDefinition[] }) {
))}
</tbody>
</table>
</div>
)
}
@@ -623,8 +627,10 @@ function DictRow({ d, t }: { d: DictionaryDefinition; t: TFunc }) {
onKeyDown={handleKey}
className="relative border-b border-line-2 last:border-b-0 cursor-pointer bg-surface hover:bg-surface-2 focus-visible:outline-none focus-visible:bg-accent-bg/30 transition-colors"
>
{/* name + subtitle (description short) + scope-color left stripe */}
<td className="relative px-3 py-2 align-top min-w-0">
{/* name + subtitle (description short) + scope-color left stripe.
* QA bug #4: min-w-[220px] — обеспечивает что name column не схлопывается
* до букв. При недостатке ширины table получает horizontal scroll. */}
<td className="relative px-3 py-2 align-top min-w-[220px]">
{/* 3px scope stripe — absolutely positioned на левом краю row.
* td имеет relative чтобы before работало в table context. */}
<span