Skip to the content

Capsid menu

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

The functions of stat-tile.react.tsx
FunctionLinesComplexityCoveredCRAPMutation
Glyph59 to 651No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
plainLink67 to 671No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
StatTile69 to 14116No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
StatTiles151 to 1611No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
<anonymous:155>155 to 1571No data: no coverage report in this mapNo data: no coverage report in this mapNo 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>
  );
}