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
|
cd apps/web
|
||||||
pnpm dlx shadcn@latest add button field input ...
|
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/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
|
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` |
|
| `<table>`, raw select | DataGrid / `Select` |
|
||||||
| `bg-emerald-*` | semantic / ReUI `variant` |
|
| `bg-emerald-*` | semantic / ReUI `variant` |
|
||||||
| Card как ops-shell | ReUI `Frame` |
|
| 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-*` |
|
| `space-y-*` | `flex` + `gap-*` |
|
||||||
| `@/components/ui/*` | `@cfdm/ui/components/*` |
|
| `@/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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| Settings | `settings/integrations.tsx` | [settings-16](https://reui.io/preview/base/settings-16) |
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ alwaysApply: false
|
|||||||
|
|
||||||
| Зона | Preview |
|
| Зона | 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) |
|
| 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 | [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) |
|
| 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
|
kanban-board.tsx ← kanban + KanbanBoardSkeleton
|
||||||
detail-panel.tsx ← detail: Frame header/metrics
|
detail-panel.tsx ← detail: Frame header/metrics
|
||||||
settings-shell.tsx
|
settings-shell.tsx
|
||||||
ops-dashboard.tsx ← KPI card-35 + charts
|
ops-dashboard.tsx ← KPI stats-12 + charts
|
||||||
catalog-board-toggle.tsx
|
catalog-board-toggle.tsx
|
||||||
empty-state.tsx
|
empty-state.tsx
|
||||||
query-state.tsx
|
query-state.tsx
|
||||||
@@ -73,7 +73,7 @@ apps/web/src/components/ ← shared + domain + layout
|
|||||||
| Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` |
|
| Kanban | `KanbanBoard` / `KanbanBoardSkeleton` | ReUI `kanban` + `Frame` |
|
||||||
| Detail | `DetailPanel` | ReUI `Frame` |
|
| Detail | `DetailPanel` | ReUI `Frame` |
|
||||||
| Settings | `SettingsShell` | — |
|
| 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` |
|
| Empty | `EmptyState` | `Empty` |
|
||||||
| Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` |
|
| Loading / Error | `QueryState` / kit skeletons | `Skeleton`, `Alert` |
|
||||||
| Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) |
|
| Status | `StatusBadge` | ReUI `Badge` (`success`/`info`/`warning`) |
|
||||||
@@ -127,7 +127,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p
|
|||||||
|
|
||||||
## Dashboard KPI
|
## 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
|
## Sidebar
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ Toggle «Доска» / «К каталогу» — всегда `outline` в `p
|
|||||||
| Shell | [app-shell-12](https://reui.io/preview/base/app-shell-12) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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) |
|
| 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` |
|
| Data Grid | `@reui` | `@/components/reui/data-grid/*` → `ResourcePage` |
|
||||||
| Filters | `@reui` | `@/components/reui/filters` |
|
| Filters | `@reui` | `@/components/reui/filters` |
|
||||||
| Frame surface | `@reui` | `@/components/reui/frame` |
|
| 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` |
|
| Semantic badge / alert | `@reui` | `@/components/reui/badge`, `@/components/reui/alert` |
|
||||||
| Number / date / autocomplete / color / kanban | `@reui` | `@/components/reui/*` |
|
| Number / date / autocomplete / color / kanban | `@reui` | `@/components/reui/*` |
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ alwaysApply: true
|
|||||||
|
|
||||||
**Kit:** `ResourcePage`, `KpiStatGrid`, `OpsDashboard`, `KanbanBoard`, `DetailPanel`, `SettingsShell`
|
**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
|
## License
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ REUI_LICENSE_KEY=
|
|||||||
|
|
||||||
| Зона | Preview |
|
| Зона | 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 |
|
| List | https://reui.io/preview/base/data-grid-filtering-2 |
|
||||||
| Settings | https://reui.io/preview/base/settings-16 |
|
| Settings | https://reui.io/preview/base/settings-16 |
|
||||||
| Auth | https://reui.io/preview/base/auth-13 |
|
| 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 { 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'
|
import { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||||
|
|
||||||
const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced'])
|
const ACTIVE_STATUSES = new Set(['active', 'ok', 'synced'])
|
||||||
@@ -28,6 +33,7 @@ interface CertKpiCardsProps {
|
|||||||
onSelectTab: (tabId: string) => void
|
onSelectTab: (tabId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** KPI strip — ReUI PRO [stats-12](https://reui.io/preview/base/stats-12) */
|
||||||
export function CertKpiCards({
|
export function CertKpiCards({
|
||||||
summary,
|
summary,
|
||||||
total,
|
total,
|
||||||
@@ -47,6 +53,8 @@ export function CertKpiCards({
|
|||||||
label: 'Всего',
|
label: 'Всего',
|
||||||
value: allCount,
|
value: allCount,
|
||||||
hint: 'Проверенные хосты',
|
hint: 'Проверенные хосты',
|
||||||
|
icon: <ShieldCheckIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-info',
|
||||||
onSelect: () => onSelectTab('all'),
|
onSelect: () => onSelectTab('all'),
|
||||||
selected: activeTab === 'all',
|
selected: activeTab === 'all',
|
||||||
},
|
},
|
||||||
@@ -55,6 +63,8 @@ export function CertKpiCards({
|
|||||||
label: 'OK',
|
label: 'OK',
|
||||||
value: okCount,
|
value: okCount,
|
||||||
hint: 'Активные сертификаты',
|
hint: 'Активные сертификаты',
|
||||||
|
icon: <CheckCircle2Icon aria-hidden />,
|
||||||
|
iconClassName: 'text-success',
|
||||||
onSelect: () => onSelectTab('active'),
|
onSelect: () => onSelectTab('active'),
|
||||||
selected: activeTab === 'active',
|
selected: activeTab === 'active',
|
||||||
},
|
},
|
||||||
@@ -63,6 +73,8 @@ export function CertKpiCards({
|
|||||||
label: 'Предупреждение',
|
label: 'Предупреждение',
|
||||||
value: warningCount,
|
value: warningCount,
|
||||||
hint: 'Истекают скоро',
|
hint: 'Истекают скоро',
|
||||||
|
icon: <AlertTriangleIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-warning',
|
||||||
onSelect: () => onSelectTab('warning'),
|
onSelect: () => onSelectTab('warning'),
|
||||||
selected: activeTab === 'warning',
|
selected: activeTab === 'warning',
|
||||||
},
|
},
|
||||||
@@ -71,6 +83,8 @@ export function CertKpiCards({
|
|||||||
label: 'Проблемы',
|
label: 'Проблемы',
|
||||||
value: problemCount,
|
value: problemCount,
|
||||||
hint: 'Ошибка или истёк',
|
hint: 'Ошибка или истёк',
|
||||||
|
icon: <ShieldAlertIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-destructive',
|
||||||
onSelect: () => onSelectTab('expired'),
|
onSelect: () => onSelectTab('expired'),
|
||||||
selected: activeTab === 'expired',
|
selected: activeTab === 'expired',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { useMemo } from 'react'
|
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 { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||||
import type { DomainListItem } from '@/lib/schemas'
|
import type { DomainListItem } from '@/lib/schemas'
|
||||||
|
|
||||||
@@ -10,7 +15,7 @@ interface DomainKpiCardsProps {
|
|||||||
onSelectTab: (tabId: string) => void
|
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({
|
export function DomainKpiCards({
|
||||||
domains,
|
domains,
|
||||||
activeTab,
|
activeTab,
|
||||||
@@ -29,6 +34,8 @@ export function DomainKpiCards({
|
|||||||
label: 'Всего',
|
label: 'Всего',
|
||||||
value: list.length,
|
value: list.length,
|
||||||
hint: 'Импортированные зоны',
|
hint: 'Импортированные зоны',
|
||||||
|
icon: <GlobeIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-info',
|
||||||
onSelect: () => onSelectTab('all'),
|
onSelect: () => onSelectTab('all'),
|
||||||
selected: activeTab === 'all',
|
selected: activeTab === 'all',
|
||||||
},
|
},
|
||||||
@@ -37,6 +44,8 @@ export function DomainKpiCards({
|
|||||||
label: 'Без группы',
|
label: 'Без группы',
|
||||||
value: withoutGroup,
|
value: withoutGroup,
|
||||||
hint: 'Не назначена группа',
|
hint: 'Не назначена группа',
|
||||||
|
icon: <FolderOpenIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-warning',
|
||||||
onSelect: () => onSelectTab('without_group'),
|
onSelect: () => onSelectTab('without_group'),
|
||||||
selected: activeTab === 'without_group',
|
selected: activeTab === 'without_group',
|
||||||
},
|
},
|
||||||
@@ -45,6 +54,8 @@ export function DomainKpiCards({
|
|||||||
label: 'Active',
|
label: 'Active',
|
||||||
value: active,
|
value: active,
|
||||||
hint: 'Статус Cloudflare',
|
hint: 'Статус Cloudflare',
|
||||||
|
icon: <CheckCircle2Icon aria-hidden />,
|
||||||
|
iconClassName: 'text-success',
|
||||||
onSelect: () => onSelectTab('all'),
|
onSelect: () => onSelectTab('all'),
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
@@ -53,6 +64,8 @@ export function DomainKpiCards({
|
|||||||
label: 'С сервисами',
|
label: 'С сервисами',
|
||||||
value: withServices,
|
value: withServices,
|
||||||
hint: 'Есть привязки',
|
hint: 'Есть привязки',
|
||||||
|
icon: <ServerIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-primary',
|
||||||
onSelect: () => onSelectTab('all'),
|
onSelect: () => onSelectTab('all'),
|
||||||
selected: false,
|
selected: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react'
|
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 { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||||
import type { GroupCatalogRow } from '@/components/columns/groups-columns'
|
import type { GroupCatalogRow } from '@/components/columns/groups-columns'
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ interface GroupKpiCardsProps {
|
|||||||
onSelectTab: (tabId: string) => void
|
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({
|
export function GroupKpiCards({
|
||||||
rows,
|
rows,
|
||||||
activeTab,
|
activeTab,
|
||||||
@@ -27,6 +27,8 @@ export function GroupKpiCards({
|
|||||||
label: 'Всего',
|
label: 'Всего',
|
||||||
value: rows.length,
|
value: rows.length,
|
||||||
hint: 'Группы доменов',
|
hint: 'Группы доменов',
|
||||||
|
icon: <FolderTreeIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-info',
|
||||||
onSelect: () => onSelectTab('all'),
|
onSelect: () => onSelectTab('all'),
|
||||||
selected: activeTab === 'all',
|
selected: activeTab === 'all',
|
||||||
},
|
},
|
||||||
@@ -35,6 +37,8 @@ export function GroupKpiCards({
|
|||||||
label: 'С доменами',
|
label: 'С доменами',
|
||||||
value: withDomains,
|
value: withDomains,
|
||||||
hint: 'Непустые группы',
|
hint: 'Непустые группы',
|
||||||
|
icon: <FoldersIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-success',
|
||||||
onSelect: () => onSelectTab('with_domains'),
|
onSelect: () => onSelectTab('with_domains'),
|
||||||
selected: activeTab === 'with_domains',
|
selected: activeTab === 'with_domains',
|
||||||
},
|
},
|
||||||
@@ -43,6 +47,8 @@ export function GroupKpiCards({
|
|||||||
label: 'Пустые',
|
label: 'Пустые',
|
||||||
value: empty,
|
value: empty,
|
||||||
hint: 'Без доменов',
|
hint: 'Без доменов',
|
||||||
|
icon: <FolderOpenIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-muted-foreground',
|
||||||
onSelect: () => onSelectTab('empty'),
|
onSelect: () => onSelectTab('empty'),
|
||||||
selected: activeTab === 'empty',
|
selected: activeTab === 'empty',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
import { Frame, FramePanel } from '@/components/reui/frame'
|
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||||
|
import { Item, ItemMedia } from '@cfdm/ui/components/item'
|
||||||
import { cn } from '@cfdm/ui/lib/utils'
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
import { Skeleton } from '@cfdm/ui/components/skeleton'
|
||||||
|
|
||||||
@@ -13,6 +14,9 @@ export interface KpiStatCard {
|
|||||||
search?: Record<string, unknown>
|
search?: Record<string, unknown>
|
||||||
onSelect?: () => void
|
onSelect?: () => void
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
|
icon?: ReactNode
|
||||||
|
iconClassName?: string
|
||||||
|
footer?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use KpiStatCard */
|
/** @deprecated Use KpiStatCard */
|
||||||
@@ -27,16 +31,39 @@ interface KpiStatGridProps {
|
|||||||
skeletonCount?: number
|
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 }) {
|
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 (
|
return (
|
||||||
<div className="relative z-10 flex flex-col gap-3">
|
<div className="relative z-10 flex h-full flex-col items-start gap-4">
|
||||||
<span className="text-muted-foreground text-sm">{card.label}</span>
|
{card.icon ? (
|
||||||
<span className="text-foreground text-xl leading-tight font-semibold tabular-nums">
|
<Item
|
||||||
{card.value}
|
className={cn(
|
||||||
</span>
|
'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.hint ? (
|
card.iconClassName ?? DEFAULT_ICON_CLASS,
|
||||||
<span className="text-muted-foreground text-xs leading-snug">{card.hint}</span>
|
)}
|
||||||
|
>
|
||||||
|
<ItemMedia variant="icon" className="size-auto">
|
||||||
|
{card.icon}
|
||||||
|
</ItemMedia>
|
||||||
|
</Item>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,13 +71,12 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
|||||||
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
||||||
const interactive = Boolean(card.to || card.onSelect)
|
const interactive = Boolean(card.to || card.onSelect)
|
||||||
const panelClass = cn(
|
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',
|
card.selected && 'ring-primary/30 bg-muted/30 ring-1',
|
||||||
interactive &&
|
interactive &&
|
||||||
'hover:bg-muted/40 focus-within:ring-ring transition-colors focus-within:ring-2',
|
'hover:bg-muted/40 focus-within:ring-ring transition-colors focus-within:ring-2',
|
||||||
)
|
)
|
||||||
|
|
||||||
// card-35: FramePanel — прямой child грида; ссылка/кнопка внутри панели
|
|
||||||
if (card.to) {
|
if (card.to) {
|
||||||
return (
|
return (
|
||||||
<FramePanel className={panelClass}>
|
<FramePanel className={panelClass}>
|
||||||
@@ -91,9 +117,12 @@ function KpiStatGridSkeleton({ count }: { count: number }) {
|
|||||||
<Frame className="@container w-full">
|
<Frame className="@container w-full">
|
||||||
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
|
<div className="grid gap-1 @2xl:grid-cols-2 @4xl:grid-cols-4">
|
||||||
{Array.from({ length: count }).map((_, index) => (
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
<FramePanel key={index} className="flex flex-col gap-3">
|
<FramePanel key={index} className="flex flex-col items-start gap-4">
|
||||||
<Skeleton className="h-4 w-20" />
|
<Skeleton className="size-10.5 rounded-lg" />
|
||||||
<Skeleton className="h-7 w-14" />
|
<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" />
|
<Skeleton className="h-3 w-28" />
|
||||||
</FramePanel>
|
</FramePanel>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { useMemo } from 'react'
|
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 { KpiStatGrid, type KpiStatCard } from '@/components/reui-kit/kpi-stat-grid'
|
||||||
import type { ServiceCatalogRow } from '@/components/columns/services-columns'
|
import type { ServiceCatalogRow } from '@/components/columns/services-columns'
|
||||||
|
|
||||||
@@ -10,7 +15,7 @@ interface ServiceKpiCardsProps {
|
|||||||
onSelectTab: (tabId: string) => void
|
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({
|
export function ServiceKpiCards({
|
||||||
rows,
|
rows,
|
||||||
activeTab,
|
activeTab,
|
||||||
@@ -29,6 +34,8 @@ export function ServiceKpiCards({
|
|||||||
label: 'Всего',
|
label: 'Всего',
|
||||||
value: rows.length,
|
value: rows.length,
|
||||||
hint: 'Сервисы',
|
hint: 'Сервисы',
|
||||||
|
icon: <ServerIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-info',
|
||||||
onSelect: () => onSelectTab('all'),
|
onSelect: () => onSelectTab('all'),
|
||||||
selected: activeTab === 'all',
|
selected: activeTab === 'all',
|
||||||
},
|
},
|
||||||
@@ -37,6 +44,8 @@ export function ServiceKpiCards({
|
|||||||
label: 'Включены',
|
label: 'Включены',
|
||||||
value: enabled,
|
value: enabled,
|
||||||
hint: 'Активные',
|
hint: 'Активные',
|
||||||
|
icon: <PowerIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-success',
|
||||||
onSelect: () => onSelectTab('enabled'),
|
onSelect: () => onSelectTab('enabled'),
|
||||||
selected: activeTab === 'enabled',
|
selected: activeTab === 'enabled',
|
||||||
},
|
},
|
||||||
@@ -45,6 +54,8 @@ export function ServiceKpiCards({
|
|||||||
label: 'Выключены',
|
label: 'Выключены',
|
||||||
value: disabled,
|
value: disabled,
|
||||||
hint: 'Отключены',
|
hint: 'Отключены',
|
||||||
|
icon: <PowerOffIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-muted-foreground',
|
||||||
onSelect: () => onSelectTab('disabled'),
|
onSelect: () => onSelectTab('disabled'),
|
||||||
selected: activeTab === 'disabled',
|
selected: activeTab === 'disabled',
|
||||||
},
|
},
|
||||||
@@ -53,6 +64,8 @@ export function ServiceKpiCards({
|
|||||||
label: 'Без группы',
|
label: 'Без группы',
|
||||||
value: ungrouped,
|
value: ungrouped,
|
||||||
hint: down > 0 ? `Down: ${down}` : 'Не в группе',
|
hint: down > 0 ? `Down: ${down}` : 'Не в группе',
|
||||||
|
icon: <FolderOpenIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-warning',
|
||||||
onSelect: () => onSelectTab('ungrouped'),
|
onSelect: () => onSelectTab('ungrouped'),
|
||||||
selected: activeTab === '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,
|
ActivityIcon,
|
||||||
AlertTriangleIcon,
|
AlertTriangleIcon,
|
||||||
FolderTreeIcon,
|
FolderTreeIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
ServerIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
certificatesQueryOptions,
|
certificatesQueryOptions,
|
||||||
@@ -173,6 +176,8 @@ function DashboardPage() {
|
|||||||
ungroupedCount > 0
|
ungroupedCount > 0
|
||||||
? `${ungroupedCount} без группы`
|
? `${ungroupedCount} без группы`
|
||||||
: 'Все в группах',
|
: 'Все в группах',
|
||||||
|
icon: <GlobeIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-info',
|
||||||
to: '/domains',
|
to: '/domains',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -180,6 +185,8 @@ function DashboardPage() {
|
|||||||
label: 'Группы',
|
label: 'Группы',
|
||||||
value: groups?.length ?? 0,
|
value: groups?.length ?? 0,
|
||||||
hint: `${groupChartData.filter((g) => g.count > 0).length} с зонами`,
|
hint: `${groupChartData.filter((g) => g.count > 0).length} с зонами`,
|
||||||
|
icon: <FolderTreeIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-primary',
|
||||||
to: '/groups',
|
to: '/groups',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -191,6 +198,8 @@ function DashboardPage() {
|
|||||||
: ungroupedServiceCount
|
: ungroupedServiceCount
|
||||||
? `${ungroupedServiceCount} без группы`
|
? `${ungroupedServiceCount} без группы`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
icon: <ServerIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-success',
|
||||||
to: '/services',
|
to: '/services',
|
||||||
search: { domainId: undefined },
|
search: { domainId: undefined },
|
||||||
},
|
},
|
||||||
@@ -202,6 +211,8 @@ function DashboardPage() {
|
|||||||
certWarnings > 0
|
certWarnings > 0
|
||||||
? `${certOk} в норме · ${certWarnings} внимания`
|
? `${certOk} в норме · ${certWarnings} внимания`
|
||||||
: `${certOk} в норме`,
|
: `${certOk} в норме`,
|
||||||
|
icon: <ShieldCheckIcon aria-hidden />,
|
||||||
|
iconClassName: 'text-warning',
|
||||||
to: '/certificates',
|
to: '/certificates',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Wiki home — synced from repository on `main` when this file changes.
|
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
|
## Overview
|
||||||
|
|
||||||
Manage Cloudflare zones, DNS records, domain groups, and TLS certificate expiry from a single UI.
|
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