SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
4.3 KB · 107 lines tsx
Raw Blame History
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