fix(design-preview): размеры, отступы и пороги ширины как на досках Auth Portal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e1ad15ba3
commit
b82c8031c2
12 files changed
+89
-60
No files matched your search
@@ -60,10 +60,10 @@ export function PageHead({
|
|||||||
actions?: ReactNode
|
actions?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<header className="@container flex flex-wrap items-end justify-between gap-x-4 gap-y-3">
|
<header className="flex flex-wrap items-end justify-between gap-3">
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight text-balance">{title}</h1>
|
<h1 className="text-xl font-semibold tracking-[-0.01em]">{title}</h1>
|
||||||
<p className="text-muted-foreground max-w-prose text-sm text-pretty">{sub}</p>
|
<p className="text-muted-foreground text-sm">{sub}</p>
|
||||||
</div>
|
</div>
|
||||||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||||||
</header>
|
</header>
|
||||||
@@ -298,7 +298,7 @@ export function KvFrame({
|
|||||||
items: Array<{ label: string; value: ReactNode }>
|
items: Array<{ label: string; value: ReactNode }>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Frame spacing="sm" className="w-full">
|
<Frame className="[--frame-panel-header-py-adjust:4px] w-full">
|
||||||
<FrameHeader>
|
<FrameHeader>
|
||||||
<FrameTitle>{title}</FrameTitle>
|
<FrameTitle>{title}</FrameTitle>
|
||||||
<FrameDescription>{description}</FrameDescription>
|
<FrameDescription>{description}</FrameDescription>
|
||||||
@@ -331,7 +331,7 @@ export function CopyList({
|
|||||||
items: Array<{ label: string; value: string }>
|
items: Array<{ label: string; value: string }>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Frame spacing="sm" className="w-full">
|
<Frame className="[--frame-panel-header-py-adjust:4px] w-full">
|
||||||
<FrameHeader>
|
<FrameHeader>
|
||||||
<FrameTitle>{title}</FrameTitle>
|
<FrameTitle>{title}</FrameTitle>
|
||||||
<FrameDescription>{description}</FrameDescription>
|
<FrameDescription>{description}</FrameDescription>
|
||||||
@@ -347,7 +347,6 @@ export function CopyList({
|
|||||||
<span className="text-muted-foreground min-w-0 flex-1 basis-48 text-[13px]">{item.label}</span>
|
<span className="text-muted-foreground min-w-0 flex-1 basis-48 text-[13px]">{item.label}</span>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
aria-label={`Скопировать: ${item.label}`}
|
aria-label={`Скопировать: ${item.label}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -425,7 +424,7 @@ export function PendingChip({ mark }: { mark: 'new' | 'dirty' | 'deleted' }) {
|
|||||||
/** Вкладки страницы как на досках: линия-подчёркивание, число — мелким индексом. */
|
/** Вкладки страницы как на досках: линия-подчёркивание, число — мелким индексом. */
|
||||||
export function PageTabsList({ children }: { children: ReactNode }) {
|
export function PageTabsList({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<TabsList variant="line" className="h-10! w-full justify-start gap-6 overflow-x-auto border-b">
|
<TabsList variant="line" className="h-10! w-full justify-start gap-0.5 overflow-x-auto border-b">
|
||||||
{children}
|
{children}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
)
|
)
|
||||||
@@ -433,9 +432,9 @@ export function PageTabsList({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
export function PageTab({ value, label, count }: { value: string; label: string; count?: number | string }) {
|
export function PageTab({ value, label, count }: { value: string; label: string; count?: number | string }) {
|
||||||
return (
|
return (
|
||||||
<TabsTrigger value={value} className="px-1 text-sm after:-bottom-px!">
|
<TabsTrigger value={value} className="gap-1.5 px-4 text-sm after:-bottom-px!">
|
||||||
{label}
|
{label}
|
||||||
{count !== undefined ? <sup className="text-muted-foreground ml-1 text-[10px] font-medium tabular-nums">{count}</sup> : null}
|
{count !== undefined ? <em className="font-mono text-[10px] font-normal not-italic opacity-60">{count}</em> : null}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -468,7 +467,7 @@ export function FormFrame({
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Frame spacing="sm" className="w-full min-w-0">
|
<Frame className="[--frame-panel-header-py-adjust:4px] w-full min-w-0">
|
||||||
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex min-w-0 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<FrameTitle>{title}</FrameTitle>
|
<FrameTitle>{title}</FrameTitle>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function AppCards({
|
|||||||
action?: ReactNode
|
action?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Frame spacing="sm" className="w-full min-w-0">
|
<Frame className="[--frame-panel-header-py-adjust:4px] w-full min-w-0">
|
||||||
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex min-w-0 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<FrameTitle>{title}</FrameTitle>
|
<FrameTitle>{title}</FrameTitle>
|
||||||
@@ -109,7 +109,7 @@ export function ActivityFeed({
|
|||||||
action?: ReactNode
|
action?: ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Frame spacing="sm" className="w-full min-w-0">
|
<Frame className="[--frame-panel-header-py-adjust:4px] w-full min-w-0">
|
||||||
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
<FrameHeader className="flex-row flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex min-w-0 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<FrameTitle>{title}</FrameTitle>
|
<FrameTitle>{title}</FrameTitle>
|
||||||
|
|||||||
@@ -13,19 +13,25 @@ html[data-preview-density='compact'] [data-slot='sidebar-inset'] > div:last-chil
|
|||||||
/* Палитра MikrotikManager v2 (доски Auth Portal): только пока смонтирован макет. */
|
/* Палитра MikrotikManager v2 (доски Auth Portal): только пока смонтирован макет. */
|
||||||
html[data-preview-density] {
|
html[data-preview-density] {
|
||||||
--background: oklch(0.973 0.004 247.858);
|
--background: oklch(0.973 0.004 247.858);
|
||||||
--foreground: oklch(0.205 0.01 264);
|
--foreground: oklch(0.141 0.006 264);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.006 264);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--secondary: oklch(0.955 0.005 264);
|
--popover-foreground: oklch(0.141 0.006 264);
|
||||||
--muted: oklch(0.95 0.006 264);
|
--secondary: oklch(0.958 0.005 247.858);
|
||||||
--muted-foreground: oklch(0.44 0.02 264);
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
--accent: oklch(0.93 0.01 264);
|
--muted: oklch(0.958 0.005 247.858);
|
||||||
--border: oklch(0.88 0.008 264);
|
--muted-foreground: oklch(0.52 0.01 264);
|
||||||
--input: oklch(0.86 0.008 264);
|
--accent: oklch(0.942 0.016 264.376);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--border: oklch(0.904 0.006 264);
|
||||||
|
--input: oklch(0.904 0.006 264);
|
||||||
--ring: oklch(0.488 0.243 264.376);
|
--ring: oklch(0.488 0.243 264.376);
|
||||||
--sidebar: oklch(0.985 0.003 264);
|
--sidebar: oklch(0.985 0.003 264);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.006 264);
|
||||||
--sidebar-accent: oklch(0.93 0.01 264);
|
--sidebar-accent: oklch(0.93 0.01 264);
|
||||||
--sidebar-border: oklch(0.88 0.008 264);
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.904 0.006 264);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
--destructive-foreground: oklch(0.45 0.2 27);
|
--destructive-foreground: oklch(0.45 0.2 27);
|
||||||
--success: oklch(0.6 0.15 163);
|
--success: oklch(0.6 0.15 163);
|
||||||
@@ -40,16 +46,22 @@ html[data-preview-density].dark {
|
|||||||
--background: oklch(0.133 0.011 264.376);
|
--background: oklch(0.133 0.011 264.376);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.191 0.011 264.376);
|
--card: oklch(0.191 0.011 264.376);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.191 0.011 264.376);
|
--popover: oklch(0.191 0.011 264.376);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.252 0.01 264.376);
|
--secondary: oklch(0.252 0.01 264.376);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.252 0.01 264.376);
|
--muted: oklch(0.252 0.01 264.376);
|
||||||
--muted-foreground: oklch(0.648 0.008 264.376);
|
--muted-foreground: oklch(0.648 0.008 264.376);
|
||||||
--accent: oklch(0.285 0.022 264.376);
|
--accent: oklch(0.285 0.022 264.376);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.488 0.243 264.376);
|
--ring: oklch(0.488 0.243 264.376);
|
||||||
--sidebar: oklch(0.191 0.011 264.376);
|
--sidebar: oklch(0.191 0.011 264.376);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.262 0.014 264.376);
|
--sidebar-accent: oklch(0.262 0.014 264.376);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||||
@@ -70,6 +82,15 @@ html[data-preview-density][data-preview-contrast='high'] {
|
|||||||
|
|
||||||
html[data-preview-density].dark[data-preview-contrast='high'] {
|
html[data-preview-density].dark[data-preview-contrast='high'] {
|
||||||
--background: oklch(0 0 0);
|
--background: oklch(0 0 0);
|
||||||
|
--foreground: oklch(1 0 0);
|
||||||
|
--success: oklch(0.82 0.19 150);
|
||||||
|
--success-foreground: oklch(0.9 0.15 150);
|
||||||
|
--warning: oklch(0.9 0.17 95);
|
||||||
|
--warning-foreground: oklch(0.95 0.12 95);
|
||||||
|
--destructive: oklch(0.78 0.2 25);
|
||||||
|
--destructive-foreground: oklch(0.88 0.12 25);
|
||||||
|
--info: oklch(0.8 0.15 300);
|
||||||
|
--info-foreground: oklch(0.9 0.1 300);
|
||||||
--card: oklch(0.12 0 0);
|
--card: oklch(0.12 0 0);
|
||||||
--popover: oklch(0.1 0 0);
|
--popover: oklch(0.1 0 0);
|
||||||
--muted: oklch(0.22 0 0);
|
--muted: oklch(0.22 0 0);
|
||||||
@@ -104,3 +125,9 @@ html[data-preview-density] #main-content :focus-visible {
|
|||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Шапка таблицы как на досках: 12px, лёгкая подложка. */
|
||||||
|
html[data-preview-density] [data-slot='data-grid-table'] thead th {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
background: color-mix(in oklch, var(--muted) 30%, transparent);
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ export function ExplainBanner({
|
|||||||
<div className="min-w-0 flex-1 basis-80 text-pretty">
|
<div className="min-w-0 flex-1 basis-80 text-pretty">
|
||||||
<b className="font-semibold">{title}</b> {children}
|
<b className="font-semibold">{title}</b> {children}
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)}>
|
<Button type="button" variant="outline" onClick={() => setOpen(true)}>
|
||||||
Объяснить
|
Объяснить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ const VALUE: Record<KpiTone, string> = {
|
|||||||
er: 'text-destructive',
|
er: 'text-destructive',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Колонки по числу ячеек: 4 → 4/2/1, 3 → 3/1, 2 → 2/1 (пороги контейнера как на досках). */
|
/** Колонки по числу ячеек и ширине окна (на досках — ширина приложения): 4 → 4/2/1, 3 → 3/1, 2 → 2/1. */
|
||||||
const COLS: Record<number, string> = {
|
const COLS: Record<number, string> = {
|
||||||
1: 'grid-cols-1',
|
1: 'grid-cols-1',
|
||||||
2: 'grid-cols-1 @[640px]:grid-cols-2',
|
2: 'grid-cols-2 max-[640px]:grid-cols-1',
|
||||||
3: 'grid-cols-1 @[1240px]:grid-cols-3',
|
3: 'grid-cols-1 min-[1241px]:grid-cols-3',
|
||||||
4: 'grid-cols-1 @[640px]:grid-cols-2 @[1240px]:grid-cols-4',
|
4: 'grid-cols-1 min-[641px]:grid-cols-2 min-[1241px]:grid-cols-4',
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLYPH: Record<KpiTone, string> = { neutral: '', ok: '●', in: '', wn: '▲', er: '✕' }
|
const GLYPH: Record<KpiTone, string> = { neutral: '', ok: '●', in: '', wn: '▲', er: '✕' }
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export function AppLinksPage() {
|
|||||||
{ label: 'Скрытые', match: (r) => !r.enabled },
|
{ label: 'Скрытые', match: (r) => !r.enabled },
|
||||||
]}
|
]}
|
||||||
tools={
|
tools={
|
||||||
<Button type="button" size="sm" variant="outline" onClick={resetOrder}>
|
<Button type="button" variant="outline" onClick={resetOrder}>
|
||||||
Сбросить порядок
|
Сбросить порядок
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function AuditPage() {
|
|||||||
description="Последние события · иконка отражает тип, цвет — важность"
|
description="Последние события · иконка отражает тип, цвет — важность"
|
||||||
items={AUDIT_FEED}
|
items={AUDIT_FEED}
|
||||||
action={
|
action={
|
||||||
<Button variant="outline" size="sm" onClick={() => setTab('table')}>
|
<Button variant="outline" onClick={() => setTab('table')}>
|
||||||
Весь журнал
|
Весь журнал
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ export function OidcPage() {
|
|||||||
tools={
|
tools={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setRotating(clients.find((c) => c.stale) ?? clients[0] ?? null)}
|
onClick={() => setRotating(clients.find((c) => c.stale) ?? clients[0] ?? null)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export function PreviewSidebar() {
|
|||||||
const pinned = NAV_ITEMS.filter((i) => pins.includes(i.id))
|
const pinned = NAV_ITEMS.filter((i) => pins.includes(i.id))
|
||||||
return (
|
return (
|
||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader>
|
<SidebarHeader className="pb-1.5">
|
||||||
<PreviewAppSwitcher />
|
<PreviewAppSwitcher />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
@@ -285,7 +285,7 @@ export function PreviewSidebar() {
|
|||||||
{NAV_GROUPS.map((group, gi) => (
|
{NAV_GROUPS.map((group, gi) => (
|
||||||
<div key={group} className="flex flex-col">
|
<div key={group} className="flex flex-col">
|
||||||
{gi > 0 ? <SidebarSeparator className="mx-2 my-1 hidden group-data-[collapsible=icon]:block" /> : null}
|
{gi > 0 ? <SidebarSeparator className="mx-2 my-1 hidden group-data-[collapsible=icon]:block" /> : null}
|
||||||
<SidebarGroup>
|
<SidebarGroup className="py-0">
|
||||||
<SidebarGroupLabel
|
<SidebarGroupLabel
|
||||||
render={
|
render={
|
||||||
<button
|
<button
|
||||||
@@ -294,13 +294,13 @@ export function PreviewSidebar() {
|
|||||||
onClick={() => setClosed((c) => ({ ...c, [group]: !c[group] }))}
|
onClick={() => setClosed((c) => ({ ...c, [group]: !c[group] }))}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
className="cursor-pointer justify-between text-[11px] font-semibold tracking-wide uppercase"
|
className="h-7 cursor-pointer justify-between text-[11px] font-semibold tracking-wide uppercase"
|
||||||
>
|
>
|
||||||
{group}
|
{group}
|
||||||
<ChevronDownIcon className={cn('size-3.5 transition-transform', closed[group] && '-rotate-90')} aria-hidden />
|
<ChevronDownIcon className={cn('size-3.5 transition-transform', closed[group] && '-rotate-90')} aria-hidden />
|
||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
<SidebarGroupContent className={cn(closed[group] && 'group-data-[collapsible=icon]:block hidden')}>
|
<SidebarGroupContent className={cn(closed[group] && 'group-data-[collapsible=icon]:block hidden')}>
|
||||||
<SidebarMenu>
|
<SidebarMenu className="gap-px">
|
||||||
{NAV_ITEMS.filter((i) => i.group === group).map((item) => (
|
{NAV_ITEMS.filter((i) => i.group === group).map((item) => (
|
||||||
<NavEntry
|
<NavEntry
|
||||||
key={item.id}
|
key={item.id}
|
||||||
@@ -330,7 +330,7 @@ const MONITOR_METRICS = [
|
|||||||
{ l: 'Сессии', v: '5', p: 40, tone: 'success' },
|
{ l: 'Сессии', v: '5', p: 40, tone: 'success' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
/** Единый центр состояния: внимание + сводка + список проблем. Адаптивен по ширине шапки. */
|
/** Единый центр состояния: внимание + сводка + список проблем. Адаптивен по ширине окна, как на досках. */
|
||||||
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 errors = bad.filter((m) => m.tone === 'er').length
|
||||||
@@ -344,22 +344,22 @@ function MonitorPopover() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Состояние системы: внимание, ${errors} сбой, ${warnings} предупреждения`}
|
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-[760px]/header:w-8 @max-[760px]/header:border-transparent @max-[760px]/header:bg-transparent @max-[760px]/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]:w-8 max-[760px]:border-transparent max-[760px]:bg-transparent max-[760px]:px-0"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ActivityIcon className="text-warning size-4" aria-hidden />
|
<ActivityIcon className="text-warning size-4" aria-hidden />
|
||||||
<span className="text-warning hidden text-sm font-medium @[1000px]/header:inline">Внимание</span>
|
<span className="text-warning hidden text-sm font-medium min-[1001px]:inline">Внимание</span>
|
||||||
<span className="hidden items-center gap-1.5 text-sm tabular-nums @[1240px]/header:inline-flex">
|
<span className="hidden items-center gap-1.5 text-sm tabular-nums min-[1241px]:inline-flex">
|
||||||
<span className="bg-destructive size-1.5 rounded-full" aria-hidden />
|
<span className="bg-destructive size-1.5 rounded-full" aria-hidden />
|
||||||
{errors}
|
{errors}
|
||||||
<span className="bg-warning size-1.5 rounded-full" aria-hidden />
|
<span className="bg-warning size-1.5 rounded-full" aria-hidden />
|
||||||
{warnings}
|
{warnings}
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-destructive hidden size-4.5 min-w-4.5 items-center justify-center rounded-full px-1 text-[11px] font-semibold text-white @[760px]/header:inline-flex @[1240px]/header:hidden">
|
<span className="bg-destructive hidden size-4.5 min-w-4.5 items-center justify-center rounded-full px-1 text-[11px] font-semibold text-white min-[761px]:inline-flex min-[1241px]:hidden">
|
||||||
{bad.length}
|
{bad.length}
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-destructive ring-background absolute top-1 right-1 size-2 rounded-full ring-2 @[760px]/header:hidden" aria-hidden />
|
<span className="bg-destructive ring-background absolute top-1 right-1 size-2 rounded-full ring-2 min-[761px]:hidden" aria-hidden />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="end" className="flex w-[22rem] max-w-[calc(100vw-1.5rem)] flex-col gap-2 p-2">
|
<PopoverContent align="end" className="flex w-[22rem] max-w-[calc(100vw-1.5rem)] flex-col gap-2 p-2">
|
||||||
<div className="flex items-start justify-between gap-2 px-1.5 pt-1.5">
|
<div className="flex items-start justify-between gap-2 px-1.5 pt-1.5">
|
||||||
@@ -425,7 +425,7 @@ function FreshnessPopover() {
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
render={
|
render={
|
||||||
<Button type="button" variant="ghost" size="icon-sm" aria-label="Источники и свежесть данных" title="Источники и свежесть данных" />
|
<Button type="button" variant="ghost" size="icon" aria-label="Источники и свежесть данных" title="Источники и свежесть данных" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<InfoIcon aria-hidden />
|
<InfoIcon aria-hidden />
|
||||||
@@ -444,7 +444,7 @@ function FreshPill() {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
role="status"
|
role="status"
|
||||||
className="text-muted-foreground hidden h-[30px] items-center gap-2 rounded-lg border px-2.5 text-xs @[1320px]/header:inline-flex"
|
className="text-muted-foreground hidden h-[30px] items-center gap-2 rounded-lg border px-2.5 text-xs min-[1321px]:inline-flex"
|
||||||
>
|
>
|
||||||
<span className="bg-success size-1.5 rounded-full" aria-hidden />
|
<span className="bg-success size-1.5 rounded-full" aria-hidden />
|
||||||
Обновлено 14:02 · Live
|
Обновлено 14:02 · Live
|
||||||
@@ -460,7 +460,7 @@ export function PreviewHeader() {
|
|||||||
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 (
|
||||||
<header className="bg-background/80 @container/header sticky top-0 z-20 flex min-h-12 shrink-0 items-center gap-2 border-b px-4 py-1.5 backdrop-blur md:px-6">
|
<header className="bg-background/80 sticky top-0 z-20 flex min-h-12 shrink-0 items-center gap-2 border-b px-4 py-1.5 backdrop-blur md:px-6">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<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">
|
||||||
@@ -476,7 +476,7 @@ export function PreviewHeader() {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon-sm"
|
size="icon"
|
||||||
aria-label="Командная палитра (⌘K)"
|
aria-label="Командная палитра (⌘K)"
|
||||||
className="md:hidden"
|
className="md:hidden"
|
||||||
onClick={() => setPaletteOpen(true)}
|
onClick={() => setPaletteOpen(true)}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
<Switch size="sm" checked={on} onCheckedChange={(v) => rowSwitch.onChange(r, v)} aria-label="Включено" />
|
<Switch size="sm" checked={on} onCheckedChange={(v) => rowSwitch.onChange(r, v)} aria-label="Включено" />
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger render={<Button type="button" variant="ghost" size="icon-sm" aria-label="Действия" />}>
|
<DropdownMenuTrigger render={<Button type="button" variant="ghost" size="icon" aria-label="Действия" />}>
|
||||||
<MoreHorizontalIcon aria-hidden />
|
<MoreHorizontalIcon aria-hidden />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="min-w-48">
|
<DropdownMenuContent align="end" className="min-w-48">
|
||||||
@@ -265,9 +265,12 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
onReorder(String(active.id), String(over.id))
|
onReorder(String(active.id), String(over.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const minWidth = visibleColumns.reduce(
|
// Колонки сжимаются под ширину рамки (текст переносится, как на досках); прокрутка — только на узких экранах.
|
||||||
(sum, c) => sum + c.size,
|
const minWidth = Math.round(
|
||||||
(selectable ? SELECT_COLUMN_SIZE : 0) + (onReorder ? DRAG_COLUMN_SIZE : 0) + ACTIONS_COLUMN_SIZE,
|
visibleColumns.reduce(
|
||||||
|
(sum, c) => sum + c.size,
|
||||||
|
(selectable ? SELECT_COLUMN_SIZE : 0) + (onReorder ? DRAG_COLUMN_SIZE : 0) + ACTIONS_COLUMN_SIZE,
|
||||||
|
) * 0.7,
|
||||||
)
|
)
|
||||||
const selectedIds = table.getSelectedRowModel().rows.map((r) => r.id)
|
const selectedIds = table.getSelectedRowModel().rows.map((r) => r.id)
|
||||||
const clear = () => setRowSelection({})
|
const clear = () => setRowSelection({})
|
||||||
@@ -284,7 +287,7 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
tableLayout={{ headerSticky: false, dense: false, width: 'auto', rowsDraggable: Boolean(onReorder) }}
|
tableLayout={{ headerSticky: false, dense: false, width: 'auto', rowsDraggable: Boolean(onReorder) }}
|
||||||
>
|
>
|
||||||
<Frame spacing="sm" className={cn('w-full min-w-0', className)}>
|
<Frame className={cn('[--frame-panel-header-py-adjust:4px] w-full min-w-0', className)}>
|
||||||
<FrameHeader className="flex-row flex-wrap items-center gap-x-3 gap-y-2">
|
<FrameHeader className="flex-row flex-wrap items-center gap-x-3 gap-y-2">
|
||||||
<div className="flex min-w-0 flex-1 basis-52 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-1 basis-52 flex-col gap-0.5">
|
||||||
<FrameTitle>{title}</FrameTitle>
|
<FrameTitle>{title}</FrameTitle>
|
||||||
@@ -293,9 +296,13 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
{typeof tools === 'function' ? tools(selectedIds) : tools}
|
{typeof tools === 'function' ? tools(selectedIds) : tools}
|
||||||
{card ? (
|
{card ? (
|
||||||
<Tabs value={cards ? 'cards' : 'table'} onValueChange={(v) => setCards(v === 'cards')} aria-label="Вид списка">
|
<Tabs value={cards ? 'cards' : 'table'} onValueChange={(v) => setCards(v === 'cards')} aria-label="Вид списка">
|
||||||
<TabsList>
|
<TabsList className="h-9 gap-0.5 p-[3px]">
|
||||||
<TabsTrigger value="table">Таблица</TabsTrigger>
|
<TabsTrigger value="table" className="h-7 px-2.5 text-[12.5px]">
|
||||||
<TabsTrigger value="cards">Карточки</TabsTrigger>
|
Таблица
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="cards" className="h-7 px-2.5 text-[12.5px]">
|
||||||
|
Карточки
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -363,7 +370,6 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
<b className="mr-1.5 font-semibold">Выбрано: {selectedIds.length}</b>
|
<b className="mr-1.5 font-semibold">Выбрано: {selectedIds.length}</b>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (bulk?.onRun) bulk.onRun(selectedIds)
|
if (bulk?.onRun) bulk.onRun(selectedIds)
|
||||||
@@ -375,7 +381,6 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (bulk?.offRun) bulk.offRun(selectedIds)
|
if (bulk?.offRun) bulk.offRun(selectedIds)
|
||||||
@@ -387,7 +392,6 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -398,7 +402,7 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
Удалить…
|
Удалить…
|
||||||
</Button>
|
</Button>
|
||||||
<span className="flex-1" />
|
<span className="flex-1" />
|
||||||
<Button type="button" size="sm" variant="outline" onClick={clear}>
|
<Button type="button" variant="outline" onClick={clear}>
|
||||||
Снять выбор
|
Снять выбор
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ export function UnsavedBar() {
|
|||||||
<b className="font-semibold">{pending.length}</b> · изменения ещё не записаны в портал
|
<b className="font-semibold">{pending.length}</b> · изменения ещё не записаны в портал
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Button type="button" size="sm" variant="outline" onClick={() => setDiffOpen(true)}>
|
<Button type="button" variant="outline" onClick={() => setDiffOpen(true)}>
|
||||||
Просмотреть diff
|
Просмотреть diff
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" size="sm" variant="outline" onClick={discardAll}>
|
<Button type="button" variant="outline" onClick={discardAll}>
|
||||||
Отменить
|
Отменить
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" size="sm" onClick={applyAll}>
|
<Button type="button" onClick={applyAll}>
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; hand
|
|||||||
{ id: 'role', title: 'Роль', size: 130, sortValue: (u) => (u.is_admin ? 'a' : 'u'), cell: (u) => <span className="font-mono text-[12.5px]">{u.is_admin ? 'Админ' : 'Пользователь'}</span> },
|
{ id: 'role', title: 'Роль', size: 130, sortValue: (u) => (u.is_admin ? 'a' : 'u'), cell: (u) => <span className="font-mono text-[12.5px]">{u.is_admin ? 'Админ' : 'Пользователь'}</span> },
|
||||||
{ id: 'apps', title: 'Приложения', size: 220, cell: (u) => <span className="block truncate font-mono text-[12.5px]">{appsSummary(u)}</span> },
|
{ id: 'apps', title: 'Приложения', size: 220, cell: (u) => <span className="block truncate font-mono text-[12.5px]">{appsSummary(u)}</span> },
|
||||||
{ id: 'passkey', title: 'Passkey', size: 100, sortValue: (u) => u.passkey_count, cell: (u) => <span className="font-mono text-[12.5px]">{passkeyLabel(u.passkey_count)}</span> },
|
{ id: 'passkey', title: 'Passkey', size: 100, sortValue: (u) => u.passkey_count, cell: (u) => <span className="font-mono text-[12.5px]">{passkeyLabel(u.passkey_count)}</span> },
|
||||||
{ id: 'activity', title: 'Активность · IP', size: 240, cell: (u) => <span className="font-mono text-[12.5px] whitespace-nowrap">{USER_META[u.id]?.activity ?? '—'}</span> },
|
{ id: 'activity', title: 'Активность · IP', size: 240, cell: (u) => <span className="font-mono text-[12.5px]">{USER_META[u.id]?.activity ?? '—'}</span> },
|
||||||
],
|
],
|
||||||
[marks],
|
[marks],
|
||||||
)
|
)
|
||||||
@@ -108,7 +108,7 @@ export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; hand
|
|||||||
onDelete: handlers.onBulkDelete,
|
onDelete: handlers.onBulkDelete,
|
||||||
}}
|
}}
|
||||||
tools={(ids) => (
|
tools={(ids) => (
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => handlers.onBulkResetPasskeys(ids.length > 0 ? ids : ['u-3'])}>
|
<Button type="button" variant="outline" onClick={() => handlers.onBulkResetPasskeys(ids.length > 0 ? ids : ['u-3'])}>
|
||||||
Сбросить passkeys…
|
Сбросить passkeys…
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user