fix(fonts): semantic typography utilities per handoff (7 roles)

Заменяет blanket override из MR !45 на типизированную type scale per
design_handoff_ordinis_mdm/README.md "Scale" section.

Семь semantic @utility в styles.css:
  text-title-xl  22/600   — modal title, section header
  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 (font-mono baked in)
  text-cap       10.5/600 — Tektur UPPERCASE caption (font/uppercase baked in)

Audit phases:
  P1: добавил 7 утилит, font/uppercase/tracking baked где надо
  P2: 43 deterministic codemods (font-mono+text-2xs/[11px] → text-mono,
      [15/17/22]px+font-semibold → text-title-*, [12.5]px → text-cell,
      [10.5]px+uppercase+tracking → text-cap)
  P3: 64 text-sm → text-body (handoff workhorse), 84 text-2xs context-aware
      (TableCell → text-cell, bare → text-cell, плюс cleanup caps в backticks
      template literals который Phase 2 пропустил), 25 text-xs (6 → text-mono
      когда с font-mono, 19 → text-cell), 8 titles text-base/lg → text-title-*
  P4: убрал --text-2xs override (no users), оставил --text-sm: 13px scoped
      к @nstart/ui passthrough (см. comment в styles.css — убирается в Stage 3.9)

Stats:
  text-body: 69 | text-cell: 99 | text-mono: 50 | text-cap: 42
  text-title-xl: 5 | text-title-lg: 5 | text-title-md: 7
  text-sm/text-2xs/text-xs в src/: 0 (только в styles.css комментариях)

Поведение всех 277 typography usages теперь явно соответствует handoff —
каждое место осознанно выбрано под роль, не плажирующий override.
This commit is contained in:
Zimin A.N.
2026-05-11 14:31:35 +03:00
parent e62633d903
commit b94912789f
46 changed files with 296 additions and 243 deletions
@@ -119,7 +119,7 @@ export const CascadeConfirmDialog = ({
{plan && (
<>
{/* Header summary */}
<p className="text-sm text-ink">
<p className="text-body text-ink">
{hasBlockers
? t('cascade.summary.blocked', {
blockers: plan.blockers.length,
@@ -137,7 +137,7 @@ export const CascadeConfirmDialog = ({
variant="error"
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) => (
<li key={b.recordId} className="flex items-center gap-2">
<XCircleIcon weight="bold" size={12} className="text-aurora shrink-0" />
@@ -153,7 +153,7 @@ export const CascadeConfirmDialog = ({
</li>
)}
</ul>
<p className="mt-2 text-2xs text-ink-2">
<p className="mt-2 text-cell text-ink-2">
{t('cascade.blockers.resolveFirst')}
</p>
</Alert>
@@ -164,7 +164,7 @@ export const CascadeConfirmDialog = ({
<section className="space-y-2">
<div className="flex items-center gap-2">
<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 })}
</h4>
</div>
@@ -176,7 +176,7 @@ export const CascadeConfirmDialog = ({
return (
<li
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">
<span className="font-mono text-accent">
@@ -190,7 +190,7 @@ export const CascadeConfirmDialog = ({
</Badge>
<Badge variant="neutral">{entries.length}</Badge>
</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) => (
<li key={e.recordId} className="font-mono">
· {e.businessKey}
@@ -215,7 +215,7 @@ export const CascadeConfirmDialog = ({
variant="warning"
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>
)}
@@ -37,10 +37,10 @@ export const DictionaryDependentsPanel = ({ dictionaryName }: Props) => {
return (
<Panel>
<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')}
</h3>
<span className="text-2xs text-mute">
<span className="text-cell text-mute">
{t('lineage.usedBy.count', { count: data.length })}
</span>
</div>
@@ -48,7 +48,7 @@ export const DictionaryDependentsPanel = ({ dictionaryName }: Props) => {
{data.map((d) => (
<li
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
to="/dictionaries/$name"
@@ -85,7 +85,7 @@ const SectionLabel = ({
const { t } = useTranslation()
return (
<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'
}`}
>
@@ -141,7 +141,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
<div className="md:order-1 order-2 space-y-2">
<SectionLabel side="left" count={outgoingDicts.length} />
{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: 'нет исходящих ссылок' })}
</div>
) : (
@@ -166,14 +166,14 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
{t('dict.list.approval', { defaultValue: 'approval' })}
</Badge>
)}
<span className="ml-auto font-mono text-[11px] text-mute">
<span className="text-mono ml-auto text-mute">
v{detail.schemaVersion}
</span>
</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}
</h2>
<div className="font-mono text-[11px] text-mute mt-0.5">
<div className="text-mono text-mute mt-0.5">
{detail.name}
</div>
{detail.description && (
@@ -197,7 +197,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
<div className="md:order-3 order-3 space-y-2">
<SectionLabel side="right" count={incomingDicts.length} />
{incomingDicts.length === 0 ? (
<div className="text-2xs text-mute/70 italic">
<div className="text-cell text-mute/70 italic">
{t('hub.noIncoming', { defaultValue: 'никто не ссылается' })}
</div>
) : (
@@ -68,19 +68,19 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
return (
<section className="space-y-3">
<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')}
</h3>
<div className="flex items-center gap-2">
{stale !== null && stale >= 2 && (
<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')}
>
{t('lineage.refs.stale', { minutes: stale })}
</span>
)}
<span className="text-2xs text-mute">
<span className="text-cell text-mute">
{t('lineage.refs.total', { count: data.total })}
</span>
</div>
@@ -91,7 +91,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
{data.perSource.map((s) => (
<li
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
to="/dictionaries/$name"
@@ -115,7 +115,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
{data.items.map((r) => (
<li
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="flex items-center gap-2">
@@ -152,7 +152,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between text-2xs">
<div className="flex items-center justify-between text-cell">
<Button
variant="secondary"
size="sm"