fix: fix spark chart dot distortion and add leading-slot + module logos to stats BarList

- Wrap `SparkAreaChart` SVG in a `div` and move the last-point dot from an inline `<circle>` to an absolutely-positioned HTML `<span>` so it stays a perfect circle regardless of `preserveAspectRatio="none"` scaling
- Add `leading?: ReactNode` slot to `BarListEntry` so any icon, logo, or swatch can be rendered before the row name; wrap name + leading in a flex container so truncation still works
- Wire `ModuleLogo` into the stats page `BarList` as the `leading` element for each module row; refactor `moduleLabel` to `findModule` so the full `ModuleSummary` is available for both the label and the logo
- Add `/stats` to the prerender list so the page is statically generated at build time
This commit is contained in:
2026-05-25 21:19:04 -04:00
parent 2103e3607e
commit c1df786210
4 changed files with 81 additions and 64 deletions
+17 -10
View File
@@ -1,8 +1,12 @@
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export type BarListEntry = {
name: string;
value: number;
/** Optional element rendered before the name (icon, logo, swatch, etc.). */
leading?: ReactNode;
/** Optional label rendered on the right edge of the row (e.g. "62%"). */
trailing?: string;
/** Secondary trailing label rendered before `trailing` in a muted weight. */
@@ -46,16 +50,19 @@ export function BarList({ data, emptyMessage = "No data yet.", className }: BarL
style={{ width: `${pct}%` }}
/>
<div className="relative flex h-full items-center justify-between gap-3 px-2 text-xs">
{entry.href ? (
<a
href={entry.href}
className="truncate font-medium text-foreground after:absolute after:inset-0 hover:underline"
>
{entry.name}
</a>
) : (
<span className="truncate font-medium text-foreground">{entry.name}</span>
)}
<div className="flex min-w-0 items-center gap-2">
{entry.leading}
{entry.href ? (
<a
href={entry.href}
className="truncate font-medium text-foreground after:absolute after:inset-0 hover:underline"
>
{entry.name}
</a>
) : (
<span className="truncate font-medium text-foreground">{entry.name}</span>
)}
</div>
{entry.trailing || entry.trailingSecondary ? (
<span className="shrink-0 font-mono text-xs tabular-nums">
{entry.trailingSecondary ? (
+48 -41
View File
@@ -53,50 +53,57 @@ export function SparkAreaChart({ data, ariaLabel, height = 80, className }: Spar
const last = points[points.length - 1];
return (
<svg
role="img"
aria-label={ariaLabel}
viewBox={`0 0 ${VIEW_WIDTH} ${height}`}
preserveAspectRatio="none"
className={cn("w-full overflow-visible", className)}
style={{ height }}
>
<line
x1={0}
x2={VIEW_WIDTH}
y1={baselineY}
y2={baselineY}
stroke="var(--color-border)"
strokeWidth={1}
vectorEffect="non-scaling-stroke"
/>
<path d={fillPath} fill="var(--color-chart-1)" fillOpacity={0.35} />
<path
d={linePath}
fill="none"
stroke="var(--color-chart-2)"
strokeWidth={1.5}
strokeLinejoin="round"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
{last ? (
<circle
cx={last.x}
cy={last.y}
r={3}
fill="var(--color-background)"
stroke="var(--color-foreground)"
strokeWidth={1.5}
<div className={cn("relative w-full", className)} style={{ height }}>
<svg
role="img"
aria-label={ariaLabel}
viewBox={`0 0 ${VIEW_WIDTH} ${height}`}
preserveAspectRatio="none"
className="block size-full overflow-visible"
>
<line
x1={0}
x2={VIEW_WIDTH}
y1={baselineY}
y2={baselineY}
stroke="var(--color-border)"
strokeWidth={1}
vectorEffect="non-scaling-stroke"
/>
<path d={fillPath} fill="var(--color-chart-1)" fillOpacity={0.35} />
<path
d={linePath}
fill="none"
stroke="var(--color-chart-2)"
strokeWidth={1.5}
strokeLinejoin="round"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
{points.map(({ x, point }) => (
<rect key={point.label} x={x - 2} y={0} width={4} height={height} fill="transparent">
<title>{`${point.label}: ${point.value}`}</title>
</rect>
))}
</svg>
{/*
* The chart SVG uses `preserveAspectRatio="none"` so the curve stretches
* to the container width. That stretches any inline <circle> into a
* horizontal oval, so the last-point dot lives in the DOM instead and
* stays perfectly round at any width.
*/}
{last ? (
<span
aria-hidden
className="pointer-events-none absolute size-[9px] rounded-full border-[1.5px] border-foreground bg-background"
style={{
right: 0,
top: `${(last.y / height) * 100}%`,
transform: "translate(50%, -50%)",
}}
/>
) : null}
{points.map(({ x, point }) => (
<rect key={point.label} x={x - 2} y={0} width={4} height={height} fill="transparent">
<title>{`${point.label}: ${point.value}`}</title>
</rect>
))}
</svg>
</div>
);
}
+1
View File
@@ -65,6 +65,7 @@ export function listPrerenderPages() {
"/docs/llms.txt",
"/docs/llms-full.txt",
...modules,
"/stats",
];
return paths.map((path) => ({ path, prerender: { enabled: true } }));
}
+15 -13
View File
@@ -2,6 +2,7 @@ import type { CategoryId, ModuleSummary } from "@stanza/registry";
import { categoryLabel, KNOWN_CATEGORIES } from "@stanza/registry";
import { createFileRoute, Link, useLoaderData } from "@tanstack/react-router";
import { ModuleLogo } from "@/components/module-logo";
import { BarList } from "@/components/ui/bar-list";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { SparkAreaChart } from "@/components/ui/spark-chart";
@@ -51,12 +52,8 @@ function StatsPage() {
const stats = Route.useLoaderData();
const { registry } = useLoaderData({ from: "__root__" });
const moduleLabel = (category: CategoryId, id: string): string => {
const found = registry.modules.find(
(m: ModuleSummary) => m.category === category && m.id === id,
);
return found?.label ?? id;
};
const findModule = (category: CategoryId, id: string): ModuleSummary | undefined =>
registry.modules.find((m: ModuleSummary) => m.category === category && m.id === id);
const activitySum = stats.activity30d.reduce((acc, day) => acc + day.count, 0);
@@ -122,13 +119,18 @@ function StatsPage() {
</CardHeader>
<CardContent>
<BarList
data={entries.map((entry) => ({
name: moduleLabel(category, entry.id),
value: entry.count,
trailingSecondary: numberFormatter.format(entry.count),
trailing: percentFormatter.format(entry.share),
href: moduleHref(category, entry.id),
}))}
data={entries.map((entry) => {
const summary = findModule(category, entry.id);
const label = summary?.label ?? entry.id;
return {
name: label,
value: entry.count,
leading: <ModuleLogo logo={summary?.logo} label={label} size="sm" />,
trailingSecondary: numberFormatter.format(entry.count),
trailing: percentFormatter.format(entry.share),
href: moduleHref(category, entry.id),
};
})}
/>
</CardContent>
</Card>