# Сайт-портфолио
`06 · portfolio-site · Open source`
Сайт, который ты сейчас читаешь. React · Vite · TypeScript, token-first CSS-модули, типизированный EN/RU/AR-контент, text mirrors, privacy-first telemetry и route-aware навигация.
**Скоуп:** Соло · в работе
**Роль:** Сайт инженерного портфолио · 2025–2026
---
## Контекст
> Шесть кейсов, в которые рекрутер не может кликнуть. Один сайт, в который может.
Из шести показанных проектов пять — за closed/NDA-скоупом: клиентская работа, внутренний тулинг, R&D. Рекрутер не может их открыть, не может склонировать репо, не может посмотреть git-историю. Сам сайт — единственный публичный артефакт, который senior-ревьюер может реально аудитировать прямо сейчас: открыть DevTools, посмотреть network, прочитать CSS, view-source dot-grid. Каждая видимая пользователю деталь — одновременно решение, видимое инженеру.
Ограничение двойное. Страница должна сканироваться рекрутером за пять секунд — фиксированной формы карточки, моно-spec-листы, ASCII-диаграммы, читающиеся как terminal-вывод. И она должна выдержать senior-глаз, который скроллит медленно — token-first дизайн, ручной i18n с compile-time парностью, view-transitions для morph-перехода с домашней карточки в hero кейса, и Hero-эмберы, которые не греют iPhone. Сайт обязан быть собственным кейсом — потому что больше здесь ничего открыть нельзя.
## Факты
| | |
|---|---|
| **Скоуп** | Соло · в работе |
| **Поверхности** | Главная + 6 кейсов · EN/RU/AR · dark/light · 3 палитры |
| **Source** | Публичный GitHub repo · CV PDF · copy-as-Markdown actions |
| **Stack** | React 18 · Vite · TypeScript · CSS-переменные |
| **Контент** | Типизированные EN/RU/AR-словари · public/private content trees · generated text mirrors |
| **Telemetry** | Cookieless same-origin events · admin-only analytics |
| **Анимации** | View transitions (карточка → case hero FLIP-morph) · prefers-reduced-motion соблюдается |
| **Статус** | Open source · live |
## Архитектура
### Дерево провайдеров + роутинг
```text
// src/theme/ThemeContext.tsx
// src/i18n/LangContext.tsx
→
```
**Порядок провайдеров важен.** ThemeProvider снаружи — палитра ставится на `` до того, как любой ребёнок прочитает color-переменные. LangProvider следующий — `useT()` доступен везде. Loader, scroll restoration и analytics живут внутри роутера, потому что им нужен routed content или `useLocation()`.
**Два функциональных роута + 404 fallback.** `/` рендерит главную; `/cases/:slug` — страницу кейса; `*` уводит на главную для неизвестных URL. Whitelist слагов (`CASE_SLUGS = [ai-crm, roblox-game, ai-video-editor, ai-warehouse, macos-vpn, portfolio-site]`) живёт в `src/config/cases.ts` — `Nav.tsx` и `ProjectDetailPage.tsx` импортируют его; server analytics держит свои отдельные allowlists.
**Code-splitting пока нет.** Оба роута уезжают одним бандлом. Lighthouse зелёный; пересмотрю, когда появится третий роут.
### Цепочка резолвинга токенов
```text
1 ThemeContext theme = 'dark' → palette = 'ochre'
│
▼ пишет data-атрибуты в
2
│
▼ tokens.css матчит через attribute-селекторы
3 [data-theme="dark"] → --bg --fg --line
[data-palette="ochre"][data-theme="dark"] → --accent --accent-soft
│
▼ CSS-модули, импортированные styles.css, читают через var()
4 .hero h1 { color: var(--fg) }
.chip { background: color-mix(in oklab, var(--bg) 10%, …) }
```
**Тема определяет палитру.** Выводится детерминистически — `dark → ochre` (тёплый золотой акцент), `light → electric` (холодный синий акцент). На `` пишется и `data-theme`, и `data-palette`. CSS делает свап через attribute-селекторы; нулевое JS-ветвление per-element.
**Token-файл + CSS-модули, без Tailwind.** `tokens.css` (153 LOC) — единый источник правды для цветов / spacing / type / radii. `styles.css` теперь маленький entry-файл, который импортирует `base`, `home`, `media`, `case-study` и `runtime`. Token-система делает то, что делал бы Tailwind config; component-селекторы делают то, что делали бы utility-классы.
**oklch + color-mix для бэкдропов.** Активно используется `color-mix(in oklab, var(--bg) X%, transparent)` для тонированных оверлеев — frosted Hero-чипсы, case-study glow, бэкдропы диаграмм. Избегает opacity-трюков на неправильном цвете.
### Loader + фазы появления интерфейса
```text
T=0 T=400 ms T=900 ms
│ │ │
▼ ▼ ▼
pulse → rush → gone
hairline два беловатых blurred loader размонтирован
opacity pulses бегут с 15% / 85% hero/about появляются
0.22-0.7 к центру (600 ms) (с staggered)
в T+400 ms фазы rush:
html.is-loading удаляется (был выставлен синхронно inline