Behive Tech Registry

Stat

v0.4.0

pnpm add @behivetech/atoms.stat

registry/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";
4
5export 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}
20
21export const Stat = ({
22 label,
23 value,
24 prefix,
25 suffix,
26 className,
27 ...rest
28}: StatProps) => {
29 const [rootClass, getChildClass] = getClassName({
30 rootClass: "BHT__Stat",
31 styles,
32 className,
33 });
34
35 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.
3
4.BHT__Stat {
5 display: flex;
6 flex-direction: column;
7 gap: var(--spacing-2);
8
9 &__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 }
18
19 &__value-row {
20 display: flex;
21 align-items: baseline;
22 gap: var(--spacing-1);
23 }
24
25 &__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 }
36
37 &__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";
2
3export const BasicStat = () => <Stat label="Total Pages" value={42} />;
4
5export const StatWithPrefix = () => (
6 <Stat label="Revenue" value={1200} prefix="$" />
7);
8
9export const StatWithSuffix = () => (
10 <Stat label="Uptime" value={99.9} suffix="%" />
11);
12
13export 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";
4
5describe("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 });
11
12 it("renders prefix when provided", () => {
13 const { getByText } = render(
14 <Stat label="Revenue" value={1200} prefix="$" />,
15 );
16 expect(getByText("$")).toBeTruthy();
17 });
18
19 it("renders suffix when provided", () => {
20 const { getByText } = render(
21 <Stat label="Uptime" value={99.9} suffix="%" />,
22 );
23 expect(getByText("%")).toBeTruthy();
24 });
25
26 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});