# Сайт-портфолио `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