Multi-page dashboard One page cannot hold it and the sections have different readers. Each page needs its own answer to is-it-okay at the top, because a reader who lands on page three never saw page one. npx shadcn@latest add tabs button dropdown-menu npm i lucide-react Tokens this needs: --card, --card-foreground, --muted, --muted-foreground, --border, --status-nominal, --status-warn, --status-critical The status, chart, scale, state and direction names are an extension, not a rename. shadcn has --destructive and five --chart-* and nothing else in this territory. ──────────────────────────────────────────────────────────────────────── // MultiPageDashboard.tsx ──────────────────────────────────────────────────────────────────────── import type { ReactNode } from "react"; import { ChevronDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; /** Closed. Every page answers is-it-okay in one of three words, so the * overview can put the answers side by side. */ export type Status = "nominal" | "warn" | "critical"; const STATUS = { nominal: "text-status-nominal", warn: "text-status-warn", critical: "text-status-critical" }; /** What every page receives. A page cannot hold its own range, which is how * two pages end up comparing different windows without anyone noticing. */ export type Shared = { range: string; scope: string }; export type Page = { id: string; title: string; status: Status; /** One line on whether it is okay. Printed at the top of the page and on * its overview tile, so a reader landing here never needs page one. */ headline: string; body?: (shared: Shared) => ReactNode; }; export const RANGES = ["1h", "6h", "24h", "7d"] as const; export function MultiPageDashboard({ subject, pages, shared, onShared, onScope, page, onNavigate }: { subject: string; /** Peers at one scope, differing in what they show. Levels of one subject * narrowing are a different pattern. */ pages: Page[]; shared: Shared; onShared: (next: Shared) => void; /** Opens the app's scope picker. */ onScope: () => void; /** "overview" or a page id. Lift it into the URL with the shared state, so * an alert can link to the right page with the scope intact. */ page: string; onNavigate: (id: string) => void; }) { const current = pages.find((p) => p.id === page); return (
{subject} · {shared.scope}
{current.status} · {current.headline}
{label}
{value}