diff --git a/apps/web/src/components/terminal/TerminalPanel.tsx b/apps/web/src/components/terminal/TerminalPanel.tsx
new file mode 100644
index 0000000..3f0ac80
--- /dev/null
+++ b/apps/web/src/components/terminal/TerminalPanel.tsx
@@ -0,0 +1,257 @@
+import type { ReactElement, CSSProperties } from "react";
+
+export interface TerminalLine {
+ type: "prompt" | "command" | "output" | "error" | "warning" | "success";
+ content: string;
+}
+
+export interface TerminalTab {
+ id: string;
+ label: string;
+}
+
+export interface TerminalPanelProps {
+ open: boolean;
+ onClose: () => void;
+ tabs?: TerminalTab[];
+ activeTab?: string;
+ onTabChange?: (id: string) => void;
+ lines?: TerminalLine[];
+ className?: string;
+}
+
+const defaultTabs: TerminalTab[] = [
+ { id: "main", label: "main" },
+ { id: "build", label: "build" },
+ { id: "logs", label: "logs" },
+];
+
+const blinkKeyframes = `
+@keyframes ms-terminal-blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+`;
+
+let blinkStyleInjected = false;
+
+function ensureBlinkStyle(): void {
+ if (blinkStyleInjected || typeof document === "undefined") return;
+ const styleEl = document.createElement("style");
+ styleEl.textContent = blinkKeyframes;
+ document.head.appendChild(styleEl);
+ blinkStyleInjected = true;
+}
+
+function getLineColor(type: TerminalLine["type"]): string {
+ switch (type) {
+ case "prompt":
+ return "var(--success)";
+ case "command":
+ return "var(--text-2)";
+ case "output":
+ return "var(--muted)";
+ case "error":
+ return "var(--danger)";
+ case "warning":
+ return "var(--warn)";
+ case "success":
+ return "var(--success)";
+ default:
+ return "var(--muted)";
+ }
+}
+
+export function TerminalPanel({
+ open,
+ onClose,
+ tabs,
+ activeTab,
+ onTabChange,
+ lines = [],
+ className = "",
+}: TerminalPanelProps): ReactElement {
+ ensureBlinkStyle();
+
+ const resolvedTabs = tabs ?? defaultTabs;
+ const resolvedActiveTab = activeTab ?? resolvedTabs[0]?.id ?? "";
+
+ const panelStyle: CSSProperties = {
+ background: "var(--bg-deep)",
+ borderTop: "1px solid var(--border)",
+ overflow: "hidden",
+ flexShrink: 0,
+ display: "flex",
+ flexDirection: "column",
+ height: open ? 280 : 0,
+ transition: "height 0.3s ease",
+ };
+
+ const headerStyle: CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ gap: 10,
+ padding: "6px 16px",
+ borderBottom: "1px solid var(--border)",
+ flexShrink: 0,
+ };
+
+ const tabBarStyle: CSSProperties = {
+ display: "flex",
+ gap: 2,
+ };
+
+ const actionsStyle: CSSProperties = {
+ marginLeft: "auto",
+ display: "flex",
+ gap: 4,
+ };
+
+ const bodyStyle: CSSProperties = {
+ flex: 1,
+ overflowY: "auto",
+ padding: "10px 16px",
+ fontFamily: "var(--mono)",
+ fontSize: "0.78rem",
+ lineHeight: 1.6,
+ };
+
+ const cursorStyle: CSSProperties = {
+ display: "inline-block",
+ width: 7,
+ height: 14,
+ background: "var(--success)",
+ marginLeft: 2,
+ animation: "ms-terminal-blink 1s step-end infinite",
+ verticalAlign: "text-bottom",
+ };
+
+ return (
+
+ {/* Header */}
+
+ {/* Tab bar */}
+
+ {resolvedTabs.map((tab) => {
+ const isActive = tab.id === resolvedActiveTab;
+ const tabStyle: CSSProperties = {
+ padding: "3px 10px",
+ borderRadius: 4,
+ fontSize: "0.75rem",
+ fontFamily: "var(--mono)",
+ color: isActive ? "var(--success)" : "var(--muted)",
+ cursor: "pointer",
+ background: isActive ? "var(--surface)" : "transparent",
+ border: "none",
+ outline: "none",
+ };
+
+ return (
+
+ );
+ })}
+
+
+ {/* Action buttons */}
+
+
+
+
+
+ {/* Body */}
+
+ {lines.map((line, index) => {
+ const isLast = index === lines.length - 1;
+ const lineStyle: CSSProperties = {
+ display: "flex",
+ gap: 8,
+ };
+ const contentStyle: CSSProperties = {
+ color: getLineColor(line.type),
+ };
+
+ return (
+
+
+ {line.content}
+ {isLast && }
+
+
+ );
+ })}
+
+ {/* Show cursor even when no lines */}
+ {lines.length === 0 && (
+
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/components/terminal/index.ts b/apps/web/src/components/terminal/index.ts
new file mode 100644
index 0000000..cd385a0
--- /dev/null
+++ b/apps/web/src/components/terminal/index.ts
@@ -0,0 +1,2 @@
+export type { TerminalLine, TerminalTab, TerminalPanelProps } from "./TerminalPanel";
+export { TerminalPanel } from "./TerminalPanel";
diff --git a/packages/ui/src/components/DataTable.tsx b/packages/ui/src/components/DataTable.tsx
new file mode 100644
index 0000000..dcecefa
--- /dev/null
+++ b/packages/ui/src/components/DataTable.tsx
@@ -0,0 +1,102 @@
+import { useState, type ReactElement, type ReactNode } from "react";
+
+export interface DataTableColumn {
+ key: string;
+ header: string;
+ render?: (row: T) => ReactNode;
+ className?: string;
+}
+
+export interface DataTableProps> {
+ columns: DataTableColumn[];
+ data: T[];
+ className?: string;
+ onRowClick?: (row: T) => void;
+}
+
+export function DataTable>({
+ columns,
+ data,
+ className = "",
+ onRowClick,
+}: DataTableProps): ReactElement {
+ const [hoveredRow, setHoveredRow] = useState(null);
+
+ return (
+
+
+
+ {columns.map((col) => (
+ |
+ {col.header}
+ |
+ ))}
+
+
+
+ {data.map((row, rowIndex) => {
+ const isLast = rowIndex === data.length - 1;
+ const isHovered = hoveredRow === rowIndex;
+
+ return (
+ {
+ onRowClick(row);
+ }
+ : undefined
+ }
+ onMouseEnter={(): void => {
+ setHoveredRow(rowIndex);
+ }}
+ onMouseLeave={(): void => {
+ setHoveredRow(null);
+ }}
+ >
+ {columns.map((col) => (
+ |
+ {col.render !== undefined ? col.render(row) : (row[col.key] as ReactNode)}
+ |
+ ))}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/packages/ui/src/components/FilterTabs.tsx b/packages/ui/src/components/FilterTabs.tsx
new file mode 100644
index 0000000..4f98d67
--- /dev/null
+++ b/packages/ui/src/components/FilterTabs.tsx
@@ -0,0 +1,85 @@
+import { useState } from "react";
+import type { ReactElement } from "react";
+
+export interface FilterTab {
+ id: string;
+ label: string;
+}
+
+export interface FilterTabsProps {
+ tabs: FilterTab[];
+ activeTab: string;
+ onTabChange: (id: string) => void;
+ className?: string;
+}
+
+function FilterTabItem({
+ tab,
+ isActive,
+ onClick,
+}: {
+ tab: FilterTab;
+ isActive: boolean;
+ onClick: () => void;
+}): ReactElement {
+ const [hovered, setHovered] = useState(false);
+
+ return (
+
+ );
+}
+
+export function FilterTabs({
+ tabs,
+ activeTab,
+ onTabChange,
+ className = "",
+}: FilterTabsProps): ReactElement {
+ return (
+
+ {tabs.map((tab) => (
+ {
+ onTabChange(tab.id);
+ }}
+ />
+ ))}
+
+ );
+}
diff --git a/packages/ui/src/components/LogLine.tsx b/packages/ui/src/components/LogLine.tsx
new file mode 100644
index 0000000..07a3877
--- /dev/null
+++ b/packages/ui/src/components/LogLine.tsx
@@ -0,0 +1,40 @@
+import type { ReactElement } from "react";
+
+export type LogLevel = "info" | "warn" | "error" | "debug" | "success";
+
+export interface LogLineProps {
+ timestamp: string;
+ level: LogLevel;
+ message: string;
+ className?: string;
+}
+
+const levelColors: Record = {
+ info: "var(--primary-l)",
+ warn: "var(--warn)",
+ error: "var(--danger)",
+ debug: "var(--muted)",
+ success: "var(--success)",
+};
+
+export function LogLine({ timestamp, level, message, className = "" }: LogLineProps): ReactElement {
+ return (
+
+ {timestamp}
+ {level}
+ {message}
+
+ );
+}
diff --git a/packages/ui/src/components/MetricsStrip.tsx b/packages/ui/src/components/MetricsStrip.tsx
new file mode 100644
index 0000000..5cd84b0
--- /dev/null
+++ b/packages/ui/src/components/MetricsStrip.tsx
@@ -0,0 +1,99 @@
+import { useState } from "react";
+import type { ReactElement } from "react";
+
+export interface MetricCell {
+ label: string;
+ value: string;
+ color: string; // CSS color, e.g., "var(--ms-blue-400)"
+ trend?: {
+ direction: "up" | "down" | "neutral";
+ text: string;
+ };
+}
+
+export interface MetricsStripProps {
+ cells: MetricCell[];
+ className?: string;
+}
+
+function MetricCellItem({ cell, isFirst }: { cell: MetricCell; isFirst: boolean }): ReactElement {
+ const [hovered, setHovered] = useState(false);
+
+ const trendColor =
+ cell.trend?.direction === "up"
+ ? "var(--success)"
+ : cell.trend?.direction === "down"
+ ? "var(--danger)"
+ : "var(--muted)";
+
+ return (
+ {
+ setHovered(true);
+ }}
+ onMouseLeave={(): void => {
+ setHovered(false);
+ }}
+ style={{
+ padding: "14px 16px",
+ background: hovered ? "var(--surface-2)" : "var(--surface)",
+ borderLeft: isFirst ? "none" : "1px solid var(--border)",
+ borderTop: `2px solid ${cell.color}`,
+ transition: "background 0.15s ease",
+ }}
+ >
+
+ {cell.value}
+
+
+ {cell.label}
+
+ {cell.trend && (
+
+ {cell.trend.text}
+
+ )}
+
+ );
+}
+
+export function MetricsStrip({ cells, className = "" }: MetricsStripProps): ReactElement {
+ return (
+
+ {cells.map((cell, index) => (
+
+ ))}
+
+ );
+}
diff --git a/packages/ui/src/components/ProgressBar.tsx b/packages/ui/src/components/ProgressBar.tsx
new file mode 100644
index 0000000..77f5c16
--- /dev/null
+++ b/packages/ui/src/components/ProgressBar.tsx
@@ -0,0 +1,53 @@
+import type { ReactElement } from "react";
+
+export type ProgressBarVariant = "blue" | "teal" | "purple" | "amber";
+
+export interface ProgressBarProps {
+ value: number; // 0-100
+ variant?: ProgressBarVariant;
+ className?: string;
+ label?: string; // screen reader label
+}
+
+const variantColors: Record = {
+ blue: "var(--primary)",
+ teal: "var(--success)",
+ purple: "var(--purple)",
+ amber: "var(--warn)",
+};
+
+export function ProgressBar({
+ value,
+ variant = "blue",
+ className = "",
+ label,
+}: ProgressBarProps): ReactElement {
+ const clampedValue = Math.min(100, Math.max(0, value));
+
+ return (
+
+ );
+}
diff --git a/packages/ui/src/components/SectionHeader.tsx b/packages/ui/src/components/SectionHeader.tsx
new file mode 100644
index 0000000..b34dc4e
--- /dev/null
+++ b/packages/ui/src/components/SectionHeader.tsx
@@ -0,0 +1,62 @@
+import type { ReactElement, ReactNode } from "react";
+
+export interface SectionHeaderProps {
+ title: string;
+ subtitle?: string;
+ actions?: ReactNode;
+ className?: string;
+}
+
+export function SectionHeader({
+ title,
+ subtitle,
+ actions,
+ className = "",
+}: SectionHeaderProps): ReactElement {
+ return (
+
+
+
+ {title}
+
+ {subtitle !== undefined && (
+
+ {subtitle}
+
+ )}
+
+ {actions !== undefined && (
+
+ {actions}
+
+ )}
+
+ );
+}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 51d263e..cea56bf 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -64,3 +64,27 @@ export type {
// Dot
export { Dot } from "./components/Dot.js";
export type { DotProps, DotVariant } from "./components/Dot.js";
+
+// MetricsStrip
+export { MetricsStrip } from "./components/MetricsStrip.js";
+export type { MetricsStripProps, MetricCell } from "./components/MetricsStrip.js";
+
+// ProgressBar
+export { ProgressBar } from "./components/ProgressBar.js";
+export type { ProgressBarProps, ProgressBarVariant } from "./components/ProgressBar.js";
+
+// FilterTabs
+export { FilterTabs } from "./components/FilterTabs.js";
+export type { FilterTabsProps, FilterTab } from "./components/FilterTabs.js";
+
+// SectionHeader
+export { SectionHeader } from "./components/SectionHeader.js";
+export type { SectionHeaderProps } from "./components/SectionHeader.js";
+
+// DataTable
+export { DataTable } from "./components/DataTable.js";
+export type { DataTableColumn, DataTableProps } from "./components/DataTable.js";
+
+// LogLine
+export { LogLine } from "./components/LogLine.js";
+export type { LogLineProps, LogLevel } from "./components/LogLine.js";