diff --git a/apps/web/src/app/(authenticated)/page.test.tsx b/apps/web/src/app/(authenticated)/page.test.tsx index 4702f0d..98da381 100644 --- a/apps/web/src/app/(authenticated)/page.test.tsx +++ b/apps/web/src/app/(authenticated)/page.test.tsx @@ -1,85 +1,55 @@ import { describe, it, expect, vi } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import DashboardPage from "./page"; -// Mock dashboard widgets -vi.mock("@/components/dashboard/RecentTasksWidget", () => ({ - RecentTasksWidget: ({ - tasks, - isLoading, - }: { - tasks: unknown[]; - isLoading: boolean; - }): React.JSX.Element => ( -
- {isLoading ? "Loading tasks" : `${String(tasks.length)} tasks`} -
+// Mock Phase 3 dashboard widgets +vi.mock("@/components/dashboard/DashboardMetrics", () => ({ + DashboardMetrics: (): React.JSX.Element => ( +
Dashboard Metrics
), })); -vi.mock("@/components/dashboard/UpcomingEventsWidget", () => ({ - UpcomingEventsWidget: ({ - events, - isLoading, - }: { - events: unknown[]; - isLoading: boolean; - }): React.JSX.Element => ( -
- {isLoading ? "Loading events" : `${String(events.length)} events`} -
+vi.mock("@/components/dashboard/OrchestratorSessions", () => ({ + OrchestratorSessions: (): React.JSX.Element => ( +
Orchestrator Sessions
), })); -vi.mock("@/components/dashboard/QuickCaptureWidget", () => ({ - QuickCaptureWidget: (): React.JSX.Element =>
Quick Capture
, +vi.mock("@/components/dashboard/QuickActions", () => ({ + QuickActions: (): React.JSX.Element =>
Quick Actions
, })); -vi.mock("@/components/dashboard/DomainOverviewWidget", () => ({ - DomainOverviewWidget: ({ - tasks, - isLoading, - }: { - tasks: unknown[]; - isLoading: boolean; - }): React.JSX.Element => ( -
- {isLoading ? "Loading overview" : `${String(tasks.length)} tasks overview`} -
- ), +vi.mock("@/components/dashboard/ActivityFeed", () => ({ + ActivityFeed: (): React.JSX.Element =>
Activity Feed
, +})); + +vi.mock("@/components/dashboard/TokenBudget", () => ({ + TokenBudget: (): React.JSX.Element =>
Token Budget
, })); describe("DashboardPage", (): void => { - it("should render the page title", (): void => { + it("should render the DashboardMetrics widget", (): void => { render(); - expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Dashboard"); + expect(screen.getByTestId("dashboard-metrics")).toBeInTheDocument(); }); - it("should show loading state initially", (): void => { + it("should render the OrchestratorSessions widget", (): void => { render(); - expect(screen.getByTestId("recent-tasks")).toHaveTextContent("Loading tasks"); - expect(screen.getByTestId("upcoming-events")).toHaveTextContent("Loading events"); - expect(screen.getByTestId("domain-overview")).toHaveTextContent("Loading overview"); + expect(screen.getByTestId("orchestrator-sessions")).toBeInTheDocument(); }); - it("should render all widgets with data after loading", async (): Promise => { + it("should render the QuickActions widget", (): void => { render(); - await waitFor((): void => { - expect(screen.getByTestId("recent-tasks")).toHaveTextContent("4 tasks"); - expect(screen.getByTestId("upcoming-events")).toHaveTextContent("3 events"); - expect(screen.getByTestId("domain-overview")).toHaveTextContent("4 tasks overview"); - expect(screen.getByTestId("quick-capture")).toBeInTheDocument(); - }); + expect(screen.getByTestId("quick-actions")).toBeInTheDocument(); }); - it("should have proper layout structure", (): void => { - const { container } = render(); - const main = container.querySelector("main"); - expect(main).toBeInTheDocument(); + it("should render the ActivityFeed widget", (): void => { + render(); + expect(screen.getByTestId("activity-feed")).toBeInTheDocument(); }); - it("should render the welcome subtitle", (): void => { + it("should render the TokenBudget widget", (): void => { render(); - expect(screen.getByText(/Welcome back/)).toBeInTheDocument(); + expect(screen.getByTestId("token-budget")).toBeInTheDocument(); }); }); diff --git a/apps/web/src/app/(authenticated)/page.tsx b/apps/web/src/app/(authenticated)/page.tsx index 85ace31..bf09ec1 100644 --- a/apps/web/src/app/(authenticated)/page.tsx +++ b/apps/web/src/app/(authenticated)/page.tsx @@ -11,13 +11,7 @@ export default function DashboardPage(): ReactElement { return (
-
+
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index f19b900..090d5d2 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -765,6 +765,62 @@ body::before { animation: scaleIn 0.1s ease-out; } +/* ----------------------------------------------------------------------------- + Dashboard Layout β€” Responsive Grids + ----------------------------------------------------------------------------- */ +.metrics-strip { + display: grid; + grid-template-columns: repeat(var(--ms-cols, 6), 1fr); + gap: 0; + border-radius: var(--r-lg); + overflow: hidden; + border: 1px solid var(--border); +} + +.metric-cell { + border-left: 1px solid var(--border); +} + +.metric-cell:first-child { + border-left: none; +} + +@media (max-width: 900px) { + .metrics-strip { + grid-template-columns: repeat(3, 1fr); + } + + .metric-cell:nth-child(3n + 1) { + border-left: none; + } +} + +@media (max-width: 640px) { + .metrics-strip { + grid-template-columns: repeat(2, 1fr); + } + + .metric-cell:nth-child(3n + 1) { + border-left: 1px solid var(--border); + } + + .metric-cell:nth-child(2n + 1) { + border-left: none; + } +} + +.dash-grid { + display: grid; + grid-template-columns: 1fr 320px; + gap: 16px; +} + +@media (max-width: 900px) { + .dash-grid { + grid-template-columns: 1fr; + } +} + /* ----------------------------------------------------------------------------- Responsive Typography Adjustments ----------------------------------------------------------------------------- */ diff --git a/apps/web/src/components/dashboard/ActivityFeed.tsx b/apps/web/src/components/dashboard/ActivityFeed.tsx index 97389cb..3a623bf 100644 --- a/apps/web/src/components/dashboard/ActivityFeed.tsx +++ b/apps/web/src/components/dashboard/ActivityFeed.tsx @@ -102,8 +102,8 @@ function ActivityItemRow({ item }: ActivityItemRowProps): ReactElement { style={{ display: "flex", alignItems: "flex-start", - gap: 10, - padding: "8px 0", + gap: 12, + padding: "10px 0", borderBottom: "1px solid var(--border)", }} > diff --git a/apps/web/src/components/dashboard/DomainOverviewWidget.tsx b/apps/web/src/components/dashboard/DomainOverviewWidget.tsx deleted file mode 100644 index 577bf3c..0000000 --- a/apps/web/src/components/dashboard/DomainOverviewWidget.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { Task } from "@mosaic/shared"; -import { TaskStatus, TaskPriority } from "@mosaic/shared"; - -interface DomainOverviewWidgetProps { - tasks: Task[]; - isLoading: boolean; -} - -export function DomainOverviewWidget({ - tasks, - isLoading, -}: DomainOverviewWidgetProps): React.JSX.Element { - if (isLoading) { - return ( -
-
-
- Loading overview... -
-
- ); - } - - const stats = { - total: tasks.length, - inProgress: tasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length, - completed: tasks.filter((t) => t.status === TaskStatus.COMPLETED).length, - highPriority: tasks.filter((t) => t.priority === TaskPriority.HIGH).length, - }; - - const StatCard = ({ - label, - value, - color, - }: { - label: string; - value: number; - color: string; - }): React.JSX.Element => ( -
-
{value}
-
{label}
-
- ); - - return ( -
-

Domain Overview

-
- - - - -
-
- ); -} diff --git a/apps/web/src/components/dashboard/OrchestratorSessions.tsx b/apps/web/src/components/dashboard/OrchestratorSessions.tsx index d496470..c9e9732 100644 --- a/apps/web/src/components/dashboard/OrchestratorSessions.tsx +++ b/apps/web/src/components/dashboard/OrchestratorSessions.tsx @@ -169,7 +169,7 @@ function OrchCard({ session }: OrchCardProps): ReactElement { style={{ background: "var(--bg-mid)", border: "1px solid var(--border)", - borderRadius: "var(--r-md)", + borderRadius: "var(--r)", padding: "12px 14px", marginBottom: 10, }} diff --git a/apps/web/src/components/dashboard/QuickActions.tsx b/apps/web/src/components/dashboard/QuickActions.tsx index 0bf4f08..e688e9a 100644 --- a/apps/web/src/components/dashboard/QuickActions.tsx +++ b/apps/web/src/components/dashboard/QuickActions.tsx @@ -12,10 +12,10 @@ interface QuickAction { } const actions: QuickAction[] = [ - { id: "new-project", label: "New Project", icon: "πŸš€", iconBg: "rgba(47,128,255,0.15)" }, - { id: "spawn-agent", label: "Spawn Agent", icon: "πŸ€–", iconBg: "rgba(139,92,246,0.15)" }, - { id: "view-telemetry", label: "View Telemetry", icon: "πŸ“Š", iconBg: "rgba(20,184,166,0.15)" }, - { id: "review-tasks", label: "Review Tasks", icon: "πŸ“‹", iconBg: "rgba(245,158,11,0.15)" }, + { id: "new-project", label: "New Project", icon: "πŸš€", iconBg: "rgba(47,128,255,0.12)" }, + { id: "spawn-agent", label: "Spawn Agent", icon: "πŸ€–", iconBg: "rgba(139,92,246,0.12)" }, + { id: "view-telemetry", label: "View Telemetry", icon: "πŸ“Š", iconBg: "rgba(20,184,166,0.12)" }, + { id: "review-tasks", label: "Review Tasks", icon: "πŸ“‹", iconBg: "rgba(245,158,11,0.12)" }, ]; interface ActionButtonProps { @@ -36,24 +36,25 @@ function ActionButton({ action }: ActionButtonProps): ReactElement { }} style={{ display: "flex", - flexDirection: "column", alignItems: "center", - justifyContent: "center", gap: 8, - padding: "16px 12px", - borderRadius: "var(--r-md)", + padding: "10px 12px", + borderRadius: "var(--r)", border: `1px solid ${hovered ? "var(--ms-border-700)" : "var(--border)"}`, background: hovered ? "var(--surface)" : "var(--bg-mid)", cursor: "pointer", - transition: "border-color 0.15s, background 0.15s", + transition: "border-color 0.15s, background 0.15s, color 0.15s", width: "100%", + fontSize: "0.8rem", + fontWeight: 600, + color: hovered ? "var(--text)" : "var(--text-2)", }} >
{action.icon}
- - {action.label} - + {action.label} ); } @@ -84,7 +77,7 @@ export function QuickActions(): ReactElement { style={{ display: "grid", gridTemplateColumns: "1fr 1fr", - gap: 10, + gap: 8, }} > {actions.map((action) => ( diff --git a/apps/web/src/components/dashboard/QuickCaptureWidget.tsx b/apps/web/src/components/dashboard/QuickCaptureWidget.tsx deleted file mode 100644 index 96cac82..0000000 --- a/apps/web/src/components/dashboard/QuickCaptureWidget.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "@mosaic/ui"; -import { useRouter } from "next/navigation"; -import { ComingSoon } from "@/components/ui/ComingSoon"; - -/** - * Check if we're in development mode (runtime check for testability) - */ -function isDevelopment(): boolean { - return process.env.NODE_ENV === "development"; -} - -/** - * Internal Quick Capture Widget implementation - */ -function QuickCaptureWidgetInternal(): React.JSX.Element { - const [idea, setIdea] = useState(""); - const router = useRouter(); - - const handleSubmit = (e: React.SyntheticEvent): void => { - e.preventDefault(); - if (!idea.trim()) return; - - // TODO: Implement quick capture API call - // For now, just show a success indicator - console.log("Quick capture:", idea); - setIdea(""); - }; - - const goToTasks = (): void => { - router.push("/tasks"); - }; - - return ( -
-

Quick Capture

-

Quickly jot down ideas or brain dumps

-
-