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.
src/lib/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 {};src/lib/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 {};src/lib/components/patterns/page-header/PageHeader.svelte
<script lang="ts">
import type { Snippet } from 'svelte';
import { Heading } from '$lib/components/typography/heading';
import { Text } from '$lib/components/typography/text';
import { cn } from '$lib/utils/cn';
import styles from './page-header.module.css';
let {
title,
description,
leading,
actions,
meta,
level = 1,
size = 'lg',
class: className = ''
}: {
title: string;
description?: string;
/** Above the title: a Breadcrumb or a SectionLabel. */
leading?: Snippet;
/** Beside the title: the page's primary and secondary actions. */
actions?: Snippet;
/** Under the title: status badges, dates, owners. */
meta?: Snippet;
/** 1 for a page; lower when the header sits inside a larger page. */
level?: 1 | 2 | 3 | 4 | 5 | 6;
size?: 'md' | 'lg';
class?: string;
} = $props();
</script>
<header class={cn(styles.root, className)}>
{@render leading?.()}
<div class={styles.row}>
<div class={styles.copy}>
<Heading {level} {size}>{title}</Heading>
{#if meta}<div class={styles.meta}>{@render meta()}</div>{/if}
{#if description}<Text tone="muted" measure>{description}</Text>{/if}
</div>
{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
</div>
</header>src/lib/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.
src/lib/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 {};src/lib/components/patterns/collection-toolbar/CollectionToolbar.svelte
<script lang="ts">
import type { Snippet } from 'svelte';
import { cn } from '$lib/utils/cn';
import styles from './collection-toolbar.module.css';
/* Filters and search on the left, a summary and actions on the right. A
layout for separately labelled controls, not an ARIA toolbar. */
let {
summary,
actions,
class: className = '',
children
}: {
/** "12 of 48 projects": updated as filters change. */
summary?: string;
/** At the end: create, export, view switches. */
actions?: Snippet;
class?: string;
children?: Snippet;
} = $props();
</script>
<div class={cn(styles.root, className)}>
<div class={styles.controls}>{@render children?.()}</div>
{#if summary || actions}
<div class={styles.trailing}>
{#if summary}<span class={styles.summary} role="status">{summary}</span>{/if}
{@render actions?.()}
</div>
{/if}
</div>src/lib/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.
src/lib/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 {};src/lib/components/patterns/selection-card/SelectionCard.svelte
<script lang="ts">
import type { Snippet } from 'svelte';
import { Checkbox } from '$lib/components/forms/checkbox';
import { Radio } from '$lib/components/forms/radio-group';
import { cn } from '$lib/utils/cn';
import styles from './selection-card.module.css';
type Base = {
label: string;
value: string;
description?: string;
disabled?: boolean;
/** Extra content below the description: a price, a feature list. */
children?: Snippet;
class?: string;
};
type Props = Base &
(
| { /** One of several: place the cards inside a RadioGroup. */ mode: 'single' }
| {
mode: 'multiple';
name?: string;
checked?: 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. */
let props: Props = $props();
const id = $props.id();
const labelId = `${id}-label`;
const descriptionId = $derived(props.description ? `${id}-description` : undefined);
</script>
<div class={cn(styles.root, props.class)}>
<label id={labelId} for={id} class={styles.label}>{props.label}</label>
<span class={styles.control}>
{#if props.mode === 'single'}
<Radio
{id}
value={props.value}
disabled={props.disabled}
aria-labelledby={labelId}
aria-describedby={descriptionId}
/>
{:else}
<Checkbox
{id}
value={props.value}
name={props.name}
disabled={props.disabled}
checked={props.checked}
onCheckedChange={props.onCheckedChange}
aria-labelledby={labelId}
aria-describedby={descriptionId}
/>
{/if}
</span>
{#if props.description}
<p id={descriptionId} class={styles.description}>{props.description}</p>
{/if}
{#if props.children}<div class={styles.body}>{@render props.children()}</div>{/if}
</div>src/lib/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.
src/lib/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 {};src/lib/components/patterns/settings-section/SettingsSection.svelte
<script lang="ts">
import type { Snippet } from 'svelte';
import { Card, CardBody, CardFooter } from '$lib/components/display/card';
import { Heading } from '$lib/components/typography/heading';
import { Text } from '$lib/components/typography/text';
import { cn } from '$lib/utils/cn';
import styles from './settings-section.module.css';
/* One group of settings: what it is on the left, the controls on a card on
the right; stacked when narrow. */
let {
title,
description,
footer,
level = 2,
tone = 'default',
class: className = '',
children
}: {
title: string;
description?: string;
/** The card's footer: usually Save, or the destructive action. */
footer?: Snippet;
level?: 1 | 2 | 3 | 4 | 5 | 6;
/** "danger" frames irreversible actions in the critical tone. */
tone?: 'default' | 'danger';
class?: string;
children: Snippet;
} = $props();
</script>
<section class={cn(styles.root, tone === 'danger' && styles.danger, className)}>
<div class={styles.grid}>
<div class={styles.copy}>
<Heading {level} size="sm" class={styles.title}>{title}</Heading>
{#if description}<Text size="sm" tone="muted">{description}</Text>{/if}
</div>
<Card as="div" class={styles.card}>
<CardBody>{@render children()}</CardBody>
{#if footer}<CardFooter>{@render footer()}</CardFooter>{/if}
</Card>
</div>
</section>src/lib/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
Ada Lovelace
OwnerAlan Turing
MemberGrace Hopper
MemberKatherine Johnson
Member26 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
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