altitude-design 0.3.2 → 0.4.0

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.
Files changed (136) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/altitude.mjs +120 -20
  3. package/dist/guidance/README.md +4 -0
  4. package/dist/guidance/agent-setup.md +3 -2
  5. package/dist/guidance/components/AILabel/README.md +25 -0
  6. package/dist/guidance/components/Accordion/README.md +7 -0
  7. package/dist/guidance/components/AgentFleet/README.md +34 -0
  8. package/dist/guidance/components/AgentLanes/README.md +21 -0
  9. package/dist/guidance/components/AgentRequests/README.md +29 -0
  10. package/dist/guidance/components/Alert/README.md +22 -0
  11. package/dist/guidance/components/AltitudeProvider/README.md +48 -0
  12. package/dist/guidance/components/AppShell/README.md +51 -0
  13. package/dist/guidance/components/AreaChart/README.md +43 -0
  14. package/dist/guidance/components/Artifact/README.md +30 -0
  15. package/dist/guidance/components/Avatar/README.md +34 -0
  16. package/dist/guidance/components/Await/README.md +80 -0
  17. package/dist/guidance/components/Badge/README.md +22 -0
  18. package/dist/guidance/components/BarChart/README.md +47 -0
  19. package/dist/guidance/components/BarList/README.md +29 -0
  20. package/dist/guidance/components/Board/README.md +31 -0
  21. package/dist/guidance/components/Breadcrumb/README.md +35 -0
  22. package/dist/guidance/components/Button/README.md +17 -0
  23. package/dist/guidance/components/Card/README.md +14 -0
  24. package/dist/guidance/components/ChartBrush/README.md +41 -0
  25. package/dist/guidance/components/ChartContainer/README.md +152 -0
  26. package/dist/guidance/components/ChartLegend/README.md +35 -0
  27. package/dist/guidance/components/ChartReference/README.md +41 -0
  28. package/dist/guidance/components/ChartTooltip/README.md +38 -0
  29. package/dist/guidance/components/Checkbox/README.md +11 -0
  30. package/dist/guidance/components/Checklist/README.md +71 -0
  31. package/dist/guidance/components/CodeBlock/README.md +28 -0
  32. package/dist/guidance/components/ColorPicker/README.md +30 -0
  33. package/dist/guidance/components/Combobox/README.md +16 -0
  34. package/dist/guidance/components/CommandPalette/README.md +27 -0
  35. package/dist/guidance/components/CommentThread/README.md +30 -0
  36. package/dist/guidance/components/Confirmation/README.md +34 -0
  37. package/dist/guidance/components/ContextMenu/README.md +30 -0
  38. package/dist/guidance/components/Conversation/README.md +45 -0
  39. package/dist/guidance/components/CopyButton/README.md +23 -0
  40. package/dist/guidance/components/DataGrid/README.md +82 -0
  41. package/dist/guidance/components/DatePicker/README.md +67 -0
  42. package/dist/guidance/components/Dialog/README.md +29 -0
  43. package/dist/guidance/components/DiffView/README.md +30 -0
  44. package/dist/guidance/components/DotPlot/README.md +38 -0
  45. package/dist/guidance/components/Drilldown/README.md +15 -0
  46. package/dist/guidance/components/DropZone/README.md +21 -0
  47. package/dist/guidance/components/DropdownMenu/README.md +19 -0
  48. package/dist/guidance/components/EditRegion/README.md +104 -0
  49. package/dist/guidance/components/EmptyState/README.md +17 -0
  50. package/dist/guidance/components/FeatureGate/README.md +28 -0
  51. package/dist/guidance/components/Field/README.md +10 -0
  52. package/dist/guidance/components/FilterBar/README.md +69 -0
  53. package/dist/guidance/components/FormattedNumber/README.md +37 -0
  54. package/dist/guidance/components/Heatmap/README.md +45 -0
  55. package/dist/guidance/components/HoldButton/README.md +16 -0
  56. package/dist/guidance/components/HoverCard/README.md +18 -0
  57. package/dist/guidance/components/InlineCitation/README.md +24 -0
  58. package/dist/guidance/components/InlineEdit/README.md +133 -0
  59. package/dist/guidance/components/Input/README.md +27 -0
  60. package/dist/guidance/components/InputOTP/README.md +30 -0
  61. package/dist/guidance/components/ItemGroup/README.md +93 -0
  62. package/dist/guidance/components/Kbd/README.md +9 -0
  63. package/dist/guidance/components/LineChart/README.md +47 -0
  64. package/dist/guidance/components/Link/README.md +28 -0
  65. package/dist/guidance/components/Markdown/README.md +32 -0
  66. package/dist/guidance/components/Meter/README.md +29 -0
  67. package/dist/guidance/components/NotificationCenter/README.md +28 -0
  68. package/dist/guidance/components/NumberInput/README.md +29 -0
  69. package/dist/guidance/components/PageHeader/README.md +29 -0
  70. package/dist/guidance/components/Pagination/README.md +25 -0
  71. package/dist/guidance/components/PasswordInput/README.md +34 -0
  72. package/dist/guidance/components/PermissionMatrix/README.md +28 -0
  73. package/dist/guidance/components/Pile/README.md +12 -0
  74. package/dist/guidance/components/PlanPicker/README.md +25 -0
  75. package/dist/guidance/components/Popover/README.md +12 -0
  76. package/dist/guidance/components/Progress/README.md +10 -0
  77. package/dist/guidance/components/PromptInput/README.md +40 -0
  78. package/dist/guidance/components/PropertyList/README.md +57 -0
  79. package/dist/guidance/components/Question/README.md +27 -0
  80. package/dist/guidance/components/RadioGroup/README.md +10 -0
  81. package/dist/guidance/components/RelativeTime/README.md +26 -0
  82. package/dist/guidance/components/RemovalReceipt/README.md +34 -0
  83. package/dist/guidance/components/Resizable/README.md +36 -0
  84. package/dist/guidance/components/SaveBar/README.md +27 -0
  85. package/dist/guidance/components/SavedViews/README.md +24 -0
  86. package/dist/guidance/components/ScatterChart/README.md +41 -0
  87. package/dist/guidance/components/Schedule/README.md +76 -0
  88. package/dist/guidance/components/ScrollArea/README.md +7 -0
  89. package/dist/guidance/components/SecretField/README.md +28 -0
  90. package/dist/guidance/components/Select/README.md +27 -0
  91. package/dist/guidance/components/Separator/README.md +26 -0
  92. package/dist/guidance/components/Sheet/README.md +32 -0
  93. package/dist/guidance/components/Skeleton/README.md +14 -0
  94. package/dist/guidance/components/Slider/README.md +25 -0
  95. package/dist/guidance/components/SortableList/README.md +11 -0
  96. package/dist/guidance/components/Sparkline/README.md +29 -0
  97. package/dist/guidance/components/SplitButton/README.md +26 -0
  98. package/dist/guidance/components/Stat/README.md +31 -0
  99. package/dist/guidance/components/Stepper/README.md +29 -0
  100. package/dist/guidance/components/Suggestions/README.md +29 -0
  101. package/dist/guidance/components/Surface/README.md +20 -0
  102. package/dist/guidance/components/Switch/README.md +9 -0
  103. package/dist/guidance/components/SyncIndicator/README.md +24 -0
  104. package/dist/guidance/components/Table/README.md +76 -0
  105. package/dist/guidance/components/Tabs/README.md +18 -0
  106. package/dist/guidance/components/TagInput/README.md +66 -0
  107. package/dist/guidance/components/Terminal/README.md +27 -0
  108. package/dist/guidance/components/ThreadList/README.md +37 -0
  109. package/dist/guidance/components/TimeInput/README.md +26 -0
  110. package/dist/guidance/components/Timeline/README.md +32 -0
  111. package/dist/guidance/components/Toaster/README.md +16 -0
  112. package/dist/guidance/components/Toggle/README.md +33 -0
  113. package/dist/guidance/components/Toolbar/README.md +37 -0
  114. package/dist/guidance/components/Tooltip/README.md +12 -0
  115. package/dist/guidance/components/Tour/README.md +29 -0
  116. package/dist/guidance/components/Trace/README.md +37 -0
  117. package/dist/guidance/components/Tree/README.md +86 -0
  118. package/dist/guidance/evidence.json +18 -0
  119. package/dist/guidance/index.json +811 -7
  120. package/dist/guidance/llms.txt +153 -2
  121. package/dist/guidance/manifest.json +133 -12
  122. package/dist/guidance/patterns/screen-layout.md +1 -1
  123. package/dist/guidance/references/build/checks/agent-setup.mjs +7 -0
  124. package/dist/guidance/references/build/checks/guidance.mjs +8 -2
  125. package/dist/guidance/references/build/checks/source-check.mjs +101 -0
  126. package/dist/guidance/rules.json +155 -0
  127. package/dist/guidance/visual/ActionsInPlace.md +463 -0
  128. package/dist/guidance/visual/Charts.md +119 -0
  129. package/dist/guidance/visual/Distinctiveness.md +106 -0
  130. package/dist/guidance/visual/Elevation.md +1 -1
  131. package/dist/guidance/visual/Mobile.md +64 -0
  132. package/dist/guidance/visual/Motion.md +225 -0
  133. package/dist/guidance/visual/README.md +98 -0
  134. package/dist/guidance/visual/Research.md +1 -1
  135. package/dist/guidance/visual/Theming.md +349 -0
  136. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ # Schedule
2
+
3
+ A day or week of time whose events you handle directly: drag across empty time to create one, carry an event to move it, pull its bottom edge to change its length, rename it in place. Room bookings, shift rosters, content calendars. Every one of those has a keyboard path, so nothing needs a pointer (WCAG 2.2 SC 2.5.7, Dragging Movements).
4
+
5
+ ```tsx
6
+ <Schedule<Booking>
7
+ aria-label="Room bookings"
8
+ view="week" // 'day' | 'week' | 'workweek'
9
+ date={weekOf} onDateChange={setWeekOf}
10
+ events={bookings}
11
+ getKey={(b) => b.id} getLabel={(b) => b.title}
12
+ getStart={(b) => b.start} getEnd={(b) => b.end} // 'YYYY-MM-DDTHH:mm'
13
+ step={15} dayStart="08:00" dayEnd="19:00"
14
+ onCreate={async ({ start, end, title }) => { const r = await api.book({ start, end, title }); if (r.ok) setBookings(r.all); return r.ok ? { ok: true, value: r.booking } : r; }}
15
+ onChange={async ({ id, start, end }, previous) => { const r = await api.reschedule(id, start, end, previous.revision); if (r.ok) setBookings(r.all); return r; }}
16
+ onRename={…} onOpen={(b) => setSheetFor(b)} onRemove={…}
17
+ heldBy={(b) => presence[b.id]}
18
+ isUnavailable={({ start, end }) => outsideOpeningHours(start, end)}
19
+ />
20
+ ```
21
+
22
+ Times are local date-times, `'YYYY-MM-DDTHH:mm'` strings with no zone, matching `DatePicker`'s ISO dates. Convert to and from your zone at your edges (DST included). Writes follow the `InlineEdit` contract: resolve `{ ok: true }` (for a create, `{ ok: true, value }`) after `events` reflects the change, or `{ ok: false, error }`. One write runs at a time.
23
+
24
+ ## Elevation
25
+
26
+ - **The grid** is ground, with `line` rules on the hour and hour labels.
27
+ - **Events** are live work items, raised one level like `BoardCard`s (`surface-raised`, `elev-1`, an accent edge on the left), lifting to `elev-2` on hover. Overlapping events share the day side by side, and their names say so.
28
+ - **Carrying** (Space, or a pointer drag): the event floats at level 3, tilted; the days open into wells, as Board columns do, and the slot under the event deepens to a trench with an accent rim, as long as the event (a critical rim where it can't go).
29
+ - **Drawing** a new event across empty time makes a well the length of the drag; releasing raises it into an event with a title field.
30
+ - **Pending** writes hover at level 2 after `dur-grace`; a **failed** one drops to a trench, the event goes back to its old place, and a bar offers Retry.
31
+ - **Held by a collaborator** (`heldBy`): the event wears their `presence-*` outline and name and can't be picked up.
32
+ - **The now line** is `accent-ink`, not `critical`: it isn't status.
33
+ - **A new week** cross-fades; it isn't a change of level.
34
+
35
+ Without height: every event is a button named with its title, day, start and end, its overlaps and who holds it; carrying is `aria-pressed`, and each step is announced in an assertive region (“Wednesday 28 October, 15:15 to 16:15”); writes and errors are announced politely. Forced colours: events 1px, carried 2px, slots and drawn spans thick dashed, pending dotted, failed thick dashed, the now line Highlight. `contrast="more"`: events get the level-1 slab, the carried event a 3px slab.
36
+
37
+ ## Keyboard
38
+
39
+ The grid (`role="grid"`) is one Tab stop: rows are time slots, columns are days.
40
+
41
+ | Where | Key | |
42
+ |---|---|---|
43
+ | Grid | ↑ / ↓, ← / → | a slot, a day (mirrored in RTL); an event starting there takes focus |
44
+ | | Home / End, PageUp / PageDown | the day's first and last slot; the previous and next week (or day) |
45
+ | | Enter | a new event here, `defaultDuration` long: type a title, Enter adds it, Escape discards it |
46
+ | Event | Enter | `onOpen` |
47
+ | | F2 | rename in place |
48
+ | | Space | pick up: ↑/↓ move by `step`, ←/→ by a day, Shift+↑/↓ change the end; Space or Enter drop; Escape cancel |
49
+ | | Shift+↑ / Shift+↓ | change the end by a step without picking up |
50
+ | | Delete | `onRemove` |
51
+
52
+ ## Props
53
+
54
+ | Prop | |
55
+ |---|---|
56
+ | `view`, `date`, `onDateChange`, `weekStartsOn` | what's shown; `onDateChange` adds Previous, Today and Next |
57
+ | `events`, `getKey`, `getStart`, `getEnd`, `getLabel`, `renderEvent` | the events |
58
+ | `step`, `dayStart`, `dayEnd`, `hourHeight`, `defaultDuration` | 15 minutes, '08:00', '19:00', 48px, 60 minutes |
59
+ | `onCreate`, `onChange`, `onRename`, `onOpen`, `onRemove` | the writes and actions |
60
+ | `heldBy`, `isUnavailable`, `now`, `locale`, `aria-label` | collaborators' holds, refused spans, the now line's time |
61
+
62
+ ## You provide
63
+
64
+ The events and their writes (with concurrency guards), time zones, and the details view (`onOpen`, in a `Sheet`). Month view and all-day events aren't covered.
65
+
66
+ ## Do and don't
67
+
68
+ - **Do** show details beside the schedule (a `Sheet`), so the week stays in view.
69
+ - **Do** refuse impossible spans with `isUnavailable`, so people see a critical rim before they drop.
70
+ - **Don't** use it for a list of dated items with no durations; that's a table.
71
+
72
+ ## Demo and verification
73
+
74
+ The `Schedule` section of `build/previews.txt` is a room-booking work week (26–30 Oct 2026): the service refuses Wednesday mornings, rooms close at 17:00 on Friday, and Ana holds the Retro. `node checks/schedule.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers the grid keys and names, creating, carrying, resizing and cancelling without a pointer, a refused move with Retry, unavailable time, a collaborator's hold, rename, remove, the next week, pointer carry, drawing and the resize handle, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
75
+ **Narrow and on touch.** Under 480px of its own width the schedule shows one day at a time (`narrowView="day"`, the default; `false` keeps the week), paged by the header's previous and next. Under a coarse pointer an hour is 96px unless you set `hourHeight`, so a 15-minute slot is 24px tall, and a tap opens an event.
76
+
@@ -0,0 +1,7 @@
1
+ # ScrollArea
2
+
3
+ A scrolling region whose edges show where content continues, so it never looks finished when it isn't.
4
+
5
+ When content continues above or below, that edge gets an inner lip (`lip-top`, `lip-bottom`), as if the content curves away under the rim. With a `header`, the header sits flush at the top and rises to `elev-2` as soon as content scrolls beneath it, then settles back when you return to the top. Both counter the illusion of completeness: people stop scrolling when a region looks complete.
6
+
7
+ **You provide:** children, `maxHeight` (px or any CSS length), an optional `header`, and an `aria-label` (the body is keyboard-focusable so it can be scrolled with arrow keys).
@@ -0,0 +1,28 @@
1
+ # SecretField
2
+
3
+ A key or token: an API key, a webhook signing secret, a personal access token. It follows the flow Stripe, GitHub and Vercel share: the full secret is shown **once**, with copy and a warning, and afterwards only a masked tail (`sk_live_••••4f2a`) with reveal (if allowed), rotate and revoke.
4
+
5
+ **Elevation: two readings.**
6
+ - **A new secret is an opening**, something you must take away. It's a sunk well in mono, which still shows its back wall although it holds text, with a raised "Copy key" beside it and a `caution` warning ("You won't see this key again."). It stays open until the person checks "I've saved this key". Then its floor rises flush, the secret masks to its prefix and tail, and focus moves to the words that replace the checkbox.
7
+ - **A stored secret is settled.** It's flush, masked, and part of the record. A reveal `Toggle` shows it again if your app allows that (`onReveal`). "Rotate key" is a secondary button. Revoking is a `HoldButton`: consequence, pushed through the ground.
8
+
9
+ **Accessibility.** The field is a `role="group"` named by `label`. A shown secret is a read-only input (`aria-readonly`) that selects itself on focus, described by the warning. **A masked secret never reads its bullets**: it's announced as "Secret key, ending in 4f2a". The reveal toggle keeps its name ("Show key") with `aria-pressed`. The acknowledgement is a `Checkbox`, and you gate closing on `onAcknowledge`. Under forced colours the new secret's well is dashed and the stored one solid.
10
+
11
+ **Props:** `label`; `noun` (default "key"); for a new secret, `value`, `once`, `onAcknowledge`, `acknowledgeLabel`, `warning`; for a stored one, `prefix`, `maskedTail`, `onReveal` (returns the secret, or a promise of it), `onRotate`, `onRevoke`.
12
+
13
+ **You provide:** the secret (once) or its tail, and what rotate and revoke do. You also keep the dialog open until `onAcknowledge`.
14
+
15
+ ```tsx
16
+ // Just created
17
+ <SecretField label="Secret key" value={created.secret} once onAcknowledge={() => setSaved(true)} />
18
+ <Button variant="primary" disabled={!saved} onClick={close}>Done</Button>
19
+
20
+ // Stored
21
+ <SecretField label="Secret key" prefix="sk_live_" maskedTail="4f2a"
22
+ onReveal={canReveal ? fetchSecret : undefined} onRotate={rotate} onRevoke={revoke} />
23
+ ```
24
+
25
+ - Do keep a new secret's dialog open until it's acknowledged.
26
+ - Do leave out `onReveal` for secrets you don't store in a retrievable form.
27
+ - Don't show a stored secret in full by default.
28
+ - Don't put revoke one click away; it's held.
@@ -0,0 +1,27 @@
1
+ # Select
2
+
3
+ Chooses one value from a list: an opening you fill by choosing, with the list rising from its edge at level 3.
4
+
5
+ **Why an opening.** In Altitude, things you press are raised and things you fill are sunk. Choosing a value fills a field, so a Select is built like `Input`, not like a button. While nothing is chosen it's a well with its back wall showing (`--alt-wall-in`) and its placeholder sitting on the floor; as the pointer approaches, the wall deepens. Once a value is chosen the floor rises flush (`surface-raised`, no wall), so a form's unanswered selects read as holes at a glance, exactly like its empty inputs. When it's invalid it drops to trench depth on `critical-soft`. A chevron says it opens.
6
+
7
+ **The list.** It rises from the field's edge to level 3 like Popover and DropdownMenu (6px from 96% scale, its shadow growing to `elev-3`), opening upward instead when there's more room above. One `accent-soft` highlight is carried between options on `ease-settle`. The chosen option wears the Checkbox family's tick (the same path and stroke, in `accent-ink`), drawn in as the list opens. Choosing sinks the list back, raises the floor flush and lets the new value settle in with a brief `accent-soft` afterglow (a fifth of `dur-settle`).
8
+
9
+ | State | Looks like |
10
+ |---|---|
11
+ | Empty | a well with its back wall showing; the placeholder in `ink-muted` |
12
+ | Chosen | flush with the page; the value in `ink` |
13
+ | Open or focused | the lit floor of a focused Input; reached by keyboard, also its 2px neutral rim (kept while the list has focus); an empty field holds its approached depth; the list at level 3 |
14
+ | Invalid | trench depth on `critical-soft` with a `critical` rim |
15
+ | Disabled | flat on the ground, `ink-faint` |
16
+
17
+ **Keyboard and screen readers.** The WAI-ARIA listbox pattern with a button trigger. The trigger is a `button` with `aria-haspopup="listbox"` and `aria-expanded`; ↓, ↑, Enter or Space open the list and focus moves into it, where `aria-activedescendant` names the highlighted option and `aria-selected` the chosen one. ↑ and ↓ move, Home and End jump to the ends, typing letters jumps to the next option that starts with them (also on the closed trigger, which opens at the match), Enter or Space chooses, and Escape closes without changing the value. Disabled options are skipped. The list is named by the field's label. In forced colours the highlighted option has a Highlight rim, empty fields a dashed rim and chosen ones a solid rim.
18
+
19
+ **As a sheet.** `presentation="auto"` (the default) opens the list as a sheet at the bottom edge on a narrow touch screen, titled by the field's label, with Close and 44px rows; a tap outside closes it too. `"anchored"` or `"sheet"` forces one.
20
+
21
+ **You provide:** options as `items` (`{ value, label, description?, icon?, disabled? }`) or `SelectItem` children; `value`/`onValueChange` or `defaultValue`; a `placeholder` that says what to choose ("Choose a region"); `name` to submit it with a form (a hidden native select carries the value and participates in constraint validation). Wrap it in `Field` for a label, hint and error: Field passes the id, the description and `invalid` through.
22
+
23
+ `required` rejects an empty value through native form validation, including without a `name`; invalid submission focuses the visible trigger, marks it invalid and associates a visible “Choose an option.” alert. Set `requiredMessage` to localize or clarify that instruction. A supplied `Field` error takes precedence so the same issue is not announced twice. Valid selection and form reset clear this native-validation feedback. Disabled selects are excluded from validation and form data. Form reset restores `defaultValue` for uncontrolled selects and calls `onValueChange` with that reset value. Controlled selects retain their application-owned value; reset that state in the form’s reset handler if desired. A cancelled form reset leaves the selection untouched.
24
+
25
+ - Do use a RadioGroup instead when there are five options or fewer: people can see and compare them without opening anything.
26
+ - Do give a non-string `label` a `textValue`, so typeahead can find it.
27
+ - Don't use it for actions; that's a DropdownMenu.
@@ -0,0 +1,26 @@
1
+ # Separator
2
+
3
+ A divider: between sections in a card, groups in a toolbar, items in a sidebar, or "or" between two ways to sign in.
4
+
5
+ **Elevation.** A divider is part of the page, so it lies on the ground: 1px of `line`, with no shadow and no wall. `line` is for decorative dividers. `line-control` is for the edges of controls, so a separator never uses it except under `contrast="more"`. Don't draw an `<hr>` with your own border colour: "never invent an in-between gray".
6
+
7
+ **Semantics.** By default it's `role="separator"` with `aria-orientation`, so a screen reader knows the content is parted. Set `decorative` when the layout already says it (a line between groups of toolbar buttons): it becomes `role="none"` and is skipped. A labelled separator (`label="or"`) is the word in `ink-muted` between two drawn lines, and it's read as plain text. `role="separator"` would make its children presentational and lose the word.
8
+
9
+ **Orientation.** `horizontal` fills its container's width. `vertical` stretches to the height of the row it's in (at least 1em), so put it in a flex row.
10
+
11
+ **Accessibility.** Under forced colours it's 1px of `CanvasText`. Under `contrast="more"` the lines are `line-control`.
12
+
13
+ **Props:** `orientation` (`'horizontal' | 'vertical'`, default horizontal); `decorative`; `label`.
14
+
15
+ **You provide:** where it goes, and whether it says something (leave `decorative` off) or only decorates.
16
+
17
+ ```tsx
18
+ <Separator />
19
+ <Separator orientation="vertical" decorative />
20
+ <Separator label="or" />
21
+ ```
22
+
23
+ - Do use it instead of a border on one side of a section.
24
+ - Do make a separator between groups of toolbar buttons `decorative`.
25
+ - Don't use it to fake a table's row lines; `Table` and `ItemGroup` draw their own (`ItemSeparator`).
26
+ - Don't give it a colour, a thickness or a shadow.
@@ -0,0 +1,32 @@
1
+ # Sheet
2
+
3
+ A large panel laid over the page at level 4, anchored to an edge, for a task that needs more room than a popover and less commitment than a page.
4
+
5
+ **Why level 4.** A sheet is a whole working surface above the page: higher than menus and popovers (level 3), because it holds a task rather than one choice, and below the summit (level 5), because it doesn't demand an answer. It's drawn with `surface-overlay` and `elev-4`, the toasts' tier.
6
+
7
+ **Risen at its edge, not slid in.** Nothing in Altitude slides in from the side. A sheet appears where it's anchored: it travels 16px out of its edge as it fades in, while its shadow grows from `elev-1` to `elev-4` (`dur-travel-4`, `ease-rise`). It stands 8px clear of the edge with `radius-xl` corners, so its own edges, and the shadow it casts below them, read as height. A modal sheet recedes the page behind it, as a dialog does. Closing lifts it off: it rises 8px and grows 1% as its shadow spreads and it fades, in 70% of the time it took to arrive.
8
+
9
+ **Inside.** The sheet scrolls as one column. `SheetHeader` stays at the top and rises to `elev-2` once content passes under it; `SheetFooter` stays at the bottom, and while there's more below, its top edge curves away (`lip-bottom`), as in ScrollArea.
10
+
11
+ **Bottom sheets** have a finger pull at the top: a small recess, because everything you put something into is an opening in Altitude, and this is where your finger goes. It deepens as the pointer approaches. Drag it and the sheet follows; past a third of its height, or with a flick, it goes; otherwise it springs back. The pull is a convenience: Escape, the close button and the scrim still close it.
12
+
13
+ **Modal or not.** By default a sheet is modal (the WAI-ARIA dialog pattern): the scrim blocks the page, Tab is trapped, Escape closes it and focus returns to what opened it. `modal={false}` makes a side panel that leaves the page usable: no scrim, no receding page, no trap; focus still moves into it, and Escape closes it from inside.
14
+
15
+ | Prop | Default | |
16
+ |---|---|---|
17
+ | `side` | `right` | `right`, `left` or `bottom` |
18
+ | `size` | `md` | width: `sm` 360, `md` 440, `lg` 560px; a bottom sheet 480, 720, 960px |
19
+ | `modal` | `true` | `false`: a non-blocking panel |
20
+ | `dismissible` | `true` | `false`: only your own buttons close it |
21
+
22
+ **On a phone.** It sizes to the visible viewport, not the largest one: with the on-screen keyboard open it shrinks to the part you can see, so its footer and the focused field stay in reach.
23
+
24
+ **You provide:** `open`/`onOpenChange` or a `trigger`, and the content: `SheetHeader` with `SheetTitle` (it names the sheet) and `SheetDescription`, the body, and `SheetFooter` with the actions (cancel as ghost first, the action last).
25
+
26
+ - Do use a sheet to edit an item without leaving the list it's in, and for filters, details and settings.
27
+ - Don't open a sheet from a Dialog or the CommandPalette. A level-4 surface can't sit on the summit: if it's opened under one, it paints beneath it and leaves focus where it is. A Dialog may open from a sheet, to confirm something.
28
+ - Don't stack a sheet on a sheet; replace its content instead.
29
+
30
+ A modal Sheet shares document-scoped isolation, scroll ownership and eligible-focus containment with Dialog. Its locks remain through exit. Opening a Dialog from a Sheet suspends the Sheet, then resumes its focus and existing background policy after the Dialog closes. A Sheet requested beneath a summit cannot take its focus or `aria-modal` ownership. A stationary Dialog also removes the lower Sheet's scrim blur until it closes.
31
+
32
+ `modal={false}` keeps the page interactive and scrollable: no scrim, recession or focus trap. Closing that Sheet does not restore focus over a newer modal.
@@ -0,0 +1,14 @@
1
+ # Skeleton
2
+
3
+ Stands in for loading content as an **imprint**: the shape of what's coming, pressed into the page. When the content arrives it rises out of its imprint and the imprint fills in behind it.
4
+
5
+ Imprints are wells (`surface-well`, inset shadow) because they're openings waiting to receive something. While waiting, a soft band of light passes over them every `dur-sweep`, staggered line by line; it stops under reduced motion. When `loaded` turns true, the children rise from the bottom of the imprint over `dur-travel-3` while the well fades out. Reveals share the attention budget, so if many land at once, no more than four begin together and the rest follow in order.
6
+
7
+ **Or don't.** `Avatar`, `Badge`, `CardTitle`, `CardDescription`, `CardContent` and `DataTable` draw their own imprint while your data is still undefined; see `Await`. Use Skeleton for layouts of your own.
8
+
9
+ **You provide:** the real content as children, `loaded`, and a shape that matches it: `shape="circle"` with a `width`, `shape="text"` with `lines`, or a block with `width` and `height`.
10
+
11
+ - Match the real layout so nothing jumps when content arrives.
12
+ - Don't show imprints for loads under about 300ms; show nothing.
13
+
14
+ Under `AltitudeProvider loading="breath"` (experimental) a Skeleton breathes in depth instead of being swept by light, on the same page clock as every other imprint, and its content fills past the ground when `loaded` turns true. See `Await`.
@@ -0,0 +1,25 @@
1
+ # Slider
2
+
3
+ Picks a number from a range by dragging a raised thumb along a trench.
4
+
5
+ The track is level −2 and the filled part is a raised `accent` bar inside it. The thumb is level 1 with an `accent` rim, level 2 on hover, and floats to level 3 (scaled 12%) while held, with its value floating above it in a bubble at the same height as a tooltip. Keyboard: arrows step, Page Up/Down jump a tenth, Home/End go to the ends.
6
+
7
+ **You provide:** `value`/`onValueChange` or `defaultValue`, `min`, `max`, `step`, an `aria-label`, and `format` for the bubble (`n => n + '%'`).
8
+
9
+ ## A range
10
+
11
+ Pass `value` (or `defaultValue`) as `[low, high]` and the Slider has two thumbs, with the raised `accent` fill running between them in the same trench: a price filter, a latency window.
12
+
13
+ ```tsx
14
+ <Slider value={[lo, hi]} onValueChange={([a, b]) => setRange([a, b])} min={0} max={500} step={5}
15
+ minStepsBetween={2} aria-label={['Minimum price', 'Maximum price']} format={(n) => `$${n}`} />
16
+ ```
17
+
18
+ - **Thumbs can't cross.** A thumb pushed against the other stops there, with no overshoot; `minStepsBetween` keeps them that many steps apart. Two thumbs that meet (with no gap) part in the direction of the first drag.
19
+ - **Drag the fill** to move both thumbs together; the window keeps its width and stops at the ends. The fill lifts to level 2 on hover and shows a grab cursor, because dragging it does something.
20
+ - **Press the track** outside the thumbs and the nearer one moves there and takes focus. Pressing the fill moves neither.
21
+ - **Only the thumb in hand floats** to level 3 with its bubble; the other stays at rest. Panning holds both.
22
+ - **Keyboard**, after the [APG multi-thumb slider](https://www.w3.org/WAI/ARIA/apg/patterns/slider-multithumb/): each thumb is its own Tab stop and `role="slider"`, with `aria-valuemin` and `aria-valuemax` bounded by the other thumb (less the gap). Arrows step, Page Up/Down jump a tenth, Home/End go to that thumb's own bounds. When `format` returns text, it's each thumb's `aria-valuetext` too.
23
+ - **Names.** Give each thumb its own name (`['Minimum price', 'Maximum price']`), or one name for the pair: the thumbs are then "Price, minimum" and "Price, maximum", in a `role="group"` named "Price".
24
+
25
+ - Do show the current value in text near the slider when it matters precisely; the bubble only shows while dragging or focused.
@@ -0,0 +1,11 @@
1
+ # SortableList
2
+
3
+ Reorders a list by picking items up: the item you hold floats in your hand, the slot it will land in opens, and it lands.
4
+
5
+ Pressing and dragging an item lifts it to level 3 (floating surface, `elev-3`), scales it 1.5% and tilts it half a degree; the others glide aside on `ease-settle`, and a well opens where it will land. Releasing animates it into place (FLIP) while its shadow falls back to `elev-1`. Keyboard: focus an item, press Space to pick it up, arrow keys to move it, Space to drop, Escape to cancel; every move is announced.
6
+
7
+ **On touch.** A swipe on a row scrolls; a press-and-hold picks it up, and the grip picks it up at once. A mouse drag needs no hold.
8
+
9
+ **You provide:** `items`, `getKey`, `onReorder` (receives the new array), `renderItem` (the row content; the row itself and its grip are drawn for you), `getLabel` for announcements, `aria-label`.
10
+
11
+ - Do keep rows short and uniform; the slot is sized to the item being moved.
@@ -0,0 +1,29 @@
1
+ # Sparkline
2
+
3
+ A trend the size of a word: the last 30 days of a metric in a `DataTable` cell, a `PropertyList` value, a `Stat`, or a sentence. It says rising, falling, spiky or flat at a glance, beside the number that gives the detail (after Tufte's sparklines).
4
+
5
+ **Why it's flat.** A sparkline is part of the record in the row it sits in, so it lies on the ground like every mark a chart draws: a 1.5px stroke with round joins, no fill under it, no shadow, no glow. There are no axes, gridlines or readout; the text beside it carries the numbers. The last point wears a 4px marker, so you can see where "now" is. `kind="bar"` draws flat bars 1px apart, standing on zero, for counts. A missing value (`null`) is a gap, never bridged. A normal range (`band`) is a flat band of `line` behind the marks.
6
+
7
+ **Its size.** Its height is the line height of the text it sits beside (`1lh`), and it sits in that line. Its width is `5em` unless you give one.
8
+
9
+ **Colour does one job.** By default it's `ink-muted`: context for the number beside it. `tone="data"` makes it `data-1`, when the sparkline *is* the series. `positive`, `caution` or `critical` only when the series means good or bad, and then the text beside it says so too ("Rising"), because colour never travels alone.
10
+
11
+ **Accessibility.** It's `role="img"` with a generated label that says the trend in words: "Up 17% over 30 days, from 1.2K to 1.4K. Low 1.2K, high 1.5K." With `x` naming daily dates it says "days"; otherwise "values". With a `band` it counts the values outside it. An `aria-label` you pass goes first ("Signups, last 30 days. Up 17%…"); `summary` replaces the generated text. When the row already says it, pass `decorative` and it's hidden from assistive technology. It's never focusable. Under forced colours it's drawn in `CanvasText` and the band is a dashed outline; under `contrast="more"` the line is 2px.
12
+
13
+ **Loading.** Pass `data` as it is: `undefined` while it loads, or a promise, and it's an imprint one line high and its own width (`Await`), which lands within the attention budget. A value that changes in a live table gets the row's afterglow, not a sparkline animation.
14
+
15
+ **Props:** `data` (numbers, or records with `y`); `y`; `x` (a date key, for the label); `kind` (`'line'` or `'bar'`); `tone` (`'neutral'`, `'data'`, `'positive'`, `'caution'`, `'critical'`); `band` (`[low, high]`); `width`; `format` (for the label; default compact); `summary`; `decorative`; `locale`.
16
+
17
+ **You provide:** the values, oldest first; the number they explain, beside them; and either an `aria-label` naming the measure or `decorative` when the row already says it all.
18
+
19
+ ```tsx
20
+ <Sparkline data={daily} x="day" y="signups" aria-label="Signups, last 30 days" />
21
+ <DataTable columns={[…, { key: 'trend', header: 'Last 30 days', cell: (r) => <Sparkline data={r.daily} decorative /> }]} rows={teams} />
22
+ ```
23
+
24
+ - Do put the number it explains right beside it; a sparkline alone has no scale.
25
+ - Do use `kind="bar"` for discrete counts (deploys per day), and the line for measures.
26
+ - Do leave missing values as `null`.
27
+ - Don't give it axes, a tooltip or a legend; when someone needs to read values, that's a `LineChart`.
28
+ - Don't colour it with a status hue unless the series means good or bad, and say so in words beside it.
29
+ - Don't compare sparklines in different rows as if they shared a scale: each is scaled to its own values.
@@ -0,0 +1,26 @@
1
+ # SplitButton
2
+
3
+ One action with its alternatives: "Save changes ▾" (Save as draft, Save and close), "Merge ▾" (squash, rebase), "Deploy ▾" (to staging, to production).
4
+
5
+ **Elevation.** It's one pressable action, so it's one slab: **two faces sharing one wall**, parted by a 1px notch (`line-control`, or a mix of `on-accent` into the accent on a primary slab). Being one slab, it moves as one: pointing at either face lifts the whole slab, and pressing either presses it all, so the faces never stand at two heights with a step between them. Which face you're on, and so what a click does, is a tint on that face alone. Each face has its own focus ring around its face and wall. The menu floats at level 3 (`DropdownMenu`). Placing a `Button` and a `DropdownMenu` side by side reads as two slabs of different importance. A SplitButton reads as one action with options.
6
+
7
+ **Loading.** `loading` holds the action face pressed with its label kept, as a `Button` does. The chevron stays usable.
8
+
9
+ **Accessibility.** A [button](https://www.w3.org/WAI/ARIA/apg/patterns/button/) and a [menu button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/) in a `role="group"` named by the action ("Save changes"). There are two tab stops. Enter or Space on the chevron, or Alt+Down on the action, opens the menu. Name the chevron for what it offers (`menuLabel="More save options"`). Under forced colours each face has a 1px border, and the notch is `CanvasText`.
10
+
11
+ **Props:** `label`; `variant` (`'primary' | 'secondary'`); `items` (`DropdownMenu` entries); `menuLabel` (default "More options"); `groupLabel` (when `label` isn't plain text); `onClick`, `loading`, `disabled`, `size` and the other `Button` props, which go to the action face.
12
+
13
+ **You provide:** the default action and its alternatives. The default is what most people want most of the time.
14
+
15
+ ```tsx
16
+ <SplitButton variant="primary" label="Save changes" onClick={save} menuLabel="More save options"
17
+ items={[
18
+ { label: 'Save as draft', onSelect: saveDraft },
19
+ { label: 'Save and close', onSelect: saveClose },
20
+ ]} />
21
+ ```
22
+
23
+ - Do put the common case on the face and the rest in the menu.
24
+ - Do mark a destructive alternative with `tone: 'critical'`.
25
+ - Don't hide the default inside the menu, or offer only one alternative (use two buttons).
26
+ - Don't nest a SplitButton in a `Toolbar`; its tools are ghosts.
@@ -0,0 +1,31 @@
1
+ # Stat
2
+
3
+ The tile at the top of a dashboard: a label, a big number, how it changed and against what ("Active users · 12,480 · Up 8% vs last month: 11,556"). It's often the most-read thing on the page, and sometimes the right answer isn't a chart at all.
4
+
5
+ **Why it's flush.** A number is the record. A stat has no level of its own: inside a `Card` it lies on the card, on its own it lies on the page. The label is `text-label` in `ink-muted`; the value is `text-title` in `ink`, weight 600, with tabular figures. `size="hero"` sets it in `text-display`, for the one number a view leads with (once per view, as the type scale says). A `trend` sits beside the value, usually a decorative [`Sparkline`](../Sparkline/README.md).
6
+
7
+ **Up isn't always good.** Without `good`, the change is neutral: `ink-muted` text with an arrow ("↘ Down 12 ms"). Say which direction is good (`good="up"` for users, `good="down"` for an error rate) and the change becomes a `Badge` plate whose lamp is `positive` or `critical`. The word says the direction, the arrow shows it, and the colour says whether it's good, so no cue stands alone. No change reads "No change" and stays neutral.
8
+
9
+ **Motion.** A changed value cross-fades (`dur-travel-2`) and the stat takes the afterglow (`settle`), within the attention budget. It never counts up: that delays the number for decoration. While the value loads (`undefined`, or a promise) it's an imprint of a number's width under a label already in place, and it lands like any `Await` content. In a `StatGroup` the values land together.
10
+
11
+ **Accessibility.** Plain HTML: a `section` named by its label, so it reads "Active users, 12,480, Up 8%, vs last month: 11,556". Direction is always a word; the arrow is decorative. With `live`, a changed value is announced politely: "Active users, 12,792, up 10.7% vs last month." Under forced colours the change plate gets a 1px border and the arrow carries direction; under `contrast="more"` it's drawn as `Badge` is.
12
+
13
+ **Props:** `label`; `value` (a number, a string, `null` for an em dash, `undefined` or a promise while it loads); `format`; `change` (`{ value, vs, previous, format }`: `value` is a fraction by default, so `0.08` reads "Up 8%"; with `format` it's in your units); `good` (`'up'` or `'down'`); `trend`; `help` (a caption of context); `size` (`'default'` or `'hero'`); `live`; `locale`. `StatGroup` lays a row of stats out to wrap (each at least 180px) and lands them together.
14
+
15
+ **You provide:** the value and, if it changed, by how much and against what, in words. Put each stat in a `Card` on a dashboard; make the card `interactive` if the whole stat opens a report.
16
+
17
+ ```tsx
18
+ <StatGroup aria-label="Last 30 days">
19
+ <Card><CardContent>
20
+ <Stat label="Active users" value={metrics?.active} good="up"
21
+ change={{ value: 0.08, vs: 'last month', previous: 11556 }}
22
+ trend={<Sparkline data={metrics?.daily} y="active" decorative />} />
23
+ </CardContent></Card>
24
+ </StatGroup>
25
+ ```
26
+
27
+ - Do say what the change is against ("vs last month"); a percentage alone has no anchor.
28
+ - Do set `good` only when a direction really is good or bad; otherwise leave the change neutral.
29
+ - Do lead a dashboard with stats, then the charts that explain them, then the table that is the record.
30
+ - Don't use `size="hero"` more than once in a view.
31
+ - Don't animate the number counting up, or colour the value itself: colour belongs to the change, and only with `good`.
@@ -0,0 +1,29 @@
1
+ # Stepper
2
+
3
+ Shows where you are in a process of a few steps: done steps settle into the record, the current one stands raised, and steps to come are imprints.
4
+
5
+ **Elevation.** A stepper is a staircase of plates standing on one ground line, in the same vocabulary as Drilldown's breadcrumb, and each step's height says which of the three things it is.
6
+
7
+ - **Done: settled into the record.** A plate flush with the ground, its edges stamped like a Badge, wearing the Checkbox family's check: a raised white mark on the accent. Done steps are part of the record now, so they don't stand up.
8
+ - **Current: live.** A raised slab with a wall (`wall-2`, as tall as the primary action), `surface-raised`, its number ringed in `accent-ink`. It has no sheen: sheen means "press me", and the current step does nothing when pressed; its wall is staircase height, as on the breadcrumb's current step.
9
+ - **To come: not built yet.** Imprints pressed into the page (`surface-well`, a dark inner top edge), like Skeleton's.
10
+
11
+ Connectors between steps are `accent` behind you and `line-control` ahead.
12
+
13
+ **Advancing** is the record being written. The current step presses down into the ground (`ease-sink`) and its check is stamped in (as a Badge is re-stamped: down 3px, settling, while the tick draws); about 150ms later the next step rises out of its imprint (`dur-travel-3`, `ease-rise`), within the attention budget. Going back runs the other way: the current step sinks back into an imprint and the step you chose rises from the record. Every state uses the same six-layer shadow list, so each animates into the others. Reduced motion keeps the changes of level and drops the travel.
14
+
15
+ **Keyboard and screen readers.** An `<ol>` (named "Progress" unless you pass `aria-label`) with `aria-current="step"` on the current step. Every step also says its state in words ("Step 2 of 5, completed"), and a polite live region announces the new step when it changes. With `onStepChange`, completed steps become buttons (`role="button"`, Enter or Space) that go back to that step; hovering one shows a ghost of the slab it would become. Steps ahead can't be jumped to. In forced colours the current step has a solid border and steps to come a dashed one.
16
+
17
+ | Prop | Default | |
18
+ |---|---|---|
19
+ | `steps` | | `{ title, description?, id? }[]` |
20
+ | `current` | | index of the step being worked on; `steps.length` means all done |
21
+ | `onStepChange` | | makes completed steps buttons that go back |
22
+ | `orientation` | `horizontal` | `vertical` stacks the plates, with descriptions under the titles |
23
+
24
+ **Narrow.** `orientation="auto"` (recommended) lays the steps out in a row when they all fit on one line, and down the page when they don't, so a phone never shows connectors leading to the next line.
25
+
26
+ **You provide:** `steps` with short titles (one or two words, sentence case), `current`, and the Back and Continue buttons that change it; `onStepChange` if people may revisit completed steps.
27
+
28
+ - Do keep it to three to six steps; more than that is a list, not a staircase.
29
+ - Don't use it for places people move between freely; that's Tabs, or Drilldown for a hierarchy.
@@ -0,0 +1,29 @@
1
+ # Suggestions
2
+
3
+ Prompts to start from or follow up with. An empty conversation offers starting points ("Summarise last week's incidents"); a finished answer offers follow-ups ("Compare with Q2"). Choosing one either fills the composer to edit or sends it at once.
4
+
5
+ **Elevation: things you press.**
6
+ - **Each suggestion** is a small secondary `Button`: raised at level 1 with its wall, lifting to 2 when pointed at, pressed to the ground. Never level 2 at rest: the view's one primary is the composer's Send.
7
+ - **Starters** sit in an `EmptyState` (its sunk opening) and are simply there.
8
+ - **Follow-ups** sit under the answer, inside its `Message`, and cross-fade in over `dur-onset`; they don't rise, because they only became available.
9
+ - **A send-mode suggestion** shows an arrow and is named "…, sends now".
10
+
11
+ **Accessibility.** A labelled `group` of buttons; with more than four, the toolbar pattern with one tab stop and arrow keys. Enter or Space chooses. After choosing, focus goes to the composer, with the caret at the end when the text was filled in. Follow-ups aren't announced when they arrive; the answer was.
12
+
13
+ **Props:** `Suggestions`: `aria-label` (required), `mode` (`'fill'` default, or `'send'`), `onChoose(value)`, `arrive` (default true inside a `Message`). `Suggestion`: `value` (filled or sent instead of the label), and button props.
14
+
15
+ **You provide:** what choosing does. For `fill`, set the composer's value (`PromptInput value`); for `send`, send it.
16
+
17
+ ```tsx
18
+ <Message from="assistant">
19
+ <MessageContent>…</MessageContent>
20
+ <Suggestions aria-label="Suggested follow-ups" mode="send" onChoose={send}>
21
+ <Suggestion>Compare with Q2</Suggestion>
22
+ <Suggestion value="List the three biggest risks, with owners">Biggest risks</Suggestion>
23
+ </Suggestions>
24
+ </Message>
25
+ ```
26
+
27
+ - Do keep them short, and few (three is plenty).
28
+ - Do use `fill` when the person will likely want to adjust the prompt.
29
+ - Don't make a suggestion the primary button, or animate follow-ups in with a rise.
@@ -0,0 +1,20 @@
1
+ # Surface
2
+
3
+ The primitive every component is built from: a box placed on one rung of the elevation ladder.
4
+
5
+ `level` picks the tint and shadow together (`surface-raised` + `elev-1` at level 1, `surface-well` + `well` at −1, and so on), so the two can never drift apart. `interactive` makes it climb one level on hover and focus and drop to ground while pressed.
6
+
7
+ **You provide:** `level`, children, and padding and radius through `className`/`style` (the default radius is `radius-lg`). With `interactive` and `onClick`, a non-native surface receives button semantics, keyboard focus, Enter activation and Space activation on release. Consumer keyboard handlers can cancel that behavior with `preventDefault()`. Prefer `as="button"` for a single action (its default `type` is `button`) or `as="a" href="…"` for navigation; their native keyboard behavior is preserved. `disabled` or `aria-disabled` prevents activation, including native links without a click callback. Disabled links omit their destination and retain link semantics. Events from nested controls do not activate the containing surface. Keep interactive content outside a native button or link; use a separate named action when a card contains several controls.
8
+
9
+ **Depth signals** (also on `Card`, and available to your own components through `useDepth`):
10
+
11
+ | Prop | What it shows |
12
+ |---|---|
13
+ | `settle` | Afterglow. Pass any value; when it changes, the surface tints with `accent-soft`, lifts to `elev-2` and settles back over `dur-settle` (full for 1s, noticeable for ~4s). `true` settles once on mount, for newly added items. |
14
+ | `status` | Commitment. `pending` hovers above the ground after `dur-grace` (so fast saves never flicker) and sets `aria-busy`; changing to `committed` lands with a settle; `failed` drops into a trench on `critical-soft`. Pair it with visible text ("Saving…", "Couldn't save"). |
15
+ | `heldBy` | Possession. `{ name, presence: 1–3 }` lifts it to level 3, outlines it in that `presence-*` colour and shows the name. Use for collaborators editing or dragging it. |
16
+ | `relevance` | Focus+context. `match` rises one level, `miss` sinks into a well in `ink-muted`. Use for filtering in place instead of removing items. |
17
+
18
+ - Do use Surface for one-off panels, sidebars and app chrome so they share the ladder with the components.
19
+ - Don't set `interactive` on something that doesn't act when clicked: a lift is a promise.
20
+ - Don't stack more than three levels in one region; if you need a fourth, the layout probably wants a dialog or a page.
@@ -0,0 +1,9 @@
1
+ # Switch
2
+
3
+ Turns a setting on or off immediately: a raised thumb riding in a trench.
4
+
5
+ The track is level −2 (`surface-trench`, `trench`). Turning it on pours the `accent` into the track behind the thumb (a clip-path that follows it), and the thumb travels on `ease-settle`. Pressing stretches the thumb before it moves. Hover lifts the thumb from level 1 to 2.
6
+
7
+ **You provide:** `label` (or `aria-label`), and `checked`/`onCheckedChange` or `defaultChecked`. It renders `role="switch"`.
8
+
9
+ - Do use it only for changes that apply instantly. If the change needs a Save, use a Checkbox.
@@ -0,0 +1,24 @@
1
+ # SyncIndicator
2
+
3
+ One honest answer to "is my work saved?", for autosaving editors, optimistic UIs and local-first apps: "Saved", "Saving…", "Offline · 3 changes waiting", "Couldn't sync", "1 conflict to resolve". Write-ups on local-first apps name showing unsynced edits as saved as the most common failure.
4
+
5
+ **Elevation.** A status plate is a label, and labels never lift. It's a flush `Badge` plate whose lamp relights when the state changes: positive when saved (with "· 2 minutes ago"), pulsing while saving, caution when offline or in conflict, critical when a sync failed. A change of state cross-fades rather than rising, because "things that merely changed state cross-fade". The *items* it covers carry `status="pending"` and hover until they land, so the per-item and the global signals agree. With a `queue`, pressing the plate opens the waiting changes in a `Popover`. The plate then lifts when pointed at, because it acts.
6
+
7
+ **When it fails.** `failed` shows Retry (`onRetry`). `conflict` shows Resolve (`onOpenConflicts`). The patterns around it (offline queues, conflicts, session expiry) are a framework pattern of their own.
8
+
9
+ **Accessibility.** The words are a polite status region, said without chatter. "Saved" is said at most every `announceEvery` ms (default 5s), not "Saving… Saved" on every key. "Saving" isn't said. Going offline, failures and conflicts are said at once. With a queue, the plate is a button named by its state ("Offline · 2 changes waiting. Show changes"). Under forced colours the words and the lamp carry it. Under `contrast="more"` the plate gets a `line-control` edge. Under reduced motion the lamp doesn't pulse; the word says "Saving…".
10
+
11
+ **Props:** `state` (`'saved' | 'saving' | 'offline' | 'failed' | 'conflict'`); `pending`; `conflicts`; `lastSyncedAt`; `onRetry`; `onOpenConflicts`; `queue`; `announceEvery`.
12
+
13
+ **You provide:** the true state of your sync. Only say "Saved" once the server has acknowledged the change.
14
+
15
+ ```tsx
16
+ <SyncIndicator state={sync.state} pending={sync.queue.length} lastSyncedAt={sync.at}
17
+ onRetry={sync.retry} onOpenConflicts={openConflicts}
18
+ queue={sync.queue.map((c) => <Surface key={c.id} status="pending">{c.label}</Surface>)} />
19
+ ```
20
+
21
+ - Do show "Saved" only after the server confirms.
22
+ - Do count what's waiting when offline.
23
+ - Don't put a spinner beside it; the pulsing lamp and the word say it.
24
+ - Don't raise the plate to get attention when a sync fails; the lamp and the words change.
@@ -0,0 +1,76 @@
1
+ # Table
2
+
3
+ Rows and columns of records: the rows sit on the ground with `line` dividers, and only the ones that are selected, matching, in hand or under your pointer rise.
4
+
5
+ **Elevation: the record.** A table is the plainest thing in Altitude: its rows are what has been settled, so they lie flush on the ground. Height is spent only on rows that mean something right now:
6
+
7
+ - **Selected** rows are tinted `accent-soft` and raised one level (`elev-1`), with a checked Checkbox; the toolbar counts them with a count Badge, the one badge that floats, because the count wants your attention.
8
+ - **Interactive** rows (ones that open something) lift one level on hover, a ghost lift; pressing drops them back to the ground. Rows that do nothing don't move at all.
9
+ - **Filtering in place** raises matches one level and sinks misses into a well (`ink-muted`), so the rows keep their places and you can still see what didn't match (Signals: relevance).
10
+ - **Carried** rows: when the order changes (a sort, a move), each row travels to its new place (`dur-travel-3`, `ease-glide`) instead of the table being reshuffled, so you can follow a row you were looking at.
11
+ - **In hand**: a row picked up to reorder floats at level 3, slightly tilted, over a slot that opens as a well where it will land; its neighbours glide aside, as in SortableList.
12
+
13
+ The header stays flush until rows scroll under it, then rises to `elev-2` (as in ScrollArea); where rows continue below, the bottom edge curves away (`lip-bottom`). Cells can't share one shadow, so each row's height is drawn as a single slab by its first cell, as wide as the table.
14
+
15
+ **Semantics.** A plain `<table>` with `<th scope="col">`: it's a table, not a grid, because its cells aren't edited or navigated one by one. Sortable headers are buttons, and the `th` carries `aria-sort`. Selection lives in the row's Checkbox (no `aria-selected` on rows of a plain table). An interactive row's first cell is a real button, so the keyboard reaches it; clicking anywhere else on the row does the same. Move handles are buttons: Space picks a row up, ↑ and ↓ move it, Space drops it, Escape puts it back; each step is announced. Selection counts and sort changes are announced too. Forced colours rule raised rows solid and sunk rows dashed.
16
+
17
+ **Two ways to build one.** The primitives mirror shadcn/ui: `Table` (with `maxHeight` for a scrolling body), `TableHeader`, `TableBody`, `TableFooter`, `TableRow` (`rowId`, `selected`, `interactive`, `relevance`, `settle`, `held`), `TableHead` (`align`, `sort`, `onSort`), `TableCell` (`align`) and `TableCaption`. `DataTable` builds the whole thing from data:
18
+
19
+ | Prop | |
20
+ |---|---|
21
+ | `columns` | `{ key, header, cell?, sortValue?, align?, width? }[]`; `sortValue` makes a column sortable (ascending, descending, none) |
22
+ | `rows`, `getRowId`, `getRowLabel` | the records, a stable id, and a name for screen readers |
23
+ | `selectable`, `selected`/`onSelectedChange`, `actions` | a checkbox column; actions appear beside the count while rows are selected |
24
+ | `onRowClick` | interactive rows |
25
+ | `relevance` | `(row) => 'match' \| 'miss' \| undefined` for filtering in place |
26
+ | `onReorder` | move handles; hidden while a sort is active |
27
+ | `renderExpanded`, `expanded`/`defaultExpanded`/`onExpandedChange`, `expandMode` | a disclosure button per row opening a full-width detail row (`'single'` by default: one open at a time) |
28
+ | `renderEditor`, `onSave`, `onRowChange` | an Edit button per row that opens the row's fields as an `EditRegion` in a full-width row beneath it (EditRegion's save contract; `onRowChange` publishes the canonical row) |
29
+ | `toolbar`, `maxHeight`, `empty` | a filter field or button at the toolbar's end (at narrow widths the toolbar wraps onto more lines and a field in it shrinks, so nothing scrolls sideways); scroll height; text for no rows |
30
+
31
+ **Loading.** While `DataTable`'s `rows` is undefined (your query hasn't returned) or a promise, the table keeps its real header over `skeleton.lines` rows of imprinted cells (default 5), so columns don't jump (see `Await`). An empty array is an empty table.
32
+
33
+ **Opening a row.** Read-only detail (`renderExpanded`) is a reveal of the record, so it stays on the ground: a full-width row under the record, inset, with the disclosure chevron turned and `aria-expanded`/`aria-controls` on its button. Editing (`renderEditor` + `onSave`) is live, so it rises: the row and its region become one raised slab (`elev-1`) holding a static `EditRegion`, with the row's cells still visible above it so the record stays identified. The first field takes focus; Enter in a single-line field saves and Escape cancels, and focus returns to the row's Edit button. While a row is in edit, the other rows' Edit buttons, sorting and reordering wait, so the edited row isn't carried away mid-edit; once saved it settles with an afterglow and, if its sort position changed, is carried to its new place. It stays a table, not a grid: the region is one `td` spanning the row. Above about eight fields, open the record in a `Sheet` beside the table instead (`onRowClick`).
34
+
35
+ **On a phone.** Under 480px of the frame's width, columns keep at least 6rem (or their `minWidth`) and the table scrolls inside its frame, fading the edge with more columns, instead of squeezing cells. `sticky: true` keeps a column in view over a flush 1px edge. Wide, nothing changes.
36
+
37
+ **You provide:** the data or the rows, short column headers in sentence case, numbers right-aligned (`align="end"`), and a `getRowLabel` that names each row the way people refer to it.
38
+
39
+ - Do filter in place with `relevance` when position matters (a ranked list, a schedule); remove rows instead when it doesn't.
40
+ - Don't make rows interactive unless clicking one does something; a lift that leads nowhere is a broken promise.
41
+ - Don't nest tables or put a table inside a raised card's padding without a reason; the table is already the record.
42
+
43
+ ## Columns and density
44
+
45
+ Wide record tables need a way to choose what shows. `columnsMenu` adds a **Columns** button to the toolbar (it says "· 3 hidden" when some are). It opens a `Popover` (level 3) holding a `SortableList` of `Checkbox`es (drag a column, or pick it up with Space and the arrows, to reorder it) and a density switch (a single-choice `ToggleGroup`: Comfortable or Compact). The first column in order can't be hidden: it names the row.
46
+
47
+ - `columnVisibility`, `onColumnVisibilityChange`: `{ [key]: false }` hides a column.
48
+ - `columnOrder`, `onColumnOrderChange`: keys in order; keys left out keep their place after the ones given.
49
+ - `density`, `onDensityChange`: `'comfortable'` (default) or `'compact'`. Compact rows keep every target at least 24px tall ([WCAG 2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum)).
50
+
51
+ Each has an uncontrolled `default…` form. Showing, hiding or reordering a column doesn't animate the table: it's a change of layout, not of the record. Each change is announced ("Owner column hidden", "Compact rows").
52
+
53
+ ## Paging from the server
54
+
55
+ `pagination` puts paging in the table's footer, under a line:
56
+
57
+ - **Pages:** `{ page, pageCount, pageSize, total, onPageChange, hrefFor?, noun? }` renders `Pagination` with its range summary ("9–16 of 214 invoices"). Leave out `pageCount` for cursor paging (Previous, Next, `hasNext`).
58
+ - **Load more:** `{ mode: 'more', hasMore, onLoadMore, total?, noun? }` renders "6 of 46 events" and a Load more button. Loaded rows arrive with the afterglow (at most four begin at once), and "6 more loaded, 12 of 46" is announced.
59
+
60
+ **While the next page loads** (`loading: true`), the current rows stay on the ground at half strength (the reading a refetching chart uses), the table is `aria-busy`, and the page thumb is held pressed. Your app loads the rows (`useAsyncCollection` keeps selection across pages with `selection: 'preserve'`). After a page change, focus stays on the pagination control.
61
+
62
+ ## Selecting past the page
63
+
64
+ To bulk-act on a filtered set larger than a page ("Archive all 2,341 inactive users"), use the richer selection:
65
+
66
+ ```tsx
67
+ <DataTable … selectable selection={selection} onSelectionChange={setSelection} matchingTotal={2341} noun="inactive users"
68
+ actions={(ids, sel) => <Button onClick={() => archive(sel)}>Archive</Button>} actionsPlacement="auto" />
69
+ ```
70
+
71
+ - **A selection** is `{ kind: 'ids', ids }` or `{ kind: 'query', total, except }`. Actions get the loaded ids and the whole selection, so they can act on the *query*. It persists through filtering, sorting and paging, because it's yours.
72
+ - **Once the header checkbox selects a page**, a flush `accent-soft` row offers "Select all 2,341 matching inactive users". A query selection is said in words ("All 2,341 matching, except 1"), and unchecking a row adds it to the exceptions.
73
+ - **`actionsPlacement="floating"`**, or `auto` once the toolbar has scrolled away, docks the count and actions as **a bar floating at the bottom edge, level 3**: a selection is a set of records in your hand. It's a labelled region ("Selection"), and it rises into place.
74
+
75
+ `selected` and `onSelectedChange` keep working for selections of loaded rows. Report each record's outcome of a long bulk action in place (`RemovalReceipt`'s reading) and in a toast.
76
+
@@ -0,0 +1,18 @@
1
+ # Tabs
2
+
3
+ Switches between views of the same content.
4
+
5
+ **`variant="folder"` (default): the sheet on top is the one you're reading.** Every tab is the top edge of its own sheet. The chosen tab and its panel are one raised piece (`surface-raised`, `elev-2`) with concave fillets where they meet; the other tabs lie flush on the ground behind it. Choosing a tab brings its sheet up from behind: its tab rises 10px into place on `ease-settle` while the old tab drops back, and the panel's shadow swells from `elev-1` as the new sheet surfaces, its content emerging 3px below. Hovering an unchosen tab peeks its sheet up a little: a ghost of the tab, before you commit.
6
+
7
+ **`variant="segmented"`:** a compact control for ranges and modes. The options sit in a well and one raised pill glides between them on `ease-settle`; hovering an option shows a faint ghost of the pill.
8
+
9
+ Arrow keys, Home and End move between tabs in both.
10
+
11
+ **Narrow.** A tab list that doesn't fit scrolls within itself, so the page never scrolls sideways: the chosen tab stays in view, and an edge with more tabs past it fades out.
12
+
13
+ **You provide:** `Tabs` with `value`/`onValueChange` or `defaultValue` and optionally `variant`; a `TabsList` (with an `aria-label`) of `TabsTrigger value="…"`; one `TabsContent value="…"` per trigger. In the folder variant put the `TabsContent`s directly after the `TabsList`: together they're one folder.
14
+
15
+ - Keep labels to one or two words.
16
+ - Don't use tabs for sequential steps; they imply the views are peers.
17
+
18
+ `TabsContent` takes `forceMount` to keep an unchosen panel mounted (inert, hidden from assistive technology, `data-state="inactive"`), for views that hold state or cross-fade, as a chart's Chart | Table views do.