Atlas
Margaret Hamilton
109 tasks · updated 11 Aug 2026
Regions every screen has, and the screens they add up to. Patterns own layout and slots; content, state, and permissions stay with the screen.
breadcrumb · title · meta · description · actions
Shared infrastructure and tooling for the autumn release.
Settings
Plan, payment method, and invoices.
components/patterns/doc.ts
/**
* patterns — arrangements that recur across screens.
*
* ┌───────────────────────────────────────────────────────────────────────────┐
* │ § CONTRACT — the oracle. Names no library, contains no code. │
* └───────────────────────────────────────────────────────────────────────────┘
*
* # What belongs here
*
* A pattern arranges primitives into a region every product screen has — a
* page header, a toolbar over a collection, a settings group — and owns only
* its layout and slots. Content, state, routing, and permissions stay with the
* screen.
*
* # Rules for every member
*
* R1 Slots take any content; a pattern never fetches, routes, or decides
* what a user may do.
* R2 Regions wrap rather than overflow when space runs out.
* R3 Headings inside a pattern take an explicit level, so the page outline
* is the screen's decision.
*
* # Recipes
*
* Whole screens (collection, detail, settings, onboarding, overview) are
* shown in the kitchen sink as recipes, not shipped as components: each
* product arranges them differently.
*/
export {};components/patterns/page-header/doc.ts
/**
* PageHeader — the top of a screen: what it is and what you can do.
*
* ┌───────────────────────────────────────────────────────────────────────────┐
* │ § CONTRACT — the oracle. Names no library, contains no code. │
* └───────────────────────────────────────────────────────────────────────────┘
*
* # Shape
*
* title content, REQUIRED
* description? one or two sentences
* leading? above the title: a Breadcrumb or a SectionLabel
* meta? under the title: status badges, dates, owners
* actions? the page's actions
* level? heading level, default 1
* size? "md" | "lg" (default)
*
* # Behaviour
*
* R1 The title is a heading of `level`; a screen has one level-1 header.
* R2 Actions sit at the end of the title row and wrap below it when there is
* no room, never squeezing the title.
* R3 The description is capped at the reading measure.
* R4 Put at most one primary action here; the rest are secondary or quiet.
*/
export {};components/patterns/page-header/page-header.tsx
import type { ComponentPropsWithRef, ReactNode } from "react";
import { Heading, type HeadingProps } from "@/components/typography/heading";
import { Text } from "@/components/typography/text";
import { cn } from "@/lib/utils/cn";
import styles from "./page-header.module.css";
export type PageHeaderProps = Omit<ComponentPropsWithRef<"header">, "title"> & {
title: ReactNode;
description?: ReactNode;
/** Above the title: a Breadcrumb or a SectionLabel. */
leading?: ReactNode;
/** Beside the title: the page's primary and secondary actions. */
actions?: ReactNode;
/** Under the title: status badges, dates, owners. */
meta?: ReactNode;
/** 1 for a page; lower when the header sits inside a larger page. */
level?: HeadingProps["level"];
size?: "md" | "lg";
};
export function PageHeader({
title,
description,
leading,
actions,
meta,
level = 1,
size = "lg",
className,
...props
}: PageHeaderProps) {
return (
<header {...props} className={cn(styles.root, className)}>
{leading}
<div className={styles.row}>
<div className={styles.copy}>
<Heading level={level} size={size}>
{title}
</Heading>
{meta ? <div className={styles.meta}>{meta}</div> : null}
{description ? (
<Text tone="muted" measure>
{description}
</Text>
) : null}
</div>
{actions ? <div className={styles.actions}>{actions}</div> : null}
</div>
</header>
);
}components/patterns/page-header/page-header.module.css
@layer composition {
.root {
display: grid;
min-width: 0;
gap: var(--space-5);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-6) var(--space-8);
}
.copy {
display: grid;
min-width: 0;
flex: 1 1 18rem;
gap: var(--space-3);
}
.meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3) var(--space-5);
color: var(--ink-3);
font-size: var(--text-12);
}
.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
}
}filters · live summary · actions
The summary is announced. It is a status region, so “12 of 24 projects” is read out when a filter changes it.
components/patterns/collection-toolbar/doc.ts
/**
* CollectionToolbar — the controls above a list, grid, or table.
*
* ┌───────────────────────────────────────────────────────────────────────────┐
* │ § CONTRACT — the oracle. Names no library, contains no code. │
* └───────────────────────────────────────────────────────────────────────────┘
*
* # Shape
*
* children the filters and search, each separately labelled
* summary? "12 of 48 projects"
* actions? create, export, view switches
*
* # Behaviour
*
* R1 Filters on the left, summary and actions on the right; each group wraps
* on its own as space shrinks.
* R2 The summary is a status region, so a change in the count after
* filtering is announced.
* R3 It is a layout, not an ARIA toolbar: every control keeps its own tab
* stop and label, because filters are unrelated controls, not one widget.
*/
export {};components/patterns/collection-toolbar/collection-toolbar.tsx
import type { ComponentPropsWithRef, ReactNode } from "react";
import { cn } from "@/lib/utils/cn";
import styles from "./collection-toolbar.module.css";
export type CollectionToolbarProps = ComponentPropsWithRef<"div"> & {
/** "12 of 48 projects": updated as filters change. */
summary?: ReactNode;
/** At the end: create, export, view switches. */
actions?: ReactNode;
};
/** Filters and search on the left, a summary and actions on the right. A
* layout for separately labelled controls, not an ARIA toolbar: each control
* keeps its own tab stop and label. */
export function CollectionToolbar({
summary,
actions,
className,
children,
...props
}: CollectionToolbarProps) {
return (
<div {...props} className={cn(styles.root, className)}>
<div className={styles.controls}>{children}</div>
{summary || actions ? (
<div className={styles.trailing}>
{summary ? (
<span className={styles.summary} role="status">
{summary}
</span>
) : null}
{actions}
</div>
) : null}
</div>
);
}components/patterns/collection-toolbar/collection-toolbar.module.css
@layer composition {
.root {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-5) var(--space-6);
}
.controls {
display: flex;
min-width: 0;
flex: 1 1 18rem;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
}
.trailing {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-4);
}
.summary {
color: var(--ink-3);
font-size: var(--text-12);
font-variant-numeric: tabular-nums;
}
}a real radio or checkbox, the size of a card
For trying Bento with a small team.
$0 / month
Up to 50 seats and 100 GB.
$249 / month
Talk to sales.
Custom
Chosen plan: team
Link commits and pull requests.
Post updates to a channel.
Sync issues both ways.
The whole card is the target. It is still a radio in a RadioGroup, so arrow keys move between plans and the choice submits with a form.
components/patterns/selection-card/doc.ts
/**
* SelectionCard — a choice presented as a card.
*
* ┌───────────────────────────────────────────────────────────────────────────┐
* │ § CONTRACT — the oracle. Names no library, contains no code. │
* └───────────────────────────────────────────────────────────────────────────┘
*
* # Shape
*
* label, value REQUIRED
* description?, children? (price, features), disabled?
* mode "single" a radio; place the cards in a RadioGroup
* mode "multiple" a checkbox; name?, checked?, defaultChecked?,
* onCheckedChange?
*
* # Behaviour
*
* R1 It is a real radio or checkbox, labelled by the card's label and
* described by its description, so it submits and is announced like one.
* R2 A click anywhere on the card chooses it.
* R3 A chosen card takes the accent border and tint; keyboard focus draws the
* ring around the whole card rather than the small control.
* R4 In a RadioGroup, arrow keys move between cards, as between radios.
*/
export {};components/patterns/selection-card/selection-card.tsx
"use client";
import { useId, type ReactNode } from "react";
import { Checkbox } from "@/components/forms/checkbox";
import { Radio } from "@/components/forms/radio-group";
import { cn } from "@/lib/utils/cn";
import styles from "./selection-card.module.css";
type BaseProps = {
label: string;
value: string;
description?: ReactNode;
disabled?: boolean;
/** Extra content below the description: a price, a feature list. */
children?: ReactNode;
className?: string;
};
export type SelectionCardProps = BaseProps &
(
| {
/** One of several: place the cards inside a RadioGroup. */
mode: "single";
}
| {
mode: "multiple";
name?: string;
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
}
);
/** A choice presented as a card: the whole card is the target, and it is a
* real radio or checkbox underneath. */
export function SelectionCard(props: SelectionCardProps) {
const { label, value, description, disabled, children, className } = props;
const id = useId();
const labelId = `${id}-label`;
const descriptionId = description ? `${id}-description` : undefined;
const control = {
id,
value,
disabled,
"aria-labelledby": labelId,
"aria-describedby": descriptionId,
};
return (
<div className={cn(styles.root, className)}>
<label id={labelId} htmlFor={id} className={styles.label}>
{label}
</label>
<span className={styles.control}>
{props.mode === "single" ? (
<Radio {...control} />
) : (
<Checkbox
{...control}
name={props.name}
checked={props.checked}
defaultChecked={props.defaultChecked}
onCheckedChange={
props.onCheckedChange
? (checked) => props.onCheckedChange?.(checked === true)
: undefined
}
/>
)}
</span>
{description ? (
<p id={descriptionId} className={styles.description}>
{description}
</p>
) : null}
{children ? <div className={styles.body}>{children}</div> : null}
</div>
);
}components/patterns/selection-card/selection-card.module.css
@layer composition {
.root {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-content: start;
gap: var(--space-3) var(--space-5);
padding: var(--space-7);
border: 1px solid var(--line-strong);
border-radius: var(--radius-3);
background: var(--surface-panel);
transition:
border-color var(--dur-2) var(--ease),
background-color var(--dur-2) var(--ease);
}
.control {
grid-column: 2;
grid-row: 1;
}
.label {
grid-column: 1;
grid-row: 1;
color: var(--ink);
font-size: var(--text-body, var(--text-13));
font-weight: var(--weight-strong);
cursor: pointer;
}
/* The label covers the card, so a click anywhere on it chooses it. */
.label::after {
position: absolute;
inset: 0;
border-radius: inherit;
content: "";
}
.description {
grid-column: 1 / -1;
margin: 0;
color: var(--ink-2);
font-size: var(--text-12);
line-height: var(--leading-body);
}
.body {
grid-column: 1 / -1;
margin-top: var(--space-3);
}
.root:where(:hover:not(:has(:disabled))) {
border-color: var(--accent-line);
}
.root:has([data-state="checked"]) {
border-color: var(--accent);
background: var(--accent-tint);
}
/* The ring moves from the small control to the whole card. */
.root:has(:focus-visible) {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.root:has(:focus-visible) :focus-visible {
outline: none;
}
.root:has(:disabled) {
opacity: 0.45;
}
.root:has(:disabled) .label {
cursor: not-allowed;
}
}label beside controls · stacks when narrow
Shown to everyone in the workspace and in invitations.
components/patterns/settings-section/doc.ts
/**
* SettingsSection — one group of settings.
*
* ┌───────────────────────────────────────────────────────────────────────────┐
* │ § CONTRACT — the oracle. Names no library, contains no code. │
* └───────────────────────────────────────────────────────────────────────────┘
*
* # Shape
*
* title, children REQUIRED
* description?, footer? (Save, or the destructive action)
* level? default 2
* tone? "default" | "danger"
*
* # Behaviour
*
* R1 The title and description sit beside a card of controls (1:2) when the
* section is at least 44rem wide, and above it when narrower. It measures
* its own width, not the window's.
* R2 The footer holds the section's commit action, end-aligned.
* R3 `danger` frames irreversible actions: the title in `--crit` and the
* card outlined in `--crit-line`. Its action should confirm first.
*
* ┌───────────────────────────────────────────────────────────────────────────┐
* │ § MECHANICS — NOT the oracle. │
* └───────────────────────────────────────────────────────────────────────────┘
*
* R1 is a container query, so the same section stacks inside a narrow column
* and spreads out on a full-width page.
*/
export {};components/patterns/settings-section/settings-section.tsx
import type { ReactNode } from "react";
import { Card, CardBody, CardFooter } from "@/components/display/card";
import { Heading, type HeadingProps } from "@/components/typography/heading";
import { Text } from "@/components/typography/text";
import { cn } from "@/lib/utils/cn";
import styles from "./settings-section.module.css";
export type SettingsSectionProps = {
title: ReactNode;
description?: ReactNode;
/** The controls. */
children: ReactNode;
/** The card's footer: usually Save, or the destructive action. */
footer?: ReactNode;
level?: HeadingProps["level"];
/** "danger" frames irreversible actions in the critical tone. */
tone?: "default" | "danger";
className?: string;
};
/** One group of settings: what it is on the left, the controls on a card on
* the right; stacked when narrow. */
export function SettingsSection({
title,
description,
children,
footer,
level = 2,
tone = "default",
className,
}: SettingsSectionProps) {
return (
<section
className={cn(styles.root, tone === "danger" && styles.danger, className)}
>
<div className={styles.grid}>
<div className={styles.copy}>
<Heading level={level} size="sm" className={styles.title}>
{title}
</Heading>
{description ? (
<Text size="sm" tone="muted">
{description}
</Text>
) : null}
</div>
<Card as="div" className={styles.card}>
<CardBody>{children}</CardBody>
{footer ? <CardFooter>{footer}</CardFooter> : null}
</Card>
</div>
</section>
);
}components/patterns/settings-section/settings-section.module.css
@layer composition {
/* Adapts to its own width, not the viewport: a settings section in a narrow
column stacks even on a wide screen. */
.root {
container-type: inline-size;
}
.grid {
display: grid;
gap: var(--space-6);
}
@container (min-width: 44rem) {
.grid {
grid-template-columns: minmax(12rem, 1fr) minmax(0, 2fr);
gap: var(--space-9);
}
}
.copy {
display: grid;
align-content: start;
gap: var(--space-3);
}
.danger .title {
color: var(--crit);
}
.danger .card {
border-color: var(--crit-line);
}
}recipe · header, toolbar, card grid, empty result
Everything the workspace is building, grouped by project.
Margaret Hamilton
109 tasks · updated 11 Aug 2026
Donald Knuth
181 tasks · updated Today
Grace Hopper
139 tasks · updated 19 Aug 2026
Donald Knuth
231 tasks · updated 25 Aug 2026
Donald Knuth
5 tasks · updated 26 Aug 2026
Tim Berners-Lee
90 tasks · updated 18 Aug 2026
Barbara Liskov
213 tasks · updated Yesterday
Frances Allen
173 tasks · updated 28 Jul 2026
Ada Lovelace
186 tasks · updated 19 Jul 2026
recipe · breadcrumb, status, stats, tabs
recipe · sections, feedback, danger zone
Changes apply to everyone in Northstar.
How the workspace appears to members and in invitations.
A sentence or two.
Changes apply immediately.
Deletes every project and file for all members. This cannot be undone.
Only the owner can delete a workspace.
Destructive actions confirm. The danger zone is framed in the critical tone and opens an AlertDialog whose focus starts on Cancel.
recipe · progress, plan choice, continue
Step 2 of 3
You can change plans at any time.
3 seats, 1 GB. Free forever.
50 seats, 100 GB, email support.
250 seats, 1 TB, priority support.
recipe · greeting, alert, stats, activity, usage
Here is what changed in Northstar this week.
2 limits nearly reached
| When | Who | What |
|---|---|---|
| 26 Sept, 17:24 UTC | Linus Torvalds | project.archived |
| 26 Sept, 15:56 UTC | Ada Lovelace | member.invited |
| 26 Sept, 13:54 UTC | Linus Torvalds | api_key.created |
| 26 Sept, 12:57 UTC | Katherine Johnson | member.role_changed |
| 26 Sept, 11:26 UTC | Alan Turing | project.archived |
Team plan · renews 1 Oct 2026
Seats
46 / 50
Storage
71.4 / 100
API requests
1,020,000 / 1,000,000