Merge branch 'fix/catalog-toolbar-compact' into 'main'

fix(admin-ui): compact catalog toolbar (single row)

See merge request 2-6/2-6-4/terravault/ordinis!19
This commit is contained in:
Александр Зимин
2026-05-10 18:30:49 +00:00
2 changed files with 72 additions and 51 deletions
+8
View File
@@ -225,6 +225,10 @@ i18n
'dict.list.section.PUBLIC': 'Публичные',
'dict.list.section.INTERNAL': 'Внутренние',
'dict.list.section.RESTRICTED': 'Ограниченные',
'dict.list.section.PUBLIC.short': 'PUB',
'dict.list.section.INTERNAL.short': 'INT',
'dict.list.section.RESTRICTED.short': 'RES',
'dict.list.deps.short': 'СО СВЯЗЯМИ',
// Catalog v2 (URL-driven filters + sort + density)
'dict.list.heading': 'Каталог справочников',
'dict.list.filter.scope': 'Фильтр по области',
@@ -695,6 +699,10 @@ i18n
'dict.list.section.PUBLIC': 'Public',
'dict.list.section.INTERNAL': 'Internal',
'dict.list.section.RESTRICTED': 'Restricted',
'dict.list.section.PUBLIC.short': 'PUB',
'dict.list.section.INTERNAL.short': 'INT',
'dict.list.section.RESTRICTED.short': 'RES',
'dict.list.deps.short': 'WITH LINKS',
// Catalog v2
'dict.list.heading': 'Dictionary catalog',
'dict.list.filter.scope': 'Scope filter',
@@ -213,9 +213,11 @@ function DictionariesPage() {
actions={createButton}
/>
{/* Toolbar: search + count + scope chips + "deps only" */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex-1 min-w-[300px] max-w-md">
{/* Compact toolbar: всё в одной строке (wraps на narrow viewport).
Tradeoff: scope/bundle chips меньше padding'а + краткие labels.
UPPERCASE labels оставлены для consistency с brand voice (Tektur-like). */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<div className="flex-1 min-w-[240px] max-w-sm">
<SearchInput
value={search.q ?? ''}
onChange={(e) => setSearch({ q: e.target.value })}
@@ -224,14 +226,15 @@ function DictionariesPage() {
/>
</div>
<span className="text-2xs uppercase tracking-label text-carbon/70 whitespace-nowrap">
{t('dict.list.found', { shown: filtered.length, total: data.length })}
<span className="text-2xs uppercase tracking-label text-carbon/60 whitespace-nowrap font-mono">
{filtered.length}/{data.length}
</span>
{/* Scope chips: компактные, dot + count, full label по hover'у через title */}
<div
role="group"
aria-label={t('dict.list.filter.scope')}
className="flex items-center gap-1.5 flex-wrap"
className="flex items-center gap-1"
>
{SCOPE_ORDER.map((scope) => {
const selected = scopeFilter.has(scope)
@@ -242,76 +245,86 @@ function DictionariesPage() {
type="button"
onClick={() => toggleScope(scope)}
aria-pressed={selected}
className={`px-3 py-1.5 rounded-full text-2xs uppercase tracking-label flex items-center gap-1.5 border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
title={t(`dict.list.section.${scope}`)}
className={`px-2 py-1 rounded-full text-2xs uppercase tracking-label flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 font-mono ${
selected
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
}`}
>
<span
className={`inline-block size-2 rounded-full ${SCOPE_DOT[scope]}`}
className={`inline-block size-1.5 rounded-full ${SCOPE_DOT[scope]}`}
aria-hidden="true"
/>
{t(`dict.list.section.${scope}`)} · {count}
{t(`dict.list.section.${scope}.short`)}
<span className="text-carbon/60 ml-0.5">{count}</span>
</button>
)
})}
</div>
{/* Vertical divider */}
<span className="h-5 w-px bg-regolith" aria-hidden="true" />
{/* Bundle filter — chips inline */}
<div
role="group"
aria-label="Bundle"
className="flex items-center gap-1"
>
<button
type="button"
onClick={() => setBundle(undefined)}
aria-pressed={!bundleFilter}
className={`px-2 py-1 rounded-sm text-2xs uppercase tracking-label border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
!bundleFilter
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
}`}
>
{t('dict.list.bundle.all')}
</button>
{Array.from(bundleCounts.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([bundle, count]) => {
const selected = bundleFilter === bundle
return (
<button
key={bundle}
type="button"
onClick={() => setBundle(selected ? undefined : bundle)}
aria-pressed={selected}
className={`px-2 py-1 rounded-sm text-2xs uppercase tracking-label flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 font-mono ${
selected
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
}`}
>
{bundle}
<span className="text-carbon/60">{count}</span>
</button>
)
})}
</div>
{/* Vertical divider + deps toggle */}
<span className="h-5 w-px bg-regolith" aria-hidden="true" />
<button
type="button"
onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
aria-pressed={withDepsOnly}
className={`px-3 py-1.5 rounded-sm text-2xs uppercase tracking-label border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
title={t('dict.list.deps.only')}
className={`px-2 py-1 rounded-sm text-2xs uppercase tracking-label border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
withDepsOnly
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
}`}
>
{t('dict.list.deps.only')}
{t('dict.list.deps.short')}
</button>
</div>
{/* Bundle filter row */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-2xs uppercase tracking-label text-carbon/70 mr-1 font-mono">
BUNDLE
</span>
<button
type="button"
onClick={() => setBundle(undefined)}
aria-pressed={!bundleFilter}
className={`px-3 py-1 rounded-sm text-2xs uppercase tracking-label border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
!bundleFilter
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
}`}
>
{t('dict.list.bundle.all')}
</button>
{Array.from(bundleCounts.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([bundle, count]) => {
const selected = bundleFilter === bundle
return (
<button
key={bundle}
type="button"
onClick={() => setBundle(selected ? undefined : bundle)}
aria-pressed={selected}
className={`px-3 py-1 rounded-sm text-2xs uppercase tracking-label border transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 font-mono ${
selected
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
}`}
>
{bundle}
<span className="ml-1.5 text-carbon/60">{count}</span>
</button>
)
})}
</div>
{/* Empty state */}
{filtered.length === 0 ? (
<div className="border border-regolith rounded-lg p-12 text-center bg-orbit/10">