feat(08-01): create ChartCard wrapper and ChartTooltip components

- ChartCard: shared chart card with title, period badge, empty state, role="img" accessibility
- ChartTooltip: custom Recharts tooltip with design system tokens, color dots, mono font
- Both use CSS custom properties for automatic dark mode support
- TypeScript compiles cleanly with Recharts TooltipContentProps type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-27 08:47:07 -04:00
1 parent a45e6bc10c
commit 31328d5a3b
2 files changed
+111

No files matched your search

@@ -0,0 +1,54 @@
import { ChartLine } from "@phosphor-icons/react";
interface ChartCardProps {
title: string;
period?: string;
isEmpty?: boolean;
emptyHeading?: string;
emptyBody?: string;
ariaLabel?: string;
children: React.ReactNode;
}
export function ChartCard({
title,
period = "14d",
isEmpty = false,
emptyHeading = "No activity yet",
emptyBody = "Charts will appear once your company starts running tasks.",
ariaLabel,
children,
}: ChartCardProps) {
return (
<div
className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-4 py-4"
role="img"
aria-label={ariaLabel}
>
<div className="mb-3 flex items-center justify-between">
<span className="text-[13px] font-semibold text-[color:var(--text)]">
{title}
</span>
<span className="text-[11px] text-[color:var(--muted)]">
{period}
</span>
</div>
{isEmpty ? (
<div className="flex h-[220px] flex-col items-center justify-center gap-2">
<ChartLine
size={32}
className="text-[color:var(--muted)] opacity-50"
/>
<span className="text-[13px] font-semibold text-[color:var(--muted)]">
{emptyHeading}
</span>
<span className="max-w-[200px] text-center text-[11px] text-[color:var(--muted)]">
{emptyBody}
</span>
</div>
) : (
<div style={{ height: 220 }}>{children}</div>
)}
</div>
);
}
@@ -0,0 +1,57 @@
import type { TooltipContentProps } from "recharts";
interface ChartTooltipExtraProps {
valuePrefix?: string;
valueSuffix?: string;
}
type Props = Partial<TooltipContentProps<number, string>> & ChartTooltipExtraProps;
export function ChartTooltip({
active,
payload,
label,
valuePrefix = "",
valueSuffix = "",
}: Props) {
if (!active || !payload?.length) return null;
const dateStr =
typeof label === "string"
? new Date(label + "T00:00:00").toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})
: String(label);
return (
<div
className="rounded-[6px] border border-[color:var(--line-strong)] bg-[color:var(--panel)] px-3 py-2 shadow-md"
style={{ maxWidth: 200, zIndex: 50 }}
>
<div className="text-[11px] text-[color:var(--muted)]">{dateStr}</div>
{payload.map((entry, i) => (
<div key={i} className="mt-1 flex items-center gap-2">
<span
className="inline-block h-2 w-2 shrink-0 rounded-full"
style={{ backgroundColor: entry.color }}
/>
<span
className="text-[13px] font-semibold text-[color:var(--text)]"
style={{
fontFamily: "var(--font-mono)",
fontVariantNumeric: "tabular-nums",
}}
>
{valuePrefix}
{typeof entry.value === "number"
? entry.value.toLocaleString()
: entry.value}
{valueSuffix}
</span>
</div>
))}
</div>
);
}