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,67 @@
|
|
|
1
|
+
# NotificationCenter and NotificationList
|
|
2
|
+
|
|
3
|
+
An inbox of what happened: mentions, assignments, approvals waiting, failed jobs, billing notices. A bell with a count opens the list, grouped by day, with Mark all as read, actions on each item, and a link to preferences.
|
|
4
|
+
|
|
5
|
+
**Elevation.**
|
|
6
|
+
- **The bell** is live: a ghost button. **Only its count floats** (`Badge variant="count"`), because a count wants you.
|
|
7
|
+
- **The panel** floats at level 3 (a `Popover`), or at level 4 as a bottom `Sheet` on narrow screens.
|
|
8
|
+
- **The list in it** lies on the ground: it's a record of what happened.
|
|
9
|
+
- **Unread is a mark, not a height**: an `accent` dot, weight 600, and "Unread" in the item's name. Keeping unread items raised or tinted would make height mean "unread", which breaks "tiers carry meaning" (see Elevation, "States that don't rise").
|
|
10
|
+
- **A new notification** arrives lifted and settles, as anything new does. An item that links lifts when pointed at. Mark all as read removes the dots with a cross-fade, and nothing travels.
|
|
11
|
+
|
|
12
|
+
**Toasts are momentary; this is the record.** Use `toast` for "Saved". Use the notification center for things that wait for someone.
|
|
13
|
+
|
|
14
|
+
**Accessibility.** The bell's name includes the unread count ("Notifications, 2 unread"). New notifications are announced politely and batched ("2 new notifications"), whether or not the panel is open. Mark all as read comes first in the panel. Items that link are links, and opening one marks it read and closes the panel. Item actions are buttons, and an unread item without a link gets "Mark as read". Under forced colours the dot keeps its colour (`forced-color-adjust: none`), and the word carries it too.
|
|
15
|
+
|
|
16
|
+
**Props.** `NotificationCenter`: `items` (`{ id, title, body?, createdAt, read, href?, actions? }[]`), `onMarkRead(ids)`, `onMarkAllRead`, `preferencesHref`, `groupBy` (`'day' | 'none'`), `onOpen`, `mobileBreakpoint` (default 640). `NotificationList` is the same list without the bell, for a page of its own.
|
|
17
|
+
|
|
18
|
+
**You provide:** the notifications and whether each is read, and what reading one does.
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<NotificationCenter items={notifications} onMarkRead={(ids) => markRead(ids)} onMarkAllRead={markAllRead}
|
|
22
|
+
preferencesHref="/settings/notifications" />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do let each notification lead to its subject (`href`).
|
|
26
|
+
- Do keep an action on the item when it's waiting for someone ("Approve").
|
|
27
|
+
- Don't raise or tint unread items.
|
|
28
|
+
- Don't put momentary confirmations here; that's a toast.
|
|
29
|
+
|
|
30
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
`NotificationCenter` as first used in its preview:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { NotificationCenter } from 'altitude-design';
|
|
37
|
+
|
|
38
|
+
<NotificationCenter items={items} onMarkRead={markRead} onMarkAllRead={() => setItems((xs) => xs.map((x) => ({ ...x, read: true })))} preferencesHref="#/settings/notifications" />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `nc`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import * as React from 'react';
|
|
45
|
+
import { Button, NotificationCenter } from 'altitude-design';
|
|
46
|
+
|
|
47
|
+
const ago = (m) => new Date(Date.now() - m * 60000);
|
|
48
|
+
export default function App() {
|
|
49
|
+
const [items, setItems] = React.useState([
|
|
50
|
+
{ id: 'n1', title: 'Maya mentioned you in Q4 planning', body: '“@you can you take the pricing section?”', createdAt: ago(5), read: false, href: '#/docs/q4' },
|
|
51
|
+
{ id: 'n2', title: 'Theo asked to approve an expense', body: '€1,240 · Conference travel', createdAt: ago(48), read: false, actions: <Button size="sm" variant="secondary">Approve</Button> },
|
|
52
|
+
{ id: 'n3', title: 'Export of 812 invoices is ready', createdAt: ago(60 * 3), read: true, href: '#/exports/7' },
|
|
53
|
+
{ id: 'n4', title: 'Nightly sync failed for Salesforce', body: 'The token expired. Reconnect to resume.', createdAt: ago(60 * 27), read: true, href: '#/integrations' },
|
|
54
|
+
]);
|
|
55
|
+
const markRead = (ids) => setItems((xs) => xs.map((x) => (ids.includes(x.id) ? { ...x, read: true } : x)));
|
|
56
|
+
return (
|
|
57
|
+
<div className="col">
|
|
58
|
+
<div className="nc">
|
|
59
|
+
<Button size="sm" variant="secondary" onClick={() => setItems((xs) => [{ id: 'n' + Date.now(), title: 'Ada assigned you “Billing page”', createdAt: new Date(), read: false, href: '#/issues/212' }, ...xs])}>Simulate a new notification</Button>
|
|
60
|
+
<NotificationCenter items={items} onMarkRead={markRead} onMarkAllRead={() => setItems((xs) => xs.map((x) => ({ ...x, read: true })))} preferencesHref="#/settings/notifications" />
|
|
61
|
+
</div>
|
|
62
|
+
<p className="cap">Unread is a dot and a bolder title, never a height. A new notification arrives lifted and settles, and is announced even while the panel is closed.</p>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
<!-- /example -->
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# NumberInput
|
|
2
|
+
|
|
3
|
+
A number field: seats on a plan, rate limits, retention days, prices, quantities. `parseNumber()` reads text the same way.
|
|
4
|
+
|
|
5
|
+
**Elevation.** It's an opening, like `Input`: a well that shows its back wall while it's empty and whose floor rises flush once it holds a value. Invalid drops it to a trench on `critical-soft`. **The steppers are small raised slabs inside the well's end cap**, because you press them (push up, fill down). They're level 1 with a 1px wall, lift to 2 when pointed at, press flat, and lie flush in `ink-faint` at a limit. The unit ("days", "€", "%") sits beside the number in `ink-muted`. Editing isn't a change to the record, so nothing glows while you type or step.
|
|
6
|
+
|
|
7
|
+
**Parsing.** What you type is read in the locale's own way: in `de-DE`, "1.250,50" is 1250.5 and "1,5" is 1.5. Arabic-Indic, Persian, Devanagari, Bengali and full-width digits are read too. The value is committed on blur, on Enter and on every step, and it's clamped to `min` and `max` when committed, so it can't leave its range. Nonsense ("abc") keeps the last good value. Steps land on the grid that starts at `min` (or 0), without binary-fraction drift (0.1 + 0.2 is 0.3).
|
|
8
|
+
|
|
9
|
+
**Formats.** `format` takes `Intl.NumberFormat` options. `{ style: 'unit', unit: 'day' }` puts "days" beside the number. `{ style: 'currency', currency: 'EUR' }` puts the currency where the locale puts it. With `{ style: 'percent' }` the value is a fraction (0.1 is shown as "10 %").
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The [spinbutton pattern](https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/): `role="spinbutton"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and an `aria-valuetext` that includes the unit ("30 days"). Up and Down step, Shift+Up/Down and PageUp/PageDown move ten steps, and Home and End go to `min` and `max`. The steppers aren't tab stops (`tabIndex={-1}`, as the APG says), so name them for what they change (`incrementLabel="Increase seats"`). The scroll wheel never changes the value unless you set `wheel`, and then only while the field is focused. Under forced colours the steppers have a 1px border. Under `contrast="more"` their wall is `ink`.
|
|
12
|
+
|
|
13
|
+
**Props:** `value`, `defaultValue` (`number | null`), `onValueChange`; `min`, `max`, `step` (default 1); `format`; `locale`; `invalid`; `size`; `wheel`; `incrementLabel`, `decrementLabel`; and the input's own attributes.
|
|
14
|
+
|
|
15
|
+
**Target size.** The two steppers sit side by side, down then up, each 24×24 (WCAG 2.5.8): stacked, two 15px steppers could never each reach 24px. Under a coarse pointer each reaches 44px tall through a transparent hit area. The arrow keys still step, and Page Up and Page Down step by ten.
|
|
16
|
+
|
|
17
|
+
**You provide:** the range, the step, the unit (in `format`) and a label (with `Field`).
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Field label="Seats">
|
|
21
|
+
<NumberInput value={seats} onValueChange={setSeats} min={1} max={500} incrementLabel="Increase seats" decrementLabel="Decrease seats" />
|
|
22
|
+
</Field>
|
|
23
|
+
<NumberInput value={days} onValueChange={setDays} format={{ style: 'unit', unit: 'day', unitDisplay: 'long' }} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Do give it `min` and `max` whenever the value has limits, and say them in the hint.
|
|
27
|
+
- Do put the unit in `format`, not in the label, so it's spoken with the value.
|
|
28
|
+
- Don't use it for identifiers that happen to be digits (a ZIP code, a card number); that's an `Input`.
|
|
29
|
+
- Don't use it to choose roughly within a range; that's a `Slider`.
|
|
30
|
+
|
|
31
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
32
|
+
## Example
|
|
33
|
+
|
|
34
|
+
`NumberInput` as first used in its preview:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { NumberInput } from 'altitude-design';
|
|
38
|
+
|
|
39
|
+
<NumberInput value={seats} onValueChange={setSeats} min={1} max={500} incrementLabel="Increase seats" decrementLabel="Decrease seats" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `ni`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import * as React from 'react';
|
|
46
|
+
import { Field, NumberInput } from 'altitude-design';
|
|
47
|
+
|
|
48
|
+
export default function App() {
|
|
49
|
+
const [seats, setSeats] = React.useState(12);
|
|
50
|
+
const [days, setDays] = React.useState(30);
|
|
51
|
+
const [price, setPrice] = React.useState(1250.5);
|
|
52
|
+
const [rate, setRate] = React.useState(null);
|
|
53
|
+
return (
|
|
54
|
+
<div className="col">
|
|
55
|
+
<div className="ni">
|
|
56
|
+
<Field label="Seats" hint="1 to 500"><NumberInput value={seats} onValueChange={setSeats} min={1} max={500} incrementLabel="Increase seats" decrementLabel="Decrease seats" /></Field>
|
|
57
|
+
<Field label="Keep logs for"><NumberInput value={days} onValueChange={setDays} min={1} max={365} format={{ style: 'unit', unit: 'day', unitDisplay: 'long' }} /></Field>
|
|
58
|
+
<Field label="Preis pro Monat" hint="German: type 1.250,50"><NumberInput value={price} onValueChange={setPrice} min={0} step={0.5} locale="de-DE" format={{ style: 'currency', currency: 'EUR' }} /></Field>
|
|
59
|
+
<Field label="Sampling rate"><NumberInput value={rate} onValueChange={setRate} min={0} max={1} step={0.05} format={{ style: 'percent' }} placeholder="10" /></Field>
|
|
60
|
+
</div>
|
|
61
|
+
<p className="cap">Arrow keys step (Shift for ten); Home and End go to the limits; a stepper at its limit lies flat. Values clamp to their range when you leave the field.</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
<!-- /example -->
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# PageHeader
|
|
2
|
+
|
|
3
|
+
The block every page opens with: breadcrumb, title, a line of description, the page's actions, and sometimes tabs.
|
|
4
|
+
|
|
5
|
+
**Elevation.** The page's name is the record, so the block lies on the ground. Only its actions are live: secondaries at level 1, ghosts at 0, and **one primary at level 2, in its own slot**. "The primary action rests at level 2" (law 1) becomes structural: `primary` takes one element, and a `variant="primary"` button put in `actions` logs a development warning naming law 1. Actions that don't need to be seen go in `overflow`, a "More actions" menu (`DropdownMenu`, level 3). With `sticky`, the header stays at the top and **rises one level when content passes under it** (`surface-raised`, `elev-1`), as `ScrollArea`'s header does.
|
|
6
|
+
|
|
7
|
+
**Layout.** The breadcrumb sits above, the title and description on the left, and the actions on the right (the primary last, nearest the reading edge). Tabs go underneath. Everything wraps on narrow screens. Set `--alt-page-pad` to the page's side padding (`AppShell` does) and the header reaches through it, so a sticky header's rise spans the page.
|
|
8
|
+
|
|
9
|
+
**Accessibility.** A `<header>` with the page's `h1` (`PageHeaderTitle`). The tab order is breadcrumb, actions, tabs. "More actions" is a [menu button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/). Under forced colours a risen sticky header gets a bottom border.
|
|
10
|
+
|
|
11
|
+
**Props.** `PageHeader`: `breadcrumb`, `primary`, `actions`, `overflow` (`DropdownMenu` entries), `tabs`, `sticky`. Inside it go `PageHeaderTitle` (an `h1`) and `PageHeaderDescription`.
|
|
12
|
+
|
|
13
|
+
**You provide:** the page's name, what it's for in one line, and its actions, with the one that matters most as `primary`.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
<PageHeader sticky
|
|
17
|
+
breadcrumb={<Breadcrumb items={[{ label: 'Settings', href: '/settings' }, { label: 'Members' }]} />}
|
|
18
|
+
primary={<Button variant="primary">Invite members</Button>}
|
|
19
|
+
actions={<Button variant="secondary">Export list</Button>}
|
|
20
|
+
overflow={[{ label: 'Download audit log', onSelect: download }]}>
|
|
21
|
+
<PageHeaderTitle>Members</PageHeaderTitle>
|
|
22
|
+
<PageHeaderDescription>People who can see and edit this workspace.</PageHeaderDescription>
|
|
23
|
+
</PageHeader>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Do give a page at most one primary action; put the rest in `actions` or `overflow`.
|
|
27
|
+
- Do keep the description to one line about what the page is for.
|
|
28
|
+
- Don't put a PageHeader inside a `Card`; that's `CardHeader`.
|
|
29
|
+
- Don't make the whole header a raised card. It lies on the ground, and only rises while content passes under it.
|
|
30
|
+
|
|
31
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
32
|
+
## Example
|
|
33
|
+
|
|
34
|
+
`PageHeader` as first used in its preview:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { Breadcrumb, Button, PageHeader, PageHeaderDescription, PageHeaderTitle, Tabs, TabsList, TabsTrigger } from 'altitude-design';
|
|
38
|
+
|
|
39
|
+
<PageHeader sticky
|
|
40
|
+
breadcrumb={<Breadcrumb items={[{ label: 'Settings', href: '#/settings' }, { label: 'Members' }]} />}
|
|
41
|
+
primary={<Button variant="primary">Invite members</Button>}
|
|
42
|
+
actions={<Button variant="secondary">Export list</Button>}
|
|
43
|
+
overflow={[{ label: 'Download audit log' }, { label: 'Members settings' }]}
|
|
44
|
+
tabs={<Tabs variant="segmented" value={tab} onValueChange={setTab}><TabsList aria-label="Members sections"><TabsTrigger value="members">Members</TabsTrigger><TabsTrigger value="invites">Invitations</TabsTrigger><TabsTrigger value="roles">Roles</TabsTrigger></TabsList></Tabs>}>
|
|
45
|
+
<PageHeaderTitle>Members</PageHeaderTitle>
|
|
46
|
+
<PageHeaderDescription>People who can see and edit this workspace.</PageHeaderDescription>
|
|
47
|
+
</PageHeader>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The full preview, as rendered in Storybook and checked in CI (`ph`, `rows` are the preview page's own layout classes, not Altitude's):
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import * as React from 'react';
|
|
54
|
+
import { Breadcrumb, Button, PageHeader, PageHeaderDescription, PageHeaderTitle, Tabs, TabsList, TabsTrigger } from 'altitude-design';
|
|
55
|
+
|
|
56
|
+
export default function App() {
|
|
57
|
+
const [tab, setTab] = React.useState('members');
|
|
58
|
+
return (
|
|
59
|
+
<div className="ph">
|
|
60
|
+
<PageHeader sticky
|
|
61
|
+
breadcrumb={<Breadcrumb items={[{ label: 'Settings', href: '#/settings' }, { label: 'Members' }]} />}
|
|
62
|
+
primary={<Button variant="primary">Invite members</Button>}
|
|
63
|
+
actions={<Button variant="secondary">Export list</Button>}
|
|
64
|
+
overflow={[{ label: 'Download audit log' }, { label: 'Members settings' }]}
|
|
65
|
+
tabs={<Tabs variant="segmented" value={tab} onValueChange={setTab}><TabsList aria-label="Members sections"><TabsTrigger value="members">Members</TabsTrigger><TabsTrigger value="invites">Invitations</TabsTrigger><TabsTrigger value="roles">Roles</TabsTrigger></TabsList></Tabs>}>
|
|
66
|
+
<PageHeaderTitle>Members</PageHeaderTitle>
|
|
67
|
+
<PageHeaderDescription>People who can see and edit this workspace.</PageHeaderDescription>
|
|
68
|
+
</PageHeader>
|
|
69
|
+
<div className="rows">{Array.from({ length: 10 }, (_, i) => <div key={i} />)}</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
<!-- /example -->
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Pagination
|
|
2
|
+
|
|
3
|
+
Moves through pages of a collection the server pages: invoices, members, audit events.
|
|
4
|
+
|
|
5
|
+
**Elevation: one thumb that travels.** The page numbers sit in a trench (`surface-well`), and **the current page is one raised thumb that travels to it** on `ease-glide`, as `RadioGroup`'s does. That reads as "you are here, among these", where a row of buttons reads as "any of these does something". Pointing at another page shows a ghost of the thumb there (feedforward). While the next page loads (`loading`), the thumb is held pressed until the rows arrive. Previous and Next are ghost buttons, flush and `ink-faint` at the ends.
|
|
6
|
+
|
|
7
|
+
**Pages.** It shows the first page, the last, and the current one with `siblings` either side, with "…" where pages are skipped. A gap of a single page shows that page instead, because "…" would take as much room. `pageRange()` gives the same list. **Cursor paging:** leave out `pageCount` and you get Previous, Next (disabled once `hasNext` is false) and "Page 3".
|
|
8
|
+
|
|
9
|
+
**Summary.** With `total` and `pageSize` it says "41–60 of 214 invoices" (`noun`) in a polite live region, so the change is announced after a page change.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A `nav` labelled "Pagination", with links when `hrefFor` gives pages URLs, or buttons otherwise. **They stay in plain Tab order:** these are links drawn like a radio group, not a radio group, so there's no roving focus. The current page has `aria-current="page"`. Pages are named "Page 3", and the steps "Previous page" and "Next page", with `aria-disabled` at the ends. Below 480px the steps show only their chevrons. Under forced colours the trench is dashed and the thumb has a 1px border. Under `contrast="more"` the thumb gets a 2px slab. Under reduced motion the thumb appears in place.
|
|
12
|
+
|
|
13
|
+
**Props:** `page` (from 1); `pageCount`; `onPageChange`; `total`, `pageSize`, `noun`; `hrefFor`; `hasNext`; `siblings` (default 1); `loading`; `locale`; `aria-label`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the current page, how many there are (or whether there's a next one), and what happens on a change.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Pagination page={page} pageCount={11} onPageChange={setPage} total={214} pageSize={20} noun="invoices" />
|
|
19
|
+
<Pagination page={page} onPageChange={setPage} hasNext={!!cursor.next} /> // cursor paging
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Do use `hrefFor` when pages have URLs, so they can be opened in a new tab.
|
|
23
|
+
- Do show the summary; "Page 3 of 11" says less than "41–60 of 214".
|
|
24
|
+
- Don't page short lists; show them whole.
|
|
25
|
+
- Don't use it for steps in a process; that's a `Stepper`.
|
|
26
|
+
|
|
27
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
28
|
+
## Example
|
|
29
|
+
|
|
30
|
+
`Pagination` as first used in its preview:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Pagination } from 'altitude-design';
|
|
34
|
+
|
|
35
|
+
<Pagination page={page} pageCount={11} onPageChange={go} total={214} pageSize={20} noun="invoices" loading={loading} />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import * as React from 'react';
|
|
42
|
+
import { Pagination } from 'altitude-design';
|
|
43
|
+
|
|
44
|
+
export default function App() {
|
|
45
|
+
const [page, setPage] = React.useState(3);
|
|
46
|
+
const [loading, setLoading] = React.useState(false);
|
|
47
|
+
const [cursor, setCursor] = React.useState(1);
|
|
48
|
+
const go = (p) => { setPage(p); setLoading(true); setTimeout(() => setLoading(false), 500); };
|
|
49
|
+
return (
|
|
50
|
+
<div className="col">
|
|
51
|
+
<Pagination page={page} pageCount={11} onPageChange={go} total={214} pageSize={20} noun="invoices" loading={loading} />
|
|
52
|
+
<Pagination page={cursor} onPageChange={setCursor} hasNext={cursor < 4} aria-label="Audit log pages" />
|
|
53
|
+
<p className="cap">The current page is a raised thumb in the trench; it's held pressed while the next page loads. Without a page count it's Previous, Next and "Page 3".</p>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
<!-- /example -->
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# PasswordInput
|
|
2
|
+
|
|
3
|
+
A password field: signing in, signing up, a reset, "change password" in settings.
|
|
4
|
+
|
|
5
|
+
**Elevation.** It's an `Input`, so it's an opening that fills. In its end cap sits a reveal `Toggle`. While the password shows, the toggle is on: sunk and filled, as every Toggle is. Its name ("Show password") never changes; `aria-pressed` says whether it's on.
|
|
6
|
+
|
|
7
|
+
**Strength is a reading.** When you pass `strength`, the score (0–4, from your scoring: zxcvbn or your own rules) is shown as a `Meter`: a flush track with flat ink, never `Progress`'s raised fill, because a password's strength is a reading, not a task moving toward done (see [Charts](../../visual/Charts.md)). The word ("Weak", "Good", "Strong") sits beside the label and carries the meaning. The fill stays the meter's own colour: Meter's thresholds mean "higher is worse", which is backwards for strength.
|
|
8
|
+
|
|
9
|
+
**Requirements** are stamped as they're met, like a done `Checklist` item: muted, with a check pressed into a small well. Unmet ones keep an empty ring.
|
|
10
|
+
|
|
11
|
+
**Caps Lock.** While the field is focused and Caps Lock is on, a caption with an icon says "Caps Lock is on".
|
|
12
|
+
|
|
13
|
+
**Accessibility.** A plain `<input type="password">`, with `autocomplete="new-password"` when there's a strength meter or requirements, and `"current-password"` otherwise (pass `autoComplete` to say otherwise). The reveal toggle is a separate tab stop after the field, and revealing doesn't move focus. Strength, requirements and the caps-lock notice describe the field (`aria-describedby`). Strength and unmet requirements are announced politely once typing pauses (0.9s), not on every key. The meter's value text is the word. Under forced colours the requirement stamps and the caps-lock icon are `CanvasText`.
|
|
14
|
+
|
|
15
|
+
**Props:** `strength` (`(value) => 0–4`); `strengthLabels`; `requirements` (`{ label, test }[]`); `revealLabel` (default "Show password"); and every `Input` prop except `type` and `trailing`.
|
|
16
|
+
|
|
17
|
+
**You provide:** the scoring and the rules, when people are creating a password. For signing in, nothing more than a label.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<Field label="New password">
|
|
21
|
+
<PasswordInput
|
|
22
|
+
strength={(v) => zxcvbn(v).score}
|
|
23
|
+
requirements={[
|
|
24
|
+
{ label: 'At least 12 characters', test: (v) => v.length >= 12 },
|
|
25
|
+
{ label: 'Not a common password', test: notCommon },
|
|
26
|
+
]}
|
|
27
|
+
/>
|
|
28
|
+
</Field>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Do let people reveal what they typed; masking alone causes errors.
|
|
32
|
+
- Do say your rules before someone breaks them, as requirements.
|
|
33
|
+
- Don't draw strength as a progress bar.
|
|
34
|
+
- Don't show a strength meter when signing in; it's for creating a password.
|
|
35
|
+
|
|
36
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
37
|
+
## Example
|
|
38
|
+
|
|
39
|
+
`PasswordInput` as first used in its preview:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { PasswordInput } from 'altitude-design';
|
|
43
|
+
|
|
44
|
+
<PasswordInput defaultValue="correct horse" />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The full preview, as rendered in Storybook and checked in CI (`pw` is the preview page's own layout class, not Altitude's):
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { Field, PasswordInput } from 'altitude-design';
|
|
51
|
+
|
|
52
|
+
// A stand-in score: in an app this would be zxcvbn or your own rules.
|
|
53
|
+
const score = (v) => Math.min(4, [v.length >= 8, v.length >= 12, /[A-Z]/.test(v) && /[a-z]/.test(v), /\d/.test(v), /[^\w\s]/.test(v)].filter(Boolean).length - (v.length < 8 ? 1 : 0));
|
|
54
|
+
const common = ['password', 'password1', '12345678', 'qwerty123'];
|
|
55
|
+
export default function App() {
|
|
56
|
+
return (
|
|
57
|
+
<div className="pw">
|
|
58
|
+
<Field label="Password"><PasswordInput defaultValue="correct horse" /></Field>
|
|
59
|
+
<Field label="New password">
|
|
60
|
+
<PasswordInput defaultValue="Glacier7" strength={score}
|
|
61
|
+
requirements={[
|
|
62
|
+
{ label: 'At least 12 characters', test: (v) => v.length >= 12 },
|
|
63
|
+
{ label: 'Not a common password', test: (v) => v.length > 0 && !common.includes(v.toLowerCase()) },
|
|
64
|
+
]} />
|
|
65
|
+
</Field>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
<!-- /example -->
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# PermissionMatrix
|
|
2
|
+
|
|
3
|
+
Roles against permissions: "Admins can manage billing, Members can invite, Viewers can only read". The same grid serves notification preferences (events against channels).
|
|
4
|
+
|
|
5
|
+
**Elevation.** The grid is settled: the record of who can do what, on the ground with `line` dividers.
|
|
6
|
+
- **An editable grant is a choice**, built like a `Checkbox`: an empty trench that the accent pours into when granted.
|
|
7
|
+
- **An inherited or fixed grant is already decided**, so it's a flush stamped check (or a dash when not granted), muted, like a done `Checklist` item. It's pressed into the record, not drawn as a disabled checkbox, because it isn't unavailable. A `locked` role (an owner) has every permission, stamped.
|
|
8
|
+
- **The active cell** is a 2px `accent-ink` ring, never a height, as in `DataGrid`.
|
|
9
|
+
- **Saving:** a toggled cell whose `onChange` returns a promise hovers as pending until it settles, then takes the afterglow. A rejection reverts it.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The [grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/) with checkbox cells. Arrow keys move, Home and End go along the row, Ctrl+Home and Ctrl+End go to the corners, and Space or Enter toggles. Each cell reads "Manage billing, Admin, checked". A stamped cell is `aria-readonly` and says why ("Granted, inherited from Everyone", "Always granted to Owner"): it's reachable, and toggling it says it can't be changed here. Changes are announced ("Create projects granted to Viewer"). Group headings head their rows. Under forced colours the ring is `Highlight` and stamped cells keep their glyph. Under `contrast="more"` the ring is 3px.
|
|
12
|
+
|
|
13
|
+
**Props:** `roles` (`{ id, name, locked? }[]`); `permissions` (`{ id, name, group? }[]`); `value` (`{ [roleId]: { [permissionId]: boolean } }`); `inherited(roleId, permissionId)` (→ `{ granted, from }` or undefined); `onChange(roleId, permissionId, granted)` (may return a promise); `aria-label`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the roles, the permissions, who has what, and where inherited grants come from.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<PermissionMatrix aria-label="Role permissions"
|
|
19
|
+
roles={[{ id: 'owner', name: 'Owner', locked: true }, { id: 'admin', name: 'Admin' }]}
|
|
20
|
+
permissions={[{ id: 'billing.manage', name: 'Manage billing', group: 'Billing' }]}
|
|
21
|
+
value={grants} inherited={(role, perm) => inheritFrom(role, perm)}
|
|
22
|
+
onChange={(role, perm, granted) => saveGrant(role, perm, granted)} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do say where an inherited grant comes from.
|
|
26
|
+
- Do return a promise from `onChange` so a cell shows it's saving.
|
|
27
|
+
- Don't draw inherited grants as disabled checkboxes.
|
|
28
|
+
- Don't lift the active cell; it's a ring.
|
|
29
|
+
|
|
30
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
`PermissionMatrix` as first used in its preview:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { PermissionMatrix } from 'altitude-design';
|
|
37
|
+
|
|
38
|
+
<PermissionMatrix aria-label="Role permissions" roles={roles} permissions={perms} value={grants} inherited={inherited}
|
|
39
|
+
onChange={(role, perm, on) => new Promise((r) => setTimeout(() => { setGrants((g) => ({ ...g, [role]: { ...g[role], [perm]: on } })); r(); }, 600))} />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import * as React from 'react';
|
|
46
|
+
import { PermissionMatrix } from 'altitude-design';
|
|
47
|
+
|
|
48
|
+
const roles = [{ id: 'owner', name: 'Owner', locked: true }, { id: 'admin', name: 'Admin' }, { id: 'member', name: 'Member' }, { id: 'viewer', name: 'Viewer' }];
|
|
49
|
+
const perms = [
|
|
50
|
+
{ id: 'billing.view', name: 'View invoices', group: 'Billing' }, { id: 'billing.manage', name: 'Manage billing', group: 'Billing' },
|
|
51
|
+
{ id: 'members.invite', name: 'Invite members', group: 'Members' }, { id: 'members.remove', name: 'Remove members', group: 'Members' },
|
|
52
|
+
{ id: 'projects.create', name: 'Create projects', group: 'Projects' }, { id: 'projects.view', name: 'View projects', group: 'Projects' },
|
|
53
|
+
];
|
|
54
|
+
export default function App() {
|
|
55
|
+
const [grants, setGrants] = React.useState({ admin: { 'billing.view': true, 'billing.manage': true, 'members.invite': true, 'members.remove': true, 'projects.create': true },
|
|
56
|
+
member: { 'members.invite': true, 'projects.create': true }, viewer: {} });
|
|
57
|
+
// Everyone can view projects; admins inherit invoices from Billing admins in this example.
|
|
58
|
+
const inherited = (role, perm) => (perm === 'projects.view' ? { granted: true, from: 'Everyone' } : role === 'viewer' && perm.startsWith('billing') ? { granted: false, from: 'Guest policy' } : undefined);
|
|
59
|
+
return (
|
|
60
|
+
<div className="col">
|
|
61
|
+
<PermissionMatrix aria-label="Role permissions" roles={roles} permissions={perms} value={grants} inherited={inherited}
|
|
62
|
+
onChange={(role, perm, on) => new Promise((r) => setTimeout(() => { setGrants((g) => ({ ...g, [role]: { ...g[role], [perm]: on } })); r(); }, 600))} />
|
|
63
|
+
<p className="cap">Editable grants are checkboxes. Owner's and inherited grants are stamped into the record: they're decided, not unavailable. Arrow keys move; Space toggles.</p>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
<!-- /example -->
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Pile
|
|
2
|
+
|
|
3
|
+
Shows a group of items as a stack. Its thickness says roughly how many; click it and the items fan out over the page.
|
|
4
|
+
|
|
5
|
+
**Closed.** The top card is raised (level 1). Beneath it sit 0–3 sheets: none for one item, one for 2–3, two for 4–9, three for 10 or more. Thickness is read in bins, so the count badge carries the number. On hover or focus the top lifts and the sheets beneath fan a little, a preview of what's underneath; nothing moves before the pointer is on it.
|
|
6
|
+
|
|
7
|
+
**Open.** Give the pile `items` and `renderItem` and its top card becomes a toggle. Clicking it (or Enter/Space) slides the items out from under the top card, one after another, into a column that **floats over the page below**: nothing on the page moves. Each item is its own card with its own shadow (`elev-2`, `elev-3` under the pointer); the scroll area itself has none, and its top and bottom edges fade out rather than cutting the shadows off. Click an item to choose it (`onSelect`). Click the top again, press Escape or click anywhere else and the items slide back under. Long piles scroll after `maxHeight`.
|
|
8
|
+
|
|
9
|
+
**You provide:** `items`, `renderItem(item, { index })`, `onSelect`, the top card's content as children, optionally `getKey`, `maxHeight` and `open`/`onOpenChange` (for example, to close it after a choice). For a pile that only shows quantity, pass `count` instead of `items`.
|
|
10
|
+
|
|
11
|
+
- Leave room below a pile, or near the bottom of the viewport the list may run off-screen.
|
|
12
|
+
- Use piles for collections people triage: review queues, inboxes, stacks of drafts.
|
|
13
|
+
|
|
14
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
`Pile` as first used in its preview:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Pile } from 'altitude-design';
|
|
21
|
+
|
|
22
|
+
<Pile count={1} className="p"><div className="face"><b>Drafts</b><span>1 document</span></div></Pile>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `piles`, `p`, `face`, `wide`, `doc`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import * as React from 'react';
|
|
29
|
+
import { Pile } from 'altitude-design';
|
|
30
|
+
|
|
31
|
+
const docs = [
|
|
32
|
+
['Pricing page copy', 'Three tiers, annual toggle, FAQ.', 'Mara · 2h'], ['Onboarding emails', 'Five-step drip for new workspaces.', 'Jon · yesterday'],
|
|
33
|
+
['Q4 launch plan', 'Milestones, owners and risks.', 'Lena · Mon'], ['Release notes 2.4', 'Faster cold starts, new regions.', 'Sam · Mon'],
|
|
34
|
+
['Support macros', 'Twelve canned replies, reviewed.', 'Ivy · last week'], ['Brand voice', 'How we sound, with examples.', 'Ada · last week'],
|
|
35
|
+
['Security review', 'Findings from the October audit.', 'Theo · Oct 2'],
|
|
36
|
+
];
|
|
37
|
+
export default function App() {
|
|
38
|
+
const [picked, setPicked] = React.useState(null);
|
|
39
|
+
return (
|
|
40
|
+
<div className="col">
|
|
41
|
+
<div className="piles">
|
|
42
|
+
<Pile count={1} className="p"><div className="face"><b>Drafts</b><span>1 document</span></div></Pile>
|
|
43
|
+
<Pile count={3} interactive className="p"><div className="face"><b>Approved</b><span>3 documents</span></div></Pile>
|
|
44
|
+
<Pile items={docs} className="p wide" onSelect={(d) => setPicked(d[0])} defaultOpen maxHeight={300}
|
|
45
|
+
renderItem={([t, d, m]) => <div className="doc"><b>{t}</b><span>{d}</span><small>{m}</small></div>}>
|
|
46
|
+
<div className="face"><b>In review</b><span>{docs.length} documents · click to fan out</span></div>
|
|
47
|
+
</Pile>
|
|
48
|
+
</div>
|
|
49
|
+
<p className="cap">{picked ? `Opened “${picked}”.` : 'One sheet for 2–3 items, two for 4–9, three for 10+; the badge carries the number. Click a pile with items and they fan out over the page below it.'}</p>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
<!-- /example -->
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# PlanPicker
|
|
2
|
+
|
|
3
|
+
Choose a plan (Free, Pro, Business) in sign-up, upgrade and downgrade flows, with a Monthly / Annually switch, prices, and what each plan includes.
|
|
4
|
+
|
|
5
|
+
**Elevation: one choice, one mark.** "A radio group is one switch with several positions, so its single thumb slides to the chosen option." The plan cards are positions in one group: they lie flush on the ground with a `line` rim, and **one raised mark travels to the chosen card's corner** on `ease-glide`. The chosen card also takes an `accent` rim. Pointing at another card shows a ghost of the mark there.
|
|
6
|
+
|
|
7
|
+
**No lifted "most popular" card.** Pricing pages usually raise and glow the plan they want you to pick. Marketing emphasis isn't importance *right now*, so it isn't height. **"Recommended" and "Current plan" are flush `Badge` plates.** The only level-2 slab is your Continue button beside the picker (law 1), and its label should name the outcome ("Continue to Pro").
|
|
8
|
+
|
|
9
|
+
**Prices** come in minor units and are formatted by `FormattedNumber`, so changing the period cross-fades them and never counts. Annual prices are shown per month, "billed annually". A plan with `price: null` says "Contact sales". A plan with `unavailable` can't be chosen and says why.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A [radio group](https://www.w3.org/WAI/ARIA/apg/patterns/radio/): one tab stop, and arrows move and choose (skipping unavailable plans). Each plan reads its name, price and period ("Pro, €20.00 per seat per month, billed annually, recommended"), with its features as the description. The period switch is a single-choice `ToggleGroup`. Under forced colours the chosen card has a 2px border and the mark keeps a `CanvasText` outline. Under `contrast="more"` the mark gets a hard slab. Under reduced motion the mark appears in place.
|
|
12
|
+
|
|
13
|
+
**Props:** `plans` (`{ id, name, description?, price ({ month, year } | number | null), currency?, unit?, features?, unavailable? }[]`); `value`, `defaultValue`, `onValueChange`; `current`; `recommended`; `period`, `onPeriodChange`; `locale`; `aria-label`.
|
|
14
|
+
|
|
15
|
+
**You provide:** the plans and their prices, the current plan, and the Continue button. The flow around it (proration, downgrade consequences) is the billing pattern.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<PlanPicker plans={plans} value={plan} onValueChange={setPlan} current="free" recommended="pro" period={period} onPeriodChange={setPeriod} />
|
|
19
|
+
<Button variant="primary" onClick={next}>Continue to {planName}</Button>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Do name the outcome on the Continue button.
|
|
23
|
+
- Do say why a plan can't be chosen.
|
|
24
|
+
- Don't lift or glow a recommended plan.
|
|
25
|
+
- Don't animate prices counting up when the period changes.
|
|
26
|
+
|
|
27
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
28
|
+
## Example
|
|
29
|
+
|
|
30
|
+
`PlanPicker` as first used in its preview:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { PlanPicker } from 'altitude-design';
|
|
34
|
+
|
|
35
|
+
<PlanPicker plans={plans} value={plan} onValueChange={setPlan} current="free" recommended="pro" period={period} onPeriodChange={setPeriod} locale="en-IE" aria-label="Choose a plan" />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `row` are the preview page's own layout classes, not Altitude's):
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import * as React from 'react';
|
|
42
|
+
import { Button, PlanPicker } from 'altitude-design';
|
|
43
|
+
|
|
44
|
+
const plans = [
|
|
45
|
+
{ id: 'free', name: 'Free', price: 0, currency: 'EUR', unit: 'per seat', features: ['3 projects', 'Community support'] },
|
|
46
|
+
{ id: 'pro', name: 'Pro', price: { month: 2400, year: 2000 }, currency: 'EUR', unit: 'per seat', features: ['Unlimited projects', 'SAML SSO', 'Audit log'] },
|
|
47
|
+
{ id: 'business', name: 'Business', price: { month: 4800, year: 4000 }, currency: 'EUR', unit: 'per seat', features: ['Everything in Pro', 'Custom roles', '99.9% uptime SLA'] },
|
|
48
|
+
{ id: 'enterprise', name: 'Enterprise', price: null, unavailable: 'Contact sales for more than 500 seats.' },
|
|
49
|
+
];
|
|
50
|
+
export default function App() {
|
|
51
|
+
const [plan, setPlan] = React.useState('free');
|
|
52
|
+
const [period, setPeriod] = React.useState('year');
|
|
53
|
+
const name = plans.find((p) => p.id === plan).name;
|
|
54
|
+
return (
|
|
55
|
+
<div className="col">
|
|
56
|
+
<PlanPicker plans={plans} value={plan} onValueChange={setPlan} current="free" recommended="pro" period={period} onPeriodChange={setPeriod} locale="en-IE" aria-label="Choose a plan" />
|
|
57
|
+
<div className="row"><Button variant="primary" disabled={plan === 'free'}>{plan === 'free' ? 'You\'re on Free' : `Continue to ${name}`}</Button></div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
<!-- /example -->
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Popover
|
|
2
|
+
|
|
3
|
+
Shows rich, interactive content anchored to a trigger, floating at level 3.
|
|
4
|
+
|
|
5
|
+
It grows from the trigger's edge (`transform-origin` on that side), rising 6px from 96% scale while its shadow climbs to `elev-3`. Focus moves to the first control inside; Escape or a click outside closes it and returns focus to the trigger.
|
|
6
|
+
|
|
7
|
+
**On a narrow screen.** It opens where its `side` and `align` put it, then shifts sideways to stay 8px inside the screen, and it's never wider than the screen less 16px.
|
|
8
|
+
|
|
9
|
+
**You provide:** `trigger` (a Button), children (padding is `space-4`), `side`, `align`, an `aria-label`, and `open`/`onOpenChange` if you need control.
|
|
10
|
+
|
|
11
|
+
- Do use it for small forms and pickers that belong to one control.
|
|
12
|
+
- Don't use it for a decision that blocks the page; that's a Dialog.
|
|
13
|
+
|
|
14
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
`Popover` as first used in its preview:
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { Button, Field, Input, Popover } from 'altitude-design';
|
|
21
|
+
|
|
22
|
+
<Popover defaultOpen aria-label="Share" trigger={<Button>Share</Button>}>
|
|
23
|
+
<div className="col" style={{ width: 280, gap: 12 }}>
|
|
24
|
+
<b style={{ font: '600 14px/20px var(--font-sans)' }}>Invite to project</b>
|
|
25
|
+
<Field label="Email address"><Input size="sm" type="email" placeholder="name@company.com" /></Field>
|
|
26
|
+
<div className="row" style={{ justifyContent: 'flex-end', gap: 8 }}><Button size="sm" variant="primary">Send invite</Button></div>
|
|
27
|
+
</div>
|
|
28
|
+
</Popover>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `row` are the preview page's own layout classes, not Altitude's):
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { Button, Field, Input, Popover } from 'altitude-design';
|
|
35
|
+
|
|
36
|
+
export default function App() {
|
|
37
|
+
return (
|
|
38
|
+
<Popover defaultOpen aria-label="Share" trigger={<Button>Share</Button>}>
|
|
39
|
+
<div className="col" style={{ width: 280, gap: 12 }}>
|
|
40
|
+
<b style={{ font: '600 14px/20px var(--font-sans)' }}>Invite to project</b>
|
|
41
|
+
<Field label="Email address"><Input size="sm" type="email" placeholder="name@company.com" /></Field>
|
|
42
|
+
<div className="row" style={{ justifyContent: 'flex-end', gap: 8 }}><Button size="sm" variant="primary">Send invite</Button></div>
|
|
43
|
+
</div>
|
|
44
|
+
</Popover>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
<!-- /example -->
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Progress
|
|
2
|
+
|
|
3
|
+
Shows how far along a task is: a raised fill rising out of a trench.
|
|
4
|
+
|
|
5
|
+
Determinate progress glides between values on `ease-glide` (`dur-travel-3`). Without a `value` it's indeterminate: a third-width fill travels across the track every `dur-sweep`, and stops under reduced motion.
|
|
6
|
+
|
|
7
|
+
**You provide:** `value` (0–`max`) or nothing, and an `aria-label` or a visible label nearby.
|
|
8
|
+
|
|
9
|
+
- Do switch from indeterminate to determinate as soon as you know the total.
|
|
10
|
+
- Don't use it for a reading such as storage used of a quota: that's the record, not a task, so it's a [`Meter`](../Meter/README.md).
|
|
11
|
+
|
|
12
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
`Progress` as first used in its preview:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { Progress } from 'altitude-design';
|
|
19
|
+
|
|
20
|
+
<Progress value={v} aria-label="Upload" />
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `wrap`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import * as React from 'react';
|
|
27
|
+
import { Progress } from 'altitude-design';
|
|
28
|
+
|
|
29
|
+
export default function App() {
|
|
30
|
+
const [v, setV] = React.useState(20);
|
|
31
|
+
React.useEffect(() => { const t = setInterval(() => setV((x) => (x >= 100 ? 8 : x + 23)), 1100); return () => clearInterval(t); }, []);
|
|
32
|
+
return (
|
|
33
|
+
<div className="col wrap">
|
|
34
|
+
<Progress value={v} aria-label="Upload" />
|
|
35
|
+
<Progress aria-label="Loading" />
|
|
36
|
+
<p className="cap">Determinate glides between values; indeterminate travels.</p>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
<!-- /example -->
|