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 { useDeferredValue, useMemo, useState } from 'react'
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 { PlusIcon } from '@phosphor-icons/react'
|
||||||
import { useQueries } from '@tanstack/react-query'
|
import { useQueries } from '@tanstack/react-query'
|
||||||
import { dictionaryDependentsQuery, useDictionaries, useDictionaryDependents } from '@/api/queries'
|
import { dictionaryDependentsQuery, useDictionaries, useDictionaryDependents } from '@/api/queries'
|
||||||
@@ -167,11 +167,6 @@ function DictionariesPage() {
|
|||||||
return out
|
return out
|
||||||
}, [data])
|
}, [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 filtersActive = Boolean(q) || scopeFilter.size > 0 || bundleFilter || withDepsOnly
|
||||||
const resetFilters = () => navigate({ search: {} })
|
const resetFilters = () => navigate({ search: {} })
|
||||||
@@ -223,40 +218,27 @@ function DictionariesPage() {
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-label={t('dict.list.heading')}
|
aria-label={t('dict.list.heading')}
|
||||||
className="space-y-6"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<PageHeader
|
{/* PageHeader per redesign: title + inline "N шт." count, без description.
|
||||||
title={t('nav.dictionaries')}
|
Actions удалены — + Создать переехала в toolbar справа от Граф ⇄. */}
|
||||||
description={t('dict.list.subtitle')}
|
<div className="flex items-baseline gap-3">
|
||||||
actions={createButton}
|
<h1 className="text-title-lg text-ink">{t('nav.dictionaries')}</h1>
|
||||||
/>
|
<span className="text-cap text-mute">
|
||||||
|
{data.length} {t('dict.list.countSuffix', { defaultValue: 'шт.' })}
|
||||||
{/* 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}
|
|
||||||
</span>
|
</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
|
<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"
|
className="flex items-center gap-1.5"
|
||||||
>
|
>
|
||||||
{SCOPE_ORDER.map((scope) => {
|
{SCOPE_ORDER.map((scope) => {
|
||||||
const selected = scopeFilter.has(scope)
|
const selected = scopeFilter.has(scope)
|
||||||
const count = scopeCounts[scope]
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={scope}
|
key={scope}
|
||||||
@@ -264,27 +246,22 @@ function DictionariesPage() {
|
|||||||
onClick={() => toggleScope(scope)}
|
onClick={() => toggleScope(scope)}
|
||||||
aria-pressed={selected}
|
aria-pressed={selected}
|
||||||
title={t(`dict.list.section.${scope}`)}
|
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
|
selected
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
? 'border-ink bg-surface text-ink ring-1 ring-ink'
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
: 'border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span
|
{scope}
|
||||||
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>
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vertical divider */}
|
{/* Vertical divider между scope и bundle */}
|
||||||
<span className="h-5 w-px bg-line" aria-hidden="true" />
|
<span className="h-6 w-px bg-line shrink-0" aria-hidden="true" />
|
||||||
|
|
||||||
{/* Bundle filter — chips inline */}
|
{/* Bundle filter — все / cuod / geo / i18n / core */}
|
||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="Bundle"
|
aria-label="Bundle"
|
||||||
@@ -294,17 +271,17 @@ function DictionariesPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setBundle(undefined)}
|
onClick={() => setBundle(undefined)}
|
||||||
aria-pressed={!bundleFilter}
|
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
|
!bundleFilter
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
? 'border-ink bg-surface text-ink ring-1 ring-ink font-medium'
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
: '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>
|
</button>
|
||||||
{Array.from(bundleCounts.entries())
|
{Array.from(bundleCounts.entries())
|
||||||
.sort(([a], [b]) => a.localeCompare(b))
|
.sort(([a], [b]) => a.localeCompare(b))
|
||||||
.map(([bundle, count]) => {
|
.map(([bundle]) => {
|
||||||
const selected = bundleFilter === bundle
|
const selected = bundleFilter === bundle
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -312,35 +289,39 @@ function DictionariesPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setBundle(selected ? undefined : bundle)}
|
onClick={() => setBundle(selected ? undefined : bundle)}
|
||||||
aria-pressed={selected}
|
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
|
selected
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
? 'border-ink bg-surface text-ink ring-1 ring-ink font-medium'
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
: 'border-line bg-surface text-ink-2 hover:border-ink-2 hover:bg-surface-2'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{bundle}
|
{bundle}
|
||||||
<span className="text-mute">{count}</span>
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vertical divider + deps toggle */}
|
{/* Spacer */}
|
||||||
<span className="h-5 w-px bg-line" aria-hidden="true" />
|
<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
|
{/* Граф ⇄ — link to relations graph view */}
|
||||||
type="button"
|
<button
|
||||||
onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
|
type="button"
|
||||||
aria-pressed={withDepsOnly}
|
onClick={() => navigate({ to: '/graph' })}
|
||||||
title={t('dict.list.deps.only')}
|
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"
|
||||||
className={`text-cap px-2 py-1 rounded-sm border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
title={t('dict.list.graph', { defaultValue: 'Граф связей' })}
|
||||||
withDepsOnly
|
>
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
{t('dict.list.graph', { defaultValue: 'Граф' })}
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
<span aria-hidden className="text-mute">⇄</span>
|
||||||
}`}
|
</button>
|
||||||
>
|
|
||||||
{t('dict.list.deps.short')}
|
{/* + Создать (moved here from PageHeader actions per redesign) */}
|
||||||
</button>
|
{createButton}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
|
|||||||
Reference in New Issue
Block a user