feat(ui): обновить KPI с card-35 на stats-12 во всех компонентах
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m27s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m52s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Failing after 6s
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m27s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m52s
Build, Test, and Push CFDM Docker Image / create-release (push) Skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Failing after 6s
Заменены ссылки на блоки card-35 на stats-12 в документации и компонентах, включая KpiStatGrid, CertKpiCards, DomainKpiCards, GroupKpiCards и ServiceKpiCards. Удален неиспользуемый компонент AppCard. Добавлены иконки для улучшения визуализации KPI. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -58,7 +58,7 @@ Monorepo — [`frontend-monorepo.mdc`](frontend-monorepo.mdc). ReUI — [`reui-m
|
||||
cd apps/web
|
||||
pnpm dlx shadcn@latest add button field input ...
|
||||
pnpm dlx shadcn@latest add @reui/frame @reui/data-grid @reui/filters
|
||||
pnpm dlx shadcn@latest add @reui/card-35 @reui/settings-16 @reui/auth-13
|
||||
pnpm dlx shadcn@latest add @reui/stats-12 @reui/card-35 @reui/settings-16 @reui/auth-13
|
||||
pnpm dlx shadcn@latest apply b2fA --only theme -y
|
||||
```
|
||||
|
||||
@@ -76,7 +76,7 @@ pnpm dlx shadcn@latest apply b2fA --only theme -y
|
||||
| `<table>`, raw select | DataGrid / `Select` |
|
||||
| `bg-emerald-*` | semantic / ReUI `variant` |
|
||||
| Card как ops-shell | ReUI `Frame` |
|
||||
| Hand-roll KPI | `KpiStatGrid` / [card-35](https://reui.io/preview/base/card-35) |
|
||||
| Hand-roll KPI | `KpiStatGrid` / [stats-12](https://reui.io/preview/base/stats-12) |
|
||||
| `space-y-*` | `flex` + `gap-*` |
|
||||
| `@/components/ui/*` | `@cfdm/ui/components/*` |
|
||||
|
||||
@@ -86,7 +86,7 @@ pnpm dlx shadcn@latest apply b2fA --only theme -y
|
||||
|------|------|---------|
|
||||
| Shell | `layout/app-shell.tsx` | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||||
| Login | `routes/login.tsx` | [auth-13](https://reui.io/preview/base/auth-13) |
|
||||
| Dashboard | `routes/_auth/index.tsx` | [card-35](https://reui.io/preview/base/card-35) / dashboard-1 |
|
||||
| Dashboard | `routes/_auth/index.tsx` | [stats-12](https://reui.io/preview/base/stats-12) / dashboard-1 |
|
||||
| Lists | `ResourcePage` | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||
| Settings | `settings/integrations.tsx` | [settings-16](https://reui.io/preview/base/settings-16) |
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ alwaysApply: false
|
||||
|
||||
| Зона | Preview |
|
||||
|------|---------|
|
||||
| KPI | [card-35](https://reui.io/preview/base/card-35) |
|
||||
| KPI | [stats-12](https://reui.io/preview/base/stats-12) (primary); [card-35](https://reui.io/preview/base/card-35) compact |
|
||||
| List | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||
| Settings | [settings-16](https://reui.io/preview/base/settings-16), [Application Settings](https://reui.io/blocks/application/settings) |
|
||||
| Settings rows / Health-check | [settings-2](https://reui.io/preview/base/settings-2), [settings-3](https://reui.io/preview/base/settings-3) |
|
||||
@@ -50,7 +50,7 @@ apps/web/src/components/ ← shared + domain + layout
|
||||
kanban-board.tsx ← kanban + KanbanBoardSkeleton
|
||||
detail-panel.tsx ← detail: Frame header/metrics
|
||||
settings-shell.tsx
|
||||
ops-dashboard.tsx ← KPI card-35 + charts
|
||||
ops-dashboard.tsx ← KPI stats-12 + charts
|
||||
catalog-board-toggle.tsx
|
||||
empty-state.tsx
|
||||
query-state.tsx
|
||||
@@ -73,7 +73,7 @@ apps/web/src/components/ ← shared + domain + layout
|
||||
| Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` |
|
||||
| Detail | `DetailPanel` | ReUI `Frame` |
|
||||
| Settings | `SettingsShell` | — |
|
||||
| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [card-35](https://reui.io/preview/base/card-35) |
|
||||
| Dashboard KPI | `OpsDashboard` / `KpiStatGrid` | ReUI Frame [stats-12](https://reui.io/preview/base/stats-12) |
|
||||
| Empty | `EmptyState` | `Empty` |
|
||||
| Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` |
|
||||
| Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) |
|
||||
@@ -127,7 +127,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p
|
||||
|
||||
## Dashboard KPI
|
||||
|
||||
Эталон: [card-35](https://reui.io/preview/base/card-35) — muted **label** + крупный **value** + optional **hint**; клик по карточке через `to` (`Link`), без отдельных hint-links.
|
||||
Эталон: [stats-12](https://reui.io/preview/base/stats-12) — icon tile + value + label + optional badge/footer; клик через `to` (`Link`) / `onSelect`. Compact strip: [card-35](https://reui.io/preview/base/card-35).
|
||||
|
||||
## Sidebar
|
||||
|
||||
@@ -149,7 +149,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p
|
||||
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
||||
| List + tabs + filters | [data-grid-filtering-2](https://reui.io/preview/base/data-grid-filtering-2) |
|
||||
| Line tabs | [c-tabs-2](https://reui.io/preview/base/components/c-tabs-2) |
|
||||
| KPI | [card-35](https://reui.io/preview/base/card-35) — на dashboard, certificates, domains, groups, services |
|
||||
| KPI | [stats-12](https://reui.io/preview/base/stats-12) — на dashboard, certificates, domains, groups, services |
|
||||
| Login | [auth-13](https://reui.io/preview/base/auth-13) |
|
||||
| Settings | [settings-16](https://reui.io/preview/base/settings-16) / [settings-6](https://reui.io/preview/base/settings-6) |
|
||||
| Empty | [empty-state-12](https://reui.io/preview/base/empty-state-12) |
|
||||
|
||||
@@ -36,7 +36,7 @@ alwaysApply: true
|
||||
| Data Grid | `@reui` | `@/components/reui/data-grid/*` → `ResourcePage` |
|
||||
| Filters | `@reui` | `@/components/reui/filters` |
|
||||
| Frame surface | `@reui` | `@/components/reui/frame` |
|
||||
| KPI | block [card-35](https://reui.io/preview/base/card-35) | `reui-kit/KpiStatGrid` |
|
||||
| KPI | block [stats-12](https://reui.io/preview/base/stats-12) | `reui-kit/KpiStatGrid` |
|
||||
| Semantic badge / alert | `@reui` | `@/components/reui/badge`, `@/components/reui/alert` |
|
||||
| Number / date / autocomplete / color / kanban | `@reui` | `@/components/reui/*` |
|
||||
|
||||
@@ -68,7 +68,7 @@ alwaysApply: true
|
||||
|
||||
**Kit:** `ResourcePage`, `KpiStatGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell`
|
||||
|
||||
**Blocks (reference):** `card-35`, `auth-13`, `app-shell-12`, `settings-16`, `settings-8`, `empty-state-12`, `form-7`, `data-grid-filtering-2`, `dashboard-1`, …
|
||||
**Blocks (reference):** `stats-12`, `card-35`, `auth-13`, `app-shell-12`, `settings-16`, `settings-8`, `empty-state-12`, `form-7`, `data-grid-filtering-2`, `dashboard-1`, …
|
||||
|
||||
## License
|
||||
|
||||
@@ -83,7 +83,7 @@ REUI_LICENSE_KEY=
|
||||
|
||||
| Зона | Preview |
|
||||
|------|---------|
|
||||
| KPI | https://reui.io/preview/base/card-35 |
|
||||
| KPI | https://reui.io/preview/base/stats-12 |
|
||||
| List | https://reui.io/preview/base/data-grid-filtering-2 |
|
||||
| Settings | https://reui.io/preview/base/settings-16 |
|
||||
| Auth | https://reui.io/preview/base/auth-13 |
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@cfdm/ui/components/card'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
|
||||
export type AppCardProps = ComponentProps<typeof Card>
|
||||
export type AppCardHeaderProps = ComponentProps<typeof CardHeader>
|
||||
export type AppCardTitleProps = ComponentProps<typeof CardTitle>
|
||||
export type AppCardDescriptionProps = ComponentProps<typeof CardDescription>
|
||||
export type AppCardActionProps = ComponentProps<typeof CardAction>
|
||||
export type AppCardContentProps = ComponentProps<typeof CardContent>
|
||||
export type AppCardFooterProps = ComponentProps<typeof CardFooter>
|
||||
|
||||
export function AppCard({ className, ...props }: AppCardProps) {
|
||||
return <Card className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardHeader({ className, ...props }: AppCardHeaderProps) {
|
||||
return <CardHeader className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardTitle({ className, ...props }: AppCardTitleProps) {
|
||||
return <CardTitle className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardDescription({
|
||||
className,
|
||||
...props
|
||||
}: AppCardDescriptionProps) {
|
||||
return <CardDescription className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardAction({ className, ...props }: AppCardActionProps) {
|
||||
return <CardAction className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardContent({ className, ...props }: AppCardContentProps) {
|
||||
return <CardContent className={cn(className)} {...props} />
|
||||
}
|
||||
|
||||
export function AppCardFooter({ className, ...props }: AppCardFooterProps) {
|
||||
return <CardFooter className={cn(className)} {...props} />
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { ShieldCheckIcon } from 'lucide-react'
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
CheckCircle2Icon,
|
||||
ShieldAlertIcon,
|
||||
ShieldCheckIcon,
|
||||
} from 'lucide-react'
|
||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||
|
||||
const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced'])
|
||||
@@ -28,6 +33,7 @@ interface CertKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function CertKpiCards({
|
||||
summary,
|
||||
total,
|
||||
@@ -47,6 +53,8 @@ export function CertKpiCards({
|
||||
label: 'Всего',
|
||||
value: allCount,
|
||||
hint: 'Проверенные хосты',
|
||||
icon: <ShieldCheckIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -55,6 +63,8 @@ export function CertKpiCards({
|
||||
label: 'OK',
|
||||
value: okCount,
|
||||
hint: 'Активные сертификаты',
|
||||
icon: <CheckCircle2Icon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('active'),
|
||||
selected: activeTab === 'active',
|
||||
},
|
||||
@@ -63,6 +73,8 @@ export function CertKpiCards({
|
||||
label: 'Предупреждение',
|
||||
value: warningCount,
|
||||
hint: 'Истекают скоро',
|
||||
icon: <AlertTriangleIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
onSelect: () => onSelectTab('warning'),
|
||||
selected: activeTab === 'warning',
|
||||
},
|
||||
@@ -71,6 +83,8 @@ export function CertKpiCards({
|
||||
label: 'Проблемы',
|
||||
value: problemCount,
|
||||
hint: 'Ошибка или истёк',
|
||||
icon: <ShieldAlertIcon aria-hidden />,
|
||||
iconClassName: 'text-destructive',
|
||||
onSelect: () => onSelectTab('expired'),
|
||||
selected: activeTab === 'expired',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { GlobeIcon } from 'lucide-react'
|
||||
import {
|
||||
CheckCircle2Icon,
|
||||
FolderOpenIcon,
|
||||
GlobeIcon,
|
||||
ServerIcon,
|
||||
} from 'lucide-react'
|
||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||
import type { DomainListItem } from '@/lib/schemas'
|
||||
|
||||
@@ -10,7 +15,7 @@ interface DomainKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function DomainKpiCards({
|
||||
domains,
|
||||
activeTab,
|
||||
@@ -29,6 +34,8 @@ export function DomainKpiCards({
|
||||
label: 'Всего',
|
||||
value: list.length,
|
||||
hint: 'Импортированные зоны',
|
||||
icon: <GlobeIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -37,6 +44,8 @@ export function DomainKpiCards({
|
||||
label: 'Без группы',
|
||||
value: withoutGroup,
|
||||
hint: 'Не назначена группа',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
onSelect: () => onSelectTab('without_group'),
|
||||
selected: activeTab === 'without_group',
|
||||
},
|
||||
@@ -45,6 +54,8 @@ export function DomainKpiCards({
|
||||
label: 'Active',
|
||||
value: active,
|
||||
hint: 'Статус Cloudflare',
|
||||
icon: <CheckCircle2Icon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: false,
|
||||
},
|
||||
@@ -53,6 +64,8 @@ export function DomainKpiCards({
|
||||
label: 'С сервисами',
|
||||
value: withServices,
|
||||
hint: 'Есть привязки',
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-primary',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: false,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react'
|
||||
import { FolderTreeIcon } from 'lucide-react'
|
||||
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'
|
||||
|
||||
@@ -10,7 +10,7 @@ interface GroupKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function GroupKpiCards({
|
||||
rows,
|
||||
activeTab,
|
||||
@@ -27,6 +27,8 @@ export function GroupKpiCards({
|
||||
label: 'Всего',
|
||||
value: rows.length,
|
||||
hint: 'Группы доменов',
|
||||
icon: <FolderTreeIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -35,6 +37,8 @@ export function GroupKpiCards({
|
||||
label: 'С доменами',
|
||||
value: withDomains,
|
||||
hint: 'Непустые группы',
|
||||
icon: <FoldersIcon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('with_domains'),
|
||||
selected: activeTab === 'with_domains',
|
||||
},
|
||||
@@ -43,6 +47,8 @@ export function GroupKpiCards({
|
||||
label: 'Пустые',
|
||||
value: empty,
|
||||
hint: 'Без доменов',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-muted-foreground',
|
||||
onSelect: () => onSelectTab('empty'),
|
||||
selected: activeTab === 'empty',
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||
|
||||
@@ -13,6 +14,9 @@ export interface KpiStatCard {
|
||||
search?: Record<string, unknown>
|
||||
onSelect?: () => void
|
||||
selected?: boolean
|
||||
icon?: ReactNode
|
||||
iconClassName?: string
|
||||
footer?: ReactNode
|
||||
}
|
||||
|
||||
/** @deprecated Use KpiStatCard */
|
||||
@@ -27,16 +31,39 @@ interface KpiStatGridProps {
|
||||
skeletonCount?: number
|
||||
}
|
||||
|
||||
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
||||
|
||||
/** KPI body — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
||||
const footerContent =
|
||||
card.footer ??
|
||||
(card.hint ? (
|
||||
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
|
||||
) : null)
|
||||
|
||||
return (
|
||||
<div className="relative z-10 flex flex-col gap-3">
|
||||
<span className="text-muted-foreground text-sm">{card.label}</span>
|
||||
<span className="text-foreground text-xl leading-tight font-semibold tabular-nums">
|
||||
{card.value}
|
||||
</span>
|
||||
{card.hint ? (
|
||||
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
|
||||
<div className="relative z-10 flex h-full flex-col items-start gap-4">
|
||||
{card.icon ? (
|
||||
<Item
|
||||
className={cn(
|
||||
'border-background bg-muted flex size-10.5 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
|
||||
card.iconClassName ?? DEFAULT_ICON_CLASS,
|
||||
)}
|
||||
>
|
||||
<ItemMedia variant="icon" className="size-auto">
|
||||
{card.icon}
|
||||
</ItemMedia>
|
||||
</Item>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
|
||||
{card.value}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm font-medium">{card.label}</span>
|
||||
</div>
|
||||
|
||||
{footerContent ? <div className="mt-auto w-full">{footerContent}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -44,13 +71,12 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
||||
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
||||
const interactive = Boolean(card.to || card.onSelect)
|
||||
const panelClass = cn(
|
||||
'relative isolate flex flex-col gap-3',
|
||||
'relative isolate flex h-full flex-col',
|
||||
card.selected && 'ring-primary/30 bg-muted/30 ring-1',
|
||||
interactive &&
|
||||
'hover:bg-muted/40 focus-within:ring-ring transition-colors focus-within:ring-2',
|
||||
)
|
||||
|
||||
// card-35: FramePanel — прямой child грида; ссылка/кнопка внутри панели
|
||||
if (card.to) {
|
||||
return (
|
||||
<FramePanel className={panelClass}>
|
||||
@@ -91,9 +117,12 @@ function KpiStatGridSkeleton({ count }: { count: number }) {
|
||||
<Frame className="@container w-full">
|
||||
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<FramePanel key={index} className="flex flex-col gap-3">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-7 w-14" />
|
||||
<FramePanel key={index} className="flex flex-col items-start gap-4">
|
||||
<Skeleton className="size-10.5 rounded-lg" />
|
||||
<div className="flex w-full flex-col gap-1.5">
|
||||
<Skeleton className="h-7 w-14" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-28" />
|
||||
</FramePanel>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useMemo } from 'react'
|
||||
import { ServerIcon } from 'lucide-react'
|
||||
import {
|
||||
FolderOpenIcon,
|
||||
PowerIcon,
|
||||
PowerOffIcon,
|
||||
ServerIcon,
|
||||
} from 'lucide-react'
|
||||
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||
import type { ServiceCatalogRow } from '@/components/columns/services-columns'
|
||||
|
||||
@@ -10,7 +15,7 @@ interface ServiceKpiCardsProps {
|
||||
onSelectTab: (tabId: string) => void
|
||||
}
|
||||
|
||||
/** KPI strip — ReUI PRO [card-35](https://reui.io/preview/base/card-35) */
|
||||
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||
export function ServiceKpiCards({
|
||||
rows,
|
||||
activeTab,
|
||||
@@ -29,6 +34,8 @@ export function ServiceKpiCards({
|
||||
label: 'Всего',
|
||||
value: rows.length,
|
||||
hint: 'Сервисы',
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
onSelect: () => onSelectTab('all'),
|
||||
selected: activeTab === 'all',
|
||||
},
|
||||
@@ -37,6 +44,8 @@ export function ServiceKpiCards({
|
||||
label: 'Включены',
|
||||
value: enabled,
|
||||
hint: 'Активные',
|
||||
icon: <PowerIcon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
onSelect: () => onSelectTab('enabled'),
|
||||
selected: activeTab === 'enabled',
|
||||
},
|
||||
@@ -45,6 +54,8 @@ export function ServiceKpiCards({
|
||||
label: 'Выключены',
|
||||
value: disabled,
|
||||
hint: 'Отключены',
|
||||
icon: <PowerOffIcon aria-hidden />,
|
||||
iconClassName: 'text-muted-foreground',
|
||||
onSelect: () => onSelectTab('disabled'),
|
||||
selected: activeTab === 'disabled',
|
||||
},
|
||||
@@ -53,6 +64,8 @@ export function ServiceKpiCards({
|
||||
label: 'Без группы',
|
||||
value: ungrouped,
|
||||
hint: down > 0 ? `Down: ${down}` : 'Не в группе',
|
||||
icon: <FolderOpenIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
onSelect: () => onSelectTab('ungrouped'),
|
||||
selected: activeTab === 'ungrouped',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* CFDM UI surface contract (ReUI Pro).
|
||||
*
|
||||
* Ops / list / dashboard / detail / settings: **Frame** only.
|
||||
* KPI: **stats-12** via `reui-kit/KpiStatGrid` (card-35 = compact strip).
|
||||
* Lists: **data-grid-filtering-2** via `reui-kit/ResourcePage`.
|
||||
*
|
||||
* @see https://reui.io/preview/base/stats-12
|
||||
* @see https://reui.io/preview/base/data-grid-filtering-2
|
||||
* @see docs/ui-design-contract.md
|
||||
*/
|
||||
export const UI_SURFACE = 'frame' as const
|
||||
|
||||
/** Grid for ReUI stats-12 KPI rows (3–6 tiles). */
|
||||
export const kpiStatGridClassName = '@container w-full'
|
||||
@@ -5,6 +5,9 @@ import {
|
||||
ActivityIcon,
|
||||
AlertTriangleIcon,
|
||||
FolderTreeIcon,
|
||||
GlobeIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
certificatesQueryOptions,
|
||||
@@ -173,6 +176,8 @@ function DashboardPage() {
|
||||
ungroupedCount > 0
|
||||
? `${ungroupedCount} без группы`
|
||||
: 'Все в группах',
|
||||
icon: <GlobeIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
to: '/domains',
|
||||
},
|
||||
{
|
||||
@@ -180,6 +185,8 @@ function DashboardPage() {
|
||||
label: 'Группы',
|
||||
value: groups?.length ?? 0,
|
||||
hint: `${groupChartData.filter((g) => g.count > 0).length} с зонами`,
|
||||
icon: <FolderTreeIcon aria-hidden />,
|
||||
iconClassName: 'text-primary',
|
||||
to: '/groups',
|
||||
},
|
||||
{
|
||||
@@ -191,6 +198,8 @@ function DashboardPage() {
|
||||
: ungroupedServiceCount
|
||||
? `${ungroupedServiceCount} без группы`
|
||||
: undefined,
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-success',
|
||||
to: '/services',
|
||||
search: { domainId: undefined },
|
||||
},
|
||||
@@ -202,6 +211,8 @@ function DashboardPage() {
|
||||
certWarnings > 0
|
||||
? `${certOk} в норме · ${certWarnings} внимания`
|
||||
: `${certOk} в норме`,
|
||||
icon: <ShieldCheckIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
to: '/certificates',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Wiki home — synced from repository on `main` when this file changes.
|
||||
|
||||
## UI
|
||||
|
||||
Design contract (Frame surface, ReUI kit): [`docs/ui-design-contract.md`](ui-design-contract.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Manage Cloudflare zones, DNS records, domain groups, and TLS certificate expiry from a single UI.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# UI Design Contract (ops apps)
|
||||
|
||||
Единый контракт для vps-tracker, CFDM и EvoBGP. Surface: **ReUI Frame**. Kit API: `apps/web/src/components/reui-kit/`.
|
||||
|
||||
Карта: [llms.txt](https://reui.io/llms.txt) · [Styling](https://reui.io/docs/styling) · [License](https://reui.io/docs/license-setup) · [Blocks](https://reui.io/blocks)
|
||||
|
||||
## Surface
|
||||
|
||||
```ts
|
||||
// apps/web/src/lib/ui-surface.ts
|
||||
export const UI_SURFACE = 'frame' as const
|
||||
```
|
||||
|
||||
Ops / list / dashboard / detail / settings — только **Frame**, не shadcn Card как shell. Не смешивать Card и Frame на одном ops-экране.
|
||||
|
||||
## Canonical PRO references
|
||||
|
||||
| Зона | Block | Preview |
|
||||
|------|-------|---------|
|
||||
| Shell | `app-shell-12` (+ cmdk/monitor где нужно) | https://reui.io/preview/base/app-shell-12 |
|
||||
| KPI | `stats-12` (primary); `card-35` compact strip | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-35 |
|
||||
| Dashboard | `dashboard-1` | https://reui.io/preview/base/dashboard-1 |
|
||||
| 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 |
|
||||
| Forms | `form-7` → Sheet/Drawer | https://reui.io/preview/base/form-7 |
|
||||
|
||||
## Kit API (`reui-kit/`)
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| `ResourcePage` | Frame + line tabs + Filters + DataGrid |
|
||||
| `KpiStatGrid` | stats-12 KPI tiles |
|
||||
| `OpsDashboard` | KPI + charts + attention queue |
|
||||
| `SettingsShell` | settings nav + Outlet |
|
||||
| `DetailPanel` | detail Frame sections |
|
||||
| `filter-utils` | apply/clear ReUI Filters |
|
||||
|
||||
## MCP workflow
|
||||
|
||||
1. MCP `user-reui` — `search` / `get_block` / `get_component` with `surface: "frame"`
|
||||
2. Cite `previewUrl` + `docsUrl`
|
||||
3. CLI from `apps/web`: `pnpm dlx shadcn@latest add @reui/...`
|
||||
4. Adapt into kit — do not hand-roll KPI/grid/settings rows
|
||||
5. `validate_usage` / `get_audit_checklist`
|
||||
|
||||
Primitives: MCP `plugin-shadcn-shadcn` + `@cfdm/ui`.
|
||||
|
||||
## Spacing
|
||||
|
||||
- Main: `gap-4 md:gap-6`, `px-4 md:px-6`
|
||||
- No `space-y-*` / `space-x-*` — use `flex` + `gap-*`
|
||||
- Max 1 primary CTA per screen
|
||||
- Semantic tokens only (`variant="success"|"info"|"warning"`) — no raw `bg-emerald-*`
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Card as ops list/dashboard shell
|
||||
- Hand-rolled data tables when ReUI DataGrid exists
|
||||
- Hand-rolled KPI grids when `KpiStatGrid` exists
|
||||
- Mixing Card and Frame surfaces on one ops screen
|
||||
Reference in New Issue
Block a user