diff --git a/ordinis-admin-ui/package.json b/ordinis-admin-ui/package.json index 48bd945..98f9de4 100644 --- a/ordinis-admin-ui/package.json +++ b/ordinis-admin-ui/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@hookform/resolvers": "^3.9.1", - "@nstart/ui": "^0.1.3", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", diff --git a/ordinis-admin-ui/pnpm-lock.yaml b/ordinis-admin-ui/pnpm-lock.yaml index 206794d..7683a0f 100644 --- a/ordinis-admin-ui/pnpm-lock.yaml +++ b/ordinis-admin-ui/pnpm-lock.yaml @@ -11,9 +11,6 @@ importers: '@hookform/resolvers': specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.75.0(react@19.2.5)) - '@nstart/ui': - specifier: ^0.1.3 - version: 0.1.3(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react-i18next@17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(react@19.2.5) '@phosphor-icons/react': specifier: ^2.1.10 version: 2.1.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -698,15 +695,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nstart/ui@0.1.3': - resolution: {integrity: sha512-dLhM2Uzvok1UB+86c+WIYz/1uYr/wN3Ch1Ab3ioZbzxBEt0bZQs+QYkj3GXAEzffCjV4zTHen31yJUHzJmpyTQ==} - engines: {node: ^24.0.0 || ^22.0.0} - peerDependencies: - i18next: ^26.0.3 - react: ^19.0.0 - react-dom: ^19.0.0 - react-i18next: ^17.0.2 - '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} @@ -2055,9 +2043,6 @@ packages: duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} - echarts@6.0.0: - resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} - electron-to-chromium@1.5.349: resolution: {integrity: sha512-QsWVGyRuY07Aqb234QytTfwd5d9AJlfNIQ5wIOl1L+PZDzI9d9+Fn0FRale/QYlFxt/bUnB0/nLd1jFPGxGK1A==} @@ -2321,9 +2306,6 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hls.js@1.6.16: - resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} - hook-std@4.0.0: resolution: {integrity: sha512-IHI4bEVOt3vRUDJ+bFA9VUJlo7SzvFARPNLw75pqSmAOP2HmTWfFJtPvLBrDrlgjEYXY9zs7SFdHPQaJShkSCQ==} engines: {node: '>=20'} @@ -2647,15 +2629,6 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - lottie-react@2.4.1: - resolution: {integrity: sha512-LQrH7jlkigIIv++wIyrOYFLHSKQpEY4zehPicL9bQsrt1rnoKRYCYgpCUe5maqylNtacy58/sQDZTkwMcTRxZw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - lottie-web@5.13.0: - resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==} - loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -3742,9 +3715,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zrender@6.0.0: - resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} - snapshots: '@adobe/css-tools@4.4.4': {} @@ -4127,17 +4097,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nstart/ui@0.1.3(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react-i18next@17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(react@19.2.5)': - dependencies: - '@phosphor-icons/react': 2.1.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - echarts: 6.0.0 - hls.js: 1.6.16 - i18next: 26.0.8(typescript@5.9.3) - lottie-react: 2.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - react-i18next: 17.0.6(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) - '@octokit/auth-token@6.0.0': {} '@octokit/core@7.0.6': @@ -5481,11 +5440,6 @@ snapshots: dependencies: readable-stream: 2.3.8 - echarts@6.0.0: - dependencies: - tslib: 2.3.0 - zrender: 6.0.0 - electron-to-chromium@1.5.349: {} emoji-regex@8.0.0: {} @@ -5808,8 +5762,6 @@ snapshots: highlight.js@10.7.3: {} - hls.js@1.6.16: {} - hook-std@4.0.0: {} hosted-git-info@7.0.2: @@ -6076,14 +6028,6 @@ snapshots: lodash@4.18.1: {} - lottie-react@2.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): - dependencies: - lottie-web: 5.13.0 - react: 19.2.5 - react-dom: 19.2.5(react@19.2.5) - - lottie-web@5.13.0: {} - loupe@3.2.1: {} lowercase-keys@3.0.0: {} @@ -6976,7 +6920,3 @@ snapshots: yoctocolors@2.1.2: {} zod@3.25.76: {} - - zrender@6.0.0: - dependencies: - tslib: 2.3.0 diff --git a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx index 676cc60..7d42d5a 100644 --- a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx +++ b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.test.tsx @@ -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() }) diff --git a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx index 78f7da4..d114e3d 100644 --- a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx +++ b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx @@ -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(' ')} > diff --git a/ordinis-admin-ui/src/components/geo/AoiPickerDialog.tsx b/ordinis-admin-ui/src/components/geo/AoiPickerDialog.tsx index bb3e57d..80f67cb 100644 --- a/ordinis-admin-ui/src/components/geo/AoiPickerDialog.tsx +++ b/ordinis-admin-ui/src/components/geo/AoiPickerDialog.tsx @@ -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')} diff --git a/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx b/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx index ce20cf4..3478965 100644 --- a/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx +++ b/ordinis-admin-ui/src/components/lineage/DictionaryHubView.tsx @@ -61,7 +61,7 @@ const NeighborCard = ({
{secret}
{revealed.secret}
Поддерживает три формы: + *
onChange синтезирует {@code ChangeEvent Сейчас простой <input type="date"> — браузерный picker (CSS hard
+ * styling limited, но functional). Phase 4 Stage 3.8b: заменить на
+ * react-day-picker если потребуется branded look или date range.
+ *
+ * Format: ISO yyyy-mm-dd. onChange получает Date|null.
+ */
+
+export type DatePickerProps = {
+ label?: React.ReactNode
+ hint?: React.ReactNode
+ error?: React.ReactNode
+ required?: boolean
+ value?: Date | string | null
+ /** Receives Date or null on clear. */
+ onChange?: (date: Date | null) => void
+ /** Min / max bounds (ISO string yyyy-mm-dd or Date). */
+ min?: Date | string
+ max?: Date | string
+ id?: string
+ disabled?: boolean
+ /** Wrapper className. */
+ rootClassName?: string
+ inputClassName?: string
+}
+
+const toIso = (v: Date | string | null | undefined): string => {
+ if (!v) return ''
+ if (typeof v === 'string') return v.slice(0, 10)
+ if (v instanceof Date && !isNaN(v.getTime())) return v.toISOString().slice(0, 10)
+ return ''
+}
+
+export function DatePicker({
+ label,
+ hint,
+ error,
+ required,
+ value,
+ onChange,
+ min,
+ max,
+ id,
+ disabled,
+ rootClassName,
+ inputClassName,
+}: DatePickerProps) {
+ const autoId = React.useId()
+ const inputId = id ?? autoId
+ const hintId = hint ? `${inputId}-hint` : undefined
+ const errorId = error ? `${inputId}-error` : undefined
+
+ const handleChange = (e: React.ChangeEvent Per handoff: label = text-body (13/400), hint/error = text-cell (12.5).
+ */
+
+export type FieldLabelProps = React.LabelHTMLAttributes Trigger показывает count selected + первые 2 selected labels.
+ * Popover — список с checkboxes. Click toggle. Поиск НЕ implemented (handoff
+ * MultiSelect не требует) — для search-heavy lists используй cmdk Combobox
+ * (Stage 3.8b TODO).
+ *
+ * API: options [{value, label}], value string[], onChange (ids: string[]).
+ */
+
+/** nstart-compat: {id, label}. Use `id` key. */
+export type MultiSelectOption = {
+ id: string
+ label: React.ReactNode
+ /** Alias of `id`. */
+ value?: string
+}
+
+export type MultiSelectProps = {
+ label?: React.ReactNode
+ hint?: React.ReactNode
+ error?: React.ReactNode
+ required?: boolean
+ options: MultiSelectOption[]
+ value: string[]
+ onChange: (next: string[]) => void
+ placeholder?: string
+ disabled?: boolean
+ /** Wrapper className. */
+ rootClassName?: string
+ /** Compact display (no badges, just count). */
+ compact?: boolean
+}
+
+export function MultiSelect({
+ label,
+ hint,
+ error,
+ required,
+ options,
+ value,
+ onChange,
+ placeholder,
+ disabled,
+ rootClassName,
+ compact,
+}: MultiSelectProps) {
+ const autoId = React.useId()
+ const triggerId = autoId
+ const hintId = hint ? `${triggerId}-hint` : undefined
+ const errorId = error ? `${triggerId}-error` : undefined
+
+ const valueSet = React.useMemo(() => new Set(value), [value])
+ const toggle = (id: string) => {
+ const next = new Set(valueSet)
+ if (next.has(id)) next.delete(id)
+ else next.add(id)
+ onChange(Array.from(next))
+ }
+
+ const selectedLabels = options
+ .filter((o) => valueSet.has(o.id ?? o.value ?? ''))
+ .map((o) => o.label)
+
+ return (
+ Native select работает на mobile out-of-box, screen readers OK, по
+ * Enter/space открывается. Custom dropdown UI можно сделать через Radix
+ * Select (Stage 3.8b refinement) если потребуется branded look.
+ */
+
+/** nstart-compat shape: {id, label}. New code may prefer the more standard
+ * {value, label} — both keys accepted (id wins if both). */
+export type SelectOption = {
+ id: string
+ label: React.ReactNode
+ /** Alias of `id` для совместимости с HTML option convention. */
+ value?: string
+}
+
+export type SingleSelectProps = {
+ label?: React.ReactNode
+ hint?: React.ReactNode
+ error?: React.ReactNode
+ required?: boolean
+ options: ReadonlyArray Per handoff: table cell text = text-cell (12.5/500). Применяется на
+ * <td/th> через CSS rule в ` TableEmpty — single full-width row для empty state.
+ */
+
+export const Table = React.forwardRef Стратегия миграции (Phase 2 stack migration):
- * Stage 3.9: @nstart/ui удалён. Все экспорты резолвят к локальным
+ * shadcn primitives в `./components/`. Имена сохранены в nstart-compat форме
+ * чтобы существующий код работал без массового рефактора imports.
*
- * Текущая семантика: import {X} from '@/ui' — Х берётся приоритетно из
- * наших shadcn primitives (overrides ниже), иначе passthrough из @nstart/ui.
- * Этот файл — single source of truth для UI imports.
+ * Группы:
+ *
+ {options.map((opt) => {
+ const checked = valueSet.has((opt.id ?? opt.value ?? ''))
+ return (
+
+
+
+ )
+ },
+)
+
+export type TableHeaderCellProps = React.ThHTMLAttributes
+ )
+ },
+)
+// Alias for shadcn convention.
+export const TableHead = TableHeaderCell
+
+export const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+ )
+})
+
+export const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+
+ )
+}
diff --git a/ordinis-admin-ui/src/ui/components/tabs-simple.tsx b/ordinis-admin-ui/src/ui/components/tabs-simple.tsx
index 694b8b2..3077efb 100644
--- a/ordinis-admin-ui/src/ui/components/tabs-simple.tsx
+++ b/ordinis-admin-ui/src/ui/components/tabs-simple.tsx
@@ -24,8 +24,10 @@ export type TabItem = {
id: string
label: React.ReactNode
disabled?: boolean
- /** Right-aligned content в tab (badge, indicator). */
+ /** Right-aligned content в tab (badge, indicator). Alias `trailing`. */
trailing?: React.ReactNode
+ /** nstart-compat alias для `trailing`. Если оба заданы — count wins. */
+ count?: React.ReactNode
}
export type TabsSimpleProps = {
@@ -43,7 +45,7 @@ export function TabsSimple({ items, value, onValueChange, className }: TabsSimpl
+ {children}
+
+
- *
+ *
+ *
*/
-// === Полная re-export из @nstart/ui — все компоненты, hooks, locale const ===
-// Дальше будут selective overrides где наши shadcn primitives лучше.
-export * from '@nstart/ui'
-
-// === Overrides: наши shadcn primitives с Tailwind v4 tokens ===
-// Эти именованные re-export'ы перекрывают одноимённые из @nstart/ui выше
-// (TS resolves last declaration → наши берут верх). Mostly cosmetic — общий
-// API design сохранён, добавлены теневые props (loading, error-variant и т.д.).
+// === Atomic ===
export { Button, buttonVariants, type ButtonProps } from './components/button'
export { Badge, type BadgeProps } from './components/badge'
export { Alert, type AlertProps } from './components/alert'
-// TextArea override — shadcn с handoff tokens. Phase 3 migration step
-// (Stage 3.8a). API совместим с @nstart/ui TextArea (label/hint/error).
-export { TextArea, type TextAreaProps } from './components/textarea'
-// PageHeader override — @nstart/ui рендерил h1 в Tektur 36px/700 (text-4xl
-// uppercase tracking-tight font-primary), что не соответствует handoff
-// page title 17/600 Inter. Наша версия использует text-title-lg utility.
-export { PageHeader } from './components/page-header'
+export { IconButton, type IconButtonProps } from './components/icon-button'
+export { Input, type InputProps } from './components/input'
+export { Label } from './components/label'
+export { Separator } from './components/separator'
+export { Skeleton } from './components/skeleton'
+export { Switch } from './components/switch'
-// === Handoff design system primitives — новые компоненты без nstart-аналога ===
+// === Handoff design primitives ===
export { Cap, type CapProps } from './components/cap'
export {
ScopeDot,
@@ -52,23 +41,72 @@ export {
type DataScope as ScopeKind,
} from './components/scope'
-// === shadcn primitives для НОВОГО кода (Phase 3) ===
-// Эти именованные exports НЕ конфликтуют с nstart (имена не пересекаются).
-// Используются в редизайне catalog/editor/drawer per handoff README.
-export { IconButton as ShadcnIconButton, type IconButtonProps } from './components/icon-button'
-export { Input as ShadcnInput, type InputProps as ShadcnInputProps } from './components/input'
-export { Label as ShadcnLabel } from './components/label'
-export { SearchInput as ShadcnSearchInput, type SearchInputProps as ShadcnSearchInputProps } from './components/search-input'
+// === Form fields ===
+export { TextInput, type TextInputProps } from './components/text-input'
+export { TextArea, type TextAreaProps } from './components/textarea'
+export { Checkbox } from './components/checkbox'
export {
- Card as ShadcnCard,
+ FieldLabel,
+ FieldHint,
+ FieldError,
+ type FieldLabelProps,
+} from './components/field'
+export { FormActions, type FormActionsProps } from './components/form-actions'
+export { DatePicker, type DatePickerProps } from './components/date-picker'
+export {
+ MultiSelect,
+ type MultiSelectOption,
+ type MultiSelectProps,
+} from './components/multi-select'
+export {
+ SingleSelect,
+ type SelectOption,
+ type SingleSelectProps,
+} from './components/single-select'
+export { SearchInput, type SearchInputProps } from './components/search-input'
+
+// === Layout ===
+export {
+ Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from './components/card'
-export { Separator } from './components/separator'
-export { Skeleton } from './components/skeleton'
+export { Panel, type PanelProps } from './components/panel'
+export { PageHeader, type PageHeaderProps } from './components/page-header'
+export { EmptyState, type EmptyStateProps } from './components/empty-state'
+export { LoadingBlock, type LoadingBlockProps } from './components/loading-block'
+
+// === Tables ===
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableRow,
+ TableHead,
+ TableHeaderCell,
+ TableCell,
+ TableCaption,
+ TableEmpty,
+ type TableHeaderCellProps,
+ type TableEmptyProps,
+} from './components/table'
+
+// === Tabs (legacy items[] API + Radix primitives для нового кода) ===
+export { TabsSimple as Tabs, type TabItem, type TabsSimpleProps } from './components/tabs-simple'
+export {
+ Tabs as TabsRoot,
+ TabsList,
+ TabsTrigger,
+ TabsContent,
+} from './components/tabs'
+
+// === Overlays ===
+export { Modal, type ModalProps } from './components/modal'
+export { Drawer, type DrawerProps } from './components/drawer'
export {
Dialog,
DialogTrigger,
@@ -106,13 +144,6 @@ export {
TooltipTrigger,
TooltipContent,
} from './components/tooltip'
-// Tabs из shadcn — composition API.
-export {
- Tabs as ShadcnTabsRoot,
- TabsList as ShadcnTabsList,
- TabsTrigger as ShadcnTabsTrigger,
- TabsContent as ShadcnTabsContent,
-} from './components/tabs'
export {
DropdownMenu,
DropdownMenuTrigger,
@@ -129,9 +160,13 @@ export {
DropdownMenuSubContent,
DropdownMenuRadioGroup,
} from './components/dropdown-menu'
-export { Switch as ShadcnSwitch } from './components/switch'
-// Toast notifications через sonner (rich, accessible, themable).
-// Mount