New

Team Capacity Grid

A responsive grid of member utilization cards, each showing an arc gauge, hours logged versus capacity, and a status badge. Composes the Gauge component. Reference: Warren's TeamUtilizationSection.

Demo

70%

Alex Kim

28h / 40h

Moderate
90%

Blake Torres

36h / 40h

Near Full
95%

Casey Reyes

38h / 40h

Near Full
102%

Dana Patel

41h / 40h

Overloaded
55%

Emery Walsh

22h / 40h

Available
100%

Fran Moore

40h / 40h

Near Full

Status thresholds

UtilizationStatusToken
< 70%Available--status-success
70–89%Moderate--status-warning
90–100%Near Full--status-caution
> 100%Overloaded--status-danger

Card anatomy

ElementSpec
Arc Gauge80 px, 8 px track, centred in card
Name12 px semibold
Hours10 px, --text-tertiary, format: Xh / Yh
Status badgeCapsule pill, 10 px semibold, status colour on status/0.15 background
Overloaded tintCard background gets --status-danger / 0.08 fill

Grid layout

Use grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)) for responsive behaviour. On macOS with wide panels, this yields 5–6 columns. On iPhone it collapses to 2. Minimum column width is 140 px to keep gauges legible.

Implementation

TeamCapacityGrid.tsx
tsx
import { ArcGauge } from "@/components/ui/ArcGauge";

interface Member {
  name: string;
  hours: number;
  capacity: number;
  avatarUrl?: string;
  initials?: string;
}

function utilizationStatus(hours: number, capacity: number) {
  const pct = hours / capacity;
  if (pct > 1)    return { label: "Overloaded", color: "rgb(var(--status-danger))" };
  if (pct >= 0.9) return { label: "Near Full",  color: "rgb(var(--status-caution))" };
  if (pct >= 0.7) return { label: "Moderate",   color: "rgb(var(--status-warning))" };
  return            { label: "Available",        color: "rgb(var(--status-success))" };
}

export function TeamCapacityGrid({ members }: { members: Member[] }) {
  return (
    <div
      style={{
        display: "grid",
        gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
        gap: 12,
      }}
    >
      {members.map((member) => {
        const pct = member.hours / member.capacity;
        const status = utilizationStatus(member.hours, member.capacity);
        const isOverloaded = pct > 1;

        return (
          <div
            key={member.name}
            style={{
              padding: 14,
              borderRadius: 12,
              background: isOverloaded
                ? `rgb(var(--status-danger) / 0.08)`
                : "rgb(var(--surface))",
              border: "1px solid rgb(var(--border))",
              boxShadow: "var(--shadow-card)",
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
              gap: 8,
              textAlign: "center",
            }}
          >
            <ArcGauge pct={pct} color={status.color} size={80} />

            <div>
              <p style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>{member.name}</p>
              <p style={{ fontSize: 10, color: "rgb(var(--text-tertiary))" }}>
                {member.hours}h / {member.capacity}h
              </p>
            </div>

            <span
              style={{
                fontSize: 10,
                fontWeight: 600,
                padding: "2px 8px",
                borderRadius: 99,
                background: status.color.replace(")", " / 0.15)").replace("rgb(", "rgba("),
                color: status.color,
              }}
            >
              {status.label}
            </span>
          </div>
        );
      })}
    </div>
  );
}