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:
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user