Merge branch 'fix/v1.3.1-fonts-semantic-audit' into 'main'
fix(fonts): semantic typography utilities per handoff (7 roles) See merge request 2-6/2-6-4/terravault/ordinis!47
This commit is contained in:
@@ -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-ink">
|
<span className="inline-flex items-center gap-1 text-body 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>
|
||||||
|
|||||||
@@ -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-sans">Ошибка входа</h1>
|
<h1 className="text-title-lg font-sans">Ошибка входа</h1>
|
||||||
<p className="text-sm text-ink-2">
|
<p className="text-body 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-accent text-accent px-4 py-2 text-sm hover:bg-accent-bg"
|
className="mt-4 inline-flex items-center justify-center rounded-sm border border-accent text-accent px-4 py-2 text-body hover:bg-accent-bg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.location.href = '/'
|
window.location.href = '/'
|
||||||
}}
|
}}
|
||||||
@@ -59,7 +59,7 @@ export function RequireAuth({ children }: { children: ReactNode }) {
|
|||||||
if (auth.error && !auth.isAuthenticated) {
|
if (auth.error && !auth.isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="px-4 py-2 bg-amber-50 border-b border-amber-200 text-xs text-amber-900 text-center">
|
<div className="px-4 py-2 bg-amber-50 border-b border-amber-200 text-cell text-amber-900 text-center">
|
||||||
Авторизация недоступна: {auth.error.message}. Доступен просмотр публичных данных.
|
Авторизация недоступна: {auth.error.message}. Доступен просмотр публичных данных.
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export const SchemaDrivenForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{idSource && (
|
{idSource && (
|
||||||
<div className="sm:col-span-2 text-2xs text-mute px-2">
|
<div className="sm:col-span-2 text-cell text-mute px-2">
|
||||||
{t('form.idSourceNote', { field: idSource })}
|
{t('form.idSourceNote', { field: idSource })}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -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-ink-2 normal-case tracking-normal whitespace-nowrap shrink-0 w-14">
|
<span className="text-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,8 +599,8 @@ const DateTimeField = ({ label, value, defaultTime, onChange, hint, error }: Dat
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{hint && !error && <p className="text-2xs text-mute mt-1">{hint}</p>}
|
{hint && !error && <p className="text-cell text-mute mt-1">{hint}</p>}
|
||||||
{error && <p className="text-2xs text-mars mt-1">{error}</p>}
|
{error && <p className="text-cell text-mars mt-1">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -797,7 +797,7 @@ 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-display',
|
'w-full appearance-none border rounded-sm pl-3 pr-10 py-2 text-body font-display',
|
||||||
'bg-white text-black transition-colors',
|
'bg-white text-black transition-colors',
|
||||||
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
|
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
|
||||||
errorMsg ? 'border-mars' : 'border-line hover:border-carbon/40',
|
errorMsg ? 'border-mars' : 'border-line hover:border-carbon/40',
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ 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-display rounded-sm border transition-colors',
|
'px-3 py-1.5 text-cell font-display rounded-sm border transition-colors',
|
||||||
active
|
active
|
||||||
? 'border-accent bg-accent text-white'
|
? 'border-accent bg-accent text-white'
|
||||||
: 'border-line bg-white text-ink hover:border-carbon/40',
|
: 'border-line bg-white text-ink hover:border-carbon/40',
|
||||||
@@ -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-ink-2">{t('aoi.dialog.help')}</p>
|
<p className="text-body 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-display rounded-sm border border-line bg-white text-ink hover:border-carbon/40 disabled:opacity-40 disabled:cursor-not-allowed"
|
className="px-3 py-1.5 text-cell 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-mute">
|
<span className="text-cell text-mute">
|
||||||
{t('aoi.dialog.polygonHint')}
|
{t('aoi.dialog.polygonHint')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ export function DictionaryGraph() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dictsQuery.error) {
|
if (dictsQuery.error) {
|
||||||
return <div className="text-sm text-pink">Ошибка загрузки: {String(dictsQuery.error)}</div>
|
return <div className="text-body text-pink">Ошибка загрузки: {String(dictsQuery.error)}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dicts.length === 0) {
|
if (dicts.length === 0) {
|
||||||
return <div className="text-sm text-mute">Справочники не найдены.</div>
|
return <div className="text-body text-mute">Справочники не найдены.</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -222,7 +222,7 @@ function GraphCanvas({
|
|||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} className="relative w-full h-[calc(100vh-220px)] min-h-[500px] rounded-lg border border-line bg-surface overflow-hidden">
|
<div ref={wrapperRef} className="relative w-full h-[calc(100vh-220px)] min-h-[500px] rounded-lg border border-line bg-surface overflow-hidden">
|
||||||
{depsLoading && (
|
{depsLoading && (
|
||||||
<div className="absolute top-3 right-3 z-10 text-2xs font-mono uppercase tracking-wider text-mute bg-surface/80 backdrop-blur px-2 py-1 rounded">
|
<div className="text-mono absolute top-3 right-3 z-10 uppercase tracking-wider text-mute bg-surface/80 backdrop-blur px-2 py-1 rounded">
|
||||||
Загрузка связей…
|
Загрузка связей…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -318,7 +318,7 @@ function GraphCanvas({
|
|||||||
})}
|
})}
|
||||||
</svg>
|
</svg>
|
||||||
{/* Legend */}
|
{/* Legend */}
|
||||||
<div className="absolute bottom-3 left-3 flex flex-col gap-1 bg-surface/90 backdrop-blur px-3 py-2 rounded-md border border-line text-2xs">
|
<div className="absolute bottom-3 left-3 flex flex-col gap-1 bg-surface/90 backdrop-blur px-3 py-2 rounded-md border border-line text-cell">
|
||||||
<div className="font-display uppercase tracking-wider text-mute mb-0.5">Bundle</div>
|
<div className="font-display uppercase tracking-wider text-mute mb-0.5">Bundle</div>
|
||||||
{Object.entries(BUNDLE_COLORS)
|
{Object.entries(BUNDLE_COLORS)
|
||||||
.filter(([k]) => k !== 'default')
|
.filter(([k]) => k !== 'default')
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function Sidebar() {
|
|||||||
{/* User block — sticky bottom */}
|
{/* User block — sticky bottom */}
|
||||||
{auth.isAuthenticated && auth.user?.profile && (
|
{auth.isAuthenticated && auth.user?.profile && (
|
||||||
<div className="border-t border-line px-3 py-3 flex items-center gap-2">
|
<div className="border-t border-line px-3 py-3 flex items-center gap-2">
|
||||||
<div className="size-8 rounded-full bg-accent text-on-accent inline-flex items-center justify-center text-xs font-semibold">
|
<div className="size-8 rounded-full bg-accent text-on-accent inline-flex items-center justify-center text-cell font-semibold">
|
||||||
{String(
|
{String(
|
||||||
auth.user.profile.preferred_username ||
|
auth.user.profile.preferred_username ||
|
||||||
auth.user.profile.name ||
|
auth.user.profile.name ||
|
||||||
@@ -150,7 +150,7 @@ export function Sidebar() {
|
|||||||
.toUpperCase()}
|
.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="text-xs font-medium text-ink truncate">
|
<div className="text-cell font-medium text-ink truncate">
|
||||||
{String(
|
{String(
|
||||||
auth.user.profile.preferred_username ||
|
auth.user.profile.preferred_username ||
|
||||||
auth.user.profile.name ||
|
auth.user.profile.name ||
|
||||||
@@ -159,7 +159,7 @@ export function Sidebar() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{auth.user.profile.email && (
|
{auth.user.profile.email && (
|
||||||
<div className="text-2xs text-mute truncate">{String(auth.user.profile.email)}</div>
|
<div className="text-cell text-mute truncate">{String(auth.user.profile.email)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -179,9 +179,8 @@ function SidebarLink({ to, label, icon: Icon, badge }: NavItem) {
|
|||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
className={cn(
|
className={cn(
|
||||||
// 13px per handoff spec для workhorse body text. text-sm = 14px (Tailwind),
|
// text-body = 13px workhorse per handoff (см. styles.css @utility).
|
||||||
// text-[13px] = exact handoff value.
|
'group flex items-center gap-2.5 px-3 py-1.5 rounded-md text-body transition-colors',
|
||||||
'group flex items-center gap-2.5 px-3 py-1.5 rounded-md text-[13px] transition-colors',
|
|
||||||
active
|
active
|
||||||
? 'bg-accent-bg text-accent font-medium'
|
? 'bg-accent-bg text-accent font-medium'
|
||||||
: 'text-ink-2 hover:text-ink hover:bg-surface-2',
|
: 'text-ink-2 hover:text-ink hover:bg-surface-2',
|
||||||
@@ -192,7 +191,7 @@ function SidebarLink({ to, label, icon: Icon, badge }: NavItem) {
|
|||||||
{badge !== undefined && (
|
{badge !== undefined && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-sm text-[11px] font-mono',
|
'inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-sm text-mono',
|
||||||
active
|
active
|
||||||
? 'bg-accent text-on-accent'
|
? 'bg-accent text-on-accent'
|
||||||
: 'bg-surface-2 text-ink-2 group-hover:bg-line',
|
: 'bg-surface-2 text-ink-2 group-hover:bg-line',
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ export function TopBar() {
|
|||||||
return (
|
return (
|
||||||
<header className="h-14 shrink-0 border-b border-line bg-surface flex items-center px-4 sm:px-6 gap-3">
|
<header className="h-14 shrink-0 border-b border-line bg-surface flex items-center px-4 sm:px-6 gap-3">
|
||||||
{/* Breadcrumb — left */}
|
{/* Breadcrumb — left */}
|
||||||
<nav className="flex items-center gap-1.5 text-sm min-w-0">
|
<nav className="flex items-center gap-1.5 text-body min-w-0">
|
||||||
{breadcrumb.map((crumb, i) => {
|
{breadcrumb.map((crumb, i) => {
|
||||||
const isLast = i === breadcrumb.length - 1
|
const isLast = i === breadcrumb.length - 1
|
||||||
return (
|
return (
|
||||||
<span key={i} className="flex items-center gap-1.5 min-w-0">
|
<span key={i} className="flex items-center gap-1.5 min-w-0">
|
||||||
{i > 0 && <span className="text-mute text-xs">/</span>}
|
{i > 0 && <span className="text-mute text-cell">/</span>}
|
||||||
{crumb.to && !isLast ? (
|
{crumb.to && !isLast ? (
|
||||||
<Link
|
<Link
|
||||||
to={crumb.to}
|
to={crumb.to}
|
||||||
@@ -101,7 +101,7 @@ export function TopBar() {
|
|||||||
href="/docs/"
|
href="/docs/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
className="hidden sm:inline text-xs text-ink-2 hover:text-ink transition-colors"
|
className="hidden sm:inline text-cell text-ink-2 hover:text-ink transition-colors"
|
||||||
>
|
>
|
||||||
{t('nav.docs')}
|
{t('nav.docs')}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const CascadeConfirmDialog = ({
|
|||||||
{plan && (
|
{plan && (
|
||||||
<>
|
<>
|
||||||
{/* Header summary */}
|
{/* Header summary */}
|
||||||
<p className="text-sm text-ink">
|
<p className="text-body text-ink">
|
||||||
{hasBlockers
|
{hasBlockers
|
||||||
? t('cascade.summary.blocked', {
|
? t('cascade.summary.blocked', {
|
||||||
blockers: plan.blockers.length,
|
blockers: plan.blockers.length,
|
||||||
@@ -137,7 +137,7 @@ export const CascadeConfirmDialog = ({
|
|||||||
variant="error"
|
variant="error"
|
||||||
title={t('cascade.blockers.title', { count: plan.blockers.length })}
|
title={t('cascade.blockers.title', { count: plan.blockers.length })}
|
||||||
>
|
>
|
||||||
<ul className="space-y-1.5 mt-2 text-2xs">
|
<ul className="space-y-1.5 mt-2 text-cell">
|
||||||
{plan.blockers.slice(0, 5).map((b) => (
|
{plan.blockers.slice(0, 5).map((b) => (
|
||||||
<li key={b.recordId} className="flex items-center gap-2">
|
<li key={b.recordId} className="flex items-center gap-2">
|
||||||
<XCircleIcon weight="bold" size={12} className="text-aurora shrink-0" />
|
<XCircleIcon weight="bold" size={12} className="text-aurora shrink-0" />
|
||||||
@@ -153,7 +153,7 @@ export const CascadeConfirmDialog = ({
|
|||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-2 text-2xs text-ink-2">
|
<p className="mt-2 text-cell 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-sans">
|
<h4 className="text-body font-sans">
|
||||||
{t('cascade.cascade.title', { count: plan.cascade.length })}
|
{t('cascade.cascade.title', { count: plan.cascade.length })}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,7 +176,7 @@ export const CascadeConfirmDialog = ({
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={key}
|
key={key}
|
||||||
className="border border-line rounded-sm px-3 py-2 text-2xs"
|
className="border border-line rounded-sm px-3 py-2 text-cell"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<span className="font-mono text-accent">
|
<span className="font-mono text-accent">
|
||||||
@@ -190,7 +190,7 @@ export const CascadeConfirmDialog = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="neutral">{entries.length}</Badge>
|
<Badge variant="neutral">{entries.length}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<ul className="text-2xs text-ink-2 ml-1 space-y-0.5">
|
<ul className="text-cell 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}
|
||||||
@@ -215,7 +215,7 @@ export const CascadeConfirmDialog = ({
|
|||||||
variant="warning"
|
variant="warning"
|
||||||
title={t('cascade.warnings.title', { count: plan.warnings.length })}
|
title={t('cascade.warnings.title', { count: plan.warnings.length })}
|
||||||
>
|
>
|
||||||
<p className="text-2xs">{t('cascade.warnings.note')}</p>
|
<p className="text-cell">{t('cascade.warnings.note')}</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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-sans text-ink">
|
<h3 className="text-body font-sans text-ink">
|
||||||
{t('lineage.usedBy.title')}
|
{t('lineage.usedBy.title')}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-2xs text-mute">
|
<span className="text-cell text-mute">
|
||||||
{t('lineage.usedBy.count', { count: data.length })}
|
{t('lineage.usedBy.count', { count: data.length })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +48,7 @@ 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-line bg-white text-2xs"
|
className="flex items-center gap-2 px-2.5 py-1.5 rounded-sm border border-line bg-white text-cell"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to="/dictionaries/$name"
|
to="/dictionaries/$name"
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const SectionLabel = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`text-2xs uppercase tracking-[0.10em] text-mute mb-2 ${
|
className={`text-cap 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-mute/70 text-right italic">
|
<div className="text-cell text-mute/70 text-right italic">
|
||||||
{t('hub.noOutgoing', { defaultValue: 'нет исходящих ссылок' })}
|
{t('hub.noOutgoing', { defaultValue: 'нет исходящих ссылок' })}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -166,14 +166,14 @@ 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-mute">
|
<span className="text-mono ml-auto text-mute">
|
||||||
v{detail.schemaVersion}
|
v{detail.schemaVersion}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-sans text-[22px] font-semibold text-accent leading-tight">
|
<h2 className="text-title-xl font-sans text-accent leading-tight">
|
||||||
{detail.displayName ?? detail.name}
|
{detail.displayName ?? detail.name}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="font-mono text-[11px] text-mute mt-0.5">
|
<div className="text-mono text-mute mt-0.5">
|
||||||
{detail.name}
|
{detail.name}
|
||||||
</div>
|
</div>
|
||||||
{detail.description && (
|
{detail.description && (
|
||||||
@@ -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-mute/70 italic">
|
<div className="text-cell 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-sans text-ink">
|
<h3 className="text-body 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-mute px-2 py-0.5 rounded-sm border border-line"
|
className="text-cell 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-mute">
|
<span className="text-cell text-mute">
|
||||||
{t('lineage.refs.total', { count: data.total })}
|
{t('lineage.refs.total', { count: data.total })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +91,7 @@ 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-line bg-line/30 text-2xs"
|
className="flex items-center gap-1.5 px-2 py-1 rounded-sm border border-line bg-line/30 text-cell"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to="/dictionaries/$name"
|
to="/dictionaries/$name"
|
||||||
@@ -115,7 +115,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
|
|||||||
{data.items.map((r) => (
|
{data.items.map((r) => (
|
||||||
<li
|
<li
|
||||||
key={r.id}
|
key={r.id}
|
||||||
className="flex items-center justify-between gap-3 px-3 py-2 text-2xs"
|
className="flex items-center justify-between gap-3 px-3 py-2 text-cell"
|
||||||
>
|
>
|
||||||
<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">
|
||||||
@@ -152,7 +152,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
|
|||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="flex items-center justify-between text-2xs">
|
<div className="flex items-center justify-between text-cell">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{data && data.length === 0 && (
|
{data && data.length === 0 && (
|
||||||
<p className="text-sm text-mute">{t('history.empty')}</p>
|
<p className="text-body text-mute">{t('history.empty')}</p>
|
||||||
)}
|
)}
|
||||||
{data && data.length > 0 && (
|
{data && data.length > 0 && (
|
||||||
<ol className="relative border-l-2 border-line pl-5 space-y-4">
|
<ol className="relative border-l-2 border-line pl-5 space-y-4">
|
||||||
@@ -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-sans text-accent">v{displayVersion}</span>
|
<span className="text-body 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-ink-2 space-y-0.5">
|
<div className="text-cell 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-accent cursor-pointer hover:underline">
|
<summary className="text-cell text-accent cursor-pointer hover:underline">
|
||||||
{t('history.viewData')}
|
{t('history.viewData')}
|
||||||
</summary>
|
</summary>
|
||||||
<pre className="mt-1 bg-line/30 rounded p-2 text-2xs font-mono overflow-x-auto">
|
<pre className="text-mono mt-1 bg-line/30 rounded p-2 overflow-x-auto">
|
||||||
{JSON.stringify(rec.data, null, 2)}
|
{JSON.stringify(rec.data, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -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-sans text-ink">
|
<span className="block text-body font-sans text-ink">
|
||||||
{t('schema.redisProjection.label')}
|
{t('schema.redisProjection.label')}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-2xs text-mute mt-0.5">
|
<span className="block text-cell text-mute mt-0.5">
|
||||||
{t('schema.redisProjection.hint')}
|
{t('schema.redisProjection.hint')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -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-sans text-ink">
|
<span className="block text-body font-sans text-ink">
|
||||||
{t('schema.approvalRequired.label')}
|
{t('schema.approvalRequired.label')}
|
||||||
</span>
|
</span>
|
||||||
<span className="block text-2xs text-mute mt-0.5">
|
<span className="block text-cell text-mute mt-0.5">
|
||||||
{t('schema.approvalRequired.hint')}
|
{t('schema.approvalRequired.hint')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -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-mute mt-0.5">
|
<p className="text-cell 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-line/30 rounded p-3 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
|
<pre className="text-mono bg-line/30 rounded p-3 overflow-x-auto whitespace-pre-wrap">
|
||||||
{JSON.stringify(schemaJson, null, 2)}
|
{JSON.stringify(schemaJson, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,8 +384,8 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
|
|||||||
{isEdit && breaking && (
|
{isEdit && breaking && (
|
||||||
<Alert variant="error" title={t('schema.diff.blockedTitle')}>
|
<Alert variant="error" title={t('schema.diff.blockedTitle')}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm">{t('schema.diff.blockedBody')}</p>
|
<p className="text-body">{t('schema.diff.blockedBody')}</p>
|
||||||
<ul className="list-disc pl-5 text-sm space-y-0.5">
|
<ul className="list-disc pl-5 text-body space-y-0.5">
|
||||||
{changes
|
{changes
|
||||||
.filter((c) => c.severity === 'breaking')
|
.filter((c) => c.severity === 'breaking')
|
||||||
.map((c, i) => (
|
.map((c, i) => (
|
||||||
@@ -400,7 +400,7 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
|
|||||||
|
|
||||||
{isEdit && !breaking && changes.length > 0 && (
|
{isEdit && !breaking && changes.length > 0 && (
|
||||||
<Alert variant="info" title={t('schema.diff.compatibleTitle')}>
|
<Alert variant="info" title={t('schema.diff.compatibleTitle')}>
|
||||||
<ul className="list-disc pl-5 text-sm space-y-0.5">
|
<ul className="list-disc pl-5 text-body space-y-0.5">
|
||||||
{changes.map((c, i) => (
|
{changes.map((c, i) => (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<span className="font-mono">{c.field}</span> — {c.reason}
|
<span className="font-mono">{c.field}</span> — {c.reason}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const EventsPreviewTab = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm text-ink-2">
|
<p className="text-body 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-ink-2">
|
<span className="text-cell text-ink-2">
|
||||||
{t('schema.events.topic')}:
|
{t('schema.events.topic')}:
|
||||||
</span>
|
</span>
|
||||||
<code className="text-2xs font-mono text-accent">
|
<code className="text-mono text-accent">
|
||||||
{current.topic}
|
{current.topic}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xs text-mute">{current.description}</p>
|
<p className="text-cell text-mute">{current.description}</p>
|
||||||
<pre className="bg-line/30 rounded p-3 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
|
<pre className="text-mono bg-line/30 rounded p-3 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-mute">
|
<p className="text-cell text-mute">
|
||||||
{t('schema.events.disclaimer')}
|
{t('schema.events.disclaimer')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,14 +118,14 @@ 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-cell space-y-2">
|
||||||
<div className="font-medium text-ink">
|
<div className="font-medium text-ink">
|
||||||
{t('schema.opaque.title')}
|
{t('schema.opaque.title')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ink-2">
|
<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-line rounded p-2 overflow-auto max-h-40">
|
<pre className="text-mono 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>
|
||||||
@@ -285,7 +285,7 @@ const ReferenceTargetPicker = ({ value, onChange }: ReferenceTargetPickerProps)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isValid && (
|
{!isValid && (
|
||||||
<div className="text-2xs text-mars">{t('schema.reference.required')}</div>
|
<div className="text-cell text-mars">{t('schema.reference.required')}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function TimeTravelPicker({
|
|||||||
}, [marks, valueDate])
|
}, [marks, valueDate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 px-4 py-3 rounded-md border border-warn bg-warn-bg text-sm">
|
<div className="flex flex-col gap-3 px-4 py-3 rounded-md border border-warn bg-warn-bg text-body">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn" />
|
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn" />
|
||||||
<span className="text-cap font-display text-ink-2">
|
<span className="text-cap font-display text-ink-2">
|
||||||
@@ -58,7 +58,7 @@ export function TimeTravelPicker({
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto text-2xs text-mute hover:text-ink hover:underline inline-flex items-center gap-1"
|
className="ml-auto text-cell 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} />
|
||||||
@@ -306,7 +306,7 @@ function TimelineTrack({
|
|||||||
|
|
||||||
{tooltipText && hover !== null && (
|
{tooltipText && hover !== null && (
|
||||||
<div
|
<div
|
||||||
className="absolute -top-2 -translate-y-full px-2 py-0.5 rounded bg-carbon text-white text-2xs font-mono pointer-events-none whitespace-nowrap"
|
className="text-mono absolute -top-2 -translate-y-full px-2 py-0.5 rounded bg-carbon text-white pointer-events-none whitespace-nowrap"
|
||||||
style={{ left: timeToX(hover), transform: 'translate(-50%, -100%)' }}
|
style={{ left: timeToX(hover), transform: 'translate(-50%, -100%)' }}
|
||||||
>
|
>
|
||||||
{tooltipText}
|
{tooltipText}
|
||||||
@@ -314,7 +314,7 @@ function TimelineTrack({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{marks.length > 0 && (
|
{marks.length > 0 && (
|
||||||
<div className="text-2xs text-mute font-display mt-1">
|
<div className="text-cell text-mute font-display mt-1">
|
||||||
{marks.length === 1
|
{marks.length === 1
|
||||||
? '1 точка изменений'
|
? '1 точка изменений'
|
||||||
: marks.length < 5
|
: marks.length < 5
|
||||||
@@ -386,7 +386,7 @@ function StatusFooter({
|
|||||||
|
|
||||||
if (!valueDate) {
|
if (!valueDate) {
|
||||||
return (
|
return (
|
||||||
<div className="text-2xs text-mute font-display italic">
|
<div className="text-cell text-mute font-display italic">
|
||||||
{t('timeTravel.hint')}
|
{t('timeTravel.hint')}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -395,14 +395,14 @@ function StatusFooter({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-warn/30">
|
<div className="flex flex-wrap items-center gap-2 pt-2 border-t border-warn/30">
|
||||||
<ClockCounterClockwiseIcon weight="regular" size={14} className="text-warn shrink-0" />
|
<ClockCounterClockwiseIcon weight="regular" size={14} className="text-warn shrink-0" />
|
||||||
<span className="text-xs font-mono text-ink">
|
<span className="text-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-mute font-display lowercase">
|
<span className="text-cell 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-line bg-white text-2xs font-mono text-center"
|
className="text-mono w-10 px-1.5 py-0.5 rounded-sm border border-line bg-white text-center"
|
||||||
aria-label={t('timeTravel.hours')}
|
aria-label={t('timeTravel.hours')}
|
||||||
/>
|
/>
|
||||||
<span className="text-mute font-mono text-2xs">:</span>
|
<span className="text-mono text-mute">:</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-line bg-white text-2xs font-mono text-center"
|
className="text-mono w-10 px-1.5 py-0.5 rounded-sm border border-line bg-white 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-accent hover:underline"
|
className="ml-auto text-cell text-accent hover:underline"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
{t('timeTravel.clear')}
|
{t('timeTravel.clear')}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function UpdateBanner() {
|
|||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
className="bg-accent text-on-accent"
|
className="bg-accent text-on-accent"
|
||||||
>
|
>
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-2 flex items-center gap-3 flex-wrap text-sm">
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-2 flex items-center gap-3 flex-wrap text-body">
|
||||||
<ArrowClockwise size={18} weight="bold" className="shrink-0" aria-hidden />
|
<ArrowClockwise size={18} weight="bold" className="shrink-0" aria-hidden />
|
||||||
<span className="flex-1 min-w-0">{message}</span>
|
<span className="flex-1 min-w-0">{message}</span>
|
||||||
<div className="flex items-center gap-2 ml-auto">
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function VersionBadge() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center gap-1.5 text-xs text-mute font-mono select-none"
|
className="inline-flex items-center gap-1.5 text-mono text-mute select-none"
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
>
|
>
|
||||||
{updateAvailable && (
|
{updateAvailable && (
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ function ActiveFilterChips({ filters, onRemove }: ActiveFilterChipsProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
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-cell',
|
||||||
'bg-white border-line hover:border-carbon/40 hover:bg-line/30',
|
'bg-white border-line hover:border-carbon/40 hover:bg-line/30',
|
||||||
'transition-colors group',
|
'transition-colors group',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
@@ -469,7 +469,7 @@ function AuditRow({ row }: { row: AuditEntry }) {
|
|||||||
<>
|
<>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-2xs tabular-nums">{timeLabel}</span>
|
<span className="text-cell tabular-nums">{timeLabel}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={actionVariant}>
|
<Badge variant={actionVariant}>
|
||||||
@@ -478,7 +478,7 @@ function AuditRow({ row }: { row: AuditEntry }) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.dictionaryName ?? '—'}</TableCell>
|
<TableCell>{row.dictionaryName ?? '—'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-xs">{row.businessKey ?? '—'}</span>
|
<span className=" text-mono">{row.businessKey ?? '—'}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.userId ?? 'anonymous'}</TableCell>
|
<TableCell>{row.userId ?? 'anonymous'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -486,7 +486,7 @@ function AuditRow({ row }: { row: AuditEntry }) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span
|
<span
|
||||||
className="font-mono text-2xs text-ink-2"
|
className="text-mono text-ink-2"
|
||||||
title={row.traceId ?? ''}
|
title={row.traceId ?? ''}
|
||||||
>
|
>
|
||||||
{row.traceId ? row.traceId.slice(0, 8) : '—'}
|
{row.traceId ? row.traceId.slice(0, 8) : '—'}
|
||||||
@@ -509,7 +509,7 @@ function AuditRow({ row }: { row: AuditEntry }) {
|
|||||||
<div className="text-cap text-ink-2 mb-1">
|
<div className="text-cap 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-line rounded p-2 max-h-60 overflow-auto">
|
<pre className="text-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)
|
||||||
: '—'}
|
: '—'}
|
||||||
@@ -519,14 +519,14 @@ function AuditRow({ row }: { row: AuditEntry }) {
|
|||||||
<div className="text-cap text-ink-2 mb-1">
|
<div className="text-cap 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-line rounded p-2 max-h-60 overflow-auto">
|
<pre className="text-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-ink-2 flex flex-wrap gap-x-4 gap-y-1 pt-1">
|
<div className="md:col-span-2 text-cell 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 && (
|
||||||
@@ -562,7 +562,7 @@ function Pagination({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
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-body">
|
||||||
<span className="text-ink-2">{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}
|
||||||
@@ -570,7 +570,7 @@ function Pagination({
|
|||||||
onChange={(id) => onSizeChange(Number(id))}
|
onChange={(id) => onSizeChange(Number(id))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-sm">
|
<div className="flex items-center gap-3 text-body">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -506,7 +506,7 @@ function DictionaryDetail() {
|
|||||||
)}
|
)}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
breadcrumb={
|
breadcrumb={
|
||||||
<div className="flex items-center gap-3 text-sm">
|
<div className="flex items-center gap-3 text-body">
|
||||||
<Link to="/dictionaries" className="text-ink-2 hover:text-accent">
|
<Link to="/dictionaries" className="text-ink-2 hover:text-accent">
|
||||||
← {t('nav.dictionaries')}
|
← {t('nav.dictionaries')}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -630,8 +630,8 @@ function DictionaryDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{aoi && (
|
{aoi && (
|
||||||
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-accent/30 bg-accent/4 text-sm">
|
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-accent/30 bg-accent/4 text-body">
|
||||||
<span className="font-mono text-xs text-ink">
|
<span className=" text-mono 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', {
|
||||||
@@ -640,7 +640,7 @@ function DictionaryDetail() {
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs text-accent hover:underline"
|
className="text-cell text-accent hover:underline"
|
||||||
onClick={handleAoiClear}
|
onClick={handleAoiClear}
|
||||||
>
|
>
|
||||||
{t('aoi.clear')}
|
{t('aoi.clear')}
|
||||||
@@ -667,16 +667,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-warn bg-warn-bg text-sm">
|
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-warn bg-warn-bg text-body">
|
||||||
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn shrink-0" />
|
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn shrink-0" />
|
||||||
<span className="font-mono text-xs text-ink">
|
<span className=" text-mono 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-accent hover:underline ml-auto"
|
className="text-cell text-accent hover:underline ml-auto"
|
||||||
onClick={() => setTimeTravelAt(undefined)}
|
onClick={() => setTimeTravelAt(undefined)}
|
||||||
>
|
>
|
||||||
{t('timeTravel.clear')}
|
{t('timeTravel.clear')}
|
||||||
@@ -717,7 +717,7 @@ 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-[0.10em] font-display transition-colors',
|
'text-cap inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border transition-colors',
|
||||||
active
|
active
|
||||||
? 'border-accent bg-accent/8 text-accent'
|
? 'border-accent bg-accent/8 text-accent'
|
||||||
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
|
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
|
||||||
@@ -733,7 +733,7 @@ function DictionaryDetail() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{filtersActive && (
|
{filtersActive && (
|
||||||
<div className="flex items-center gap-2 text-xs text-ink-2">
|
<div className="flex items-center gap-2 text-cell text-ink-2">
|
||||||
<span>
|
<span>
|
||||||
{t('dict.filter.matched', { count: filteredCount, total: totalRecords })}
|
{t('dict.filter.matched', { count: filteredCount, total: totalRecords })}
|
||||||
</span>
|
</span>
|
||||||
@@ -800,7 +800,7 @@ function DictionaryDetail() {
|
|||||||
)}
|
)}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="font-mono text-2xs">{r.businessKey}</span>
|
<span className="text-mono">{r.businessKey}</span>
|
||||||
{pendingByKey.has(r.businessKey) ? (
|
{pendingByKey.has(r.businessKey) ? (
|
||||||
<span title={t('dict.pendingReview.tooltip')}>
|
<span title={t('dict.pendingReview.tooltip')}>
|
||||||
<Badge variant="warning">{t('dict.pendingReview.label')}</Badge>
|
<Badge variant="warning">{t('dict.pendingReview.label')}</Badge>
|
||||||
@@ -819,7 +819,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-mute/70">—</span>
|
<span className="text-cell text-mute/70">—</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -828,7 +828,7 @@ function DictionaryDetail() {
|
|||||||
<TableCell key={c.key}>
|
<TableCell key={c.key}>
|
||||||
<span
|
<span
|
||||||
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-cell',
|
||||||
c.isFk
|
c.isFk
|
||||||
? 'bg-accent/8 text-accent'
|
? 'bg-accent/8 text-accent'
|
||||||
: 'bg-carbon/8 text-ink',
|
: 'bg-carbon/8 text-ink',
|
||||||
@@ -843,7 +843,7 @@ function DictionaryDetail() {
|
|||||||
<Badge variant="info">{r.dataScope}</Badge>
|
<Badge variant="info">{r.dataScope}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-2xs text-mute">
|
<span className="text-cell text-mute">
|
||||||
{new Date(r.validFrom).toLocaleDateString()}
|
{new Date(r.validFrom).toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -970,7 +970,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-ink">
|
<p className="text-body text-ink">
|
||||||
{t('dict.confirmClose.body', { key: edit.record.businessKey })}
|
{t('dict.confirmClose.body', { key: edit.record.businessKey })}
|
||||||
</p>
|
</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -1093,13 +1093,13 @@ function BulkSelectionToolbar({
|
|||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm font-medium tabular-nums">
|
<span className="text-body font-medium tabular-nums">
|
||||||
{t('dict.bulk.selectedCount', { count, total: totalVisible })}
|
{t('dict.bulk.selectedCount', { count, total: totalVisible })}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
className="inline-flex items-center gap-1 text-2xs underline-offset-2 hover:underline opacity-80 hover:opacity-100"
|
className="inline-flex items-center gap-1 text-cell underline-offset-2 hover:underline opacity-80 hover:opacity-100"
|
||||||
>
|
>
|
||||||
<XIcon size={12} weight="bold" />
|
<XIcon size={12} weight="bold" />
|
||||||
{t('dict.bulk.clearSelection')}
|
{t('dict.bulk.clearSelection')}
|
||||||
@@ -1150,10 +1150,10 @@ 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-ink-2 tabular-nums">
|
<span className="text-cell 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-body">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -1163,7 +1163,7 @@ function RecordsPagination({
|
|||||||
>
|
>
|
||||||
{t('dict.page.prev')}
|
{t('dict.page.prev')}
|
||||||
</Button>
|
</Button>
|
||||||
<span className="tabular-nums text-2xs text-ink-2">
|
<span className="tabular-nums text-cell 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
|
||||||
@@ -1209,19 +1209,19 @@ function BulkCloseDialog({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{result.closed.length > 0 && (
|
{result.closed.length > 0 && (
|
||||||
<div className="flex items-center gap-2 text-sm text-soyuz-green">
|
<div className="flex items-center gap-2 text-body text-soyuz-green">
|
||||||
<CheckCircleIcon size={18} weight="fill" />
|
<CheckCircleIcon size={18} weight="fill" />
|
||||||
<span>{t('dict.bulk.resultClosed', { count: result.closed.length })}</span>
|
<span>{t('dict.bulk.resultClosed', { count: result.closed.length })}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{result.skipped.length > 0 && (
|
{result.skipped.length > 0 && (
|
||||||
<div className="flex items-center gap-2 text-sm text-saturn">
|
<div className="flex items-center gap-2 text-body text-saturn">
|
||||||
<WarningIcon size={18} weight="fill" />
|
<WarningIcon size={18} weight="fill" />
|
||||||
<span>{t('dict.bulk.resultSkipped', { count: result.skipped.length })}</span>
|
<span>{t('dict.bulk.resultSkipped', { count: result.skipped.length })}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{result.errors.length > 0 && (
|
{result.errors.length > 0 && (
|
||||||
<div className="flex items-center gap-2 text-sm text-mars">
|
<div className="flex items-center gap-2 text-body text-mars">
|
||||||
<XCircleIcon size={18} weight="fill" />
|
<XCircleIcon size={18} weight="fill" />
|
||||||
<span>{t('dict.bulk.resultErrors', { count: result.errors.length })}</span>
|
<span>{t('dict.bulk.resultErrors', { count: result.errors.length })}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1230,7 +1230,7 @@ function BulkCloseDialog({
|
|||||||
|
|
||||||
{(result.skipped.length > 0 || result.errors.length > 0) && (
|
{(result.skipped.length > 0 || result.errors.length > 0) && (
|
||||||
<div className="border border-line rounded p-3 max-h-48 overflow-auto bg-line/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-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">
|
||||||
· {s.businessKey} — {s.reason}
|
· {s.businessKey} — {s.reason}
|
||||||
@@ -1256,7 +1256,7 @@ function BulkCloseDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-ink">{t('dict.bulk.confirmBody', { count })}</p>
|
<p className="text-body 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')}
|
||||||
@@ -1361,7 +1361,7 @@ function EditorTabBar({
|
|||||||
onClick={() => onChange(tab.id)}
|
onClick={() => onChange(tab.id)}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
className={[
|
className={[
|
||||||
'px-3.5 h-10 text-sm transition-colors -mb-px border-b-2',
|
'px-3.5 h-10 text-body transition-colors -mb-px border-b-2',
|
||||||
isActive
|
isActive
|
||||||
? 'border-accent text-ink font-semibold'
|
? 'border-accent text-ink font-semibold'
|
||||||
: 'border-transparent text-ink-2 hover:text-ink',
|
: 'border-transparent text-ink-2 hover:text-ink',
|
||||||
@@ -1382,14 +1382,14 @@ function FieldsTabContent({ detail }: { detail: { schemaJson: import('@/api/clie
|
|||||||
const entries = Object.entries(props)
|
const entries = Object.entries(props)
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-mute py-8 text-center">
|
<div className="text-body text-mute py-8 text-center">
|
||||||
Schema без properties.
|
Schema без properties.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-line overflow-hidden">
|
<div className="rounded-lg border border-line overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-body">
|
||||||
<thead className="text-cap bg-surface-2 font-display text-mute">
|
<thead className="text-cap bg-surface-2 font-display text-mute">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left px-3 py-2">name</th>
|
<th className="text-left px-3 py-2">name</th>
|
||||||
@@ -1404,15 +1404,15 @@ function FieldsTabContent({ detail }: { detail: { schemaJson: import('@/api/clie
|
|||||||
<tbody>
|
<tbody>
|
||||||
{entries.map(([key, prop]) => (
|
{entries.map(([key, prop]) => (
|
||||||
<tr key={key} className="border-t border-line-2 hover:bg-surface-2/50">
|
<tr key={key} className="border-t border-line-2 hover:bg-surface-2/50">
|
||||||
<td className="px-3 py-1.5 font-mono text-[12.5px]">{key}</td>
|
<td className="text-cell px-3 py-1.5 font-mono">{key}</td>
|
||||||
<td className="px-3 py-1.5 text-ink-2">{prop.type ?? '—'}</td>
|
<td className="px-3 py-1.5 text-ink-2">{prop.type ?? '—'}</td>
|
||||||
<td className="px-3 py-1.5">{required.has(key) ? '✓' : ''}</td>
|
<td className="px-3 py-1.5">{required.has(key) ? '✓' : ''}</td>
|
||||||
<td className="px-3 py-1.5">{prop['x-unique'] ? '✓' : ''}</td>
|
<td className="px-3 py-1.5">{prop['x-unique'] ? '✓' : ''}</td>
|
||||||
<td className="px-3 py-1.5">{prop['x-localized'] ? '✓' : ''}</td>
|
<td className="px-3 py-1.5">{prop['x-localized'] ? '✓' : ''}</td>
|
||||||
<td className="px-3 py-1.5 font-mono text-[11px] text-accent">
|
<td className="text-mono px-3 py-1.5 text-accent">
|
||||||
{prop['x-references'] ?? ''}
|
{prop['x-references'] ?? ''}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-1.5 text-ink-2 text-xs max-w-[300px] truncate">
|
<td className="px-3 py-1.5 text-ink-2 text-cell max-w-[300px] truncate">
|
||||||
{prop.description ?? ''}
|
{prop.description ?? ''}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1437,13 +1437,13 @@ function JsonTabContent({ detail }: { detail: { schemaJson: import('@/api/client
|
|||||||
function EventsTabContent({ dictName }: { dictName: string }) {
|
function EventsTabContent({ dictName }: { dictName: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-line bg-surface p-6 text-center space-y-3">
|
<div className="rounded-lg border border-line bg-surface p-6 text-center space-y-3">
|
||||||
<p className="text-sm text-ink-2">
|
<p className="text-body text-ink-2">
|
||||||
События для справочника <span className="font-mono">{dictName}</span> — в общем аудит-логе.
|
События для справочника <span className="font-mono">{dictName}</span> — в общем аудит-логе.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/audit"
|
to="/audit"
|
||||||
search={{ dict: dictName }}
|
search={{ dict: dictName }}
|
||||||
className="inline-flex items-center gap-1 text-accent hover:underline text-sm"
|
className="inline-flex items-center gap-1 text-accent hover:underline text-body"
|
||||||
>
|
>
|
||||||
Открыть /audit с фильтром →
|
Открыть /audit с фильтром →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -1454,7 +1454,7 @@ function EventsTabContent({ dictName }: { dictName: string }) {
|
|||||||
/** History tab — placeholder (Stage 3.x followup, требует backend changelog endpoint). */
|
/** History tab — placeholder (Stage 3.x followup, требует backend changelog endpoint). */
|
||||||
function HistoryTabContent() {
|
function HistoryTabContent() {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-dashed border-line bg-surface-2/30 p-8 text-center text-sm text-mute">
|
<div className="rounded-lg border border-dashed border-line bg-surface-2/30 p-8 text-center text-body text-mute">
|
||||||
Timeline изменений справочника — скоро. Требует backend endpoint для schema versions.
|
Timeline изменений справочника — скоро. Требует backend endpoint для schema versions.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ 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-[0.10em] flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 font-mono ${
|
className={`text-cap px-2 py-1 rounded-full flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 font-mono ${
|
||||||
selected
|
selected
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
? 'border-accent bg-accent-bg text-accent'
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
: 'border-line hover:border-accent/60 text-ink'
|
||||||
@@ -303,7 +303,7 @@ 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-[0.10em] border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
className={`text-cap px-2 py-1 rounded-sm border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||||
!bundleFilter
|
!bundleFilter
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
? 'border-accent bg-accent-bg text-accent'
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
: 'border-line hover:border-accent/60 text-ink'
|
||||||
@@ -321,7 +321,7 @@ 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-[0.10em] flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 font-mono ${
|
className={`text-cap px-2 py-1 rounded-sm flex items-center gap-1 border transition focus:outline-none focus:ring-2 focus:ring-accent/40 font-mono ${
|
||||||
selected
|
selected
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
? 'border-accent bg-accent-bg text-accent'
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
: 'border-line hover:border-accent/60 text-ink'
|
||||||
@@ -342,7 +342,7 @@ function DictionariesPage() {
|
|||||||
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-[0.10em] border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
className={`text-cap px-2 py-1 rounded-sm border transition focus:outline-none focus:ring-2 focus:ring-accent/40 ${
|
||||||
withDepsOnly
|
withDepsOnly
|
||||||
? 'border-accent bg-accent-bg text-accent'
|
? 'border-accent bg-accent-bg text-accent'
|
||||||
: 'border-line hover:border-accent/60 text-ink'
|
: 'border-line hover:border-accent/60 text-ink'
|
||||||
@@ -355,7 +355,7 @@ function DictionariesPage() {
|
|||||||
{/* Empty state */}
|
{/* Empty state */}
|
||||||
{filtered.length === 0 ? (
|
{filtered.length === 0 ? (
|
||||||
<div className="border border-line rounded-lg p-12 text-center bg-surface-2">
|
<div className="border border-line rounded-lg p-12 text-center bg-surface-2">
|
||||||
<p className="font-sans text-lg text-accent mb-2">
|
<p className="font-sans text-title-md text-accent mb-2">
|
||||||
{t('dict.list.search.empty')}
|
{t('dict.list.search.empty')}
|
||||||
</p>
|
</p>
|
||||||
{filtersActive && (
|
{filtersActive && (
|
||||||
@@ -369,7 +369,7 @@ function DictionariesPage() {
|
|||||||
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-line">
|
<h2 className="flex items-baseline gap-3 pb-2 border-b border-line">
|
||||||
<span className="text-sm uppercase tracking-[0.10em] text-accent font-mono font-medium">
|
<span className="text-body uppercase tracking-[0.10em] text-accent font-mono font-medium">
|
||||||
{bundle}
|
{bundle}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-cap text-mute">
|
<span className="text-cap text-mute">
|
||||||
@@ -467,10 +467,10 @@ const DictCard = ({ d, t }: { d: DictionaryDefinition; t: TFunc }) => {
|
|||||||
{/* 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="text-[15px] font-semibold text-navy truncate">
|
<h3 className="text-title-md text-navy truncate">
|
||||||
{d.displayName ?? d.name}
|
{d.displayName ?? d.name}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="font-mono text-[11px] text-mute truncate">{d.name}</span>
|
<span className="text-mono 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>
|
||||||
)}
|
)}
|
||||||
@@ -494,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-mute">
|
<span className="text-mono text-mute">
|
||||||
+{refBy.length - MAX_REFBY_CHIPS}
|
+{refBy.length - MAX_REFBY_CHIPS}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -505,7 +505,7 @@ 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-mute whitespace-nowrap">
|
<span className="text-mono text-mute whitespace-nowrap">
|
||||||
v{d.schemaVersion}
|
v{d.schemaVersion}
|
||||||
</span>
|
</span>
|
||||||
{typeof d.recordCount === 'number' && (
|
{typeof d.recordCount === 'number' && (
|
||||||
|
|||||||
@@ -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-mute">
|
<p className="text-cell text-mute">
|
||||||
{t('myDrafts.total', { count: drafts.data.totalElements })}
|
{t('myDrafts.total', { count: drafts.data.totalElements })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,25 +102,25 @@ function MyDraftsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{drafts.data.items.map((d) => (
|
{drafts.data.items.map((d) => (
|
||||||
<TableRow key={d.id}>
|
<TableRow key={d.id}>
|
||||||
<TableCell className="font-mono text-2xs">{d.businessKey}</TableCell>
|
<TableCell className="text-mono">{d.businessKey}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
|
<Badge variant={operationVariant(d.operation)}>{d.operation}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<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-ink-2">
|
<TableCell className="text-cell text-ink-2">
|
||||||
{new Date(d.submittedAt).toLocaleString()}
|
{new Date(d.submittedAt).toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-2xs text-ink-2">
|
<TableCell className="text-cell text-ink-2">
|
||||||
{d.reviewedAt
|
{d.reviewedAt
|
||||||
? new Date(d.reviewedAt).toLocaleString()
|
? new Date(d.reviewedAt).toLocaleString()
|
||||||
: '—'}
|
: '—'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-2xs">
|
<TableCell className="text-mono">
|
||||||
{d.reviewerId ?? '—'}
|
{d.reviewerId ?? '—'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-2xs text-ink max-w-md">
|
<TableCell className="text-cell 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-mute/70">—</span>
|
<span className="text-cell text-mute/70">—</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -90,29 +90,29 @@ function OutboxPage() {
|
|||||||
{dlq.data.content.map((row) => (
|
{dlq.data.content.map((row) => (
|
||||||
<TableRow key={row.id}>
|
<TableRow key={row.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-2xs">{row.id}</span>
|
<span className="text-mono">{row.id}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="warning">{row.eventType}</Badge>
|
<Badge variant="warning">{row.eventType}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.dictionaryName ?? '—'}</TableCell>
|
<TableCell>{row.dictionaryName ?? '—'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-2xs">{row.aggregateId}</span>
|
<span className="text-mono">{row.aggregateId}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-2xs">{row.kafkaTopic}</span>
|
<span className="text-mono">{row.kafkaTopic}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{row.attemptCount}</TableCell>
|
<TableCell>{row.attemptCount}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span
|
<span
|
||||||
className="text-2xs text-ink-2 truncate max-w-md inline-block align-middle"
|
className="text-cell text-ink-2 truncate max-w-md inline-block align-middle"
|
||||||
title={row.lastError ?? ''}
|
title={row.lastError ?? ''}
|
||||||
>
|
>
|
||||||
{row.lastError ?? '—'}
|
{row.lastError ?? '—'}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-2xs tabular-nums">
|
<span className="text-cell tabular-nums">
|
||||||
{new Date(row.dlqAt).toLocaleString()}
|
{new Date(row.dlqAt).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -120,7 +120,7 @@ function OutboxPage() {
|
|||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<div className="flex items-center justify-end gap-3 text-sm">
|
<div className="flex items-center justify-end gap-3 text-body">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ function ReviewsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ul className="space-y-1 text-2xs">
|
<ul className="space-y-1 text-cell">
|
||||||
{bulkResult.approved.length > 0 && (
|
{bulkResult.approved.length > 0 && (
|
||||||
<li>
|
<li>
|
||||||
{t('reviews.bulk.approvedCount', { count: bulkResult.approved.length })}
|
{t('reviews.bulk.approvedCount', { count: bulkResult.approved.length })}
|
||||||
@@ -196,7 +196,7 @@ 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-mute">
|
<p className="text-cell text-mute">
|
||||||
{t('reviews.queueTotal', { count: queue.data.totalElements })}
|
{t('reviews.queueTotal', { count: queue.data.totalElements })}
|
||||||
{anySelected ? (
|
{anySelected ? (
|
||||||
<span className="ml-2 text-accent font-medium">
|
<span className="ml-2 text-accent font-medium">
|
||||||
@@ -264,17 +264,17 @@ function ReviewsPage() {
|
|||||||
<Link
|
<Link
|
||||||
to="/dictionaries/$name"
|
to="/dictionaries/$name"
|
||||||
params={{ name: d.dictionaryId }}
|
params={{ name: d.dictionaryId }}
|
||||||
className="text-accent hover:underline font-mono text-2xs"
|
className="text-mono text-accent hover:underline"
|
||||||
>
|
>
|
||||||
{d.dictionaryId.slice(0, 8)}…
|
{d.dictionaryId.slice(0, 8)}…
|
||||||
</Link>
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-2xs">{d.businessKey}</TableCell>
|
<TableCell className="text-mono">{d.businessKey}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<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="text-mono">{d.makerId}</TableCell>
|
||||||
<TableCell className="text-2xs text-ink-2">
|
<TableCell className="text-cell 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-ink-2">
|
<p className="text-cell text-ink-2">
|
||||||
{t('reviews.bulk.rejectModalDescription')}
|
{t('reviews.bulk.rejectModalDescription')}
|
||||||
</p>
|
</p>
|
||||||
<TextArea
|
<TextArea
|
||||||
@@ -400,7 +400,7 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
|||||||
|
|
||||||
{draft && (
|
{draft && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-2xs">
|
<div className="flex flex-wrap items-center gap-2 text-cell">
|
||||||
<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-mute">
|
<span className="text-mute">
|
||||||
@@ -413,7 +413,7 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{draft.makerComment && (
|
{draft.makerComment && (
|
||||||
<div className="px-3 py-2 rounded-sm border border-line bg-line/30 text-2xs">
|
<div className="px-3 py-2 rounded-sm border border-line bg-line/30 text-cell">
|
||||||
<span className="text-mute uppercase tracking-[0.10em] font-display">
|
<span className="text-mute uppercase tracking-[0.10em] font-display">
|
||||||
{t('reviews.drawer.makerComment')}:
|
{t('reviews.drawer.makerComment')}:
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
@@ -428,12 +428,12 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
|||||||
</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-mute italic">
|
<p className="text-cell text-mute italic">
|
||||||
{t('reviews.drawer.noLive')}
|
{t('reviews.drawer.noLive')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{liveQ.data && (
|
{liveQ.data && (
|
||||||
<pre className="bg-line/30 rounded p-2 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
|
<pre className="text-mono bg-line/30 rounded p-2 overflow-x-auto whitespace-pre-wrap">
|
||||||
{JSON.stringify(liveQ.data.data, null, 2)}
|
{JSON.stringify(liveQ.data.data, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
@@ -443,11 +443,11 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
|||||||
{t('reviews.drawer.proposed')}
|
{t('reviews.drawer.proposed')}
|
||||||
</p>
|
</p>
|
||||||
{draft.operation === 'CLOSE' ? (
|
{draft.operation === 'CLOSE' ? (
|
||||||
<p className="text-2xs text-aurora italic">
|
<p className="text-cell text-aurora italic">
|
||||||
{t('reviews.drawer.closeNote')}
|
{t('reviews.drawer.closeNote')}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<pre className="bg-accent/4 rounded p-2 text-2xs font-mono overflow-x-auto whitespace-pre-wrap">
|
<pre className="text-mono bg-accent/4 rounded p-2 overflow-x-auto whitespace-pre-wrap">
|
||||||
{JSON.stringify(draft.data, null, 2)}
|
{JSON.stringify(draft.data, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function SearchPage() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{tooShort && (
|
{tooShort && (
|
||||||
<p className="text-2xs text-mute">{t('search.tooShort')}</p>
|
<p className="text-cell 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-mute">
|
<p className="text-cell 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-sans text-accent hover:underline"
|
className="text-body font-sans text-accent hover:underline"
|
||||||
>
|
>
|
||||||
{g.dictDisplayName ?? g.dictName}
|
{g.dictDisplayName ?? g.dictName}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -110,7 +110,7 @@ function SearchPage() {
|
|||||||
{g.items.map((it) => (
|
{g.items.map((it) => (
|
||||||
<li
|
<li
|
||||||
key={`${g.dictName}-${it.businessKey}`}
|
key={`${g.dictName}-${it.businessKey}`}
|
||||||
className="flex items-center justify-between gap-3 py-2 text-2xs"
|
className="flex items-center justify-between gap-3 py-2 text-cell"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
to="/dictionaries/$name"
|
to="/dictionaries/$name"
|
||||||
@@ -136,7 +136,7 @@ function SearchPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasQuery && !tooShort && (
|
{!hasQuery && !tooShort && (
|
||||||
<p className="text-2xs text-mute">{t('search.intro')}</p>
|
<p className="text-cell text-mute">{t('search.intro')}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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-ink-2 hover:text-accent">
|
<Link to="/webhooks" className="text-body text-ink-2 hover:text-accent">
|
||||||
← {t('nav.webhooks')}
|
← {t('nav.webhooks')}
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ function WebhookDetailPage() {
|
|||||||
<Panel>
|
<Panel>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4">
|
||||||
<InfoRow label={t('webhooks.field.url')}>
|
<InfoRow label={t('webhooks.field.url')}>
|
||||||
<code className="font-mono text-2xs break-all">{data.url}</code>
|
<code className="text-mono break-all">{data.url}</code>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
<InfoRow label={t('webhooks.col.status')}>
|
<InfoRow label={t('webhooks.col.status')}>
|
||||||
<Badge variant={data.active ? 'success' : 'neutral'}>
|
<Badge variant={data.active ? 'success' : 'neutral'}>
|
||||||
@@ -177,28 +177,28 @@ function WebhookDetailPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-2xs text-mute">{t('webhooks.allScopes')}</span>
|
<span className="text-cell text-mute">{t('webhooks.allScopes')}</span>
|
||||||
)}
|
)}
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
<InfoRow label={t('webhooks.field.dictionaryFilter')}>
|
<InfoRow label={t('webhooks.field.dictionaryFilter')}>
|
||||||
<span className="text-2xs">
|
<span className="text-cell">
|
||||||
{data.dictionaryFilter && data.dictionaryFilter.length > 0
|
{data.dictionaryFilter && data.dictionaryFilter.length > 0
|
||||||
? data.dictionaryFilter.join(', ')
|
? data.dictionaryFilter.join(', ')
|
||||||
: t('webhooks.allDicts')}
|
: t('webhooks.allDicts')}
|
||||||
</span>
|
</span>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
<InfoRow label={t('webhooks.field.eventTypeFilter')}>
|
<InfoRow label={t('webhooks.field.eventTypeFilter')}>
|
||||||
<span className="text-2xs">
|
<span className="text-cell">
|
||||||
{data.eventTypeFilter && data.eventTypeFilter.length > 0
|
{data.eventTypeFilter && data.eventTypeFilter.length > 0
|
||||||
? data.eventTypeFilter.join(', ')
|
? data.eventTypeFilter.join(', ')
|
||||||
: t('webhooks.allEvents')}
|
: t('webhooks.allEvents')}
|
||||||
</span>
|
</span>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
<InfoRow label="HMAC secret">
|
<InfoRow label="HMAC secret">
|
||||||
<code className="font-mono text-2xs">{data.hmacSecret}</code>
|
<code className="text-mono">{data.hmacSecret}</code>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
<InfoRow label={t('webhooks.field.createdBy')}>
|
<InfoRow label={t('webhooks.field.createdBy')}>
|
||||||
<span className="text-2xs">
|
<span className="text-cell">
|
||||||
{data.createdBy} · {new Date(data.createdAt).toLocaleString()}
|
{data.createdBy} · {new Date(data.createdAt).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
@@ -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-sans text-base text-accent">
|
<h2 className="font-sans text-title-md text-accent">
|
||||||
{t('webhooks.deliveries.title')}
|
{t('webhooks.deliveries.title')}
|
||||||
</h2>
|
</h2>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -232,7 +232,7 @@ 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-[0.10em] font-display transition-colors',
|
'text-cap inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border transition-colors',
|
||||||
active
|
active
|
||||||
? 'border-accent bg-accent/8 text-accent'
|
? 'border-accent bg-accent/8 text-accent'
|
||||||
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
|
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
|
||||||
@@ -249,7 +249,7 @@ function WebhookDetailPage() {
|
|||||||
setPage(0)
|
setPage(0)
|
||||||
setStatusFilter(undefined)
|
setStatusFilter(undefined)
|
||||||
}}
|
}}
|
||||||
className="text-xs text-accent hover:underline ml-2"
|
className="text-cell text-accent hover:underline ml-2"
|
||||||
>
|
>
|
||||||
{t('webhooks.deliveries.filter.clear')}
|
{t('webhooks.deliveries.filter.clear')}
|
||||||
</button>
|
</button>
|
||||||
@@ -282,7 +282,7 @@ function WebhookDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<TableRow key={d.id}>
|
<TableRow key={d.id}>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-2xs">{d.outboxEventId}</span>
|
<span className="text-mono">{d.outboxEventId}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={STATUS_BADGE[d.status] ?? 'neutral'}>
|
<Badge variant={STATUS_BADGE[d.status] ?? 'neutral'}>
|
||||||
@@ -290,22 +290,22 @@ function WebhookDetailPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-2xs">{d.attemptCount}</span>
|
<span className="text-mono">{d.attemptCount}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-2xs text-ink-2">
|
<span className="text-cell text-ink-2">
|
||||||
{d.lastAttemptAt
|
{d.lastAttemptAt
|
||||||
? new Date(d.lastAttemptAt).toLocaleString()
|
? new Date(d.lastAttemptAt).toLocaleString()
|
||||||
: '—'}
|
: '—'}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-2xs">
|
<span className="text-mono">
|
||||||
{d.lastStatusCode ?? '—'}
|
{d.lastStatusCode ?? '—'}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-2xs text-ink-2 line-clamp-2 max-w-md inline-block">
|
<span className="text-cell 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-ink-2 self-center">
|
<span className="text-body text-ink-2 self-center">
|
||||||
{page + 1} / {deliveries.data.totalPages}
|
{page + 1} / {deliveries.data.totalPages}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
@@ -384,14 +384,14 @@ function TestResultAlert({
|
|||||||
action={
|
action={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs text-ink-2 hover:text-ink"
|
className="text-cell text-ink-2 hover:text-ink"
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-1 text-sm">
|
<div className="space-y-1 text-body">
|
||||||
{result.httpStatus != null && (
|
{result.httpStatus != null && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-cap text-mute mr-2">
|
<span className="text-cap text-mute mr-2">
|
||||||
@@ -409,7 +409,7 @@ function TestResultAlert({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{result.message && (
|
{result.message && (
|
||||||
<div className="font-mono text-2xs text-ink whitespace-pre-wrap break-all">
|
<div className="text-mono text-ink whitespace-pre-wrap break-all">
|
||||||
{result.message}
|
{result.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -446,14 +446,14 @@ 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-sans text-lg text-accent">
|
<h3 className="font-sans text-title-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-line 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="text-mono break-all">{secret}</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button variant="primary" onClick={onClose}>
|
<Button variant="primary" onClick={onClose}>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ function WebhooksPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="font-mono text-2xs truncate max-w-xs inline-block align-middle">
|
<span className="text-mono truncate max-w-xs inline-block align-middle">
|
||||||
{sub.url}
|
{sub.url}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -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-ink">
|
<span className="text-cell 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-mute">{t('webhooks.allDicts')}</span>
|
<span className="text-cell text-mute">{t('webhooks.allDicts')}</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
@@ -175,7 +175,7 @@ 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-mute">{t('webhooks.allScopes')}</span>
|
return <span className="text-cell text-mute">{t('webhooks.allScopes')}</span>
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@@ -368,7 +368,7 @@ function SecretRevealModal({
|
|||||||
{t('webhooks.secret.warning')}
|
{t('webhooks.secret.warning')}
|
||||||
</Alert>
|
</Alert>
|
||||||
<div className="bg-carbon/5 border border-line 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="text-mono break-all">{revealed.secret}</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -146,32 +146,86 @@
|
|||||||
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
|
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
|
||||||
--font-display: 'Tektur', 'Inter', sans-serif;
|
--font-display: 'Tektur', 'Inter', sans-serif;
|
||||||
|
|
||||||
/* Font sizes per design_handoff_ordinis_mdm/README.md spec.
|
/* Font scale.
|
||||||
* Override Tailwind defaults так чтобы существующие utility classes
|
*
|
||||||
* автоматически соответствовали handoff:
|
* Наши собственные классы — семантические @utility ниже (text-body,
|
||||||
* text-sm : 13px (workhorse — body, buttons, tabs)
|
* text-cell, text-mono, text-title-*, text-cap). Каждая роль = место в UI.
|
||||||
* text-xs : 12px (default, без изменений — secondary meta)
|
*
|
||||||
* text-2xs : 11px (custom — IDs, dates, table mono meta)
|
* --text-sm: 13px — нужно потому что @nstart/ui dist внутри использует
|
||||||
* Tektur caps 10.5px → используй .cap или text-cap utility (отдельно).
|
* text-sm для buttons/inputs/forms (см. @source выше). Без этого override
|
||||||
* Table cells 12.5px / page titles 17px / modal titles 22px — explicit
|
* библиотечные компоненты ушли бы к Tailwind default 14px и разошлись с
|
||||||
* через text-[Npx] arbitrary value по месту. */
|
* нашим handoff workhorse 13px. После Stage 3.9 (pnpm remove @nstart/ui)
|
||||||
|
* этот override можно убрать — наш код text-sm не использует.
|
||||||
|
*
|
||||||
|
* --text-2xs override НЕ нужен: убрали все usage в audit, text-2xs больше
|
||||||
|
* нигде не появляется. */
|
||||||
--text-sm: 13px;
|
--text-sm: 13px;
|
||||||
--text-sm--line-height: 1.4;
|
--text-sm--line-height: 1.45;
|
||||||
--text-2xs: 11px;
|
|
||||||
--text-2xs--line-height: 1.35;
|
|
||||||
|
|
||||||
--radius-sm: 4px;
|
--radius-sm: 4px;
|
||||||
--radius-md: 6px;
|
--radius-md: 6px;
|
||||||
--radius-lg: 8px;
|
--radius-lg: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tektur caps utility — handoff 10.5px uppercase tracking 0.10em.
|
/* === Semantic typography utilities per design_handoff_ordinis_mdm/README.md ===
|
||||||
* Codemod target: replace `text-2xs uppercase tracking-[0.10em]` → `text-cap`. */
|
* Type system — 7 ролей, каждая соответствует месту в UI:
|
||||||
|
*
|
||||||
|
* text-title-xl — 22/600 modal title, section header (rare)
|
||||||
|
* text-title-lg — 17/600 page title в editor
|
||||||
|
* text-title-md — 15/600 dictionary card title
|
||||||
|
* text-body — 13/400 workhorse: body, buttons, tabs, inputs
|
||||||
|
* text-cell — 12.5/500 table cell text
|
||||||
|
* text-mono — 11/500 Mono: IDs, dates, FK refs
|
||||||
|
* text-cap — 10.5/600 Tektur UPPERCASE caption / section label
|
||||||
|
*
|
||||||
|
* Color и weight overrides поверх — добавлять отдельными utility classes
|
||||||
|
* (e.g. `text-body font-medium text-ink`). НЕ накладывать font-mono/
|
||||||
|
* font-display/uppercase/tracking — они baked в соответствующие utility. */
|
||||||
|
|
||||||
|
@utility text-title-xl {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-title-lg {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-title-md {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-body {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-cell {
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility text-mono {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
@utility text-cap {
|
@utility text-cap {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
line-height: 1;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.10em;
|
letter-spacing: 0.10em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { cn } from '@/lib/utils'
|
|||||||
* shadcn-style API: children может быть [Title, Description] или произвольный.
|
* shadcn-style API: children может быть [Title, Description] или произвольный.
|
||||||
*/
|
*/
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
'relative w-full rounded-md border-l-4 px-4 py-3 text-sm [&>svg]:text-ink-2 [&>svg]:size-4 [&>svg]:absolute [&>svg]:left-3 [&>svg]:top-3 [&>svg+*]:pl-6',
|
'relative w-full rounded-md border-l-4 px-4 py-3 text-body [&>svg]:text-ink-2 [&>svg]:size-4 [&>svg]:absolute [&>svg]:left-3 [&>svg]:top-3 [&>svg+*]:pl-6',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-2xs font-medium font-mono uppercase tracking-wide whitespace-nowrap',
|
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-cell font-medium font-mono uppercase tracking-wide whitespace-nowrap',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { cn } from '@/lib/utils'
|
|||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
[
|
[
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap',
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap',
|
||||||
'rounded-md text-sm font-medium transition-colors',
|
'rounded-md text-body font-medium transition-colors',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg',
|
||||||
'disabled:pointer-events-none disabled:opacity-50',
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
'[&_svg]:pointer-events-none [&_svg]:shrink-0',
|
'[&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||||
@@ -42,7 +42,7 @@ const buttonVariants = cva(
|
|||||||
'text-accent underline-offset-4 hover:underline',
|
'text-accent underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
sm: 'h-7 px-2.5 text-xs',
|
sm: 'h-7 px-2.5 text-cell',
|
||||||
md: 'h-9 px-3.5',
|
md: 'h-9 px-3.5',
|
||||||
lg: 'h-10 px-4',
|
lg: 'h-10 px-4',
|
||||||
'icon-sm': 'h-6 w-6',
|
'icon-sm': 'h-6 w-6',
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttribut
|
|||||||
return (
|
return (
|
||||||
<h3
|
<h3
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-base font-semibold leading-tight text-ink', className)}
|
className={cn('text-title-md text-ink', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -43,7 +43,7 @@ export const CardDescription = React.forwardRef<
|
|||||||
HTMLParagraphElement,
|
HTMLParagraphElement,
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
>(function CardDescription({ className, ...props }, ref) {
|
>(function CardDescription({ className, ...props }, ref) {
|
||||||
return <p ref={ref} className={cn('text-sm text-ink-2', className)} {...props} />
|
return <p ref={ref} className={cn('text-body text-ink-2', className)} {...props} />
|
||||||
})
|
})
|
||||||
|
|
||||||
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const DialogTitle = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-base font-semibold text-ink leading-tight', className)}
|
className={cn('text-title-xl text-ink', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -131,7 +131,7 @@ export const DialogDescription = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm text-ink-2', className)}
|
className={cn('text-body text-ink-2', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-surface-2 data-[state=open]:bg-surface-2',
|
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-body outline-none focus:bg-surface-2 data-[state=open]:bg-surface-2',
|
||||||
inset && 'pl-8',
|
inset && 'pl-8',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -77,7 +77,7 @@ export const DropdownMenuItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-surface-2 focus:text-ink',
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-body outline-none transition-colors focus:bg-surface-2 focus:text-ink',
|
||||||
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
inset && 'pl-8',
|
inset && 'pl-8',
|
||||||
className,
|
className,
|
||||||
@@ -95,7 +95,7 @@ export const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-surface-2',
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-body outline-none transition-colors focus:bg-surface-2',
|
||||||
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -120,7 +120,7 @@ export const DropdownMenuRadioItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-surface-2',
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-body outline-none transition-colors focus:bg-surface-2',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -142,7 +142,7 @@ export const DropdownMenuLabel = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('px-2 py-1.5 text-2xs font-semibold uppercase tracking-wider text-mute', inset && 'pl-8', className)}
|
className={cn('px-2 py-1.5 text-cell font-semibold uppercase tracking-wider text-mute', inset && 'pl-8', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export function EmptyState({
|
|||||||
>
|
>
|
||||||
{icon && <div className="text-mute [&>svg]:size-8">{icon}</div>}
|
{icon && <div className="text-mute [&>svg]:size-8">{icon}</div>}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-base font-semibold text-ink">{title}</div>
|
<div className="text-title-md text-ink">{title}</div>
|
||||||
{description && <div className="text-sm text-ink-2 max-w-md">{description}</div>}
|
{description && <div className="text-body text-ink-2 max-w-md">{description}</div>}
|
||||||
</div>
|
</div>
|
||||||
{action && <div className="mt-2">{action}</div>}
|
{action && <div className="mt-2">{action}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
aria-required={required || undefined}
|
aria-required={required || undefined}
|
||||||
aria-invalid={Boolean(error) || undefined}
|
aria-invalid={Boolean(error) || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-9 w-full rounded-md border border-line bg-surface px-3 py-1 text-sm text-ink',
|
'flex h-9 w-full rounded-md border border-line bg-surface px-3 py-1 text-body text-ink',
|
||||||
'placeholder:text-mute',
|
'placeholder:text-mute',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-bg',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-bg',
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
'file:border-0 file:bg-transparent file:text-sm file:font-medium',
|
'file:border-0 file:bg-transparent file:text-body file:font-medium',
|
||||||
error && 'border-pink focus-visible:ring-pink',
|
error && 'border-pink focus-visible:ring-pink',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
@@ -64,9 +64,9 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
)}
|
)}
|
||||||
{inputEl}
|
{inputEl}
|
||||||
{error ? (
|
{error ? (
|
||||||
<span className="text-xs text-pink">{error}</span>
|
<span className="text-cell text-pink">{error}</span>
|
||||||
) : hint ? (
|
) : hint ? (
|
||||||
<span className="text-xs text-mute">{hint}</span>
|
<span className="text-cell text-mute">{hint}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const Label = React.forwardRef<
|
|||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-2xs font-medium font-display uppercase tracking-[0.10em] text-mute leading-none',
|
'text-cap font-medium text-mute ',
|
||||||
'peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
'peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function LoadingBlock({ className, label, size = 'md', ...props }: Loadin
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Loader2 className={cn('animate-spin text-accent', s.icon)} />
|
<Loader2 className={cn('animate-spin text-accent', s.icon)} />
|
||||||
{label && <span className="text-sm">{label}</span>}
|
{label && <span className="text-body">{label}</span>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export function PageHeader({
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h1 className="text-lg font-semibold text-ink leading-tight truncate">{title}</h1>
|
<h1 className="text-title-lg text-ink truncate">{title}</h1>
|
||||||
{description && <p className="text-sm text-ink-2 mt-1 max-w-2xl">{description}</p>}
|
{description && <p className="text-body text-ink-2 mt-1 max-w-2xl">{description}</p>}
|
||||||
</div>
|
</div>
|
||||||
{actions && <div className="shrink-0 flex items-center gap-2">{actions}</div>}
|
{actions && <div className="shrink-0 flex items-center gap-2">{actions}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const PopoverContent = React.forwardRef<
|
|||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 w-72 rounded-md border border-line bg-surface p-3 text-sm text-ink shadow-lg outline-none',
|
'z-50 w-72 rounded-md border border-line bg-surface p-3 text-body text-ink shadow-lg outline-none',
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function ScopeBadge({
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 rounded-sm px-1.5 py-0.5 text-2xs font-mono font-medium',
|
'inline-flex items-center gap-1.5 rounded-sm px-1.5 py-0.5 text-cell font-mono font-medium',
|
||||||
SCOPE_BG[scope],
|
SCOPE_BG[scope],
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const SheetTitle = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-base font-semibold text-ink', className)}
|
className={cn('text-title-xl text-ink', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -126,7 +126,7 @@ export const SheetDescription = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-sm text-ink-2', className)}
|
className={cn('text-body text-ink-2', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const TabsTrigger = React.forwardRef<
|
|||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center justify-center whitespace-nowrap px-3.5 h-10 text-sm text-ink-2 -mb-px',
|
'inline-flex items-center justify-center whitespace-nowrap px-3.5 h-10 text-body text-ink-2 -mb-px',
|
||||||
'border-b-2 border-transparent transition-all hover:text-ink',
|
'border-b-2 border-transparent transition-all hover:text-ink',
|
||||||
'focus-visible:outline-none focus-visible:text-accent',
|
'focus-visible:outline-none focus-visible:text-accent',
|
||||||
'disabled:pointer-events-none disabled:opacity-50',
|
'disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
aria-required={required || undefined}
|
aria-required={required || undefined}
|
||||||
aria-invalid={Boolean(error) || undefined}
|
aria-invalid={Boolean(error) || undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex w-full rounded-md border border-line bg-surface px-3 py-2 text-sm text-ink',
|
'flex w-full rounded-md border border-line bg-surface px-3 py-2 text-body text-ink',
|
||||||
'placeholder:text-mute',
|
'placeholder:text-mute',
|
||||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-bg',
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-bg',
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
@@ -63,9 +63,9 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
)}
|
)}
|
||||||
{textareaEl}
|
{textareaEl}
|
||||||
{error ? (
|
{error ? (
|
||||||
<span className="text-xs text-pink">{error}</span>
|
<span className="text-cell text-pink">{error}</span>
|
||||||
) : hint ? (
|
) : hint ? (
|
||||||
<span className="text-xs text-mute">{hint}</span>
|
<span className="text-cell text-mute">{hint}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function Toaster() {
|
|||||||
},
|
},
|
||||||
classNames: {
|
classNames: {
|
||||||
title: 'font-medium',
|
title: 'font-medium',
|
||||||
description: 'text-mute text-xs',
|
description: 'text-mute text-cell',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const TooltipContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 overflow-hidden rounded-sm bg-ink text-on-accent px-2 py-1 text-2xs font-medium shadow',
|
'z-50 overflow-hidden rounded-sm bg-ink text-on-accent px-2 py-1 text-cell font-medium shadow',
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
className,
|
className,
|
||||||
|
|||||||
Reference in New Issue
Block a user