diff --git a/apps/web/src/design-preview/explain-banner.tsx b/apps/web/src/design-preview/explain-banner.tsx index d4c0306..7dfc491 100644 --- a/apps/web/src/design-preview/explain-banner.tsx +++ b/apps/web/src/design-preview/explain-banner.tsx @@ -1,6 +1,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' /** @@ -23,17 +24,20 @@ export function ExplainBanner({ const navigate = useNavigate() return ( <> - setOpen(true)}> - Объяснить - - } +
- {children} - +
+ {title} {children} +
+ +
= { + neutral: 'text-foreground', + ok: 'text-foreground', + in: 'text-foreground', + wn: 'text-warning', + er: 'text-destructive', +} + +/** Колонки по числу ячеек: 4 → 4/2/1, 3 → 3/1, 2 → 2/1 (пороги контейнера как на досках). */ +const COLS: Record = { + 1: 'grid-cols-1', + 2: 'grid-cols-1 @[640px]:grid-cols-2', + 3: 'grid-cols-1 @[1240px]:grid-cols-3', + 4: 'grid-cols-1 @[640px]:grid-cols-2 @[1240px]:grid-cols-4', +} + +const GLYPH: Record = { neutral: '', ok: '●', in: '', wn: '▲', er: '✕' } + +const BADGE = { + neutral: 'outline', + ok: 'success-light', + in: 'info-light', + wn: 'warning-light', + er: 'destructive-light', +} as const + +/** + * Полоса показателей как на досках: один Frame, ячейки разделены линиями, + * в ячейке подпись, крупное значение и плашка-подсказка. + */ +export function KpiStrip({ items, 'aria-label': ariaLabel }: { items: KpiItem[]; 'aria-label'?: string }) { + return ( + + +
+ {items.map((k) => ( +
+
{k.label}
+
+ {k.value} + + {GLYPH[k.tone] ? {GLYPH[k.tone]} : null} + {k.hint} + +
+
+ ))} +
+
+ + ) +} diff --git a/apps/web/src/design-preview/kpi.tsx b/apps/web/src/design-preview/kpi.tsx index e816841..86d0b6b 100644 --- a/apps/web/src/design-preview/kpi.tsx +++ b/apps/web/src/design-preview/kpi.tsx @@ -1,46 +1,20 @@ -import { createElement } from 'react' -import type { LucideIcon } from 'lucide-react' -import { Badge } from '@/components/reui/badge' -import type { KpiStatItem } from '@/components/reui-kit/kpi-stat-grid' - export type KpiTone = 'neutral' | 'ok' | 'wn' | 'er' | 'in' -const ICON_CLASS: Record = { - neutral: 'text-muted-foreground [&_svg]:text-current', - ok: 'text-success [&_svg]:text-current', - wn: 'text-warning [&_svg]:text-current', - er: 'text-destructive [&_svg]:text-current', - in: 'text-info [&_svg]:text-current', +export type KpiItem = { + id: string + label: string + value: string + /** Плашка-подсказка рядом со значением. */ + hint: string + tone: KpiTone } -const BADGE: Record = { - neutral: 'outline', - ok: 'success-light', - wn: 'warning-light', - er: 'destructive-light', - in: 'info-light', -} - -/** Плитка KPI: иконка по тону, значение с пояснением, бейдж статуса. */ +/** Плитка KPI доски: подпись, значение (цветом при wn/er) и плашка-подсказка. */ export function kpi( id: string, label: string, value: string, - opts: { icon: LucideIcon; tone?: KpiTone; badge: string; sub?: string }, -): KpiStatItem { - const tone = opts.tone ?? 'neutral' - return { - id, - label, - icon: createElement(opts.icon), - iconClassName: ICON_CLASS[tone], - variant: tone === 'wn' ? 'warning' : tone === 'er' ? 'destructive' : 'default', - value: createElement( - 'span', - { className: 'flex flex-wrap items-baseline gap-x-2' }, - value, - opts.sub ? createElement('span', { className: 'text-muted-foreground text-xs font-normal' }, opts.sub) : null, - ), - footer: createElement(Badge, { variant: BADGE[tone], size: 'sm', className: 'whitespace-nowrap' }, opts.badge), - } + opts: { icon?: unknown; tone?: KpiTone; badge: string; sub?: string }, +): KpiItem { + return { id, label, value, hint: opts.badge, tone: opts.tone ?? 'neutral' } } diff --git a/apps/web/src/design-preview/pages/app-links-page.tsx b/apps/web/src/design-preview/pages/app-links-page.tsx index ca9d69d..90f593a 100644 --- a/apps/web/src/design-preview/pages/app-links-page.tsx +++ b/apps/web/src/design-preview/pages/app-links-page.tsx @@ -2,16 +2,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ChartColumnIcon, CloudIcon, - EyeIcon, - FingerprintIcon, - KeyRoundIcon, LayoutDashboardIcon, - LayoutGridIcon, NetworkIcon, PlusIcon, RotateCcwIcon, ServerIcon, - type LucideIcon, + type LucideIcon } from 'lucide-react' import { toast } from 'sonner' import type { AppId, AppSwitcherIconName } from '@authportal/shared' @@ -24,11 +20,11 @@ import { SelectGroup, SelectItem, SelectTrigger, - SelectValue, + SelectValue } from '@authportal/ui/components/select' import { Switch } from '@authportal/ui/components/switch' import { Tabs, TabsContent } from '@authportal/ui/components/tabs' -import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' +import { KpiStrip } from '../kpi-strip' import { PageHead, PreviewSheet, StatusBadge, PageTab, PageTabsList, FormFrame, SettingRow } from '../bits' import { AppCards } from '../canvas-blocks' import { kpi } from '../kpi' @@ -50,7 +46,7 @@ const ICONS: Record = server: { label: 'Сервер', Icon: ServerIcon }, globe: { label: 'Сеть', Icon: NetworkIcon }, dashboard: { label: 'Дашборд', Icon: LayoutDashboardIcon }, - chart: { label: 'График', Icon: ChartColumnIcon }, + chart: { label: 'График', Icon: ChartColumnIcon } } const MODES = [ @@ -64,7 +60,7 @@ const INITIAL: LinkRow[] = MOCK_SWITCHER.apps.map((a) => ({ url: a.url, icon: a.icon, mode: (a.authMode ?? 'jwt').toUpperCase() as 'JWT' | 'OIDC', - enabled: a.id !== 'mm', + enabled: a.id !== 'mm' })) export function AppLinksPage() { @@ -92,7 +88,7 @@ export function AppLinksPage() { lines: names.map((n) => `${enabled ? '+' : '−'} ${n}`), resource: 'app-links', marks: Object.fromEntries(ids.map((i) => [i, 'dirty' as const])), - revert: () => setRows(before), + revert: () => setRows(before) }) }, [addChange], @@ -115,7 +111,7 @@ export function AppLinksPage() { lines: [` ${item!.name}`, `− позиция ${from + 1}`, `+ позиция ${to + 1}`], resource: 'app-links', marks: { [item!.id]: 'dirty' }, - revert: () => setRows(before), + revert: () => setRows(before) }) }, [addChange], @@ -140,7 +136,7 @@ export function AppLinksPage() { lines: ['− пользовательский порядок', '+ порядок каталога'], resource: 'app-links', marks: Object.fromEntries(before.map((r) => [r.id, 'dirty' as const])), - revert: () => setRows(before), + revert: () => setRows(before) }) }, [addChange]) @@ -167,7 +163,7 @@ export function AppLinksPage() { variant="outline" onClick={() => toast('Добавление недоступно в макете', { - description: 'Каталог фиксирован контрактом: cfdm · vps · bgp · fw · dns · cdn · mm.', + description: 'Каталог фиксирован контрактом: cfdm · vps · bgp · fw · dns · cdn · mm.' }) } > @@ -176,13 +172,13 @@ export function AppLinksPage() { } /> - r.mode === 'JWT').length), { icon: KeyRoundIcon, tone: 'in', badge: 'fragment-токен', sub: 'без пароля' }), - kpi('oidc', 'OIDC', String(rows.filter((r) => r.mode === 'OIDC').length), { icon: FingerprintIcon, tone: 'in', badge: 'Technitium DNS', sub: 'внешний RP' }), + kpi('n', 'Приложений', String(rows.length), { badge: 'в каталоге' }), + kpi('sw', 'В переключателе', String(shown.length), { tone: 'ok', badge: 'видны всем' }), + kpi('jwt', 'JWT SSO', String(rows.filter((r) => r.mode === 'JWT').length), { badge: 'fragment-токен' }), + kpi('oidc', 'OIDC', String(rows.filter((r) => r.mode === 'OIDC').length), { badge: 'Technitium DNS' }), ]} /> @@ -215,13 +211,13 @@ export function AppLinksPage() { onLabel: 'Показать в меню', offLabel: 'Скрыть', onRun: (ids) => setEnabled(ids as AppId[], true), - offRun: (ids) => setEnabled(ids as AppId[], false), + offRun: (ids) => setEnabled(ids as AppId[], false) }} rowSwitch={{ value: (r) => r.enabled, onChange: (r, on) => setEnabled([r.id], on) }} rowMenu={(r) => ({ onOpen: () => setEditing(r), toggleLabel: r.enabled ? 'Скрыть из меню' : 'Показать в меню', - onToggle: () => setEnabled([r.id], !r.enabled), + onToggle: () => setEnabled([r.id], !r.enabled) })} onReorder={dropOn} rowStatus={(r) => marks[r.id]} @@ -252,7 +248,7 @@ export function AppLinksPage() { badge: r.mode, badgeVariant: r.mode === 'OIDC' ? ('warning-light' as const) : ('info-light' as const), meta: r.id, - icon: , + icon: } })} /> diff --git a/apps/web/src/design-preview/pages/apps-page.tsx b/apps/web/src/design-preview/pages/apps-page.tsx index 8155c73..1684253 100644 --- a/apps/web/src/design-preview/pages/apps-page.tsx +++ b/apps/web/src/design-preview/pages/apps-page.tsx @@ -1,18 +1,14 @@ import { Link } from '@tanstack/react-router' import { CloudIcon, - FingerprintIcon, GlobeIcon, - KeyRoundIcon, LayoutDashboardIcon, - LayoutGridIcon, ServerIcon, - ShieldCheckIcon, - ShieldIcon, + ShieldIcon } from 'lucide-react' import { toast } from 'sonner' import { APPS, type AppId } from '@authportal/shared' -import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' +import { KpiStrip } from '../kpi-strip' import { Button } from '@authportal/ui/components/button' import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { PageHead, PageTab, PageTabsList } from '../bits' @@ -26,7 +22,7 @@ const ICONS: Record = { fw: ShieldIcon, dns: GlobeIcon, cdn: CloudIcon, - mm: LayoutDashboardIcon, + mm: LayoutDashboardIcon } const NO_ACCESS: AppId[] = ['cdn'] @@ -49,7 +45,7 @@ export function AppsPage() { onSelect: () => open ? toast(`Открыть ${app.title}`, { description: `${meta} (макет)` }) - : toast.warning(`Нет доступа к ${app.title}`, { description: 'Запросите права у администратора.' }), + : toast.warning(`Нет доступа к ${app.title}`, { description: 'Запросите права у администратора.' }) } } @@ -64,13 +60,13 @@ export function AppsPage() { } /> - a.authMode === 'jwt').length), { icon: KeyRoundIcon, tone: 'in', badge: 'fragment-токен', sub: 'вход без пароля' }), - kpi('oidc', 'OIDC', String(allowed.filter((a) => a.authMode === 'oidc').length), { icon: FingerprintIcon, tone: 'in', badge: 'Technitium DNS', sub: 'внешний RP' }), - kpi('role', 'Роль', 'Админ', { icon: ShieldCheckIcon, tone: 'wn', badge: 'is_admin', sub: 'полный доступ' }), + kpi('avail', 'Доступно приложений', String(allowed.length), { tone: 'ok', badge: `из ${APPS.length}` }), + kpi('jwt', 'JWT SSO', String(allowed.filter((a) => a.authMode === 'jwt').length), { badge: 'fragment-токен' }), + kpi('oidc', 'OIDC', String(allowed.filter((a) => a.authMode === 'oidc').length), { badge: 'Technitium DNS' }), + kpi('role', 'Роль', 'Админ', { badge: 'is_admin' }), ]} /> diff --git a/apps/web/src/design-preview/pages/audit-page.tsx b/apps/web/src/design-preview/pages/audit-page.tsx index a6beb06..3ace05b 100644 --- a/apps/web/src/design-preview/pages/audit-page.tsx +++ b/apps/web/src/design-preview/pages/audit-page.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' -import { HistoryIcon, NetworkIcon, ShieldAlertIcon, Trash2Icon, UserCogIcon } from 'lucide-react' +import { Trash2Icon } from 'lucide-react' import { toast } from 'sonner' -import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' +import { KpiStrip } from '../kpi-strip' import { Button } from '@authportal/ui/components/button' import { Input } from '@authportal/ui/components/input' import { Switch } from '@authportal/ui/components/switch' @@ -47,13 +47,13 @@ export function AuditPage() { } /> - diff --git a/apps/web/src/design-preview/pages/logins-page.tsx b/apps/web/src/design-preview/pages/logins-page.tsx index 5c5c003..130bff9 100644 --- a/apps/web/src/design-preview/pages/logins-page.tsx +++ b/apps/web/src/design-preview/pages/logins-page.tsx @@ -1,8 +1,8 @@ import { useMemo, useState } from 'react' import { useNavigate } from '@tanstack/react-router' -import { DownloadIcon, LogInIcon, ShieldAlertIcon, GlobeIcon, MonitorSmartphoneIcon } from 'lucide-react' +import { DownloadIcon } from 'lucide-react' import { toast } from 'sonner' -import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' +import { KpiStrip } from '../kpi-strip' import { Button } from '@authportal/ui/components/button' import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { ExplainBanner } from '../explain-banner' @@ -62,13 +62,13 @@ export function LoginsPage() { 6 неудачных входов с 203.0.113.58 за 10 минут. Действия: «Показать», отключить пользователя в разделе «Пользователи». - @@ -107,7 +107,7 @@ export function LoginsPage() { onToggle: () => { setSessions((s) => s.filter((x) => x.id !== r.id)) toast.success(`Сессия ${r.user} завершена (макет)`) - }, + } })} /> diff --git a/apps/web/src/design-preview/pages/oidc-page.tsx b/apps/web/src/design-preview/pages/oidc-page.tsx index e6ba347..c5a3679 100644 --- a/apps/web/src/design-preview/pages/oidc-page.tsx +++ b/apps/web/src/design-preview/pages/oidc-page.tsx @@ -1,8 +1,8 @@ import { useMemo, useState } from 'react' -import { CopyIcon, PlusIcon, RefreshCwIcon, AppWindowIcon, PowerIcon, TicketIcon, KeyRoundIcon } from 'lucide-react' +import { CopyIcon, PlusIcon, RefreshCwIcon } from 'lucide-react' import { toast } from 'sonner' import { CreateOidcClientSheet, type CreateOidcClientValues } from '@/components/reui-kit/create-oidc-client-sheet' -import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' +import { KpiStrip } from '../kpi-strip' import { Button } from '@authportal/ui/components/button' import { AlertDialog, @@ -10,7 +10,7 @@ import { AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, - AlertDialogTitle, + AlertDialogTitle } from '@authportal/ui/components/alert-dialog' import { Field, FieldDescription, FieldLabel } from '@authportal/ui/components/field' import { Input } from '@authportal/ui/components/input' @@ -26,7 +26,7 @@ import { OIDC_CAPABILITIES, OIDC_ENDPOINTS, type OidcClientRow, - type Tone, + type Tone } from '../mock' import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' @@ -66,7 +66,7 @@ export function OidcPage() { secret: 'действует', created: '17.10.2026', enabled: values.enabled, - stale: false, + stale: false } setClients((c) => [...c, created]) setCreateOpen(false) @@ -96,13 +96,13 @@ export function OidcPage() { Обновите client_secret и внесите новое значение в Technitium (Settings → SSO). Старый секрет перестанет работать сразу после обновления. ) : null} - c.enabled).length), { icon: PowerIcon, tone: 'ok', badge: 'принимают вход', sub: 'активны' }), - kpi('tok', 'Токенов за 24 ч', '41', { icon: TicketIcon, tone: 'in', badge: 'authorization_code', sub: 'выдано' }), - kpi('key', 'Ключ подписи', 'RS256', { icon: KeyRoundIcon, tone: staleCount > 0 ? 'wn' : 'ok', badge: 'ротация вручную', sub: 'JWKS активен' }), + kpi('n', 'Клиентов', String(clients.length), { badge: 'зарегистрировано' }), + kpi('on', 'Включено', String(clients.filter((c) => c.enabled).length), { tone: 'ok', badge: 'принимают вход' }), + kpi('tok', 'Токенов за 24 ч', '41', { badge: 'authorization_code' }), + kpi('key', 'Ключ подписи', 'RS256', { badge: 'ротация вручную' }), ]} /> @@ -139,7 +139,7 @@ export function OidcPage() { rowMenu={(r) => ({ onOpen: () => setEditing(r), toggleLabel: r.enabled ? 'Выключить' : 'Включить', - onToggle: () => setClients((c) => c.map((x) => (x.id === r.id ? { ...x, enabled: !x.enabled } : x))), + onToggle: () => setClients((c) => c.map((x) => (x.id === r.id ? { ...x, enabled: !x.enabled } : x))) })} onRowClick={setEditing} /> diff --git a/apps/web/src/design-preview/pages/security-page.tsx b/apps/web/src/design-preview/pages/security-page.tsx index f6ab26c..b3d67a4 100644 --- a/apps/web/src/design-preview/pages/security-page.tsx +++ b/apps/web/src/design-preview/pages/security-page.tsx @@ -1,7 +1,7 @@ import { useMemo, useState } from 'react' -import { KeyRoundIcon, PlusIcon, TrashIcon, FingerprintIcon, ClockIcon, LockKeyholeIcon, MonitorSmartphoneIcon } from 'lucide-react' +import { KeyRoundIcon, PlusIcon, TrashIcon } from 'lucide-react' import { toast } from 'sonner' -import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' +import { KpiStrip } from '../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' @@ -59,13 +59,13 @@ export function SecurityPage() { Если устройство будет потеряно, вход возможен только по паролю. Добавьте второй ключ на другом устройстве или ключ безопасности. ) : null} - 1 ? 'ok' : 'wn', badge: keys.length > 1 ? 'резерв есть' : 'одна копия', sub: 'активны' }), - kpi('last', 'Последний вход', 'сегодня', { icon: ClockIcon, badge: 'Touch ID', sub: '09:41' }), - kpi('pw', 'Пароль', 'Задан', { icon: LockKeyholeIcon, tone: 'ok', badge: 'запасной вход', sub: 'сменён 11.03' }), - kpi('sess', 'Сессий', '3', { icon: MonitorSmartphoneIcon, badge: 'устройства', sub: 'сейчас активны' }), + kpi('keys', 'Passkeys', String(keys.length), { tone: 'ok', badge: 'активны' }), + kpi('last', 'Последний вход', 'сегодня', { badge: 'MacBook Touch ID' }), + kpi('pw', 'Пароль', 'Задан', { badge: 'запасной вход' }), + kpi('sess', 'Сессий', '3', { badge: 'устройства' }), ]} /> @@ -90,7 +90,7 @@ export function SecurityPage() { onChange: (r, on) => { setRevoked((list) => (on ? list.filter((id) => id !== r.id) : [...list, r.id])) toast(on ? 'Passkey включён (макет)' : 'Passkey отозван (макет)') - }, + } }} rowMenu={(r) => ({ onOpen: () => { @@ -99,7 +99,7 @@ export function SecurityPage() { }, toggleLabel: r.status === 'Отозван' ? 'Включить' : 'Отозвать', onToggle: () => setRevoked((list) => (list.includes(r.id) ? list.filter((id) => id !== r.id) : [...list, r.id])), - onDelete: () => setDeleting(r), + onDelete: () => setDeleting(r) })} onRowClick={(r) => { setSelected(r) diff --git a/apps/web/src/design-preview/pages/users-page.tsx b/apps/web/src/design-preview/pages/users-page.tsx index 017bde2..f5cddd5 100644 --- a/apps/web/src/design-preview/pages/users-page.tsx +++ b/apps/web/src/design-preview/pages/users-page.tsx @@ -1,8 +1,7 @@ -import { UsersIcon, ShieldCheckIcon, FingerprintIcon, UserXIcon } from 'lucide-react' import { useCallback, useEffect, useRef, useState } from 'react' import type { AdminUser, CreateUserRequest } from '@authportal/shared' import { CreateUserSheet } from '@/components/reui-kit/create-user-sheet' -import { KpiStatGrid } from '@/components/reui-kit/kpi-stat-grid' +import { KpiStrip } from '../kpi-strip' import { Button } from '@authportal/ui/components/button' import { Tabs, TabsContent } from '@authportal/ui/components/tabs' import { UserPlusIcon } from 'lucide-react' @@ -55,7 +54,7 @@ export function UsersPage() { last_login_ip: null, passkey_count: 0, created_at: new Date().toISOString(), - updated_at: new Date().toISOString(), + updated_at: new Date().toISOString() } setUsers((prev) => [...prev, created]) addChange({ @@ -64,7 +63,7 @@ export function UsersPage() { lines: [`+ email: ${created.email}`, `+ name: ${created.name}`, `+ is_admin: ${String(created.is_admin)}`], resource: 'users', marks: { [created.id]: 'new' }, - revert: () => setUsers((prev) => prev.filter((u) => u.id !== created.id)), + revert: () => setUsers((prev) => prev.filter((u) => u.id !== created.id)) }) setCreateOpen(false) setAccessFor(created) @@ -85,7 +84,7 @@ export function UsersPage() { patch(ids, (u) => ({ ...u, disabled }), `${disabled ? 'Отключено' : 'Включено'} пользователей: ${ids.length}`, () => [ `− disabled: ${String(!disabled)}`, `+ disabled: ${String(disabled)}`, - ]), + ]) } const admins = users.filter((u) => u.is_admin).length @@ -111,13 +110,13 @@ export function UsersPage() { > 6 неудачных входов с 203.0.113.58 за 10 минут. Проверьте журнал входов и при необходимости отключите учётную запись. Действие: «Показать». - 0 ? 'wn' : 'ok', badge: 'только пароль', sub: 'нет резервного входа' }), - kpi('off', 'Отключённых', String(disabled), { icon: UserXIcon, badge: 'вход закрыт', sub: 'не удаляются' }), + kpi('total', 'Пользователей', String(users.length), { badge: 'в каталоге' }), + kpi('admins', 'Администраторов', String(admins), { tone: 'wn', badge: 'is_admin' }), + kpi('nopk', 'Без passkey', String(noPasskey), { tone: 'wn', badge: 'только пароль' }), + kpi('off', 'Отключённых', String(disabled), { badge: 'вход закрыт' }), ]} /> @@ -169,7 +168,7 @@ export function UsersPage() { lines: [`− email: ${target.email}`, `− apps: ${target.apps.length}`], resource: 'users', marks: { [target.id]: 'deleted' }, - commit: () => setUsers((prev) => prev.filter((u) => u.id !== target.id)), + commit: () => setUsers((prev) => prev.filter((u) => u.id !== target.id)) }) setDeleteFor(null) }} @@ -191,7 +190,7 @@ export function UsersPage() { lines: targets.map((t) => `− ${t.email}`), resource: 'users', marks: Object.fromEntries(targets.map((t) => [t.id, 'deleted' as const])), - commit: () => setUsers((prev) => prev.filter((u) => !ids.includes(u.id))), + commit: () => setUsers((prev) => prev.filter((u) => !ids.includes(u.id))) }) setBulkDeleteIds(null) }} diff --git a/apps/web/src/design-preview/preview-chrome.tsx b/apps/web/src/design-preview/preview-chrome.tsx index 6d2571a..e0d3004 100644 --- a/apps/web/src/design-preview/preview-chrome.tsx +++ b/apps/web/src/design-preview/preview-chrome.tsx @@ -11,6 +11,7 @@ import { GlobeIcon, InfoIcon, KeyRoundIcon, + ShieldCheckIcon, LayoutDashboardIcon, LogOutIcon, SearchIcon, @@ -89,11 +90,11 @@ function PreviewAppSwitcher() { render={} >
- +
Auth Portal - auth.shnt.top + Единый вход · SSO
@@ -271,7 +272,7 @@ export function PreviewSidebar() { {pinned.length > 0 ? ( - Закреплённые + Закреплённые {pinned.map((item) => ( @@ -293,7 +294,7 @@ export function PreviewSidebar() { onClick={() => setClosed((c) => ({ ...c, [group]: !c[group] }))} /> } - className="cursor-pointer justify-between" + className="cursor-pointer justify-between text-[11px] font-semibold tracking-wide uppercase" > {group} @@ -343,22 +344,22 @@ function MonitorPopover() { variant="outline" type="button" aria-label={`Состояние системы: внимание, ${errors} сбой, ${warnings} предупреждения`} - className="border-warning/40 bg-warning/10 hover:bg-warning/20 aria-expanded:bg-warning/20 relative h-8 gap-2 px-2 @max-3xl/header:w-8 @max-3xl/header:border-transparent @max-3xl/header:bg-transparent @max-3xl/header:px-0" + className="border-warning/40 bg-warning/10 hover:bg-warning/20 aria-expanded:bg-warning/20 relative h-8 gap-2 px-2 @max-[760px]/header:w-8 @max-[760px]/header:border-transparent @max-[760px]/header:bg-transparent @max-[760px]/header:px-0" /> } > - Внимание - + Внимание + {errors} {warnings} - + {bad.length} - +
diff --git a/apps/web/src/design-preview/section-grid.tsx b/apps/web/src/design-preview/section-grid.tsx index f864d19..555695c 100644 --- a/apps/web/src/design-preview/section-grid.tsx +++ b/apps/web/src/design-preview/section-grid.tsx @@ -282,7 +282,7 @@ export function PreviewSectionGrid({ onRowClick={onRowClick} getRowStatus={rowStatus} emptyMessage={emptyMessage} - tableLayout={{ headerSticky: false, dense: true, width: 'auto', rowsDraggable: Boolean(onReorder) }} + tableLayout={{ headerSticky: false, dense: false, width: 'auto', rowsDraggable: Boolean(onReorder) }} >