feat(catalog): toolbar layout per redesign prototype
Match handoff screenshot: - Title "Справочники" + inline "N шт." count caption (no description text) - Remove page-level SearchInput (TopBar already has global search ⌘K) - Reorder toolbar to single row: [scope pills PUBLIC|INTERNAL|RESTRICTED] | divider | [bundle pills все|cuod|geo|i18n|core] | spacer | N/M counter [Граф ⇄] (→ /graph) [+ Создать] (moved here from PageHeader actions) - Scope/bundle pills are bordered chips с ring-1 для active state - Drop deps-only toggle (not in redesign) - Drop scopeCounts useMemo (counts moved out of chip labels per design)
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
import { useDeferredValue, useMemo, useState } from 'react'
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader, SearchInput } from '@/ui'
|
||||
import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader } from '@/ui'
|
||||
import { PlusIcon } from '@phosphor-icons/react'
|
||||
import { useQueries } from '@tanstack/react-query'
|
||||
import { dictionaryDependentsQuery, useDictionaries, useDictionaryDependents } from '@/api/queries'
|
||||
@@ -167,11 +167,6 @@ function DictionariesPage() {
|
||||
return out
|
||||
}, [data])
|
||||
|
||||
const scopeCounts = useMemo(() => {
|
||||
const out: Record<DataScope, number> = { PUBLIC: 0, INTERNAL: 0, RESTRICTED: 0 }
|
||||
if (data) for (const d of data) out[d.scope]++
|
||||
return out
|
||||
}, [data])
|
||||
|
||||
const filtersActive = Boolean(q) || scopeFilter.size > 0 || bundleFilter || withDepsOnly
|
||||
const resetFilters = () => navigate({ search: {} })
|
||||
@@ -223,40 +218,27 @@ function DictionariesPage() {
|
||||
return (
|
||||
<section
|
||||
aria-label={t('dict.list.heading')}
|
||||
className="space-y-6"
|
||||
className="space-y-4"
|
||||
>
|
||||
<PageHeader
|
||||
title={t('nav.dictionaries')}
|
||||
description={t('dict.list.subtitle')}
|
||||
actions={createButton}
|
||||
/>
|
||||
|
||||
{/* 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 })}
|
||||
placeholder={t('dict.list.search.placeholder')}
|
||||
aria-label={t('dict.list.search.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-mono text-mute whitespace-nowrap tabular-nums">
|
||||
{filtered.length}/{data.length}
|
||||
{/* PageHeader per redesign: title + inline "N шт." count, без description.
|
||||
Actions удалены — + Создать переехала в toolbar справа от Граф ⇄. */}
|
||||
<div className="flex items-baseline gap-3">
|
||||
<h1 className="text-title-lg text-ink">{t('nav.dictionaries')}</h1>
|
||||
<span className="text-cap text-mute">
|
||||
{data.length} {t('dict.list.countSuffix', { defaultValue: 'шт.' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Scope chips: компактные, dot + count, full label по hover'у через title */}
|
||||
{/* Toolbar per redesign prototype: [scope pills] | [bundle pills] [...] N/M [Граф ⇄] [+ Создать] */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Scope filter: PUBLIC / INTERNAL / RESTRICTED — pill chips с border */}
|
||||
<div
|
||||
role="group"
|
||||
aria-label={t('dict.list.filter.scope')}
|
||||
className="flex items-center gap-1"
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
{SCOPE_ORDER.map((scope) => {
|
||||
const selected = scopeFilter.has(scope)
|
||||
const count = scopeCounts[scope]
|
||||
return (
|
||||
<button
|
||||
key={scope}
|
||||
@@ -264,27 +246,22 @@ function DictionariesPage() {
|
||||
onClick={() => toggleScope(scope)}
|
||||
aria-pressed={selected}
|
||||
title={t(`dict.list.section.${scope}`)}
|
||||
className={`text-cap px-2 py-1 rounded-full flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
className={`text-cap tracking-[0.14em] uppercase font-semibold px-3 py-1.5 rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
selected
|
||||
? 'border-accent bg-accent-bg text-accent'
|
||||
: 'border-line hover:border-accent/60 text-ink'
|
||||
? 'border-ink bg-surface text-ink ring-1 ring-ink'
|
||||
: 'border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block size-1.5 rounded-full ${SCOPE_DOT[scope]}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(`dict.list.section.${scope}.short`)}
|
||||
<span className="text-mute ml-0.5">{count}</span>
|
||||
{scope}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Vertical divider */}
|
||||
<span className="h-5 w-px bg-line" aria-hidden="true" />
|
||||
{/* Vertical divider между scope и bundle */}
|
||||
<span className="h-6 w-px bg-line shrink-0" aria-hidden="true" />
|
||||
|
||||
{/* Bundle filter — chips inline */}
|
||||
{/* Bundle filter — все / cuod / geo / i18n / core */}
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Bundle"
|
||||
@@ -294,17 +271,17 @@ function DictionariesPage() {
|
||||
type="button"
|
||||
onClick={() => setBundle(undefined)}
|
||||
aria-pressed={!bundleFilter}
|
||||
className={`text-cap px-2 py-1 rounded-sm border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
className={`text-body px-3 py-1.5 rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
!bundleFilter
|
||||
? 'border-accent bg-accent-bg text-accent'
|
||||
: 'border-line hover:border-accent/60 text-ink'
|
||||
? 'border-ink bg-surface text-ink ring-1 ring-ink font-medium'
|
||||
: 'border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2'
|
||||
}`}
|
||||
>
|
||||
{t('dict.list.bundle.all')}
|
||||
{t('dict.list.bundle.all', { defaultValue: 'все' })}
|
||||
</button>
|
||||
{Array.from(bundleCounts.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([bundle, count]) => {
|
||||
.map(([bundle]) => {
|
||||
const selected = bundleFilter === bundle
|
||||
return (
|
||||
<button
|
||||
@@ -312,35 +289,39 @@ function DictionariesPage() {
|
||||
type="button"
|
||||
onClick={() => setBundle(selected ? undefined : bundle)}
|
||||
aria-pressed={selected}
|
||||
className={`text-cap px-2 py-1 rounded-sm flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
className={`text-mono text-body px-3 py-1.5 rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
selected
|
||||
? 'border-accent bg-accent-bg text-accent'
|
||||
: 'border-line hover:border-accent/60 text-ink'
|
||||
? 'border-ink bg-surface text-ink ring-1 ring-ink font-medium'
|
||||
: 'border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2'
|
||||
}`}
|
||||
>
|
||||
{bundle}
|
||||
<span className="text-mute">{count}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Vertical divider + deps toggle */}
|
||||
<span className="h-5 w-px bg-line" aria-hidden="true" />
|
||||
{/* Spacer */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{/* N / M counter */}
|
||||
<span className="text-cell text-mute whitespace-nowrap tabular-nums">
|
||||
{filtered.length} / {data.length}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
|
||||
aria-pressed={withDepsOnly}
|
||||
title={t('dict.list.deps.only')}
|
||||
className={`text-cap px-2 py-1 rounded-sm border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||
withDepsOnly
|
||||
? 'border-accent bg-accent-bg text-accent'
|
||||
: 'border-line hover:border-accent/60 text-ink'
|
||||
}`}
|
||||
>
|
||||
{t('dict.list.deps.short')}
|
||||
</button>
|
||||
{/* Граф ⇄ — link to relations graph view */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate({ to: '/graph' })}
|
||||
className="h-9 px-3 rounded-md border border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2 text-body inline-flex items-center gap-1.5 transition-colors"
|
||||
title={t('dict.list.graph', { defaultValue: 'Граф связей' })}
|
||||
>
|
||||
{t('dict.list.graph', { defaultValue: 'Граф' })}
|
||||
<span aria-hidden className="text-mute">⇄</span>
|
||||
</button>
|
||||
|
||||
{/* + Создать (moved here from PageHeader actions per redesign) */}
|
||||
{createButton}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
|
||||
Reference in New Issue
Block a user