Stat
v0.4.0registry/behivetech/atoms/stat · depends on @behivetech/get-class-name
stat.tsx
1import { type HTMLAttributes } from "react";2import { getClassName } from "@behivetech/get-class-name";3import styles from "./stat.module.scss";45export interface StatProps extends Omit<6 HTMLAttributes<HTMLDivElement>,7 "prefix"8> {9 /** Descriptive label shown above the value */10 label: string;11 /** The primary metric to display */12 value: string | number;13 /** Optional prefix rendered before the value (e.g. "$") */14 prefix?: React.ReactNode;15 /** Optional suffix rendered after the value (e.g. "%", "ms") */16 suffix?: React.ReactNode;17 /** Additional class names to merge with the component root element */18 className?: string;19}2021export const Stat = ({22 label,23 value,24 prefix,25 suffix,26 className,27 ...rest28}: StatProps) => {29 const [rootClass, getChildClass] = getClassName({30 rootClass: "BHT__Stat",31 styles,32 className,33 });3435 return (36 <div {...rest} className={rootClass}>37 <span className={getChildClass("label")}>{label}</span>38 <div className={getChildClass("value-row")}>39 {prefix && <span className={getChildClass("prefix")}>{prefix}</span>}40 <span className={getChildClass("value")}>{value}</span>41 {suffix && <span className={getChildClass("suffix")}>{suffix}</span>}42 </div>43 </div>44 );45};
stat.module.scss
1// Material Design 3 type roles for a single metric: label-medium caption,2// headline-medium value, title-medium affixes.34.BHT__Stat {5 display: flex;6 flex-direction: column;7 gap: var(--spacing-2);89 &__label {10 font-family: var(--md-sys-typescale-label-medium-font, var(--font-body));11 font-size: var(--md-sys-typescale-label-medium-size, 0.75rem);12 font-weight: var(--md-sys-typescale-label-medium-weight, 500);13 line-height: var(--md-sys-typescale-label-medium-line-height, 1rem);14 letter-spacing: var(--md-sys-typescale-label-medium-tracking, 0.0417em);15 color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));16 text-transform: uppercase;17 }1819 &__value-row {20 display: flex;21 align-items: baseline;22 gap: var(--spacing-1);23 }2425 &__value {26 font-family: var(27 --md-sys-typescale-headline-medium-font,28 var(--font-heading)29 );30 font-size: var(--md-sys-typescale-headline-medium-size, 1.75rem);31 font-weight: var(--md-sys-typescale-headline-medium-weight, 400);32 line-height: var(--md-sys-typescale-headline-medium-line-height, 2.25rem);33 letter-spacing: var(--md-sys-typescale-headline-medium-tracking, 0);34 color: var(--md-sys-color-on-surface, var(--color-text-heading));35 }3637 &__prefix,38 &__suffix {39 font-family: var(--md-sys-typescale-title-medium-font, var(--font-body));40 font-size: var(--md-sys-typescale-title-medium-size, 1rem);41 font-weight: var(--md-sys-typescale-title-medium-weight, 500);42 line-height: var(--md-sys-typescale-title-medium-line-height, 1.5rem);43 letter-spacing: var(--md-sys-typescale-title-medium-tracking, 0.0094em);44 color: var(--md-sys-color-on-surface-variant, var(--color-text-muted));45 }46}
index.ts
1export { Stat } from "./stat.js";2export type { StatProps } from "./stat.js";
stat.composition.tsx
1import { Stat } from "./stat.js";23export const BasicStat = () => <Stat label="Total Pages" value={42} />;45export const StatWithPrefix = () => (6 <Stat label="Revenue" value={1200} prefix="$" />7);89export const StatWithSuffix = () => (10 <Stat label="Uptime" value={99.9} suffix="%" />11);1213export const StatWithPrefixAndSuffix = () => (14 <Stat label="Response Time" value={120} suffix="ms" />15);
stat.spec.tsx
1import { render } from "@testing-library/react";2import { describe, it, expect } from "vitest";3import { Stat } from "./stat.js";45describe("Stat", () => {6 it("renders label and value", () => {7 const { getByText } = render(<Stat label="Total Pages" value={42} />);8 expect(getByText("Total Pages")).toBeTruthy();9 expect(getByText("42")).toBeTruthy();10 });1112 it("renders prefix when provided", () => {13 const { getByText } = render(14 <Stat label="Revenue" value={1200} prefix="$" />,15 );16 expect(getByText("$")).toBeTruthy();17 });1819 it("renders suffix when provided", () => {20 const { getByText } = render(21 <Stat label="Uptime" value={99.9} suffix="%" />,22 );23 expect(getByText("%")).toBeTruthy();24 });2526 it("omits prefix and suffix wrappers when not provided", () => {27 const { container } = render(<Stat label="Count" value={5} />);28 expect(container.querySelector(".Stat__prefix")).toBeNull();29 expect(container.querySelector(".Stat__suffix")).toBeNull();30 });31});