mirror of
https://github.com/jakejarvis/stanza.git
synced 2026-07-16 18:05:58 -04:00
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:
@@ -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 ? (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ export function listPrerenderPages() {
|
||||
"/docs/llms.txt",
|
||||
"/docs/llms-full.txt",
|
||||
...modules,
|
||||
"/stats",
|
||||
];
|
||||
return paths.map((path) => ({ path, prerender: { enabled: true } }));
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user