Tabs as genres The same data serving different questions, each with its own page. Tabs named after questions beat tabs named after data sources, because the reader arrives with a question rather than a table name. npx shadcn@latest add tabs button dropdown-menu npm i lucide-react Tokens this needs: --card, --card-foreground, --muted, --muted-foreground, --border, --chart-1 ──────────────────────────────────────────────────────────────────────── // GenreTabs.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"; /** What every tab receives. Range and filters live above the strip and * carry across; a tab that kept its own would be a separate dashboard. */ export type Shared = { range: string; filter: string }; export type Genre = { id: string; /** Named from the reader's question, never from a table. */ name: string; /** The question, spelled out. Required: a tab that cannot state the * question it answers is named after a data source. Shown under the * strip so a reader knows what is on a tab before opening it. */ question: string; /** This tab's one-line answer right now. The overview is built from * these, which is what makes it a router rather than a leftovers drawer. */ answer: string; view: (shared: Shared) => ReactNode; }; /** With Overview, four to six tabs. Nine is a menu, and a menu is a sidebar. */ export type Genres = [Genre, Genre, Genre] | [Genre, Genre, Genre, Genre] | [Genre, Genre, Genre, Genre, Genre]; export const RANGES = ["24h", "7d", "28d", "90d"] as const; export function GenreTabs({ genres, topline, shared, onShared, onFilter, tab, onTab }: { genres: Genres; /** The top question's answer, for the reader who wants one number. */ topline: ReactNode; shared: Shared; onShared: (next: Shared) => void; onFilter: () => void; /** "overview" or a genre id. Put it in the URL so a link opens the right tab. */ tab: string; onTab: (id: string) => void; }) { const current = genres.find((g) => g.id === tab); return (
{shared.filter}
{current.question}
{current.answer}
{label}
{value}