fix(ui): выровнять KPI IconTile chrome и доступность
Build and Push CFDM Docker Image / build-and-push (push) Successful in 2m15s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 6s

Переименовать KpiStats, добавить skip-link и IconTile в kit.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-17 15:36:54 +07:00
co-authored by Cursor
parent ce8393e0c3
commit 7cd86fcda1
21 changed files with 256 additions and 69 deletions
+10 -9
View File
@@ -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
+1 -1
View File
@@ -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`
+36
View File
@@ -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).
@@ -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<KpiStatCard[]>(() => {
const allCount = total ?? totalFromSummary(summary)
const okCount = countFromSummary(summary, ACTIVE_STATUSES)
@@ -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<KpiStatCard[]>(() => {
const list = domains ?? []
const withoutGroup = list.filter((d) => d.group_id == null).length
+2 -2
View File
@@ -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,
@@ -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<KpiStatCard[]>(() => {
const withDomains = rows.filter((r) => r.domainCount > 0).length
const empty = rows.filter((r) => r.domainCount === 0).length
+7 -3
View File
@@ -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
}
>
<a href="#main-content" className={SKIP_TO_CONTENT_CLASS}>
К содержимому
</a>
<AppSidebar />
<SidebarInset>
<SidebarInset id="main-content">
<SiteHeader />
<main className="flex flex-1 flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
<div className="flex flex-1 flex-col gap-4 px-4 py-4 md:gap-6 md:px-6 md:py-5">
{children}
</main>
</div>
</SidebarInset>
<SearchMenu hotkeyOnly />
</SidebarProvider>
+5 -1
View File
@@ -35,7 +35,11 @@ export function QueryState({
children,
}: QueryStateProps) {
if (isLoading) {
return skeleton ?? <DefaultSkeleton />
return (
<div role="status" aria-live="polite" aria-busy="true">
{skeleton ?? <DefaultSkeleton />}
</div>
)
}
if (isError) {
@@ -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 (
<div className="relative z-10 flex h-full items-start gap-3">
{item.icon ? (
<Item
className={cn(
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
item.iconClassName ?? DEFAULT_ICON_CLASS,
)}
<IconTile
variant="elevated"
aria-hidden="true"
className={cn('size-10.5', item.iconClassName ?? DEFAULT_ICON_CLASS)}
>
<ItemMedia variant="icon" className="size-auto">
{item.icon}
</ItemMedia>
</Item>
{item.icon}
</IconTile>
) : null}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
@@ -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 (
<div className="relative z-10 flex h-full items-start gap-3">
{action.icon ? (
<Item
className={cn(
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
action.iconClassName ?? DEFAULT_ICON_CLASS,
)}
<IconTile
variant="elevated"
aria-hidden="true"
className={cn('size-10.5', action.iconClassName ?? DEFAULT_ICON_CLASS)}
>
<ItemMedia variant="icon" className="size-auto">
{action.icon}
</ItemMedia>
</Item>
{action.icon}
</IconTile>
) : null}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
+125
View File
@@ -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<typeof iconTileVariants>["variant"]
size?: VariantProps<typeof iconTileVariants>["size"]
radius?: VariantProps<typeof iconTileVariants>["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 }
@@ -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<KpiStatCard[]>(() => {
const enabled = rows.filter((r) => r.enabled).length
const disabled = rows.filter((r) => !r.enabled).length
+21
View File
@@ -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 (36 tiles). */
export const kpiStatGridClassName = '@container w-full'
+2 -2
View File
@@ -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}
/>
<CertKpiCards
<CertKpiStats
summary={summary}
total={certs?.length}
activeTab={activeTab}
+2 -2
View File
@@ -16,7 +16,7 @@ import {
} from '@/queries'
import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
import { DomainKpiCards } from '@/components/domain-kpi-cards'
import { DomainKpiStats } from '@/components/domain-kpi-stats'
import { ResourcePage } from '@/components/reui-kit'
import {
DOMAIN_TABS,
@@ -155,7 +155,7 @@ function DomainsPage() {
description="Импортированные зоны Cloudflare"
actions={primaryAction}
/>
<DomainKpiCards
<DomainKpiStats
domains={domains}
activeTab={activeTab}
isLoading={isLoading && !domains}
+2 -2
View File
@@ -15,7 +15,7 @@ import { api } from '@/lib/api-client'
import type { CreateGroupInput, Group } from '@/lib/schemas'
import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
import { GroupKpiCards } from '@/components/group-kpi-cards'
import { GroupKpiStats } from '@/components/group-kpi-stats'
import { EmptyState } from '@/components/empty-state'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { DomainGroupEditSheet } from '@/components/domain-group-edit-sheet'
@@ -301,7 +301,7 @@ function GroupsPage() {
)
) : (
<>
<GroupKpiCards
<GroupKpiStats
rows={catalogRows}
activeTab={activeTab}
isLoading={false}
+2 -2
View File
@@ -22,7 +22,7 @@ import type {
} from '@/lib/schemas'
import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
import { ServiceKpiCards } from '@/components/service-kpi-cards'
import { ServiceKpiStats } from '@/components/service-kpi-stats'
import { EmptyState } from '@/components/empty-state'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { ServiceEditSheet } from '@/components/service-edit-sheet'
@@ -692,7 +692,7 @@ function ServicesPage() {
</>
) : (
<>
<ServiceKpiCards
<ServiceKpiStats
rows={catalogRows}
activeTab={activeTab}
isLoading={false}
+3 -3
View File
@@ -28,7 +28,7 @@ Settings: секции через Frame + `gap` (без hairline `Separator` п
| Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 |
| Settings | `settings-16` + SettingRow | https://reui.io/preview/base/settings-16 |
| Auth | `auth-13` | https://reui.io/preview/base/auth-13 |
| Empty | `empty-state-12` | https://reui.io/preview/base/empty-state-12 |
| Empty | `empty-state-14` (prefer) / `empty-state-12` | https://reui.io/preview/base/empty-state-14 · https://reui.io/preview/base/empty-state-12 |
| Forms | `form-7` → Sheet/Drawer | https://reui.io/preview/base/form-7 |
## Kit API (`reui-kit/`)
@@ -75,9 +75,9 @@ Gating: DB `show_quick_actions` / `showQuickActions` / `ui_show_quick_actions` (
NavUser (footer): avatar + name/email; dropdown — Настройки / Тема (segmented) / Выйти. Preview: [app-shell-1](https://reui.io/preview/base/app-shell-1).
App Switcher: auth-portal `GET /api/v1/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw`. Admin: portal `/admin/apps`.
App Switcher: auth-portal `GET /api/v1/app-switcher`. Ids: `cfdm` · `vps` · `bgp` · `fw` · `dns`. Admin: portal `/admin/apps`.
QuickActionGrid / KPI icons: только semantic **text** (`text-info` / `text-primary` / …) на kit `bg-muted` — без solid fills.
QuickActionGrid / KPI icons: только semantic **text** (`text-info` / `text-primary` / …) на `IconTile` `elevated` — без solid fills.
## System monitor
+2 -3
View File
@@ -68,9 +68,8 @@ function SheetContent({
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
<XIcon aria-hidden="true" />
<span className="sr-only">Закрыть</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
+10 -7
View File
@@ -194,8 +194,8 @@ function Sidebar({
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
<SheetTitle>Боковое меню</SheetTitle>
<SheetDescription>Навигация по приложению</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
@@ -254,7 +254,7 @@ function SidebarTrigger({
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar()
const { toggleSidebar, open, openMobile, isMobile } = useSidebar()
return (
<Button
@@ -262,6 +262,9 @@ function SidebarTrigger({
data-slot="sidebar-trigger"
variant="ghost"
size="icon-sm"
type="button"
aria-label="Боковое меню"
aria-expanded={isMobile ? openMobile : open}
className={cn(className)}
onClick={(event) => {
onClick?.(event)
@@ -269,8 +272,8 @@ function SidebarTrigger({
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
<PanelLeftIcon aria-hidden="true" />
<span className="sr-only">Боковое меню</span>
</Button>
)
}
@@ -282,10 +285,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
aria-label="Боковое меню"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
title="Боковое меню"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",