feat(admin-ui): catalog refresh + token migration (Stage 3.2)
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user