fix(admin-ui): compact catalog toolbar (single row)
This commit is contained in:
@@ -225,6 +225,10 @@ i18n
|
|||||||
'dict.list.section.PUBLIC': 'Публичные',
|
'dict.list.section.PUBLIC': 'Публичные',
|
||||||
'dict.list.section.INTERNAL': 'Внутренние',
|
'dict.list.section.INTERNAL': 'Внутренние',
|
||||||
'dict.list.section.RESTRICTED': 'Ограниченные',
|
'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)
|
// Catalog v2 (URL-driven filters + sort + density)
|
||||||
'dict.list.heading': 'Каталог справочников',
|
'dict.list.heading': 'Каталог справочников',
|
||||||
'dict.list.filter.scope': 'Фильтр по области',
|
'dict.list.filter.scope': 'Фильтр по области',
|
||||||
@@ -695,6 +699,10 @@ i18n
|
|||||||
'dict.list.section.PUBLIC': 'Public',
|
'dict.list.section.PUBLIC': 'Public',
|
||||||
'dict.list.section.INTERNAL': 'Internal',
|
'dict.list.section.INTERNAL': 'Internal',
|
||||||
'dict.list.section.RESTRICTED': 'Restricted',
|
'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
|
// Catalog v2
|
||||||
'dict.list.heading': 'Dictionary catalog',
|
'dict.list.heading': 'Dictionary catalog',
|
||||||
'dict.list.filter.scope': 'Scope filter',
|
'dict.list.filter.scope': 'Scope filter',
|
||||||
|
|||||||
@@ -213,9 +213,11 @@ function DictionariesPage() {
|
|||||||
actions={createButton}
|
actions={createButton}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Toolbar: search + count + scope chips + "deps only" */}
|
{/* Compact toolbar: всё в одной строке (wraps на narrow viewport).
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
Tradeoff: scope/bundle chips меньше padding'а + краткие labels.
|
||||||
<div className="flex-1 min-w-[300px] max-w-md">
|
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
|
<SearchInput
|
||||||
value={search.q ?? ''}
|
value={search.q ?? ''}
|
||||||
onChange={(e) => setSearch({ q: e.target.value })}
|
onChange={(e) => setSearch({ q: e.target.value })}
|
||||||
@@ -224,14 +226,15 @@ function DictionariesPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-2xs uppercase tracking-label text-carbon/70 whitespace-nowrap">
|
<span className="text-2xs uppercase tracking-label text-carbon/60 whitespace-nowrap font-mono">
|
||||||
{t('dict.list.found', { shown: filtered.length, total: data.length })}
|
{filtered.length}/{data.length}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{/* Scope chips: компактные, dot + count, full label по hover'у через title */}
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-label={t('dict.list.filter.scope')}
|
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) => {
|
{SCOPE_ORDER.map((scope) => {
|
||||||
const selected = scopeFilter.has(scope)
|
const selected = scopeFilter.has(scope)
|
||||||
@@ -242,76 +245,86 @@ function DictionariesPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleScope(scope)}
|
onClick={() => toggleScope(scope)}
|
||||||
aria-pressed={selected}
|
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
|
selected
|
||||||
? 'border-ultramarain bg-orbit/40 text-ultramarain'
|
? 'border-ultramarain bg-orbit/40 text-ultramarain'
|
||||||
: 'border-regolith hover:border-ultramarain/60 text-carbon'
|
: 'border-regolith hover:border-ultramarain/60 text-carbon'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
<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"
|
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>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
|
onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
|
||||||
aria-pressed={withDepsOnly}
|
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
|
withDepsOnly
|
||||||
? 'border-ultramarain bg-orbit/40 text-ultramarain'
|
? 'border-ultramarain bg-orbit/40 text-ultramarain'
|
||||||
: 'border-regolith hover:border-ultramarain/60 text-carbon'
|
: 'border-regolith hover:border-ultramarain/60 text-carbon'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{t('dict.list.deps.only')}
|
{t('dict.list.deps.short')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Empty state */}
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="border border-regolith rounded-lg p-12 text-center bg-orbit/10">
|
<div className="border border-regolith rounded-lg p-12 text-center bg-orbit/10">
|
||||||
|
|||||||
Reference in New Issue
Block a user