Utility Libraries

Core utility functions, application constants, color maps, and formatting helpers used throughout the dashboard.

Table of contents

  1. src/lib/utils.ts
  2. src/lib/constants.ts
  3. src/lib/format.ts

src/lib/utils.ts

Tailwind CSS class merging utility powered by clsx and tailwind-merge. This is the standard cn() helper used by shadcn/ui components.

import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

/**
 * Merge Tailwind CSS classes with conflict resolution.
 *
 * Accepts any combination of strings, arrays, objects, and conditional
 * expressions supported by `clsx`, then deduplicates and resolves
 * conflicting Tailwind utilities via `tailwind-merge`.
 *
 * @example
 * cn('px-4 py-2', isActive && 'bg-primary text-primary-foreground')
 * cn('text-sm', className) // allows consumer overrides
 */
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

src/lib/constants.ts

Application-wide constants including API configuration, color maps, agent names, gate definitions, and navigation items.

/* ------------------------------------------------------------------ */
/* API Configuration                                                  */
/* ------------------------------------------------------------------ */

/** Base URL for the MetaForge Gateway REST API */
export const API_BASE_URL: string =
  import.meta.env.VITE_API_URL || 'http://localhost:3000';

/** WebSocket URL for real-time events */
export const WS_URL: string =
  import.meta.env.VITE_WS_URL || 'ws://localhost:3000/ws';

/** Whether mock API mode is enabled */
export const MOCK_API_ENABLED: boolean =
  import.meta.env.VITE_MOCK_API === 'true';

/* ------------------------------------------------------------------ */
/* Color Maps                                                         */
/* ------------------------------------------------------------------ */

import type { RiskLevel, LifecycleStatus } from '@/types/bom';
import type { SessionStatus } from '@/types/session';
import type { AgentStatus } from '@/types/agent';
import type { ComplianceMarket } from '@/types/compliance';

/**
 * Tailwind color classes for risk levels.
 * Each entry provides a text class and a background class.
 */
export const RISK_COLORS: Record<RiskLevel, { text: string; bg: string }> = {
  low: { text: 'text-emerald-600', bg: 'bg-emerald-100 dark:bg-emerald-900/30' },
  medium: { text: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/30' },
  high: { text: 'text-orange-600', bg: 'bg-orange-100 dark:bg-orange-900/30' },
  critical: { text: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/30' },
};

/** Tailwind color classes for component lifecycle statuses */
export const LIFECYCLE_COLORS: Record<LifecycleStatus, { text: string; bg: string }> = {
  active: { text: 'text-emerald-600', bg: 'bg-emerald-100 dark:bg-emerald-900/30' },
  nrnd: { text: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/30' },
  eol: { text: 'text-orange-600', bg: 'bg-orange-100 dark:bg-orange-900/30' },
  obsolete: { text: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/30' },
  unknown: { text: 'text-gray-500', bg: 'bg-gray-100 dark:bg-gray-800/30' },
};

/** Tailwind color classes for session statuses */
export const SESSION_STATUS_COLORS: Record<SessionStatus, { text: string; bg: string; dot: string }> = {
  created: { text: 'text-gray-600', bg: 'bg-gray-100 dark:bg-gray-800/30', dot: 'bg-gray-400' },
  running: { text: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/30', dot: 'bg-blue-500' },
  processing: { text: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/30', dot: 'bg-blue-500' },
  pending_approval: { text: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/30', dot: 'bg-amber-500' },
  approved: { text: 'text-emerald-600', bg: 'bg-emerald-100 dark:bg-emerald-900/30', dot: 'bg-emerald-500' },
  applying: { text: 'text-violet-600', bg: 'bg-violet-100 dark:bg-violet-900/30', dot: 'bg-violet-500' },
  completed: { text: 'text-emerald-600', bg: 'bg-emerald-100 dark:bg-emerald-900/30', dot: 'bg-emerald-500' },
  failed: { text: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/30', dot: 'bg-red-500' },
  rejected: { text: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/30', dot: 'bg-red-500' },
};

/** Tailwind color classes for agent statuses */
export const AGENT_STATUS_COLORS: Record<AgentStatus, { text: string; bg: string; dot: string }> = {
  idle: { text: 'text-gray-600', bg: 'bg-gray-100 dark:bg-gray-800/30', dot: 'bg-gray-400' },
  running: { text: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/30', dot: 'bg-blue-500' },
  error: { text: 'text-red-600', bg: 'bg-red-100 dark:bg-red-900/30', dot: 'bg-red-500' },
  disabled: { text: 'text-gray-400', bg: 'bg-gray-50 dark:bg-gray-900/30', dot: 'bg-gray-300' },
};

/** Gate status color mapping */
export const GATE_STATUS_COLORS: Record<string, { text: string; bg: string }> = {
  'not-ready': { text: 'text-gray-600', bg: 'bg-gray-100 dark:bg-gray-800/30' },
  'at-risk': { text: 'text-amber-600', bg: 'bg-amber-100 dark:bg-amber-900/30' },
  ready: { text: 'text-emerald-600', bg: 'bg-emerald-100 dark:bg-emerald-900/30' },
  passed: { text: 'text-blue-600', bg: 'bg-blue-100 dark:bg-blue-900/30' },
};

/** Compliance market brand colors */
export const COMPLIANCE_MARKET_COLORS: Record<ComplianceMarket, { text: string; bg: string; border: string }> = {
  UKCA: {
    text: 'text-indigo-700 dark:text-indigo-400',
    bg: 'bg-indigo-100 dark:bg-indigo-900/30',
    border: 'border-indigo-300 dark:border-indigo-700',
  },
  CE: {
    text: 'text-blue-700 dark:text-blue-400',
    bg: 'bg-blue-100 dark:bg-blue-900/30',
    border: 'border-blue-300 dark:border-blue-700',
  },
  FCC: {
    text: 'text-emerald-700 dark:text-emerald-400',
    bg: 'bg-emerald-100 dark:bg-emerald-900/30',
    border: 'border-emerald-300 dark:border-emerald-700',
  },
};

/* ------------------------------------------------------------------ */
/* Agent Display Names                                                */
/* ------------------------------------------------------------------ */

/**
 * Mapping from agent short codes to human-readable display names.
 * Covers all 18 agents across Phase 1, 2, and 3.
 */
export const AGENT_DISPLAY_NAMES: Record<string, string> = {
  // Phase 1 agents (6-7)
  REQ: 'Requirements Agent',
  ARCH: 'Architecture Agent',
  PWR: 'Power Budget Agent',
  SCH: 'Schematic Agent',
  BOM: 'BOM Agent',
  DFM: 'DFM Agent',
  COMP: 'Compliance Agent',

  // Phase 2 agents (additional)
  IND: 'Industrial Design Agent',
  MECH: 'Mechanical Engineering Agent',
  FW: 'Firmware Agent',
  SIM: 'Simulation Agent',
  PROTO: 'Prototyping Agent',
  TEST: 'Testing Agent',
  MFG: 'Manufacturing Agent',
  COST: 'Cost Engineering Agent',
  SYS: 'Systems Engineering Agent',

  // Phase 3 agents (additional)
  FIELD: 'Field Engineering Agent',
  SAFE: 'Safety Engineering Agent',
};

/* ------------------------------------------------------------------ */
/* Gate Definitions                                                   */
/* ------------------------------------------------------------------ */

import type { GateType } from '@/types/gate';

/** Manufacturing gate metadata */
export const GATE_DEFINITIONS: Record<GateType, { label: string; description: string }> = {
  EVT: {
    label: 'Engineering Validation Test',
    description:
      'Validates that the design meets functional requirements. Focuses on core functionality, power performance, and basic environmental testing.',
  },
  DVT: {
    label: 'Design Validation Test',
    description:
      'Validates that the product meets design specifications under expected conditions. Includes reliability, compliance, and manufacturing readiness.',
  },
  PVT: {
    label: 'Production Validation Test',
    description:
      'Validates the production process and confirms that mass-manufactured units meet quality and performance targets.',
  },
};

/* ------------------------------------------------------------------ */
/* Navigation                                                         */
/* ------------------------------------------------------------------ */

/**
 * Sidebar navigation items.
 * The `icon` field references a Lucide React icon name.
 */
export interface NavItem {
  label: string;
  path: string;
  icon: string;
}

export const NAV_ITEMS: NavItem[] = [
  { label: 'Overview', path: '/', icon: 'LayoutDashboard' },
  { label: 'Sessions', path: '/sessions', icon: 'Play' },
  { label: 'Agents', path: '/agents', icon: 'Bot' },
  { label: 'Digital Twin', path: '/digital-twin', icon: 'Box' },
  { label: 'BOM', path: '/bom', icon: 'ClipboardList' },
  { label: 'Compliance', path: '/compliance', icon: 'ShieldCheck' },
  { label: 'Testing', path: '/testing', icon: 'FlaskConical' },
  { label: 'Supply Chain', path: '/supply-chain', icon: 'Truck' },
  { label: 'Approvals', path: '/approvals', icon: 'CheckSquare' },
  { label: 'Settings', path: '/settings', icon: 'Settings' },
];

/* ------------------------------------------------------------------ */
/* Misc Constants                                                     */
/* ------------------------------------------------------------------ */

/** Default pagination page size */
export const DEFAULT_PAGE_SIZE = 25;

/** WebSocket reconnection parameters */
export const WS_RECONNECT = {
  initialDelay: 1_000,
  maxDelay: 30_000,
  backoffMultiplier: 2,
} as const;

/** Lead time threshold (weeks) above which a component is flagged */
export const LONG_LEAD_THRESHOLD_WEEKS = 12;

/** RPN threshold above which an FMEA entry is flagged as high priority */
export const HIGH_RPN_THRESHOLD = 200;

src/lib/format.ts

Formatting utility functions for dates, currency, file sizes, percentages, and domain-specific values.

import { formatDistanceToNow, format, parseISO } from 'date-fns';

/* ------------------------------------------------------------------ */
/* Date & Time                                                        */
/* ------------------------------------------------------------------ */

/**
 * Format an ISO date string into a human-readable date.
 *
 * @example formatDate('2025-03-15T10:30:00Z') // "Mar 15, 2025, 10:30 AM"
 */
export function formatDate(date: string): string {
  try {
    return format(parseISO(date), 'MMM d, yyyy, h:mm a');
  } catch {
    return date;
  }
}

/**
 * Format an ISO date string as a relative time string.
 *
 * @example formatRelativeTime('2025-03-15T10:30:00Z') // "2 hours ago"
 */
export function formatRelativeTime(date: string): string {
  try {
    return formatDistanceToNow(parseISO(date), { addSuffix: true });
  } catch {
    return date;
  }
}

/**
 * Format an ISO date string into a short date (no time).
 *
 * @example formatShortDate('2025-03-15T10:30:00Z') // "Mar 15, 2025"
 */
export function formatShortDate(date: string): string {
  try {
    return format(parseISO(date), 'MMM d, yyyy');
  } catch {
    return date;
  }
}

/* ------------------------------------------------------------------ */
/* Currency                                                           */
/* ------------------------------------------------------------------ */

/**
 * Format a number as a USD currency string.
 *
 * @example formatCurrency(1234.5) // "$1,234.50"
 */
export function formatCurrency(amount: number): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(amount);
}

/* ------------------------------------------------------------------ */
/* File Size                                                          */
/* ------------------------------------------------------------------ */

const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB'] as const;

/**
 * Format a byte count into a human-readable file size string.
 *
 * @example formatFileSize(1_258_291) // "1.2 MB"
 */
export function formatFileSize(bytes: number): string {
  if (bytes === 0) return '0 B';

  const exponent = Math.min(
    Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024)),
    FILE_SIZE_UNITS.length - 1,
  );
  const value = bytes / Math.pow(1024, exponent);
  const unit = FILE_SIZE_UNITS[exponent];

  return `${value.toFixed(exponent === 0 ? 0 : 1)} ${unit}`;
}

/* ------------------------------------------------------------------ */
/* Percentage                                                         */
/* ------------------------------------------------------------------ */

/**
 * Format a decimal or whole number as a percentage string.
 * Values > 1 are treated as already being percentages.
 *
 * @example formatPercentage(0.853)  // "85.3%"
 * @example formatPercentage(85.3)   // "85.3%"
 */
export function formatPercentage(value: number): string {
  const pct = value > 1 ? value : value * 100;
  return `${pct.toFixed(1)}%`;
}

/* ------------------------------------------------------------------ */
/* Lead Time                                                          */
/* ------------------------------------------------------------------ */

/**
 * Format a lead time value (in weeks) into a human-readable string.
 * Uses months for values >= 8 weeks.
 *
 * @example formatLeadTime(4)   // "4 weeks"
 * @example formatLeadTime(12)  // "3 months"
 * @example formatLeadTime(1)   // "1 week"
 */
export function formatLeadTime(weeks: number): string {
  if (weeks < 1) return 'In stock';
  if (weeks === 1) return '1 week';
  if (weeks < 8) return `${weeks} weeks`;

  const months = Math.round(weeks / 4.33);
  return months === 1 ? '1 month' : `${months} months`;
}

/* ------------------------------------------------------------------ */
/* FMEA                                                               */
/* ------------------------------------------------------------------ */

/**
 * Calculate the Risk Priority Number (RPN) for an FMEA entry.
 * RPN = Severity x Occurrence x Detection
 * Each factor ranges from 1 to 10, so RPN ranges from 1 to 1000.
 *
 * @example formatRPN(8, 4, 6) // 192
 */
export function formatRPN(severity: number, occurrence: number, detection: number): number {
  return severity * occurrence * detection;
}

/* ------------------------------------------------------------------ */
/* String                                                             */
/* ------------------------------------------------------------------ */

/**
 * Truncate a string to a maximum length, appending an ellipsis if truncated.
 *
 * @example truncate('Hello, world!', 8) // "Hello..."
 */
export function truncate(str: string, length: number): string {
  if (str.length <= length) return str;
  return `${str.slice(0, length - 3)}...`;
}

/* ------------------------------------------------------------------ */
/* Numbers                                                            */
/* ------------------------------------------------------------------ */

/**
 * Format a large number with compact notation.
 *
 * @example formatCompactNumber(1_500_000) // "1.5M"
 */
export function formatCompactNumber(value: number): string {
  return new Intl.NumberFormat('en-US', {
    notation: 'compact',
    maximumFractionDigits: 1,
  }).format(value);
}

/**
 * Format a number with thousand separators.
 *
 * @example formatNumber(12345) // "12,345"
 */
export function formatNumber(value: number): string {
  return new Intl.NumberFormat('en-US').format(value);
}

/* ------------------------------------------------------------------ */
/* Status Labels                                                      */
/* ------------------------------------------------------------------ */

/**
 * Convert a snake_case or kebab-case status string into a Title Case label.
 *
 * @example formatStatusLabel('pending_approval') // "Pending Approval"
 * @example formatStatusLabel('not-ready')        // "Not Ready"
 */
export function formatStatusLabel(status: string): string {
  return status
    .replace(/[_-]/g, ' ')
    .replace(/\b\w/g, (char) => char.toUpperCase());
}