Shared Components

Reusable components shared across multiple pages of the MetaForge Digital Twin Dashboard. Organized into three categories: Charts, Data Display, and Feedback.


Charts

Lightweight chart components for embedding in cards and dashboards. Built with Recharts and raw SVG.


src/components/charts/risk-gauge.tsx

Semicircular gauge using Recharts PieChart. Color changes based on value thresholds: green (0–25), yellow (25–50), orange (50–75), red (75–100). The numeric value is displayed in the center.

import { PieChart, Pie, Cell } from "recharts";

import { cn } from "@/lib/utils";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface RiskGaugeProps {
  /** Value from 0 to 100. */
  value: number;
  /** Diameter in pixels. @default 140 */
  size?: number;
  /** Label rendered below the value. */
  label?: string;
  /** Additional class names for the wrapper. */
  className?: string;
}

// ---------------------------------------------------------------------------
// Color helpers
// ---------------------------------------------------------------------------

function gaugeColor(value: number): string {
  if (value <= 25) return "#22c55e"; // green-500
  if (value <= 50) return "#eab308"; // yellow-500
  if (value <= 75) return "#f97316"; // orange-500
  return "#ef4444"; // red-500
}

function gaugeBgColor(): string {
  return "#e5e7eb"; // gray-200
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function RiskGauge({
  value,
  size = 140,
  label,
  className,
}: RiskGaugeProps) {
  const clamped = Math.max(0, Math.min(100, value));
  const color = gaugeColor(clamped);

  // Recharts Pie for semicircle: startAngle=180, endAngle=0
  const data = [
    { name: "value", value: clamped },
    { name: "remainder", value: 100 - clamped },
  ];

  return (
    <div
      className={cn("relative flex flex-col items-center", className)}
      style={{ width: size, height: size * 0.65 }}
    >
      <PieChart width={size} height={size * 0.65}>
        <Pie
          data={data}
          cx={size / 2}
          cy={size * 0.6}
          startAngle={180}
          endAngle={0}
          innerRadius={size * 0.3}
          outerRadius={size * 0.44}
          dataKey="value"
          stroke="none"
          isAnimationActive
          animationDuration={800}
        >
          <Cell fill={color} />
          <Cell fill={gaugeBgColor()} />
        </Pie>
      </PieChart>

      {/* Center label */}
      <div
        className="absolute flex flex-col items-center justify-center"
        style={{
          top: size * 0.22,
          left: "50%",
          transform: "translateX(-50%)",
        }}
      >
        <span
          className="font-bold leading-none"
          style={{ fontSize: size * 0.18, color }}
        >
          {clamped}
        </span>
        {label && (
          <span
            className="mt-0.5 text-muted-foreground"
            style={{ fontSize: Math.max(10, size * 0.08) }}
          >
            {label}
          </span>
        )}
      </div>
    </div>
  );
}

src/components/charts/progress-ring.tsx

Circular progress ring rendered with SVG. Animates the stroke-dasharray on mount.

import { cn } from "@/lib/utils";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface ProgressRingProps {
  /** Completion percentage (0--100). */
  value: number;
  /** Diameter in pixels. @default 64 */
  size?: number;
  /** Stroke width in pixels. @default 6 */
  strokeWidth?: number;
  /** Stroke color. Defaults to the current primary color. */
  color?: string;
  /** Label displayed in the center. Falls back to `value%`. */
  label?: string;
  /** Additional class names for the wrapper `<div>`. */
  className?: string;
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function ProgressRing({
  value,
  size = 64,
  strokeWidth = 6,
  color,
  label,
  className,
}: ProgressRingProps) {
  const clamped = Math.max(0, Math.min(100, value));
  const radius = (size - strokeWidth) / 2;
  const circumference = 2 * Math.PI * radius;
  const strokeDashoffset = circumference - (clamped / 100) * circumference;
  const center = size / 2;

  return (
    <div
      className={cn("relative inline-flex items-center justify-center", className)}
      style={{ width: size, height: size }}
    >
      <svg width={size} height={size} className="-rotate-90">
        {/* Background circle */}
        <circle
          cx={center}
          cy={center}
          r={radius}
          fill="none"
          stroke="currentColor"
          strokeWidth={strokeWidth}
          className="text-muted/30"
        />
        {/* Progress arc */}
        <circle
          cx={center}
          cy={center}
          r={radius}
          fill="none"
          stroke={color ?? "currentColor"}
          strokeWidth={strokeWidth}
          strokeLinecap="round"
          strokeDasharray={circumference}
          strokeDashoffset={strokeDashoffset}
          className={cn(!color && "text-primary")}
          style={{
            transition: "stroke-dashoffset 0.6s ease-in-out",
          }}
        />
      </svg>

      {/* Center label */}
      <span
        className="absolute text-xs font-semibold"
        style={{ fontSize: Math.max(10, size * 0.2) }}
      >
        {label ?? `${clamped}%`}
      </span>
    </div>
  );
}

src/components/charts/sparkline.tsx

Minimal line chart using Recharts. No axes, labels, or grid – just the line with an area fill. Ideal for inline trend indicators.

import {
  AreaChart,
  Area,
  ResponsiveContainer,
} from "recharts";

import { cn } from "@/lib/utils";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface SparklineProps {
  /** Ordered data points (y-values). */
  data: number[];
  /** Width in pixels. @default 80 */
  width?: number;
  /** Height in pixels. @default 32 */
  height?: number;
  /** Line and fill color. @default "currentColor" mapped to chart-1. */
  color?: string;
  /** Additional class names for the wrapper. */
  className?: string;
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function Sparkline({
  data,
  width = 80,
  height = 32,
  color = "var(--color-chart-1)",
  className,
}: SparklineProps) {
  const chartData = data.map((value, index) => ({ index, value }));

  return (
    <div className={cn("inline-block", className)} style={{ width, height }}>
      <ResponsiveContainer width="100%" height="100%">
        <AreaChart
          data={chartData}
          margin={{ top: 2, right: 2, bottom: 2, left: 2 }}
        >
          <defs>
            <linearGradient id={`sparkGrad-${color}`} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor={color} stopOpacity={0.3} />
              <stop offset="100%" stopColor={color} stopOpacity={0.05} />
            </linearGradient>
          </defs>
          <Area
            type="monotone"
            dataKey="value"
            stroke={color}
            strokeWidth={1.5}
            fill={`url(#sparkGrad-${color})`}
            isAnimationActive={false}
          />
        </AreaChart>
      </ResponsiveContainer>
    </div>
  );
}

src/components/charts/status-dot.tsx

Small colored dot indicator with optional pulse animation. Used for online/offline/warning/error states across the dashboard.

import { cn } from "@/lib/utils";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type StatusDotVariant = "online" | "offline" | "warning" | "error";

export interface StatusDotProps {
  /** Visual status. */
  status: StatusDotVariant;
  /** Diameter. @default "md" */
  size?: "sm" | "md" | "lg";
  /** Enable a subtle pulse animation. @default false */
  pulse?: boolean;
  /** Additional class names. */
  className?: string;
}

// ---------------------------------------------------------------------------
// Mappings
// ---------------------------------------------------------------------------

const COLOR_MAP: Record<StatusDotVariant, string> = {
  online: "bg-green-500",
  offline: "bg-red-500",
  warning: "bg-amber-500",
  error: "bg-red-500",
};

const SIZE_MAP: Record<NonNullable<StatusDotProps["size"]>, string> = {
  sm: "h-1.5 w-1.5",
  md: "h-2.5 w-2.5",
  lg: "h-3.5 w-3.5",
};

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function StatusDot({
  status,
  size = "md",
  pulse = false,
  className,
}: StatusDotProps) {
  return (
    <span
      className={cn(
        "inline-block shrink-0 rounded-full",
        COLOR_MAP[status],
        SIZE_MAP[size],
        pulse && "animate-pulse-dot",
        className,
      )}
      role="status"
      aria-label={status}
    />
  );
}

Data Display

Components for rendering metrics, statuses, timelines, and empty states.


src/components/data-display/metric-card.tsx

Stats card displaying a title, a large value, an optional change percentage with directional arrow, and an optional sparkline. Built on top of the Card UI component.

import { type LucideIcon, TrendingUp, TrendingDown, Minus } from "lucide-react";

import { cn } from "@/lib/utils";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Sparkline } from "@/components/charts/sparkline";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface MetricCardProps {
  /** Short label above the value. */
  title: string;
  /** The primary metric value (number or formatted string). */
  value: string | number;
  /** Percentage change (positive = improvement, negative = decline). */
  change?: number;
  /** Optional icon rendered top-right. */
  icon?: LucideIcon;
  /** Trend direction. When omitted, inferred from `change`. */
  trend?: "up" | "down" | "flat";
  /** Data points for an inline sparkline below the value. */
  sparklineData?: number[];
  /** Additional class names for the Card wrapper. */
  className?: string;
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function resolveTrend(
  change: number | undefined,
  trend: MetricCardProps["trend"],
): "up" | "down" | "flat" {
  if (trend) return trend;
  if (change == null || change === 0) return "flat";
  return change > 0 ? "up" : "down";
}

const TREND_ICON: Record<"up" | "down" | "flat", LucideIcon> = {
  up: TrendingUp,
  down: TrendingDown,
  flat: Minus,
};

const TREND_COLOR: Record<"up" | "down" | "flat", string> = {
  up: "text-green-600 dark:text-green-400",
  down: "text-red-600 dark:text-red-400",
  flat: "text-muted-foreground",
};

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function MetricCard({
  title,
  value,
  change,
  icon: Icon,
  trend,
  sparklineData,
  className,
}: MetricCardProps) {
  const resolved = resolveTrend(change, trend);
  const TrendIcon = TREND_ICON[resolved];

  return (
    <Card className={cn("relative overflow-hidden", className)}>
      <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
        <CardTitle className="text-sm font-medium">{title}</CardTitle>
        {Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
      </CardHeader>
      <CardContent>
        <div className="text-2xl font-bold">{value}</div>

        {/* Change row */}
        {change != null && (
          <div className={cn("mt-1 flex items-center gap-1 text-xs", TREND_COLOR[resolved])}>
            <TrendIcon className="h-3 w-3" />
            <span>
              {change > 0 ? "+" : ""}
              {change.toFixed(1)}%
            </span>
          </div>
        )}

        {/* Optional sparkline */}
        {sparklineData && sparklineData.length > 1 && (
          <div className="mt-3">
            <Sparkline data={sparklineData} width={160} height={28} />
          </div>
        )}
      </CardContent>
    </Card>
  );
}

src/components/data-display/status-badge.tsx

Colored badge that maps various domain-specific statuses to badge variants. Accepts arbitrary status strings and automatically selects the appropriate color through a lookup table. Falls back to the default variant for unknown statuses.

import { cn } from "@/lib/utils";
import { Badge, type BadgeProps } from "@/components/ui/badge";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface StatusBadgeProps extends Omit<BadgeProps, "variant"> {
  /** The status string to render (e.g. "running", "passed", "critical"). */
  status: string;
  /** Force a specific badge variant. When omitted, auto-detected from status. */
  variant?: BadgeProps["variant"];
}

// ---------------------------------------------------------------------------
// Status-to-variant mapping
// ---------------------------------------------------------------------------

type BadgeVariant = NonNullable<BadgeProps["variant"]>;

const STATUS_VARIANT_MAP: Record<string, BadgeVariant> = {
  // Session statuses
  running: "default",
  completed: "secondary",
  failed: "destructive",
  pending: "outline",
  cancelled: "outline",

  // Lifecycle statuses
  active: "default",
  deprecated: "outline",
  "end-of-life": "destructive",
  eol: "destructive",
  nrnd: "outline", // Not Recommended for New Designs

  // Risk levels
  low: "secondary",
  medium: "outline",
  high: "destructive",
  critical: "destructive",

  // Compliance statuses
  compliant: "secondary",
  "non-compliant": "destructive",
  "in-progress": "default",
  "not-started": "outline",

  // Agent statuses
  online: "default",
  offline: "outline",
  idle: "secondary",
  busy: "default",
  error: "destructive",

  // Approval statuses
  approved: "secondary",
  rejected: "destructive",
  "awaiting-review": "outline",

  // Testing statuses
  passed: "secondary",
  skipped: "outline",
};

const STATUS_LABEL_MAP: Record<string, string> = {
  eol: "EOL",
  nrnd: "NRND",
  "end-of-life": "End of Life",
  "non-compliant": "Non-Compliant",
  "in-progress": "In Progress",
  "not-started": "Not Started",
  "awaiting-review": "Awaiting Review",
};

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function StatusBadge({
  status,
  variant,
  className,
  ...props
}: StatusBadgeProps) {
  const resolvedVariant: BadgeVariant =
    variant ?? STATUS_VARIANT_MAP[status.toLowerCase()] ?? "outline";

  const label =
    STATUS_LABEL_MAP[status.toLowerCase()] ??
    status.charAt(0).toUpperCase() + status.slice(1);

  return (
    <Badge
      variant={resolvedVariant}
      className={cn("whitespace-nowrap", className)}
      {...props}
    >
      {label}
    </Badge>
  );
}

src/components/data-display/activity-timeline.tsx

Vertical timeline rendering a list of events with colored dots, timestamps, and optional agent attribution. Color-coded by severity level.

import { formatDistanceToNow } from "date-fns";

import { cn } from "@/lib/utils";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type TimelineLevel = "info" | "warn" | "error" | "success";

export interface TimelineEvent {
  /** Unique key. */
  id: string;
  /** ISO 8601 timestamp. */
  timestamp: string;
  /** Short event title. */
  title: string;
  /** Optional longer description. */
  description?: string;
  /** Name of the agent that produced this event. */
  agent?: string;
  /** Severity level — determines dot color. */
  level?: TimelineLevel;
}

export interface ActivityTimelineProps {
  events: TimelineEvent[];
  /** Additional class names for the wrapper. */
  className?: string;
}

// ---------------------------------------------------------------------------
// Color mappings
// ---------------------------------------------------------------------------

const DOT_COLOR: Record<TimelineLevel, string> = {
  info: "bg-blue-500",
  warn: "bg-amber-500",
  error: "bg-red-500",
  success: "bg-green-500",
};

const TEXT_COLOR: Record<TimelineLevel, string> = {
  info: "text-blue-600 dark:text-blue-400",
  warn: "text-amber-600 dark:text-amber-400",
  error: "text-red-600 dark:text-red-400",
  success: "text-green-600 dark:text-green-400",
};

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function ActivityTimeline({ events, className }: ActivityTimelineProps) {
  if (events.length === 0) return null;

  return (
    <div className={cn("relative space-y-0", className)}>
      {/* Vertical line */}
      <div className="absolute left-[7px] top-2 bottom-2 w-px bg-border" />

      {events.map((event) => {
        const level = event.level ?? "info";
        const relativeTime = formatDistanceToNow(new Date(event.timestamp), {
          addSuffix: true,
        });

        return (
          <div key={event.id} className="relative flex gap-3 pb-6 last:pb-0">
            {/* Dot */}
            <div className="relative z-10 mt-1.5">
              <span
                className={cn(
                  "block h-[9px] w-[9px] rounded-full ring-2 ring-background",
                  DOT_COLOR[level],
                )}
              />
            </div>

            {/* Content */}
            <div className="flex-1 space-y-0.5">
              <div className="flex items-baseline gap-2">
                <p className="text-sm font-medium leading-tight">
                  {event.title}
                </p>
                {event.agent && (
                  <span
                    className={cn(
                      "text-[10px] font-semibold uppercase tracking-wider",
                      TEXT_COLOR[level],
                    )}
                  >
                    {event.agent}
                  </span>
                )}
              </div>

              {event.description && (
                <p className="text-xs text-muted-foreground">
                  {event.description}
                </p>
              )}

              <p className="text-[11px] text-muted-foreground/60">
                {relativeTime}
              </p>
            </div>
          </div>
        );
      })}
    </div>
  );
}

src/components/data-display/empty-state.tsx

Centered empty-state placeholder with an icon, title, description, and an optional call-to-action button.

import { type LucideIcon } from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface EmptyStateAction {
  label: string;
  onClick: () => void;
}

export interface EmptyStateProps {
  /** Large icon rendered above the title. */
  icon: LucideIcon;
  /** Heading text. */
  title: string;
  /** Explanatory body text. */
  description: string;
  /** Optional CTA button. */
  action?: EmptyStateAction;
  /** Additional class names. */
  className?: string;
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export function EmptyState({
  icon: Icon,
  title,
  description,
  action,
  className,
}: EmptyStateProps) {
  return (
    <div
      className={cn(
        "flex flex-col items-center justify-center rounded-lg border border-dashed p-8 text-center animate-fade-in",
        className,
      )}
    >
      <div className="flex h-14 w-14 items-center justify-center rounded-full bg-muted">
        <Icon className="h-6 w-6 text-muted-foreground" />
      </div>
      <h3 className="mt-4 text-lg font-semibold">{title}</h3>
      <p className="mt-1 max-w-sm text-sm text-muted-foreground">
        {description}
      </p>
      {action && (
        <Button onClick={action.onClick} className="mt-4" size="sm">
          {action.label}
        </Button>
      )}
    </div>
  );
}

Usage Example

import { Inbox } from "lucide-react";
import { EmptyState } from "@/components/data-display/empty-state";

function NoSessions() {
  return (
    <EmptyState
      icon={Inbox}
      title="No sessions yet"
      description="Start a new orchestration session to see it listed here."
      action={{
        label: "New Session",
        onClick: () => console.log("create session"),
      }}
    />
  );
}

src/components/data-display/loading-skeleton.tsx

Preset skeleton layouts for common page elements. Each variant composes the base Skeleton component from @/components/ui/skeleton.

import { cn } from "@/lib/utils";
import { Skeleton } from "@/components/ui/skeleton";

// ---------------------------------------------------------------------------
// TableSkeleton
// ---------------------------------------------------------------------------

export interface TableSkeletonProps {
  /** Number of skeleton rows to render. @default 5 */
  rows?: number;
  /** Number of columns. @default 4 */
  columns?: number;
  className?: string;
}

export function TableSkeleton({
  rows = 5,
  columns = 4,
  className,
}: TableSkeletonProps) {
  return (
    <div className={cn("w-full space-y-2", className)}>
      {/* Header row */}
      <div className="flex gap-4">
        {Array.from({ length: columns }).map((_, col) => (
          <Skeleton key={`head-${col}`} className="h-8 flex-1" />
        ))}
      </div>
      {/* Body rows */}
      {Array.from({ length: rows }).map((_, row) => (
        <div key={`row-${row}`} className="flex gap-4">
          {Array.from({ length: columns }).map((_, col) => (
            <Skeleton key={`cell-${row}-${col}`} className="h-6 flex-1" />
          ))}
        </div>
      ))}
    </div>
  );
}

// ---------------------------------------------------------------------------
// CardSkeleton
// ---------------------------------------------------------------------------

export interface CardSkeletonProps {
  className?: string;
}

export function CardSkeleton({ className }: CardSkeletonProps) {
  return (
    <div
      className={cn(
        "rounded-xl border bg-card p-6 shadow space-y-4",
        className,
      )}
    >
      <div className="flex items-center justify-between">
        <Skeleton className="h-4 w-24" />
        <Skeleton className="h-4 w-4 rounded-full" />
      </div>
      <Skeleton className="h-8 w-20" />
      <Skeleton className="h-3 w-16" />
    </div>
  );
}

// ---------------------------------------------------------------------------
// ChartSkeleton
// ---------------------------------------------------------------------------

export interface ChartSkeletonProps {
  /** Height of the chart placeholder. @default 200 */
  height?: number;
  className?: string;
}

export function ChartSkeleton({ height = 200, className }: ChartSkeletonProps) {
  return (
    <div className={cn("rounded-xl border bg-card p-6 shadow space-y-4", className)}>
      <div className="flex items-center justify-between">
        <Skeleton className="h-4 w-32" />
        <Skeleton className="h-6 w-24 rounded-md" />
      </div>
      <Skeleton className="w-full rounded-md" style={{ height }} />
    </div>
  );
}

// ---------------------------------------------------------------------------
// PageSkeleton
// ---------------------------------------------------------------------------

export interface PageSkeletonProps {
  className?: string;
}

export function PageSkeleton({ className }: PageSkeletonProps) {
  return (
    <div className={cn("space-y-6 p-6", className)}>
      {/* Page header skeleton */}
      <div className="space-y-2">
        <Skeleton className="h-8 w-48" />
        <Skeleton className="h-4 w-72" />
      </div>

      {/* Metric cards row */}
      <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {Array.from({ length: 4 }).map((_, i) => (
          <CardSkeleton key={`metric-${i}`} />
        ))}
      </div>

      {/* Main content area */}
      <div className="grid gap-6 lg:grid-cols-3">
        <ChartSkeleton className="lg:col-span-2" height={260} />
        <div className="space-y-4">
          <CardSkeleton />
          <CardSkeleton />
        </div>
      </div>
    </div>
  );
}

Feedback

Components for communicating system status, connection state, and toast notifications.


src/components/feedback/connection-status.tsx

WebSocket connection indicator that reads status from the websocket-store. Renders a colored dot with a label, and exposes details (connected URL, reconnect attempts) via a tooltip.

import { cn } from "@/lib/utils";
import { useWebSocketStore, type WSStatus } from "@/store/websocket-store";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function dotColor(status: WSStatus): string {
  switch (status) {
    case "connected":
      return "bg-green-500";
    case "connecting":
      return "bg-amber-500 animate-pulse-dot";
    case "disconnected":
      return "bg-red-500";
    case "error":
      return "bg-red-500";
    default:
      return "bg-muted-foreground";
  }
}

function statusLabel(status: WSStatus): string {
  switch (status) {
    case "connected":
      return "Connected";
    case "connecting":
      return "Connecting";
    case "disconnected":
      return "Disconnected";
    case "error":
      return "Error";
    default:
      return "Unknown";
  }
}

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export interface ConnectionStatusProps {
  /** Additional class names for the outer wrapper. */
  className?: string;
}

export function ConnectionStatus({ className }: ConnectionStatusProps) {
  const status = useWebSocketStore((s) => s.status);
  const reconnectAttempts = useWebSocketStore((s) => s.reconnectAttempts);

  const wsUrl = import.meta.env.VITE_WS_URL ?? "ws://localhost:3000/ws";

  return (
    <TooltipProvider delayDuration={200}>
      <Tooltip>
        <TooltipTrigger asChild>
          <div
            className={cn(
              "inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm",
              className,
            )}
          >
            <span className={cn("h-2 w-2 shrink-0 rounded-full", dotColor(status))} />
            <span className="text-xs text-muted-foreground">
              {statusLabel(status)}
            </span>
          </div>
        </TooltipTrigger>
        <TooltipContent side="bottom" className="max-w-xs">
          <div className="space-y-1 text-xs">
            <p>
              <span className="font-medium">Status:</span> {statusLabel(status)}
            </p>
            <p>
              <span className="font-medium">URL:</span>{" "}
              <code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">
                {wsUrl}
              </code>
            </p>
            {reconnectAttempts > 0 && (
              <p>
                <span className="font-medium">Reconnect attempts:</span>{" "}
                {reconnectAttempts}
              </p>
            )}
          </div>
        </TooltipContent>
      </Tooltip>
    </TooltipProvider>
  );
}

src/components/feedback/toast-provider.tsx

Wrapper around Sonner’s Toaster that integrates with the MetaForge ui-store for theme-aware toasts. Also exports convenience helper functions for common toast patterns.

import { Toaster, toast } from "sonner";

import { useUIStore, resolveTheme } from "@/store/ui-store";

// ---------------------------------------------------------------------------
// ToastProvider Component
// ---------------------------------------------------------------------------

export function ToastProvider() {
  const theme = useUIStore((s) => s.theme);
  const resolved = resolveTheme(theme);

  return (
    <Toaster
      theme={resolved}
      position="bottom-right"
      toastOptions={{
        classNames: {
          toast:
            "group toast bg-background text-foreground border-border shadow-lg",
          title: "text-foreground font-medium",
          description: "text-muted-foreground",
          actionButton: "bg-primary text-primary-foreground",
          cancelButton: "bg-muted text-muted-foreground",
        },
      }}
      closeButton
      richColors
    />
  );
}

// ---------------------------------------------------------------------------
// Helper functions
// ---------------------------------------------------------------------------

/**
 * Show a success toast.
 *
 * @example
 * showSuccess("Session created", "Session #42 is now running.");
 */
export function showSuccess(title: string, description?: string) {
  toast.success(title, { description });
}

/**
 * Show an error toast.
 *
 * @example
 * showError("Connection lost", "Attempting to reconnect...");
 */
export function showError(title: string, description?: string) {
  toast.error(title, { description });
}

/**
 * Show an informational toast.
 *
 * @example
 * showInfo("BOM updated", "3 components were refreshed from the supplier API.");
 */
export function showInfo(title: string, description?: string) {
  toast.info(title, { description });
}

/**
 * Show a warning toast.
 *
 * @example
 * showWarning("High risk detected", "Component U5 has a single-source supplier.");
 */
export function showWarning(title: string, description?: string) {
  toast.warning(title, { description });
}

Usage Example

import { ToastProvider, showSuccess, showError } from "@/components/feedback/toast-provider";

// In your root layout:
function RootLayout() {
  return (
    <>
      {/* ... layout content ... */}
      <ToastProvider />
    </>
  );
}

// Anywhere in the app:
async function handleApprove(id: string) {
  try {
    await approveItem(id);
    showSuccess("Approved", `Item ${id} has been approved.`);
  } catch {
    showError("Approval failed", "Please try again.");
  }
}