refactor(web): вынести общие блоки макета в components/portal

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-23 13:53:59 +07:00
1 parent b89e72ec1b
commit 6898d2255d
23 files changed
+92 -86

No files matched your search

@@ -33,7 +33,7 @@ import {
SheetHeader,
SheetTitle,
} from '@authportal/ui/components/sheet'
import type { Tone } from './mock'
import type { Tone } from './tone'
import { TONE_BADGE } from './tone'
const TONE_GLYPH: Record<Tone, string> = { ok: '●', wn: '▲', er: '✕', mu: '○', in: '' }
@@ -104,7 +104,7 @@ export function SignalAlert({
}
/** Боковое окно (как CreateUserSheet): заголовок, прокручиваемое тело, подвал. */
export function PreviewSheet({
export function PortalSheet({
open,
onOpenChange,
title,
@@ -3,7 +3,7 @@ import { Badge } from '@/components/reui/badge'
import { Frame, FrameDescription, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
import { IconTile } from '@/components/reui/icon-tile'
import { cn } from '@authportal/ui/lib/utils'
import type { Tone } from './mock'
import type { Tone } from './tone'
import { StatusBadge } from './bits'
export type AppCardItem = {
@@ -2,7 +2,7 @@ import { useState, type ReactNode } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { Button } from '@authportal/ui/components/button'
import { cn } from '@authportal/ui/lib/utils'
import { CodeBlock, PreviewSheet, SignalAlert, StatusBadge } from './bits'
import { CodeBlock, PortalSheet, SignalAlert, StatusBadge } from './bits'
/**
* Баннер доски: заголовок и пояснение, справа «Объяснить» — открывает панель
@@ -38,7 +38,7 @@ export function ExplainBanner({
Объяснить
</Button>
</div>
<PreviewSheet
<PortalSheet
open={open}
onOpenChange={setOpen}
title="Ассистент · разбор"
@@ -77,7 +77,7 @@ export function ExplainBanner({
<SignalAlert tone="wn" title="Ассистент ничего не меняет сам">
Блокировка и сброс passkeys выполняются вручную с подтверждением и попадают в журнал «Изменения».
</SignalAlert>
</PreviewSheet>
</PortalSheet>
</>
)
}
File renamed without changes.
@@ -32,8 +32,8 @@ import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
import { cn } from '@authportal/ui/lib/utils'
import { StatusBadge } from './bits'
import type { Tone } from './mock'
import type { RowMark } from './preview-store'
import type { Tone } from './tone'
import type { RowMark } from './tone'
import { TONE_DOT } from './tone'
export type GridColumn<T> = {
@@ -83,7 +83,7 @@ const ACTIONS_COLUMN_SIZE = 120
* сохранённые виды линией со счётчиками, панель массовых действий, таблица со статусом и меню строки,
* подвал «Показано N из M». Ширина таблицы не меньше суммы колонок — прокрутка внутри контейнера.
*/
export function PreviewSectionGrid<T extends object>({
export function SectionGrid<T extends object>({
title,
description,
rows,
@@ -1,4 +1,8 @@
import type { Tone } from './mock'
export type Tone = 'ok' | 'wn' | 'er' | 'mu' | 'in'
/** Пометка строки с накопленной правкой. */
export type RowMark = 'new' | 'dirty' | 'deleted'
type BadgeVariant =
| 'success-light'
@@ -10,7 +10,7 @@ import {
} from '@authportal/ui/components/select'
import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
import { CodeBlock, SignalAlert } from './bits'
import { CodeBlock, SignalAlert } from '@/components/portal/bits'
import { withLevel, levelOf, type AccessValue, type Level } from './access-model'
import { APP_SHORT, USER_JOURNAL } from './mock'
@@ -1,6 +1,6 @@
import { StatusBadge } from './bits'
import { StatusBadge } from '@/components/portal/bits'
import type { Tone } from './mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid'
import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
type MatrixRow = {
id: string
@@ -39,7 +39,7 @@ const COLUMNS: GridColumn<MatrixRow>[] = [
/** Матрица доступа по доске: сколько пользователей имеет доступ к каждому приложению и с каким уровнем. */
export function AccessMatrix() {
return (
<PreviewSectionGrid
<SectionGrid
title="Матрица доступа"
description="Сколько пользователей имеет доступ к каждому приложению и с каким уровнем"
rows={ROWS}
+3 -3
View File
@@ -3,7 +3,7 @@ import type { AdminUser } from '@authportal/shared'
import { Button } from '@authportal/ui/components/button'
import { AccessEditor } from './access-editor'
import { diffAccess, type AccessValue } from './access-model'
import { PreviewSheet } from './bits'
import { PortalSheet } from '@/components/portal/bits'
/** Окно «Права доступа»: широкая правая панель, чтобы таблица оставалась в контексте. */
export function AccessSheet({
@@ -19,7 +19,7 @@ export function AccessSheet({
const changes = useMemo(() => diffAccess({ apps: user.apps, permissions: user.permissions }, value).length, [user, value])
return (
<PreviewSheet
<PortalSheet
open
onOpenChange={(o) => !o && onClose()}
size="lg"
@@ -47,6 +47,6 @@ export function AccessSheet({
}
>
<AccessEditor user={user} value={value} onChange={setValue} />
</PreviewSheet>
</PortalSheet>
)
}
@@ -6,10 +6,10 @@ import { Field, FieldDescription, FieldError, FieldLabel } from '@authportal/ui/
import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch'
import { Textarea } from '@authportal/ui/components/textarea'
import { PreviewSheet, SignalAlert } from './bits'
import { PortalSheet, SignalAlert } from '@/components/portal/bits'
/** Окно «Создать пользователя» по доске: email, имя, временный пароль, администратор и заметка про журнал. */
export function CreateUserPreviewSheet({
export function CreateUserPortalSheet({
open,
onOpenChange,
onSubmit,
@@ -37,7 +37,7 @@ export function CreateUserPreviewSheet({
}
return (
<PreviewSheet
<PortalSheet
open={open}
onOpenChange={(o) => {
if (!o) reset()
@@ -93,12 +93,12 @@ export function CreateUserPreviewSheet({
<SignalAlert tone="wn" title="Журнал">
Создание записывается в журнал «Изменения» с вашим именем и IP.
</SignalAlert>
</PreviewSheet>
</PortalSheet>
)
}
/** Окно «Создать OIDC-клиент» по доске: название, redirect URI, включён. */
export function CreateOidcPreviewSheet({
export function CreateOidcPortalSheet({
open,
onOpenChange,
onSubmit,
@@ -128,7 +128,7 @@ export function CreateOidcPreviewSheet({
}
return (
<PreviewSheet
<PortalSheet
open={open}
onOpenChange={(o) => {
if (!o) reset()
@@ -189,6 +189,6 @@ export function CreateOidcPreviewSheet({
<FieldDescription>Выключенный клиент не может получить токены</FieldDescription>
</div>
</Field>
</PreviewSheet>
</PortalSheet>
)
}
+2 -1
View File
@@ -21,7 +21,8 @@ import {
type AuditLogEntry,
} from '@authportal/shared'
export type Tone = 'ok' | 'wn' | 'er' | 'mu' | 'in'
import type { Tone } from '@/components/portal/tone'
export type { Tone }
export type NavItem = {
id: string
@@ -15,13 +15,13 @@ import {
} from '@authportal/ui/components/select'
import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { KpiStrip } from '../kpi-strip'
import { PageHead, PreviewSheet, StatusBadge, PageTab, PageTabsList, FormFrame, SettingRow } from '../bits'
import { AppCards } from '../canvas-blocks'
import { kpi } from '../kpi'
import { KpiStrip } from '@/components/portal/kpi-strip'
import { PageHead, PortalSheet, StatusBadge, PageTab, PageTabsList, FormFrame, SettingRow } from '@/components/portal/bits'
import { AppCards } from '@/components/portal/canvas-blocks'
import { kpi } from '@/components/portal/kpi'
import { MOCK_SWITCHER } from '../mock'
import { usePreviewStore } from '../preview-store'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
type LinkRow = {
id: AppId
@@ -180,7 +180,7 @@ export function AppLinksPage() {
<PageTab value="menu" label="Меню" />
</PageTabsList>
<TabsContent value="list" className="min-w-0">
<PreviewSectionGrid
<SectionGrid
title="Ссылки сервисов"
description="Порядок строк = порядок в меню · переключатель управляет видимостью · изменения применяются после сохранения"
rows={rows}
@@ -247,7 +247,7 @@ export function AppLinksPage() {
</TabsContent>
</Tabs>
<PreviewSheet
<PortalSheet
open={editing !== null}
onOpenChange={(o) => !o && setEditing(null)}
title="Приложение"
@@ -323,7 +323,7 @@ export function AppLinksPage() {
</Field>
</>
) : null}
</PreviewSheet>
</PortalSheet>
</>
)
}
@@ -2,12 +2,12 @@ import { Link } from '@tanstack/react-router'
import { BadgeCheckIcon, CloudIcon, GlobeIcon, KeyRoundIcon, LayoutDashboardIcon, LayoutGridIcon, ServerIcon, ShieldIcon } from 'lucide-react'
import { toast } from 'sonner'
import { APPS, type AppId } from '@authportal/shared'
import { KpiStrip } from '../kpi-strip'
import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { PageHead, PageTab, PageTabsList } from '../bits'
import { AppCards, type AppCardItem } from '../canvas-blocks'
import { kpi } from '../kpi'
import { PageHead, PageTab, PageTabsList } from '@/components/portal/bits'
import { AppCards, type AppCardItem } from '@/components/portal/canvas-blocks'
import { kpi } from '@/components/portal/kpi'
const ICONS: Record<AppId, typeof CloudIcon> = {
cfdm: CloudIcon,
@@ -1,16 +1,16 @@
import { HistoryIcon, NetworkIcon, OctagonAlertIcon, UserCogIcon } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { KpiStrip } from '../kpi-strip'
import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button'
import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { CodeBlock, FormFrame, KvList, PageHead, PageTab, PageTabsList, PreviewSheet, SettingRow, StatusBadge, TypedConfirm } from '../bits'
import { ActivityFeed } from '../canvas-blocks'
import { kpi } from '../kpi'
import { CodeBlock, FormFrame, KvList, PageHead, PageTab, PageTabsList, PortalSheet, SettingRow, StatusBadge, TypedConfirm } from '@/components/portal/bits'
import { ActivityFeed } from '@/components/portal/canvas-blocks'
import { kpi } from '@/components/portal/kpi'
import { AUDIT_FEED, AUDIT_ROWS, type AuditRow } from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
const mono = (v: string) => <span className="font-mono text-[12.5px] whitespace-nowrap">{v}</span>
@@ -74,7 +74,7 @@ export function AuditPage() {
/>
</TabsContent>
<TabsContent value="table" className="min-w-0">
<PreviewSectionGrid
<SectionGrid
title="Журнал изменений"
description="Поиск по событию, пользователю и IP · виды по важности"
rows={AUDIT_ROWS}
@@ -113,7 +113,7 @@ export function AuditPage() {
</TabsContent>
</Tabs>
<PreviewSheet
<PortalSheet
open={entry !== null}
onOpenChange={(o) => !o && setEntry(null)}
title="Событие журнала"
@@ -137,7 +137,7 @@ export function AuditPage() {
</CodeBlock>
</>
) : null}
</PreviewSheet>
</PortalSheet>
<TypedConfirm
open={purgeOpen}
@@ -9,7 +9,7 @@ import { Button } from '@authportal/ui/components/button'
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from '@authportal/ui/components/field'
import { Input } from '@authportal/ui/components/input'
import { Separator } from '@authportal/ui/components/separator'
import { SignalAlert } from '../bits'
import { SignalAlert } from '@/components/portal/bits'
const PERKS = [
{ icon: FingerprintIcon, title: 'Passkey вместо пароля', text: 'Отпечаток, Face ID или ключ безопасности' },
@@ -2,14 +2,14 @@ import { CircleCheckIcon, GlobeIcon, MonitorSmartphoneIcon, ShieldAlertIcon } fr
import { useMemo, useState } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { toast } from 'sonner'
import { KpiStrip } from '../kpi-strip'
import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { ExplainBanner } from '../explain-banner'
import { Segmented, CodeBlock, KvList, PageHead, PreviewSheet, StatusBadge, PageTab, PageTabsList } from '../bits'
import { kpi } from '../kpi'
import { ExplainBanner } from '@/components/portal/explain-banner'
import { Segmented, CodeBlock, KvList, PageHead, PortalSheet, StatusBadge, PageTab, PageTabsList } from '@/components/portal/bits'
import { kpi } from '@/components/portal/kpi'
import { MOCK_LOGINS, MOCK_SESSIONS, type LoginRow, type SessionRow } from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
const PERIODS = ['24 ч', '7 д', '30 д', '90 д']
@@ -76,7 +76,7 @@ export function LoginsPage() {
<PageTab value="sess" label="Активные сессии" count={sessions.length} />
</PageTabsList>
<TabsContent value="hist" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid
<SectionGrid
title="Журнал входов"
description="Поиск по логину и IP · фильтры: результат, приложение, пользователь · период выбирается выше"
rows={MOCK_LOGINS}
@@ -94,7 +94,7 @@ export function LoginsPage() {
/>
</TabsContent>
<TabsContent value="sess" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid
<SectionGrid
title="Активные сессии"
description="Токены, действующие сейчас · завершение сессии выкидывает пользователя из портала и приложений"
rows={sessions}
@@ -112,7 +112,7 @@ export function LoginsPage() {
</TabsContent>
</Tabs>
<PreviewSheet
<PortalSheet
open={event !== null}
onOpenChange={(o) => !o && setEvent(null)}
title="Событие входа"
@@ -148,7 +148,7 @@ export function LoginsPage() {
<CodeBlock label="Событие">{`action: ${event.tone === 'er' ? 'auth.login_failed' : 'auth.login'}\nsource_app: portal\nseverity: ${event.tone === 'er' ? 'warning' : 'info'}\nip: ${event.ip}`}</CodeBlock>
</>
) : null}
</PreviewSheet>
</PortalSheet>
</>
)
}
+10 -10
View File
@@ -2,8 +2,8 @@ import { useMemo, useState } from 'react'
import { CircleCheckIcon, CopyIcon, KeyRoundIcon, LockKeyholeIcon, TicketIcon } from 'lucide-react'
import { toast } from 'sonner'
import type { CreateOidcClientValues } from '@/components/reui-kit/create-oidc-client-sheet'
import { CreateOidcPreviewSheet } from '../create-sheets'
import { KpiStrip } from '../kpi-strip'
import { CreateOidcPortalSheet } from '../create-sheets'
import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button'
import {
AlertDialog,
@@ -18,9 +18,9 @@ import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { Textarea } from '@authportal/ui/components/textarea'
import { ExplainBanner } from '../explain-banner'
import { CopyList, KvFrame, PageHead, PreviewSheet, StatusBadge, TypedConfirm, PageTab, PageTabsList } from '../bits'
import { kpi } from '../kpi'
import { ExplainBanner } from '@/components/portal/explain-banner'
import { CopyList, KvFrame, PageHead, PortalSheet, StatusBadge, TypedConfirm, PageTab, PageTabsList } from '@/components/portal/bits'
import { kpi } from '@/components/portal/kpi'
import {
MOCK_CLIENT_SECRET,
MOCK_OIDC_CLIENTS,
@@ -29,7 +29,7 @@ import {
type OidcClientRow,
type Tone
} from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
function tone(c: OidcClientRow): Tone {
return !c.enabled ? 'mu' : c.stale ? 'wn' : 'ok'
@@ -111,7 +111,7 @@ export function OidcPage() {
<PageTab value="meta" label="Параметры issuer" />
</PageTabsList>
<TabsContent value="clients" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid
<SectionGrid
title="Зарегистрированные клиенты"
description="Каждый клиент — внешнее приложение (RP) · секрет показывается один раз при создании и обновлении"
rows={clients}
@@ -148,9 +148,9 @@ export function OidcPage() {
</TabsContent>
</Tabs>
<CreateOidcPreviewSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} />
<CreateOidcPortalSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} />
<PreviewSheet
<PortalSheet
open={editing !== null}
onOpenChange={(o) => !o && setEditing(null)}
title="OIDC-клиент"
@@ -193,7 +193,7 @@ export function OidcPage() {
</Field>
</>
) : null}
</PreviewSheet>
</PortalSheet>
<TypedConfirm
open={rotating !== null}
@@ -1,17 +1,17 @@
import { useMemo, useState } from 'react'
import { ClockIcon, FingerprintIcon, KeyRoundIcon, LockIcon, MonitorSmartphoneIcon, TrashIcon } from 'lucide-react'
import { toast } from 'sonner'
import { KpiStrip } from '../kpi-strip'
import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button'
import { Field, FieldDescription, FieldLabel } from '@authportal/ui/components/field'
import { Input } from '@authportal/ui/components/input'
import { Switch } from '@authportal/ui/components/switch'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { ExplainBanner } from '../explain-banner'
import { KvFrame, KvList, PageHead, PreviewSheet, SignalAlert, StatusBadge, TypedConfirm, PageTab, PageTabsList, FormFrame, SettingRow } from '../bits'
import { kpi } from '../kpi'
import { ExplainBanner } from '@/components/portal/explain-banner'
import { KvFrame, KvList, PageHead, PortalSheet, SignalAlert, StatusBadge, TypedConfirm, PageTab, PageTabsList, FormFrame, SettingRow } from '@/components/portal/bits'
import { kpi } from '@/components/portal/kpi'
import { MOCK_PASSKEYS, type PasskeyRow } from '../mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
export function SecurityPage() {
const [keys, setKeys] = useState<PasskeyRow[]>(MOCK_PASSKEYS)
@@ -72,7 +72,7 @@ export function SecurityPage() {
<PageTab value="acc" label="Учётная запись" />
</PageTabsList>
<TabsContent value="keys" className="flex min-w-0 flex-col gap-4">
<PreviewSectionGrid
<SectionGrid
title="Мои passkeys"
description="Вход без пароля: отпечаток, Face ID, PIN или ключ безопасности · удаление необратимо"
rows={view}
@@ -137,7 +137,7 @@ export function SecurityPage() {
</TabsContent>
</Tabs>
<PreviewSheet
<PortalSheet
open={addOpen}
onOpenChange={setAddOpen}
title="Добавить passkey"
@@ -169,9 +169,9 @@ export function SecurityPage() {
<SignalAlert tone="in" title="Что произойдёт дальше">
Откроется системный диалог: подтвердите отпечатком, Face ID, PIN или ключом безопасности. Закрытый ключ не покидает устройство.
</SignalAlert>
</PreviewSheet>
</PortalSheet>
<PreviewSheet
<PortalSheet
open={selected !== null}
onOpenChange={(o) => !o && setSelected(null)}
title="Passkey"
@@ -213,7 +213,7 @@ export function SecurityPage() {
['Последний вход', `${selected?.lastUsed ?? ''} · 203.0.113.10`],
]}
/>
</PreviewSheet>
</PortalSheet>
<TypedConfirm
open={deleting !== null}
@@ -1,16 +1,16 @@
import { FingerprintIcon, ShieldIcon, UserXIcon, UsersIcon } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import type { AdminUser, CreateUserRequest } from '@authportal/shared'
import { CreateUserPreviewSheet } from '../create-sheets'
import { KpiStrip } from '../kpi-strip'
import { CreateUserPortalSheet } from '../create-sheets'
import { KpiStrip } from '@/components/portal/kpi-strip'
import { Button } from '@authportal/ui/components/button'
import { Tabs, TabsContent } from '@authportal/ui/components/tabs'
import { AccessSheet } from '../access-sheet'
import { diffAccess, type AccessValue } from '../access-model'
import { PageHead, TypedConfirm, PageTab, PageTabsList } from '../bits'
import { kpi } from '../kpi'
import { PageHead, TypedConfirm, PageTab, PageTabsList } from '@/components/portal/bits'
import { kpi } from '@/components/portal/kpi'
import { ALL_APP_IDS } from '../mock'
import { ExplainBanner } from '../explain-banner'
import { ExplainBanner } from '@/components/portal/explain-banner'
import { AccessMatrix } from '../access-matrix'
import { UsersGrid, type UsersGridHandlers } from '../users-grid'
import { usePreviewStore } from '../preview-store'
@@ -131,7 +131,7 @@ export function UsersPage() {
</TabsContent>
</Tabs>
<CreateUserPreviewSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} />
<CreateUserPortalSheet open={createOpen} onOpenChange={setCreateOpen} onSubmit={onCreate} />
{accessFor ? (
<AccessSheet
@@ -67,7 +67,7 @@ import {
useSidebar,
} from '@authportal/ui/components/sidebar'
import { cn } from '@authportal/ui/lib/utils'
import { Segmented } from './bits'
import { Segmented } from '@/components/portal/bits'
import { FRESH_BY_PAGE, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS, type NavItem } from './mock'
import { usePreviewStore } from './preview-store'
+2 -1
View File
@@ -3,7 +3,8 @@ import type { AdminUser } from '@authportal/shared'
export type ChangeKind = 'add' | 'chg' | 'del'
export type RowMark = 'new' | 'dirty' | 'deleted'
import type { RowMark } from '@/components/portal/tone'
export type { RowMark }
export type PendingChange = {
id: string
+3 -3
View File
@@ -2,10 +2,10 @@ import { useMemo } from 'react'
import type { AdminUser } from '@authportal/shared'
import { toast } from 'sonner'
import { Button } from '@authportal/ui/components/button'
import { PendingChip, StatusBadge } from './bits'
import { PendingChip, StatusBadge } from '@/components/portal/bits'
import type { RowMark } from './preview-store'
import { ALL_APP_IDS, APP_SHORT, USER_META, type Tone } from './mock'
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid'
import { SectionGrid, TwoLineCell, type GridColumn } from '@/components/portal/section-grid'
function userStatus(u: AdminUser): { tone: Tone; label: string } {
if (u.disabled) return { tone: 'mu', label: 'Отключён' }
@@ -65,7 +65,7 @@ export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; hand
)
return (
<PreviewSectionGrid
<SectionGrid
title="Пользователи портала"
description="Поиск по email и имени · фильтры: роль, статус, приложения, passkey · колонки настраиваются, вид сохраняется"
rows={users}