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
@@ -66,8 +66,8 @@ describe('SchemaDrivenForm', () => {
onCancel={() => {}}
/>,
)
// @nstart/ui Tabs использует role="button" не "tab".
await user.click(screen.getByRole('button', { name: /Дополнительно/i }))
// Radix Tabs (Stage 3.9) использует role="tab" — semantically correct.
await user.click(screen.getByRole('tab', { name: /Дополнительно/i }))
// description (optional) теперь виден. i18n field.description=Описание.
expect(screen.getByText('Описание')).toBeInTheDocument()
})
@@ -775,7 +775,7 @@ const ReferenceSelectField = ({
return records
.map((r) => buildOption(r, refField, defaultLocale))
.filter((o): o is SelectOption => o !== null)
.sort((a, b) => a.label.localeCompare(b.label, 'ru'))
.sort((a, b) => String(a.label ?? '').localeCompare(String(b.label ?? ''), 'ru'))
}, [records, refField, defaultLocale])
const hint = schema.description ?? `FK на ${ref}`
@@ -832,7 +832,7 @@ const ReferenceSelectField = ({
'w-full appearance-none border rounded-sm pl-3 pr-10 py-2 text-body font-display',
'bg-white text-black transition-colors',
'focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent',
errorMsg ? 'border-mars' : 'border-line hover:border-carbon/40',
errorMsg ? 'border-mars' : 'border-line hover:border-ink/40',
isLoading ? 'cursor-wait text-mute' : 'cursor-pointer',
].join(' ')}
>
@@ -166,7 +166,7 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
'px-3 py-1.5 text-cell font-display rounded-sm border transition-colors',
active
? 'border-accent bg-accent text-white'
: 'border-line bg-white text-ink hover:border-carbon/40',
: 'border-line bg-white text-ink hover:border-ink/40',
].join(' ')}
>
{label}
@@ -192,7 +192,7 @@ export const AoiPickerDialog = ({ isOpen, onClose, onApply, initial }: Props) =>
type="button"
onClick={handleClear}
disabled={!shape}
className="px-3 py-1.5 text-cell font-display rounded-sm border border-line bg-white text-ink hover:border-carbon/40 disabled:opacity-40 disabled:cursor-not-allowed"
className="px-3 py-1.5 text-cell font-display rounded-sm border border-line bg-white text-ink hover:border-ink/40 disabled:opacity-40 disabled:cursor-not-allowed"
>
{t('aoi.dialog.clear')}
</button>
@@ -61,7 +61,7 @@ const NeighborCard = ({
<div className={`flex items-baseline gap-2 ${side === 'left' ? 'justify-end' : ''}`}>
<span
className={`inline-block size-1.5 rounded-full shrink-0 ${
SCOPE_STRIP[dict.scope] ?? 'bg-carbon/40'
SCOPE_STRIP[dict.scope] ?? 'bg-ink/40'
}`}
aria-hidden="true"
/>
@@ -67,9 +67,9 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
isLatest && status === 'active'
? 'bg-accent border-accent'
: status === 'future'
? 'bg-orbit border-horizon'
? 'bg-warn border-horizon'
: status === 'expired'
? 'bg-white border-carbon/40'
? 'bg-white border-ink/40'
: 'bg-aurora border-aurora'
}`}
/>
@@ -306,7 +306,7 @@ function TimelineTrack({
{tooltipText && hover !== null && (
<div
className="text-mono absolute -top-2 -translate-y-full px-2 py-0.5 rounded bg-carbon text-white pointer-events-none whitespace-nowrap"
className="text-mono absolute -top-2 -translate-y-full px-2 py-0.5 rounded bg-ink text-white pointer-events-none whitespace-nowrap"
style={{ left: timeToX(hover), transform: 'translate(-50%, -100%)' }}
>
{tooltipText}