feat(ui): drop @nstart/ui dependency (Stage 3.9)

Все font cascade bugs из последних MR были вызваны @nstart/ui pollution
(Tektur inheritance от Card parent'ов, Onest font в Button, font-primary
class overrides). Решили обрезать раньше plan'а.

What changed:

New primitives (8 файлов в src/ui/components/):
  - panel.tsx           — container с border/surface
  - field.tsx           — FieldLabel/Hint/Error typography helpers
  - text-input.tsx      — Input + label/hint/error composite
  - table.tsx           — Table/TableHeader/Body/Row/HeaderCell/Cell/Empty
  - date-picker.tsx     — native <input type=date> wrapper с label/hint/error
  - multi-select.tsx    — Radix Popover + inline checkboxes
  - single-select.tsx   — native <select> с label/hint/error chrome
  - language-switch.tsx — segmented control в Tektur uppercase

Compatibility shims (existing components extended):
  - checkbox.tsx        — + label/description/error/onChange (nstart-compat
                          synthetic event), wrap в <label> когда есть chrome
  - modal.tsx           — + panelClassName / bodyClassName props
  - form-actions.tsx    — + align prop (start/end/between)
  - tabs-simple.tsx     — TabItem.count alias for .trailing (nstart-compat)
  - table TableHead     — alias TableHeaderCell с align prop

Barrel rewrite (src/ui/index.ts):
  - Удалён 'export * from @nstart/ui'
  - Explicit exports для всех 45+ нужных символов
  - SelectOption / MultiSelectOption / LanguageOption теперь {id, label}
    с optional 'value' alias — nstart-compat без массового codemod call-sites

styles.css cleanup:
  - Removed @import @nstart/ui/styles/{fonts,theme}.css
  - Removed @source nstart/dist directive (Tailwind v4 scan)
  - Removed --text-sm: 13px override (был костыль для @nstart/ui dist)
  - Legacy color tokens (--color-carbon/ultramarain/orbit/regolith/asteroid)
    sсодержали fallback mappings — оставлены, но callsites больше не используют

Codemod: 9 файлов конвертированы legacy colors → handoff tokens
  carbon → ink, ultramarain → accent, orbit → warn, regolith → line, asteroid → mute

main.tsx:
  - NStartUiProvider → TooltipProvider (Radix)
  - DatePickerProvider убран (наш DatePicker native, no provider needed)

routes/dictionaries.$name.tsx:
  - Removed useRef-based <input>.indeterminate hack
  - Radix Checkbox принимает checked='indeterminate' declaratively

package.json: -@nstart/ui (was 0.1.3)

Bundle impact:
  before: vendor-nstart-ui chunk = 1228 KB
  after:  no nstart chunk, всё в vendor-misc (+~70 KB Radix wrappers)
  net savings: ~1.15 MB minified, ~440 KB gzip

Tests: 116 pass (1 fixed: tab role from 'button' → 'tab', Radix semantically correct).
TS strict: clean.
Browser-verified (preview): 7 utilities computed correctly, nstart bundle absent.
This commit is contained in:
Zimin A.N.
2026-05-11 15:17:37 +03:00
parent f0dbc20f62
commit 5007e0f4eb
27 changed files with 916 additions and 191 deletions
+1 -1
View File
@@ -309,7 +309,7 @@ function ActiveFilterChips({ filters, onRemove }: ActiveFilterChipsProps) {
onClick={() => onRemove(f.key)}
className={[
'inline-flex items-center gap-1.5 px-2 py-1 rounded-sm border text-cell',
'bg-white border-line hover:border-carbon/40 hover:bg-line/30',
'bg-white border-line hover:border-ink/40 hover:bg-line/30',
'transition-colors group',
].join(' ')}
title={t('audit.filter.removeChip')}
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import axios from 'axios'
@@ -322,18 +322,12 @@ function DictionaryDetail() {
const someVisibleSelected =
!allVisibleSelected && visibleKeys.some((k) => selection.has(k))
// Native checkbox indeterminate state — sync через useEffect, не callback ref.
// Callback ref срабатывает только на mount/unmount → state становится stale
// когда user toggle'ает selection per-row. useEffect re-runs на изменение
// someVisibleSelected. См. design handoff review #17 + Checkbox в @nstart/ui:
// ref forwarded к нативному <input>, но `indeterminate` это JS DOM-only prop
// (не HTML attribute), нужен manual sync.
const selectAllRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (selectAllRef.current) {
selectAllRef.current.indeterminate = someVisibleSelected
}
}, [someVisibleSelected])
// Stage 3.9: Radix Checkbox принимает {@code checked="indeterminate"} как
// tri-state значение declaratively. Старый useEffect+ref hack (для нативного
// input.indeterminate JS-only prop) больше не нужен.
const selectAllChecked: boolean | 'indeterminate' = someVisibleSelected
? 'indeterminate'
: allVisibleSelected
const toggleSelect = (key: string) => {
setSelection((prev) => {
@@ -723,7 +717,7 @@ function DictionaryDetail() {
'text-cap inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border transition-colors',
active
? 'border-accent bg-accent/8 text-accent'
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
: 'border-line bg-white text-ink-2 hover:border-ink/40',
].join(' ')}
>
<span
@@ -773,8 +767,7 @@ function DictionaryDetail() {
<TableHeaderCell>
<Checkbox
aria-label={t('dict.bulk.selectAll')}
checked={allVisibleSelected}
ref={selectAllRef}
checked={selectAllChecked}
onChange={toggleSelectAll}
/>
</TableHeaderCell>
@@ -834,7 +827,7 @@ function DictionaryDetail() {
'inline-block px-1.5 py-0.5 rounded-sm font-mono text-cell',
c.isFk
? 'bg-accent/8 text-accent'
: 'bg-carbon/8 text-ink',
: 'bg-ink/8 text-ink',
].join(' ')}
>
{text}
+2 -2
View File
@@ -235,7 +235,7 @@ function WebhookDetailPage() {
'text-cap inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm border transition-colors',
active
? 'border-accent bg-accent/8 text-accent'
: 'border-line bg-white text-ink-2 hover:border-carbon/40',
: 'border-line bg-white text-ink-2 hover:border-ink/40',
].join(' ')}
>
{s}
@@ -452,7 +452,7 @@ function RotateRevealModal({
<Alert variant="warning" title={t('webhooks.secret.warningTitle')}>
{t('webhooks.secret.rotatedWarning')}
</Alert>
<div className="bg-carbon/5 border border-line rounded-md p-3">
<div className="bg-ink/5 border border-line rounded-md p-3">
<code className="text-mono break-all">{secret}</code>
</div>
<div className="flex justify-end">
@@ -367,7 +367,7 @@ function SecretRevealModal({
<Alert variant="warning" title={t('webhooks.secret.warningTitle')}>
{t('webhooks.secret.warning')}
</Alert>
<div className="bg-carbon/5 border border-line rounded-md p-3">
<div className="bg-ink/5 border border-line rounded-md p-3">
<code className="text-mono break-all">{revealed.secret}</code>
</div>
<div className="flex justify-end gap-2">