components/stat-tile/stat-tile.react.tsx
148 lines and 5 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| Glyph | 59 to 65 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| plainLink | 67 to 67 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| StatTile | 69 to 141 | 16 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| StatTiles | 151 to 161 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
| <anonymous:155> | 155 to 157 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: mutation testing has not run on this repo yet |
Imports
It imports components/chart-frame/chart-frame.react.tsx, components/chart-frame/chart-frame.ts. No module of this repo imports it.
Source
import { Children, useId, type ReactNode } from "react";
import { ChartData } from "../chart-frame/chart-frame.react.tsx";
import type { DataTable } from "../chart-frame/chart-frame.ts";
export type StatTone = "ok" | "warn" | "crit" | "nodata";
export interface StatTileLinkProps {
href: string;
className: string;
"data-tone"?: StatTone;
"aria-describedby"?: string;
children: ReactNode;
}
interface StatTileBase {
label: string;
tone?: StatTone;
// Where the figure stands against its threshold: "Over limit", "Near limit", "All up".
// Shown with a status glyph when the tone is warn or crit.
word?: string;
detail?: ReactNode;
// Enarratio's sparkline() output, an SVG string, placed in the chart slot as it comes.
chart?: string;
// One sentence with the chart's numbers: "Actions minutes per day, last 7 days: 260 rising to 340."
chartSummary?: string;
// The chart's numbers (Enarratio's sparklineTable().table). With it the tile sits in a chart
// frame and "Show data" opens the table under the tile, outside the link. Name it in
// chartTableLabel ("Actions minutes by day").
chartTable?: DataTable;
chartTableLabel?: string;
// Opens the view behind the figure: a link with href, or a button with onClick.
href?: string;
onClick?: () => void;
renderLink?: (props: StatTileLinkProps) => ReactNode;
}
export type StatTileProps =
| (StatTileBase & { tone?: "ok" | "warn" | "crit"; figure: ReactNode; unit?: ReactNode })
// No data is a state with a reason, never a zero: the figure says "No data" and the
// reason is the detail line.
| (StatTileBase & { tone: "nodata"; reason: string; figure?: never; unit?: never });
const GLYPH: Record<"warn" | "crit" | "nodata", ReactNode> = {
crit: (
<>
<path fill="currentColor" d="M5 1h6l4 4v6l-4 4H5l-4-4V5z" />
<path fill="var(--surface)" d="M7.2 4h1.6v5H7.2zM7.2 10.4h1.6V12H7.2z" />
</>
),
warn: (
<>
<path fill="currentColor" d="M8 1.2 15.4 14H.6z" />
<path fill="var(--surface)" d="M7.3 6h1.4v4H7.3zM7.3 11h1.4v1.4H7.3z" />
</>
),
nodata: <><circle cx="8" cy="8" r="6.2" fill="none" stroke="currentColor" strokeWidth="1.6" /><path d="M5 8h6" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" /></>,
};
function Glyph({ tone }: { tone: "warn" | "crit" | "nodata" }) {
return (
<svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
{GLYPH[tone]}
</svg>
);
}
const plainLink = ({ children, ...props }: StatTileLinkProps) => <a {...props}>{children}</a>;
export function StatTile(props: StatTileProps) {
const { label, tone, word, chart, chartSummary, chartTable, chartTableLabel, href, onClick, renderLink = plainLink } = props;
const id = useId();
const nodata = props.tone === "nodata";
const describedBy = chart && chartSummary && !nodata ? `${id}-chart` : undefined;
// The figure, its label and its word on one line, the detail beneath (the Portal's compact
// tile). The figure comes first in the DOM, so the name reads "6 of 6 Sites up All up".
const body =
props.tone === "nodata" ? (
<>
<span className="cap-tile-line">
<span className="cap-tile-figure">
<Glyph tone="nodata" />
No data
</span>
<span className="cap-tile-label">{label}</span>
</span>
<span className="cap-tile-detail">{props.reason}</span>
</>
) : (
<>
<span className="cap-tile-line">
<span className="cap-tile-figure">
{props.figure}
{props.unit ? <> <small>{props.unit}</small></> : null}
</span>
<span className="cap-tile-label">{label}</span>
{word ? (
<span className="cap-tile-word">
{props.tone === "warn" || props.tone === "crit" ? <Glyph tone={props.tone} /> : null}
{word}
</span>
) : null}
</span>
{props.detail ? <span className="cap-tile-detail">{props.detail}</span> : null}
{chart ? <span className="cap-tile-chart" aria-hidden="true" dangerouslySetInnerHTML={{ __html: chart }} /> : null}
</>
);
const tile =
href !== undefined ? (
renderLink({ href, className: "cap-tile", "data-tone": tone, "aria-describedby": describedBy, children: body })
) : (
<button type="button" className="cap-tile" data-tone={tone} aria-describedby={describedBy} onClick={onClick}>
{body}
</button>
);
// With a table the tile joins the chart frame: the sentence is the frame's summary (hidden
// visually, still text), and "Show data" sits under the tile, never inside the link.
if (describedBy && chartTable) {
return (
<div className="cap-chart" data-variant="tile" data-summary="hidden">
{tile}
<p className="cap-chart-summary" id={describedBy}>
{chartSummary}
</p>
<ChartData table={chartTable} label={chartTableLabel ?? `${label}, by period`} />
</div>
);
}
return (
<>
{tile}
{describedBy ? (
<span className="cap-sr-only" id={describedBy}>
{chartSummary}
</span>
) : null}
</>
);
}
export interface StatTilesProps {
// Names the list for a screen reader: "Overview figures".
label?: string;
children: ReactNode;
}
// The row of tiles: each as wide as its text, wrapping onto more rows instead of cutting a
// word, so a phone gets two or one across without a breakpoint.
export function StatTiles({ label, children }: StatTilesProps) {
return (
<div className="cap-tiles">
<ul className="cap-tiles-list" aria-label={label}>
{Children.toArray(children).map((child, i) => (
<li key={i}>{child}</li>
))}
</ul>
</div>
);
}