Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8b83af3
feat(Button): подключены сгенерированные токены
fabulous-unicorn Jul 30, 2026
8c2e208
feat(InputSwitch): подключены сгенерированные токены
fabulous-unicorn Jul 30, 2026
0eba8b2
feat(ProgressSpinner): подключены сгенерированные токены
fabulous-unicorn Jul 30, 2026
4616c51
feat(Skeleton): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
56607ae
feat(ProgressBar): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
42dd628
feat(Divider): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
194707e
feat(Avatar): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
14008bc
feat(Badge): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
8a7acf3
feat(Accordion): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
4676402
feat(Chip): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
9b26cd3
feat(Tag): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
da620c1
feat(Dialog): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
9a47ff4
feat(Checkbox): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
387411d
feat(Slider): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
e71d35a
feat(RadioButton): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
bae2a8d
feat(Tabs): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
20e1844
feat(Input): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
ad77026
feat(Button): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
bfd7946
feat(Typography): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
cd0e6e9
feat(Rating): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
84befac
feat(Menu): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
6c52db7
feat(Message): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
7efd14e
feat(SelectButton): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
c757fba
feat(ToggleButton): подключены сгенерированные токены
fabulous-unicorn Jul 31, 2026
75b05d4
chore(tokens): отмечены raw transparent
fabulous-unicorn Jul 31, 2026
5c9a6b4
chore(tokens): отмечены семантические ошибки
fabulous-unicorn Jul 31, 2026
1b00580
chore(tokens): отмечены пропущенные значения
fabulous-unicorn Jul 31, 2026
72dee6e
chore(tokens): добавлен ревизор TODO-комментариев миграции
Aug 3, 2026
8c0341c
fix(tokens): обоснован литерал transparent, фон link-кнопки переведён…
Aug 3, 2026
0c52093
feat(tokens): семейства шрифтов выводятся генератором вместо хардкода…
Aug 3, 2026
8498ad0
docs(tokens): обоснованы semantic-токены Typography и заимствование i…
Aug 3, 2026
f721171
refactor(tokens): disabled-состояния переведены с токенов кнопки на s…
Aug 3, 2026
c774faa
feat(tokens): шрифтовые примитивы целиком выводятся генератором и дос…
Aug 3, 2026
d847f0e
feat(tokens): размеры текста переведены на примитивы шрифтов с базой …
Aug 3, 2026
b2f0841
feat(tokens): интерлиньяж, трекинг и начертания переведены на примити…
Aug 3, 2026
94580ba
feat(tokens): 41 обращение переведено на компонентные токены
Aug 3, 2026
9c60057
feat(tokens): отступы, рамки и цвет вторичного текста переведены на s…
Aug 3, 2026
aaa881b
docs(tokens): описание миграции компонентов на токены
Aug 3, 2026
7f5d39a
feat(tokens): размеры полей и цветовые роли переведены на semantic-то…
Aug 3, 2026
bcd03ed
docs(tokens): актуализировано описание под финальное состояние
Aug 3, 2026
0e59bea
feat(tokens): сервисные цвета и цвет таймера переведены на semantic-роли
Aug 3, 2026
129d281
feat(tokens): непрозрачность и цвета ListBase переведены на semantic-…
Aug 3, 2026
b19705a
refactor(tokens): анимация курсора OTP берёт непрозрачность из токенов
Aug 3, 2026
813d95e
docs(tokens): узаконены semantic-токены SelectButton, разметка разобр…
Aug 3, 2026
de4aab2
refactor(tokens): размеры иконок, рамки и нулевые отступы переведены …
Aug 3, 2026
89aaf16
fix(tokens): забытые значения старой шкалы переведены на токены
Aug 3, 2026
337bf82
fix(tokens): цвета иконок статусов переведены на semantic-роли
Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 289 additions & 0 deletions docs/tokens-migration.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"tokens:check": "ts-node --transpile-only --project scripts/token-generator/tsconfig.json scripts/token-generator/build.ts --check",
"tokens:typecheck": "tsc --noEmit -p scripts/token-generator/tsconfig.json",
"tokens:validate": "yarn tokens:typecheck && yarn tokens:check",
"tokens:audit": "ts-node --transpile-only --project scripts/token-generator/tsconfig.json scripts/token-generator/audit.ts",
"doctor": "expo-doctor",
"check": "expo install --check",
"lint:check": "eslint --cache .",
Expand Down
14 changes: 11 additions & 3 deletions scripts/token-generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ Semantic-токены доступны потребителям библиоте

## Результат генерации

Из `design-tokens/input/tokens.json` создаются четыре файла:
Из `design-tokens/input/tokens.json` создаются пять файлов:

```text
src/theme/tokens
├── fonts.json
├── semantic
│ ├── light.json
│ └── dark.json
Expand All @@ -43,6 +44,12 @@ src/theme/tokens
└── dark.json
```

`fonts.json` собирается из `primitive.fonts.fontFamily` и не зависит от цветовой
схемы, поэтому файл один на обе темы. Публичный `FontsConfig` оперирует ролями
`primary` и `secondary`, поэтому `commonTheme` отображает `heading` в `primary`,
а `base` — в `secondary`. Отсутствие `primitive.fonts.fontFamily` во входных
токенах — ошибка генерации.

Для каждого выходного файла выбирается соответствующая ветка `colorScheme.light`
или `colorScheme.dark`. Ключ `colorScheme` сохраняется, а уровень `light`/`dark`
удаляется:
Expand All @@ -55,8 +62,9 @@ components.button.colorScheme.light.outlined
→ components/light.json: button.colorScheme.outlined
```

`primitive` нужен только для подстановки ссылок и не записывается в результат.
Light- и dark-файлы должны иметь одинаковую структуру и типы значений.
`primitive` нужен для подстановки ссылок и в результат не записывается —
единственное исключение `fonts.fontFamily`, попадающий в `fonts.json`. Light- и
dark-файлы должны иметь одинаковую структуру и типы значений.

## Преобразование значений

Expand Down
149 changes: 149 additions & 0 deletions scripts/token-generator/__tests__/audit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

import {
auditTodos,
collectTodos,
parseTodo,
resolveLegacyPath,
} from '../audit'

describe('parseTodo', () => {
test('разбирает поля комментария', () => {
const todo = parseTodo(
'// TODO(tokens-migration): reason=value-mismatch; legacy=spacing.Gap.gap-2; value=7; target=components.accordion.extend.extHeader.gap; targetValue=8',
'src/components/Accordion/Accordion.tsx',
168
)

expect(todo).toStrictEqual({
file: 'src/components/Accordion/Accordion.tsx',
line: 168,
reason: 'value-mismatch',
legacy: 'spacing.Gap.gap-2',
value: '7',
light: '',
dark: '',
target: 'components.accordion.extend.extHeader.gap',
targetValue: '8',
})
})

test('разбирает цветовые пары light/dark', () => {
expect(
parseTodo(
'// TODO(tokens-migration): reason=missing; legacy=components.button.extend.disabledColor; light=#85888e; dark=#a2a5a9',
'file.tsx',
1
)
).toMatchObject({ light: '#85888e', dark: '#a2a5a9', value: '' })
})

test('игнорирует строки без маркера и без legacy', () => {
expect(parseTodo('const gap = 8', 'file.tsx', 1)).toBeNull()
expect(
parseTodo('// TODO(tokens-migration): reason=missing', 'file.tsx', 1)
).toBeNull()
})
})

describe('resolveLegacyPath', () => {
test('находит сгенерированные component- и semantic-токены', () => {
expect(
resolveLegacyPath('components.button.extend.disabledColor')
).toBeDefined()
expect(
resolveLegacyPath('semantic.colorScheme.color.fg.muted')
).toBeDefined()
})

test('находит поддеревья theme, подмешанные отдельно от themeLight.json', () => {
expect(resolveLegacyPath('theme.InputSize.base.min-height')).toBe(35)
})

test('возвращает undefined для неймспейсов вне пайплайна', () => {
expect(resolveLegacyPath('fonts.primary')).toBeUndefined()
expect(resolveLegacyPath('hardcode.transparent')).toBeUndefined()
})
})

describe('auditTodos', () => {
test('классифицирует TODO по резолву legacy-пути', () => {
const [existing, mismatched, outside] = auditTodos([
{
file: 'a.tsx',
line: 1,
reason: 'missing',
legacy: 'theme.InputSize.base.min-height',
value: '35',
light: '',
dark: '',
target: '',
targetValue: '',
},
{
file: 'b.tsx',
line: 2,
reason: 'value-mismatch',
legacy: 'theme.InputSize.base.min-height',
value: '35',
light: '',
dark: '',
target: '',
targetValue: '99',
},
{
file: 'c.tsx',
line: 3,
reason: 'missing',
legacy: 'hardcode.transparent',
value: 'transparent',
light: '',
dark: '',
target: '',
targetValue: '',
},
])

expect(existing.status).toBe('токен есть')
expect(mismatched.status).toBe('значение расходится')
expect(outside.status).toBe('вне пайплайна')
})
})

describe('collectTodos', () => {
test('обходит вложенные каталоги и собирает комментарии', () => {
const directory = mkdtempSync(join(tmpdir(), 'tokens-audit-'))

try {
mkdirSync(join(directory, 'nested'))
writeFileSync(
join(directory, 'Component.tsx'),
'// TODO(tokens-migration): reason=missing; legacy=spacing.Gap.gap-2; value=7\nconst gap = 7\n'
)
writeFileSync(
join(directory, 'nested', 'useStyles.ts'),
'const size = 1\n// TODO(tokens-migration): reason=missing; legacy=border.Width.border; value=1\n'
)
writeFileSync(join(directory, 'readme.md'), 'TODO(tokens-migration): x')

const todos = collectTodos(directory)

expect(todos).toHaveLength(2)
expect(todos.map((todo) => todo.legacy).sort()).toStrictEqual([
'border.Width.border',
'spacing.Gap.gap-2',
])
expect(todos.map((todo) => todo.line).sort()).toStrictEqual([1, 2])
} finally {
rmSync(directory, { recursive: true, force: true })
}
})

test('в исходниках компонентов не осталось неразобранных комментариев', () => {
expect(collectTodos(`${__dirname}/../../../src/components`)).toStrictEqual(
[]
)
})
})
33 changes: 30 additions & 3 deletions scripts/token-generator/__tests__/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
parseBoxShorthand,
} from '../core/normalization'
import { resolveReferences } from '../core/resolution'
import { resolvePath } from '../core/tree'
import {
isTokenTree,
type CompiledTokens,
Expand Down Expand Up @@ -240,6 +241,7 @@ describe('token compiler', () => {
primitive: {
sizing: { small: '0.5rem', base: '1rem' },
motion: { fast: '150ms' },
fonts: { fontFamily: { base: 'Noto Sans', heading: 'TT Fellows' } },
},
semantic: {
dimension: {
Expand Down Expand Up @@ -320,6 +322,7 @@ describe('token compiler', () => {
primitive: {
sizing: { focusRing: '0.25rem' },
colors: { focus: '#d4fedc' },
fonts: { fontFamily: { base: 'Noto Sans', heading: 'TT Fellows' } },
},
semantic: {
dimension: {
Expand Down Expand Up @@ -402,8 +405,28 @@ describe('production input tokens', () => {
expect(tokenFiles).toStrictEqual(['tokens.json'])
})

test('выводит шрифтовые примитивы с нормализованными значениями', () => {
const primitive = source.primitive

if (!isTokenTree(primitive)) throw new Error('Expected primitive tokens')

const input = resolvePath(primitive, 'fonts')

if (!isTokenTree(input)) throw new Error('Expected font primitives')

expect(Object.keys(compiled.fonts)).toStrictEqual(Object.keys(input))
expect(compiled.fonts.fontFamily).toStrictEqual(input.fontFamily)
// rem пересчитан в px: "1rem" -> 16
expect(resolvePath(compiled.fonts, 'fontSize.300')).toBe(16)
expect(JSON.stringify(compiled.fonts)).not.toMatch(/rem/)
})

test('не экспортирует primitive и убирает разделение тем из colorScheme', () => {
expect(Object.keys(compiled)).toStrictEqual(['semantic', 'components'])
expect(Object.keys(compiled)).toStrictEqual([
'semantic',
'components',
'fonts',
])
expect(compiled).not.toHaveProperty('primitive')

const inputComponents = source.components
Expand Down Expand Up @@ -443,7 +466,9 @@ describe('production input tokens', () => {
test('содержит только целевые файлы', () => {
const tokenFiles = listJsonFiles(TOKENS_DIRECTORY)
const expectedFiles = Object.values(OUTPUT_FILES)
.flatMap((group) => Object.values(group))
.flatMap((group) =>
typeof group === 'string' ? [group] : Object.values(group)
)
.sort()

expect(tokenFiles).toStrictEqual(expectedFiles)
Expand Down Expand Up @@ -546,7 +571,9 @@ describe('production input tokens', () => {

expect(listJsonFiles(temporaryDirectory)).toStrictEqual(
Object.values(OUTPUT_FILES)
.flatMap((group) => Object.values(group))
.flatMap((group) =>
typeof group === 'string' ? [group] : Object.values(group)
)
.sort()
)
expect(
Expand Down
Loading
Loading