feat(admin-ui): scope accent on dictionary detail page

Перенесли scope-визуальные константы (SCOPE_ORDER, SCOPE_DOT,
SCOPE_ACCENT) в `lib/scope-style.ts` для переиспользования. На detail
странице (`dictionaries/$name`) добавили top accent strip 4px цветом
scope + цветной чип в breadcrumb рядом с back-link. Теперь при переходе
из списка в карточку scope сразу виден без чтения badge в таблице.
This commit is contained in:
Zimin A.N.
2026-05-06 12:50:44 +03:00
parent 353130c529
commit 3f2a6b5d53
3 changed files with 66 additions and 17 deletions
+42
View File
@@ -0,0 +1,42 @@
import type { DataScope } from '@/api/client'
/**
* Visual ordering: PUBLIC → INTERNAL → RESTRICTED.
* Reads as escalating sensitivity, matches semaphore mental model.
*/
export const SCOPE_ORDER: ReadonlyArray<DataScope> = [
'PUBLIC',
'INTERNAL',
'RESTRICTED',
]
/**
* Solid background classes for ●-shaped scope indicators (size-2 dots,
* section-header bullets).
*/
export const SCOPE_DOT: Record<DataScope, string> = {
PUBLIC: 'bg-emerald-500',
INTERNAL: 'bg-amber-500',
RESTRICTED: 'bg-rose-500',
}
/**
* `before:` background classes used for the 4px left-accent stripe on
* dictionary cards. Pair with `relative` + `before:absolute before:left-0
* before:top-0 before:bottom-0 before:w-1 before:rounded-l-lg`.
*/
export const SCOPE_ACCENT: Record<DataScope, string> = {
PUBLIC: 'before:bg-emerald-500',
INTERNAL: 'before:bg-amber-500',
RESTRICTED: 'before:bg-rose-500',
}
/**
* Solid border-top classes for page-level scope banner stripes (used on
* dictionary detail page).
*/
export const SCOPE_BORDER_TOP: Record<DataScope, string> = {
PUBLIC: 'border-t-emerald-500',
INTERNAL: 'border-t-amber-500',
RESTRICTED: 'border-t-rose-500',
}
@@ -28,6 +28,7 @@ import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog' import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer' import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer'
import { nowIsoLocal } from '@/lib/dates' import { nowIsoLocal } from '@/lib/dates'
import { SCOPE_BORDER_TOP, SCOPE_DOT } from '@/lib/scope-style'
export const Route = createFileRoute('/dictionaries/$name')({ export const Route = createFileRoute('/dictionaries/$name')({
component: DictionaryDetail, component: DictionaryDetail,
@@ -87,14 +88,33 @@ function DictionaryDetail() {
const serverError = serverErrorMessage(activeMutation?.error) const serverError = serverErrorMessage(activeMutation?.error)
const totalRecords = recordsResult.data?.length ?? 0 const totalRecords = recordsResult.data?.length ?? 0
const scope = detailQuery.data?.scope
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{scope && (
<div
className={`-mt-6 -mx-4 sm:-mx-6 lg:-mx-8 mb-2 h-1 border-t-4 ${SCOPE_BORDER_TOP[scope]}`}
aria-hidden="true"
/>
)}
<PageHeader <PageHeader
breadcrumb={ breadcrumb={
<Link to="/dictionaries" className="text-sm text-carbon/70 hover:text-ultramarain"> <div className="flex items-center gap-3 text-sm">
<Link to="/dictionaries" className="text-carbon/70 hover:text-ultramarain">
{t('nav.dictionaries')} {t('nav.dictionaries')}
</Link> </Link>
{scope && (
<span className="flex items-center gap-1.5 text-carbon/70">
<span className="text-carbon/40">·</span>
<span
className={`inline-block size-2 rounded-full ${SCOPE_DOT[scope]}`}
aria-hidden="true"
/>
<span className="font-medium">{t(`dict.list.section.${scope}`)}</span>
</span>
)}
</div>
} }
title={detailQuery.data?.displayName ?? name} title={detailQuery.data?.displayName ?? name}
description={ description={
@@ -14,6 +14,7 @@ import { PlusIcon } from '@phosphor-icons/react'
import { useDictionaries } from '@/api/queries' import { useDictionaries } from '@/api/queries'
import type { DataScope, DictionaryDefinition } from '@/api/client' import type { DataScope, DictionaryDefinition } from '@/api/client'
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog' import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
import { SCOPE_ACCENT, SCOPE_DOT, SCOPE_ORDER } from '@/lib/scope-style'
export const Route = createFileRoute('/dictionaries/')({ export const Route = createFileRoute('/dictionaries/')({
component: DictionariesPage, component: DictionariesPage,
@@ -27,20 +28,6 @@ const matchesQuery = (d: DictionaryDefinition, q: string): boolean => {
return haystack.includes(q) return haystack.includes(q)
} }
const SCOPE_ORDER: ReadonlyArray<DataScope> = ['PUBLIC', 'INTERNAL', 'RESTRICTED']
const SCOPE_DOT: Record<DataScope, string> = {
PUBLIC: 'bg-emerald-500',
INTERNAL: 'bg-amber-500',
RESTRICTED: 'bg-rose-500',
}
const SCOPE_ACCENT: Record<DataScope, string> = {
PUBLIC: 'before:bg-emerald-500',
INTERNAL: 'before:bg-amber-500',
RESTRICTED: 'before:bg-rose-500',
}
function DictionariesPage() { function DictionariesPage() {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()