diff --git a/apps/web/src/design-preview/access-editor.tsx b/apps/web/src/design-preview/access-editor.tsx index 768cfad..e2b10f2 100644 --- a/apps/web/src/design-preview/access-editor.tsx +++ b/apps/web/src/design-preview/access-editor.tsx @@ -1,3 +1,4 @@ +import { Field, FieldDescription, FieldLabel } from '@authportal/ui/components/field' import { APPS, PERMISSION_CATALOG, type AdminUser, type AppId } from '@authportal/shared' import { Select, @@ -78,18 +79,13 @@ export function AccessEditor({ if (!catalog) return null return ( - + ) })} diff --git a/apps/web/src/design-preview/bits.tsx b/apps/web/src/design-preview/bits.tsx index 3732add..fcd5e6c 100644 --- a/apps/web/src/design-preview/bits.tsx +++ b/apps/web/src/design-preview/bits.tsx @@ -145,11 +145,11 @@ export function PreviewSheet({ side="right" showCloseButton={false} className={cn( - 'inset-y-4 right-4 left-auto flex h-[calc(100svh-2rem)] max-w-none flex-col gap-0 overflow-hidden rounded-xl p-0 outline-none sm:max-w-none', + 'inset-y-0 right-0 left-auto flex h-svh max-w-none flex-col gap-0 overflow-hidden rounded-none p-0 outline-none sm:max-w-none', 'data-[side=right]:sm:max-w-none', size === 'lg' - ? 'data-[side=right]:w-[min(56rem,calc(100vw-2rem))]' - : 'data-[side=right]:w-[min(26rem,calc(100vw-2rem))]', + ? 'data-[side=right]:w-[min(45rem,100vw)]' + : 'data-[side=right]:w-[min(30rem,100vw)]', )} > diff --git a/apps/web/src/design-preview/design-preview.css b/apps/web/src/design-preview/design-preview.css index 55d4837..9ec2b9c 100644 --- a/apps/web/src/design-preview/design-preview.css +++ b/apps/web/src/design-preview/design-preview.css @@ -13,25 +13,26 @@ html[data-preview-density='compact'] [data-slot='sidebar-inset'] > div:last-chil /* Палитра MikrotikManager v2 (доски Auth Portal): только пока смонтирован макет. */ html[data-preview-density] { --background: oklch(0.973 0.004 247.858); - --foreground: oklch(0.141 0.006 264); + --foreground: oklch(0.205 0.01 264); --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.006 264); + --card-foreground: oklch(0.205 0.01 264); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.141 0.006 264); - --secondary: oklch(0.958 0.005 247.858); + --popover-foreground: oklch(0.205 0.01 264); + --secondary: oklch(0.955 0.005 264); --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.958 0.005 247.858); - --muted-foreground: oklch(0.52 0.01 264); - --accent: oklch(0.942 0.016 264.376); + --muted: oklch(0.95 0.006 264); + --muted-foreground: oklch(0.44 0.02 264); + --accent: oklch(0.93 0.01 264); --accent-foreground: oklch(0.205 0 0); - --border: oklch(0.904 0.006 264); - --input: oklch(0.904 0.006 264); + --border: oklch(0.88 0.008 264); + --input: oklch(0.86 0.008 264); --ring: oklch(0.488 0.243 264.376); --sidebar: oklch(0.985 0.003 264); - --sidebar-foreground: oklch(0.141 0.006 264); + --sidebar-foreground: oklch(0.205 0.01 264); --sidebar-accent: oklch(0.93 0.01 264); --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.904 0.006 264); + --sidebar-border: oklch(0.88 0.008 264); + --invert: oklch(0.9 0.01 264); --sidebar-primary: oklch(0.488 0.243 264.376); --destructive-foreground: oklch(0.45 0.2 27); --success: oklch(0.6 0.15 163); @@ -63,6 +64,7 @@ html[data-preview-density].dark { --sidebar-accent: oklch(0.262 0.014 264.376); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); + --invert: oklch(0.37 0.013 285.805); --destructive: oklch(0.704 0.191 22.216); --destructive-foreground: oklch(0.577 0.245 27.325); --success: oklch(0.696 0.17 162.48); @@ -82,6 +84,9 @@ html[data-preview-density][data-preview-contrast='high'] { html[data-preview-density].dark[data-preview-contrast='high'] { --background: oklch(0 0 0); + --primary: oklch(1 0 0); + --primary-foreground: oklch(0 0 0); + --sidebar: oklch(0.1 0 0); --foreground: oklch(1 0 0); --success: oklch(0.82 0.19 150); --success-foreground: oklch(0.9 0.15 150); @@ -132,3 +137,9 @@ html[data-preview-density] [data-slot='data-grid-table'] thead th :is(span, butt font-size: 0.75rem; background: color-mix(in oklch, var(--muted) 30%, transparent); } + +/* Затемнение под панелями без размытия — как на досках. */ +html[data-preview-density] :is([data-slot='sheet-overlay'], [data-slot='dialog-overlay'], [data-slot='alert-dialog-overlay']) { + background: oklch(0 0 0 / 55%); + backdrop-filter: none; +} diff --git a/apps/web/src/design-preview/preview-chrome.tsx b/apps/web/src/design-preview/preview-chrome.tsx index def9b4b..c3a9c65 100644 --- a/apps/web/src/design-preview/preview-chrome.tsx +++ b/apps/web/src/design-preview/preview-chrome.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo, useState, type ComponentType } from 'react' import { Link, useNavigate, useRouterState } from '@tanstack/react-router' import { ActivityIcon, @@ -18,7 +18,9 @@ import { ServerIcon, StarIcon, ShieldIcon, - UserPlusIcon, + MoonIcon, + PlayIcon, + PlusIcon, } from 'lucide-react' import { useTheme } from 'next-themes' import { toast } from 'sonner' @@ -67,7 +69,7 @@ import { } from '@authportal/ui/components/sidebar' import { cn } from '@authportal/ui/lib/utils' import { Segmented } from './bits' -import { APP_SHORT, FRESH_BY_PAGE, MOCK_OIDC_CLIENTS, MOCK_USERS, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS, type NavItem } from './mock' +import { FRESH_BY_PAGE, MONITOR_ITEMS, NAV_GROUPS, NAV_ITEMS, type NavItem } from './mock' import { usePreviewStore } from './preview-store' const APP_ICON: Record = { @@ -284,7 +286,7 @@ export function PreviewSidebar() { ) : null} {NAV_GROUPS.map((group, gi) => (
- {gi > 0 ? : null} + {gi > 0 ? : null} + dot?: 'ok' | 'er' | 'mu' run: () => void } +const PALETTE_USERS = [ + { name: 'admin@shnt.top', role: 'Админ · 7 приложений', dot: 'ok' }, + { name: 'ops@shnt.top', role: 'Пользователь · 4 приложения', dot: 'er' }, + { name: 'denis@shnt.top', role: 'Админ · 7 приложений', dot: 'ok' }, + { name: 'dns-bot@shnt.top', role: 'Пользователь · Technitium DNS', dot: 'ok' }, + { name: 'former@shnt.top', role: 'Отключён', dot: 'mu' }, +] as const + +const PALETTE_APPS = [ + { name: 'Cloudflare Domain Manager', role: 'SSO · JWT' }, + { name: 'VPS Tracker', role: 'SSO · JWT' }, + { name: 'EvoBGP', role: 'SSO · JWT' }, + { name: 'EvoFirewall', role: 'SSO · JWT' }, + { name: 'Technitium DNS', role: 'OIDC' }, +] + +const PALETTE_OIDC = [ + { name: 'Technitium DNS', role: 'client_id dns-shnt · включён' }, + { name: 'Grafana (тест)', role: 'client_id grafana-test · выключен' }, +] + +const DOT_BG = { ok: 'bg-success', er: 'bg-destructive', mu: 'bg-muted-foreground' } as const + +/** Палитра ⌘K как на доске: 640px, группы заглавными, подсказки моноширинным справа, подвал с подсказкой. */ export function CommandPalette() { const { paletteOpen, setPaletteOpen } = usePreviewStore() const navigate = useNavigate() + const { theme, setTheme, resolvedTheme } = useTheme() const [query, setQuery] = useState('') const [active, setActive] = useState(0) - const entries = useMemo(() => { - const go = (to: string) => () => void navigate({ to }) - return [ - { id: 'act-user', group: 'Действия', label: 'Создать пользователя', hint: 'Пользователи', run: go('/design-preview/users') }, - { id: 'act-client', group: 'Действия', label: 'Создать OIDC-клиента', hint: 'OIDC-клиенты', run: go('/design-preview/oidc') }, - { id: 'act-passkey', group: 'Действия', label: 'Добавить passkey', hint: 'Безопасность', run: go('/design-preview/account') }, - ...NAV_ITEMS.map((i) => ({ id: `nav-${i.id}`, group: 'Разделы', label: i.label, hint: i.group, run: go(i.to) })), - ...MOCK_USERS.map((u) => ({ id: `user-${u.id}`, group: 'Пользователи', label: u.email, hint: u.is_admin ? 'Админ' : 'Пользователь', run: go('/design-preview/users') })), - ...APPS.map((a) => ({ id: `app-${a.id}`, group: 'Приложения', label: APP_SHORT[a.id], hint: a.url.replace('https://', ''), run: go('/design-preview/apps') })), - ...MOCK_OIDC_CLIENTS.map((c) => ({ id: `oidc-${c.id}`, group: 'OIDC-клиенты', label: c.name, hint: c.clientId, run: go('/design-preview/oidc') })), - ] - }, [navigate]) - const shown = useMemo(() => { const q = query.trim().toLowerCase() - const found = q ? entries.filter((e) => `${e.label} ${e.hint}`.toLowerCase().includes(q)) : entries + const match = (...t: string[]) => !q || t.some((x) => x.toLowerCase().includes(q)) + const go = (to: string) => () => void navigate({ to }) + const current = theme === 'light' || theme === 'dark' ? theme : resolvedTheme === 'light' ? 'light' : 'dark' + const list: PaletteEntry[] = [ + ...[ + { id: 'act-user', label: 'Создать пользователя', hint: 'U', icon: PlusIcon, run: go('/design-preview/users') }, + { id: 'act-client', label: 'Создать OIDC-клиент', hint: 'O', icon: PlusIcon, run: go('/design-preview/oidc') }, + { id: 'act-passkey', label: 'Добавить passkey', hint: 'K', icon: PlusIcon, run: go('/design-preview/account') }, + { id: 'act-logins', label: 'Открыть журнал входов', hint: 'L', icon: PlayIcon, run: go('/design-preview/logins') }, + { id: 'act-theme', label: 'Переключить тему', hint: 'T', icon: MoonIcon, run: () => setTheme(current === 'dark' ? 'light' : 'dark') }, + ] + .filter((e) => match(e.label)) + .map((e) => ({ ...e, group: 'Действия' })), + ...NAV_ITEMS.filter((i) => match(i.label, i.group)) + .slice(0, 7) + .map((i) => ({ id: `nav-${i.id}`, group: 'Разделы', label: i.label, hint: i.group, icon: i.icon, run: go(i.to) })), + ...PALETTE_USERS.filter((u) => match(u.name, u.role)) + .slice(0, 5) + .map((u) => ({ id: `user-${u.name}`, group: 'Пользователи', label: u.name, hint: u.role, dot: u.dot, run: go('/design-preview/users') })), + ...PALETTE_APPS.filter((a) => match(a.name)) + .slice(0, 4) + .map((a) => ({ id: `app-${a.name}`, group: 'Приложения', label: a.name, hint: a.role, dot: 'mu' as const, run: go('/design-preview/apps') })), + ...PALETTE_OIDC.filter((c) => match(c.name)) + .slice(0, 4) + .map((c) => ({ id: `oidc-${c.name}`, group: 'OIDC-клиенты', label: c.name, hint: c.role, dot: 'mu' as const, run: go('/design-preview/oidc') })), + ] // Запрос из трёх и более слов — черновик ассистента (как на доске «Палитра ⌘K»). if (query.trim().split(/\s+/).filter(Boolean).length >= 3) { return [ { id: 'ai', - group: 'Ассистент · запрос обычным текстом (черновик)', + group: 'Ассистент · запрос на естественном языке', label: `Понял так: «${query.trim()}»`, - hint: 'Найдено 6 событий у 2 пользователей — открыть журнал входов с фильтром. Результат нужно проверить.', + hint: 'Найдено 6 событий у 2 пользователей. Это черновик — результат нужно проверить.', run: () => void navigate({ to: '/design-preview/logins' }), } as PaletteEntry, - ...found, + ...list, ] } - return found - }, [entries, query, navigate]) + return list + }, [query, navigate, theme, resolvedTheme, setTheme]) const groups = useMemo(() => { const map = new Map>() @@ -553,17 +594,17 @@ export function CommandPalette() { return ( (o ? setPaletteOpen(true) : close())}> - + Командная палитра Разделы, пользователи, приложения и действия -
- +
+ { setQuery(e.target.value) setActive(0) @@ -584,42 +625,44 @@ export function CommandPalette() { /> Esc
-
+
{groups.length === 0 ? ( -

Ничего не найдено

+

Ничего не найдено

) : ( groups.map(([group, list]) => (
-
{group}
- {list.map((e) => ( - - ))} +
{group}
+ {list.map((e) => { + const Icon = e.icon + return ( + + ) + })}
)) )}
-
+
↑↓ выбор ↵ открыть Esc закрыть + Подсказка: «неудачные входы ops за сутки»