Аудит паритета UI: каждый паттерн одинаков везде
Систематический аудит: доказать что каждый повторяющийся паттерн (кнопки, отступы, цвета, типографика, анимации) идентичен во всех экземплярах. Строится реестр расхождений (divergence ledger) с точными значениями, файлами и путём фикса к токену.
Дизайн-системы не ломаются внезапно — они дрейфуют. Один разработчик дописал border-radius: 6px вместо var(--radius-sm), другой скопировал кнопку и забыл про hover-цвет. Через полгода в продукте живёт 11 вариантов border-radius там где система определяет 3. Пользователь не назовёт это «несоответствием токенов», но чувствует что что-то «не так».
Этот аудит — не «посмотри как выглядит», а структурированное доказательство: для каждого повторяющегося паттерна — список всех его экземпляров, извлечённые computed-значения, сравнение с single source of truth, дивергенции с точным severity и конкретным фиксом.
Product: {{product_url}} Tokens source: {{tokens_source}}
1. Parity rule: когда два экземпляра ДОЛЖНЫ быть идентичны
Прежде чем проверять — определи правило паритета для каждого паттерна. Без него «различие» и «намеренный вариант» неразличимы.
Формат parity rule:
"Все primary buttons одного размера должны иметь одинаковый background-color, border-radius, font-size, font-weight, padding, hover-color, focus-ring и disabled-opacity — независимо от страницы и контейнера."
- ✓ Primary button на /login vs primary button в modal — должны совпадать (одна роль, один паттерн)
- ✓ Card с border на /dashboard vs card на /settings — должны совпадать
- ✗ Primary button vs secondary button — намеренно разные, paritу rule не применяется
- ✗ Skeleton на /feed vs skeleton на /profile — одинаковый компонент, одна реализация
Для каждого паттерна в sections 2-4: сначала запиши parity rule, потом inventory.
2. Inventory паттернов: что проверять
2.1 Кнопки
Для каждого варианта (primary, secondary, ghost, destructive, link-style) × каждого размера (sm, md, lg, icon):
Извлечь по каждому экземпляру:
button.primary.md {
background-color: computed → должен быть var(--color-primary-600)
color: computed
border-radius: computed → должен быть var(--radius-md)
padding: computed
font-size: computed → должен быть var(--text-sm)
font-weight: computed
transition: computed → должен быть var(--duration-fast) ease
}
:hover → background-color
:focus-visible → outline, outline-offset
:disabled → opacity, cursor, pointer-events
Как извлечь: DevTools → Elements → Computed → скопировать ключевые свойства. Для sweep по всему сайту: document.querySelectorAll('[class*="btn"],[class*="button"]') → getComputedStyle на каждом элементе → сгруппировать по variant+size.
Красные флаги:
- ✓ 3 экземпляра primary.md на странице — одинаковые computed значения → pass
- ✗ 11 distinct border-radius values при 3 задокументированных → divergence
2.2 Inputs и формы
Каждое поле: text, email, password, textarea, select, checkbox, radio, toggle, file-upload, datepicker.
Проверить:
- border-color в default / focus / error / disabled состояниях → единый токен
- border-radius → единый
- padding → единый
- label: позиция (over/above/inside), font-size, color
- placeholder color →
var(--color-muted)или хардкод? - error message: color, font-size, gap от поля
- focus ring: одинаковый ли outline на всех полях и всех страницах?
2.3 Modals и диалоги
- border-radius → один ли токен
- backdrop: opacity, color, blur
- padding inside
- close button: позиция, размер, hover-цвет
- ESC/outside-click dismiss — единое поведение везде?
- Анимация входа/выхода: duration, easing
2.4 Cards
- border-radius → единый для карточек одного типа
- border или box-shadow: хватает одного, не оба ли используются вперемешку
- padding →
var(--space-4)везде или 16px / 1rem / p-4 в разных местах? - hover-state: lift (translateY), shadow-change, border-color-change — или их микс?
2.5 Empty states
- иллюстрация vs иконка: единый подход или рандомно?
- heading + body copy: единый размер и weight
- CTA: primary button или link — единый паттерн?
- Отступы от краёв контейнера
2.6 Skeletons / loaders
- Pulse-анимация: duration, easing → единые по всему продукту
- border-radius skeleton-блоков: совпадает ли с реальными элементами?
- Spinner: единый размер и цвет везде (не 3 разных компонента spinner)
2.7 Toasts / уведомления
- success / error / warning / info: цвет иконки + border-left + bg — единая палитра
- Позиция (bottom-right? top-center?): одна для всего продукта
- Duration: 3 с / 5 с / indefinite — единое правило
- Dismiss: кнопка, свайп, авто — единообразие
3. Токены: drift-детектор
Самый частый источник дивергенции — хардкод вместо токена. Даже правильное значение (#3B82F6 вместо var(--color-primary-500)) — это drift.
Как искать token drift
Метод 1: computed vs var() — в DevTools смотри Computed, не Styles. Если видишь color: rgb(59, 130, 246) — определи, это через var() или хардкод. В Styles-панели ищи источник: если color: #3b82f6 без ссылки на переменную — drift.
Метод 2: CSS grep — в исходниках:
# Хардкод hex-цветов вне токенного файла:
grep -r '#[0-9a-fA-F]\{3,6\}' src/components --include="*.css" --include="*.module.css"
# px-значения отступов (признак дрейфа от spacing-scale):
grep -r 'padding:\s*[0-9]\+px\|margin:\s*[0-9]\+px' src/components
# Хардкод border-radius:
grep -r 'border-radius:\s*[0-9]' src/components
Метод 3: Figma inspect — если {{tokens_source}} — Figma: проверить через Dev Mode, что применённые токены совпадают с реализованными.
Token drift severity
| Тип drift | Severity | Пример |
|---|---|---|
| Правильное значение, хардкод | Low | border-radius: 8px вместо var(--radius-md) → сейчас совпадает, сломается при изменении токена |
| Близкое значение, другой токен | Medium | var(--radius-sm) где должен быть var(--radius-md) → 4px vs 8px, видно глазу |
| Другое значение, хардкод | High | border-radius: 12px — нет такого токена, ни один компонент кроме этого не использует |
| Другой цвет, хардкод | Critical | background: #2563EB вместо var(--color-primary-600) — пройдёт мимо color-theme change |
4. Spacing, typography, motion, icons
Spacing scale
Документированная scale, например: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64px.
Инвентаризовать:
- gap between cards на /home
- padding inside each card
- margin-bottom под headings
- gap в nav links
- padding в модальных окнах
Ожидаемый результат: все значения принадлежат scale. Красный флаг: gap: 18px / padding: 22px — off-scale, нет такого токена.
Как считать distinct values:
// В DevTools console на странице:
[...document.querySelectorAll('*')].map(el => getComputedStyle(el).gap).filter(v => v && v !== 'normal').reduce((acc, v) => { acc[v] = (acc[v]||0)+1; return acc; }, {})
Type scale
Проверить: сколько distinct font-size values в продукте vs сколько определено в системе.
Допустимые: xs (12px), sm (14px), base (16px), lg (18px), xl (20px), 2xl (24px), 3xl (30px), 4xl (36px).
Красный флаг: 13px, 15px, 17px, 19px — off-scale. Сколько их и где?
| Значение | Кол-во элементов | Файлы | Соответствует токену? |
|---|---|---|---|
| 14px | 47 | button, label, nav | ✓ var(--text-sm) |
| 13px | 3 | /pricing table | ✗ нет такого токена |
| 15px | 1 | footer copyright | ✗ нет такого токена |
Icon stroke и size
- Один ли набор иконок? (не lucide + heroicons вперемешку)
- stroke-width: один ли (1.5 везде или 2 везде, не смесь)?
- Размер: 16px / 20px / 24px — соответствует scale или рандомный?
- Цвет: через
currentColorили хардкод?
Motion
- transition-duration: сколько distinct values? Система определяет 3 (fast 100ms, base 200ms, slow 400ms)?
- easing functions: ease / ease-in-out / cubic-bezier(...) — один ли паттерн?
- Анимации входа (fade-in, slide-up): одинаковы ли на разных компонентах?
5. Microcopy и лейблы
Паrity rule для copy: один и тот же action называется одним словом везде.
Inventory:
- Кнопка отмены: «Отмена» / «Отменить» / «Cancel» / «Закрыть» — сколько вариантов?
- Destructive confirm: «Удалить» / «Удалить навсегда» / «Delete» — единый ли?
- Empty state copy: «Ничего нет» / «Пусто» / «Нет данных» / «Список пуст»
- Loading copy: «Загрузка...» / «Загружаем...» / ничего
- Error copy: «Что-то пошло не так» / «Ошибка» / «Попробуйте позже»
Таблица:
| Action | Variant 1 | Variant 2 | Canonical | Offending locations |
|---|---|---|---|---|
| Cancel button | «Отмена» (32 вхождения) | «Отменить» (4 вхождения) | «Отмена» | /settings/profile, /billing |
| Delete confirm | «Удалить» (8) | «Удалить навсегда» (2) | «Удалить» | /admin/users |
6. Как решать, что canonical
Когда два экземпляра расходятся и неясно какой правильный:
- Токен в {{tokens_source}} → он canonical. Экземпляр без токена — дрейфует.
- Если оба без токена → canonical = чаще встречающийся (majority wins) или определённый дизайнером.
- Если расходятся визуально и оба «выглядят нормально» → canonical = тот, что в более видимом, высокотрафиковом месте (main CTA, home page).
- Если нет консенсуса → завести токен в {{tokens_source}}, зафиксировать значение, привести все к нему.
7. Divergence Ledger
Финальный реестр расхождений:
| Pattern | Rule | Instance A | Instance B | A value | B value | Canonical | Severity | Fix |
|---|---|---|---|---|---|---|---|---|
| Primary button radius | все одинаковые | /login CTA | /modal CTA | 8px (var) | 6px (hardcode) | 8px | Medium | заменить 6px на var(--radius-md) в Modal.tsx:L42 |
| Gap between cards | spacing scale | /home feed | /search results | 24px | 18px | 24px | High | /search/Results.tsx: gap-[18px] → gap-6 |
| Button font-weight | все primary 600 | /home hero | /pricing table | 600 | 500 | 600 | Low | PricingCard.tsx:L88 |
| Spinner size | 24px everywhere | /dashboard | /settings save | 24px | 20px | 24px | Low | SettingsForm.tsx |
Severity scale:
- Critical — разные бренд-цвета, разные паттерны confirm для destructive action
- High — off-scale spacing, заметное расхождение border-radius
- Medium — close but wrong (8px vs 6px), правильное значение но хардкод
- Low — copy-вариации, off-by-one font-weight
Anti-patterns
- ❌ Проверять «на глаз» без computed values — 8px vs 6px неотличимы без цифр
- ❌ Игнорировать hover/focus/disabled состояния — drift чаще всего именно там
- ❌ Считать правильное значение без var() «ОК» — это drift, ломается при смене токена
- ❌ Составлять Ledger без fix-пути к конкретному файлу и строке — бесполезен для разработчика
- ❌ Делать аудит без parity rule — каждый «нашёл» будет спорить что это «намеренный вариант»
- ❌ Смешивать намеренные варианты (primary vs secondary) с дрейфом — засоряет Ledger
- ❌ Забывать mobile viewport — на мобильном отступы часто перегружены отдельными медиазапросами с off-scale значениями
- ❌ Проверять только CSS, не checking Figma — drift между документацией и реализацией важнее
- ❌ Не считать количество distinct values — «разные» без числа не конвертируются в приоритет
Output
- Parity rules doc — список паттернов × их правило паритета (что должно совпадать)
- Token drift report — harcoded values vs var(), count per file, severity
- Spacing distinct-values dump — вывод console-скрипта + annotated список off-scale значений
- Type scale violations — таблица font-size values с количеством и файлами
- Divergence Ledger — полная таблица: pattern × instance A × instance B × canonical × severity × fix-path
- Critical fixes list — топ-10 по severity для первого PR
- Token backfill plan — какие хардкод-значения нужно завести как токен в {{tokens_source}}
Полный UX-аудит сайта
Эвристическая оценка по Нильсену + проверка ключевых сценариев. На выходе — приоритизированный список проблем.
Аудит производительности (Core Web Vitals)
Глубокая проверка LCP, INP, CLS с привязкой к коду и приоритизированным планом исправлений.
Аудит доступности по WCAG 2.2 AA
Проверка контраста, клавиатурной навигации, скринридеров, фокус-индикаторов и ARIA.