feat(admin-ui): catalog refresh + token migration (Stage 3.2)

This commit is contained in:
Александр Зимин
2026-05-11 09:08:38 +00:00
parent 3e19fd2cf9
commit 6db2a0c345
24 changed files with 289 additions and 273 deletions
@@ -84,10 +84,11 @@ export const groupByBundle = (
}
// Scope strip color — vertical 3px на левом крае карточки.
// Использует design handoff tokens — auto-switch в dark mode.
const SCOPE_STRIP: Record<DataScope, string> = {
PUBLIC: 'bg-emerald-500',
INTERNAL: 'bg-amber-500',
RESTRICTED: 'bg-rose-500',
PUBLIC: 'bg-accent',
INTERNAL: 'bg-warn',
RESTRICTED: 'bg-pink',
}
// ===== Route =====
@@ -252,7 +253,7 @@ function DictionariesPage() {
/>
</div>
<span className="text-2xs uppercase tracking-label text-carbon/60 whitespace-nowrap font-mono">
<span className="text-2xs uppercase tracking-[0.10em] text-mute whitespace-nowrap font-mono">
{filtered.length}/{data.length}
</span>
@@ -272,10 +273,10 @@ function DictionariesPage() {
onClick={() => toggleScope(scope)}
aria-pressed={selected}
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 ${
className={`px-2 py-1 rounded-full text-2xs uppercase tracking-[0.10em] flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 font-mono ${
selected
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
? 'border-accent bg-accent-bg text-accent'
: 'border-line hover:border-accent/60 text-ink'
}`}
>
<span
@@ -283,14 +284,14 @@ function DictionariesPage() {
aria-hidden="true"
/>
{t(`dict.list.section.${scope}.short`)}
<span className="text-carbon/60 ml-0.5">{count}</span>
<span className="text-mute ml-0.5">{count}</span>
</button>
)
})}
</div>
{/* Vertical divider */}
<span className="h-5 w-px bg-regolith" aria-hidden="true" />
<span className="h-5 w-px bg-line" aria-hidden="true" />
{/* Bundle filter — chips inline */}
<div
@@ -302,10 +303,10 @@ function DictionariesPage() {
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 ${
className={`px-2 py-1 rounded-sm text-2xs uppercase tracking-[0.10em] border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
!bundleFilter
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
? 'border-accent bg-accent-bg text-accent'
: 'border-line hover:border-accent/60 text-ink'
}`}
>
{t('dict.list.bundle.all')}
@@ -320,31 +321,31 @@ function DictionariesPage() {
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 ${
className={`px-2 py-1 rounded-sm text-2xs uppercase tracking-[0.10em] flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 font-mono ${
selected
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
? 'border-accent bg-accent-bg text-accent'
: 'border-line hover:border-accent/60 text-ink'
}`}
>
{bundle}
<span className="text-carbon/60">{count}</span>
<span className="text-mute">{count}</span>
</button>
)
})}
</div>
{/* Vertical divider + deps toggle */}
<span className="h-5 w-px bg-regolith" aria-hidden="true" />
<span className="h-5 w-px bg-line" aria-hidden="true" />
<button
type="button"
onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
aria-pressed={withDepsOnly}
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 ${
className={`px-2 py-1 rounded-sm text-2xs uppercase tracking-[0.10em] border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
withDepsOnly
? 'border-ultramarain bg-orbit/40 text-ultramarain'
: 'border-regolith hover:border-ultramarain/60 text-carbon'
? 'border-accent bg-accent-bg text-accent'
: 'border-line hover:border-accent/60 text-ink'
}`}
>
{t('dict.list.deps.short')}
@@ -353,8 +354,8 @@ function DictionariesPage() {
{/* Empty state */}
{filtered.length === 0 ? (
<div className="border border-regolith rounded-lg p-12 text-center bg-orbit/10">
<p className="font-primary text-lg text-ultramarain mb-2">
<div className="border border-line rounded-lg p-12 text-center bg-surface-2">
<p className="font-sans text-lg text-accent mb-2">
{t('dict.list.search.empty')}
</p>
{filtersActive && (
@@ -367,11 +368,11 @@ function DictionariesPage() {
// Bundle-grouped 2-col card grid
Array.from(grouped.entries()).map(([bundle, items]) => (
<section key={bundle} className="space-y-3">
<h2 className="flex items-baseline gap-3 pb-2 border-b border-regolith">
<span className="text-sm uppercase tracking-label text-ultramarain font-mono font-medium">
<h2 className="flex items-baseline gap-3 pb-2 border-b border-line">
<span className="text-sm uppercase tracking-[0.10em] text-accent font-mono font-medium">
{bundle}
</span>
<span className="text-2xs uppercase tracking-label text-carbon/60">
<span className="text-2xs uppercase tracking-[0.10em] text-mute">
· {items.length} {t('dict.list.records.short')}
</span>
</h2>
@@ -433,10 +434,11 @@ const FkChip = ({
to="/dictionaries/$name"
params={{ name: to }}
onClick={(e) => e.stopPropagation()}
className={`inline-flex items-center px-1.5 py-0.5 rounded font-mono text-[11px] transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
title={to}
className={`inline-flex items-center px-[7px] py-[2px] rounded-[4px] font-mono text-[11px] transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
dim
? 'border border-dashed border-regolith text-carbon/60 hover:border-ultramarain/60 hover:text-ultramarain'
: 'border border-regolith bg-orbit/30 text-ultramarain hover:bg-orbit/60'
? 'border border-dashed border-line text-mute hover:border-accent/60 hover:text-accent'
: 'bg-accent-bg text-accent hover:bg-accent hover:text-on-accent'
}`}
>
{label}
@@ -457,30 +459,30 @@ const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => {
<Link
to="/dictionaries/$name"
params={{ name: d.name }}
className="grid grid-cols-[3px_1fr_auto] bg-white border border-regolith rounded-lg overflow-hidden transition hover:shadow-hover hover:-translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ultramarain/40"
className="grid grid-cols-[3px_1fr_auto] bg-surface border border-line rounded-lg overflow-hidden transition-all hover:-translate-y-px hover:shadow-md hover:border-line-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
{/* Scope strip 3px вертикальная полоса */}
{/* Scope strip 3px вертикальная полоса (handoff spec) */}
<span className={SCOPE_STRIP[d.scope]} aria-hidden="true" />
{/* Body */}
<div className="px-3.5 py-3 min-w-0">
<div className="flex items-baseline gap-2.5 flex-wrap">
<h3 className="font-primary text-[15px] font-semibold text-ultramarain truncate">
<h3 className="text-[15px] font-semibold text-navy truncate">
{d.displayName ?? d.name}
</h3>
<span className="font-mono text-[11px] text-carbon/60 truncate">{d.name}</span>
<span className="font-mono text-[11px] text-mute truncate">{d.name}</span>
{d.approvalRequired && (
<Badge variant="warning">{t('dict.list.approval')}</Badge>
)}
</div>
{d.description && (
<p className="text-[12px] text-carbon mt-1 leading-snug line-clamp-2">
<p className="text-[12px] text-ink-2 mt-1 leading-snug line-clamp-2">
{d.description}
</p>
)}
{hasFkRow && (
<div className="flex flex-wrap items-center gap-1.5 mt-2.5">
<span className="text-2xs uppercase tracking-label text-carbon/60 mr-1">
<span className="text-2xs font-display uppercase tracking-[0.10em] text-mute mr-1">
{t('dict.list.fk.usedBy', { count: refBy.length })}
</span>
{refBy.slice(0, MAX_REFBY_CHIPS).map((dep) => (
@@ -492,7 +494,7 @@ const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => {
/>
))}
{refBy.length > MAX_REFBY_CHIPS && (
<span className="font-mono text-[11px] text-carbon/60">
<span className="font-mono text-[11px] text-mute">
+{refBy.length - MAX_REFBY_CHIPS}
</span>
)}
@@ -503,11 +505,11 @@ const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => {
{/* Right rail: scope badge + version */}
<div className="px-3.5 py-3 flex flex-col items-end gap-1.5 shrink-0">
<Badge variant="info">{d.scope}</Badge>
<span className="font-mono text-[11px] text-carbon/60 whitespace-nowrap">
<span className="font-mono text-[11px] text-mute whitespace-nowrap">
v{d.schemaVersion}
</span>
{typeof d.recordCount === 'number' && (
<span className="text-2xs uppercase tracking-label text-carbon/50 whitespace-nowrap">
<span className="text-2xs font-display uppercase tracking-[0.10em] text-mute/80 whitespace-nowrap">
{t('dict.list.recordCount', { count: d.recordCount })}
</span>
)}