import { useState, type ReactNode } from 'react' import { CircleAlertIcon, InfoIcon, TriangleAlertIcon, XIcon } from 'lucide-react' import { toast } from 'sonner' import { Alert, AlertAction, AlertDescription, AlertTitle } from '@/components/reui/alert' import { Badge } from '@/components/reui/badge' import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle, } from '@/components/reui/frame' import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@authportal/ui/components/alert-dialog' import { Button } from '@authportal/ui/components/button' import { cn } from '@authportal/ui/lib/utils' import { Input } from '@authportal/ui/components/input' import { Label } from '@authportal/ui/components/label' import { Tabs, TabsList, TabsTrigger } from '@authportal/ui/components/tabs' import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@authportal/ui/components/sheet' import type { Tone } from './mock' import { TONE_BADGE } from './tone' const TONE_GLYPH: Record = { ok: '●', wn: '▲', er: '✕', mu: '○', in: '' } /** Плашка статуса: слово + глиф (● ▲ ✕ ○), чтобы цвет не был единственным признаком. */ export function StatusBadge({ tone, children }: { tone: Tone; children: ReactNode }) { const glyph = TONE_GLYPH[tone] return ( {glyph ? {glyph} : null} {children} ) } /** Заголовок страницы: название, описание, метка свежести и действия. */ export function PageHead({ title, sub, actions, }: { title: string sub: string actions?: ReactNode }) { return (

{title}

{sub}

{actions ?
{actions}
: null}
) } const ALERT_ICON = { er: , wn: , in: , } as const const ALERT_VARIANT = { er: 'destructive', wn: 'warning', in: 'info', } as const export function SignalAlert({ tone, title, children, action, }: { tone: 'er' | 'wn' | 'in' title: string children: ReactNode action?: ReactNode }) { return ( {ALERT_ICON[tone]} {title} {children} {action ? {action} : null} ) } /** Боковое окно (как CreateUserSheet): заголовок, прокручиваемое тело, подвал. */ export function PreviewSheet({ open, onOpenChange, title, description, children, footer, size = 'md', dirty, }: { open: boolean onOpenChange: (open: boolean) => void title: string description: string children: ReactNode footer?: ReactNode /** Ширина панели: md — форма, lg — редактор с двумя колонками. */ size?: 'md' | 'lg' /** Явный признак изменений (иначе ловим ввод в полях и переключатели). */ dirty?: boolean }) { const [touchedState, setTouched] = useState(false) const touched = dirty ?? touchedState const [guard, setGuard] = useState(false) function requestChange(next: boolean) { if (!next && touched) { setGuard(true) return } if (!next) setTouched(false) onOpenChange(next) } return ( <>
{title} } >
{description}
setTouched(true)} onClick={(e) => { if ((e.target as HTMLElement).closest('[role=switch]')) setTouched(true) }} > {children}
{footer ? ( { // «Закрыть» и «Отмена» при изменённой форме тоже спрашивают подтверждение. const label = (e.target as HTMLElement).closest('button')?.textContent?.trim() if (touched && (label === 'Закрыть' || label === 'Отмена')) { e.stopPropagation() setGuard(true) } }} >{footer} ) : null}
Закрыть без сохранения? Изменения в форме будут потеряны. Остаться ) } /** Подтверждение опасного действия вводом слова (email, «сбросить», «очистить»…). */ export function TypedConfirm({ open, onOpenChange, title, description, expected, confirmLabel, onConfirm, }: { open: boolean onOpenChange: (open: boolean) => void title: string description: ReactNode expected: string confirmLabel: string onConfirm: () => void }) { const [value, setValue] = useState('') const matches = value.trim().toLowerCase() === expected.toLowerCase() return ( { if (!next) setValue('') onOpenChange(next) }} > {title} {description}
setValue(e.target.value)} aria-invalid={value.length > 0 && !matches} />
Отмена
) } export function KvFrame({ title, description, items, }: { title: string description: string items: Array<{ label: string; value: ReactNode }> }) { return ( {title} {description}
{items.map((item) => (
{item.label}
{item.value}
))}
) } /** Список «значение + подпись + копировать» (параметры issuer). */ export function CopyList({ title, description, items, }: { title: string description: string items: Array<{ label: string; value: string }> }) { return ( {title} {description}
    {items.map((item) => (
  • {item.value} {item.label}
  • ))}
) } /** Компактный список «ключ — значение» для боковых окон. */ export function KvList({ items }: { items: Array<[string, string]> }) { return (
{items.map(([k, v]) => (
{k}
{v}
))}
) } export function CodeBlock({ label, children }: { label: string; children: string }) { return (
{label}
{children}
) } /** Сегментированный переключатель на Tabs (вид таблицы, период). */ export function Segmented({ label, options, value, onChange, }: { label: string options: string[] value: number onChange: (index: number) => void }) { return ( onChange(Number(v))} aria-label={label} className="max-w-full"> {options.map((o, i) => ( {o} ))} ) } /** Плашка строки с накопленной правкой: «Не сохранено» или «Будет удалено». */ export function PendingChip({ mark }: { mark: 'new' | 'dirty' | 'deleted' }) { return ( {mark === 'deleted' ? 'Будет удалено' : 'Не сохранено'} ) } /** Вкладки страницы как на досках: линия-подчёркивание, число — мелким индексом. */ export function PageTabsList({ children }: { children: ReactNode }) { return ( {children} ) } export function PageTab({ value, label, count }: { value: string; label: string; count?: number | string }) { return ( {label} {count !== undefined ? {count} : null} ) } /** Строка настройки как на досках: слева название и описание, справа элемент управления. */ export function SettingRow({ title, description, children }: { title: string; description: string; children: ReactNode }) { return (
{title} {description}
{children}
) } /** Frame с заголовком, итоговой плашкой справа и строками настроек (блок «form» досок). */ export function FormFrame({ title, description, badge, tone = 'ok', children, }: { title: string description: string badge: string tone?: Tone children: ReactNode }) { return (
{title} {description}
{badge}
{children} ) }