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
Alex Kim
28h / 40h
Blake Torres
36h / 40h
Casey Reyes
38h / 40h
Dana Patel
41h / 40h
Emery Walsh
22h / 40h
Fran Moore
40h / 40h
Status thresholds
| Utilization | Status | Token |
|---|---|---|
| < 70% | Available | --status-success |
| 70–89% | Moderate | --status-warning |
| 90–100% | Near Full | --status-caution |
| > 100% | Overloaded | --status-danger |
Card anatomy
| Element | Spec |
|---|---|
| Arc Gauge | 80 px, 8 px track, centred in card |
| Name | 12 px semibold |
| Hours | 10 px, --text-tertiary, format: Xh / Yh |
| Status badge | Capsule pill, 10 px semibold, status colour on status/0.15 background |
| Overloaded tint | Card 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>
);
}