Product
Page
Dashboard page column, centered header, sections, and card entrance animation.
$ npx context-ds add pageUsage
Use it for
Every product page.
Avoid
- Left-aligned page titles
- Bold titles
Example
examples/page.tsx
"use client";import { useState } from "react";import { ExampleNameDialog } from "./demo-feedback";import { PageColumn, PageHeader, PageRise, PageSection } from "@/components/product/page";import { Button } from "@/components/ui/button";import { Card } from "@/components/ui/card";export function Product() { const [open, setOpen] = useState(false); const [monitors, setMonitors] = useState<string[]>([]); return ( <PageColumn className="w-full"> <PageHeader title="Monitors" description="Watch pages and get told when they change." action={<Button variant="brand" size="sm" onClick={() => setOpen(true)}>New monitor</Button>} /> <PageSection title="Active"> <PageRise index={0}> <Card className="text-body-sm text-fg-muted">{3 + monitors.length} monitors checking on schedule.</Card> </PageRise> </PageSection> {monitors.map((name) => <Card key={name}>{name}</Card>)} <ExampleNameDialog title="New monitor" open={open} onOpenChange={setOpen} onSave={(name) => setMonitors((all) => [...all, name])} submitLabel="Create" /> </PageColumn> );}Source
components/product/page.tsx
import * as React from "react";import { cn } from "@/lib/utils";// Every product page's column: 880px at most, balanced between the sidebar and what floats at the right (the support// button) rather than the bare edges, so beside the sidebar it sits 80px left of the middle of its space, or as much// of that as the room allows. Below 768px the sidebar is a drawer and the column simply centers. It is an @container,// so cards inside lay out by the page's width. The header is centered at the top (h4 title at weight 450, one muted// line under it, then the page's main action).function PageColumn({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="page-column" data-page-column className={cn("@container mx-auto flex w-full max-w-(--ds-layout-product-column) flex-col gap-6 px-8 pt-8 pb-10 max-md:px-4 md:ml-[max(0px,calc((100%-var(--ds-layout-product-column))/2-80px))]", className)} {...props} /> );}function PageHeader({ title, description, action, className }: { title: React.ReactNode; description?: React.ReactNode; action?: React.ReactNode; className?: string }) { return ( <header data-slot="page-header" className={cn("flex flex-col items-center gap-3 text-center", className)}> <div> <h1 className="text-h4 leading-title font-[450] tracking-normal text-fg">{title}</h1> {description && <p className="mt-1 text-body-sm text-balance text-fg-muted">{description}</p>} </div> {action} </header> );}/** A titled group of cards on a page: the title in body text, an action (a link or a button) at the right. */function PageSection({ title, action, className, children }: { title?: React.ReactNode; action?: React.ReactNode; className?: string; children: React.ReactNode }) { return ( <section data-slot="page-section" className={cn("flex flex-col gap-3", className)}> {(title || action) && ( <div className="flex items-center justify-between gap-3"> {title && <h2 className="text-body-sm font-normal tracking-normal text-fg">{title}</h2>} {action} </div> )} {children} </section> );}/** * Cards rise in one after another, 50ms apart, on the page's first render (the brand's content-in: a short rise and a * fade, ease-out); cards that mount later, after the page has entered, should render without it. Reduced motion * keeps the fade only. */function PageRise({ index = 0, className, children }: { index?: number; className?: string; children: React.ReactNode }) { return ( <div data-slot="page-rise" className={cn("grid animate-content-in", className)} style={{ animationDelay: `${index * 50}ms`, animationFillMode: "backwards" }}> {children} </div> );}export { PageColumn, PageHeader, PageRise, PageSection };Reference: context-dashboard c0ddf7a src/app/globals.css; src/components/api-keys/api-keys-page.tsx.