953f598b9b
Componentes React reutilizables: card, dialog, tabs, select, alert, badge, button, input, label, skeleton, tooltip, progress_bar, page_header, form_field, settings_page, crud_page, analytics_page, dashboard_layout. Charts: area, bar, line, sparkline, kpi_card, chart_container. Hooks Wails: use_wails_query, use_wails_mutation, use_wails_stream, use_wails_event, use_animated_canvas. Funciones core: cn, format_compact, chart_colors, get_series_color, wails_cache, theme_config_to_colors. Tipos: chart_series, wails_ipc, theme_config, component_variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
102 lines
3.0 KiB
TypeScript
102 lines
3.0 KiB
TypeScript
import * as React from 'react'
|
|
import { cn } from '../core/cn'
|
|
|
|
interface MetricConfig {
|
|
label: string
|
|
value: string | number
|
|
delta?: { value: number; isPositive: boolean }
|
|
sparklineData?: number[]
|
|
}
|
|
|
|
interface ChartConfig {
|
|
id: string
|
|
title: string
|
|
type: 'line' | 'bar' | 'area'
|
|
span?: 1 | 2
|
|
height?: number
|
|
content: React.ReactNode
|
|
}
|
|
|
|
interface AnalyticsPageProps {
|
|
title: string
|
|
subtitle?: string
|
|
dateRange?: React.ReactNode
|
|
metrics: MetricConfig[]
|
|
charts: ChartConfig[]
|
|
actions?: React.ReactNode
|
|
className?: string
|
|
}
|
|
|
|
export function analyticsPage({
|
|
title,
|
|
subtitle,
|
|
dateRange,
|
|
metrics,
|
|
charts,
|
|
actions,
|
|
className,
|
|
}: AnalyticsPageProps): React.ReactElement {
|
|
return (
|
|
<div className={cn('space-y-6', className)}>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between border-b pb-4">
|
|
<div className="space-y-1">
|
|
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{dateRange}
|
|
{actions}
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPI Row */}
|
|
<div className={cn(
|
|
'grid gap-4',
|
|
metrics.length <= 2 ? 'grid-cols-1 md:grid-cols-2' :
|
|
metrics.length <= 3 ? 'grid-cols-1 md:grid-cols-3' :
|
|
'grid-cols-1 md:grid-cols-2 lg:grid-cols-4'
|
|
)}>
|
|
{metrics.map((metric, i) => (
|
|
<div key={i} className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
|
|
<p className="text-sm text-muted-foreground">{metric.label}</p>
|
|
<div className="mt-2 flex items-end justify-between gap-4">
|
|
<div className="space-y-1">
|
|
<p className="text-3xl font-bold tracking-tight">{metric.value}</p>
|
|
{metric.delta && (
|
|
<div className={cn(
|
|
'flex items-center gap-1 text-sm font-medium',
|
|
metric.delta.value === 0 ? 'text-muted-foreground' :
|
|
metric.delta.isPositive ? 'text-green-600 dark:text-green-500' :
|
|
'text-red-600 dark:text-red-500'
|
|
)}>
|
|
<span>{metric.delta.value > 0 ? '+' : ''}{metric.delta.value}%</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Charts Grid */}
|
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
{charts.map((chart) => (
|
|
<div
|
|
key={chart.id}
|
|
className={cn(
|
|
'rounded-xl border bg-card p-4 text-card-foreground shadow-sm',
|
|
chart.span === 2 && 'lg:col-span-2'
|
|
)}
|
|
>
|
|
<h3 className="mb-3 text-sm font-medium text-muted-foreground">{chart.title}</h3>
|
|
{chart.content}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export type { AnalyticsPageProps, MetricConfig, ChartConfig }
|