From 7cd86fcda195e34f0ad5462a51003ef664caa468 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 17 Aug 2026 15:36:54 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=B2=D1=8B=D1=80=D0=BE=D0=B2=D0=BD?= =?UTF-8?q?=D1=8F=D1=82=D1=8C=20KPI=20IconTile=20chrome=20=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BD=D0=BE=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переименовать KpiStats, добавить skip-link и IconTile в kit. Co-authored-by: Cursor --- .cursor/rules/kpi-hybrid.mdc | 19 +-- .cursor/rules/reui-mcp.mdc | 2 +- AGENTS.md | 36 +++++ ...{cert-kpi-cards.tsx => cert-kpi-stats.tsx} | 6 +- ...ain-kpi-cards.tsx => domain-kpi-stats.tsx} | 6 +- apps/web/src/components/empty-state.tsx | 4 +- ...roup-kpi-cards.tsx => group-kpi-stats.tsx} | 6 +- apps/web/src/components/layout/app-shell.tsx | 10 +- apps/web/src/components/query-state.tsx | 6 +- .../src/components/reui-kit/kpi-stat-grid.tsx | 17 +-- .../components/reui-kit/quick-action-grid.tsx | 17 +-- apps/web/src/components/reui/icon-tile.tsx | 125 ++++++++++++++++++ ...ce-kpi-cards.tsx => service-kpi-stats.tsx} | 6 +- apps/web/src/lib/ui-surface.ts | 21 +++ apps/web/src/routes/_auth/certificates.tsx | 4 +- apps/web/src/routes/_auth/domains/index.tsx | 4 +- apps/web/src/routes/_auth/groups.tsx | 4 +- apps/web/src/routes/_auth/services.tsx | 4 +- docs/ui-design-contract.md | 6 +- packages/ui/src/components/sheet.tsx | 5 +- packages/ui/src/components/sidebar.tsx | 17 ++- 21 files changed, 256 insertions(+), 69 deletions(-) create mode 100644 AGENTS.md rename apps/web/src/components/{cert-kpi-cards.tsx => cert-kpi-stats.tsx} (97%) rename apps/web/src/components/{domain-kpi-cards.tsx => domain-kpi-stats.tsx} (96%) rename apps/web/src/components/{group-kpi-cards.tsx => group-kpi-stats.tsx} (95%) create mode 100644 apps/web/src/components/reui/icon-tile.tsx rename apps/web/src/components/{service-kpi-cards.tsx => service-kpi-stats.tsx} (96%) create mode 100644 apps/web/src/lib/ui-surface.ts diff --git a/.cursor/rules/kpi-hybrid.mdc b/.cursor/rules/kpi-hybrid.mdc index f52f964..39b9a46 100644 --- a/.cursor/rules/kpi-hybrid.mdc +++ b/.cursor/rules/kpi-hybrid.mdc @@ -1,11 +1,11 @@ --- -description: Только hybrid KPI — KpiStatGrid / row tile DNA (stats-12). Запрет SectionCards и hand-roll. +description: Только hybrid KPI — KpiStatGrid / row tile DNA (stats-12 + IconTile). Запрет SectionCards и hand-roll. alwaysApply: true --- -# KPI hybrid — только kit (stats-12 DNA) +# KPI hybrid — только kit (stats-12 + IconTile) -Preview: [stats-12](https://reui.io/preview/base/stats-12). SoT DNA = EvoBGP. Markup в проекте: `apps/web/src/components/reui-kit/kpi-stat-grid.tsx`. +Preview: [stats-12](https://reui.io/preview/base/stats-12). Primitive: [icon-tile](https://reui.io/docs/components/base/icon-tile). SoT DNA = EvoBGP. Markup: `apps/web/src/components/reui-kit/kpi-stat-grid.tsx`. Связанные: [`reui-mcp.mdc`](reui-mcp.mdc), [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc). @@ -14,16 +14,17 @@ Preview: [stats-12](https://reui.io/preview/base/stats-12). SoT DNA = EvoBGP. Ma | Зона | Компонент / DNA | |------|-----------------| | KPI-полосы / dashboard metrics | только `reui-kit/KpiStatGrid` (через `OpsDashboard` / `DetailPanel.Metrics` при наличии) | -| Markup | horizontal compact hybrid: icon left `Item` `size-10.5` `bg-muted` + `border-background` + shadow + `ItemMedia` + label/Badge + value ± `variant` | -| Row icon tiles (data-grid) | та же DNA — semantic `text-*` на `bg-muted` | -| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA) | +| Markup | horizontal compact hybrid: icon left `IconTile` `variant="elevated"` `className="size-10.5"` + label/Badge + value ± `variant` | +| Row icon tiles (data-grid) | та же DNA — semantic `text-*` на IconTile elevated | +| Quick Actions | только `reui-kit/QuickActionGrid` (sibling hybrid DNA, IconTile elevated) | -Импорты UI: `@cfdm/ui/components/*`. +Импорты UI: `@cfdm/ui/components/*`. IconTile: `@/components/reui/icon-tile`. ## NEVER - SectionCards / vertical-only KPI / hand-roll Frame/Card KPI -- Другой size / radius / solid brand fill вместо `bg-muted` +- `Item` `size-10.5` `bg-muted` hybrid вместо IconTile +- Другой size / radius / solid brand fill вместо `elevated` - `card-35` как замена stats-12 hybrid KPI - Копипаст ReUI block в route — adapt через `reui-kit/` -- Голый lucide `size-4` в name-cell без hybrid tile +- Голый lucide `size-4` в name-cell без IconTile diff --git a/.cursor/rules/reui-mcp.mdc b/.cursor/rules/reui-mcp.mdc index ab0b8cf..ed5b41a 100644 --- a/.cursor/rules/reui-mcp.mdc +++ b/.cursor/rules/reui-mcp.mdc @@ -75,7 +75,7 @@ alwaysApply: true ## Установленные ReUI (apps/web) -**Components:** `frame`, `data-grid/*`, `filters`, `kanban`, `badge`, `alert`, `autocomplete`, `number-field`, `date-selector`, `color-picker`, `timeline`, `rating`, `phone-input`, `icon-stack` +**Components:** `frame`, `data-grid/*`, `filters`, `kanban`, `badge`, `alert`, `autocomplete`, `number-field`, `date-selector`, `color-picker`, `timeline`, `rating`, `phone-input`, `icon-stack`, `icon-tile` **Kit:** `ResourcePage`, `KpiStatGrid`, `QuickActionGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0e8884f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# Cloudflare Domain Manager — Руководство для ИИ + +## Описание + +CFDM — self-hosted управление доменами, DNS и SSL через Cloudflare API. Группы доменов, сервисы, health-check, балансировка. + +## Стек + +- Monorepo: pnpm + turbo (`apps/*`, `packages/*`) +- `apps/web` — Vite + React + TanStack + shadcn **base-nova** + ReUI Frame +- `apps/api` — Fastify + Drizzle + SQLite +- Packages: `@cfdm/ui`, `@cfdm/shared`, `@cfdm/db` + +## Команды + +```bash +pnpm install +pnpm --filter @cfdm/shared build +pnpm --filter @cfdm/db build +pnpm --filter @cfdm/api dev +pnpm --filter web dev +pnpm --filter web build +``` + +## ReUI PRO + +Surface: **frame** (`apps/web/src/lib/ui-surface.ts` → `UI_SURFACE = 'frame'`). +Hierarchy: **ReUI PRO > shadcn**. Contract: [`docs/ui-design-contract.md`](docs/ui-design-contract.md). +MCP `user-reui` primary + `plugin-shadcn-shadcn` primitives. CLI из `apps/web`. +KPI/QuickActions: EvoBGP hybrid SoT (`KpiStatGrid`, `QuickActionGrid`) + `IconTile` `elevated` `size-10.5`. +Docs: [Introduction](https://reui.io/docs) · [llms.txt](https://reui.io/llms.txt) · [MCP](https://reui.io/docs/mcp) · [Agent Skills](https://reui.io/docs/agent-skills). +Skill: `.claude/skills/reui` (`668fb463eb`). + +## Docs + +[`docs/Home.md`](docs/Home.md), [`CONTRIBUTING.md`](CONTRIBUTING.md), [`docs/ui-design-contract.md`](docs/ui-design-contract.md). diff --git a/apps/web/src/components/cert-kpi-cards.tsx b/apps/web/src/components/cert-kpi-stats.tsx similarity index 97% rename from apps/web/src/components/cert-kpi-cards.tsx rename to apps/web/src/components/cert-kpi-stats.tsx index ce36f36..33a3eec 100644 --- a/apps/web/src/components/cert-kpi-cards.tsx +++ b/apps/web/src/components/cert-kpi-stats.tsx @@ -25,7 +25,7 @@ function totalFromSummary(summary: [string, number][] | undefined): number { return summary.reduce((sum, [, count]) => sum + count, 0) } -interface CertKpiCardsProps { +interface CertKpiStatsProps { summary?: [string, number][] total?: number activeTab: string @@ -34,13 +34,13 @@ interface CertKpiCardsProps { } /** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ -export function CertKpiCards({ +export function CertKpiStats({ summary, total, activeTab, isLoading, onSelectTab, -}: CertKpiCardsProps) { +}: CertKpiStatsProps) { const cards = useMemo(() => { const allCount = total ?? totalFromSummary(summary) const okCount = countFromSummary(summary, ACTIVE_STATUSES) diff --git a/apps/web/src/components/domain-kpi-cards.tsx b/apps/web/src/components/domain-kpi-stats.tsx similarity index 96% rename from apps/web/src/components/domain-kpi-cards.tsx rename to apps/web/src/components/domain-kpi-stats.tsx index 4d6d157..4297c37 100644 --- a/apps/web/src/components/domain-kpi-cards.tsx +++ b/apps/web/src/components/domain-kpi-stats.tsx @@ -8,7 +8,7 @@ import { import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' import type { DomainListItem } from '@/lib/schemas' -interface DomainKpiCardsProps { +interface DomainKpiStatsProps { domains: DomainListItem[] | undefined activeTab: string isLoading?: boolean @@ -16,12 +16,12 @@ interface DomainKpiCardsProps { } /** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ -export function DomainKpiCards({ +export function DomainKpiStats({ domains, activeTab, isLoading, onSelectTab, -}: DomainKpiCardsProps) { +}: DomainKpiStatsProps) { const cards = useMemo(() => { const list = domains ?? [] const withoutGroup = list.filter((d) => d.group_id == null).length diff --git a/apps/web/src/components/empty-state.tsx b/apps/web/src/components/empty-state.tsx index acff7ab..a0cd703 100644 --- a/apps/web/src/components/empty-state.tsx +++ b/apps/web/src/components/empty-state.tsx @@ -27,8 +27,8 @@ interface EmptyStateProps { } /** - * ReUI Empty + IconStack — adapted from empty-state-12. - * @see https://reui.io/preview/base/empty-state-12 + * ReUI Empty + IconStack — Frame-friendly empty-state-14 / empty-state-12. + * Preview: https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12 */ export function EmptyState({ icon: Icon = InboxIcon, diff --git a/apps/web/src/components/group-kpi-cards.tsx b/apps/web/src/components/group-kpi-stats.tsx similarity index 95% rename from apps/web/src/components/group-kpi-cards.tsx rename to apps/web/src/components/group-kpi-stats.tsx index 989862b..a0665e8 100644 --- a/apps/web/src/components/group-kpi-cards.tsx +++ b/apps/web/src/components/group-kpi-stats.tsx @@ -3,7 +3,7 @@ import { FolderOpenIcon, FolderTreeIcon, FoldersIcon } from 'lucide-react' import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' import type { GroupCatalogRow } from '@/components/columns/groups-columns' -interface GroupKpiCardsProps { +interface GroupKpiStatsProps { rows: GroupCatalogRow[] activeTab: string isLoading?: boolean @@ -11,12 +11,12 @@ interface GroupKpiCardsProps { } /** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ -export function GroupKpiCards({ +export function GroupKpiStats({ rows, activeTab, isLoading, onSelectTab, -}: GroupKpiCardsProps) { +}: GroupKpiStatsProps) { const cards = useMemo(() => { const withDomains = rows.filter((r) => r.domainCount > 0).length const empty = rows.filter((r) => r.domainCount === 0).length diff --git a/apps/web/src/components/layout/app-shell.tsx b/apps/web/src/components/layout/app-shell.tsx index b7d28fa..29da359 100644 --- a/apps/web/src/components/layout/app-shell.tsx +++ b/apps/web/src/components/layout/app-shell.tsx @@ -4,6 +4,7 @@ import { SiteHeader } from '@/components/layout/site-header' import { SearchMenu } from '@/components/layout/search-menu' import { TooltipProvider } from '@cfdm/ui/components/tooltip' import { SidebarInset, SidebarProvider } from '@cfdm/ui/components/sidebar' +import { SKIP_TO_CONTENT_CLASS } from '@/lib/ui-surface' /** Shared ops chrome — etalon EvoBGP. @see docs/ui-design-contract.md */ export function AppShell({ children }: { children: ReactNode }) { @@ -16,12 +17,15 @@ export function AppShell({ children }: { children: ReactNode }) { } as CSSProperties } > + + К содержимому + - + -
+
{children} -
+
diff --git a/apps/web/src/components/query-state.tsx b/apps/web/src/components/query-state.tsx index 7edd942..f0bfe22 100644 --- a/apps/web/src/components/query-state.tsx +++ b/apps/web/src/components/query-state.tsx @@ -35,7 +35,11 @@ export function QueryState({ children, }: QueryStateProps) { if (isLoading) { - return skeleton ?? + return ( +
+ {skeleton ?? } +
+ ) } if (isError) { diff --git a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx index 05caecc..e75db71 100644 --- a/apps/web/src/components/reui-kit/kpi-stat-grid.tsx +++ b/apps/web/src/components/reui-kit/kpi-stat-grid.tsx @@ -5,7 +5,7 @@ import { Frame, FramePanel } from '@/components/reui/frame' import { Badge } from '@/components/reui/badge' import { cn } from '@cfdm/ui/lib/utils' import { kpiCols } from './kpi-cols' -import { Item, ItemMedia } from '@cfdm/ui/components/item' +import { IconTile } from '@/components/reui/icon-tile' import { Skeleton } from '@cfdm/ui/components/skeleton' export type KpiStatVariant = 'default' | 'warning' | 'destructive' @@ -83,16 +83,13 @@ function KpiStatCardBody({ item }: { item: KpiStatItem }) { return (
{item.icon ? ( - + {item.icon} + ) : null}
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 7f1ea7c..12071bf 100644 --- a/apps/web/src/components/reui-kit/quick-action-grid.tsx +++ b/apps/web/src/components/reui-kit/quick-action-grid.tsx @@ -8,7 +8,7 @@ import { FrameTitle, } from '@/components/reui/frame' import { Badge } from '@/components/reui/badge' -import { Item, ItemMedia } from '@cfdm/ui/components/item' +import { IconTile } from '@/components/reui/icon-tile' import { cn } from '@cfdm/ui/lib/utils' import { kpiCols } from './kpi-cols' @@ -35,16 +35,13 @@ function QuickActionBody({ action }: { action: QuickActionItem }) { return (
{action.icon ? ( - + {action.icon} + ) : null}
diff --git a/apps/web/src/components/reui/icon-tile.tsx b/apps/web/src/components/reui/icon-tile.tsx new file mode 100644 index 0000000..deb0192 --- /dev/null +++ b/apps/web/src/components/reui/icon-tile.tsx @@ -0,0 +1,125 @@ +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@cfdm/ui/lib/utils" + +/** + * CSS variable architecture: + * + * The root owns four variables so every part of the tile stays in proportion + * and stays overridable from a single `className`: + * + * --icon-tile-size tile width/height + * --icon-tile-icon-size glyph size applied to child svgs + * --icon-tile-radius corner radius (also drives the nested inner card) + * --icon-tile-inset gap between the outer ring and the inner card + * + * The `frame` and `soft` variants paint their inner card with an `::after` + * pseudo element instead of a wrapper node. `isolate` makes the root a stacking + * context, so the negative z-index pseudo paints above the root background but + * below the in-flow icon - no extra DOM, and `render` composition keeps working. + * + * Tone: `soft` and `solid` derive every fill and border from `currentColor`, so + * a single text color class (e.g. `text-success`) retints the whole tile. They + * default to `text-primary`; override it to recolor without touching internals. + */ +const iconTileVariants = cva( + [ + "relative inline-flex shrink-0 items-center justify-center align-middle", + "size-(--icon-tile-size) rounded-(--icon-tile-radius)", + "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-(--icon-tile-icon-size)", + ], + { + variants: { + variant: { + /** Plain bordered surface. The quiet default for list rows and toolbars. */ + outline: "border border-border bg-background dark:bg-input/32", + /** Raised muted fill with a background-colored ring. Reads as a physical chip. */ + elevated: + "border-2 border-background bg-muted text-accent-foreground shadow-[0_1px_3px_0_rgb(0_0_0/0.14)] dark:border", + /** + * Tinted double container: an opacity-filled outer ring with no border + * around a bordered inner card, all derived from `currentColor`. The + * quiet, colorful sibling of `frame`. Retint with a text color class. + */ + soft: [ + "isolate p-(--icon-tile-inset) text-primary bg-current/10", + "after:absolute after:-z-10 after:inset-(--icon-tile-inset)", + "after:rounded-[calc(var(--icon-tile-radius)-var(--icon-tile-inset))]", + "after:border after:border-current/20 after:bg-current/5", + ], + /** Filled tone with a contrasting glyph. Retint with `bg-*` + a text color. */ + solid: "bg-primary text-primary-foreground", + /** Double container - a muted ring around an inset card, matching Frame. */ + frame: [ + "isolate border border-border bg-muted/50 p-(--icon-tile-inset)", + "after:absolute after:-z-10 after:inset-(--icon-tile-inset)", + "after:rounded-[calc(var(--icon-tile-radius)-var(--icon-tile-inset))]", + "after:border after:border-border after:bg-card after:shadow-xs", + ], + }, + size: { + xs: "[--icon-tile-size:--spacing(6)] [--icon-tile-icon-size:--spacing(3.5)] [--icon-tile-inset:--spacing(0.5)]", + sm: "[--icon-tile-size:--spacing(8)] [--icon-tile-icon-size:--spacing(4)] [--icon-tile-inset:--spacing(0.5)]", + default: + "[--icon-tile-size:--spacing(10)] [--icon-tile-icon-size:--spacing(4.5)] [--icon-tile-inset:--spacing(0.75)]", + lg: "[--icon-tile-size:--spacing(12)] [--icon-tile-icon-size:--spacing(5.5)] [--icon-tile-inset:--spacing(0.75)]", + xl: "[--icon-tile-size:--spacing(14)] [--icon-tile-icon-size:--spacing(7)] [--icon-tile-inset:--spacing(1)]", + }, + /** + * `default`: active style radius. `full`: circular. + * The plain value is the fallback outside a style scope; the `style-*` + * tokens win by specificity inside one, and survive the registry + * transform as the single resolved value per generated style. + * + * Each style token is clamped to a fraction of the tile. A flat radius + * is a circle once it reaches half the box, so the soft styles used to + * render `xs` (24px) and `sm` (32px) as plain circles and swallow the + * `full` variant's meaning. Clamping keeps one corner ratio at every + * size instead, so a tile still reads as the same shape when it scales. + * Luma and Rhea take the gentler quarter ratio; Maia stays at a third. + */ + radius: { + default: + "[--icon-tile-radius:min(var(--radius-md),calc(var(--icon-tile-size)/3))] [--icon-tile-radius:min(var(--radius-md),calc(var(--icon-tile-size)/3))]", + full: "[--icon-tile-radius:calc(infinity*1px)]", + }, + }, + defaultVariants: { + variant: "outline", + size: "default", + radius: "default", + }, + } +) + +interface IconTileProps extends useRender.ComponentProps<"span"> { + variant?: VariantProps["variant"] + size?: VariantProps["size"] + radius?: VariantProps["radius"] +} + +function IconTile({ + className, + variant = "outline", + size = "default", + radius = "default", + render, + ...props +}: IconTileProps) { + const defaultProps = { + "data-slot": "icon-tile", + "data-variant": variant, + "data-size": size, + className: cn(iconTileVariants({ variant, size, radius, className })), + } + + return useRender({ + defaultTagName: "span", + render, + props: mergeProps<"span">(defaultProps, props), + }) +} + +export { IconTile, iconTileVariants, type IconTileProps } \ No newline at end of file diff --git a/apps/web/src/components/service-kpi-cards.tsx b/apps/web/src/components/service-kpi-stats.tsx similarity index 96% rename from apps/web/src/components/service-kpi-cards.tsx rename to apps/web/src/components/service-kpi-stats.tsx index c024d25..d29ff2b 100644 --- a/apps/web/src/components/service-kpi-cards.tsx +++ b/apps/web/src/components/service-kpi-stats.tsx @@ -8,7 +8,7 @@ import { import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid' import type { ServiceCatalogRow } from '@/components/columns/services-columns' -interface ServiceKpiCardsProps { +interface ServiceKpiStatsProps { rows: ServiceCatalogRow[] activeTab: string isLoading?: boolean @@ -16,12 +16,12 @@ interface ServiceKpiCardsProps { } /** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */ -export function ServiceKpiCards({ +export function ServiceKpiStats({ rows, activeTab, isLoading, onSelectTab, -}: ServiceKpiCardsProps) { +}: ServiceKpiStatsProps) { const cards = useMemo(() => { const enabled = rows.filter((r) => r.enabled).length const disabled = rows.filter((r) => !r.enabled).length diff --git a/apps/web/src/lib/ui-surface.ts b/apps/web/src/lib/ui-surface.ts new file mode 100644 index 0000000..88019f8 --- /dev/null +++ b/apps/web/src/lib/ui-surface.ts @@ -0,0 +1,21 @@ +/** + * CFDM UI surface contract (ReUI Pro). + * + * Ops / list / dashboard / detail / settings: **Frame** only. + * KPI: horizontal compact hybrid via `reui-kit/KpiStatGrid` + * (`IconTile` elevated `size-10.5` + label/Badge + value ± variant). + * Lists: **data-grid-filtering-2** via `reui-kit/ResourcePage`. + * + * @see https://reui.io/preview/base/stats-12 + * @see https://reui.io/docs/components/base/icon-tile + * @see https://reui.io/preview/base/data-grid-filtering-2 + * @see docs/ui-design-contract.md + */ +export const UI_SURFACE = 'frame' as const + +/** Skip link — keyboard: first Tab. */ +export const SKIP_TO_CONTENT_CLASS = + 'bg-background text-foreground ring-ring sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:rounded-md focus:px-3 focus:py-2 focus:text-sm focus:font-medium focus:shadow-sm focus:ring-2' + +/** Grid for horizontal compact hybrid KPI rows (3–6 tiles). */ +export const kpiStatGridClassName = '@container w-full' diff --git a/apps/web/src/routes/_auth/certificates.tsx b/apps/web/src/routes/_auth/certificates.tsx index 0cb6fea..5bb4e78 100644 --- a/apps/web/src/routes/_auth/certificates.tsx +++ b/apps/web/src/routes/_auth/certificates.tsx @@ -8,7 +8,7 @@ import { api } from '@/lib/api-client' import { PageShell } from '@/components/page-shell' import { PageHeader } from '@/components/page-header' import { ResourcePage } from '@/components/reui-kit' -import { CertKpiCards } from '@/components/cert-kpi-cards' +import { CertKpiStats } from '@/components/cert-kpi-stats' import { CERT_TABS, certFilterFieldValue, @@ -77,7 +77,7 @@ function CertificatesPage() { description="Мониторинг SSL: health-check с проверкой TLS, либо режим «Обязательно»" actions={primaryAction} /> - - - ) : ( <> - } > - - Close +