altitude-design 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/altitude.mjs +120 -20
- package/dist/guidance/README.md +4 -0
- package/dist/guidance/agent-setup.md +3 -2
- package/dist/guidance/components/AILabel/README.md +60 -0
- package/dist/guidance/components/Accordion/README.md +32 -0
- package/dist/guidance/components/AgentFleet/README.md +81 -0
- package/dist/guidance/components/AgentLanes/README.md +53 -0
- package/dist/guidance/components/AgentRequests/README.md +76 -0
- package/dist/guidance/components/Alert/README.md +71 -0
- package/dist/guidance/components/AltitudeProvider/README.md +111 -0
- package/dist/guidance/components/AppShell/README.md +121 -0
- package/dist/guidance/components/AreaChart/README.md +101 -0
- package/dist/guidance/components/Artifact/README.md +99 -0
- package/dist/guidance/components/Avatar/README.md +75 -0
- package/dist/guidance/components/Await/README.md +128 -0
- package/dist/guidance/components/Badge/README.md +66 -0
- package/dist/guidance/components/BarChart/README.md +109 -0
- package/dist/guidance/components/BarList/README.md +87 -0
- package/dist/guidance/components/Board/README.md +98 -0
- package/dist/guidance/components/Breadcrumb/README.md +66 -0
- package/dist/guidance/components/Button/README.md +58 -0
- package/dist/guidance/components/Card/README.md +52 -0
- package/dist/guidance/components/ChartBrush/README.md +98 -0
- package/dist/guidance/components/ChartContainer/README.md +229 -0
- package/dist/guidance/components/ChartLegend/README.md +78 -0
- package/dist/guidance/components/ChartReference/README.md +115 -0
- package/dist/guidance/components/ChartTooltip/README.md +101 -0
- package/dist/guidance/components/Checkbox/README.md +46 -0
- package/dist/guidance/components/Checklist/README.md +137 -0
- package/dist/guidance/components/CodeBlock/README.md +72 -0
- package/dist/guidance/components/ColorPicker/README.md +69 -0
- package/dist/guidance/components/Combobox/README.md +51 -0
- package/dist/guidance/components/CommandPalette/README.md +204 -0
- package/dist/guidance/components/CommentThread/README.md +81 -0
- package/dist/guidance/components/Confirmation/README.md +94 -0
- package/dist/guidance/components/ContextMenu/README.md +81 -0
- package/dist/guidance/components/Conversation/README.md +169 -0
- package/dist/guidance/components/CopyButton/README.md +54 -0
- package/dist/guidance/components/DataGrid/README.md +157 -0
- package/dist/guidance/components/DatePicker/README.md +116 -0
- package/dist/guidance/components/Dialog/README.md +204 -0
- package/dist/guidance/components/DiffView/README.md +107 -0
- package/dist/guidance/components/DotPlot/README.md +82 -0
- package/dist/guidance/components/Drilldown/README.md +43 -0
- package/dist/guidance/components/DropZone/README.md +71 -0
- package/dist/guidance/components/DropdownMenu/README.md +63 -0
- package/dist/guidance/components/EditRegion/README.md +263 -0
- package/dist/guidance/components/EmptyState/README.md +66 -0
- package/dist/guidance/components/FeatureGate/README.md +67 -0
- package/dist/guidance/components/Field/README.md +46 -0
- package/dist/guidance/components/FilterBar/README.md +132 -0
- package/dist/guidance/components/FormattedNumber/README.md +81 -0
- package/dist/guidance/components/Heatmap/README.md +116 -0
- package/dist/guidance/components/HoldButton/README.md +48 -0
- package/dist/guidance/components/HoverCard/README.md +71 -0
- package/dist/guidance/components/InlineCitation/README.md +56 -0
- package/dist/guidance/components/InlineEdit/README.md +379 -0
- package/dist/guidance/components/Input/README.md +63 -0
- package/dist/guidance/components/InputOTP/README.md +76 -0
- package/dist/guidance/components/ItemGroup/README.md +193 -0
- package/dist/guidance/components/Kbd/README.md +36 -0
- package/dist/guidance/components/LineChart/README.md +109 -0
- package/dist/guidance/components/Link/README.md +69 -0
- package/dist/guidance/components/Markdown/README.md +113 -0
- package/dist/guidance/components/Meter/README.md +81 -0
- package/dist/guidance/components/NotificationCenter/README.md +67 -0
- package/dist/guidance/components/NumberInput/README.md +66 -0
- package/dist/guidance/components/PageHeader/README.md +74 -0
- package/dist/guidance/components/Pagination/README.md +58 -0
- package/dist/guidance/components/PasswordInput/README.md +70 -0
- package/dist/guidance/components/PermissionMatrix/README.md +68 -0
- package/dist/guidance/components/Pile/README.md +54 -0
- package/dist/guidance/components/PlanPicker/README.md +62 -0
- package/dist/guidance/components/Popover/README.md +48 -0
- package/dist/guidance/components/Progress/README.md +41 -0
- package/dist/guidance/components/PromptInput/README.md +138 -0
- package/dist/guidance/components/PropertyList/README.md +137 -0
- package/dist/guidance/components/Question/README.md +81 -0
- package/dist/guidance/components/RadioGroup/README.md +47 -0
- package/dist/guidance/components/RelativeTime/README.md +73 -0
- package/dist/guidance/components/RemovalReceipt/README.md +111 -0
- package/dist/guidance/components/Resizable/README.md +65 -0
- package/dist/guidance/components/SaveBar/README.md +65 -0
- package/dist/guidance/components/SavedViews/README.md +82 -0
- package/dist/guidance/components/ScatterChart/README.md +104 -0
- package/dist/guidance/components/Schedule/README.md +142 -0
- package/dist/guidance/components/ScrollArea/README.md +35 -0
- package/dist/guidance/components/SecretField/README.md +74 -0
- package/dist/guidance/components/Select/README.md +97 -0
- package/dist/guidance/components/Separator/README.md +70 -0
- package/dist/guidance/components/Sheet/README.md +126 -0
- package/dist/guidance/components/Skeleton/README.md +50 -0
- package/dist/guidance/components/Slider/README.md +60 -0
- package/dist/guidance/components/SortableList/README.md +45 -0
- package/dist/guidance/components/Sparkline/README.md +87 -0
- package/dist/guidance/components/SplitButton/README.md +65 -0
- package/dist/guidance/components/Stat/README.md +92 -0
- package/dist/guidance/components/Stepper/README.md +90 -0
- package/dist/guidance/components/Suggestions/README.md +87 -0
- package/dist/guidance/components/Surface/README.md +48 -0
- package/dist/guidance/components/Switch/README.md +38 -0
- package/dist/guidance/components/SyncIndicator/README.md +78 -0
- package/dist/guidance/components/Table/README.md +138 -0
- package/dist/guidance/components/Tabs/README.md +73 -0
- package/dist/guidance/components/TagInput/README.md +116 -0
- package/dist/guidance/components/Terminal/README.md +69 -0
- package/dist/guidance/components/ThreadList/README.md +110 -0
- package/dist/guidance/components/TimeInput/README.md +63 -0
- package/dist/guidance/components/Timeline/README.md +87 -0
- package/dist/guidance/components/Toaster/README.md +54 -0
- package/dist/guidance/components/Toggle/README.md +85 -0
- package/dist/guidance/components/Toolbar/README.md +106 -0
- package/dist/guidance/components/Tooltip/README.md +41 -0
- package/dist/guidance/components/Tour/README.md +73 -0
- package/dist/guidance/components/Trace/README.md +129 -0
- package/dist/guidance/components/Tree/README.md +171 -0
- package/dist/guidance/evidence.json +18 -0
- package/dist/guidance/index.json +811 -7
- package/dist/guidance/llms.txt +153 -2
- package/dist/guidance/manifest.json +133 -12
- package/dist/guidance/patterns/screen-layout.md +1 -1
- package/dist/guidance/references/build/checks/agent-setup.mjs +7 -0
- package/dist/guidance/references/build/checks/guidance.mjs +8 -2
- package/dist/guidance/references/build/checks/source-check.mjs +101 -0
- package/dist/guidance/rules.json +155 -0
- package/dist/guidance/visual/ActionsInPlace.md +463 -0
- package/dist/guidance/visual/Charts.md +119 -0
- package/dist/guidance/visual/Distinctiveness.md +106 -0
- package/dist/guidance/visual/Elevation.md +1 -1
- package/dist/guidance/visual/Mobile.md +64 -0
- package/dist/guidance/visual/Motion.md +225 -0
- package/dist/guidance/visual/README.md +98 -0
- package/dist/guidance/visual/Research.md +1 -1
- package/dist/guidance/visual/Theming.md +349 -0
- package/dist/types/agent-fleet.d.ts +28 -0
- package/dist/types/ai-label.d.ts +7 -0
- package/dist/types/app-shell.d.ts +23 -0
- package/dist/types/area-chart.d.ts +8 -0
- package/dist/types/artifact.d.ts +14 -0
- package/dist/types/bar-chart.d.ts +6 -0
- package/dist/types/bar-list.d.ts +6 -0
- package/dist/types/breadcrumb.d.ts +6 -0
- package/dist/types/chart-brush.d.ts +6 -0
- package/dist/types/chart.d.ts +29 -1
- package/dist/types/checklist.d.ts +6 -0
- package/dist/types/code-block.d.ts +6 -0
- package/dist/types/collections.d.ts +15 -0
- package/dist/types/color-picker.d.ts +6 -0
- package/dist/types/command.d.ts +13 -0
- package/dist/types/comment-thread.d.ts +11 -0
- package/dist/types/confirmation.d.ts +8 -0
- package/dist/types/context-menu.d.ts +11 -0
- package/dist/types/controls.d.ts +174 -10
- package/dist/types/conversation.d.ts +21 -0
- package/dist/types/copy-button.d.ts +6 -0
- package/dist/types/core.d.ts +31 -0
- package/dist/types/data-grid.d.ts +25 -0
- package/dist/types/date-picker.d.ts +6 -0
- package/dist/types/diff-view.d.ts +8 -0
- package/dist/types/dot-plot.d.ts +6 -0
- package/dist/types/edit-region.d.ts +23 -0
- package/dist/types/empty.d.ts +9 -0
- package/dist/types/feature-gate.d.ts +8 -0
- package/dist/types/filter-bar.d.ts +8 -0
- package/dist/types/formatted-number.d.ts +6 -0
- package/dist/types/heatmap.d.ts +6 -0
- package/dist/types/inline-citation.d.ts +6 -0
- package/dist/types/inline-edit.d.ts +7 -0
- package/dist/types/input-otp.d.ts +11 -0
- package/dist/types/item-group.d.ts +29 -0
- package/dist/types/line-chart.d.ts +6 -0
- package/dist/types/link.d.ts +8 -1
- package/dist/types/markdown.d.ts +6 -0
- package/dist/types/meter.d.ts +6 -0
- package/dist/types/nav.d.ts +6 -0
- package/dist/types/notifications.d.ts +6 -0
- package/dist/types/number-input.d.ts +6 -0
- package/dist/types/overlays.d.ts +61 -0
- package/dist/types/page-header.d.ts +14 -0
- package/dist/types/pagination.d.ts +6 -0
- package/dist/types/password-input.d.ts +6 -0
- package/dist/types/permission-matrix.d.ts +7 -0
- package/dist/types/pile.d.ts +6 -0
- package/dist/types/plan-picker.d.ts +6 -0
- package/dist/types/prompt-input.d.ts +11 -0
- package/dist/types/property-list.d.ts +17 -0
- package/dist/types/question.d.ts +11 -0
- package/dist/types/relative-time.d.ts +8 -1
- package/dist/types/removal-receipt.d.ts +18 -1
- package/dist/types/save-bar.d.ts +6 -0
- package/dist/types/saved-views.d.ts +12 -0
- package/dist/types/scatter-chart.d.ts +8 -0
- package/dist/types/schedule.d.ts +21 -0
- package/dist/types/secret-field.d.ts +6 -0
- package/dist/types/select.d.ts +12 -0
- package/dist/types/separator.d.ts +8 -1
- package/dist/types/sheet.d.ts +19 -0
- package/dist/types/sparkline.d.ts +6 -0
- package/dist/types/split-button.d.ts +7 -0
- package/dist/types/stat.d.ts +7 -0
- package/dist/types/stepper.d.ts +12 -0
- package/dist/types/suggestions.d.ts +10 -0
- package/dist/types/sync-indicator.d.ts +8 -0
- package/dist/types/tag-input.d.ts +8 -0
- package/dist/types/terminal.d.ts +6 -0
- package/dist/types/thread-list.d.ts +16 -0
- package/dist/types/time-input.d.ts +6 -0
- package/dist/types/timeline.d.ts +9 -0
- package/dist/types/toggle.d.ts +6 -0
- package/dist/types/toolbar.d.ts +20 -0
- package/dist/types/tour.d.ts +10 -0
- package/dist/types/trace.d.ts +16 -0
- package/dist/types/tree.d.ts +21 -0
- package/dist/types-cjs/agent-fleet.d.cts +28 -0
- package/dist/types-cjs/ai-label.d.cts +7 -0
- package/dist/types-cjs/app-shell.d.cts +23 -0
- package/dist/types-cjs/area-chart.d.cts +8 -0
- package/dist/types-cjs/artifact.d.cts +14 -0
- package/dist/types-cjs/bar-chart.d.cts +6 -0
- package/dist/types-cjs/bar-list.d.cts +6 -0
- package/dist/types-cjs/breadcrumb.d.cts +6 -0
- package/dist/types-cjs/chart-brush.d.cts +6 -0
- package/dist/types-cjs/chart.d.cts +29 -1
- package/dist/types-cjs/checklist.d.cts +6 -0
- package/dist/types-cjs/code-block.d.cts +6 -0
- package/dist/types-cjs/collections.d.cts +15 -0
- package/dist/types-cjs/color-picker.d.cts +6 -0
- package/dist/types-cjs/command.d.cts +13 -0
- package/dist/types-cjs/comment-thread.d.cts +11 -0
- package/dist/types-cjs/confirmation.d.cts +8 -0
- package/dist/types-cjs/context-menu.d.cts +11 -0
- package/dist/types-cjs/controls.d.cts +174 -10
- package/dist/types-cjs/conversation.d.cts +21 -0
- package/dist/types-cjs/copy-button.d.cts +6 -0
- package/dist/types-cjs/core.d.cts +31 -0
- package/dist/types-cjs/data-grid.d.cts +25 -0
- package/dist/types-cjs/date-picker.d.cts +6 -0
- package/dist/types-cjs/diff-view.d.cts +8 -0
- package/dist/types-cjs/dot-plot.d.cts +6 -0
- package/dist/types-cjs/edit-region.d.cts +23 -0
- package/dist/types-cjs/empty.d.cts +9 -0
- package/dist/types-cjs/feature-gate.d.cts +8 -0
- package/dist/types-cjs/filter-bar.d.cts +8 -0
- package/dist/types-cjs/formatted-number.d.cts +6 -0
- package/dist/types-cjs/heatmap.d.cts +6 -0
- package/dist/types-cjs/inline-citation.d.cts +6 -0
- package/dist/types-cjs/inline-edit.d.cts +7 -0
- package/dist/types-cjs/input-otp.d.cts +11 -0
- package/dist/types-cjs/item-group.d.cts +29 -0
- package/dist/types-cjs/line-chart.d.cts +6 -0
- package/dist/types-cjs/link.d.cts +8 -1
- package/dist/types-cjs/markdown.d.cts +6 -0
- package/dist/types-cjs/meter.d.cts +6 -0
- package/dist/types-cjs/nav.d.cts +6 -0
- package/dist/types-cjs/notifications.d.cts +6 -0
- package/dist/types-cjs/number-input.d.cts +6 -0
- package/dist/types-cjs/overlays.d.cts +61 -0
- package/dist/types-cjs/page-header.d.cts +14 -0
- package/dist/types-cjs/pagination.d.cts +6 -0
- package/dist/types-cjs/password-input.d.cts +6 -0
- package/dist/types-cjs/permission-matrix.d.cts +7 -0
- package/dist/types-cjs/pile.d.cts +6 -0
- package/dist/types-cjs/plan-picker.d.cts +6 -0
- package/dist/types-cjs/prompt-input.d.cts +11 -0
- package/dist/types-cjs/property-list.d.cts +17 -0
- package/dist/types-cjs/question.d.cts +11 -0
- package/dist/types-cjs/relative-time.d.cts +8 -1
- package/dist/types-cjs/removal-receipt.d.cts +18 -1
- package/dist/types-cjs/save-bar.d.cts +6 -0
- package/dist/types-cjs/saved-views.d.cts +12 -0
- package/dist/types-cjs/scatter-chart.d.cts +8 -0
- package/dist/types-cjs/schedule.d.cts +21 -0
- package/dist/types-cjs/secret-field.d.cts +6 -0
- package/dist/types-cjs/select.d.cts +12 -0
- package/dist/types-cjs/separator.d.cts +8 -1
- package/dist/types-cjs/sheet.d.cts +19 -0
- package/dist/types-cjs/sparkline.d.cts +6 -0
- package/dist/types-cjs/split-button.d.cts +7 -0
- package/dist/types-cjs/stat.d.cts +7 -0
- package/dist/types-cjs/stepper.d.cts +12 -0
- package/dist/types-cjs/suggestions.d.cts +10 -0
- package/dist/types-cjs/sync-indicator.d.cts +8 -0
- package/dist/types-cjs/tag-input.d.cts +8 -0
- package/dist/types-cjs/terminal.d.cts +6 -0
- package/dist/types-cjs/thread-list.d.cts +16 -0
- package/dist/types-cjs/time-input.d.cts +6 -0
- package/dist/types-cjs/timeline.d.cts +9 -0
- package/dist/types-cjs/toggle.d.cts +6 -0
- package/dist/types-cjs/toolbar.d.cts +20 -0
- package/dist/types-cjs/tour.d.cts +10 -0
- package/dist/types-cjs/trace.d.cts +16 -0
- package/dist/types-cjs/tree.d.cts +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Await
|
|
2
|
+
|
|
3
|
+
Components that bring their own loading state. Pass your data the way you already do: while it's `undefined` (`user?.name` while a query loads), the component shows its own imprint at its own size, and when the data arrives the content rises out of it. You don't write skeletons or loading flags. `Await` is the Suspense boundary inside each of them, exported so your components can do the same.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
const { data: user } = useQuery(userQuery(id)); // useSWR, a loader, your own state: anything that's undefined until it isn't
|
|
7
|
+
|
|
8
|
+
<Card>
|
|
9
|
+
<CardHeader>
|
|
10
|
+
<Avatar name={user?.name} size={44} />
|
|
11
|
+
<CardTitle>{user?.name}</CardTitle>
|
|
12
|
+
<CardDescription>{user?.role}</CardDescription>
|
|
13
|
+
</CardHeader>
|
|
14
|
+
<CardContent>{user?.bio}</CardContent>
|
|
15
|
+
</Card>
|
|
16
|
+
<DataTable columns={columns} rows={deploys} getRowId={(d) => d.id} />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Elevation: loading is an imprint.** Content still to come is the shape of what's coming, pressed into the page (`surface-well`, `well`), as `Skeleton` draws it. The difference is who draws it. A component holds its own `<Suspense>` boundary, and its fallback is drawn from the same props and tokens as the component itself: an `Avatar` of `size={44}` leaves a 44px disc; a `CardTitle` leaves one line as tall as the title's line-height; a `Badge` leaves a plate; a `DataTable` keeps its real header over rows of cell imprints. So the imprint is the size of the content, and nothing moves when it lands.
|
|
20
|
+
|
|
21
|
+
**What counts as "not here yet".** `undefined` is on its way; `null` is nothing, and renders empty at once. Required content waits whenever it's undefined. Optional content (a card's description, text passed straight into `CardContent`) waits only while its card's required parts do, so a field that's simply missing from your data never leaves an imprint behind.
|
|
22
|
+
|
|
23
|
+
| Content | Waits while | Imprint |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `Avatar` `name` | undefined | a disc of its `size` |
|
|
26
|
+
| `CardTitle` children | undefined | one line of the title's type; `skeleton={{ width }}` |
|
|
27
|
+
| `Badge` children | undefined | a plate (`width`, default 64px; a `count` is round) |
|
|
28
|
+
| `DataTable` `rows` | undefined | the real header over `skeleton.lines` rows of imprinted cells (default 5) |
|
|
29
|
+
| `CardDescription` children | undefined and the card's required parts are waiting | two lines of the description's type; `skeleton={{ lines, width }}` |
|
|
30
|
+
| `CardContent` children | undefined and the card's required parts are waiting, or anything of yours inside suspends | `lines` lines (default 3) |
|
|
31
|
+
| `Card` | anything of yours inside suspends | a title line over `lines` lines (default 3) |
|
|
32
|
+
|
|
33
|
+
Every one of them also takes a promise in place of the value (from a server component, say) and waits for it the same way.
|
|
34
|
+
|
|
35
|
+
**If a request fails** your data stays undefined, so the imprint stays. Render your error state in place of the content, as you would anyway, or pass `null`. `skeleton={false}` on any component renders exactly what you pass, with no imprint.
|
|
36
|
+
|
|
37
|
+
**Motion.** An imprint waits `dur-grace` (300ms) before it fades in, so loads quicker than that show nothing and land without moving: Skeleton's rule, "don't show imprints for loads under about 300ms", is now kept for you. When content lands after its imprint showed, it rises 8px out of it (`dur-travel-3`, `ease-rise`) while the imprint fills in behind it. Rises share the attention budget (`riseDelay`): if many land at once, no more than four begin together. Containers (`Card`, `CardContent`, `DataTable`) leave no imprint behind; their content just rises. Under reduced motion it fades in place.
|
|
38
|
+
|
|
39
|
+
**Landing together.** A page full of boundaries fills in piece by piece as each one's data arrives. Wrap related parts in `RevealGroup` and each part that lands early stays an imprint until the slowest one in the group has landed; then they rise together (still four at a time). Group things that load from the same place; a group waits for its slowest member.
|
|
40
|
+
|
|
41
|
+
**Without height.** Every imprint carries a visually hidden "Loading" and `aria-busy`, and is otherwise hidden from assistive technology; the content that replaces it is announced as normal content. Under forced colours imprints have a dashed `CanvasText` outline, like every sunk surface.
|
|
42
|
+
|
|
43
|
+
**How it works.** An undefined value becomes a suspension: the component's outer part (which stays mounted and re-renders when your data arrives) holds a promise that resolves once the value turns up (`useArrival`), and its inner part reads it, so the component's own boundary shows the imprint, and the reveal is the same one a real promise gets. Reading is React 19's `use()` when it's there; on React 18 the same contract is kept by hand (the promise is thrown to the boundary and its result remembered), so both work. A component that has never waited takes the old path exactly: no boundary, no extra element. Once it has, the wrapper stays, as `display: contents`, so it adds nothing to layout.
|
|
44
|
+
|
|
45
|
+
**Limits.** Your own markup can't tell: `<p>{user?.bio}</p>` inside a card renders an empty paragraph while it loads; pass the text straight to `CardContent` or `CardDescription` instead. An optional part whose data comes from a different, slower request than its card's required parts is treated as missing once they land, and appears when its data does, without an imprint.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// Promises work too: from a server component, start the request and pass it along.
|
|
49
|
+
const user = getUser(id);
|
|
50
|
+
<Avatar name={user.then((u) => u.name)} size={44} />
|
|
51
|
+
|
|
52
|
+
// Your own component, the same way:
|
|
53
|
+
<Await inline shape="circle" imprint={<Skeleton shape="circle" width={24} />}><Flag code={country} /></Await>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Breath (experimental).** `<AltitudeProvider loading="breath">` changes how imprints wait and how content lands; nothing changes without it. Instead of light sweeping over them, imprints breathe in depth: the floor deepens to trench depth and releases (the back wall in Nightfall), once every `dur-breath` (4.8s), on one page clock, with a slow swell down the view. Ten seconds in, the breath calms. When content lands:
|
|
57
|
+
|
|
58
|
+
| Content | `settle` | Lands |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| On its own surface (a card from one request) | `"raised"` | rises 8px to level 2 and settles to level 1 over `dur-arrive` |
|
|
61
|
+
| On the ground (text, a badge, a row) | `"flush"` (default) | its floor comes up just past the ground and fills in flush |
|
|
62
|
+
| Into a view already showing (Load more, a boundary that suspends again) | `late` | an untinted plate under it, gone in `dur-plate` |
|
|
63
|
+
|
|
64
|
+
A `Card`'s parts land together, and a `RevealGroup` takes one place in the attention budget (its parts 16ms apart). A card's optional parts still don't wait: if its description is `undefined` once its title has landed, it renders empty, so pass a promise for a detail that's still coming. Chart, stat and meter imprints hold still, and a chart's marks still fade in place. Under reduced motion the breath stops and nothing travels.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<AltitudeProvider scheme="daylight" loading="breath">
|
|
68
|
+
<Await settle="raised" imprint={<Skeleton height={103} radius="var(--radius-lg)" />}><DeployCard deploy={deploy} /></Await>
|
|
69
|
+
{older.map((e) => <Await key={e.id} late imprint={<Skeleton height={26} />}><Event event={e.promise} /></Await>)}
|
|
70
|
+
</AltitudeProvider>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**You provide:** your data as it is, `undefined` until it arrives. Hints where the default shape won't match (`skeleton={{ lines: 3 }}`). If you pass promises, make them stable across renders: started in a server component, a loader, a cache or state, never created inline in a client component's render. `skeleton={false}` on any of them to opt out.
|
|
74
|
+
|
|
75
|
+
- Do pass `user?.name` and friends straight in, and let each part land when its data does.
|
|
76
|
+
- Do wrap parts that read better arriving together (a profile header) in `RevealGroup`.
|
|
77
|
+
- Don't wrap Suspense-native components in your own `Skeleton`s; they already imprint themselves.
|
|
78
|
+
- Don't use it for errors: a rejected promise throws to your nearest error boundary, where an `Alert` belongs.
|
|
79
|
+
- Don't use `null` for "still loading"; it means there's nothing, and renders empty.
|
|
80
|
+
- `Skeleton` with `loaded` is still the way for layouts of your own.
|
|
81
|
+
|
|
82
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
83
|
+
## Example
|
|
84
|
+
|
|
85
|
+
The full preview, as rendered in Storybook and checked in CI (`card`, `who`, `id`, `col`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import * as React from 'react';
|
|
89
|
+
import { Avatar, Badge, Card, CardContent, CardDescription, CardHeader, CardTitle } from 'altitude-design';
|
|
90
|
+
|
|
91
|
+
const USER = { name: 'Mara Quist', role: 'Platform engineer, Lisbon', bio: 'Merged “Faster cold starts” into main. Cold starts are down 38% across all regions.' };
|
|
92
|
+
// Stands in for useQuery or useSWR: undefined until the data arrives.
|
|
93
|
+
function useLoaded(value, ms) {
|
|
94
|
+
const [data, setData] = React.useState(undefined);
|
|
95
|
+
React.useEffect(() => { const t = setTimeout(() => setData(value), ms); return () => clearTimeout(t); }, []);
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
function Member() {
|
|
99
|
+
const user = useLoaded(USER, 1200);
|
|
100
|
+
const status = useLoaded('On call', 1800);
|
|
101
|
+
return (
|
|
102
|
+
<Card className="card">
|
|
103
|
+
<CardHeader>
|
|
104
|
+
<div className="who">
|
|
105
|
+
<Avatar name={user?.name} size={44} status="active" />
|
|
106
|
+
<div className="id">
|
|
107
|
+
<CardTitle>{user?.name}</CardTitle>
|
|
108
|
+
<CardDescription skeleton={{ lines: 1, width: '70%' }}>{user?.role}</CardDescription>
|
|
109
|
+
</div>
|
|
110
|
+
<Badge tone="positive">{status}</Badge>
|
|
111
|
+
</div>
|
|
112
|
+
</CardHeader>
|
|
113
|
+
<CardContent skeleton={{ lines: 2 }}>{user?.bio}</CardContent>
|
|
114
|
+
</Card>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
export default function App() {
|
|
118
|
+
const [round, setRound] = React.useState(0);
|
|
119
|
+
React.useEffect(() => { const t = setTimeout(() => setRound((r) => r + 1), 4400); return () => clearTimeout(t); }, [round]);
|
|
120
|
+
return (
|
|
121
|
+
<div className="col">
|
|
122
|
+
<Member key={round} />
|
|
123
|
+
<p className="cap">No skeleton code: while user is undefined, each part imprints itself at its own size, and rises out of it when the data arrives.</p>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
<!-- /example -->
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Badge
|
|
2
|
+
|
|
3
|
+
Labels status or metadata. A badge is an **indicator plate**: part of the record, so it's set flush into the page (a shallow stamp, darker at its top edge with a light lip below) and never lifts.
|
|
4
|
+
|
|
5
|
+
- **Lamp.** Status carries an indicator lamp: a flat 7px dot in its tone's own colour (`positive`, `caution`, `critical` or `accent`, the same colour the tone uses for text), with no gloss, socket or glow. It's printed on the plate rather than set into it, so it never competes with the plate's height. It's on by default for `positive`, `caution` and `critical`; `lamp` on a neutral badge shows an unlit lamp, an empty `ink-muted` ring ("Draft"); `lamp={false}` hides it. With a lamp, the label stays in `ink` and the colour lives in the lamp. Without one, a toned plate says its tone in its lettering.
|
|
6
|
+
- **Relight and re-stamp.** When `tone` changes the lamp relights: it starts as an empty ring and fills in over `dur-travel-3`. When the label changes, the plate is re-stamped (it comes down 3px onto the page and settles). Neither happens on first render.
|
|
7
|
+
- **Pulse.** `lamp="pulse"` fades the dot to a third and back over `dur-hover`, for something in progress ("Deploying"). It holds still under reduced motion.
|
|
8
|
+
|
|
9
|
+
| `variant` | Looks like | Use |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `plate` (default) | a neutral plate stamped into the ground | Most status and metadata. |
|
|
12
|
+
| `tinted` | the plate takes its tone's `-soft` colour, lettering in the tone | When status needs to be seen from across the page. (`soft` is the old name.) |
|
|
13
|
+
| `outline` | a `line-control` hairline only | Versions, IDs, counts of things that don't need attention. |
|
|
14
|
+
| `count` | a number on a floating pill (`elev-2`) | Unread or waiting items: the one badge that rises, because it wants you. It pops up when the number changes. Solid `accent` or `critical`; neutral is `surface-floating`. Position it yourself (for example absolutely, over a Button's corner). |
|
|
15
|
+
|
|
16
|
+
**Loading.** While the label is undefined (your data hasn't arrived) or a promise, the badge is a plate-shaped imprint (see `Await`).
|
|
17
|
+
|
|
18
|
+
**You provide:** a short label (one or two words, sentence case) or a number for `count`, `tone`, optionally `variant` and `lamp`.
|
|
19
|
+
|
|
20
|
+
- Do pair status colour with the word: "Outage", never a lamp alone.
|
|
21
|
+
- Don't make badges clickable; if it filters or navigates, it's a ghost Button.
|
|
22
|
+
- Don't use `count` for totals that aren't asking for attention; use a plate or plain text.
|
|
23
|
+
|
|
24
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
25
|
+
## Example
|
|
26
|
+
|
|
27
|
+
`Badge` as first used in its preview:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Badge } from 'altitude-design';
|
|
31
|
+
|
|
32
|
+
<Badge lamp>Draft</Badge>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `row`, `anchor`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import * as React from 'react';
|
|
39
|
+
import { Badge, Button } from 'altitude-design';
|
|
40
|
+
|
|
41
|
+
export default function App() {
|
|
42
|
+
const [deploying, setDeploying] = React.useState(true);
|
|
43
|
+
const [n, setN] = React.useState(3);
|
|
44
|
+
React.useEffect(() => { const t = setInterval(() => { setDeploying((d) => !d); setN((x) => (x % 5) + 1); }, 2400); return () => clearInterval(t); }, []);
|
|
45
|
+
return (
|
|
46
|
+
<div className="col">
|
|
47
|
+
<div className="row">
|
|
48
|
+
<Badge lamp>Draft</Badge>
|
|
49
|
+
<Badge tone="accent">Beta</Badge>
|
|
50
|
+
<Badge tone="positive">Operational</Badge>
|
|
51
|
+
<Badge tone="caution">Degraded</Badge>
|
|
52
|
+
<Badge tone="critical">Outage</Badge>
|
|
53
|
+
<Badge tone={deploying ? 'accent' : 'positive'} lamp={deploying ? 'pulse' : true}>{deploying ? 'Deploying' : 'Live'}</Badge>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="row">
|
|
56
|
+
<Badge tone="positive" variant="tinted">Operational</Badge>
|
|
57
|
+
<Badge tone="critical" variant="tinted">Outage</Badge>
|
|
58
|
+
<Badge variant="outline">v2.4.0</Badge>
|
|
59
|
+
<span className="anchor"><Button>Inbox</Button><Badge variant="count" tone="critical">{n}</Badge></span>
|
|
60
|
+
</div>
|
|
61
|
+
<p className="cap">Labels are stamped into the record; status wears a lamp that relights when it changes. A count is the one badge that floats, because it wants you.</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
<!-- /example -->
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# BarChart
|
|
2
|
+
|
|
3
|
+
Amounts compared across categories or periods: deploys per team, signups per month, cost per region. One series, several side by side (grouped), or stacked into a total. Length on a common baseline is the most accurately read encoding there is (Cleveland & McGill, 1984), so this is the workhorse chart. It renders inside a [`ChartContainer`](../ChartContainer/README.md).
|
|
4
|
+
|
|
5
|
+
**Why it's flat.** Bars are the record drawn as lengths, so they stand on the ground line: flat `data-*` fills, square where they meet the ground and rounded (`radius-xs`) only at the data end. Neighbours and stacked segments are parted by a real 2px gap, never a shadow or an outline. A bar never lifts, on hover or anywhere else: a lifted bar leaves the ground line or casts a shadow below its top, and both misstate its value. A negative bar hangs below the ground line. That's position, not depth: it has no inner wall and doesn't read as an opening. One series wears `data-1` throughout; bars are never coloured by their value.
|
|
6
|
+
|
|
7
|
+
**Without colour.** Under forced colours and in print, and with `patterns` on the container, each series gets its hatch (the first stays solid) and filled bars a `CanvasText` edge. With `contrast="more"` bars get a 1px `ink` edge and 3px gaps. Value labels are `ink` text, never a series colour.
|
|
8
|
+
|
|
9
|
+
**Motion.** Values glide at one level, re-sorts are carried, a rescale comes first, and nothing grows from the ground line on first render; a bar grows only when its value really goes from zero to something ([Motion](../../visual/Motion.md), "Charts").
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The container names the chart and summarises it; without a written `summary`, the generated one names the series and each one's lowest and highest category. The keyboard path through the bars and the readout are #63 and #52, and the table view is #64.
|
|
12
|
+
|
|
13
|
+
**Props:**
|
|
14
|
+
|
|
15
|
+
- `data` and `x`: the records and the key of each one's category;
|
|
16
|
+
- `sort`: `'desc'` or `'asc'` by total;
|
|
17
|
+
- `stack`: `true` for parts of a total, `'expand'` for shares of 100%;
|
|
18
|
+
- `layout="horizontal"`: categories down the side, for long names or many of them. Names longer than 40% of the width are cut with an ellipsis; the full name stays in the summary;
|
|
19
|
+
- `labels`: `'max'` labels the largest bar (or stack), `'last'` the last category's. Never every bar;
|
|
20
|
+
- `status`: which categories aren't yet the record (`'partial'` or `'projected'`), and `boundary`, the rule's word. See below;
|
|
21
|
+
- `window` and `onWindowChange`: for bars over time (a bar per week), a stretch of the categories by date, `{ start, end }` or the last `{ points }`. A `ChartBrush` inside the chart picks it on an overview.
|
|
22
|
+
|
|
23
|
+
**Selecting and drilling.** `selected` and `onSelect` make the marks a selection that cross-filters the rest of a dashboard, and `drill` opens a level down inside a `Drilldown`. Hover only promises what a click does. See [ChartContainer](../ChartContainer/README.md), "Marks you can select or drill into".
|
|
24
|
+
|
|
25
|
+
**Filtering in place.** `relevance={(d) => 'match' | 'miss' | undefined}` recedes misses to 35% and keeps everything in place; `rescale` fits the axis to the matches. See [ChartContainer](../ChartContainer/README.md), "Filtering in place".
|
|
26
|
+
|
|
27
|
+
**Not yet the record.** `status={(d) => d.month === thisMonth ? 'partial' : undefined}` keeps this month so far from reading as a drop. A partial bar is its series colour hatched at 45° on the plot's own colour, with no solid fill; a projected one also keeps a 1.5px outline in its colour. Neither is lifted: a pending write hovers, but a bar that hovers misstates its value, so it says "not yet" with pattern and a word instead. A 1px `line-control` rule marks where the record ends, named "Today" (`boundary="…"` renames it, `boundary={false}` drops it). The bar's value label, readout, keyboard name and table row say "So far" or "Projected". When the period completes and its status goes, the hatching cross-fades to solid over `dur-travel-3`: it's been pressed into the record. Under forced colours the pending bar takes a dotted `CanvasText` outline, the same cue as a pending write.
|
|
28
|
+
|
|
29
|
+
The y axis always includes zero, whatever the container's `zero` says: a truncated bar axis isn't offered.
|
|
30
|
+
|
|
31
|
+
**Focus.** `focus={['web']}` keeps those series' colours and draws the rest in `data-context`; on a one-series chart, `focus={(d) => d.team === 'Cedar' || d.team === 'Delta'}` picks out records instead, the categories the takeaway names. See [ChartContainer](../ChartContainer/README.md), "Focus".
|
|
32
|
+
|
|
33
|
+
**Against the record before.** `compare={{ key: 'lastQuarter', label: 'Q2' }}` (a field on each record) or `compare={{ data: lastWeek, label: 'Last week' }}` (records matched by position, before any sort) draws the earlier value as a 2px `ink` tick across each bar, 4px past it on each side: Few's comparative measure. A value that fell leaves its tick past the bar's end, the drop visible as empty space. Grouped bars compare one `series` (default the first); a stack compares its total; shares (`stack="expand"`) don't compare. The readout, point names ("Up 11% from Q2"), the table ("Q2" and "vs Q2" columns) and the summary ("Against Q2: 3 rose, 1 fell.") say the difference, and earlier values join the axis.
|
|
34
|
+
|
|
35
|
+
**You provide:** a `ChartContainer` with a `config` naming each series, the `data` and `x`, and usually a `summary`.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<ChartContainer config={{ deploys: { label: 'Deploys' } }} summary="Cedar deployed most, 51 times.">
|
|
39
|
+
<BarChart data={byTeam} x="team" sort="desc" labels="max" />
|
|
40
|
+
</ChartContainer>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- Do sort categories that have no order of their own, so the comparison reads at a glance.
|
|
44
|
+
- Do keep grouped bars to four series or fewer; past that, stack them or use small multiples.
|
|
45
|
+
- Do use `stack="expand"` only when the shares matter more than the totals, and say so in the title.
|
|
46
|
+
- Don't label every bar: label the one the chart is about, and leave the rest to the readout and the table.
|
|
47
|
+
- Don't colour bars by their value or give them a gradient; for polarity, a diverging ramp on a separate chart says it better.
|
|
48
|
+
|
|
49
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
50
|
+
## Example
|
|
51
|
+
|
|
52
|
+
`BarChart` as first used in its preview:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { BarChart } from 'altitude-design';
|
|
56
|
+
|
|
57
|
+
<BarChart data={teams} x="team" sort="desc" labels="max" focus={(d) => d.team === 'Cedar' || d.team === 'Delta'} />
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The full preview, as rendered in Storybook and checked in CI (`bc` is the preview page's own layout class, not Altitude's):
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { BarChart, Card, CardContent, CardDescription, CardHeader, CardTitle, ChartContainer } from 'altitude-design';
|
|
64
|
+
|
|
65
|
+
const teams = [{ team: 'Atlas', deploys: 42 }, { team: 'Birch', deploys: 35 }, { team: 'Cedar', deploys: 51 }, { team: 'Delta', deploys: 19 }, { team: 'Ember', deploys: 28 }];
|
|
66
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'].map((month, i) => ({ month, api: 20 + ((i * 7) % 11), web: 14 + ((i * 5) % 9), jobs: 6 + ((i * 3) % 7) }));
|
|
67
|
+
const net = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'].map((month, i) => ({ month, change: [12, -8, 5, -15, 9, 18][i] }));
|
|
68
|
+
const regions = [{ region: 'North America', seats: 1240, before: 1120 }, { region: 'Europe, Middle East and Africa', seats: 980, before: 1040 }, { region: 'Asia Pacific', seats: 610, before: 470 }, { region: 'Latin America', seats: 270, before: 240 }];
|
|
69
|
+
const stack3 = { api: { label: 'API' }, web: { label: 'Web' }, jobs: { label: 'Jobs' } };
|
|
70
|
+
function Chart({ id, title, sub, children, ...props }) {
|
|
71
|
+
// With a takeaway the figure names itself: the title becomes the label over the takeaway, the heading.
|
|
72
|
+
if (props.takeaway) return (
|
|
73
|
+
<Card>
|
|
74
|
+
<CardContent style={{ paddingTop: 'var(--space-5)' }}><ChartContainer title={title} description={sub} aspect={1.45} {...props}>{children}</ChartContainer></CardContent>
|
|
75
|
+
</Card>
|
|
76
|
+
);
|
|
77
|
+
return (
|
|
78
|
+
<Card>
|
|
79
|
+
<CardHeader><CardTitle id={id}>{title}</CardTitle><CardDescription>{sub}</CardDescription></CardHeader>
|
|
80
|
+
<CardContent><ChartContainer aria-labelledby={id} aspect={1.45} {...props}>{children}</ChartContainer></CardContent>
|
|
81
|
+
</Card>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
export default function App() {
|
|
85
|
+
return (
|
|
86
|
+
<div className="bc">
|
|
87
|
+
<Chart id="b1" title="Deploys by team" takeaway="Cedar deployed nearly three times as often as Delta" sub='One series, sorted, labels="max"' config={{ deploys: { label: 'Deploys' } }} summary="Cedar deployed most, 51 times; Delta least, 19.">
|
|
88
|
+
<BarChart data={teams} x="team" sort="desc" labels="max" focus={(d) => d.team === 'Cedar' || d.team === 'Delta'} />
|
|
89
|
+
</Chart>
|
|
90
|
+
<Chart id="b2" title="Requests by service" sub="Grouped: three series side by side" config={stack3}>
|
|
91
|
+
<BarChart data={months.slice(3)} x="month" />
|
|
92
|
+
</Chart>
|
|
93
|
+
<Chart id="b3" title="Requests, all services" sub='Stacked, labels="last", focus={["web"]}' config={stack3}>
|
|
94
|
+
<BarChart data={months} x="month" stack labels="last" focus={['web']} />
|
|
95
|
+
</Chart>
|
|
96
|
+
<Chart id="b4" title="Share of requests" sub='stack="expand": parts of 100%' config={stack3}>
|
|
97
|
+
<BarChart data={months} x="month" stack="expand" />
|
|
98
|
+
</Chart>
|
|
99
|
+
<Chart id="b5" title="Net new customers" sub="Negative bars hang below the ground line" config={{ change: { label: 'Net change', format: (v) => (v > 0 ? '+' : '') + v } }}>
|
|
100
|
+
<BarChart data={net} x="month" />
|
|
101
|
+
</Chart>
|
|
102
|
+
<Chart id="b6" title="Seats by region" sub='layout="horizontal", against last quarter' config={{ seats: { label: 'Seats', format: (v) => v.toLocaleString('en') } }}>
|
|
103
|
+
<BarChart data={regions} x="region" layout="horizontal" compare={{ key: 'before', label: 'Q2' }} labels="max" />
|
|
104
|
+
</Chart>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
<!-- /example -->
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# BarList
|
|
2
|
+
|
|
3
|
+
Top pages, top referrers, top customers by spend: a ranked list where each row is its own bar, the label is readable at any length, and the exact value sits at the end. It sits between a `Table` (exact, no shape) and a horizontal `BarChart` (shape, but long labels squeeze the plot).
|
|
4
|
+
|
|
5
|
+
**Why it's flat.** Rows are the record, so they lie flush on the ground with `line` dividers, like a table's rows. Each row's bar is a flat tint of `data-1` behind its label, as long as the row's value against the largest (the top row's bar fills its row), and faint enough that `ink` holds 4.5:1 over it in every scheme. It has a `radius-xs` corner and no shadow, wall or gradient. A long label wraps; it's never cut. The value sits at the row's end in tabular figures, and with `share` its percentage of the total sits beyond the bar in `ink-muted`.
|
|
6
|
+
|
|
7
|
+
**Hover is a promise.** A row that does something (`href` makes it a link, `onSelect` a button) lifts one level when pointed at, exactly as `Table`'s interactive rows do, and drops back when pressed; its bar stays flat on it. A row that does nothing never moves.
|
|
8
|
+
|
|
9
|
+
**Other and Show all.** With `limit`, the rows past it fold into one "Other (14 pages)" row in `data-other`, and a ghost "Show all 21 pages" button lists them all (and "Show fewer" folds them back). `noun` names what the rows are.
|
|
10
|
+
|
|
11
|
+
**Filter in place.** `relevance` raises matches one level and sinks misses into a well, as `TableRow relevance` does; nothing is removed, and misses say "not matching" to screen readers. Under the list it says how many match: "2 of 5 pages match", using `noun`.
|
|
12
|
+
|
|
13
|
+
**Motion.** When values change, rows are carried to their new ranks (FLIP, `dur-travel-3`, `ease-glide`), all at once, because a re-rank is one change; bars glide to their new lengths. Under reduced motion both change in place. While `data` is undefined the list is imprinted rows, which land like `Await` content.
|
|
14
|
+
|
|
15
|
+
**Accessibility.** Plain HTML: an `ol` (rank is meaningful) of links, buttons or plain items, each reading "1. /pricing, 4,210, 23%". The bar is hidden from assistive technology; the value is text. Under forced colours bars are a light `Highlight` tint with a 1px `Highlight` border, matches are outlined and misses dashed. Under `contrast="more"` bars get a 1px `ink` edge.
|
|
16
|
+
|
|
17
|
+
**Props:** `data` (`{ name, value, key?, href? }[]`); `format`; `limit`; `share`; `sort` (`'desc'`, the default, or `false` to keep your order); `noun`; `onSelect`; `relevance`; `locale`; and any `ol` attribute (name it with `aria-labelledby` or `aria-label`).
|
|
18
|
+
|
|
19
|
+
**You provide:** the rows, a name for the list, and a `format` with the unit when it isn't a plain count.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
<BarList aria-labelledby="pages-title" data={pages} format={(n) => n.toLocaleString()} limit={8} share noun="pages" />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do use it for a ranked top-N with long labels; use a `BarChart` to compare a few categories across series.
|
|
26
|
+
- Do give rows an `href` when they open a report, so they're real links.
|
|
27
|
+
- Do fold a long tail into "Other" with `limit` rather than scrolling a hundred rows.
|
|
28
|
+
- Don't colour bars by status or give each row its own colour: colour is identity, and these rows are one series.
|
|
29
|
+
- Don't set the bars' lengths to share of total yourself; `share` says it in words, and the bars compare rows with each other.
|
|
30
|
+
|
|
31
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
32
|
+
## Example
|
|
33
|
+
|
|
34
|
+
`BarList` as first used in its preview:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { BarList } from 'altitude-design';
|
|
38
|
+
|
|
39
|
+
<BarList aria-labelledby="bl-pages" data={pages.map((p) => ({ ...p, href: '#' + p.name }))} limit={7} share noun="pages" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The full preview, as rendered in Storybook and checked in CI (`bl`, `bl-tools` are the preview page's own layout classes, not Altitude's):
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import * as React from 'react';
|
|
46
|
+
import { BarList, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input } from 'altitude-design';
|
|
47
|
+
|
|
48
|
+
const pages = [
|
|
49
|
+
{ name: '/pricing', value: 4210 }, { name: '/', value: 3880 }, { name: '/docs/getting-started', value: 2410 }, { name: '/blog/height-is-the-interface-why-we-stopped-using-shadows-for-everything', value: 1630 },
|
|
50
|
+
{ name: '/changelog', value: 1210 }, { name: '/docs/components/chart-container', value: 980 }, { name: '/careers', value: 610 },
|
|
51
|
+
...Array.from({ length: 14 }, (_, i) => ({ name: `/docs/page-${i + 1}`, value: 420 - i * 25 })),
|
|
52
|
+
];
|
|
53
|
+
const referrers0 = [{ name: 'google.com', value: 5120 }, { name: 'github.com', value: 2310 }, { name: 'news.ycombinator.com', value: 1980 }, { name: 'x.com', value: 760 }, { name: 'Direct', value: 3400 }];
|
|
54
|
+
const referrers1 = [{ name: 'google.com', value: 5240 }, { name: 'github.com', value: 2390 }, { name: 'news.ycombinator.com', value: 6120 }, { name: 'x.com', value: 1310 }, { name: 'Direct', value: 3460 }];
|
|
55
|
+
export default function App() {
|
|
56
|
+
const [spike, setSpike] = React.useState(false);
|
|
57
|
+
const [q, setQ] = React.useState('');
|
|
58
|
+
const [later, setLater] = React.useState(undefined);
|
|
59
|
+
React.useEffect(() => { const t = setTimeout(() => setLater([{ name: 'Acme Corp', value: 48200 }, { name: 'Globex', value: 31900 }, { name: 'Initech', value: 22750 }, { name: 'Umbrella', value: 9400 }]), 2400); return () => clearTimeout(t); }, []);
|
|
60
|
+
const usd = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
|
|
61
|
+
return (
|
|
62
|
+
<div className="bl">
|
|
63
|
+
<Card>
|
|
64
|
+
<CardHeader><CardTitle id="bl-pages">Top pages</CardTitle><CardDescription>Last 7 days · views</CardDescription></CardHeader>
|
|
65
|
+
<CardContent><BarList aria-labelledby="bl-pages" data={pages.map((p) => ({ ...p, href: '#' + p.name }))} limit={7} share noun="pages" /></CardContent>
|
|
66
|
+
</Card>
|
|
67
|
+
<div style={{ display: 'grid', gap: 16, alignContent: 'start' }}>
|
|
68
|
+
<Card>
|
|
69
|
+
<CardHeader><CardTitle id="bl-ref">Top referrers</CardTitle><CardDescription>Static rows: nothing lifts</CardDescription></CardHeader>
|
|
70
|
+
<CardContent>
|
|
71
|
+
<div className="bl-tools">
|
|
72
|
+
<Button size="sm" variant="secondary" onClick={() => setSpike((s) => !s)}>{spike ? 'Yesterday' : 'Today'}</Button>
|
|
73
|
+
<Input size="sm" placeholder="Filter" aria-label="Filter referrers" value={q} onChange={(e) => setQ(e.target.value)} />
|
|
74
|
+
</div>
|
|
75
|
+
<BarList aria-labelledby="bl-ref" data={spike ? referrers1 : referrers0} relevance={q ? (d) => (d.name.includes(q) ? 'match' : 'miss') : undefined} />
|
|
76
|
+
</CardContent>
|
|
77
|
+
</Card>
|
|
78
|
+
<Card>
|
|
79
|
+
<CardHeader><CardTitle id="bl-cust">Top customers</CardTitle><CardDescription>Spend this quarter</CardDescription></CardHeader>
|
|
80
|
+
<CardContent><BarList aria-labelledby="bl-cust" data={later} format={usd.format} limit={4} /></CardContent>
|
|
81
|
+
</Card>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
<!-- /example -->
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Board
|
|
2
|
+
|
|
3
|
+
Columns of cards you move between, a kanban: columns are receptacles that open when you carry a card, and the card floats in your hand until it lands.
|
|
4
|
+
|
|
5
|
+
**Elevation: receptacles and a hand.** At rest a board is just its cards: the columns lie flush on the ground, with only their titles and counts to mark them, and the cards are raised (level 1), lifting to level 2 under the pointer. The moment a card is carried, every column opens into a well (things you put something into are openings), and the column under the card deepens to a trench with an accent rim, as a DropZone does when it's ready to receive. The carried card floats at level 3, tilted 1.2° and 1.5% larger, and follows the pointer; the cards around it glide aside (`ease-settle`), and the slot it will land in opens as a trench. Let go and it's carried into the slot with a slight overshoot (`ease-settle`) while its shadow sinks back to `elev-1`, and the columns close up. A move to the column it came from, or nowhere, travels back the same way.
|
|
6
|
+
|
|
7
|
+
- **A collapsed column is a Pile.** Its thickness (one, two or three sheets) and its count badge say how many cards it holds. It still takes drops: carried over it, it deepens and fans its sheets toward the card, and the card is added to the end.
|
|
8
|
+
- **Someone else's hand.** A card a collaborator is holding (`heldBy`) is lifted to level 3, outlined in their presence colour and wears their name; you can't pick it up meanwhile.
|
|
9
|
+
- **Afterglow.** `settle` makes a card arrive tinted and lifted, then settle: for cards moved by someone else.
|
|
10
|
+
|
|
11
|
+
**Editing in place.** On a board the column you add to is the context, so cards are added and edited where they are, not in a Dialog.
|
|
12
|
+
|
|
13
|
+
- **Add.** With `onCreate(title)` on a `BoardColumn`, the column ends in an add slot: a dashed well (an opening) named “Add a card to To do”, deepening to a trench under the pointer. Activating it raises it into a new card (static, level 1) holding a “Card title” field. Enter creates the card and leaves a fresh, empty field, so several can be added in a row; Escape, or Enter on an empty field, closes it and returns focus to the slot. A pending card hovers after `dur-grace`; a refused one drops to a trench with its title kept and Retry. Resolve `{ ok: true, value: { id } }` after the card is among the children, or `{ ok: false, error }`. A collapsed column shows no add slot. Announced: “Added Fix login to To do, position 4 of 4.”
|
|
14
|
+
- **Rename.** With `onRename(draft)` (InlineEdit's contract) and a `label`, F2 or the card's visible Rename button turns the title into a field, selected. Enter saves, Escape cancels, a refusal keeps the draft with its error, and focus returns to the card. While the field is open, Space and Enter type into it and don't pick the card up; Space and Enter on the card itself still pick it up, so renaming has F2 and a button rather than Enter.
|
|
15
|
+
- **Edit fields.** With `value`, `renderEditor(field)` and `onSave` (EditRegion's contract), the card's Edit button grows it into an `EditRegion` in place, and the cards below are pushed down. `onValueChange` publishes the saved record. A card being edited can't be picked up.
|
|
16
|
+
|
|
17
|
+
**Keyboard and screen readers.** Each column is a `section` named by its title (an `h3`), with its card count. Cards are focusable buttons with `aria-roledescription="draggable card"` and instructions attached. Space or Enter picks a card up (`aria-pressed`); ↑ and ↓ move it within its column, ← and → carry it to the next open column; Space drops it; Escape puts it back where it started. Every step is announced in an assertive live region ("Review, position 2 of 3"). A card held by a collaborator says so instead of lifting. Moving focus away puts a held card down where it is. Under reduced motion cards don't travel or tilt; their levels still change. Forced colours dash the open columns and outline the target in Highlight.
|
|
18
|
+
|
|
19
|
+
| Part | |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `Board` | `onMove({ cardId, from, to })`, where `from` and `to` are `{ column, index }`; you update your data and the board carries the cards |
|
|
22
|
+
| `BoardColumn` | `id`, `title`, `collapsible`, `collapsed`/`defaultCollapsed`/`onCollapsedChange`, `onCreate`, `addLabel` |
|
|
23
|
+
| `BoardCard` | `id`, `label` (for announcements), `heldBy`, `settle`, `onRename`, `value`/`renderEditor`/`onSave`/`onValueChange`; anything inside it |
|
|
24
|
+
|
|
25
|
+
**On touch.** A swipe scrolls the board; a press-and-hold picks a card up, then a drag moves it. The card tools are always shown where there's no hover.
|
|
26
|
+
|
|
27
|
+
**You provide:** the data and an `onMove` that moves the card in it (splice it out of `from.column` at `from.index`, into `to.column` at `to.index`); card content (a title, then a `small` line of detail); short column titles.
|
|
28
|
+
|
|
29
|
+
- Do keep buttons and links inside cards: pressing them doesn't pick the card up.
|
|
30
|
+
- Do collapse finished columns rather than hiding them; the pile keeps their weight visible.
|
|
31
|
+
- Don't use a board for a single list you reorder; that's SortableList.
|
|
32
|
+
|
|
33
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
`Board` as first used in its preview:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { Board, BoardCard, BoardColumn, Field, Input } from 'altitude-design';
|
|
40
|
+
|
|
41
|
+
<Board onMove={move} aria-label="Launch board">
|
|
42
|
+
{Object.keys(titles).map(k => <BoardColumn key={k} id={k} title={titles[k]} collapsible={k === "done"} defaultCollapsed={k === "done"} onCreate={k === "done" ? undefined : create(k)}>
|
|
43
|
+
{cols[k].map(c => <BoardCard key={c.id} id={c.id} label={c.t} heldBy={c.held} onRename={rename(k, c.id)}
|
|
44
|
+
{...(k === "review" ? { value: { title: c.t, meta: c.m }, onValueChange: update(k, c.id),
|
|
45
|
+
renderEditor: field => <><Field label="Title"><Input {...field("title")} size="sm" /></Field><Field label="Details"><Input {...field("meta")} size="sm" /></Field></>,
|
|
46
|
+
onSave: async draft => { await wait(200); return refuse(draft.title) || { ok: true, value: draft }; } } : {})}>
|
|
47
|
+
{c.t}<small>{c.m}</small>
|
|
48
|
+
</BoardCard>)}
|
|
49
|
+
</BoardColumn>)}
|
|
50
|
+
</Board>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import * as React from 'react';
|
|
57
|
+
import { Board, BoardCard, BoardColumn, Field, Input } from 'altitude-design';
|
|
58
|
+
|
|
59
|
+
const start = {
|
|
60
|
+
todo: [{ id: "c1", t: "Write release notes", m: "Docs \xB7 Jon" }, { id: "c2", t: "Fix cold-start regression", m: "Runtime \xB7 Lena" }, { id: "c3", t: "Plan Q1 roadmap", m: "Planning \xB7 Ada" }],
|
|
61
|
+
doing: [{ id: "c4", t: "Pricing page copy", m: "Marketing \xB7 Mara", held: { name: "Mara", presence: 1 } }, { id: "c5", t: "Support macros", m: "Support \xB7 Ivy" }],
|
|
62
|
+
review: [{ id: "c6", t: "Brand voice", m: "Design \xB7 Ada" }],
|
|
63
|
+
done: Array.from({ length: 12 }, (_, i) => ({ id: "d" + i, t: "Shipped item " + (i + 1), m: "Done" }))
|
|
64
|
+
};
|
|
65
|
+
const titles = { todo: "To do", doing: "In progress", review: "Review", done: "Done" };
|
|
66
|
+
export default function App() {
|
|
67
|
+
const [cols, setCols] = React.useState(start);
|
|
68
|
+
const [last, setLast] = React.useState(null);
|
|
69
|
+
const seq = React.useRef(0);
|
|
70
|
+
const move = ({ cardId, from, to }) => setCols((c) => {
|
|
71
|
+
const next = Object.fromEntries(Object.entries(c).map(([k, v]) => [k, v.slice()]));
|
|
72
|
+
const [card] = next[from.column].splice(from.index, 1);
|
|
73
|
+
next[to.column].splice(to.index, 0, card);
|
|
74
|
+
setLast(`${card.t} \u2192 ${titles[to.column]}`);
|
|
75
|
+
return next;
|
|
76
|
+
});
|
|
77
|
+
const wait = ms => new Promise(r => setTimeout(r, ms));
|
|
78
|
+
// The demo service refuses titles with “TBD” in them.
|
|
79
|
+
const refuse = t => /tbd/i.test(t) ? { ok: false, error: "Write the task, not \u201cTBD\u201d." } : null;
|
|
80
|
+
const create = k => async t => { await wait(250); const no = refuse(t); if (no) return no; const card = { id: "n" + ++seq.current, t, m: "New" }; setCols(c => ({ ...c, [k]: [...c[k], card] })); return { ok: true, value: { id: card.id } }; };
|
|
81
|
+
const rename = (k, id) => async t => { await wait(200); const no = refuse(t); if (no) return no; setCols(c => ({ ...c, [k]: c[k].map(x => x.id === id ? { ...x, t } : x) })); return { ok: true, value: t }; };
|
|
82
|
+
const update = (k, id) => v => setCols(c => ({ ...c, [k]: c[k].map(x => x.id === id ? { ...x, t: v.title, m: v.meta } : x) }));
|
|
83
|
+
return <div className="col" style={{ gap: 4 }}>
|
|
84
|
+
<Board onMove={move} aria-label="Launch board">
|
|
85
|
+
{Object.keys(titles).map(k => <BoardColumn key={k} id={k} title={titles[k]} collapsible={k === "done"} defaultCollapsed={k === "done"} onCreate={k === "done" ? undefined : create(k)}>
|
|
86
|
+
{cols[k].map(c => <BoardCard key={c.id} id={c.id} label={c.t} heldBy={c.held} onRename={rename(k, c.id)}
|
|
87
|
+
{...(k === "review" ? { value: { title: c.t, meta: c.m }, onValueChange: update(k, c.id),
|
|
88
|
+
renderEditor: field => <><Field label="Title"><Input {...field("title")} size="sm" /></Field><Field label="Details"><Input {...field("meta")} size="sm" /></Field></>,
|
|
89
|
+
onSave: async draft => { await wait(200); return refuse(draft.title) || { ok: true, value: draft }; } } : {})}>
|
|
90
|
+
{c.t}<small>{c.m}</small>
|
|
91
|
+
</BoardCard>)}
|
|
92
|
+
</BoardColumn>)}
|
|
93
|
+
</Board>
|
|
94
|
+
<p className="cap">{last ? `Moved: ${last}.` : "Drag a card, or focus one and press Space, then the arrow keys. F2 renames a card; each column adds cards at its foot. Done is collapsed into a pile and still takes cards."}</p>
|
|
95
|
+
</div>;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
<!-- /example -->
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Breadcrumb
|
|
2
|
+
|
|
3
|
+
Where you are in an app with routes: Settings › Members › Jane Doe, or Projects › Atlas › Issues.
|
|
4
|
+
|
|
5
|
+
**Elevation: a staircase.** It's `Drilldown`'s breadcrumb, now available on its own, and the two share one implementation. Every step stands on the same ground line, outlined face and wall together, and **each deeper place is a taller step** (its wall 3px longer per level). Steps you can go back to are live: they lift one level when pointed at and press to the ground. The current page is the last and tallest step, in weight 600, and it doesn't press. A route-based app has no stack of views, so there's no pile under it, and **a route change re-renders it without motion**: a new route isn't an arrival.
|
|
6
|
+
|
|
7
|
+
**Deep paths.** With `maxItems`, a longer path keeps its first step and its last `maxItems − 1`. The middle waits behind a "…" step that opens a `DropdownMenu`, which floats at level 3.
|
|
8
|
+
|
|
9
|
+
**No separators.** Each step's height already says it's deeper than the one before, so there are no slashes or chevrons. `BreadcrumbSeparator` exists for code written for shadcn/ui and renders nothing.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The [breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/): a `nav` labelled "Breadcrumb", an ordered list, and `aria-current="page"` on the last step. The "…" step is a menu button named "Show 3 more levels". Under forced colours steps keep a 1px outline and the current step a 2px one. Under `contrast="more"` each step's wall is `ink`.
|
|
12
|
+
|
|
13
|
+
**Props.**
|
|
14
|
+
- `items` (`{ label, href?, onClick? }[]`; the last entry is the current page).
|
|
15
|
+
- `maxItems`.
|
|
16
|
+
- `renderLink(item, props)` for a router's link: spread `props` onto it.
|
|
17
|
+
- `onNavigate(item)` for steps chosen from the "…" menu (default: follow `href`).
|
|
18
|
+
- **Composable parts:** `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbPage` and `BreadcrumbEllipsis` mirror shadcn/ui's for full control.
|
|
19
|
+
|
|
20
|
+
**Fitting the width.** `fit="collapse"` (the default) keeps the trail on one line: when it doesn't fit, steps from the second onward move behind "…" until it does, keeping the first step and the current page, and they come back when there's room. `fit="wrap"` lets it wrap instead. `maxItems` still caps it by count.
|
|
21
|
+
|
|
22
|
+
**You provide:** the path, from the top of the app to here.
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<Breadcrumb
|
|
26
|
+
items={[{ label: 'Settings', href: '/settings' }, { label: 'Members', href: '/settings/members' }, { label: 'Jane Doe' }]}
|
|
27
|
+
maxItems={4}
|
|
28
|
+
renderLink={(item, props) => <NextLink href={item.href!} {...props}>{item.label}</NextLink>}
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Do end with the current page, as plain text.
|
|
33
|
+
- Do use it for location in a hierarchy, not for history ("back").
|
|
34
|
+
- Don't add separators between steps.
|
|
35
|
+
- Don't use it inside a `Drilldown`, which has its own.
|
|
36
|
+
|
|
37
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
38
|
+
## Example
|
|
39
|
+
|
|
40
|
+
`Breadcrumb` as first used in its preview:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { Breadcrumb } from 'altitude-design';
|
|
44
|
+
|
|
45
|
+
<Breadcrumb items={[{ label: 'Settings', href: '#/settings' }, { label: 'Members', href: '#/settings/members' }, { label: 'Jane Doe' }]} />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import * as React from 'react';
|
|
52
|
+
import { Breadcrumb } from 'altitude-design';
|
|
53
|
+
|
|
54
|
+
const RouterLink = React.forwardRef(({ to, ...rest }, ref) => <a ref={ref} href={'#' + to} {...rest} />);
|
|
55
|
+
export default function App() {
|
|
56
|
+
return (
|
|
57
|
+
<div className="col">
|
|
58
|
+
<Breadcrumb items={[{ label: 'Settings', href: '#/settings' }, { label: 'Members', href: '#/settings/members' }, { label: 'Jane Doe' }]} />
|
|
59
|
+
<Breadcrumb maxItems={4} renderLink={(item, props) => <RouterLink to={item.href.slice(1)} {...props}>{item.label}</RouterLink>}
|
|
60
|
+
items={[{ label: 'Projects', href: '#/projects' }, { label: 'Atlas', href: '#/projects/atlas' }, { label: 'Q4 launch', href: '#/projects/atlas/q4' }, { label: 'Issues', href: '#/projects/atlas/q4/issues' }, { label: 'Billing page', href: '#/projects/atlas/q4/issues/212' }, { label: 'Comments' }]} />
|
|
61
|
+
<p className="cap">Each step you can go back to lifts when pointed at; the current page is the tallest step and doesn't press. A deep path keeps its first and last steps, with the middle behind "…".</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
<!-- /example -->
|