Frontend:
- apps/web (Vite+TS, TanStack Router/Query, shadcn/ui @cfdm/ui base-nova)
- 10 страниц в routes/_auth/, Recharts через shadcn Chart, lucide-react
- формы на RHF + Zod (FormSheet/FormField)
- удалены Tabler, Chart.js, react-router-dom
Backend (параллельный трек):
- apps/api (Fastify 5 + Drizzle + better-sqlite3)
- packages/db: Drizzle-схема и repositories по сущностям
- packages/shared: Zod-контракты
- роуты с валидацией и единым форматом ошибок { error: { code, message } }
- sync/backup — заглушки 501 (billmanager-адаптеры переносятся отдельно)
- legacy Express оставлен как runtime по умолчанию (RUNTIME=express)
Infra:
- Dockerfile multi-stage под pnpm workspaces
- .dockerignore и docker-compose обновлены под monorepo
Rules:
- удалены нерелевантные правила (rust, cloudflare, server/frontend-conventions)
- project-structure.mdc и AGENTS.md переписаны под monorepo
- frontend-shadcn.mdc, shadcn-ui-production.mdc, sqlite.mdc обновлены
Co-authored-by: Cursor <cursoragent@cursor.com>
30 lines
1.2 KiB
TypeScript
30 lines
1.2 KiB
TypeScript
import type { ReactElement, ReactNode } from 'react'
|
|
import { Card, CardContent } from '@cfdm/ui/components/card'
|
|
import { cn } from '@cfdm/ui/lib/utils'
|
|
|
|
export interface SectionCardItem {
|
|
label: ReactNode
|
|
value: string | number | ReactElement
|
|
hint?: ReactNode
|
|
icon?: ReactNode
|
|
}
|
|
|
|
export function SectionCards({ items, className }: { items: SectionCardItem[]; className?: string }) {
|
|
return (
|
|
<div className={cn('grid gap-4 sm:grid-cols-2 lg:grid-cols-4', className)}>
|
|
{items.map((item, idx) => (
|
|
<Card key={typeof item.label === 'string' ? item.label : idx} className="gap-0">
|
|
<CardContent className="flex flex-col gap-1 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">{item.label}</span>
|
|
{item.icon ? <span className="text-muted-foreground">{item.icon}</span> : null}
|
|
</div>
|
|
<span className="text-2xl font-semibold tabular-nums">{item.value}</span>
|
|
{item.hint ? <span className="text-xs text-muted-foreground">{item.hint}</span> : null}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|