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
+14 -14
View File
@@ -299,7 +299,7 @@ function ActiveFilterChips({ filters, onRemove }: ActiveFilterChipsProps) {
const { t } = useTranslation()
return (
<div className="flex flex-wrap items-center gap-2">
<span className="text-2xs uppercase tracking-label text-carbon/70">
<span className="text-2xs uppercase tracking-[0.10em] text-ink-2">
{t('audit.filter.active')}
</span>
{filters.map((f) => (
@@ -309,14 +309,14 @@ function ActiveFilterChips({ filters, onRemove }: ActiveFilterChipsProps) {
onClick={() => onRemove(f.key)}
className={[
'inline-flex items-center gap-1.5 px-2 py-1 rounded-sm border text-2xs',
'bg-white border-regolith hover:border-carbon/40 hover:bg-regolith/30',
'bg-white border-line hover:border-carbon/40 hover:bg-line/30',
'transition-colors group',
].join(' ')}
title={t('audit.filter.removeChip')}
>
<span className="text-carbon/70">{f.label}:</span>
<span className="text-ink-2">{f.label}:</span>
<span className="font-medium text-black">{f.value}</span>
<XIcon size={12} className="text-carbon/50 group-hover:text-mars" />
<XIcon size={12} className="text-mute group-hover:text-mars" />
</button>
))}
</div>
@@ -356,7 +356,7 @@ function FilterPanel({
}
return (
<div className="bg-white border border-regolith rounded-lg p-4 space-y-3">
<div className="bg-white border border-line rounded-lg p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<SingleSelect
label={t('audit.filter.dictionary')}
@@ -411,7 +411,7 @@ function FilterPanel({
}
/>
</div>
<div className="flex justify-end gap-2 pt-2 border-t border-regolith">
<div className="flex justify-end gap-2 pt-2 border-t border-line">
<Button type="button" variant="secondary" onClick={onReset}>
{t('audit.filter.reset')}
</Button>
@@ -486,7 +486,7 @@ function AuditRow({ row }: { row: AuditEntry }) {
</TableCell>
<TableCell>
<span
className="font-mono text-2xs text-carbon/70"
className="font-mono text-2xs text-ink-2"
title={row.traceId ?? ''}
>
{row.traceId ? row.traceId.slice(0, 8) : '—'}
@@ -504,29 +504,29 @@ function AuditRow({ row }: { row: AuditEntry }) {
{open && (
<TableRow>
<TableCell colSpan={8}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 bg-regolith/30 rounded p-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 bg-line/30 rounded p-3">
<div>
<div className="text-2xs uppercase tracking-label text-carbon/70 mb-1">
<div className="text-2xs uppercase tracking-[0.10em] text-ink-2 mb-1">
{t('audit.diff.before')}
</div>
<pre className="text-2xs font-mono whitespace-pre-wrap break-all bg-white border border-regolith rounded p-2 max-h-60 overflow-auto">
<pre className="text-2xs font-mono whitespace-pre-wrap break-all bg-white border border-line rounded p-2 max-h-60 overflow-auto">
{row.payloadBefore
? JSON.stringify(row.payloadBefore, null, 2)
: '—'}
</pre>
</div>
<div>
<div className="text-2xs uppercase tracking-label text-carbon/70 mb-1">
<div className="text-2xs uppercase tracking-[0.10em] text-ink-2 mb-1">
{t('audit.diff.after')}
</div>
<pre className="text-2xs font-mono whitespace-pre-wrap break-all bg-white border border-regolith rounded p-2 max-h-60 overflow-auto">
<pre className="text-2xs font-mono whitespace-pre-wrap break-all bg-white border border-line rounded p-2 max-h-60 overflow-auto">
{row.payloadAfter
? JSON.stringify(row.payloadAfter, null, 2)
: '—'}
</pre>
</div>
{(row.ipAddress || row.userAgent || row.requestId) && (
<div className="md:col-span-2 text-2xs text-carbon/70 flex flex-wrap gap-x-4 gap-y-1 pt-1">
<div className="md:col-span-2 text-2xs text-ink-2 flex flex-wrap gap-x-4 gap-y-1 pt-1">
{row.ipAddress && <span>IP: {row.ipAddress}</span>}
{row.requestId && <span>req: {row.requestId}</span>}
{row.userAgent && (
@@ -563,7 +563,7 @@ function Pagination({
return (
<div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-2 text-sm">
<span className="text-carbon/70">{t('audit.page.size')}</span>
<span className="text-ink-2">{t('audit.page.size')}</span>
<SingleSelect
options={PAGE_SIZE_OPTIONS}
value={String(size)}
@@ -493,12 +493,12 @@ function DictionaryDetail() {
<PageHeader
breadcrumb={
<div className="flex items-center gap-3 text-sm">
<Link to="/dictionaries" className="text-carbon/70 hover:text-ultramarain">
<Link to="/dictionaries" className="text-ink-2 hover:text-accent">
{t('nav.dictionaries')}
</Link>
{scope && (
<span className="flex items-center gap-1.5 text-carbon/70">
<span className="text-carbon/40">·</span>
<span className="flex items-center gap-1.5 text-ink-2">
<span className="text-mute/70">·</span>
<span
className={`inline-block size-2 rounded-full ${SCOPE_DOT[scope]}`}
aria-hidden="true"
@@ -569,7 +569,7 @@ function DictionaryDetail() {
{/* View toggle: Записи (default table-based view) | Связи (Hub view с
neighbors на боках). Per design handoff B. */}
{detailQuery.data && (
<div role="group" aria-label="View mode" className="flex border border-regolith rounded-sm overflow-hidden w-fit">
<div role="group" aria-label="View mode" className="flex border border-line rounded-sm overflow-hidden w-fit">
{(['records', 'hub'] as const).map((mode) => {
const active = (urlSearch.view ?? 'records') === mode
return (
@@ -586,10 +586,10 @@ function DictionaryDetail() {
})
}
aria-pressed={active}
className={`px-3 py-1 text-2xs uppercase tracking-label transition focus:outline-none focus:ring-2 focus:ring-ultramarain/40 ${
className={`px-3 py-1 text-2xs uppercase tracking-[0.10em] transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
active
? 'bg-orbit/40 text-ultramarain'
: 'hover:bg-orbit/20 text-carbon'
? 'bg-accent-bg text-accent'
: 'hover:bg-accent-bg/20 text-ink'
}`}
>
{t(`dict.view.${mode}`, {
@@ -611,8 +611,8 @@ function DictionaryDetail() {
)}
{aoi && (
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-ultramarain/30 bg-ultramarain/4 text-sm">
<span className="font-mono text-xs text-carbon/80">
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-accent/30 bg-accent/4 text-sm">
<span className="font-mono text-xs text-ink">
{aoi.kind === 'bbox'
? t('aoi.activeBbox', { value: aoi.bboxCsv })
: t('aoi.activePolygon', {
@@ -621,7 +621,7 @@ function DictionaryDetail() {
</span>
<button
type="button"
className="text-xs text-ultramarain hover:underline"
className="text-xs text-accent hover:underline"
onClick={handleAoiClear}
>
{t('aoi.clear')}
@@ -644,16 +644,16 @@ function DictionaryDetail() {
{/* Active state — ambient banner если ?at= задан, даже если picker закрыт. */}
{timeTravelAt && !timeTravelOpen && (
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-orbit/40 bg-orbit/8 text-sm">
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-orbit shrink-0" />
<span className="font-mono text-xs text-carbon/80">
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-warn bg-warn-bg text-sm">
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn shrink-0" />
<span className="font-mono text-xs text-ink">
{t('timeTravel.viewing', {
date: new Date(timeTravelAt).toLocaleString(),
})}
</span>
<button
type="button"
className="text-xs text-ultramarain hover:underline ml-auto"
className="text-xs text-accent hover:underline ml-auto"
onClick={() => setTimeTravelAt(undefined)}
>
{t('timeTravel.clear')}
@@ -694,10 +694,10 @@ function DictionaryDetail() {
onClick={() => toggleScope(s)}
aria-pressed={active}
className={[
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border text-2xs uppercase tracking-label font-secondary transition-colors',
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border text-2xs uppercase tracking-[0.10em] font-display transition-colors',
active
? 'border-ultramarain bg-ultramarain/8 text-ultramarain'
: 'border-regolith bg-white text-carbon/70 hover:border-carbon/40',
? 'border-accent bg-accent/8 text-accent'
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
].join(' ')}
>
<span
@@ -710,14 +710,14 @@ function DictionaryDetail() {
})}
</div>
{filtersActive && (
<div className="flex items-center gap-2 text-xs text-carbon/70">
<div className="flex items-center gap-2 text-xs text-ink-2">
<span>
{t('dict.filter.matched', { count: filteredCount, total: totalRecords })}
</span>
<button
type="button"
onClick={handleClearFilters}
className="text-ultramarain hover:underline"
className="text-accent hover:underline"
>
{t('dict.filter.clear')}
</button>
@@ -796,7 +796,7 @@ function DictionaryDetail() {
if (v == null || v === '') {
return (
<TableCell key={c.key}>
<span className="text-2xs text-carbon/40"></span>
<span className="text-2xs text-mute/70"></span>
</TableCell>
)
}
@@ -807,8 +807,8 @@ function DictionaryDetail() {
className={[
'inline-block px-1.5 py-0.5 rounded-sm font-mono text-2xs',
c.isFk
? 'bg-ultramarain/8 text-ultramarain'
: 'bg-carbon/8 text-carbon/80',
? 'bg-accent/8 text-accent'
: 'bg-carbon/8 text-ink',
].join(' ')}
>
{text}
@@ -820,7 +820,7 @@ function DictionaryDetail() {
<Badge variant="info">{r.dataScope}</Badge>
</TableCell>
<TableCell>
<span className="text-2xs text-carbon/60">
<span className="text-2xs text-mute">
{new Date(r.validFrom).toLocaleDateString()}
</span>
</TableCell>
@@ -943,7 +943,7 @@ function DictionaryDetail() {
>
{edit.kind === 'close-confirm' && (
<div className="space-y-4">
<p className="text-sm text-carbon">
<p className="text-sm text-ink">
{t('dict.confirmClose.body', { key: edit.record.businessKey })}
</p>
<TextInput
@@ -955,7 +955,7 @@ function DictionaryDetail() {
{serverErrorMessage(closeMut.error) && (
<Alert variant="error">{serverErrorMessage(closeMut.error)}</Alert>
)}
<div className="flex justify-end gap-2 pt-2 border-t border-regolith">
<div className="flex justify-end gap-2 pt-2 border-t border-line">
<Button
type="button"
variant="ghost"
@@ -1062,7 +1062,7 @@ function BulkSelectionToolbar({
<div
className={[
'sticky top-0 z-10 -mx-1 flex items-center justify-between gap-3',
'bg-ultramarain text-white rounded-md shadow-sm px-4 py-2',
'bg-accent text-white rounded-md shadow-sm px-4 py-2',
].join(' ')}
>
<div className="flex items-center gap-3">
@@ -1123,7 +1123,7 @@ function RecordsPagination({
const to = Math.min((page + 1) * pageSize, totalCount)
return (
<div className="flex items-center justify-between flex-wrap gap-3 px-2">
<span className="text-2xs text-carbon/70 tabular-nums">
<span className="text-2xs text-ink-2 tabular-nums">
{t('dict.page.range', { from, to, total: totalCount })}
</span>
<div className="flex items-center gap-3 text-sm">
@@ -1136,7 +1136,7 @@ function RecordsPagination({
>
{t('dict.page.prev')}
</Button>
<span className="tabular-nums text-2xs text-carbon/70">
<span className="tabular-nums text-2xs text-ink-2">
{t('dict.page.of', { cur: page + 1, total: totalPages })}
</span>
<Button
@@ -1202,7 +1202,7 @@ function BulkCloseDialog({
</div>
{(result.skipped.length > 0 || result.errors.length > 0) && (
<div className="border border-regolith rounded p-3 max-h-48 overflow-auto bg-regolith/30">
<div className="border border-line rounded p-3 max-h-48 overflow-auto bg-line/30">
<ul className="text-2xs font-mono space-y-1">
{result.skipped.map((s) => (
<li key={`s-${s.businessKey}`} className="text-saturn">
@@ -1218,7 +1218,7 @@ function BulkCloseDialog({
</div>
)}
<div className="flex justify-end pt-2 border-t border-regolith">
<div className="flex justify-end pt-2 border-t border-line">
<Button type="button" variant="primary" onClick={onClose}>
{t('dict.bulk.dismiss')}
</Button>
@@ -1229,7 +1229,7 @@ function BulkCloseDialog({
return (
<div className="space-y-4">
<p className="text-sm text-carbon">{t('dict.bulk.confirmBody', { count })}</p>
<p className="text-sm text-ink">{t('dict.bulk.confirmBody', { count })}</p>
<TextInput
label={t('dict.confirmClose.reason')}
placeholder={t('dict.confirmClose.reasonPlaceholder')}
@@ -1237,7 +1237,7 @@ function BulkCloseDialog({
onChange={(e) => onReasonChange(e.target.value)}
/>
{serverError && <Alert variant="error">{serverError}</Alert>}
<div className="flex justify-end gap-2 pt-2 border-t border-regolith">
<div className="flex justify-end gap-2 pt-2 border-t border-line">
<Button
type="button"
variant="ghost"
@@ -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>
)}
+5 -5
View File
@@ -82,7 +82,7 @@ function MyDraftsPage() {
{drafts.data && drafts.data.totalElements > 0 && (
<Panel>
<div className="flex items-center justify-between mb-3">
<p className="text-2xs text-carbon/60">
<p className="text-2xs text-mute">
{t('myDrafts.total', { count: drafts.data.totalElements })}
</p>
</div>
@@ -109,10 +109,10 @@ function MyDraftsPage() {
<TableCell>
<Badge variant={statusVariant(d.status)}>{d.status}</Badge>
</TableCell>
<TableCell className="text-2xs text-carbon/70">
<TableCell className="text-2xs text-ink-2">
{new Date(d.submittedAt).toLocaleString()}
</TableCell>
<TableCell className="text-2xs text-carbon/70">
<TableCell className="text-2xs text-ink-2">
{d.reviewedAt
? new Date(d.reviewedAt).toLocaleString()
: '—'}
@@ -120,7 +120,7 @@ function MyDraftsPage() {
<TableCell className="font-mono text-2xs">
{d.reviewerId ?? '—'}
</TableCell>
<TableCell className="text-2xs text-carbon/80 max-w-md">
<TableCell className="text-2xs text-ink max-w-md">
{d.reviewComment ?? d.makerComment ?? '—'}
</TableCell>
<TableCell>
@@ -134,7 +134,7 @@ function MyDraftsPage() {
{t('myDrafts.action.withdraw')}
</Button>
) : (
<span className="text-2xs text-carbon/40"></span>
<span className="text-2xs text-mute/70"></span>
)}
</TableCell>
</TableRow>
+5 -5
View File
@@ -105,7 +105,7 @@ function OutboxPage() {
<TableCell>{row.attemptCount}</TableCell>
<TableCell>
<span
className="text-2xs text-carbon/70 truncate max-w-md inline-block align-middle"
className="text-2xs text-ink-2 truncate max-w-md inline-block align-middle"
title={row.lastError ?? ''}
>
{row.lastError ?? '—'}
@@ -162,13 +162,13 @@ function StatCard({ label, value, tone }: StatCardProps) {
? 'text-mars'
: tone === 'success'
? 'text-grass'
: 'text-ultramarain'
: 'text-accent'
return (
<div className="bg-white border border-regolith rounded-lg p-4">
<div className="text-2xs uppercase tracking-label text-carbon/70 mb-1">
<div className="bg-white border border-line rounded-lg p-4">
<div className="text-2xs uppercase tracking-[0.10em] text-ink-2 mb-1">
{label}
</div>
<div className={`text-2xl font-primary tabular-nums ${colour}`}>{value}</div>
<div className={`text-2xl font-sans tabular-nums ${colour}`}>{value}</div>
</div>
)
}
+16 -16
View File
@@ -196,10 +196,10 @@ function ReviewsPage() {
{queue.data && queue.data.totalElements > 0 && (
<Panel>
<div className="flex items-center justify-between mb-3 gap-3">
<p className="text-2xs text-carbon/60">
<p className="text-2xs text-mute">
{t('reviews.queueTotal', { count: queue.data.totalElements })}
{anySelected ? (
<span className="ml-2 text-ultramarain font-medium">
<span className="ml-2 text-accent font-medium">
· {t('reviews.bulk.selectedCount', { count: selection.size })}
</span>
) : null}
@@ -264,7 +264,7 @@ function ReviewsPage() {
<Link
to="/dictionaries/$name"
params={{ name: d.dictionaryId }}
className="text-ultramarain hover:underline font-mono text-2xs"
className="text-accent hover:underline font-mono text-2xs"
>
{d.dictionaryId.slice(0, 8)}
</Link>
@@ -274,7 +274,7 @@ function ReviewsPage() {
<Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
</TableCell>
<TableCell className="font-mono text-2xs">{d.makerId}</TableCell>
<TableCell className="text-2xs text-carbon/70">
<TableCell className="text-2xs text-ink-2">
{new Date(d.submittedAt).toLocaleString()}
</TableCell>
<TableCell>
@@ -304,7 +304,7 @@ function ReviewsPage() {
title={t('reviews.bulk.rejectModalTitle', { count: selection.size })}
>
<div className="space-y-3">
<p className="text-2xs text-carbon/70">
<p className="text-2xs text-ink-2">
{t('reviews.bulk.rejectModalDescription')}
</p>
<TextArea
@@ -403,18 +403,18 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
<div className="flex flex-wrap items-center gap-2 text-2xs">
<Badge variant={operationVariant(draft.operation)}>{draft.operation}</Badge>
<span className="font-mono">{draft.businessKey}</span>
<span className="text-carbon/60">
<span className="text-mute">
{t('reviews.drawer.maker')}: {draft.makerId}
</span>
<span className="text-carbon/60">
<span className="text-mute">
{t('reviews.drawer.submitted')}:{' '}
{new Date(draft.submittedAt).toLocaleString()}
</span>
</div>
{draft.makerComment && (
<div className="px-3 py-2 rounded-sm border border-regolith bg-regolith/30 text-2xs">
<span className="text-carbon/60 uppercase tracking-label font-secondary">
<div className="px-3 py-2 rounded-sm border border-line bg-line/30 text-2xs">
<span className="text-mute uppercase tracking-[0.10em] font-display">
{t('reviews.drawer.makerComment')}:
</span>{' '}
{draft.makerComment}
@@ -423,23 +423,23 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<p className="text-2xs uppercase tracking-label text-carbon/60 mb-1">
<p className="text-2xs uppercase tracking-[0.10em] text-mute mb-1">
{t('reviews.drawer.live')}
</p>
{liveQ.isLoading && <LoadingBlock size="sm" label={t('loading')} />}
{liveQ.data === null && (
<p className="text-2xs text-carbon/60 italic">
<p className="text-2xs text-mute italic">
{t('reviews.drawer.noLive')}
</p>
)}
{liveQ.data && (
<pre className="bg-regolith/30 rounded p-2 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
<pre className="bg-line/30 rounded p-2 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(liveQ.data.data, null, 2)}
</pre>
)}
</div>
<div>
<p className="text-2xs uppercase tracking-label text-carbon/60 mb-1">
<p className="text-2xs uppercase tracking-[0.10em] text-mute mb-1">
{t('reviews.drawer.proposed')}
</p>
{draft.operation === 'CLOSE' ? (
@@ -447,7 +447,7 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
{t('reviews.drawer.closeNote')}
</p>
) : (
<pre className="bg-ultramarain/4 rounded p-2 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
<pre className="bg-accent/4 rounded p-2 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(draft.data, null, 2)}
</pre>
)}
@@ -460,10 +460,10 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
</Alert>
)}
<div className="border-t border-regolith pt-3 space-y-2">
<div className="border-t border-line pt-3 space-y-2">
<label
htmlFor="review-comment"
className="text-2xs uppercase tracking-label text-carbon/70"
className="text-2xs uppercase tracking-[0.10em] text-ink-2"
>
{t('reviews.drawer.comment')}
</label>
+6 -6
View File
@@ -70,7 +70,7 @@ function SearchPage() {
</form>
{tooShort && (
<p className="text-2xs text-carbon/60">{t('search.tooShort')}</p>
<p className="text-2xs text-mute">{t('search.tooShort')}</p>
)}
{hasQuery && result.isLoading && (
@@ -89,7 +89,7 @@ function SearchPage() {
{hasQuery && result.data && result.data.total > 0 && (
<>
<p className="text-2xs text-carbon/60">
<p className="text-2xs text-mute">
{t('search.totalHits', { count: result.data.total })}
</p>
<div className="space-y-3">
@@ -100,7 +100,7 @@ function SearchPage() {
to="/dictionaries/$name"
params={{ name: g.dictName }}
search={{ q: urlSearch.q }}
className="text-sm font-primary text-ultramarain hover:underline"
className="text-sm font-sans text-accent hover:underline"
>
{g.dictDisplayName ?? g.dictName}
</Link>
@@ -116,13 +116,13 @@ function SearchPage() {
to="/dictionaries/$name"
params={{ name: g.dictName }}
search={{ q: it.businessKey }}
className="font-mono text-ultramarain hover:underline truncate"
className="font-mono text-accent hover:underline truncate"
>
{it.businessKey}
</Link>
<div className="flex items-center gap-2 shrink-0">
<Badge variant="neutral">{it.dataScope}</Badge>
<span className="text-carbon/60">
<span className="text-mute">
{new Date(it.createdAt).toLocaleString()}
</span>
</div>
@@ -136,7 +136,7 @@ function SearchPage() {
)}
{!hasQuery && !tooShort && (
<p className="text-2xs text-carbon/60">{t('search.intro')}</p>
<p className="text-2xs text-mute">{t('search.intro')}</p>
)}
</div>
)
+18 -18
View File
@@ -115,7 +115,7 @@ function WebhookDetailPage() {
<div className="space-y-6">
<PageHeader
breadcrumb={
<Link to="/webhooks" className="text-sm text-carbon/70 hover:text-ultramarain">
<Link to="/webhooks" className="text-sm text-ink-2 hover:text-accent">
{t('nav.webhooks')}
</Link>
}
@@ -169,7 +169,7 @@ function WebhookDetailPage() {
{data.scopeFilter.map((s) => (
<span
key={s}
className="inline-flex items-center gap-1 text-2xs uppercase tracking-label"
className="inline-flex items-center gap-1 text-2xs uppercase tracking-[0.10em]"
>
<span className={`inline-block size-2 rounded-full ${SCOPE_DOT[s]}`} aria-hidden />
{s}
@@ -177,7 +177,7 @@ function WebhookDetailPage() {
))}
</div>
) : (
<span className="text-2xs text-carbon/50">{t('webhooks.allScopes')}</span>
<span className="text-2xs text-mute">{t('webhooks.allScopes')}</span>
)}
</InfoRow>
<InfoRow label={t('webhooks.field.dictionaryFilter')}>
@@ -207,7 +207,7 @@ function WebhookDetailPage() {
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="font-primary text-base text-ultramarain">
<h2 className="font-sans text-base text-accent">
{t('webhooks.deliveries.title')}
</h2>
<IconButton
@@ -232,10 +232,10 @@ function WebhookDetailPage() {
onClick={() => handleStatusToggle(s)}
aria-pressed={active}
className={[
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border text-2xs uppercase tracking-label font-secondary transition-colors',
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border text-2xs uppercase tracking-[0.10em] font-display transition-colors',
active
? 'border-ultramarain bg-ultramarain/8 text-ultramarain'
: 'border-regolith bg-white text-carbon/70 hover:border-carbon/40',
? 'border-accent bg-accent/8 text-accent'
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
].join(' ')}
>
{s}
@@ -249,7 +249,7 @@ function WebhookDetailPage() {
setPage(0)
setStatusFilter(undefined)
}}
className="text-xs text-ultramarain hover:underline ml-2"
className="text-xs text-accent hover:underline ml-2"
>
{t('webhooks.deliveries.filter.clear')}
</button>
@@ -293,7 +293,7 @@ function WebhookDetailPage() {
<span className="font-mono text-2xs">{d.attemptCount}</span>
</TableCell>
<TableCell>
<span className="text-2xs text-carbon/70">
<span className="text-2xs text-ink-2">
{d.lastAttemptAt
? new Date(d.lastAttemptAt).toLocaleString()
: '—'}
@@ -305,7 +305,7 @@ function WebhookDetailPage() {
</span>
</TableCell>
<TableCell>
<span className="text-2xs text-carbon/70 line-clamp-2 max-w-md inline-block">
<span className="text-2xs text-ink-2 line-clamp-2 max-w-md inline-block">
{d.lastError ?? '—'}
</span>
</TableCell>
@@ -337,7 +337,7 @@ function WebhookDetailPage() {
>
</Button>
<span className="text-sm text-carbon/70 self-center">
<span className="text-sm text-ink-2 self-center">
{page + 1} / {deliveries.data.totalPages}
</span>
<Button
@@ -384,7 +384,7 @@ function TestResultAlert({
action={
<button
type="button"
className="text-xs text-carbon/70 hover:text-carbon"
className="text-xs text-ink-2 hover:text-ink"
onClick={onDismiss}
>
@@ -394,7 +394,7 @@ function TestResultAlert({
<div className="space-y-1 text-sm">
{result.httpStatus != null && (
<div>
<span className="text-2xs uppercase tracking-label text-carbon/60 mr-2">
<span className="text-2xs uppercase tracking-[0.10em] text-mute mr-2">
HTTP
</span>
<span className="font-mono">{result.httpStatus}</span>
@@ -402,14 +402,14 @@ function TestResultAlert({
)}
{result.latencyMs != null && (
<div>
<span className="text-2xs uppercase tracking-label text-carbon/60 mr-2">
<span className="text-2xs uppercase tracking-[0.10em] text-mute mr-2">
{t('webhooks.test.latency')}
</span>
<span className="font-mono">{result.latencyMs} ms</span>
</div>
)}
{result.message && (
<div className="font-mono text-2xs text-carbon/80 whitespace-pre-wrap break-all">
<div className="font-mono text-2xs text-ink whitespace-pre-wrap break-all">
{result.message}
</div>
)}
@@ -421,7 +421,7 @@ function TestResultAlert({
function InfoRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<div className="text-2xs uppercase tracking-label text-carbon/60 mb-1">
<div className="text-2xs uppercase tracking-[0.10em] text-mute mb-1">
{label}
</div>
<div>{children}</div>
@@ -446,13 +446,13 @@ function RotateRevealModal({
className="bg-white rounded-lg p-6 max-w-xl w-full space-y-4"
onClick={(e) => e.stopPropagation()}
>
<h3 className="font-primary text-lg text-ultramarain">
<h3 className="font-sans text-lg text-accent">
{t('webhooks.secret.rotatedTitle')}
</h3>
<Alert variant="warning" title={t('webhooks.secret.warningTitle')}>
{t('webhooks.secret.rotatedWarning')}
</Alert>
<div className="bg-carbon/5 border border-regolith rounded-md p-3">
<div className="bg-carbon/5 border border-line rounded-md p-3">
<code className="font-mono text-2xs break-all">{secret}</code>
</div>
<div className="flex justify-end">
@@ -112,7 +112,7 @@ function WebhooksPage() {
<Link
to="/webhooks/$id"
params={{ id: sub.id }}
className="font-medium text-ultramarain hover:underline"
className="font-medium text-accent hover:underline"
>
{sub.name}
</Link>
@@ -127,12 +127,12 @@ function WebhooksPage() {
</TableCell>
<TableCell>
{sub.dictionaryFilter && sub.dictionaryFilter.length > 0 ? (
<span className="text-2xs text-carbon">
<span className="text-2xs text-ink">
{sub.dictionaryFilter.slice(0, 3).join(', ')}
{sub.dictionaryFilter.length > 3 && ` +${sub.dictionaryFilter.length - 3}`}
</span>
) : (
<span className="text-2xs text-carbon/50">{t('webhooks.allDicts')}</span>
<span className="text-2xs text-mute">{t('webhooks.allDicts')}</span>
)}
</TableCell>
<TableCell>
@@ -175,14 +175,14 @@ function WebhooksPage() {
function ScopeChips({ scopes }: { scopes?: DataScope[] | null }) {
const { t } = useTranslation()
if (!scopes || scopes.length === 0) {
return <span className="text-2xs text-carbon/50">{t('webhooks.allScopes')}</span>
return <span className="text-2xs text-mute">{t('webhooks.allScopes')}</span>
}
return (
<div className="flex gap-1">
{scopes.map((s) => (
<span
key={s}
className="inline-flex items-center gap-1 text-2xs uppercase tracking-label"
className="inline-flex items-center gap-1 text-2xs uppercase tracking-[0.10em]"
title={s}
>
<span className={`inline-block size-2 rounded-full ${SCOPE_DOT[s]}`} aria-hidden />
@@ -367,7 +367,7 @@ function SecretRevealModal({
<Alert variant="warning" title={t('webhooks.secret.warningTitle')}>
{t('webhooks.secret.warning')}
</Alert>
<div className="bg-carbon/5 border border-regolith rounded-md p-3">
<div className="bg-carbon/5 border border-line rounded-md p-3">
<code className="font-mono text-2xs break-all">{revealed.secret}</code>
</div>
<div className="flex justify-end gap-2">