diff --git a/apps/web/src/components/reui-kit/quick-action-grid.tsx b/apps/web/src/components/reui-kit/quick-action-grid.tsx index 92fc739..627b8ff 100644 --- a/apps/web/src/components/reui-kit/quick-action-grid.tsx +++ b/apps/web/src/components/reui-kit/quick-action-grid.tsx @@ -33,6 +33,8 @@ interface QuickActionGridProps { title?: string description?: string className?: string + /** Плитки в Frame с отступами и сеткой auto-fit от 17rem (как KPI-полоса): без вылета за границы. */ + autoFit?: boolean } const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current' @@ -108,18 +110,24 @@ export function QuickActionGrid({ title = 'Быстрые действия', description, className, + autoFit = false, }: QuickActionGridProps) { if (actions.length === 0) return null return ( - + {(title || description) && ( {title ? {title} : null} {description ? {description} : null} )} -
+
{actions.map((action) => ( div:last-chil } html[data-preview-contrast='high'] { - --muted-foreground: color-mix(in oklch, var(--foreground) 82%, var(--background)); - --border: color-mix(in oklch, var(--foreground) 32%, var(--background)); - --input: color-mix(in oklch, var(--foreground) 40%, var(--background)); + --border: color-mix(in oklch, var(--foreground) 55%, transparent); + --input: color-mix(in oklch, var(--foreground) 60%, transparent); + --muted-foreground: color-mix(in oklch, var(--foreground) 88%, transparent); + --ring: oklch(0.85 0.17 95); } /* Шрифты дизайн-системы (Geist / Geist Mono) — только пока смонтирован макет. */ @@ -26,3 +27,18 @@ html[data-preview-density], html[data-preview-density] body { font-family: var(--font-sans); } + +/* Видимый фокус и reduced motion — как в макете MikrotikManager v2. */ +html[data-preview-density] [data-slot='sidebar-wrapper'] :focus-visible, +html[data-preview-density] #main-content :focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; +} + +@media (prefers-reduced-motion: reduce) { + html[data-preview-density] #main-content *, + html[data-preview-density] [data-slot='sidebar-wrapper'] * { + animation: none !important; + transition: none !important; + } +} 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 468eaee..f1da6e6 100644 --- a/apps/web/src/design-preview/pages/app-links-page.tsx +++ b/apps/web/src/design-preview/pages/app-links-page.tsx @@ -342,6 +342,7 @@ export function AppLinksPage() { { diff --git a/apps/web/src/design-preview/pages/apps-page.tsx b/apps/web/src/design-preview/pages/apps-page.tsx index 81e11ad..6e8278a 100644 --- a/apps/web/src/design-preview/pages/apps-page.tsx +++ b/apps/web/src/design-preview/pages/apps-page.tsx @@ -41,6 +41,7 @@ export function AppsPage() { { @@ -60,6 +61,7 @@ export function AppsPage() { { diff --git a/apps/web/src/design-preview/preview-chrome.tsx b/apps/web/src/design-preview/preview-chrome.tsx index 50c365f..26dadb1 100644 --- a/apps/web/src/design-preview/preview-chrome.tsx +++ b/apps/web/src/design-preview/preview-chrome.tsx @@ -1,33 +1,27 @@ -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { Link, useNavigate, useRouterState } from '@tanstack/react-router' import { ActivityIcon, + ArrowRightIcon, CheckIcon, ChevronsUpDownIcon, CloudIcon, - ContrastIcon, FingerprintIcon, GlobeIcon, InfoIcon, KeyRoundIcon, LayoutDashboardIcon, LogOutIcon, - MonitorIcon, - MoonIcon, - PaletteIcon, - RowsIcon, SearchIcon, ServerIcon, StarIcon, ShieldIcon, - SunIcon, UserPlusIcon, } from 'lucide-react' import { useTheme } from 'next-themes' import { toast } from 'sonner' import { APPS, type AppId } from '@authportal/shared' import { Badge } from '@/components/reui/badge' -import { Avatar, AvatarFallback } from '@authportal/ui/components/avatar' import { Breadcrumb, BreadcrumbItem, @@ -47,7 +41,6 @@ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from '@authportal/ui/components/dropdown-menu' import { Kbd } from '@authportal/ui/components/kbd' @@ -71,6 +64,7 @@ import { useSidebar, } from '@authportal/ui/components/sidebar' import { cn } from '@authportal/ui/lib/utils' +import { Segmented } from './bits' import { APP_SHORT, MOCK_USERS, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS, type NavItem } from './mock' import { usePreviewStore } from './preview-store' @@ -136,163 +130,90 @@ function PreviewAppSwitcher() { ) } -const THEMES = [ - { value: 'light', label: 'Светлая', icon: SunIcon }, - { value: 'dark', label: 'Тёмная', icon: MoonIcon }, - { value: 'system', label: 'Системная', icon: MonitorIcon }, -] as const - -function Segmented({ - label, - value, - options, - onChange, -}: { - label: string - value: T - options: ReadonlyArray<{ value: T; label: string; icon?: typeof SunIcon }> - onChange: (v: T) => void -}) { - return ( -
e.stopPropagation()} - onPointerDown={(e) => e.stopPropagation()} - > - {options.map((o) => { - const active = value === o.value - const Icon = o.icon - return ( - - ) - })} -
- ) -} +const LABEL_CLASS = 'text-muted-foreground text-[11px] font-medium uppercase' function PreviewNavUser() { const { isMobile } = useSidebar() - const { theme, setTheme } = useTheme() - const [mounted, setMounted] = useState(false) - useEffect(() => setMounted(true), []) + const { theme, setTheme, resolvedTheme } = useTheme() const { density, setDensity, contrast, setContrast } = usePreviewStore() const navigate = useNavigate() + const current = theme === 'light' || theme === 'dark' ? theme : resolvedTheme === 'light' ? 'light' : 'dark' return ( - - + } > - - AD - -
- Admin +
+ AD +
+
+ Admin admin@shnt.top
- - - - - - - AD - -
- Admin - admin@shnt.top -
-
-
- - - void navigate({ to: '/design-preview/account' })}> - - Безопасность - - - - Тема -
- -
-
- - - Плотность -
- -
-
- - - Контраст -
- -
-
-
- - - void navigate({ to: '/design-preview/login' })}> - - Выйти - - -
- + + + +
+
+ AD +
+
+ Admin + admin@shnt.top +
+
+ + +
+ Тема + setTheme(i === 1 ? 'light' : 'dark')} + /> + Плотность + setDensity(i === 1 ? 'compact' : 'comfortable')} + /> + Контраст + setContrast(i === 1 ? 'high' : 'standard')} + /> +
+ + +
+ ) @@ -308,19 +229,21 @@ function NavEntry({ item, pathname, pinned, onTogglePin }: { item: NavItem; path } > - - - {item.problem ? ( - - ) : null} - + {item.label} + {item.problem ? ( + + ) : null} m.tone !== 'ok') const errors = bad.filter((m) => m.tone === 'er').length const warnings = bad.length - errors + const ok = MONITOR_ITEMS.length - bad.length return ( } > - - Внимание - + + Внимание + + + {errors} + + {warnings} + + {bad.length} + - -
-
- Монитор Auth Portal - обновлено 14:02 + +
+
+ Состояние системы + обновлено 14:02
Внимание
-
- {errors} ошибка - {warnings} предупреждения - {MONITOR_ITEMS.length - bad.length} в норме +
+ {[ + { n: errors, l: 'сбой', c: 'text-destructive' }, + { n: warnings, l: 'предупреждения', c: 'text-warning' }, + { n: ok, l: 'в норме', c: 'text-success' }, + ].map((x) => ( + + {x.n} + {x.l} + + ))}
-
    - {MONITOR_ITEMS.map((m) => ( -
  • - - - - {m.title} - {m.text} +
      + {MONITOR_ITEMS.filter((m) => m.tone !== 'ok').map((m) => ( +
    • + + + + {m.title} + {m.text} - {m.age} + {m.age}
    • ))}
    +
    + {MONITOR_METRICS.map((m) => ( +
    +
    + {m.l} + {m.v} +
    +
    + +
    +
    + ))} +
    + + Журнал входов + + ) @@ -476,7 +434,7 @@ export function PreviewHeader() { const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`)) return ( -
    +