feat(design-preview): шапка как в канвасе — заголовок, инфо-кнопка и монитор «Внимание»

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
DenozordecandClaude Sonnet 5.5 committed 2026-10-18 01:08:42 +07:00
1 parent a31a964228
commit f486b72884
2 files changed
+54 -20

No files matched your search

+8
View File
@@ -99,6 +99,14 @@ export const MONITOR_ITEMS: Array<{
age: '3 дн', age: '3 дн',
to: '/design-preview/oidc', to: '/design-preview/oidc',
}, },
{
id: 'nopasskey',
tone: 'wn',
title: '3 пользователя без passkey',
text: 'ops, pavel, dns-bot · вход только по паролю',
age: '1 дн',
to: '/design-preview/users',
},
{ id: 'api', tone: 'ok', title: 'API портала', text: '/ready · ответ 18 мс', age: 'сейчас', to: '/design-preview/apps' }, { id: 'api', tone: 'ok', title: 'API портала', text: '/ready · ответ 18 мс', age: 'сейчас', to: '/design-preview/apps' },
{ id: 'jwks', tone: 'ok', title: 'JWKS и discovery', text: 'Подпись RS256 · ключ активен', age: 'сейчас', to: '/design-preview/oidc' }, { id: 'jwks', tone: 'ok', title: 'JWKS и discovery', text: 'Подпись RS256 · ключ активен', age: 'сейчас', to: '/design-preview/oidc' },
] ]
+46 -20
View File
@@ -8,6 +8,7 @@ import {
ContrastIcon, ContrastIcon,
FingerprintIcon, FingerprintIcon,
GlobeIcon, GlobeIcon,
InfoIcon,
KeyRoundIcon, KeyRoundIcon,
LayoutDashboardIcon, LayoutDashboardIcon,
LogOutIcon, LogOutIcon,
@@ -32,7 +33,6 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator,
} from '@authportal/ui/components/breadcrumb' } from '@authportal/ui/components/breadcrumb'
import { Button } from '@authportal/ui/components/button' import { Button } from '@authportal/ui/components/button'
import { import {
@@ -387,27 +387,37 @@ export function PreviewSidebar() {
function MonitorPopover() { function MonitorPopover() {
const bad = MONITOR_ITEMS.filter((m) => m.tone !== 'ok') const bad = MONITOR_ITEMS.filter((m) => m.tone !== 'ok')
const errors = bad.filter((m) => m.tone === 'er').length
const warnings = bad.length - errors
return ( return (
<Popover> <Popover>
<PopoverTrigger <PopoverTrigger
render={ render={
<button <button
type="button" type="button"
aria-label="Монитор Auth Portal: внимание, 1 ошибка, 1 предупреждение" aria-label={`Монитор Auth Portal: внимание, ${errors} ошибка, ${warnings} предупреждения`}
className="border-border hover:bg-accent focus-visible:ring-ring inline-flex h-8 items-center gap-1.5 rounded-md border px-2 outline-none focus-visible:ring-2" className="border-warning/40 bg-warning/10 text-warning hover:bg-warning/15 focus-visible:ring-ring inline-flex h-9 items-center gap-2 rounded-lg border px-3 text-sm font-medium outline-none focus-visible:ring-2"
/> />
} }
> >
<ActivityIcon aria-hidden className="text-destructive size-3.5" /> <ActivityIcon aria-hidden className="size-4" />
<span className="text-foreground hidden text-xs font-medium @2xl/header:inline">Система</span> <span className="hidden @md/header:inline">Внимание</span>
<Badge variant="destructive-light" size="xs" className="h-4 px-1.5 text-[10px]"> <span className="bg-destructive inline-flex size-5 items-center justify-center rounded-full text-xs font-semibold text-white tabular-nums">
{bad.length} {bad.length}
</Badge> </span>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent align="end" sideOffset={8} className="flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-0! p-0!"> <PopoverContent align="end" sideOffset={8} className="flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-0! p-0!">
<div className="border-border flex items-center justify-between border-b px-3 py-2.5"> <div className="border-border flex items-center justify-between border-b px-3 py-2.5">
<span className="text-xs font-medium">Монитор Auth Portal</span> <div className="flex flex-col">
<Badge variant="warning-light" size="xs">Внимание</Badge> <span className="text-xs font-medium">Монитор Auth Portal</span>
<span className="text-muted-foreground text-[11px]">обновлено 14:02</span>
</div>
<Badge variant="warning-light" size="sm">Внимание</Badge>
</div>
<div className="text-muted-foreground flex gap-3 border-b px-3 py-2 text-xs">
<span><b className="text-destructive font-semibold">{errors}</b> ошибка</span>
<span><b className="text-warning font-semibold">{warnings}</b> предупреждения</span>
<span><b className="text-success font-semibold">{MONITOR_ITEMS.length - bad.length}</b> в норме</span>
</div> </div>
<ul className="flex flex-col"> <ul className="flex flex-col">
{MONITOR_ITEMS.map((m) => ( {MONITOR_ITEMS.map((m) => (
@@ -434,10 +444,35 @@ function MonitorPopover() {
) )
} }
function FreshnessPopover() {
const [auto, setAuto] = useState(true)
return (
<Popover>
<PopoverTrigger
render={
<Button type="button" variant="ghost" size="icon-sm" aria-label="Источники и свежесть данных" title="Источники и свежесть данных" />
}
>
<InfoIcon aria-hidden />
</PopoverTrigger>
<PopoverContent align="end" sideOffset={8} className="flex w-72 max-w-[calc(100vw-2rem)] flex-col gap-3">
<div className="flex items-center gap-2">
<span className="bg-success size-2 rounded-full" aria-hidden />
<span className="text-sm font-medium">Обновлено 14:02:11 · Live</span>
</div>
<p className="text-muted-foreground text-xs">Данные портала: GET /api/v1/admin/* · сессия и права из базы портала. В макете запросы не отправляются.</p>
<label className="flex items-center justify-between gap-2 text-sm">
Автообновление
<Switch checked={auto} onCheckedChange={setAuto} aria-label="Автообновление" />
</label>
</PopoverContent>
</Popover>
)
}
export function PreviewHeader() { export function PreviewHeader() {
const pathname = useRouterState({ select: (s) => s.location.pathname }) const pathname = useRouterState({ select: (s) => s.location.pathname })
const { setPaletteOpen } = usePreviewStore() const { setPaletteOpen } = usePreviewStore()
const [auto, setAuto] = useState(true)
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
return ( return (
@@ -446,22 +481,13 @@ export function PreviewHeader() {
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" /> <Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
<Breadcrumb className="min-w-0"> <Breadcrumb className="min-w-0">
<BreadcrumbList className="flex-nowrap"> <BreadcrumbList className="flex-nowrap">
<BreadcrumbItem className="hidden @lg/header:inline-flex">Auth Portal</BreadcrumbItem>
<BreadcrumbSeparator className="hidden @lg/header:block" />
<BreadcrumbItem className="hidden @md/header:inline-flex">{current?.group ?? 'Портал'}</BreadcrumbItem>
<BreadcrumbSeparator className="hidden @md/header:block" />
<BreadcrumbItem className="min-w-0"> <BreadcrumbItem className="min-w-0">
<BreadcrumbPage className="truncate">{current?.label ?? 'Макет'}</BreadcrumbPage> <BreadcrumbPage className="truncate">{current?.label ?? 'Макет'}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
<div className="ml-auto flex shrink-0 items-center gap-2"> <div className="ml-auto flex shrink-0 items-center gap-2">
<span role="status" className="text-muted-foreground hidden items-center gap-1.5 text-xs @3xl/header:flex"> <FreshnessPopover />
<span className="bg-success size-1.5 rounded-full" aria-hidden />
Обновлено 14:02:11 · Live
<Switch size="sm" checked={auto} onCheckedChange={setAuto} aria-label="Автообновление" />
<span>авто</span>
</span>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"