Merge branch 'feat/v1.2.1-catalog-refresh' into 'main'

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

See merge request 2-6/2-6-4/terravault/ordinis!36
This commit is contained in:
Александр Зимин
2026-05-11 09:08:38 +00:00
24 changed files with 289 additions and 273 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ export function AuthBadge() {
if (auth.isLoading) { if (auth.isLoading) {
return ( return (
<span className="text-2xs uppercase tracking-label text-carbon/60"> <span className="text-2xs uppercase tracking-[0.10em] text-mute">
{t('loading')} {t('loading')}
</span> </span>
) )
@@ -58,7 +58,7 @@ export function AuthBadge() {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1 text-sm text-carbon"> <span className="inline-flex items-center gap-1 text-sm text-ink">
<UserIcon size={14} weight="duotone" /> <UserIcon size={14} weight="duotone" />
<span className="hidden sm:inline">{display}</span> <span className="hidden sm:inline">{display}</span>
</span> </span>
+3 -3
View File
@@ -34,14 +34,14 @@ export function RequireAuth({ children }: { children: ReactNode }) {
return ( return (
<div className="min-h-screen flex items-center justify-center p-8"> <div className="min-h-screen flex items-center justify-center p-8">
<div className="max-w-md text-center space-y-2"> <div className="max-w-md text-center space-y-2">
<h1 className="text-lg font-primary">Ошибка входа</h1> <h1 className="text-lg font-sans">Ошибка входа</h1>
<p className="text-sm text-carbon/70"> <p className="text-sm text-ink-2">
Keycloak вернул ошибку:{' '} Keycloak вернул ошибку:{' '}
<code className="font-mono">{oidcError}</code> <code className="font-mono">{oidcError}</code>
</p> </p>
<button <button
type="button" type="button"
className="mt-4 inline-flex items-center justify-center rounded-sm border border-ultramarain text-ultramarain px-4 py-2 text-sm hover:bg-orbit" className="mt-4 inline-flex items-center justify-center rounded-sm border border-accent text-accent px-4 py-2 text-sm hover:bg-accent-bg"
onClick={() => { onClick={() => {
window.location.href = '/' window.location.href = '/'
}} }}
@@ -184,7 +184,7 @@ export const SchemaDrivenForm = ({
</div> </div>
)} )}
{idSource && ( {idSource && (
<div className="sm:col-span-2 text-2xs text-carbon/60 px-2"> <div className="sm:col-span-2 text-2xs text-mute px-2">
{t('form.idSourceNote', { field: idSource })} {t('form.idSourceNote', { field: idSource })}
</div> </div>
)} )}
@@ -327,9 +327,9 @@ const FieldBody = ({
<div> <div>
<FieldLabel required={required}> <FieldLabel required={required}>
{label}{' '} {label}{' '}
<span className="text-2xs uppercase tracking-label text-carbon/60 ml-1">i18n</span> <span className="text-2xs uppercase tracking-[0.10em] text-mute ml-1">i18n</span>
</FieldLabel> </FieldLabel>
<div className="space-y-2 pl-3 border-l-2 border-regolith mt-1"> <div className="space-y-2 pl-3 border-l-2 border-line mt-1">
{supportedLocales.map((loc) => ( {supportedLocales.map((loc) => (
<Controller <Controller
key={loc} key={loc}
@@ -337,7 +337,7 @@ const FieldBody = ({
name={`data.${fieldKey}.${loc}` as `data.${string}`} name={`data.${fieldKey}.${loc}` as `data.${string}`}
render={({ field }) => ( render={({ field }) => (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-xs font-mono text-carbon/70 normal-case tracking-normal whitespace-nowrap shrink-0 w-14"> <span className="text-xs font-mono text-ink-2 normal-case tracking-normal whitespace-nowrap shrink-0 w-14">
{loc === defaultLocale ? <strong>{loc}</strong> : loc} {loc === defaultLocale ? <strong>{loc}</strong> : loc}
</span> </span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -599,7 +599,7 @@ const DateTimeField = ({ label, value, defaultTime, onChange, hint, error }: Dat
/> />
</div> </div>
</div> </div>
{hint && !error && <p className="text-2xs text-carbon/60 mt-1">{hint}</p>} {hint && !error && <p className="text-2xs text-mute mt-1">{hint}</p>}
{error && <p className="text-2xs text-mars mt-1">{error}</p>} {error && <p className="text-2xs text-mars mt-1">{error}</p>}
</div> </div>
) )
@@ -797,11 +797,11 @@ const ReferenceSelectField = ({
// appearance-none убирает OS default стрелку. // appearance-none убирает OS default стрелку.
// py-2 (8px) match'ит TextInput высоту. pr-10 — место под адорнмент. // py-2 (8px) match'ит TextInput высоту. pr-10 — место под адорнмент.
className={[ className={[
'w-full appearance-none border rounded-sm pl-3 pr-10 py-2 text-sm font-secondary', 'w-full appearance-none border rounded-sm pl-3 pr-10 py-2 text-sm font-display',
'bg-white text-black transition-colors', 'bg-white text-black transition-colors',
'focus:outline-none focus:ring-1 focus:ring-ultramarain focus:border-ultramarain', 'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
errorMsg ? 'border-mars' : 'border-regolith hover:border-carbon/40', errorMsg ? 'border-mars' : 'border-line hover:border-carbon/40',
isLoading ? 'cursor-wait text-carbon/60' : 'cursor-pointer', isLoading ? 'cursor-wait text-mute' : 'cursor-pointer',
].join(' ')} ].join(' ')}
> >
<option value="">{isLoading ? '…' : '—'}</option> <option value="">{isLoading ? '…' : '—'}</option>
@@ -816,9 +816,9 @@ const ReferenceSelectField = ({
aria-hidden="true" aria-hidden="true"
> >
{isLoading ? ( {isLoading ? (
<CircleNotchIcon size={16} weight="bold" className="text-ultramarain animate-spin" /> <CircleNotchIcon size={16} weight="bold" className="text-accent animate-spin" />
) : ( ) : (
<CaretDownIcon size={16} weight="regular" className="text-carbon/40" /> <CaretDownIcon size={16} weight="regular" className="text-mute/70" />
)} )}
</div> </div>
</div> </div>
@@ -163,10 +163,10 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
type="button" type="button"
onClick={() => setDrawMode(active ? 'idle' : mode)} onClick={() => setDrawMode(active ? 'idle' : mode)}
className={[ className={[
'px-3 py-1.5 text-xs font-secondary rounded-sm border transition-colors', 'px-3 py-1.5 text-xs font-display rounded-sm border transition-colors',
active active
? 'border-ultramarain bg-ultramarain text-white' ? 'border-accent bg-accent text-white'
: 'border-regolith bg-white text-carbon hover:border-carbon/40', : 'border-line bg-white text-ink hover:border-carbon/40',
].join(' ')} ].join(' ')}
> >
{label} {label}
@@ -183,7 +183,7 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
panelClassName="max-h-[calc(100vh-2rem)] my-4 flex flex-col" panelClassName="max-h-[calc(100vh-2rem)] my-4 flex flex-col"
bodyClassName="flex-1 flex flex-col gap-3 overflow-hidden" bodyClassName="flex-1 flex flex-col gap-3 overflow-hidden"
> >
<p className="text-sm text-carbon/70">{t('aoi.dialog.help')}</p> <p className="text-sm text-ink-2">{t('aoi.dialog.help')}</p>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{toolbarBtn('rectangle', t('aoi.dialog.drawRectangle'))} {toolbarBtn('rectangle', t('aoi.dialog.drawRectangle'))}
@@ -192,12 +192,12 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
type="button" type="button"
onClick={handleClear} onClick={handleClear}
disabled={!shape} disabled={!shape}
className="px-3 py-1.5 text-xs font-secondary rounded-sm border border-regolith bg-white text-carbon hover:border-carbon/40 disabled:opacity-40 disabled:cursor-not-allowed" className="px-3 py-1.5 text-xs font-display rounded-sm border border-line bg-white text-ink hover:border-carbon/40 disabled:opacity-40 disabled:cursor-not-allowed"
> >
{t('aoi.dialog.clear')} {t('aoi.dialog.clear')}
</button> </button>
{drawMode === 'polygon' && ( {drawMode === 'polygon' && (
<span className="text-2xs text-carbon/60"> <span className="text-2xs text-mute">
{t('aoi.dialog.polygonHint')} {t('aoi.dialog.polygonHint')}
</span> </span>
)} )}
@@ -205,7 +205,7 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
<div <div
ref={mapDivRef} ref={mapDivRef}
className="rounded-sm border border-regolith shrink-0" className="rounded-sm border border-line shrink-0"
style={{ display: 'block', width: '100%', height: 520, minHeight: 520 }} style={{ display: 'block', width: '100%', height: 520, minHeight: 520 }}
aria-label={t('aoi.dialog.mapAriaLabel')} aria-label={t('aoi.dialog.mapAriaLabel')}
/> />
@@ -119,7 +119,7 @@ export const CascadeConfirmDialog = ({
{plan && ( {plan && (
<> <>
{/* Header summary */} {/* Header summary */}
<p className="text-sm text-carbon"> <p className="text-sm text-ink">
{hasBlockers {hasBlockers
? t('cascade.summary.blocked', { ? t('cascade.summary.blocked', {
blockers: plan.blockers.length, blockers: plan.blockers.length,
@@ -144,16 +144,16 @@ export const CascadeConfirmDialog = ({
<span className="font-mono"> <span className="font-mono">
{b.sourceDict}/{b.businessKey} {b.sourceDict}/{b.businessKey}
</span> </span>
<span className="text-carbon/60">.{b.sourceField}</span> <span className="text-mute">.{b.sourceField}</span>
</li> </li>
))} ))}
{plan.blockers.length > 5 && ( {plan.blockers.length > 5 && (
<li className="text-carbon/60"> <li className="text-mute">
{t('cascade.blockers.more', { count: plan.blockers.length - 5 })} {t('cascade.blockers.more', { count: plan.blockers.length - 5 })}
</li> </li>
)} )}
</ul> </ul>
<p className="mt-2 text-2xs text-carbon/70"> <p className="mt-2 text-2xs text-ink-2">
{t('cascade.blockers.resolveFirst')} {t('cascade.blockers.resolveFirst')}
</p> </p>
</Alert> </Alert>
@@ -164,7 +164,7 @@ export const CascadeConfirmDialog = ({
<section className="space-y-2"> <section className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<WarningIcon weight="bold" size={14} className="text-horizon" /> <WarningIcon weight="bold" size={14} className="text-horizon" />
<h4 className="text-sm font-primary"> <h4 className="text-sm font-sans">
{t('cascade.cascade.title', { count: plan.cascade.length })} {t('cascade.cascade.title', { count: plan.cascade.length })}
</h4> </h4>
</div> </div>
@@ -176,13 +176,13 @@ export const CascadeConfirmDialog = ({
return ( return (
<li <li
key={key} key={key}
className="border border-regolith rounded-sm px-3 py-2 text-2xs" className="border border-line rounded-sm px-3 py-2 text-2xs"
> >
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="font-mono text-ultramarain"> <span className="font-mono text-accent">
{first.sourceDisplayName ?? first.sourceDict} {first.sourceDisplayName ?? first.sourceDict}
</span> </span>
<span className="font-mono text-carbon/70"> <span className="font-mono text-ink-2">
.{first.sourceField} .{first.sourceField}
</span> </span>
<Badge variant={onCloseVariant(first.onClose)}> <Badge variant={onCloseVariant(first.onClose)}>
@@ -190,14 +190,14 @@ export const CascadeConfirmDialog = ({
</Badge> </Badge>
<Badge variant="neutral">{entries.length}</Badge> <Badge variant="neutral">{entries.length}</Badge>
</div> </div>
<ul className="text-2xs text-carbon/70 ml-1 space-y-0.5"> <ul className="text-2xs text-ink-2 ml-1 space-y-0.5">
{sample.map((e) => ( {sample.map((e) => (
<li key={e.recordId} className="font-mono"> <li key={e.recordId} className="font-mono">
· {e.businessKey} · {e.businessKey}
</li> </li>
))} ))}
{overflow > 0 && ( {overflow > 0 && (
<li className="text-carbon/50"> <li className="text-mute">
{t('cascade.cascade.more', { count: overflow })} {t('cascade.cascade.more', { count: overflow })}
</li> </li>
)} )}
@@ -224,7 +224,7 @@ export const CascadeConfirmDialog = ({
<div> <div>
<label <label
htmlFor="cascade-reason" htmlFor="cascade-reason"
className="text-2xs text-carbon/70 uppercase tracking-label" className="text-2xs text-ink-2 uppercase tracking-[0.10em]"
> >
{t('cascade.reason.label')} {t('cascade.reason.label')}
</label> </label>
@@ -242,7 +242,7 @@ export const CascadeConfirmDialog = ({
<div> <div>
<label <label
htmlFor="cascade-confirm" htmlFor="cascade-confirm"
className="text-2xs text-carbon/70 uppercase tracking-label" className="text-2xs text-ink-2 uppercase tracking-[0.10em]"
> >
{t('cascade.confirmGate.label', { word: 'CONFIRM' })} {t('cascade.confirmGate.label', { word: 'CONFIRM' })}
</label> </label>
@@ -264,7 +264,7 @@ export const CascadeConfirmDialog = ({
)} )}
{/* Footer actions */} {/* Footer actions */}
<div className="flex items-center justify-end gap-2 pt-2 border-t border-regolith"> <div className="flex items-center justify-end gap-2 pt-2 border-t border-line">
<Button <Button
variant="secondary" variant="secondary"
onClick={onClose} onClick={onClose}
@@ -37,10 +37,10 @@ export const DictionaryDependentsPanel = ({ dictionaryName }: Props) => {
return ( return (
<Panel> <Panel>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-primary text-carbon"> <h3 className="text-sm font-sans text-ink">
{t('lineage.usedBy.title')} {t('lineage.usedBy.title')}
</h3> </h3>
<span className="text-2xs text-carbon/60"> <span className="text-2xs text-mute">
{t('lineage.usedBy.count', { count: data.length })} {t('lineage.usedBy.count', { count: data.length })}
</span> </span>
</div> </div>
@@ -48,21 +48,21 @@ export const DictionaryDependentsPanel = ({ dictionaryName }: Props) => {
{data.map((d) => ( {data.map((d) => (
<li <li
key={`${d.sourceDict}.${d.sourceField}`} key={`${d.sourceDict}.${d.sourceField}`}
className="flex items-center gap-2 px-2.5 py-1.5 rounded-sm border border-regolith bg-white text-2xs" className="flex items-center gap-2 px-2.5 py-1.5 rounded-sm border border-line bg-white text-2xs"
> >
<Link <Link
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: d.sourceDict }} params={{ name: d.sourceDict }}
className="text-ultramarain hover:underline font-mono" className="text-accent hover:underline font-mono"
aria-label={t('lineage.usedBy.openSourceDict', { aria-label={t('lineage.usedBy.openSourceDict', {
dict: d.sourceDisplayName ?? d.sourceDict, dict: d.sourceDisplayName ?? d.sourceDict,
})} })}
> >
{d.sourceDisplayName ?? d.sourceDict} {d.sourceDisplayName ?? d.sourceDict}
</Link> </Link>
<span className="font-mono text-carbon/70">.{d.sourceField}</span> <span className="font-mono text-ink-2">.{d.sourceField}</span>
<ArrowRightIcon weight="bold" size={12} className="text-carbon/40" /> <ArrowRightIcon weight="bold" size={12} className="text-mute/70" />
<span className="font-mono text-carbon/70">{d.targetField}</span> <span className="font-mono text-ink-2">{d.targetField}</span>
{d.activeRecordsInSourceDict > 0 && ( {d.activeRecordsInSourceDict > 0 && (
<Badge variant="neutral"> <Badge variant="neutral">
{t('lineage.usedBy.activeCount', { {t('lineage.usedBy.activeCount', {
@@ -54,7 +54,7 @@ const NeighborCard = ({
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: dict.name }} params={{ name: dict.name }}
search={{ view: 'hub' }} search={{ view: 'hub' }}
className={`group block bg-white border border-regolith rounded-lg p-3 transition hover:shadow-hover hover:border-ultramarain/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ultramarain/40 ${ className={`group block bg-white border border-line rounded-lg p-3 transition hover:shadow-hover hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
side === 'left' ? 'text-right' : 'text-left' side === 'left' ? 'text-right' : 'text-left'
}`} }`}
> >
@@ -65,11 +65,11 @@ const NeighborCard = ({
}`} }`}
aria-hidden="true" aria-hidden="true"
/> />
<h4 className="font-primary text-[14px] font-semibold text-ultramarain truncate group-hover:underline"> <h4 className="font-sans text-[14px] font-semibold text-accent truncate group-hover:underline">
{dict.displayName ?? dict.name} {dict.displayName ?? dict.name}
</h4> </h4>
</div> </div>
<div className="mt-0.5 font-mono text-[10px] text-carbon/60 truncate"> <div className="mt-0.5 font-mono text-[10px] text-mute truncate">
{dict.name} {dict.name}
</div> </div>
</Link> </Link>
@@ -85,7 +85,7 @@ const SectionLabel = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div <div
className={`text-2xs uppercase tracking-label text-carbon/60 mb-2 ${ className={`text-2xs uppercase tracking-[0.10em] text-mute mb-2 ${
side === 'left' ? 'text-right' : 'text-left' side === 'left' ? 'text-right' : 'text-left'
}`} }`}
> >
@@ -141,7 +141,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
<div className="md:order-1 order-2 space-y-2"> <div className="md:order-1 order-2 space-y-2">
<SectionLabel side="left" count={outgoingDicts.length} /> <SectionLabel side="left" count={outgoingDicts.length} />
{outgoingDicts.length === 0 ? ( {outgoingDicts.length === 0 ? (
<div className="text-2xs text-carbon/40 text-right italic"> <div className="text-2xs text-mute/70 text-right italic">
{t('hub.noOutgoing', { defaultValue: 'нет исходящих ссылок' })} {t('hub.noOutgoing', { defaultValue: 'нет исходящих ссылок' })}
</div> </div>
) : ( ) : (
@@ -156,7 +156,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
{/* Center: focused dict (large card) */} {/* Center: focused dict (large card) */}
<div className="md:order-2 order-1 relative"> <div className="md:order-2 order-1 relative">
<div <div
className="relative bg-white rounded-xl p-5 border-2 border-ultramarain" className="relative bg-white rounded-xl p-5 border-2 border-accent"
style={{ boxShadow: '0 18px 36px -22px rgba(20,30,140,.5)' }} style={{ boxShadow: '0 18px 36px -22px rgba(20,30,140,.5)' }}
> >
<div className="flex items-start gap-2 mb-2 flex-wrap"> <div className="flex items-start gap-2 mb-2 flex-wrap">
@@ -166,22 +166,22 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
{t('dict.list.approval', { defaultValue: 'approval' })} {t('dict.list.approval', { defaultValue: 'approval' })}
</Badge> </Badge>
)} )}
<span className="ml-auto font-mono text-[11px] text-carbon/60"> <span className="ml-auto font-mono text-[11px] text-mute">
v{detail.schemaVersion} v{detail.schemaVersion}
</span> </span>
</div> </div>
<h2 className="font-primary text-[22px] font-semibold text-ultramarain leading-tight"> <h2 className="font-sans text-[22px] font-semibold text-accent leading-tight">
{detail.displayName ?? detail.name} {detail.displayName ?? detail.name}
</h2> </h2>
<div className="font-mono text-[11px] text-carbon/60 mt-0.5"> <div className="font-mono text-[11px] text-mute mt-0.5">
{detail.name} {detail.name}
</div> </div>
{detail.description && ( {detail.description && (
<p className="text-[13px] text-carbon mt-3 leading-relaxed"> <p className="text-[13px] text-ink mt-3 leading-relaxed">
{detail.description} {detail.description}
</p> </p>
)} )}
<div className="mt-4 pt-3 border-t border-regolith flex items-center justify-between text-2xs uppercase tracking-label text-carbon/60"> <div className="mt-4 pt-3 border-t border-line flex items-center justify-between text-2xs uppercase tracking-[0.10em] text-mute">
<span>{detail.bundle}</span> <span>{detail.bundle}</span>
<span>{detail.supportedLocales.join(', ')}</span> <span>{detail.supportedLocales.join(', ')}</span>
{typeof detail.recordCount === 'number' && ( {typeof detail.recordCount === 'number' && (
@@ -197,7 +197,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
<div className="md:order-3 order-3 space-y-2"> <div className="md:order-3 order-3 space-y-2">
<SectionLabel side="right" count={incomingDicts.length} /> <SectionLabel side="right" count={incomingDicts.length} />
{incomingDicts.length === 0 ? ( {incomingDicts.length === 0 ? (
<div className="text-2xs text-carbon/40 italic"> <div className="text-2xs text-mute/70 italic">
{t('hub.noIncoming', { defaultValue: 'никто не ссылается' })} {t('hub.noIncoming', { defaultValue: 'никто не ссылается' })}
</div> </div>
) : ( ) : (
@@ -68,19 +68,19 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
return ( return (
<section className="space-y-3"> <section className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-primary text-carbon"> <h3 className="text-sm font-sans text-ink">
{t('lineage.refs.title')} {t('lineage.refs.title')}
</h3> </h3>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{stale !== null && stale >= 2 && ( {stale !== null && stale >= 2 && (
<span <span
className="text-2xs text-carbon/60 px-2 py-0.5 rounded-sm border border-regolith" className="text-2xs text-mute px-2 py-0.5 rounded-sm border border-line"
title={t('lineage.refs.staleHint')} title={t('lineage.refs.staleHint')}
> >
{t('lineage.refs.stale', { minutes: stale })} {t('lineage.refs.stale', { minutes: stale })}
</span> </span>
)} )}
<span className="text-2xs text-carbon/60"> <span className="text-2xs text-mute">
{t('lineage.refs.total', { count: data.total })} {t('lineage.refs.total', { count: data.total })}
</span> </span>
</div> </div>
@@ -91,16 +91,16 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
{data.perSource.map((s) => ( {data.perSource.map((s) => (
<li <li
key={`${s.sourceDict}.${s.sourceField}`} key={`${s.sourceDict}.${s.sourceField}`}
className="flex items-center gap-1.5 px-2 py-1 rounded-sm border border-regolith bg-regolith/30 text-2xs" className="flex items-center gap-1.5 px-2 py-1 rounded-sm border border-line bg-line/30 text-2xs"
> >
<Link <Link
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: s.sourceDict }} params={{ name: s.sourceDict }}
className="text-ultramarain hover:underline font-mono" className="text-accent hover:underline font-mono"
> >
{s.sourceDisplayName ?? s.sourceDict} {s.sourceDisplayName ?? s.sourceDict}
</Link> </Link>
<span className="font-mono text-carbon/70">.{s.sourceField}</span> <span className="font-mono text-ink-2">.{s.sourceField}</span>
<Badge variant="neutral">{s.count}</Badge> <Badge variant="neutral">{s.count}</Badge>
<Badge variant={onCloseVariant(s.onClose)}> <Badge variant={onCloseVariant(s.onClose)}>
{t(`lineage.onClose.${s.onClose}`)} {t(`lineage.onClose.${s.onClose}`)}
@@ -111,7 +111,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
{/* Items list */} {/* Items list */}
{data.items.length > 0 && ( {data.items.length > 0 && (
<ul className="divide-y divide-regolith border border-regolith rounded-sm"> <ul className="divide-y divide-regolith border border-line rounded-sm">
{data.items.map((r) => ( {data.items.map((r) => (
<li <li
key={r.id} key={r.id}
@@ -119,13 +119,13 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-mono text-carbon/70">{r.sourceDict}</span> <span className="font-mono text-ink-2">{r.sourceDict}</span>
<span className="text-carbon/40">/</span> <span className="text-mute/70">/</span>
<Link <Link
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: r.sourceDict }} params={{ name: r.sourceDict }}
search={{ q: r.businessKey }} search={{ q: r.businessKey }}
className="text-ultramarain hover:underline font-mono truncate" className="text-accent hover:underline font-mono truncate"
aria-label={t('lineage.refs.openSourceRecord', { aria-label={t('lineage.refs.openSourceRecord', {
key: r.businessKey, key: r.businessKey,
})} })}
@@ -135,10 +135,10 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
<ArrowSquareOutIcon <ArrowSquareOutIcon
weight="bold" weight="bold"
size={12} size={12}
className="text-carbon/40 shrink-0" className="text-mute/70 shrink-0"
/> />
</div> </div>
<div className="text-carbon/60 mt-0.5"> <div className="text-mute mt-0.5">
{t('lineage.refs.field', { field: r.sourceField })} ·{' '} {t('lineage.refs.field', { field: r.sourceField })} ·{' '}
{t('lineage.refs.created', { {t('lineage.refs.created', {
when: new Date(r.createdAt).toLocaleString(), when: new Date(r.createdAt).toLocaleString(),
@@ -162,7 +162,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
> >
{t('pagination.prev')} {t('pagination.prev')}
</Button> </Button>
<span className="text-carbon/70"> <span className="text-ink-2">
{t('pagination.pageOf', { page: page + 1, total: totalPages })} {t('pagination.pageOf', { page: page + 1, total: totalPages })}
</span> </span>
<Button <Button
@@ -40,10 +40,10 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
</Alert> </Alert>
)} )}
{data && data.length === 0 && ( {data && data.length === 0 && (
<p className="text-sm text-carbon/60">{t('history.empty')}</p> <p className="text-sm text-mute">{t('history.empty')}</p>
)} )}
{data && data.length > 0 && ( {data && data.length > 0 && (
<ol className="relative border-l-2 border-regolith pl-5 space-y-4"> <ol className="relative border-l-2 border-line pl-5 space-y-4">
{data.map((rec, idx) => { {data.map((rec, idx) => {
const isLatest = idx === 0 const isLatest = idx === 0
const displayVersion = data.length - idx const displayVersion = data.length - idx
@@ -65,7 +65,7 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
<span <span
className={`absolute -left-[27px] top-1 h-3 w-3 rounded-full border-2 ${ className={`absolute -left-[27px] top-1 h-3 w-3 rounded-full border-2 ${
isLatest && status === 'active' isLatest && status === 'active'
? 'bg-ultramarain border-ultramarain' ? 'bg-accent border-accent'
: status === 'future' : status === 'future'
? 'bg-orbit border-horizon' ? 'bg-orbit border-horizon'
: status === 'expired' : status === 'expired'
@@ -74,7 +74,7 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
}`} }`}
/> />
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="text-sm font-primary text-ultramarain">v{displayVersion}</span> <span className="text-sm font-sans text-accent">v{displayVersion}</span>
{isLatest && status === 'active' && ( {isLatest && status === 'active' && (
<Badge variant="primary">{t('history.current')}</Badge> <Badge variant="primary">{t('history.current')}</Badge>
)} )}
@@ -84,7 +84,7 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
<Badge variant="success">{t('history.active')}</Badge> <Badge variant="success">{t('history.active')}</Badge>
)} )}
</div> </div>
<div className="text-2xs text-carbon/70 space-y-0.5"> <div className="text-2xs text-ink-2 space-y-0.5">
<div> <div>
<span className="font-mono">{t('history.validFrom')}:</span>{' '} <span className="font-mono">{t('history.validFrom')}:</span>{' '}
{new Date(rec.validFrom).toLocaleString()} {new Date(rec.validFrom).toLocaleString()}
@@ -102,10 +102,10 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
)} )}
</div> </div>
<details className="mt-2"> <details className="mt-2">
<summary className="text-2xs text-ultramarain cursor-pointer hover:underline"> <summary className="text-2xs text-accent cursor-pointer hover:underline">
{t('history.viewData')} {t('history.viewData')}
</summary> </summary>
<pre className="mt-1 bg-regolith/30 rounded p-2 text-2xs font-mono overflow-x-auto"> <pre className="mt-1 bg-line/30 rounded p-2 text-2xs font-mono overflow-x-auto">
{JSON.stringify(rec.data, null, 2)} {JSON.stringify(rec.data, null, 2)}
</pre> </pre>
</details> </details>
@@ -202,7 +202,7 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
<div className={activeTab === 'metadata' ? 'block' : 'hidden'}> <div className={activeTab === 'metadata' ? 'block' : 'hidden'}>
{!isEdit && ( {!isEdit && (
<div className="mb-3 p-3 rounded-md border border-ultramarain/20 bg-ultramarain/4"> <div className="mb-3 p-3 rounded-md border border-accent/20 bg-accent/4">
<SingleSelect <SingleSelect
label={t('schema.template.label')} label={t('schema.template.label')}
hint={t('schema.template.hint')} hint={t('schema.template.hint')}
@@ -300,7 +300,7 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
/> />
{/* CEO plan E2: per-dict Redis projection opt-in. Default false. {/* CEO plan E2: per-dict Redis projection opt-in. Default false.
Включается когда dict хочет 10k+ RPS на read-api (вместо PG). */} Включается когда dict хочет 10k+ RPS на read-api (вместо PG). */}
<label className="md:col-span-2 flex items-start gap-3 px-3 py-2 rounded-sm border border-regolith bg-regolith/20 cursor-pointer"> <label className="md:col-span-2 flex items-start gap-3 px-3 py-2 rounded-sm border border-line bg-line/20 cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={redisProjection} checked={redisProjection}
@@ -308,10 +308,10 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
className="mt-0.5 size-4 accent-ultramarain" className="mt-0.5 size-4 accent-ultramarain"
/> />
<span className="flex-1"> <span className="flex-1">
<span className="block text-sm font-primary text-carbon"> <span className="block text-sm font-sans text-ink">
{t('schema.redisProjection.label')} {t('schema.redisProjection.label')}
</span> </span>
<span className="block text-2xs text-carbon/60 mt-0.5"> <span className="block text-2xs text-mute mt-0.5">
{t('schema.redisProjection.hint')} {t('schema.redisProjection.hint')}
</span> </span>
</span> </span>
@@ -320,7 +320,7 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
{/* Approval Workflow v2: per-dict opt-in. Default false. Включается {/* Approval Workflow v2: per-dict opt-in. Default false. Включается
для критичных dicts (ground_station, spacecraft и т.д.) где для критичных dicts (ground_station, spacecraft и т.д.) где
один admin не должен иметь power публиковать без review. */} один admin не должен иметь power публиковать без review. */}
<label className="md:col-span-2 flex items-start gap-3 px-3 py-2 rounded-sm border border-orbit/40 bg-orbit/8 cursor-pointer"> <label className="md:col-span-2 flex items-start gap-3 px-3 py-2 rounded-sm border border-warn bg-warn-bg cursor-pointer">
<input <input
type="checkbox" type="checkbox"
checked={approvalRequired} checked={approvalRequired}
@@ -328,10 +328,10 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
className="mt-0.5 size-4 accent-orbit" className="mt-0.5 size-4 accent-orbit"
/> />
<span className="flex-1"> <span className="flex-1">
<span className="block text-sm font-primary text-carbon"> <span className="block text-sm font-sans text-ink">
{t('schema.approvalRequired.label')} {t('schema.approvalRequired.label')}
</span> </span>
<span className="block text-2xs text-carbon/60 mt-0.5"> <span className="block text-2xs text-mute mt-0.5">
{t('schema.approvalRequired.hint')} {t('schema.approvalRequired.hint')}
</span> </span>
</span> </span>
@@ -341,7 +341,7 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
<div className="md:col-span-2"> <div className="md:col-span-2">
<label <label
htmlFor="approval-min-role" htmlFor="approval-min-role"
className="text-2xs uppercase tracking-label text-carbon/70" className="text-2xs uppercase tracking-[0.10em] text-ink-2"
> >
{t('schema.approvalMinRole.label')} {t('schema.approvalMinRole.label')}
</label> </label>
@@ -351,7 +351,7 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
onChange={(e) => setApprovalMinRole(e.target.value)} onChange={(e) => setApprovalMinRole(e.target.value)}
placeholder="ordinis:internal" placeholder="ordinis:internal"
/> />
<p className="text-2xs text-carbon/60 mt-0.5"> <p className="text-2xs text-mute mt-0.5">
{t('schema.approvalMinRole.hint')} {t('schema.approvalMinRole.hint')}
</p> </p>
</div> </div>
@@ -364,7 +364,7 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
</div> </div>
<div className={activeTab === 'preview' ? 'block' : 'hidden'}> <div className={activeTab === 'preview' ? 'block' : 'hidden'}>
<pre className="bg-regolith/30 rounded p-3 text-2xs font-mono overflow-x-auto whitespace-pre-wrap"> <pre className="bg-line/30 rounded p-3 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(schemaJson, null, 2)} {JSON.stringify(schemaJson, null, 2)}
</pre> </pre>
</div> </div>
@@ -47,7 +47,7 @@ export const EventsPreviewTab = ({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-sm text-carbon/70"> <p className="text-sm text-ink-2">
{t('schema.events.intro')} {t('schema.events.intro')}
</p> </p>
@@ -57,18 +57,18 @@ export const EventsPreviewTab = ({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<Badge variant="info">{current.eventType}</Badge> <Badge variant="info">{current.eventType}</Badge>
<span className="text-2xs text-carbon/70"> <span className="text-2xs text-ink-2">
{t('schema.events.topic')}: {t('schema.events.topic')}:
</span> </span>
<code className="text-2xs font-mono text-ultramarain"> <code className="text-2xs font-mono text-accent">
{current.topic} {current.topic}
</code> </code>
</div> </div>
<p className="text-2xs text-carbon/60">{current.description}</p> <p className="text-2xs text-mute">{current.description}</p>
<pre className="bg-regolith/30 rounded p-3 text-2xs font-mono overflow-x-auto whitespace-pre-wrap"> <pre className="bg-line/30 rounded p-3 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(current.envelope, null, 2)} {JSON.stringify(current.envelope, null, 2)}
</pre> </pre>
<p className="text-2xs text-carbon/50"> <p className="text-2xs text-mute">
{t('schema.events.disclaimer')} {t('schema.events.disclaimer')}
</p> </p>
</div> </div>
@@ -49,9 +49,9 @@ export const PropertyEditor = ({
const update = (patch: Partial<PropertyDef>) => onChange({ ...prop, ...patch }) const update = (patch: Partial<PropertyDef>) => onChange({ ...prop, ...patch })
return ( return (
<div className="border border-regolith rounded-lg p-3 space-y-3 bg-white"> <div className="border border-line rounded-lg p-3 space-y-3 bg-white">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<span className="text-2xs uppercase tracking-label text-carbon/60"> <span className="text-2xs uppercase tracking-[0.10em] text-mute">
{t('schema.property')} #{index + 1} {t('schema.property')} #{index + 1}
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@@ -119,13 +119,13 @@ export const PropertyEditor = ({
{prop.kind === 'opaque' && ( {prop.kind === 'opaque' && (
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 text-2xs space-y-2"> <div className="bg-amber-50 border border-amber-200 rounded-md p-3 text-2xs space-y-2">
<div className="font-medium text-carbon"> <div className="font-medium text-ink">
{t('schema.opaque.title')} {t('schema.opaque.title')}
</div> </div>
<div className="text-carbon/70"> <div className="text-ink-2">
{t('schema.opaque.hint')} {t('schema.opaque.hint')}
</div> </div>
<pre className="font-mono text-2xs bg-white border border-regolith rounded p-2 overflow-auto max-h-40"> <pre className="font-mono text-2xs bg-white border border-line rounded p-2 overflow-auto max-h-40">
{JSON.stringify(prop.rawSchema ?? {}, null, 2)} {JSON.stringify(prop.rawSchema ?? {}, null, 2)}
</pre> </pre>
</div> </div>
@@ -260,7 +260,7 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
const isValid = Boolean(refDict && refField) const isValid = Boolean(refDict && refField)
return ( return (
<div className="border border-ultramarain/20 bg-ultramarain/4 rounded-md p-3 space-y-3"> <div className="border border-accent/20 bg-accent/4 rounded-md p-3 space-y-3">
<div> <div>
<FieldLabel required>{t('schema.reference.title')}</FieldLabel> <FieldLabel required>{t('schema.reference.title')}</FieldLabel>
<FieldHint>{t('schema.reference.hint')}</FieldHint> <FieldHint>{t('schema.reference.hint')}</FieldHint>
@@ -50,15 +50,15 @@ export function TimeTravelPicker({
}, [marks, valueDate]) }, [marks, valueDate])
return ( return (
<div className="flex flex-col gap-3 px-4 py-3 rounded-md border border-orbit/40 bg-orbit/4 text-sm"> <div className="flex flex-col gap-3 px-4 py-3 rounded-md border border-warn bg-warn-bg text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-orbit" /> <ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn" />
<span className="text-2xs font-secondary uppercase tracking-label text-carbon/70"> <span className="text-2xs font-display uppercase tracking-[0.10em] text-ink-2">
{t('timeTravel.label')} {t('timeTravel.label')}
</span> </span>
<button <button
type="button" type="button"
className="ml-auto text-2xs text-carbon/60 hover:text-carbon hover:underline inline-flex items-center gap-1" className="ml-auto text-2xs text-mute hover:text-ink hover:underline inline-flex items-center gap-1"
onClick={onClose} onClick={onClose}
> >
<XIcon weight="bold" size={12} /> <XIcon weight="bold" size={12} />
@@ -314,7 +314,7 @@ function TimelineTrack({
)} )}
{marks.length > 0 && ( {marks.length > 0 && (
<div className="text-2xs text-carbon/50 font-secondary mt-1"> <div className="text-2xs text-mute font-display mt-1">
{marks.length === 1 {marks.length === 1
? '1 точка изменений' ? '1 точка изменений'
: marks.length < 5 : marks.length < 5
@@ -340,7 +340,7 @@ function QuickPresets({ onPick }: { onPick: (deltaMs: number) => void }) {
] ]
return ( return (
<div className="flex flex-wrap items-center gap-1.5"> <div className="flex flex-wrap items-center gap-1.5">
<span className="text-2xs uppercase tracking-label text-carbon/60 font-secondary mr-1"> <span className="text-2xs uppercase tracking-[0.10em] text-mute font-display mr-1">
{t('timeTravel.quickPresets')} {t('timeTravel.quickPresets')}
</span> </span>
{presets.map((p) => ( {presets.map((p) => (
@@ -348,7 +348,7 @@ function QuickPresets({ onPick }: { onPick: (deltaMs: number) => void }) {
key={p.key} key={p.key}
type="button" type="button"
onClick={() => onPick(p.deltaMs)} onClick={() => onPick(p.deltaMs)}
className="px-2.5 py-1 rounded-full border border-regolith hover:border-ultramarain hover:bg-ultramarain/4 text-2xs font-mono uppercase tracking-label text-carbon transition-colors" className="px-2.5 py-1 rounded-full border border-line hover:border-accent hover:bg-accent/4 text-2xs font-mono uppercase tracking-[0.10em] text-ink transition-colors"
> >
{p.label} {p.label}
</button> </button>
@@ -386,23 +386,23 @@ function StatusFooter({
if (!valueDate) { if (!valueDate) {
return ( return (
<div className="text-2xs text-carbon/60 font-secondary italic"> <div className="text-2xs text-mute font-display italic">
{t('timeTravel.hint')} {t('timeTravel.hint')}
</div> </div>
) )
} }
return ( return (
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-orbit/20"> <div className="flex flex-wrap items-center gap-2 pt-2 border-t border-warn/30">
<ClockCounterClockwiseIcon weight="regular" size={14} className="text-orbit shrink-0" /> <ClockCounterClockwiseIcon weight="regular" size={14} className="text-warn shrink-0" />
<span className="text-xs font-mono text-carbon/80"> <span className="text-xs font-mono text-ink">
{valueDate.toLocaleString(lang === 'en' ? 'en-US' : 'ru-RU', { {valueDate.toLocaleString(lang === 'en' ? 'en-US' : 'ru-RU', {
dateStyle: 'long', dateStyle: 'long',
timeStyle: 'short', timeStyle: 'short',
})} })}
</span> </span>
{relativeLabel && ( {relativeLabel && (
<span className="text-2xs text-carbon/60 font-secondary lowercase"> <span className="text-2xs text-mute font-display lowercase">
({relativeLabel}) ({relativeLabel})
</span> </span>
)} )}
@@ -418,10 +418,10 @@ function StatusFooter({
valueDate.getMinutes(), valueDate.getMinutes(),
) )
} }
className="w-10 px-1.5 py-0.5 rounded-sm border border-regolith bg-white text-2xs font-mono text-center" className="w-10 px-1.5 py-0.5 rounded-sm border border-line bg-white text-2xs font-mono text-center"
aria-label={t('timeTravel.hours')} aria-label={t('timeTravel.hours')}
/> />
<span className="text-carbon/50 font-mono text-2xs">:</span> <span className="text-mute font-mono text-2xs">:</span>
<input <input
type="number" type="number"
min={0} min={0}
@@ -433,13 +433,13 @@ function StatusFooter({
Math.max(0, Math.min(59, parseInt(e.target.value, 10) || 0)), Math.max(0, Math.min(59, parseInt(e.target.value, 10) || 0)),
) )
} }
className="w-10 px-1.5 py-0.5 rounded-sm border border-regolith bg-white text-2xs font-mono text-center" className="w-10 px-1.5 py-0.5 rounded-sm border border-line bg-white text-2xs font-mono text-center"
aria-label={t('timeTravel.minutes')} aria-label={t('timeTravel.minutes')}
/> />
</span> </span>
<button <button
type="button" type="button"
className="ml-auto text-2xs text-ultramarain hover:underline" className="ml-auto text-2xs text-accent hover:underline"
onClick={onClear} onClick={onClear}
> >
{t('timeTravel.clear')} {t('timeTravel.clear')}
@@ -29,17 +29,17 @@ export function VersionBadge() {
return ( return (
<span <span
className="inline-flex items-center gap-1.5 text-xs text-carbon/60 font-mono select-none" className="inline-flex items-center gap-1.5 text-xs text-mute font-mono select-none"
title={tooltip} title={tooltip}
> >
{updateAvailable && ( {updateAvailable && (
<span <span
aria-label="Доступна новая версия" aria-label="Доступна новая версия"
className="inline-block w-1.5 h-1.5 rounded-full bg-ultramarain animate-pulse" className="inline-block w-1.5 h-1.5 rounded-full bg-accent animate-pulse"
/> />
)} )}
<span>v{clientVersion}</span> <span>v{clientVersion}</span>
<span className="text-carbon/40">·</span> <span className="text-mute/70">·</span>
<span>{clientCommit.substring(0, 7)}</span> <span>{clientCommit.substring(0, 7)}</span>
</span> </span>
) )
+32 -18
View File
@@ -11,32 +11,46 @@ export const SCOPE_ORDER: ReadonlyArray<DataScope> = [
] ]
/** /**
* Solid background classes for -shaped scope indicators (size-2 dots, * Solid background classes для -shaped scope indicators (size-2 dots,
* section-header bullets). * section-header bullets). Use design handoff tokens (Tailwind v4 @theme):
* - PUBLIC accent (terracotta, primary brand)
* - INTERNAL warn (amber)
* - RESTRICTED pink (brick)
*/ */
export const SCOPE_DOT: Record<DataScope, string> = { export const SCOPE_DOT: Record<DataScope, string> = {
PUBLIC: 'bg-emerald-500', PUBLIC: 'bg-accent',
INTERNAL: 'bg-amber-500', INTERNAL: 'bg-warn',
RESTRICTED: 'bg-rose-500', RESTRICTED: 'bg-pink',
}
/** Same colors но для text-color (PUB chip filled bg, etc.). */
export const SCOPE_TEXT: Record<DataScope, string> = {
PUBLIC: 'text-accent',
INTERNAL: 'text-warn',
RESTRICTED: 'text-pink',
}
/** Same colors но для tinted background (pill chips, banner backgrounds). */
export const SCOPE_BG_TINT: Record<DataScope, string> = {
PUBLIC: 'bg-accent-bg',
INTERNAL: 'bg-warn-bg',
RESTRICTED: 'bg-pink-bg',
} }
/** /**
* `before:` background classes used for the 4px left-accent stripe on * `before:` background classes для 4px left-accent stripe на card'ах.
* dictionary cards. Pair with `relative` + `before:absolute before:left-0 * Pair with `relative` + `before:absolute before:left-0 before:top-0
* before:top-0 before:bottom-0 before:w-1 before:rounded-l-lg`. * before:bottom-0 before:w-1 before:rounded-l-lg`.
*/ */
export const SCOPE_ACCENT: Record<DataScope, string> = { export const SCOPE_ACCENT: Record<DataScope, string> = {
PUBLIC: 'before:bg-emerald-500', PUBLIC: 'before:bg-accent',
INTERNAL: 'before:bg-amber-500', INTERNAL: 'before:bg-warn',
RESTRICTED: 'before:bg-rose-500', RESTRICTED: 'before:bg-pink',
} }
/** /** Solid border-top classes для page-level scope banner stripes. */
* Solid border-top classes for page-level scope banner stripes (used on
* dictionary detail page).
*/
export const SCOPE_BORDER_TOP: Record<DataScope, string> = { export const SCOPE_BORDER_TOP: Record<DataScope, string> = {
PUBLIC: 'border-t-emerald-500', PUBLIC: 'border-t-accent',
INTERNAL: 'border-t-amber-500', INTERNAL: 'border-t-warn',
RESTRICTED: 'border-t-rose-500', RESTRICTED: 'border-t-pink',
} }
+14 -14
View File
@@ -299,7 +299,7 @@ function ActiveFilterChips({ filters, onRemove }: ActiveFilterChipsProps) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex flex-wrap items-center gap-2"> <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')} {t('audit.filter.active')}
</span> </span>
{filters.map((f) => ( {filters.map((f) => (
@@ -309,14 +309,14 @@ function ActiveFilterChips({ filters, onRemove }: ActiveFilterChipsProps) {
onClick={() => onRemove(f.key)} onClick={() => onRemove(f.key)}
className={[ className={[
'inline-flex items-center gap-1.5 px-2 py-1 rounded-sm border text-2xs', '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', 'transition-colors group',
].join(' ')} ].join(' ')}
title={t('audit.filter.removeChip')} 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> <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> </button>
))} ))}
</div> </div>
@@ -356,7 +356,7 @@ function FilterPanel({
} }
return ( 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"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<SingleSelect <SingleSelect
label={t('audit.filter.dictionary')} label={t('audit.filter.dictionary')}
@@ -411,7 +411,7 @@ function FilterPanel({
} }
/> />
</div> </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}> <Button type="button" variant="secondary" onClick={onReset}>
{t('audit.filter.reset')} {t('audit.filter.reset')}
</Button> </Button>
@@ -486,7 +486,7 @@ function AuditRow({ row }: { row: AuditEntry }) {
</TableCell> </TableCell>
<TableCell> <TableCell>
<span <span
className="font-mono text-2xs text-carbon/70" className="font-mono text-2xs text-ink-2"
title={row.traceId ?? ''} title={row.traceId ?? ''}
> >
{row.traceId ? row.traceId.slice(0, 8) : '—'} {row.traceId ? row.traceId.slice(0, 8) : '—'}
@@ -504,29 +504,29 @@ function AuditRow({ row }: { row: AuditEntry }) {
{open && ( {open && (
<TableRow> <TableRow>
<TableCell colSpan={8}> <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>
<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')} {t('audit.diff.before')}
</div> </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 {row.payloadBefore
? JSON.stringify(row.payloadBefore, null, 2) ? JSON.stringify(row.payloadBefore, null, 2)
: '—'} : '—'}
</pre> </pre>
</div> </div>
<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')} {t('audit.diff.after')}
</div> </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 {row.payloadAfter
? JSON.stringify(row.payloadAfter, null, 2) ? JSON.stringify(row.payloadAfter, null, 2)
: '—'} : '—'}
</pre> </pre>
</div> </div>
{(row.ipAddress || row.userAgent || row.requestId) && ( {(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.ipAddress && <span>IP: {row.ipAddress}</span>}
{row.requestId && <span>req: {row.requestId}</span>} {row.requestId && <span>req: {row.requestId}</span>}
{row.userAgent && ( {row.userAgent && (
@@ -563,7 +563,7 @@ function Pagination({
return ( return (
<div className="flex items-center justify-between flex-wrap gap-3"> <div className="flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-2 text-sm"> <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 <SingleSelect
options={PAGE_SIZE_OPTIONS} options={PAGE_SIZE_OPTIONS}
value={String(size)} value={String(size)}
@@ -493,12 +493,12 @@ function DictionaryDetail() {
<PageHeader <PageHeader
breadcrumb={ breadcrumb={
<div className="flex items-center gap-3 text-sm"> <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')} {t('nav.dictionaries')}
</Link> </Link>
{scope && ( {scope && (
<span className="flex items-center gap-1.5 text-carbon/70"> <span className="flex items-center gap-1.5 text-ink-2">
<span className="text-carbon/40">·</span> <span className="text-mute/70">·</span>
<span <span
className={`inline-block size-2 rounded-full ${SCOPE_DOT[scope]}`} className={`inline-block size-2 rounded-full ${SCOPE_DOT[scope]}`}
aria-hidden="true" aria-hidden="true"
@@ -569,7 +569,7 @@ function DictionaryDetail() {
{/* View toggle: Записи (default table-based view) | Связи (Hub view с {/* View toggle: Записи (default table-based view) | Связи (Hub view с
neighbors на боках). Per design handoff B. */} neighbors на боках). Per design handoff B. */}
{detailQuery.data && ( {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) => { {(['records', 'hub'] as const).map((mode) => {
const active = (urlSearch.view ?? 'records') === mode const active = (urlSearch.view ?? 'records') === mode
return ( return (
@@ -586,10 +586,10 @@ function DictionaryDetail() {
}) })
} }
aria-pressed={active} 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 active
? 'bg-orbit/40 text-ultramarain' ? 'bg-accent-bg text-accent'
: 'hover:bg-orbit/20 text-carbon' : 'hover:bg-accent-bg/20 text-ink'
}`} }`}
> >
{t(`dict.view.${mode}`, { {t(`dict.view.${mode}`, {
@@ -611,8 +611,8 @@ function DictionaryDetail() {
)} )}
{aoi && ( {aoi && (
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-ultramarain/30 bg-ultramarain/4 text-sm"> <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-carbon/80"> <span className="font-mono text-xs text-ink">
{aoi.kind === 'bbox' {aoi.kind === 'bbox'
? t('aoi.activeBbox', { value: aoi.bboxCsv }) ? t('aoi.activeBbox', { value: aoi.bboxCsv })
: t('aoi.activePolygon', { : t('aoi.activePolygon', {
@@ -621,7 +621,7 @@ function DictionaryDetail() {
</span> </span>
<button <button
type="button" type="button"
className="text-xs text-ultramarain hover:underline" className="text-xs text-accent hover:underline"
onClick={handleAoiClear} onClick={handleAoiClear}
> >
{t('aoi.clear')} {t('aoi.clear')}
@@ -644,16 +644,16 @@ function DictionaryDetail() {
{/* Active state — ambient banner если ?at= задан, даже если picker закрыт. */} {/* Active state — ambient banner если ?at= задан, даже если picker закрыт. */}
{timeTravelAt && !timeTravelOpen && ( {timeTravelAt && !timeTravelOpen && (
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-orbit/40 bg-orbit/8 text-sm"> <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-orbit shrink-0" /> <ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn shrink-0" />
<span className="font-mono text-xs text-carbon/80"> <span className="font-mono text-xs text-ink">
{t('timeTravel.viewing', { {t('timeTravel.viewing', {
date: new Date(timeTravelAt).toLocaleString(), date: new Date(timeTravelAt).toLocaleString(),
})} })}
</span> </span>
<button <button
type="button" type="button"
className="text-xs text-ultramarain hover:underline ml-auto" className="text-xs text-accent hover:underline ml-auto"
onClick={() => setTimeTravelAt(undefined)} onClick={() => setTimeTravelAt(undefined)}
> >
{t('timeTravel.clear')} {t('timeTravel.clear')}
@@ -694,10 +694,10 @@ function DictionaryDetail() {
onClick={() => toggleScope(s)} onClick={() => toggleScope(s)}
aria-pressed={active} aria-pressed={active}
className={[ 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 active
? 'border-ultramarain bg-ultramarain/8 text-ultramarain' ? 'border-accent bg-accent/8 text-accent'
: 'border-regolith bg-white text-carbon/70 hover:border-carbon/40', : 'border-line bg-white text-ink-2 hover:border-carbon/40',
].join(' ')} ].join(' ')}
> >
<span <span
@@ -710,14 +710,14 @@ function DictionaryDetail() {
})} })}
</div> </div>
{filtersActive && ( {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> <span>
{t('dict.filter.matched', { count: filteredCount, total: totalRecords })} {t('dict.filter.matched', { count: filteredCount, total: totalRecords })}
</span> </span>
<button <button
type="button" type="button"
onClick={handleClearFilters} onClick={handleClearFilters}
className="text-ultramarain hover:underline" className="text-accent hover:underline"
> >
{t('dict.filter.clear')} {t('dict.filter.clear')}
</button> </button>
@@ -796,7 +796,7 @@ function DictionaryDetail() {
if (v == null || v === '') { if (v == null || v === '') {
return ( return (
<TableCell key={c.key}> <TableCell key={c.key}>
<span className="text-2xs text-carbon/40"></span> <span className="text-2xs text-mute/70"></span>
</TableCell> </TableCell>
) )
} }
@@ -807,8 +807,8 @@ function DictionaryDetail() {
className={[ className={[
'inline-block px-1.5 py-0.5 rounded-sm font-mono text-2xs', 'inline-block px-1.5 py-0.5 rounded-sm font-mono text-2xs',
c.isFk c.isFk
? 'bg-ultramarain/8 text-ultramarain' ? 'bg-accent/8 text-accent'
: 'bg-carbon/8 text-carbon/80', : 'bg-carbon/8 text-ink',
].join(' ')} ].join(' ')}
> >
{text} {text}
@@ -820,7 +820,7 @@ function DictionaryDetail() {
<Badge variant="info">{r.dataScope}</Badge> <Badge variant="info">{r.dataScope}</Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-2xs text-carbon/60"> <span className="text-2xs text-mute">
{new Date(r.validFrom).toLocaleDateString()} {new Date(r.validFrom).toLocaleDateString()}
</span> </span>
</TableCell> </TableCell>
@@ -943,7 +943,7 @@ function DictionaryDetail() {
> >
{edit.kind === 'close-confirm' && ( {edit.kind === 'close-confirm' && (
<div className="space-y-4"> <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 })} {t('dict.confirmClose.body', { key: edit.record.businessKey })}
</p> </p>
<TextInput <TextInput
@@ -955,7 +955,7 @@ function DictionaryDetail() {
{serverErrorMessage(closeMut.error) && ( {serverErrorMessage(closeMut.error) && (
<Alert variant="error">{serverErrorMessage(closeMut.error)}</Alert> <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 <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -1062,7 +1062,7 @@ function BulkSelectionToolbar({
<div <div
className={[ className={[
'sticky top-0 z-10 -mx-1 flex items-center justify-between gap-3', '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(' ')} ].join(' ')}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -1123,7 +1123,7 @@ function RecordsPagination({
const to = Math.min((page + 1) * pageSize, totalCount) const to = Math.min((page + 1) * pageSize, totalCount)
return ( return (
<div className="flex items-center justify-between flex-wrap gap-3 px-2"> <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 })} {t('dict.page.range', { from, to, total: totalCount })}
</span> </span>
<div className="flex items-center gap-3 text-sm"> <div className="flex items-center gap-3 text-sm">
@@ -1136,7 +1136,7 @@ function RecordsPagination({
> >
{t('dict.page.prev')} {t('dict.page.prev')}
</Button> </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 })} {t('dict.page.of', { cur: page + 1, total: totalPages })}
</span> </span>
<Button <Button
@@ -1202,7 +1202,7 @@ function BulkCloseDialog({
</div> </div>
{(result.skipped.length > 0 || result.errors.length > 0) && ( {(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"> <ul className="text-2xs font-mono space-y-1">
{result.skipped.map((s) => ( {result.skipped.map((s) => (
<li key={`s-${s.businessKey}`} className="text-saturn"> <li key={`s-${s.businessKey}`} className="text-saturn">
@@ -1218,7 +1218,7 @@ function BulkCloseDialog({
</div> </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}> <Button type="button" variant="primary" onClick={onClose}>
{t('dict.bulk.dismiss')} {t('dict.bulk.dismiss')}
</Button> </Button>
@@ -1229,7 +1229,7 @@ function BulkCloseDialog({
return ( return (
<div className="space-y-4"> <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 <TextInput
label={t('dict.confirmClose.reason')} label={t('dict.confirmClose.reason')}
placeholder={t('dict.confirmClose.reasonPlaceholder')} placeholder={t('dict.confirmClose.reasonPlaceholder')}
@@ -1237,7 +1237,7 @@ function BulkCloseDialog({
onChange={(e) => onReasonChange(e.target.value)} onChange={(e) => onReasonChange(e.target.value)}
/> />
{serverError && <Alert variant="error">{serverError}</Alert>} {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 <Button
type="button" type="button"
variant="ghost" variant="ghost"
@@ -84,10 +84,11 @@ export const groupByBundle = (
} }
// Scope strip color — vertical 3px на левом крае карточки. // Scope strip color — vertical 3px на левом крае карточки.
// Использует design handoff tokens — auto-switch в dark mode.
const SCOPE_STRIP: Record<DataScope, string> = { const SCOPE_STRIP: Record<DataScope, string> = {
PUBLIC: 'bg-emerald-500', PUBLIC: 'bg-accent',
INTERNAL: 'bg-amber-500', INTERNAL: 'bg-warn',
RESTRICTED: 'bg-rose-500', RESTRICTED: 'bg-pink',
} }
// ===== Route ===== // ===== Route =====
@@ -252,7 +253,7 @@ function DictionariesPage() {
/> />
</div> </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} {filtered.length}/{data.length}
</span> </span>
@@ -272,10 +273,10 @@ 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={`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 selected
? 'border-ultramarain bg-orbit/40 text-ultramarain' ? 'border-accent bg-accent-bg text-accent'
: 'border-regolith hover:border-ultramarain/60 text-carbon' : 'border-line hover:border-accent/60 text-ink'
}`} }`}
> >
<span <span
@@ -283,14 +284,14 @@ function DictionariesPage() {
aria-hidden="true" aria-hidden="true"
/> />
{t(`dict.list.section.${scope}.short`)} {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> </button>
) )
})} })}
</div> </div>
{/* Vertical divider */} {/* 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 */} {/* Bundle filter — chips inline */}
<div <div
@@ -302,10 +303,10 @@ function DictionariesPage() {
type="button" type="button"
onClick={() => setBundle(undefined)} onClick={() => setBundle(undefined)}
aria-pressed={!bundleFilter} 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 !bundleFilter
? 'border-ultramarain bg-orbit/40 text-ultramarain' ? 'border-accent bg-accent-bg text-accent'
: 'border-regolith hover:border-ultramarain/60 text-carbon' : 'border-line hover:border-accent/60 text-ink'
}`} }`}
> >
{t('dict.list.bundle.all')} {t('dict.list.bundle.all')}
@@ -320,31 +321,31 @@ function DictionariesPage() {
type="button" type="button"
onClick={() => setBundle(selected ? undefined : bundle)} onClick={() => setBundle(selected ? undefined : bundle)}
aria-pressed={selected} 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 selected
? 'border-ultramarain bg-orbit/40 text-ultramarain' ? 'border-accent bg-accent-bg text-accent'
: 'border-regolith hover:border-ultramarain/60 text-carbon' : 'border-line hover:border-accent/60 text-ink'
}`} }`}
> >
{bundle} {bundle}
<span className="text-carbon/60">{count}</span> <span className="text-mute">{count}</span>
</button> </button>
) )
})} })}
</div> </div>
{/* Vertical divider + deps toggle */} {/* 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 <button
type="button" type="button"
onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })} onClick={() => setSearch({ deps: withDepsOnly ? undefined : '1' })}
aria-pressed={withDepsOnly} aria-pressed={withDepsOnly}
title={t('dict.list.deps.only')} 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 withDepsOnly
? 'border-ultramarain bg-orbit/40 text-ultramarain' ? 'border-accent bg-accent-bg text-accent'
: 'border-regolith hover:border-ultramarain/60 text-carbon' : 'border-line hover:border-accent/60 text-ink'
}`} }`}
> >
{t('dict.list.deps.short')} {t('dict.list.deps.short')}
@@ -353,8 +354,8 @@ function DictionariesPage() {
{/* Empty state */} {/* Empty state */}
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className="border border-regolith rounded-lg p-12 text-center bg-orbit/10"> <div className="border border-line rounded-lg p-12 text-center bg-surface-2">
<p className="font-primary text-lg text-ultramarain mb-2"> <p className="font-sans text-lg text-accent mb-2">
{t('dict.list.search.empty')} {t('dict.list.search.empty')}
</p> </p>
{filtersActive && ( {filtersActive && (
@@ -367,11 +368,11 @@ function DictionariesPage() {
// Bundle-grouped 2-col card grid // Bundle-grouped 2-col card grid
Array.from(grouped.entries()).map(([bundle, items]) => ( Array.from(grouped.entries()).map(([bundle, items]) => (
<section key={bundle} className="space-y-3"> <section key={bundle} className="space-y-3">
<h2 className="flex items-baseline gap-3 pb-2 border-b border-regolith"> <h2 className="flex items-baseline gap-3 pb-2 border-b border-line">
<span className="text-sm uppercase tracking-label text-ultramarain font-mono font-medium"> <span className="text-sm uppercase tracking-[0.10em] text-accent font-mono font-medium">
{bundle} {bundle}
</span> </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')} · {items.length} {t('dict.list.records.short')}
</span> </span>
</h2> </h2>
@@ -433,10 +434,11 @@ const FkChip = ({
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: to }} params={{ name: to }}
onClick={(e) => e.stopPropagation()} 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 dim
? 'border border-dashed border-regolith text-carbon/60 hover:border-ultramarain/60 hover:text-ultramarain' ? 'border border-dashed border-line text-mute hover:border-accent/60 hover:text-accent'
: 'border border-regolith bg-orbit/30 text-ultramarain hover:bg-orbit/60' : 'bg-accent-bg text-accent hover:bg-accent hover:text-on-accent'
}`} }`}
> >
{label} {label}
@@ -457,30 +459,30 @@ const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => {
<Link <Link
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: d.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" /> <span className={SCOPE_STRIP[d.scope]} aria-hidden="true" />
{/* Body */} {/* Body */}
<div className="px-3.5 py-3 min-w-0"> <div className="px-3.5 py-3 min-w-0">
<div className="flex items-baseline gap-2.5 flex-wrap"> <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} {d.displayName ?? d.name}
</h3> </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 && ( {d.approvalRequired && (
<Badge variant="warning">{t('dict.list.approval')}</Badge> <Badge variant="warning">{t('dict.list.approval')}</Badge>
)} )}
</div> </div>
{d.description && ( {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} {d.description}
</p> </p>
)} )}
{hasFkRow && ( {hasFkRow && (
<div className="flex flex-wrap items-center gap-1.5 mt-2.5"> <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 })} {t('dict.list.fk.usedBy', { count: refBy.length })}
</span> </span>
{refBy.slice(0, MAX_REFBY_CHIPS).map((dep) => ( {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 && ( {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} +{refBy.length - MAX_REFBY_CHIPS}
</span> </span>
)} )}
@@ -503,11 +505,11 @@ const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => {
{/* Right rail: scope badge + version */} {/* Right rail: scope badge + version */}
<div className="px-3.5 py-3 flex flex-col items-end gap-1.5 shrink-0"> <div className="px-3.5 py-3 flex flex-col items-end gap-1.5 shrink-0">
<Badge variant="info">{d.scope}</Badge> <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} v{d.schemaVersion}
</span> </span>
{typeof d.recordCount === 'number' && ( {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 })} {t('dict.list.recordCount', { count: d.recordCount })}
</span> </span>
)} )}
+5 -5
View File
@@ -82,7 +82,7 @@ function MyDraftsPage() {
{drafts.data && drafts.data.totalElements > 0 && ( {drafts.data && drafts.data.totalElements > 0 && (
<Panel> <Panel>
<div className="flex items-center justify-between mb-3"> <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 })} {t('myDrafts.total', { count: drafts.data.totalElements })}
</p> </p>
</div> </div>
@@ -109,10 +109,10 @@ function MyDraftsPage() {
<TableCell> <TableCell>
<Badge variant={statusVariant(d.status)}>{d.status}</Badge> <Badge variant={statusVariant(d.status)}>{d.status}</Badge>
</TableCell> </TableCell>
<TableCell className="text-2xs text-carbon/70"> <TableCell className="text-2xs text-ink-2">
{new Date(d.submittedAt).toLocaleString()} {new Date(d.submittedAt).toLocaleString()}
</TableCell> </TableCell>
<TableCell className="text-2xs text-carbon/70"> <TableCell className="text-2xs text-ink-2">
{d.reviewedAt {d.reviewedAt
? new Date(d.reviewedAt).toLocaleString() ? new Date(d.reviewedAt).toLocaleString()
: '—'} : '—'}
@@ -120,7 +120,7 @@ function MyDraftsPage() {
<TableCell className="font-mono text-2xs"> <TableCell className="font-mono text-2xs">
{d.reviewerId ?? '—'} {d.reviewerId ?? '—'}
</TableCell> </TableCell>
<TableCell className="text-2xs text-carbon/80 max-w-md"> <TableCell className="text-2xs text-ink max-w-md">
{d.reviewComment ?? d.makerComment ?? '—'} {d.reviewComment ?? d.makerComment ?? '—'}
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -134,7 +134,7 @@ function MyDraftsPage() {
{t('myDrafts.action.withdraw')} {t('myDrafts.action.withdraw')}
</Button> </Button>
) : ( ) : (
<span className="text-2xs text-carbon/40"></span> <span className="text-2xs text-mute/70"></span>
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
+5 -5
View File
@@ -105,7 +105,7 @@ function OutboxPage() {
<TableCell>{row.attemptCount}</TableCell> <TableCell>{row.attemptCount}</TableCell>
<TableCell> <TableCell>
<span <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 ?? ''} title={row.lastError ?? ''}
> >
{row.lastError ?? '—'} {row.lastError ?? '—'}
@@ -162,13 +162,13 @@ function StatCard({ label, value, tone }: StatCardProps) {
? 'text-mars' ? 'text-mars'
: tone === 'success' : tone === 'success'
? 'text-grass' ? 'text-grass'
: 'text-ultramarain' : 'text-accent'
return ( return (
<div className="bg-white border border-regolith rounded-lg p-4"> <div className="bg-white border border-line rounded-lg p-4">
<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">
{label} {label}
</div> </div>
<div className={`text-2xl font-primary tabular-nums ${colour}`}>{value}</div> <div className={`text-2xl font-sans tabular-nums ${colour}`}>{value}</div>
</div> </div>
) )
} }
+16 -16
View File
@@ -196,10 +196,10 @@ function ReviewsPage() {
{queue.data && queue.data.totalElements > 0 && ( {queue.data && queue.data.totalElements > 0 && (
<Panel> <Panel>
<div className="flex items-center justify-between mb-3 gap-3"> <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 })} {t('reviews.queueTotal', { count: queue.data.totalElements })}
{anySelected ? ( {anySelected ? (
<span className="ml-2 text-ultramarain font-medium"> <span className="ml-2 text-accent font-medium">
· {t('reviews.bulk.selectedCount', { count: selection.size })} · {t('reviews.bulk.selectedCount', { count: selection.size })}
</span> </span>
) : null} ) : null}
@@ -264,7 +264,7 @@ function ReviewsPage() {
<Link <Link
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: d.dictionaryId }} 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)} {d.dictionaryId.slice(0, 8)}
</Link> </Link>
@@ -274,7 +274,7 @@ function ReviewsPage() {
<Badge variant={operationVariant(d.operation)}>{d.operation}</Badge> <Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
</TableCell> </TableCell>
<TableCell className="font-mono text-2xs">{d.makerId}</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()} {new Date(d.submittedAt).toLocaleString()}
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -304,7 +304,7 @@ function ReviewsPage() {
title={t('reviews.bulk.rejectModalTitle', { count: selection.size })} title={t('reviews.bulk.rejectModalTitle', { count: selection.size })}
> >
<div className="space-y-3"> <div className="space-y-3">
<p className="text-2xs text-carbon/70"> <p className="text-2xs text-ink-2">
{t('reviews.bulk.rejectModalDescription')} {t('reviews.bulk.rejectModalDescription')}
</p> </p>
<TextArea <TextArea
@@ -403,18 +403,18 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
<div className="flex flex-wrap items-center gap-2 text-2xs"> <div className="flex flex-wrap items-center gap-2 text-2xs">
<Badge variant={operationVariant(draft.operation)}>{draft.operation}</Badge> <Badge variant={operationVariant(draft.operation)}>{draft.operation}</Badge>
<span className="font-mono">{draft.businessKey}</span> <span className="font-mono">{draft.businessKey}</span>
<span className="text-carbon/60"> <span className="text-mute">
{t('reviews.drawer.maker')}: {draft.makerId} {t('reviews.drawer.maker')}: {draft.makerId}
</span> </span>
<span className="text-carbon/60"> <span className="text-mute">
{t('reviews.drawer.submitted')}:{' '} {t('reviews.drawer.submitted')}:{' '}
{new Date(draft.submittedAt).toLocaleString()} {new Date(draft.submittedAt).toLocaleString()}
</span> </span>
</div> </div>
{draft.makerComment && ( {draft.makerComment && (
<div className="px-3 py-2 rounded-sm border border-regolith bg-regolith/30 text-2xs"> <div className="px-3 py-2 rounded-sm border border-line bg-line/30 text-2xs">
<span className="text-carbon/60 uppercase tracking-label font-secondary"> <span className="text-mute uppercase tracking-[0.10em] font-display">
{t('reviews.drawer.makerComment')}: {t('reviews.drawer.makerComment')}:
</span>{' '} </span>{' '}
{draft.makerComment} {draft.makerComment}
@@ -423,23 +423,23 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<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.live')} {t('reviews.drawer.live')}
</p> </p>
{liveQ.isLoading && <LoadingBlock size="sm" label={t('loading')} />} {liveQ.isLoading && <LoadingBlock size="sm" label={t('loading')} />}
{liveQ.data === null && ( {liveQ.data === null && (
<p className="text-2xs text-carbon/60 italic"> <p className="text-2xs text-mute italic">
{t('reviews.drawer.noLive')} {t('reviews.drawer.noLive')}
</p> </p>
)} )}
{liveQ.data && ( {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)} {JSON.stringify(liveQ.data.data, null, 2)}
</pre> </pre>
)} )}
</div> </div>
<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')} {t('reviews.drawer.proposed')}
</p> </p>
{draft.operation === 'CLOSE' ? ( {draft.operation === 'CLOSE' ? (
@@ -447,7 +447,7 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
{t('reviews.drawer.closeNote')} {t('reviews.drawer.closeNote')}
</p> </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)} {JSON.stringify(draft.data, null, 2)}
</pre> </pre>
)} )}
@@ -460,10 +460,10 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
</Alert> </Alert>
)} )}
<div className="border-t border-regolith pt-3 space-y-2"> <div className="border-t border-line pt-3 space-y-2">
<label <label
htmlFor="review-comment" 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')} {t('reviews.drawer.comment')}
</label> </label>
+6 -6
View File
@@ -70,7 +70,7 @@ function SearchPage() {
</form> </form>
{tooShort && ( {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 && ( {hasQuery && result.isLoading && (
@@ -89,7 +89,7 @@ function SearchPage() {
{hasQuery && result.data && result.data.total > 0 && ( {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 })} {t('search.totalHits', { count: result.data.total })}
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
@@ -100,7 +100,7 @@ function SearchPage() {
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: g.dictName }} params={{ name: g.dictName }}
search={{ q: urlSearch.q }} 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} {g.dictDisplayName ?? g.dictName}
</Link> </Link>
@@ -116,13 +116,13 @@ function SearchPage() {
to="/dictionaries/$name" to="/dictionaries/$name"
params={{ name: g.dictName }} params={{ name: g.dictName }}
search={{ q: it.businessKey }} search={{ q: it.businessKey }}
className="font-mono text-ultramarain hover:underline truncate" className="font-mono text-accent hover:underline truncate"
> >
{it.businessKey} {it.businessKey}
</Link> </Link>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<Badge variant="neutral">{it.dataScope}</Badge> <Badge variant="neutral">{it.dataScope}</Badge>
<span className="text-carbon/60"> <span className="text-mute">
{new Date(it.createdAt).toLocaleString()} {new Date(it.createdAt).toLocaleString()}
</span> </span>
</div> </div>
@@ -136,7 +136,7 @@ function SearchPage() {
)} )}
{!hasQuery && !tooShort && ( {!hasQuery && !tooShort && (
<p className="text-2xs text-carbon/60">{t('search.intro')}</p> <p className="text-2xs text-mute">{t('search.intro')}</p>
)} )}
</div> </div>
) )
+18 -18
View File
@@ -115,7 +115,7 @@ function WebhookDetailPage() {
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
breadcrumb={ 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')} {t('nav.webhooks')}
</Link> </Link>
} }
@@ -169,7 +169,7 @@ function WebhookDetailPage() {
{data.scopeFilter.map((s) => ( {data.scopeFilter.map((s) => (
<span <span
key={s} 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 /> <span className={`inline-block size-2 rounded-full ${SCOPE_DOT[s]}`} aria-hidden />
{s} {s}
@@ -177,7 +177,7 @@ function WebhookDetailPage() {
))} ))}
</div> </div>
) : ( ) : (
<span className="text-2xs text-carbon/50">{t('webhooks.allScopes')}</span> <span className="text-2xs text-mute">{t('webhooks.allScopes')}</span>
)} )}
</InfoRow> </InfoRow>
<InfoRow label={t('webhooks.field.dictionaryFilter')}> <InfoRow label={t('webhooks.field.dictionaryFilter')}>
@@ -207,7 +207,7 @@ function WebhookDetailPage() {
<div> <div>
<div className="flex items-center justify-between mb-3"> <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')} {t('webhooks.deliveries.title')}
</h2> </h2>
<IconButton <IconButton
@@ -232,10 +232,10 @@ function WebhookDetailPage() {
onClick={() => handleStatusToggle(s)} onClick={() => handleStatusToggle(s)}
aria-pressed={active} aria-pressed={active}
className={[ 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 active
? 'border-ultramarain bg-ultramarain/8 text-ultramarain' ? 'border-accent bg-accent/8 text-accent'
: 'border-regolith bg-white text-carbon/70 hover:border-carbon/40', : 'border-line bg-white text-ink-2 hover:border-carbon/40',
].join(' ')} ].join(' ')}
> >
{s} {s}
@@ -249,7 +249,7 @@ function WebhookDetailPage() {
setPage(0) setPage(0)
setStatusFilter(undefined) 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')} {t('webhooks.deliveries.filter.clear')}
</button> </button>
@@ -293,7 +293,7 @@ function WebhookDetailPage() {
<span className="font-mono text-2xs">{d.attemptCount}</span> <span className="font-mono text-2xs">{d.attemptCount}</span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-2xs text-carbon/70"> <span className="text-2xs text-ink-2">
{d.lastAttemptAt {d.lastAttemptAt
? new Date(d.lastAttemptAt).toLocaleString() ? new Date(d.lastAttemptAt).toLocaleString()
: '—'} : '—'}
@@ -305,7 +305,7 @@ function WebhookDetailPage() {
</span> </span>
</TableCell> </TableCell>
<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 ?? '—'} {d.lastError ?? '—'}
</span> </span>
</TableCell> </TableCell>
@@ -337,7 +337,7 @@ function WebhookDetailPage() {
> >
</Button> </Button>
<span className="text-sm text-carbon/70 self-center"> <span className="text-sm text-ink-2 self-center">
{page + 1} / {deliveries.data.totalPages} {page + 1} / {deliveries.data.totalPages}
</span> </span>
<Button <Button
@@ -384,7 +384,7 @@ function TestResultAlert({
action={ action={
<button <button
type="button" type="button"
className="text-xs text-carbon/70 hover:text-carbon" className="text-xs text-ink-2 hover:text-ink"
onClick={onDismiss} onClick={onDismiss}
> >
@@ -394,7 +394,7 @@ function TestResultAlert({
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
{result.httpStatus != null && ( {result.httpStatus != null && (
<div> <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 HTTP
</span> </span>
<span className="font-mono">{result.httpStatus}</span> <span className="font-mono">{result.httpStatus}</span>
@@ -402,14 +402,14 @@ function TestResultAlert({
)} )}
{result.latencyMs != null && ( {result.latencyMs != null && (
<div> <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')} {t('webhooks.test.latency')}
</span> </span>
<span className="font-mono">{result.latencyMs} ms</span> <span className="font-mono">{result.latencyMs} ms</span>
</div> </div>
)} )}
{result.message && ( {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} {result.message}
</div> </div>
)} )}
@@ -421,7 +421,7 @@ function TestResultAlert({
function InfoRow({ label, children }: { label: string; children: React.ReactNode }) { function InfoRow({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<div> <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} {label}
</div> </div>
<div>{children}</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" className="bg-white rounded-lg p-6 max-w-xl w-full space-y-4"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<h3 className="font-primary text-lg text-ultramarain"> <h3 className="font-sans text-lg text-accent">
{t('webhooks.secret.rotatedTitle')} {t('webhooks.secret.rotatedTitle')}
</h3> </h3>
<Alert variant="warning" title={t('webhooks.secret.warningTitle')}> <Alert variant="warning" title={t('webhooks.secret.warningTitle')}>
{t('webhooks.secret.rotatedWarning')} {t('webhooks.secret.rotatedWarning')}
</Alert> </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> <code className="font-mono text-2xs break-all">{secret}</code>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
@@ -112,7 +112,7 @@ function WebhooksPage() {
<Link <Link
to="/webhooks/$id" to="/webhooks/$id"
params={{ id: sub.id }} params={{ id: sub.id }}
className="font-medium text-ultramarain hover:underline" className="font-medium text-accent hover:underline"
> >
{sub.name} {sub.name}
</Link> </Link>
@@ -127,12 +127,12 @@ function WebhooksPage() {
</TableCell> </TableCell>
<TableCell> <TableCell>
{sub.dictionaryFilter && sub.dictionaryFilter.length > 0 ? ( {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.slice(0, 3).join(', ')}
{sub.dictionaryFilter.length > 3 && ` +${sub.dictionaryFilter.length - 3}`} {sub.dictionaryFilter.length > 3 && ` +${sub.dictionaryFilter.length - 3}`}
</span> </span>
) : ( ) : (
<span className="text-2xs text-carbon/50">{t('webhooks.allDicts')}</span> <span className="text-2xs text-mute">{t('webhooks.allDicts')}</span>
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
@@ -175,14 +175,14 @@ function WebhooksPage() {
function ScopeChips({ scopes }: { scopes?: DataScope[] | null }) { function ScopeChips({ scopes }: { scopes?: DataScope[] | null }) {
const { t } = useTranslation() const { t } = useTranslation()
if (!scopes || scopes.length === 0) { 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 ( return (
<div className="flex gap-1"> <div className="flex gap-1">
{scopes.map((s) => ( {scopes.map((s) => (
<span <span
key={s} 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} title={s}
> >
<span className={`inline-block size-2 rounded-full ${SCOPE_DOT[s]}`} aria-hidden /> <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')}> <Alert variant="warning" title={t('webhooks.secret.warningTitle')}>
{t('webhooks.secret.warning')} {t('webhooks.secret.warning')}
</Alert> </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> <code className="font-mono text-2xs break-all">{revealed.secret}</code>
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">