Реальные измерения через DevTools на staging выявили 3 бага после MR !47:
Bug 1: text-title-md/lg/xl inherited Tektur от @nstart/ui Card parent.
Handoff: 'Inter for all headings'. Computed: Tektur.
Fix: добавил font-family: var(--font-sans) explicit на text-title-*
и text-body/text-cell. После Stage 3.9 (nstart gone) можно убрать —
body inherit будет достаточно.
Bug 2: text-cap + font-mono cohabit (catalog scope chips, time-travel
presets) → Tailwind v4 cascade order: font-mono идёт после text-cap
в bundled CSS → JetBrains Mono перебивает Tektur. text-cap baked
font-display, font-mono token redundant.
Fix: codemod strip font-mono где есть text-cap (4 sites). Computed
font-family теперь правильно Tektur.
Bug 3: @nstart/ui PageHeader рендерил h1 'DICTIONARIES' в Tektur
36px/700 uppercase (font-primary text-4xl). Handoff page title:
17px/600 Inter normal case. Наш src/ui/components/page-header.tsx
написан но не экспортирован через barrel — все routes резолвили
PageHeader к nstart версии.
Fix: добавил 'export { PageHeader }' override в src/ui/index.ts.
Все 7 PageHeader usages (catalog/search/reviews/webhooks/outbox/
my-drafts/graph) теперь получат handoff-conformant heading.
Bug 4 (small): catalog counter '37/37' использовал text-cap но это
numeric value — handoff role text-mono (11px JetBrains Mono).
Fix: text-cap → text-mono + tabular-nums для grid alignment.
Verified via injected test elements + CDP computed styles:
text-title-xl: 22/600 Inter
text-title-lg: 17/600 Inter
text-title-md: 15/600 Inter
text-body: 13/400 Inter
text-cell: 12.5/500 Inter
text-mono: 11/500 JetBrains Mono
text-cap: 10.5/600 Tektur
Все 7 utilities точно matches handoff scale.
Files: 4 modified. TS strict + 116 tests pass.
Перенесли scope-визуальные константы (SCOPE_ORDER, SCOPE_DOT,
SCOPE_ACCENT) в `lib/scope-style.ts` для переиспользования. На detail
странице (`dictionaries/$name`) добавили top accent strip 4px цветом
scope + цветной чип в breadcrumb рядом с back-link. Теперь при переходе
из списка в карточку scope сразу виден без чтения badge в таблице.
40 справочников в плоском grid тяжело сканировать. Разделили на секции
PUBLIC / INTERNAL / RESTRICTED с цветными accent-полосами слева у карточек
(emerald / amber / rose). Search фильтрует across all sections, пустые
секции скрыты. Цвет даёт instant scan, секции — explicit разделение.
Backend:
- DictionaryRecordRepository.countActiveGroupedByDictionary — один GROUP BY
по всем справочникам с фильтром по scope, без N+1.
- DictionaryResponse.recordCount (nullable) + factory from(d, count).
- DictionaryDefinitionController передаёт счётчики в list/get,
фильтрованные по currentScopes() пользователя.
Frontend:
- SearchInput над сеткой карточек, client-side filter по
name/displayName/description (case-insensitive, useDeferredValue).
- Badge "N записей" (i18n plural ru/en) в углу карточки.
- "Показано N из M" рядом с поиском.
- EmptyState когда поиск пустой.
Свои Modal/Button/inputs выкинуты — берём nstart-ui v0.1.3 как в других corp
React приложениях. Установка через .npmrc → repo.nstart.cloud/repository/npm-hosted/.
Что сделано:
- .npmrc: scope @nstart на corp Nexus + min-release-age=7
- deps: @nstart/ui ^0.1.3, @phosphor-icons/react ^2.1.10
- bumped peer deps: i18next ^26 + react-i18next ^17 (требование nstart-ui)
- styles.css: импорт @nstart/ui/styles/{fonts,theme}.css + critical @source
директива (без неё Tailwind v4 tree-shaking дропнул бы все классы из dist либы)
- main.tsx: <NStartUiProvider> на корне
- DROP src/components/ui/Modal.tsx (свой Modal больше не нужен)
Routes:
- __root: LanguageSwitch вместо двух кнопок RU/EN, токены ultramarain/regolith/carbon
- dictionaries.index: PageHeader + Badge для scope chip + Alert/EmptyState/LoadingBlock
- dictionaries.$name: PageHeader с actions, Modal/Panel/Table/TableRow/Cell,
IconButton с Phosphor (PencilSimple, XCircle), Button variant=primary/danger/ghost
Form (SchemaDrivenForm):
- TextInput/TextArea/SingleSelect/Checkbox + FieldLabel/Hint/Error из @nstart/ui
- FormSection + FormGrid columns=1|2 для группировки
- FormActions align=end с Button loading state
- Alert variant=error для server-side ошибок (422 IdempotencyConflict / 409 InProgress)
- ajv валидация по schema_json остаётся такой же (Draft-07, как у backend)
Bundle: 192KB → 659KB gzip — тянет echarts/lottie/brand assets из nstart-ui,
для internal admin tool приемлемо (cached after first load).
После regen routeTree.gen.ts /dictionaries/$name стал child route /dictionaries.
dictionaries.tsx рендерил карточки без <Outlet/> — клик по карточке менял URL,
но child компонент негде было рендерить, поэтому видна та же страница со списком.
Разнёс по конвенции:
- dictionaries.tsx — layout с <Outlet/>
- dictionaries.index.tsx — карточки списка (matches /dictionaries)
- dictionaries.$name.tsx — без изменений (matches /dictionaries/{name})