Phase 1: Dashboard Polish + Theming (#457) (#458)
All checks were successful
ci/woodpecker/push/orchestrator Pipeline was successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #458.
This commit is contained in:
2026-02-23 00:16:45 +00:00
committed by jason.woltje
parent 7c55464d54
commit 07f5225a76
17 changed files with 159 additions and 506 deletions

View File

@@ -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 => (
<div data-testid="recent-tasks">
{isLoading ? "Loading tasks" : `${String(tasks.length)} tasks`}
</div>
// Mock Phase 3 dashboard widgets
vi.mock("@/components/dashboard/DashboardMetrics", () => ({
DashboardMetrics: (): React.JSX.Element => (
<div data-testid="dashboard-metrics">Dashboard Metrics</div>
),
}));
vi.mock("@/components/dashboard/UpcomingEventsWidget", () => ({
UpcomingEventsWidget: ({
events,
isLoading,
}: {
events: unknown[];
isLoading: boolean;
}): React.JSX.Element => (
<div data-testid="upcoming-events">
{isLoading ? "Loading events" : `${String(events.length)} events`}
</div>
vi.mock("@/components/dashboard/OrchestratorSessions", () => ({
OrchestratorSessions: (): React.JSX.Element => (
<div data-testid="orchestrator-sessions">Orchestrator Sessions</div>
),
}));
vi.mock("@/components/dashboard/QuickCaptureWidget", () => ({
QuickCaptureWidget: (): React.JSX.Element => <div data-testid="quick-capture">Quick Capture</div>,
vi.mock("@/components/dashboard/QuickActions", () => ({
QuickActions: (): React.JSX.Element => <div data-testid="quick-actions">Quick Actions</div>,
}));
vi.mock("@/components/dashboard/DomainOverviewWidget", () => ({
DomainOverviewWidget: ({
tasks,
isLoading,
}: {
tasks: unknown[];
isLoading: boolean;
}): React.JSX.Element => (
<div data-testid="domain-overview">
{isLoading ? "Loading overview" : `${String(tasks.length)} tasks overview`}
</div>
),
vi.mock("@/components/dashboard/ActivityFeed", () => ({
ActivityFeed: (): React.JSX.Element => <div data-testid="activity-feed">Activity Feed</div>,
}));
vi.mock("@/components/dashboard/TokenBudget", () => ({
TokenBudget: (): React.JSX.Element => <div data-testid="token-budget">Token Budget</div>,
}));
describe("DashboardPage", (): void => {
it("should render the page title", (): void => {
it("should render the DashboardMetrics widget", (): void => {
render(<DashboardPage />);
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(<DashboardPage />);
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<void> => {
it("should render the QuickActions widget", (): void => {
render(<DashboardPage />);
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(<DashboardPage />);
const main = container.querySelector("main");
expect(main).toBeInTheDocument();
it("should render the ActivityFeed widget", (): void => {
render(<DashboardPage />);
expect(screen.getByTestId("activity-feed")).toBeInTheDocument();
});
it("should render the welcome subtitle", (): void => {
it("should render the TokenBudget widget", (): void => {
render(<DashboardPage />);
expect(screen.getByText(/Welcome back/)).toBeInTheDocument();
expect(screen.getByTestId("token-budget")).toBeInTheDocument();
});
});

View File

@@ -11,13 +11,7 @@ export default function DashboardPage(): ReactElement {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<DashboardMetrics />
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 320px",
gap: 16,
}}
>
<div className="dash-grid">
<div style={{ display: "flex", flexDirection: "column", gap: 16, minWidth: 0 }}>
<OrchestratorSessions />
<QuickActions />

View File

@@ -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
----------------------------------------------------------------------------- */