TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import * as React from "react";2import { cn } from "@/lib/utils";3import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";45export interface ChartTableView {6 columns: string[];7 rows: Array<Array<string | number>>;8}910/**11 * Card wrapper for a chart: title, description, optional right slot, an "empty" overlay when12 * there is no data, and a collapsible table twin so every value is reachable without hover.13 * Server-compatible (no client hooks) so pages can pass client charts as children.14 */15export function ChartFrame({16 title,17 description,18 right,19 empty,20 emptyMessage = "No data for this period yet.",21 table,22 children,23 className,24 contentClassName,25}: {26 title: React.ReactNode;27 description?: React.ReactNode;28 right?: React.ReactNode;29 empty?: boolean;30 emptyMessage?: React.ReactNode;31 table?: ChartTableView;32 children: React.ReactNode;33 className?: string;34 contentClassName?: string;35}) {36 return (37 <Card className={cn("flex flex-col", className)}>38 <CardHeader className="flex-row items-start justify-between gap-3 pb-2">39 <div className="min-w-0">40 <CardTitle className="text-[14px]">{title}</CardTitle>41 {description ? <CardDescription className="mt-0.5 text-[12.5px]">{description}</CardDescription> : null}42 </div>43 {right ? <div className="shrink-0">{right}</div> : null}44 </CardHeader>45 <CardContent className={cn("flex-1 pt-1", contentClassName)}>46 <div className="relative">47 <div className={cn(empty && "pointer-events-none opacity-30 grayscale")} aria-hidden={empty || undefined}>48 {children}49 </div>50 {empty ? (51 <div className="absolute inset-0 flex items-center justify-center">52 <p className="rounded-md border border-border bg-bg-elevated px-3 py-1.5 text-[12.5px] text-fg-muted shadow-xs">{emptyMessage}</p>53 </div>54 ) : null}55 </div>56 {table && !empty && table.rows.length > 0 ? (57 <details className="group mt-3">58 <summary className="cursor-pointer select-none text-[12px] text-fg-subtle underline-offset-4 hover:text-fg hover:underline">59 <span className="group-open:hidden">View as table</span>60 <span className="hidden group-open:inline">Hide table</span>61 </summary>62 <div className="mt-2 max-h-56 overflow-auto rounded-md border border-border scrollbar-thin">63 <table className="w-full text-[12px]">64 <thead className="sticky top-0 bg-bg-subtle">65 <tr>66 {table.columns.map((c, i) => (67 <th key={c} className={cn("px-2.5 py-1.5 text-left font-medium text-fg-subtle", i > 0 && "text-right")}>68 {c}69 </th>70 ))}71 </tr>72 </thead>73 <tbody>74 {table.rows.map((r, i) => (75 <tr key={i} className="border-t border-border">76 {r.map((v, j) => (77 <td key={j} className={cn("px-2.5 py-1 tabular", j > 0 ? "text-right font-mono" : "text-fg-muted")}>78 {v}79 </td>80 ))}81 </tr>82 ))}83 </tbody>84 </table>85 </div>86 </details>87 ) : null}88 </CardContent>89 </Card>90 );91}9293/** Text legend rendered in HTML (outside the SVG) so it is readable and selectable. */94export function ChartLegend({ items, className }: { items: Array<{ label: string; color: string; value?: React.ReactNode; dashed?: boolean }>; className?: string }) {95 return (96 <ul className={cn("flex flex-wrap items-center gap-x-4 gap-y-1 text-[12px] text-fg-muted", className)}>97 {items.map((it) => (98 <li key={it.label} className="flex items-center gap-1.5">99 <span aria-hidden className={cn("inline-block h-2 w-3 rounded-[2px]", it.dashed && "border-t-2 border-dashed bg-transparent h-0")} style={it.dashed ? { borderColor: it.color } : { backgroundColor: it.color }} />100 <span>{it.label}</span>101 {it.value !== undefined ? <span className="font-mono tabular text-fg">{it.value}</span> : null}102 </li>103 ))}104 </ul>105 );106}107