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
|
||||
}) {
|
||||
return (
|
||||
<header className="@container flex flex-wrap items-end justify-between gap-x-4 gap-y-3">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-balance">{title}</h1>
|
||||
<p className="text-muted-foreground max-w-prose text-sm text-pretty">{sub}</p>
|
||||
<header className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<h1 className="text-xl font-semibold tracking-[-0.01em]">{title}</h1>
|
||||
<p className="text-muted-foreground text-sm">{sub}</p>
|
||||
</div>
|
||||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</header>
|
||||
@@ -298,7 +298,7 @@ export function KvFrame({
|
||||
items: Array<{ label: string; value: ReactNode }>
|
||||
}) {
|
||||
return (
|
||||
<Frame spacing="sm" className="w-full">
|
||||
<Frame className="[--frame-panel-header-py-adjust:4px] w-full">
|
||||
<FrameHeader>
|
||||
<FrameTitle>{title}</FrameTitle>
|
||||
<FrameDescription>{description}</FrameDescription>
|
||||
@@ -331,7 +331,7 @@ export function CopyList({
|
||||
items: Array<{ label: string; value: string }>
|
||||
}) {
|
||||
return (
|
||||
<Frame spacing="sm" className="w-full">
|
||||
<Frame className="[--frame-panel-header-py-adjust:4px] w-full">
|
||||
<FrameHeader>
|
||||
<FrameTitle>{title}</FrameTitle>
|
||||
<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>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label={`Скопировать: ${item.label}`}
|
||||
onClick={() => {
|
||||
@@ -425,7 +424,7 @@ export function PendingChip({ mark }: { mark: 'new' | 'dirty' | 'deleted' }) {
|
||||
/** Вкладки страницы как на досках: линия-подчёркивание, число — мелким индексом. */
|
||||
export function PageTabsList({ children }: { children: ReactNode }) {
|
||||
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}
|
||||
</TabsList>
|
||||
)
|
||||
@@ -433,9 +432,9 @@ export function PageTabsList({ children }: { children: ReactNode }) {
|
||||
|
||||
export function PageTab({ value, label, count }: { value: string; label: string; count?: number | string }) {
|
||||
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}
|
||||
{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>
|
||||
)
|
||||
}
|
||||
@@ -468,7 +467,7 @@ export function FormFrame({
|
||||
children: ReactNode
|
||||
}) {
|
||||
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">
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<FrameTitle>{title}</FrameTitle>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function AppCards({
|
||||
action?: ReactNode
|
||||
}) {
|
||||
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">
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<FrameTitle>{title}</FrameTitle>
|
||||
@@ -109,7 +109,7 @@ export function ActivityFeed({
|
||||
action?: ReactNode
|
||||
}) {
|
||||
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">
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<FrameTitle>{title}</FrameTitle>
|
||||
|
||||
@@ -13,19 +13,25 @@ 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.205 0.01 264);
|
||||
--foreground: oklch(0.141 0.006 264);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.006 264);
|
||||
--popover: oklch(1 0 0);
|
||||
--secondary: oklch(0.955 0.005 264);
|
||||
--muted: oklch(0.95 0.006 264);
|
||||
--muted-foreground: oklch(0.44 0.02 264);
|
||||
--accent: oklch(0.93 0.01 264);
|
||||
--border: oklch(0.88 0.008 264);
|
||||
--input: oklch(0.86 0.008 264);
|
||||
--popover-foreground: oklch(0.141 0.006 264);
|
||||
--secondary: oklch(0.958 0.005 247.858);
|
||||
--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);
|
||||
--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);
|
||||
--sidebar: oklch(0.985 0.003 264);
|
||||
--sidebar-foreground: oklch(0.141 0.006 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);
|
||||
--destructive-foreground: oklch(0.45 0.2 27);
|
||||
--success: oklch(0.6 0.15 163);
|
||||
@@ -40,16 +46,22 @@ html[data-preview-density].dark {
|
||||
--background: oklch(0.133 0.011 264.376);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.191 0.011 264.376);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.191 0.011 264.376);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.252 0.01 264.376);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.252 0.01 264.376);
|
||||
--muted-foreground: oklch(0.648 0.008 264.376);
|
||||
--accent: oklch(0.285 0.022 264.376);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.488 0.243 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-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--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'] {
|
||||
--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);
|
||||
--popover: oklch(0.1 0 0);
|
||||
--muted: oklch(0.22 0 0);
|
||||
@@ -104,3 +125,9 @@ html[data-preview-density] #main-content :focus-visible {
|
||||
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">
|
||||
<b className="font-semibold">{title}</b> {children}
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setOpen(true)}>
|
||||
<Button type="button" variant="outline" onClick={() => setOpen(true)}>
|
||||
Объяснить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,12 +11,12 @@ const VALUE: Record<KpiTone, string> = {
|
||||
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> = {
|
||||
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',
|
||||
2: 'grid-cols-2 max-[640px]:grid-cols-1',
|
||||
3: 'grid-cols-1 min-[1241px]:grid-cols-3',
|
||||
4: 'grid-cols-1 min-[641px]:grid-cols-2 min-[1241px]:grid-cols-4',
|
||||
}
|
||||
|
||||
const GLYPH: Record<KpiTone, string> = { neutral: '', ok: '●', in: '', wn: '▲', er: '✕' }
|
||||
|
||||
@@ -202,7 +202,7 @@ export function AppLinksPage() {
|
||||
{ label: 'Скрытые', match: (r) => !r.enabled },
|
||||
]}
|
||||
tools={
|
||||
<Button type="button" size="sm" variant="outline" onClick={resetOrder}>
|
||||
<Button type="button" variant="outline" onClick={resetOrder}>
|
||||
Сбросить порядок
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function AuditPage() {
|
||||
description="Последние события · иконка отражает тип, цвет — важность"
|
||||
items={AUDIT_FEED}
|
||||
action={
|
||||
<Button variant="outline" size="sm" onClick={() => setTab('table')}>
|
||||
<Button variant="outline" onClick={() => setTab('table')}>
|
||||
Весь журнал
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ export function OidcPage() {
|
||||
tools={
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
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))
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<SidebarHeader className="pb-1.5">
|
||||
<PreviewAppSwitcher />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
@@ -285,7 +285,7 @@ export function PreviewSidebar() {
|
||||
{NAV_GROUPS.map((group, gi) => (
|
||||
<div key={group} className="flex flex-col">
|
||||
{gi > 0 ? <SidebarSeparator className="mx-2 my-1 hidden group-data-[collapsible=icon]:block" /> : null}
|
||||
<SidebarGroup>
|
||||
<SidebarGroup className="py-0">
|
||||
<SidebarGroupLabel
|
||||
render={
|
||||
<button
|
||||
@@ -294,13 +294,13 @@ export function PreviewSidebar() {
|
||||
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}
|
||||
<ChevronDownIcon className={cn('size-3.5 transition-transform', closed[group] && '-rotate-90')} aria-hidden />
|
||||
</SidebarGroupLabel>
|
||||
<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) => (
|
||||
<NavEntry
|
||||
key={item.id}
|
||||
@@ -330,7 +330,7 @@ const MONITOR_METRICS = [
|
||||
{ l: 'Сессии', v: '5', p: 40, tone: 'success' },
|
||||
] as const
|
||||
|
||||
/** Единый центр состояния: внимание + сводка + список проблем. Адаптивен по ширине шапки. */
|
||||
/** Единый центр состояния: внимание + сводка + список проблем. Адаптивен по ширине окна, как на досках. */
|
||||
function MonitorPopover() {
|
||||
const bad = MONITOR_ITEMS.filter((m) => m.tone !== 'ok')
|
||||
const errors = bad.filter((m) => m.tone === 'er').length
|
||||
@@ -344,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-[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 />
|
||||
<span className="text-warning hidden text-sm font-medium @[1000px]/header:inline">Внимание</span>
|
||||
<span className="hidden items-center gap-1.5 text-sm tabular-nums @[1240px]/header:inline-flex">
|
||||
<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 min-[1241px]:inline-flex">
|
||||
<span className="bg-destructive size-1.5 rounded-full" aria-hidden />
|
||||
{errors}
|
||||
<span className="bg-warning size-1.5 rounded-full" aria-hidden />
|
||||
{warnings}
|
||||
</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}
|
||||
</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>
|
||||
<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">
|
||||
@@ -425,7 +425,7 @@ function FreshnessPopover() {
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
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 />
|
||||
@@ -444,7 +444,7 @@ function FreshPill() {
|
||||
return (
|
||||
<span
|
||||
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 />
|
||||
Обновлено 14:02 · Live
|
||||
@@ -460,7 +460,7 @@ export function PreviewHeader() {
|
||||
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
|
||||
|
||||
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" />
|
||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||
<Breadcrumb className="min-w-0">
|
||||
@@ -476,7 +476,7 @@ export function PreviewHeader() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
size="icon"
|
||||
aria-label="Командная палитра (⌘K)"
|
||||
className="md:hidden"
|
||||
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="Включено" />
|
||||
) : null}
|
||||
<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 />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-48">
|
||||
@@ -265,9 +265,12 @@ export function PreviewSectionGrid<T extends object>({
|
||||
onReorder(String(active.id), String(over.id))
|
||||
}
|
||||
|
||||
const minWidth = visibleColumns.reduce(
|
||||
(sum, c) => sum + c.size,
|
||||
(selectable ? SELECT_COLUMN_SIZE : 0) + (onReorder ? DRAG_COLUMN_SIZE : 0) + ACTIONS_COLUMN_SIZE,
|
||||
// Колонки сжимаются под ширину рамки (текст переносится, как на досках); прокрутка — только на узких экранах.
|
||||
const minWidth = Math.round(
|
||||
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 clear = () => setRowSelection({})
|
||||
@@ -284,7 +287,7 @@ export function PreviewSectionGrid<T extends object>({
|
||||
emptyMessage={emptyMessage}
|
||||
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">
|
||||
<div className="flex min-w-0 flex-1 basis-52 flex-col gap-0.5">
|
||||
<FrameTitle>{title}</FrameTitle>
|
||||
@@ -293,9 +296,13 @@ export function PreviewSectionGrid<T extends object>({
|
||||
{typeof tools === 'function' ? tools(selectedIds) : tools}
|
||||
{card ? (
|
||||
<Tabs value={cards ? 'cards' : 'table'} onValueChange={(v) => setCards(v === 'cards')} aria-label="Вид списка">
|
||||
<TabsList>
|
||||
<TabsTrigger value="table">Таблица</TabsTrigger>
|
||||
<TabsTrigger value="cards">Карточки</TabsTrigger>
|
||||
<TabsList className="h-9 gap-0.5 p-[3px]">
|
||||
<TabsTrigger value="table" className="h-7 px-2.5 text-[12.5px]">
|
||||
Таблица
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cards" className="h-7 px-2.5 text-[12.5px]">
|
||||
Карточки
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
) : null}
|
||||
@@ -363,7 +370,6 @@ export function PreviewSectionGrid<T extends object>({
|
||||
<b className="mr-1.5 font-semibold">Выбрано: {selectedIds.length}</b>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (bulk?.onRun) bulk.onRun(selectedIds)
|
||||
@@ -375,7 +381,6 @@ export function PreviewSectionGrid<T extends object>({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (bulk?.offRun) bulk.offRun(selectedIds)
|
||||
@@ -387,7 +392,6 @@ export function PreviewSectionGrid<T extends object>({
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-destructive"
|
||||
onClick={() => {
|
||||
@@ -398,7 +402,7 @@ export function PreviewSectionGrid<T extends object>({
|
||||
Удалить…
|
||||
</Button>
|
||||
<span className="flex-1" />
|
||||
<Button type="button" size="sm" variant="outline" onClick={clear}>
|
||||
<Button type="button" variant="outline" onClick={clear}>
|
||||
Снять выбор
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -37,13 +37,13 @@ export function UnsavedBar() {
|
||||
<b className="font-semibold">{pending.length}</b> · изменения ещё не записаны в портал
|
||||
</span>
|
||||
<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
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="outline" onClick={discardAll}>
|
||||
<Button type="button" variant="outline" onClick={discardAll}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={applyAll}>
|
||||
<Button type="button" onClick={applyAll}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</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: '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: '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],
|
||||
)
|
||||
@@ -108,7 +108,7 @@ export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; hand
|
||||
onDelete: handlers.onBulkDelete,
|
||||
}}
|
||||
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…
|
||||
</Button>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user