Merge branch 'feat/v1.1.8-ui-codemod' into 'main'
feat(admin-ui): codemod @nstart/ui → @/ui (Stage 2.2) See merge request 2-6/2-6-4/terravault/ordinis!33
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useAuth } from 'react-oidc-context'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, IconButton } from '@nstart/ui'
|
||||
import { Button, IconButton } from '@/ui'
|
||||
import { SignInIcon, SignOutIcon, UserIcon } from '@phosphor-icons/react'
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
TextInput,
|
||||
type SelectOption,
|
||||
type TabItem,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import type { CreateRecordRequest, FlattenedRecord, JsonSchema } from '@/api/client'
|
||||
import { useReferencedRecords } from '@/api/queries'
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Button, FormActions, Modal } from '@nstart/ui'
|
||||
import { Alert, Button, FormActions, Modal } from '@/ui'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Badge, Button, LoadingBlock, Modal, TextInput } from '@nstart/ui'
|
||||
import { Alert, Badge, Button, LoadingBlock, Modal, TextInput } from '@/ui'
|
||||
import { WarningIcon, XCircleIcon } from '@phosphor-icons/react'
|
||||
import { useCascadePreview } from '@/api/queries'
|
||||
import { useCascadeCloseRecord } from '@/api/mutations'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Badge, Panel } from '@nstart/ui'
|
||||
import { Badge, Panel } from '@/ui'
|
||||
import { ArrowRightIcon } from '@phosphor-icons/react'
|
||||
import { useDictionaryDependents } from '@/api/queries'
|
||||
import type { OnCloseAction } from '@/api/client'
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Badge } from '@nstart/ui'
|
||||
import { Badge } from '@/ui'
|
||||
import { useDictionaryDependents, useDictionaries } from '@/api/queries'
|
||||
import type { DictionaryDetail, DictionaryDefinition, JsonSchema } from '@/api/client'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Alert, Badge, Button, LoadingBlock } from '@nstart/ui'
|
||||
import { Alert, Badge, Button, LoadingBlock } from '@/ui'
|
||||
import { CaretLeftIcon, CaretRightIcon, ArrowSquareOutIcon } from '@phosphor-icons/react'
|
||||
import { useRecordDependents } from '@/api/queries'
|
||||
import type { OnCloseAction } from '@/api/client'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Badge, Drawer, LoadingBlock } from '@nstart/ui'
|
||||
import { Alert, Badge, Drawer, LoadingBlock } from '@/ui'
|
||||
import { useRecordHistory } from '@/api/queries'
|
||||
import { RecordDependentsPanel } from '@/components/lineage/RecordDependentsPanel'
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TextInput,
|
||||
type SelectOption,
|
||||
type TabItem,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import { useCreateDictionary, useUpdateDictionary } from '@/api/mutations'
|
||||
import { dictionaryDetailQuery, useDictionaries } from '@/api/queries'
|
||||
import type { CreateDictionaryRequest, DataScope, DictionaryDetail } from '@/api/client'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Badge, Tabs, type TabItem } from '@nstart/ui'
|
||||
import { Badge, Tabs, type TabItem } from '@/ui'
|
||||
import { generateEventPreviews } from './eventPreview'
|
||||
import type { DataScope, JsonSchema } from '@/api/client'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TextInput,
|
||||
TextArea,
|
||||
type SelectOption,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import { TrashIcon, ArrowUpIcon, ArrowDownIcon } from '@phosphor-icons/react'
|
||||
import type { PropertyDef, PropertyKind } from './types'
|
||||
import { PROPERTY_KINDS } from './types'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, EmptyState } from '@nstart/ui'
|
||||
import { Button, EmptyState } from '@/ui'
|
||||
import { PlusIcon } from '@phosphor-icons/react'
|
||||
import { PropertyEditor } from './PropertyEditor'
|
||||
import { newPropertyDef, type PropertyDef } from './types'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@nstart/ui'
|
||||
import { Button } from '@/ui'
|
||||
import { ArrowClockwise, BookOpen, X } from '@phosphor-icons/react'
|
||||
import { useState } from 'react'
|
||||
import { useAppVersion } from './useAppVersion'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
||||
import { NStartUiProvider, DatePickerProvider, DATE_PICKER_LOCALE_RU } from '@nstart/ui'
|
||||
import { NStartUiProvider, DatePickerProvider, DATE_PICKER_LOCALE_RU } from '@/ui'
|
||||
import { AuthProvider } from 'react-oidc-context'
|
||||
import { routeTree } from './routeTree.gen'
|
||||
import { oidcAuthConfig } from '@/auth/oidcConfig'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createRootRouteWithContext, Link, Outlet } from '@tanstack/react-router'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LanguageSwitch } from '@nstart/ui'
|
||||
import { LanguageSwitch } from '@/ui'
|
||||
import { AuthBadge } from '@/auth/AuthBadge'
|
||||
import { useCanMutate } from '@/auth/useCanMutate'
|
||||
import { ThemeSwitch } from '@/components/layout/ThemeSwitch'
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import {
|
||||
ArrowsClockwiseIcon,
|
||||
CaretDownIcon,
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import { PlusIcon, PencilSimpleIcon, XCircleIcon, GearIcon, ClockCounterClockwiseIcon, MapTrifoldIcon, TrashIcon, CheckCircleIcon, WarningIcon, XIcon, DownloadSimpleIcon, CaretLeftIcon, CaretRightIcon } from '@phosphor-icons/react'
|
||||
import {
|
||||
useDictionaryDetail,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { useDeferredValue, useMemo, useState } from 'react'
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader, SearchInput } from '@nstart/ui'
|
||||
import { Alert, Badge, Button, EmptyState, LoadingBlock, PageHeader, SearchInput } from '@/ui'
|
||||
import { PlusIcon } from '@phosphor-icons/react'
|
||||
import { useQueries } from '@tanstack/react-query'
|
||||
import { dictionaryDependentsQuery, useDictionaries, useDictionaryDependents } from '@/api/queries'
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import { useMyDrafts } from '@/api/queries'
|
||||
import { useWithdrawDraft } from '@/api/mutations'
|
||||
import type { DraftOperation, DraftStatus } from '@/api/client'
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import { ArrowsClockwiseIcon } from '@phosphor-icons/react'
|
||||
import { useDlq, useOutboxStats } from '@/api/queries'
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
TableRow,
|
||||
TextArea,
|
||||
TextInput,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import { CheckCircleIcon, XCircleIcon } from '@phosphor-icons/react'
|
||||
import { useDraft, useLiveRecord, useReviewQueue } from '@/api/queries'
|
||||
import { useApproveDraft, useRejectDraft } from '@/api/mutations'
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PageHeader,
|
||||
Panel,
|
||||
SearchInput,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import { useSearch } from '@/api/queries'
|
||||
|
||||
type SearchSearch = { q?: string }
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import {
|
||||
ArrowClockwiseIcon,
|
||||
ArrowsClockwiseIcon,
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
TextArea,
|
||||
TextInput,
|
||||
type SelectOption,
|
||||
} from '@nstart/ui'
|
||||
} from '@/ui'
|
||||
import {
|
||||
CopyIcon,
|
||||
PlusIcon,
|
||||
|
||||
@@ -15,6 +15,8 @@ const alertVariants = cva(
|
||||
success: 'border-l-green bg-green-bg text-ink',
|
||||
warning: 'border-l-warn bg-warn-bg text-ink',
|
||||
danger: 'border-l-pink bg-pink-bg text-ink',
|
||||
// Alias для nstart-compat: error === danger визуально.
|
||||
error: 'border-l-pink bg-pink-bg text-ink',
|
||||
neutral: 'border-l-line bg-surface-2 text-ink',
|
||||
},
|
||||
},
|
||||
@@ -25,14 +27,21 @@ const alertVariants = cva(
|
||||
export type AlertProps = React.HTMLAttributes<HTMLDivElement> &
|
||||
VariantProps<typeof alertVariants> & {
|
||||
title?: React.ReactNode
|
||||
/** Right-aligned slot для action button(s) inside alert. */
|
||||
action?: React.ReactNode
|
||||
}
|
||||
|
||||
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||
function Alert({ className, variant, title, children, ...props }, ref) {
|
||||
function Alert({ className, variant, title, action, children, ...props }, ref) {
|
||||
return (
|
||||
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props}>
|
||||
{title && <div className="font-medium mb-0.5">{title}</div>}
|
||||
<div className="text-ink-2 text-[13px] leading-snug">{children}</div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
{title && <div className="font-medium mb-0.5">{title}</div>}
|
||||
<div className="text-ink-2 text-[13px] leading-snug">{children}</div>
|
||||
</div>
|
||||
{action && <div className="shrink-0 flex items-center gap-2">{action}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -12,6 +12,13 @@ const badgeVariants = cva(
|
||||
success: 'bg-green-bg text-green',
|
||||
warning: 'bg-warn-bg text-warn',
|
||||
danger: 'bg-pink-bg text-pink',
|
||||
// Aliases для nstart-compat: error == danger, neutral == default,
|
||||
// info == accent.
|
||||
error: 'bg-pink-bg text-pink',
|
||||
neutral: 'bg-surface-2 text-ink-2 border border-line',
|
||||
info: 'bg-accent-bg text-accent',
|
||||
// Alias для nstart-compat — старый Badge variant='primary' эквивалентен accent.
|
||||
primary: 'bg-accent text-on-accent',
|
||||
outline: 'border border-line text-ink-2',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -61,21 +61,44 @@ export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
asChild?: boolean
|
||||
leftIcon?: React.ReactNode
|
||||
rightIcon?: React.ReactNode
|
||||
/** Loading state — disables button + replaces leftIcon с inline spinner. */
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function Button(
|
||||
{ className, variant, size, asChild, leftIcon, rightIcon, children, ...props },
|
||||
{
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
loading,
|
||||
disabled,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
const renderedLeftIcon = loading ? (
|
||||
<svg className="animate-spin size-3.5" viewBox="0 0 24 24" aria-hidden>
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" strokeOpacity="0.25" strokeWidth="4" />
|
||||
<path d="M4 12a8 8 0 0 1 8-8" fill="none" stroke="currentColor" strokeWidth="4" strokeLinecap="round" />
|
||||
</svg>
|
||||
) : (
|
||||
leftIcon
|
||||
)
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
disabled={disabled || loading}
|
||||
aria-busy={loading || undefined}
|
||||
{...props}
|
||||
>
|
||||
{leftIcon}
|
||||
{renderedLeftIcon}
|
||||
{children}
|
||||
{rightIcon}
|
||||
</Comp>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from './sheet'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* Drawer — adapter с @nstart/ui-style API над Radix Sheet (slide-over).
|
||||
* Используется существующим кодом для side panels (record drawer, history,
|
||||
* review preview).
|
||||
*
|
||||
* Props mapping:
|
||||
* isOpen → Sheet.open
|
||||
* onClose → Sheet.onOpenChange
|
||||
* title → SheetTitle
|
||||
* side → SheetContent.side (right default)
|
||||
* widthClassName → custom className на SheetContent (override defaults)
|
||||
* size → SheetContent.size shorthand (sm/md/lg/xl)
|
||||
*/
|
||||
|
||||
export type DrawerProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
side?: 'left' | 'right' | 'top' | 'bottom'
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
widthClassName?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function Drawer({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
side = 'right',
|
||||
size,
|
||||
widthClassName,
|
||||
children,
|
||||
}: DrawerProps) {
|
||||
return (
|
||||
<Sheet
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose()
|
||||
}}
|
||||
>
|
||||
<SheetContent
|
||||
side={side}
|
||||
size={size}
|
||||
className={cn('overflow-y-auto', widthClassName)}
|
||||
>
|
||||
{(title || description) && (
|
||||
<SheetHeader>
|
||||
{title && <SheetTitle>{title}</SheetTitle>}
|
||||
{description && <SheetDescription>{description}</SheetDescription>}
|
||||
</SheetHeader>
|
||||
)}
|
||||
{children}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
@@ -4,27 +4,71 @@ import { cn } from '@/lib/utils'
|
||||
/**
|
||||
* Text input — single-line. Использует наши tokens, focus-ring через accent.
|
||||
*
|
||||
* Wrap'итесь в {@code <Label>} выше для accessibility. Для search input'а
|
||||
* с иконкой см. {@link SearchInput}.
|
||||
* Поддерживает 2 режима:
|
||||
* - "bare" (default) — просто <input>, для inline использования в кастомных
|
||||
* layout'ах (см. SearchInput, FormGrid с FieldLabel separately).
|
||||
* - "field" — когда передан label / hint / error, рендерится FormField
|
||||
* обёртка с label сверху + hint/error снизу. Match'ит @nstart/ui TextInput API.
|
||||
*/
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||
/** Если задан — рендерится field-обёртка с label выше. */
|
||||
label?: React.ReactNode
|
||||
/** Optional helper text under input. */
|
||||
hint?: React.ReactNode
|
||||
/** Error message — overrides hint визуально, accent в pink. */
|
||||
error?: React.ReactNode
|
||||
/** Wrapper className когда rendering field-mode. */
|
||||
containerClassName?: string
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
function Input({ className, type = 'text', ...props }, ref) {
|
||||
return (
|
||||
function Input(
|
||||
{ className, type = 'text', label, hint, error, required, containerClassName, id, ...props },
|
||||
ref,
|
||||
) {
|
||||
const autoId = React.useId()
|
||||
const inputId = id ?? autoId
|
||||
const inputEl = (
|
||||
<input
|
||||
id={inputId}
|
||||
ref={ref}
|
||||
type={type}
|
||||
required={required}
|
||||
aria-required={required || undefined}
|
||||
aria-invalid={Boolean(error) || undefined}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-line bg-surface px-3 py-1 text-sm text-ink',
|
||||
'placeholder:text-mute',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-bg',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'file:border-0 file:bg-transparent file:text-sm file:font-medium',
|
||||
error && 'border-pink focus-visible:ring-pink',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!label && !hint && !error) return inputEl
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-1', containerClassName)}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="text-2xs font-medium font-display uppercase tracking-[0.10em] text-mute leading-none"
|
||||
>
|
||||
{label}
|
||||
{required && <span className="text-pink ml-0.5">*</span>}
|
||||
</label>
|
||||
)}
|
||||
{inputEl}
|
||||
{error ? (
|
||||
<span className="text-xs text-pink">{error}</span>
|
||||
) : hint ? (
|
||||
<span className="text-xs text-mute">{hint}</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,23 +4,37 @@ import { cn } from '@/lib/utils'
|
||||
/**
|
||||
* LoadingBlock — central spinner с подписью. Заменяет {@code @nstart/ui}
|
||||
* LoadingBlock. Для inline placeholder используйте Skeleton.
|
||||
*
|
||||
* Size variants:
|
||||
* - sm: small inline spinner (py-4)
|
||||
* - md: default centered (py-12)
|
||||
* - lg: prominent (py-20)
|
||||
*/
|
||||
export type LoadingBlockProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
label?: React.ReactNode
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
export function LoadingBlock({ className, label, ...props }: LoadingBlockProps) {
|
||||
const SIZES = {
|
||||
sm: { wrapper: 'py-4', icon: 'size-3' },
|
||||
md: { wrapper: 'py-12', icon: 'size-5' },
|
||||
lg: { wrapper: 'py-20', icon: 'size-8' },
|
||||
} as const
|
||||
|
||||
export function LoadingBlock({ className, label, size = 'md', ...props }: LoadingBlockProps) {
|
||||
const s = SIZES[size]
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
aria-busy="true"
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-2 py-12 text-mute',
|
||||
'flex flex-col items-center justify-center gap-2 text-mute',
|
||||
s.wrapper,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Loader2 className="size-5 animate-spin text-accent" />
|
||||
<Loader2 className={cn('animate-spin text-accent', s.icon)} />
|
||||
{label && <span className="text-sm">{label}</span>}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from './dialog'
|
||||
|
||||
/**
|
||||
* Modal — adapter с @nstart/ui-style API над Radix Dialog. Используется
|
||||
* существующим кодом (см. dictionaries.$name.tsx, webhooks, reviews etc.) —
|
||||
* упрощает codemod без рефактора каждого callsite.
|
||||
*
|
||||
* Props mapping:
|
||||
* isOpen → Dialog.open
|
||||
* onClose → Dialog.onOpenChange (false case)
|
||||
* title → DialogTitle children
|
||||
* description → DialogDescription
|
||||
* maxWidth → DialogContent size (mapped: max-w-md→sm, max-w-lg→md, max-w-2xl→lg, max-w-4xl→xl)
|
||||
* children → DialogContent body
|
||||
*
|
||||
* Новый код может использовать Dialog напрямую — он остаётся exported.
|
||||
*/
|
||||
|
||||
const SIZE_MAP: Record<string, 'sm' | 'md' | 'lg' | 'xl' | 'full'> = {
|
||||
'max-w-sm': 'sm',
|
||||
'max-w-md': 'sm',
|
||||
'max-w-lg': 'md',
|
||||
'max-w-xl': 'md',
|
||||
'max-w-2xl': 'lg',
|
||||
'max-w-3xl': 'lg',
|
||||
'max-w-4xl': 'xl',
|
||||
'max-w-5xl': 'xl',
|
||||
'max-w-6xl': 'full',
|
||||
}
|
||||
|
||||
export type ModalProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
maxWidth?: string
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
||||
children?: React.ReactNode
|
||||
hideClose?: boolean
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
maxWidth,
|
||||
size,
|
||||
hideClose,
|
||||
children,
|
||||
}: ModalProps) {
|
||||
const resolved = size ?? (maxWidth ? SIZE_MAP[maxWidth] : undefined) ?? 'md'
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onClose()
|
||||
}}
|
||||
>
|
||||
<DialogContent size={resolved} hideClose={hideClose}>
|
||||
{(title || description) && (
|
||||
<DialogHeader>
|
||||
{title && <DialogTitle>{title}</DialogTitle>}
|
||||
{description && <DialogDescription>{description}</DialogDescription>}
|
||||
</DialogHeader>
|
||||
)}
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -3,11 +3,15 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* PageHeader — title + optional description + actions slot. Use в верху route'а.
|
||||
*
|
||||
* breadcrumb prop рендерится тонкой строкой ВЫШЕ title (cap-style).
|
||||
* Обычно JSX с <Link>ами через separator: "Справочники / Космические аппараты".
|
||||
*/
|
||||
export type PageHeaderProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
title: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
actions?: React.ReactNode
|
||||
breadcrumb?: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
@@ -15,11 +19,17 @@ export function PageHeader({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
breadcrumb,
|
||||
children,
|
||||
...props
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-1 mb-4', className)} {...props}>
|
||||
{breadcrumb && (
|
||||
<div className="text-2xs font-display uppercase tracking-[0.10em] text-mute leading-tight mb-1">
|
||||
{breadcrumb}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-start justify-between gap-3 flex-wrap">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-lg font-semibold text-ink leading-tight truncate">{title}</h1>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
Tabs as RadixTabs,
|
||||
TabsList as RadixTabsList,
|
||||
TabsTrigger as RadixTabsTrigger,
|
||||
} from './tabs'
|
||||
|
||||
/**
|
||||
* TabItem-based single-component API над Radix Tabs. Замена @nstart/ui Tabs
|
||||
* с теми же props ({@code items, value, onValueChange}).
|
||||
*
|
||||
* Использование (legacy):
|
||||
* <Tabs items={[{id: 'a', label: 'A'}, {id: 'b', label: 'B'}]}
|
||||
* value={active} onValueChange={setActive} />
|
||||
*
|
||||
* Для content panels используйте TabsContent с radix Tabs root напрямую —
|
||||
* этот adapter рендерит только tab bar.
|
||||
*
|
||||
* Новый код может использовать `<Tabs.Root>...<TabsList><TabsTrigger value="a">A</...`
|
||||
* directly из ui/components/tabs.tsx.
|
||||
*/
|
||||
|
||||
export type TabItem = {
|
||||
id: string
|
||||
label: React.ReactNode
|
||||
disabled?: boolean
|
||||
/** Right-aligned content в tab (badge, indicator). */
|
||||
trailing?: React.ReactNode
|
||||
}
|
||||
|
||||
export type TabsSimpleProps = {
|
||||
items: ReadonlyArray<TabItem>
|
||||
value: string
|
||||
onValueChange: (id: string) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function TabsSimple({ items, value, onValueChange, className }: TabsSimpleProps) {
|
||||
return (
|
||||
<RadixTabs value={value} onValueChange={onValueChange} className={className}>
|
||||
<RadixTabsList>
|
||||
{items.map((item) => (
|
||||
<RadixTabsTrigger key={item.id} value={item.id} disabled={item.disabled}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{item.label}
|
||||
{item.trailing}
|
||||
</span>
|
||||
</RadixTabsTrigger>
|
||||
))}
|
||||
</RadixTabsList>
|
||||
</RadixTabs>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +1,59 @@
|
||||
/**
|
||||
* Barrel re-export для shadcn-style UI primitives.
|
||||
* Barrel re-export для UI primitives.
|
||||
*
|
||||
* <p>Это единая точка входа для UI компонентов в admin-ui. Цель — постепенно
|
||||
* переключить все импорты с {@code @nstart/ui} на {@code @/ui}, и затем
|
||||
* удалить @nstart/ui из package.json.
|
||||
* <p>Стратегия миграции (Phase 2 stack migration):
|
||||
* <ul>
|
||||
* <li>{@code @nstart/ui} остаётся подключённым — все его компоненты
|
||||
* pass-through через этот barrel, чтобы существующий код работал без
|
||||
* рефактора props (Checkbox.label/description, Modal.panelClassName,
|
||||
* FormActions.align, и т.п.).</li>
|
||||
* <li>shadcn-style primitives в {@code ./components/*} добавлены как
|
||||
* параллельный набор. Новый код использует их через alias'ы где они
|
||||
* чище nstart'а ({@code Button}, {@code Badge}, {@code Alert} с
|
||||
* Tailwind v4 tokens; {@code Cap}, {@code ScopeBadge}, {@code ScopeDot},
|
||||
* {@code ScopeStrip} — handoff design system primitives).</li>
|
||||
* <li>Phase 3: компоненты которые имеют bug'и или missing features в nstart
|
||||
* (Combobox vs MultiSelect, TanStack Table vs Table) — заменяются
|
||||
* per-file rewrite.</li>
|
||||
* <li>Phase 4: @nstart/ui удаляется из package.json когда все callsites
|
||||
* переписаны.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Phase 2 миграции (handoff plan): новые компоненты — Radix primitives +
|
||||
* Tailwind v4 tokens. Phase 3 — каждый touched файл переносится с {@code
|
||||
* @nstart/ui} на {@code @/ui} имена. Phase 4 — pkg removed.
|
||||
*
|
||||
* <p>Все компоненты используют наши design tokens (--color-ink, --color-accent
|
||||
* etc.) через @theme inline в styles.css.
|
||||
* <p>Текущая семантика: import {X} from '@/ui' — Х берётся приоритетно из
|
||||
* наших shadcn primitives (overrides ниже), иначе passthrough из @nstart/ui.
|
||||
* Этот файл — single source of truth для UI imports.
|
||||
*/
|
||||
|
||||
// Buttons + actions
|
||||
// === Полная 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 и т.д.).
|
||||
export { Button, buttonVariants, type ButtonProps } from './components/button'
|
||||
export { IconButton, type IconButtonProps } from './components/icon-button'
|
||||
|
||||
// Inputs
|
||||
export { Input, type InputProps } from './components/input'
|
||||
export { Label } from './components/label'
|
||||
export { SearchInput, type SearchInputProps } from './components/search-input'
|
||||
|
||||
// Form layout
|
||||
export { FormActions, type FormActionsProps } from './components/form-actions'
|
||||
|
||||
// Status / feedback
|
||||
export { Badge, type BadgeProps } from './components/badge'
|
||||
export { Alert, type AlertProps } from './components/alert'
|
||||
export { Skeleton } from './components/skeleton'
|
||||
export { LoadingBlock, type LoadingBlockProps } from './components/loading-block'
|
||||
export { EmptyState, type EmptyStateProps } from './components/empty-state'
|
||||
|
||||
// Surfaces
|
||||
// === Handoff design system primitives — новые компоненты без nstart-аналога ===
|
||||
export { Cap, type CapProps } from './components/cap'
|
||||
export {
|
||||
Card,
|
||||
ScopeDot,
|
||||
ScopeBadge,
|
||||
ScopeStrip,
|
||||
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'
|
||||
export {
|
||||
Card as ShadcnCard,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
@@ -42,9 +61,7 @@ export {
|
||||
CardFooter,
|
||||
} from './components/card'
|
||||
export { Separator } from './components/separator'
|
||||
export { PageHeader, type PageHeaderProps } from './components/page-header'
|
||||
|
||||
// Overlays
|
||||
export { Skeleton } from './components/skeleton'
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -82,9 +99,13 @@ export {
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
} from './components/tooltip'
|
||||
|
||||
// Navigation
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent } from './components/tabs'
|
||||
// Tabs из shadcn — composition API.
|
||||
export {
|
||||
Tabs as ShadcnTabsRoot,
|
||||
TabsList as ShadcnTabsList,
|
||||
TabsTrigger as ShadcnTabsTrigger,
|
||||
TabsContent as ShadcnTabsContent,
|
||||
} from './components/tabs'
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -101,25 +122,4 @@ export {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuRadioGroup,
|
||||
} from './components/dropdown-menu'
|
||||
|
||||
// Form controls
|
||||
export { Checkbox } from './components/checkbox'
|
||||
export { Switch } from './components/switch'
|
||||
|
||||
// Domain-specific atoms (handoff design system)
|
||||
export { Cap, type CapProps } from './components/cap'
|
||||
export {
|
||||
ScopeDot,
|
||||
ScopeBadge,
|
||||
ScopeStrip,
|
||||
type DataScope,
|
||||
} from './components/scope'
|
||||
|
||||
// === @nstart/ui compatibility aliases ===
|
||||
// Чтобы упростить миграцию touched файлов: перевод import только-source,
|
||||
// not всего code. Каждый alias документирован, как соотносится с новым API.
|
||||
|
||||
export { Card as Panel } from './components/card'
|
||||
export { Dialog as Modal } from './components/dialog'
|
||||
export { Sheet as Drawer } from './components/sheet'
|
||||
export { Input as TextInput } from './components/input'
|
||||
export { Switch as ShadcnSwitch } from './components/switch'
|
||||
|
||||
Reference in New Issue
Block a user