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,138 @@
|
|
|
1
|
+
# PromptInput
|
|
2
|
+
|
|
3
|
+
Where a person writes to an assistant or agent: a textarea that grows, attachments as chips, optional tools or a model picker, Send, and Stop while a response is generating.
|
|
4
|
+
|
|
5
|
+
**Elevation: an opening.** The whole composer is the `Input` well. Empty, its back wall shows and it deepens as the pointer nears; once it holds text its floor rises flush (Completeness and Anticipation, as `Textarea`).
|
|
6
|
+
- **Send is the level-2 primary.** With nothing to send (no text, or a file still uploading) it's flush and faint, but stays focusable (`aria-disabled`).
|
|
7
|
+
- **While generating, Send becomes Stop in the same slot and size**, held as the live action. Its icon cross-fades without travel, so nothing moves.
|
|
8
|
+
- **Attachments are `Tag` plates** above the text. One being uploaded (your `onAttach` returned a promise) hovers as pending, "Uploading". A rejection removes it and says so.
|
|
9
|
+
- **Dragging a file over** drops the well to a trench, "Drop to attach", as `DropZone` does. It opens only while something could be dropped.
|
|
10
|
+
|
|
11
|
+
**Keyboard.** Enter sends. Shift+Enter adds a line. **Enter during IME composition never sends**, because it confirms the composition (Japanese, Chinese, Korean input). Escape stops generating. ⌘U or Ctrl+U attaches. Pasting a file attaches it. After sending, focus stays in the composer.
|
|
12
|
+
|
|
13
|
+
**Accessibility.** Plain HTML: a `<form>` with a labelled `<textarea>` ("Message" unless you name it) and buttons named "Send message", "Stop generating" and "Attach files". Sending isn't announced, because the conversation announces the response. Stopping is ("Generating stopped"), and so are attachments added and removed. Under forced colours the empty well is dashed and a drop target dashed thick. Under reduced motion the composer grows without gliding.
|
|
14
|
+
|
|
15
|
+
**Parts:** `PromptInput` (`onSubmit({ text, files })`, `status` (`'ready' | 'submitted' | 'streaming' | 'error'`), `onStop`, `value`, `onValueChange`, `onAttach`, `accept`); `PromptInputAttachments`; `PromptInputTextarea` (`placeholder`, `maxHeight`); `PromptInputToolbar`; `PromptInputAttach`; `PromptInputSubmit`.
|
|
16
|
+
|
|
17
|
+
**Queueing.** With `onQueue`, Enter while the agent works queues the message instead of doing nothing: the button stays Stop, the composer empties and keeps focus, and "Message queued. It will be sent when the assistant finishes." is announced. Show the queue with `ConversationQueue`, and send the first one when the turn ends.
|
|
18
|
+
|
|
19
|
+
**Context and commands.** With `mentionable(query)`, typing @ opens a list of matches above the composer, grouped by kind (Files, Records, People), ranked as you return them; a promise shows "Searching…". Choosing one puts "@label" in the text and a plate in the composer's context row; Backspace right after it removes it whole, and its plate's remove button does the same. With `commands`, / at the start of an empty composer lists them (best match first). `onSubmit` gets `mentions` and `command` separately; the command is taken out of `text`. The text is a combobox (arrows, Enter or Tab to choose); while generating, the first Escape closes the list and only a second one stops. IME composition never opens or drives it.
|
|
20
|
+
|
|
21
|
+
**Dictation.** `PromptInputDictate` (in the toolbar) is a microphone toggle; your app supplies the speech recognition (the Web Speech API or a server transcriber) and passes `status`, `interim` and `final`. Listening is latched like a Toggle that's on (a shallow well with accent-soft poured in) with "Listening" and a steady lamp: no pulse, no waveform. Interim words are a ghost under the text in `ink-faint`, like a suggestion, never in the value; `final` words go in at the caret. ⌘D / Ctrl+D toggles from the text; Escape stops listening before it stops generating. "Listening" and "Stopped listening" are announced; interim words aren't. Blocked or unavailable stays focusable and says why.
|
|
22
|
+
|
|
23
|
+
**You provide:** sending, stopping, uploading, and the `status` of the response. Leave out `accept` and there are no attachments.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<PromptInput onSubmit={(msg) => send(msg)} status={status} onStop={stop} accept="image/*,.pdf">
|
|
27
|
+
<PromptInputAttachments />
|
|
28
|
+
<PromptInputTextarea placeholder="Ask about this workspace" />
|
|
29
|
+
<PromptInputToolbar>
|
|
30
|
+
<PromptInputAttach />
|
|
31
|
+
<Select size="sm" aria-label="Model" items={models} />
|
|
32
|
+
<PromptInputSubmit />
|
|
33
|
+
</PromptInputToolbar>
|
|
34
|
+
</PromptInput>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Do keep Stop where Send was.
|
|
38
|
+
- Do return a promise from `onAttach` so an upload shows it's in flight.
|
|
39
|
+
- Don't send on Enter during IME composition (it's handled; don't override `onKeyDown` to send).
|
|
40
|
+
- Don't announce "Message sent".
|
|
41
|
+
|
|
42
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
43
|
+
## Example
|
|
44
|
+
|
|
45
|
+
`PromptInput` as first used in its preview:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { PromptInput, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar } from 'altitude-design';
|
|
49
|
+
|
|
50
|
+
<PromptInput onSubmit={({ text, mentions, command }) => setSent(`${command ? `/${command} · ` : ''}${text}${mentions ? ` · context: ${mentions.map((m) => m.label).join(', ')}` : ''}`)}
|
|
51
|
+
mentionable={(q) => new Promise((r) => setTimeout(() => r(context.filter((c) => c.label.toLowerCase().includes(q.toLowerCase()))), 250))}
|
|
52
|
+
commands={[{ name: 'review', description: 'Review the current changes' }, { name: 'compact', description: 'Summarise the conversation so far' }, { name: 'clear', description: 'Start a new conversation' }]}>
|
|
53
|
+
<PromptInputTextarea placeholder="@ to add context, / for commands" />
|
|
54
|
+
<PromptInputToolbar><PromptInputSubmit /></PromptInputToolbar>
|
|
55
|
+
</PromptInput>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `cap`, `pi` are the preview page's own layout classes, not Altitude's):
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import * as React from 'react';
|
|
62
|
+
import { PromptInput, PromptInputAttach, PromptInputAttachments, PromptInputDictate, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar, Select } from 'altitude-design';
|
|
63
|
+
|
|
64
|
+
const context = [
|
|
65
|
+
{ id: 'f1', label: 'pricing.ts', kind: 'file', description: 'src/pricing.ts' },
|
|
66
|
+
{ id: 'f2', label: 'plans.test.ts', kind: 'file', description: 'src/plans.test.ts' },
|
|
67
|
+
{ id: 'r1', label: 'Q3 report', kind: 'record', description: 'Shared by Maya, 2 days ago' },
|
|
68
|
+
{ id: 'p1', label: 'Maya Chen', kind: 'person', description: 'Billing team' },
|
|
69
|
+
];
|
|
70
|
+
function Pointed() {
|
|
71
|
+
const [sent, setSent] = React.useState('');
|
|
72
|
+
return (
|
|
73
|
+
<div className="col">
|
|
74
|
+
<PromptInput onSubmit={({ text, mentions, command }) => setSent(`${command ? `/${command} · ` : ''}${text}${mentions ? ` · context: ${mentions.map((m) => m.label).join(', ')}` : ''}`)}
|
|
75
|
+
mentionable={(q) => new Promise((r) => setTimeout(() => r(context.filter((c) => c.label.toLowerCase().includes(q.toLowerCase()))), 250))}
|
|
76
|
+
commands={[{ name: 'review', description: 'Review the current changes' }, { name: 'compact', description: 'Summarise the conversation so far' }, { name: 'clear', description: 'Start a new conversation' }]}>
|
|
77
|
+
<PromptInputTextarea placeholder="@ to add context, / for commands" />
|
|
78
|
+
<PromptInputToolbar><PromptInputSubmit /></PromptInputToolbar>
|
|
79
|
+
</PromptInput>
|
|
80
|
+
<p className="cap">{sent ? `Sent: ${sent}` : 'Type @ to point at a file, record or person, or / at the start for a command.'}</p>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
// Dictation: here a scripted transcriber stands in for the app's speech recognition.
|
|
85
|
+
function Dictated() {
|
|
86
|
+
const [status, setStatus] = React.useState('idle');
|
|
87
|
+
const [interim, setInterim] = React.useState('');
|
|
88
|
+
const [final, setFinal] = React.useState();
|
|
89
|
+
const timers = React.useRef([]);
|
|
90
|
+
const start = () => {
|
|
91
|
+
setStatus('listening');
|
|
92
|
+
const words = 'Summarise the open risks and who owns each one'.split(' ');
|
|
93
|
+
words.forEach((_, k) => timers.current.push(setTimeout(() => setInterim(words.slice(0, k + 1).join(' ')), 180 * (k + 1))));
|
|
94
|
+
timers.current.push(setTimeout(() => { setFinal({ id: Date.now(), text: words.join(' ') + '.' }); setInterim(''); setStatus('idle'); }, 180 * words.length + 400));
|
|
95
|
+
};
|
|
96
|
+
const stop = () => { timers.current.forEach(clearTimeout); timers.current = []; setInterim(''); setStatus('idle'); };
|
|
97
|
+
return (
|
|
98
|
+
<div className="col">
|
|
99
|
+
<PromptInput onSubmit={() => {}}>
|
|
100
|
+
<PromptInputTextarea placeholder="Type, or press the microphone and speak" />
|
|
101
|
+
<PromptInputToolbar>
|
|
102
|
+
<PromptInputDictate status={status} interim={interim} final={final} onStart={start} onStop={stop} />
|
|
103
|
+
<PromptInputDictate status="denied" label="Dictate (blocked example)" />
|
|
104
|
+
<PromptInputSubmit />
|
|
105
|
+
</PromptInputToolbar>
|
|
106
|
+
</PromptInput>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
export default function App() {
|
|
111
|
+
const [status, setStatus] = React.useState('ready');
|
|
112
|
+
const [last, setLast] = React.useState('');
|
|
113
|
+
const timer = React.useRef();
|
|
114
|
+
const send = ({ text, files }) => {
|
|
115
|
+
setLast(`Sent “${text}”${files.length ? ` with ${files.length} file${files.length > 1 ? 's' : ''}` : ''}.`);
|
|
116
|
+
setStatus('submitted');
|
|
117
|
+
timer.current = setTimeout(() => { setStatus('streaming'); timer.current = setTimeout(() => setStatus('ready'), 2500); }, 600);
|
|
118
|
+
};
|
|
119
|
+
return (
|
|
120
|
+
<div className="col pi">
|
|
121
|
+
<PromptInput onSubmit={send} status={status} onStop={() => { clearTimeout(timer.current); setStatus('ready'); }} accept="image/*,.pdf,.csv"
|
|
122
|
+
onAttach={() => new Promise((r) => setTimeout(r, 900))}>
|
|
123
|
+
<PromptInputAttachments />
|
|
124
|
+
<PromptInputTextarea placeholder="Ask about this workspace" />
|
|
125
|
+
<PromptInputToolbar>
|
|
126
|
+
<PromptInputAttach />
|
|
127
|
+
<Select size="sm" aria-label="Model" defaultValue="opus" items={[{ value: 'opus', label: 'Opus 5.5' }, { value: 'sonnet', label: 'Sonnet 5.5' }, { value: 'haiku', label: 'Haiku 5.5' }]} />
|
|
128
|
+
<PromptInputSubmit />
|
|
129
|
+
</PromptInputToolbar>
|
|
130
|
+
</PromptInput>
|
|
131
|
+
<p className="cap">{status === 'ready' ? (last || 'Enter sends, Shift+Enter adds a line. Paste or drop a file to attach it.') : status === 'submitted' ? 'Sending…' : 'Generating… Escape or Stop ends it.'}</p>
|
|
132
|
+
<Pointed />
|
|
133
|
+
<Dictated />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
<!-- /example -->
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# PropertyList
|
|
2
|
+
|
|
3
|
+
One record's properties as label–value pairs, each editable where it sits: an issue's sidebar (status, assignee, priority, estimate), a customer's details, a settings summary, a “Check your answers” page. It's GOV.UK's summary list and Ant Design's Descriptions, with Linear-style editing: each editable value is an [InlineEdit](../InlineEdit/README.md) with any control, and “Edit all” is one [EditRegion](../EditRegion/README.md).
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
<PropertyList aria-label="Issue details" shortcuts>
|
|
7
|
+
<PropertyListItem label="Status" value={issue.status} shortcut="s"
|
|
8
|
+
renderValue={(s) => <Badge tone={toneFor(s)}>{labelFor(s)}</Badge>}
|
|
9
|
+
editor={(control) => <Select {...control} items={statuses} />} commit="on-choose"
|
|
10
|
+
onSave={(draft) => api.patch(issue.id, { status: draft }, issue.revision)}
|
|
11
|
+
onValueChange={(status) => setIssue({ ...issue, status })} />
|
|
12
|
+
<PropertyListItem label="Priority" value={issue.priority} emptyLabel="Add priority" … />
|
|
13
|
+
<PropertyListItem label="Created" value={formatDate(issue.createdAt)} /> {/* read-only: no onSave */}
|
|
14
|
+
</PropertyList>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`onSave` makes an item editable, with InlineEdit's whole contract (`editor`, `commit`, `validate`, `required`, `isEqual`, retained input on failure, conflict latching). Without it the value is read-only text.
|
|
18
|
+
|
|
19
|
+
## Elevation
|
|
20
|
+
|
|
21
|
+
- **Properties** are the record, on the ground: a `dl`, labels `ink-muted` above (`layout="stacked"`, a sidebar) or beside (`layout="inline"`, a summary, with `line` rules) their values.
|
|
22
|
+
- **An editable value** is one plate, the InlineEdit trigger, that lifts one level (`elev-1`) when pointed at, because clicking it opens its editor. Read-only values never lift, so editability shows without a pencil per row (hover is a promise).
|
|
23
|
+
- **An unset value** is a small opening: a dashed well, the empty field's treatment at plate size, naming what to add (“Add priority”), so a half-filled record reads at a glance. Setting it fills it.
|
|
24
|
+
- **Editing** opens InlineEdit's well in the value's place, under its own text, so the value doesn't move; with `commit="on-choose"` the picker's list rises from it and there's no Save. Pending, failed and accepted are InlineEdit's.
|
|
25
|
+
- **Edit all** (`editAll`) lifts the whole list into one raised `EditRegion`, for records with cross-field rules (a last day on or after the first).
|
|
26
|
+
- **A proposed change** (an item's `proposed`: an agent read an email and suggests "Stage: Qualified → Proposal", or a teammate's suggested edit) is status that asks for action, so that field is **raised one level** (as `Confirmation`). It shows the current value struck through beside the proposed one (`DiffView`'s property change), "Proposed by Assistant", and Reject and Accept. **Nothing touches the record until it's accepted.** Accepting saves through `onSave`, hovering as pending ("Saving…") and settling with the afterglow (staggered for Accept all, at most four at once). A failed save drops to a trench with the reason and Retry. A raised summary above the list counts the proposals ("3 proposed changes by Assistant") with Accept all and Reject all.
|
|
27
|
+
|
|
28
|
+
Each editable value is a button named by the label, the value and the verb (“Status: In review. Change status”), as GOV.UK's visually hidden “Change name” guidance asks, so it makes sense out of context. Each change is announced (“Status set to Done”). Proposals are a labelled group (“3 proposed changes”); each Accept and Reject names its field (“Accept new amount”), the old and new values are read as “Current” and “Proposed”, and outcomes are announced (“New amount saved”, “Rejected the new stage”). Forced colours: unset values dashed, the hover plate outlined. `contrast="more"`: the hover plate gets the level-1 ink slab.
|
|
29
|
+
|
|
30
|
+
## Keyboard
|
|
31
|
+
|
|
32
|
+
Tab moves through the editable values; Enter or Space opens one, and the editor behaves as InlineEdit (Escape cancels, focus returns to the value). With `shortcuts` on the list, an item's `shortcut` key opens it while focus is in the list and not in a field (`s` for status, as Linear does), and the value announces it with `aria-keyshortcuts`. Shortcuts are off by default: WCAG 2.1.4 asks that single-key shortcuts can be turned off or remapped, so offer a setting before turning them on.
|
|
33
|
+
|
|
34
|
+
## Props
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `PropertyList` | `layout` (`'stacked'` or `'inline'`), `shortcuts`, `editAll` (`{ value, onValueChange, onSave, validate?, renderEditor, label? }`), `proposedBy`, `aria-label`/`aria-labelledby` |
|
|
39
|
+
| `PropertyListItem` | `label`, `value`, `renderValue`, `emptyLabel`; editable with `onSave`, `onValueChange`, `editor`, `commit`, `validate`, `required`, `isEqual`, `shortcut`; `proposed` and `onProposalResolved(accepted, value)` (clear `proposed` there) |
|
|
40
|
+
|
|
41
|
+
InlineEdit gains `trigger="value"` (read mode as one value plate), `emptyLabel` and `shortcut` for this; they're available on their own too.
|
|
42
|
+
|
|
43
|
+
## You provide
|
|
44
|
+
|
|
45
|
+
The record and each property's write, and for Edit all the whole-record write and its cross-field rules.
|
|
46
|
+
|
|
47
|
+
## Do and don't
|
|
48
|
+
|
|
49
|
+
- **Do** use `emptyLabel` to name what's missing (“Add due date”), not just “Not set”.
|
|
50
|
+
- **Do** keep Edit all for records with rules across fields; otherwise edit each property where it sits.
|
|
51
|
+
- **Don't** turn on shortcuts without a way to turn them off.
|
|
52
|
+
|
|
53
|
+
## Demo and verification
|
|
54
|
+
|
|
55
|
+
The `PropertyList` section of `build/previews.txt` shows an issue sidebar (status, assignee, priority unset, estimate, created) with shortcuts on, and a “Check your answers” summary with an unset phone number and Edit all. `node checks/property-list.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers the `dl` structure, value buttons and their names, read-only text, unset openings, on-choose saves and announcements, filling an unset value, validation and Escape, shortcuts, Edit all with a cross-field rule and focus return, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
|
|
56
|
+
**Narrow.** Under 480px of its own width, an inline list stacks each label over its value, and a proposed change stacks its values, who proposed it and its Reject and Accept.
|
|
57
|
+
|
|
58
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
59
|
+
## Example
|
|
60
|
+
|
|
61
|
+
`PropertyList` as first used in its preview:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { Badge, Combobox, Input, PropertyList, PropertyListItem, Select } from 'altitude-design';
|
|
65
|
+
|
|
66
|
+
<PropertyList aria-labelledby="pl-issue" shortcuts>
|
|
67
|
+
<PropertyListItem label="Status" value={issue.status} shortcut="s" commit="on-choose" onSave={save('status')} onValueChange={set('status')}
|
|
68
|
+
renderValue={s => <Badge tone={PL_TONE[s]}>{plLabel(PL_STATUS, s)}</Badge>} editor={c => <Select {...c} items={PL_STATUS} size="sm" />} />
|
|
69
|
+
<PropertyListItem label="Assignee" value={issue.assignee} shortcut="a" commit="on-choose" onSave={save('assignee')} onValueChange={set('assignee')}
|
|
70
|
+
renderValue={p => plLabel(PL_PEOPLE, p)} editor={c => <Combobox {...c} items={PL_PEOPLE} size="sm" />} />
|
|
71
|
+
<PropertyListItem label="Priority" value={issue.priority} emptyLabel="Add priority" commit="on-choose" onSave={save('priority')} onValueChange={set('priority')}
|
|
72
|
+
renderValue={p => plLabel(PL_PRIORITY, p)} editor={c => <Select {...c} items={PL_PRIORITY} size="sm" />} />
|
|
73
|
+
<PropertyListItem label="Estimate" value={issue.estimate} onSave={save('estimate')} onValueChange={set('estimate')} required
|
|
74
|
+
validate={n => Number.isInteger(n) && n >= 0 ? undefined : 'Use a whole number, 0 or more.'} renderValue={n => `${n} points`}
|
|
75
|
+
editor={({ value, onValueChange, ...c }) => <Input {...c} type="number" size="sm" value={value ?? ''} onChange={e => onValueChange(e.target.value === '' ? null : Number(e.target.value))} />} />
|
|
76
|
+
<PropertyListItem label="Created" value={issue.created} />
|
|
77
|
+
</PropertyList>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The full preview, as rendered in Storybook and checked in CI (`pl-demo`, `pl-side` are the preview page's own layout classes, not Altitude's):
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import * as React from 'react';
|
|
84
|
+
import { Badge, Combobox, Field, Input, PropertyList, PropertyListItem, Select } from 'altitude-design';
|
|
85
|
+
|
|
86
|
+
const PL_STATUS = [{ value: 'todo', label: 'To do' }, { value: 'progress', label: 'In progress' }, { value: 'review', label: 'In review' }, { value: 'done', label: 'Done' }];
|
|
87
|
+
const PL_TONE = { todo: 'neutral', progress: 'accent', review: 'caution', done: 'positive' };
|
|
88
|
+
const PL_PEOPLE = [{ value: 'mara', label: 'Mara Quist' }, { value: 'ines', label: 'Inés Ortega' }, { value: 'tobi', label: 'Tobi Adeyemi' }];
|
|
89
|
+
const PL_PRIORITY = [{ value: 'low', label: 'Low' }, { value: 'medium', label: 'Medium' }, { value: 'high', label: 'High' }];
|
|
90
|
+
const plWait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
91
|
+
const plLabel = (list, v) => list.find(o => o.value === v)?.label;
|
|
92
|
+
export default function App() {
|
|
93
|
+
const [issue, setIssue] = React.useState({ status: 'review', assignee: 'mara', priority: '', estimate: 3, created: '21 Oct 2026' });
|
|
94
|
+
const [trip, setTrip] = React.useState({ name: 'Mara Quist', email: 'mara@northwind.example', start: '2026-11-02', end: '2026-11-06', phone: '' });
|
|
95
|
+
const set = key => v => setIssue(i => ({ ...i, [key]: v }));
|
|
96
|
+
const save = key => async draft => { await plWait(250); if (key === 'status' && draft === 'done' && !issue.assignee) return { ok: false, error: 'Assign someone before marking it done.' }; return { ok: true, value: draft }; };
|
|
97
|
+
const tripSet = key => v => setTrip(t => ({ ...t, [key]: v }));
|
|
98
|
+
const tripSave = async draft => { await plWait(200); return { ok: true, value: draft }; };
|
|
99
|
+
return <div className="pl-demo">
|
|
100
|
+
<section className="pl-side">
|
|
101
|
+
<h3 id="pl-issue">Issue details</h3>
|
|
102
|
+
<PropertyList aria-labelledby="pl-issue" shortcuts>
|
|
103
|
+
<PropertyListItem label="Status" value={issue.status} shortcut="s" commit="on-choose" onSave={save('status')} onValueChange={set('status')}
|
|
104
|
+
renderValue={s => <Badge tone={PL_TONE[s]}>{plLabel(PL_STATUS, s)}</Badge>} editor={c => <Select {...c} items={PL_STATUS} size="sm" />} />
|
|
105
|
+
<PropertyListItem label="Assignee" value={issue.assignee} shortcut="a" commit="on-choose" onSave={save('assignee')} onValueChange={set('assignee')}
|
|
106
|
+
renderValue={p => plLabel(PL_PEOPLE, p)} editor={c => <Combobox {...c} items={PL_PEOPLE} size="sm" />} />
|
|
107
|
+
<PropertyListItem label="Priority" value={issue.priority} emptyLabel="Add priority" commit="on-choose" onSave={save('priority')} onValueChange={set('priority')}
|
|
108
|
+
renderValue={p => plLabel(PL_PRIORITY, p)} editor={c => <Select {...c} items={PL_PRIORITY} size="sm" />} />
|
|
109
|
+
<PropertyListItem label="Estimate" value={issue.estimate} onSave={save('estimate')} onValueChange={set('estimate')} required
|
|
110
|
+
validate={n => Number.isInteger(n) && n >= 0 ? undefined : 'Use a whole number, 0 or more.'} renderValue={n => `${n} points`}
|
|
111
|
+
editor={({ value, onValueChange, ...c }) => <Input {...c} type="number" size="sm" value={value ?? ''} onChange={e => onValueChange(e.target.value === '' ? null : Number(e.target.value))} />} />
|
|
112
|
+
<PropertyListItem label="Created" value={issue.created} />
|
|
113
|
+
</PropertyList>
|
|
114
|
+
</section>
|
|
115
|
+
<section>
|
|
116
|
+
<h3 id="pl-trip">Check your answers</h3>
|
|
117
|
+
<PropertyList aria-labelledby="pl-trip" layout="inline"
|
|
118
|
+
editAll={{ label: 'trip details', value: trip, onValueChange: setTrip,
|
|
119
|
+
validate: d => d.end < d.start ? { end: 'The last day must be on or after the first.' } : undefined,
|
|
120
|
+
onSave: async d => { await plWait(250); return { ok: true, value: d }; },
|
|
121
|
+
renderEditor: field => <>
|
|
122
|
+
<Field label="Name" error={field.error('name')}><Input {...field('name')} /></Field>
|
|
123
|
+
<Field label="Email" error={field.error('email')}><Input {...field('email')} type="email" /></Field>
|
|
124
|
+
<Field label="First day" error={field.error('start')}><Input {...field('start')} type="date" /></Field>
|
|
125
|
+
<Field label="Last day" error={field.error('end')}><Input {...field('end')} type="date" /></Field>
|
|
126
|
+
<Field label="Phone"><Input {...field('phone')} type="tel" /></Field>
|
|
127
|
+
</> }}>
|
|
128
|
+
<PropertyListItem label="Name" value={trip.name} onSave={tripSave} onValueChange={tripSet('name')} required />
|
|
129
|
+
<PropertyListItem label="Email" value={trip.email} onSave={tripSave} onValueChange={tripSet('email')} required />
|
|
130
|
+
<PropertyListItem label="Dates" value={`${trip.start} to ${trip.end}`} />
|
|
131
|
+
<PropertyListItem label="Phone" value={trip.phone} emptyLabel="Add phone number" onSave={tripSave} onValueChange={tripSet('phone')} />
|
|
132
|
+
</PropertyList>
|
|
133
|
+
</section>
|
|
134
|
+
</div>;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
<!-- /example -->
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Question
|
|
2
|
+
|
|
3
|
+
An agent asks the person something before it carries on: "Which environment should I deploy to?", "Keep the old column or drop it?". The person picks one or several options, or writes their own answer, and the run continues. This is the elicitation half of human-in-the-loop (MCP elicitation, Claude Code's AskUserQuestion); `Confirmation` is the approval half.
|
|
4
|
+
|
|
5
|
+
**Elevation: status that asks, until it's answered.**
|
|
6
|
+
- **Asking** is `Confirmation`'s requested card: raised at level 1 ("The assistant is asking", the question as its title). Nothing has been dispatched, so it's raised, not pending. It arrives with the afterglow.
|
|
7
|
+
- **Options** are trenches: a radio group for one, checkboxes for several, with descriptions. **Your own answer** is a field (a well).
|
|
8
|
+
- **Answer** is the one primary; **Skip** (`skipLabel`) is a ghost button.
|
|
9
|
+
- **Answered**, it presses to the ground as one muted line in the record: "Which layout…? · You chose: Compact · 3 minutes ago". **Expired** says "No longer needed".
|
|
10
|
+
|
|
11
|
+
**Accessibility.** A section labelled by its question, holding a radio group (arrows) or a group of checkboxes; not a dialog. There's no default answer and nothing is focused for you; the composer keeps focus. Asked, it's announced assertively once ("The assistant is asking: …"), because the run is waiting on it; answered, politely ("Answered: Staging"). Enter in your own answer doesn't submit.
|
|
12
|
+
|
|
13
|
+
**Props:** `Question`: `title`, `state` (`'asking' | 'answered' | 'expired'`), `multiple`, `allowOther`, `otherLabel`, `answerLabel`, `skipLabel`, `onSkip`, `onAnswer({ values, other })`, `answer`, `answeredAt`, `requestedBy`, `name` (its key in a set). `QuestionOption`: `value`, `label`, `description`. `QuestionSet`: `state`, `onAnswer(answers by name)`, `answerLabel`, `skipLabel`, `onSkip`: several questions answered by one button.
|
|
14
|
+
|
|
15
|
+
**You provide:** the options, what answering does, and the answer to show once it's answered.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<Question title="Which environment should I deploy to?" state={q.state} answer={q.answer} allowOther
|
|
19
|
+
answerLabel="Deploy" skipLabel="Let the assistant decide" onAnswer={(a) => respond(q.id, a)} onSkip={() => skip(q.id)}>
|
|
20
|
+
<QuestionOption value="staging" label="Staging" description="Resets nightly. Safe to try things." />
|
|
21
|
+
<QuestionOption value="production" label="Production" description="Customers will see it immediately." />
|
|
22
|
+
</Question>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Do describe what each option means for the run.
|
|
26
|
+
- Do offer a way to let the assistant decide when a default exists.
|
|
27
|
+
- Don't preselect an option or move focus to the question.
|
|
28
|
+
|
|
29
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
`Question` as first used in its preview:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { Question, QuestionOption } from 'altitude-design';
|
|
36
|
+
|
|
37
|
+
<Question title="Which environment should I deploy to?" state={q.state} answer={q.answer} answeredAt={q.at} allowOther
|
|
38
|
+
answerLabel="Deploy" skipLabel="Let the assistant decide" onSkip={() => setQ({ state: 'expired' })}
|
|
39
|
+
onAnswer={(answer) => setQ({ state: 'answered', answer, at: Date.now() })}>
|
|
40
|
+
<QuestionOption value="staging" label="Staging" description="Resets nightly. Safe to try things." />
|
|
41
|
+
<QuestionOption value="production" label="Production" description="Customers will see it immediately." />
|
|
42
|
+
</Question>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The full preview, as rendered in Storybook and checked in CI (`qs` is the preview page's own layout class, not Altitude's):
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import * as React from 'react';
|
|
49
|
+
import { Question, QuestionOption, QuestionSet } from 'altitude-design';
|
|
50
|
+
|
|
51
|
+
export default function App() {
|
|
52
|
+
const [q, setQ] = React.useState({ state: 'asking' });
|
|
53
|
+
const [set, setSet] = React.useState({ state: 'asking' });
|
|
54
|
+
return (
|
|
55
|
+
<div className="qs">
|
|
56
|
+
<Question title="Which environment should I deploy to?" state={q.state} answer={q.answer} answeredAt={q.at} allowOther
|
|
57
|
+
answerLabel="Deploy" skipLabel="Let the assistant decide" onSkip={() => setQ({ state: 'expired' })}
|
|
58
|
+
onAnswer={(answer) => setQ({ state: 'answered', answer, at: Date.now() })}>
|
|
59
|
+
<QuestionOption value="staging" label="Staging" description="Resets nightly. Safe to try things." />
|
|
60
|
+
<QuestionOption value="production" label="Production" description="Customers will see it immediately." />
|
|
61
|
+
</Question>
|
|
62
|
+
<QuestionSet state={set.state} onAnswer={(answers) => setSet({ state: 'answered', answers })}>
|
|
63
|
+
<Question name="column" title="Keep the old status column, or drop it?" answer={set.answers?.column}>
|
|
64
|
+
<QuestionOption value="keep" label="Keep it" description="Writes go to both columns until you remove it." />
|
|
65
|
+
<QuestionOption value="drop" label="Drop it" description="The migration removes it after copying." />
|
|
66
|
+
</Question>
|
|
67
|
+
<Question name="notify" title="Who should hear about it?" multiple answer={set.answers?.notify}>
|
|
68
|
+
<QuestionOption value="eng" label="#eng-billing" />
|
|
69
|
+
<QuestionOption value="support" label="#support" />
|
|
70
|
+
<QuestionOption value="owner" label="The account owner" />
|
|
71
|
+
</Question>
|
|
72
|
+
</QuestionSet>
|
|
73
|
+
<Question title="Which layout should I use for the invoice?" state="answered" answer={{ values: ['compact'] }} answeredAt={Date.now() - 3 * 60000}>
|
|
74
|
+
<QuestionOption value="compact" label="Compact" />
|
|
75
|
+
<QuestionOption value="detailed" label="Detailed" />
|
|
76
|
+
</Question>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
<!-- /example -->
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# RadioGroup
|
|
2
|
+
|
|
3
|
+
Chooses exactly one option. A radio group is built like the Switch with more positions: **one switch, several positions, one thumb**.
|
|
4
|
+
|
|
5
|
+
Each option is a round trench, the same as the switch track. The group owns a single raised thumb; choosing an option slides it to that track on `ease-settle`, and the accent pours into the track from under the thumb as it arrives while the old one drains. Because there's only one thumb, the choice can't be in two places, and its movement shows where it came from. Hovering an unchosen option shows a faint ghost of the thumb where it would land (feedforward); hovering the chosen option lifts the thumb; pressing it stretches the thumb, like a switch. Arrow keys move the choice, as native radios do, and the thumb follows.
|
|
6
|
+
|
|
7
|
+
**You provide:** `Radio` children with `value`, `label` and optional `description`; `value`/`onValueChange` or `defaultValue`; an `aria-label` for the group if there's no visible heading; `orientation="horizontal"` for two or three short options.
|
|
8
|
+
|
|
9
|
+
- Default to a sensible choice; a group with no thumb is rarely what you want.
|
|
10
|
+
- Don't use radios for more than about six options; use a menu.
|
|
11
|
+
|
|
12
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
`RadioGroup` as first used in its preview:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { Radio, RadioGroup } from 'altitude-design';
|
|
19
|
+
|
|
20
|
+
<RadioGroup defaultValue="team" aria-label="Plan">
|
|
21
|
+
<Radio value="solo" label="Solo" description="One seat, unlimited projects." />
|
|
22
|
+
<Radio value="team" label="Team" description="Up to 20 seats and shared libraries." />
|
|
23
|
+
<Radio value="org" label="Organization" disabled description="Talk to us." />
|
|
24
|
+
</RadioGroup>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The full preview, as rendered in Storybook and checked in CI (`pair` is the preview page's own layout class, not Altitude's):
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { Radio, RadioGroup } from 'altitude-design';
|
|
31
|
+
|
|
32
|
+
export default function App() {
|
|
33
|
+
return (
|
|
34
|
+
<div className="pair">
|
|
35
|
+
<RadioGroup defaultValue="team" aria-label="Plan">
|
|
36
|
+
<Radio value="solo" label="Solo" description="One seat, unlimited projects." />
|
|
37
|
+
<Radio value="team" label="Team" description="Up to 20 seats and shared libraries." />
|
|
38
|
+
<Radio value="org" label="Organization" disabled description="Talk to us." />
|
|
39
|
+
</RadioGroup>
|
|
40
|
+
<RadioGroup defaultValue="m" orientation="horizontal" aria-label="Size">
|
|
41
|
+
<Radio value="s" label="Small" /><Radio value="m" label="Medium" /><Radio value="l" label="Large" />
|
|
42
|
+
</RadioGroup>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
<!-- /example -->
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# RelativeTime
|
|
2
|
+
|
|
3
|
+
A moment shown as "3 minutes ago", for activity feeds, audit logs, comments and "last edited". `formatRelativeTime()` gives the same words as a string.
|
|
4
|
+
|
|
5
|
+
**Elevation.** A timestamp is part of the record, so it's plain text on the ground. **Ticking forward is not a change to the record:** "3 minutes ago" becoming "4 minutes ago" swaps the words with no transition and no afterglow. When the moment itself changes (the record was edited again), that is a change, and the time takes the same ground-level tint as a changed `FormattedNumber`, decaying over `dur-settle`.
|
|
6
|
+
|
|
7
|
+
**Refreshing.** Every RelativeTime on the page shares one timer (every 15 seconds). It stops while the page is hidden and catches up as soon as the page is visible again. Words come from `Intl.RelativeTimeFormat` with `numeric: "auto"`, so you get "now", "yesterday" and "last month", pluralised for the locale.
|
|
8
|
+
|
|
9
|
+
**Formats.** `relative` (default) always says how long ago or until. `auto` is relative within `threshold` (an ISO 8601 duration such as `"P7D"` or `"PT12H"`, or milliseconds; default 7 days), then a date ("Mar 4", or "Mar 4, 2025" in another year). `tense="past"` never says "in 2 minutes", even when a client's clock runs ahead, and `tense="future"` never says "ago".
|
|
10
|
+
|
|
11
|
+
**Accessibility.** It renders a `<time dateTime="…ISO…">`. With `tooltip` (on by default) it's focusable, and the exact date, time and zone ("Wednesday, October 29, 2026 at 3:04:12 PM EDT") is shown in a `Tooltip` on hover and focus and attached with `aria-describedby`. With `tooltip={false}` the exact time is its `title`. Ticking is never a live region. Under forced colours a new moment's tint is a `Highlight` outline.
|
|
12
|
+
|
|
13
|
+
**Props:** `date` (a `Date`, ISO string or epoch ms); `format` (`'relative' | 'auto'`); `threshold`; `tense` (`'auto' | 'past' | 'future'`); `locale`; `tooltip` (default true).
|
|
14
|
+
|
|
15
|
+
**You provide:** the moment, and whether old moments should become dates (`format="auto"`).
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<RelativeTime date={event.createdAt} /> // "3 minutes ago"
|
|
19
|
+
<RelativeTime date={due} format="auto" threshold="P7D" /> // relative within 7 days, then "Mar 4"
|
|
20
|
+
<RelativeTime date={d} locale="fr" tense="past" /> // "il y a 3 minutes"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Do use `format="auto"` in logs and feeds that reach back further than a week.
|
|
24
|
+
- Do keep the tooltip on wherever the exact time matters (audit logs, billing).
|
|
25
|
+
- Don't put a RelativeTime in a live region, or announce it ticking.
|
|
26
|
+
- Don't format times by hand with string concatenation.
|
|
27
|
+
|
|
28
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
29
|
+
## Example
|
|
30
|
+
|
|
31
|
+
`RelativeTime` as first used in its preview:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { RelativeTime } from 'altitude-design';
|
|
35
|
+
|
|
36
|
+
<RelativeTime date={when} format="auto" />
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The full preview, as rendered in Storybook and checked in CI (`rt`, `col`, `row`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import * as React from 'react';
|
|
43
|
+
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, RelativeTime } from 'altitude-design';
|
|
44
|
+
|
|
45
|
+
const ago = (ms) => new Date(Date.now() - ms);
|
|
46
|
+
const events = [
|
|
47
|
+
['Deploy #482 finished', ago(20_000)],
|
|
48
|
+
['Maya invited Theo', ago(3 * 60_000)],
|
|
49
|
+
['Invoice INV-0042 paid', ago(5 * 3_600_000)],
|
|
50
|
+
['API key rotated', ago(26 * 3_600_000)],
|
|
51
|
+
['Workspace created', ago(40 * 86_400_000)],
|
|
52
|
+
];
|
|
53
|
+
export default function App() {
|
|
54
|
+
const [edited, setEdited] = React.useState(() => ago(12 * 60_000));
|
|
55
|
+
return (
|
|
56
|
+
<div className="rt">
|
|
57
|
+
<ul aria-label="Activity">
|
|
58
|
+
{events.map(([what, when]) => <li key={what}><span>{what}</span><RelativeTime date={when} format="auto" /></li>)}
|
|
59
|
+
</ul>
|
|
60
|
+
<Card>
|
|
61
|
+
<CardHeader><CardTitle>Q3 board deck</CardTitle><CardDescription>Last edited <RelativeTime date={edited} /></CardDescription></CardHeader>
|
|
62
|
+
<CardContent>
|
|
63
|
+
<div className="col">
|
|
64
|
+
<div className="row"><Button size="sm" onClick={() => setEdited(new Date())}>Edit again</Button></div>
|
|
65
|
+
<p className="cap">Time passing changes the words with nothing else. A new moment (an edit) settles with a tint. Point at or tab to a time for its exact date, time and zone. Past a week, the feed shows a date.</p>
|
|
66
|
+
</div>
|
|
67
|
+
</CardContent>
|
|
68
|
+
</Card>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
<!-- /example -->
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# RemovalReceipt
|
|
2
|
+
|
|
3
|
+
The slot of an item that can leave its list: a file archived, a webhook removed, an invitation revoked. When the item goes, its result shows in its own slot, at the height it had, so nothing on the page moves while it's read. The slot holds, lifts its receipt off, and only then glides the gap closed. Use it instead of removing the row at once and adding an Alert below the list, which reshapes the page twice and puts the feedback away from where the person acted.
|
|
4
|
+
|
|
5
|
+
**Left in place, then closed.** With `removed`, the item's content cross-fades (`dur-travel-2`) to the receipt: a check (or, with `tone="critical"`, an alert icon), your `title`, and your `action`. While `pending`, the write isn't confirmed: the receipt shows a spinner in place of the check, the slot hovers as pending only after `dur-grace` (a fast write never flickers), and the hold hasn't started. Once confirmed, the receipt holds for `dur-linger` (2000ms), then lifts off as an Alert does (8px, in 70% of `dur-travel-3`, `ease-sink`), and the gap glides shut (`dur-travel-3`, `ease-glide`). The rows below are pushed by the height, not animated on their own. `onClosed` is called once the gap has closed: drop the item from your list there.
|
|
6
|
+
|
|
7
|
+
**The hold waits for the person.** It pauses while the receipt is pointed at or focused, and while a modal owns the document. When it's due and a mouse is still over the list (the slot's parent), it waits for the pointer to leave, up to `dur-settle`, so the next row doesn't slide under a pointer reaching for it. Touch has no hover, so on touch it closes when the hold ends. Receipts due within 250ms of each other close together, as one change.
|
|
8
|
+
|
|
9
|
+
**Why it's on the ground.** The receipt is a trace of what happened to the record, not a live surface, so it doesn't rise for attention: muted ink on the ground. Only its exit lifts off. Pending uses the shared pending signal (`surface-lifted`, `elev-2`, breathing over `dur-hover`). An item brought back from elsewhere (`arrive`) glides its slot open and settles with an afterglow; an item restored while its receipt is still held (`removed` back to false) returns in place with an afterglow.
|
|
10
|
+
|
|
11
|
+
**Accessibility.** The receipt's text is a polite live region (`role="status"`) present from the slot's first render, so the result is announced when it appears. Put Undo in `action` and name it fully (`aria-label="Undo archive of Brand guidelines.pdf"`). Before the slot closes, focus inside it moves to the first focusable control of the next slot, else the previous one, else `focusFallback()`. Under reduced motion the receipt still fades and holds, nothing travels, and the gap closes at once. Under forced colours the receipt is outlined in `CanvasText`.
|
|
12
|
+
|
|
13
|
+
**Props:** `removed`; `pending`; `title`; `action`; `tone` (`neutral` | `critical`); `icon`; `hold` (ms, default `dur-linger`); `onClosed`; `focusFallback`; `arrive`; `settle`; `as` (`div` | `li` | `section` | `article`, default `div`). Other props go to the slot's element, and the item's own content (`children`) keeps the slot's layout (its wrapper is `display: contents`).
|
|
14
|
+
|
|
15
|
+
**You provide:** the item, whether it's removed and whether that's confirmed, the words and the actions, and what happens next. Undo has to call your real inverse operation. After the slot closes, offer Undo in a `toast` if you like, but keep a durable recovery route (Archived → Restore), and withdraw a toast's Undo (`toast.dismiss(id)`) once a newer revision makes it invalid. See [Actions in place](../../visual/ActionsInPlace.md) for the full recipe: pending, failure, conflict, late receipts and focus.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
<ul>
|
|
19
|
+
{files.map((file) => (
|
|
20
|
+
<RemovalReceipt as="li" key={file.id} removed={!!receipts[file.id]} pending={receipts[file.id]?.pending}
|
|
21
|
+
title={`${file.name} archived.`}
|
|
22
|
+
action={<Button size="sm" aria-label={`Undo archive of ${file.name}`} onClick={() => restore(file)}>Undo</Button>}
|
|
23
|
+
onClosed={() => { dropReceipt(file.id); toast({ title: `${file.name} archived.`, action: { label: 'Undo', onClick: () => restore(file) } }); }}>
|
|
24
|
+
<FileRow file={file} onArchive={() => archive(file)} />
|
|
25
|
+
</RemovalReceipt>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Do keep the item in its slot until its removal is confirmed; show a failure in the restored row, not somewhere else.
|
|
31
|
+
- Do put Undo where the removing action was, so a second click under the pointer reverses rather than navigates.
|
|
32
|
+
- Don't hold the receipt for longer to "make sure": the person can stop it by pointing at it or focusing it.
|
|
33
|
+
- Don't use it for a line removed while typing (an empty `Checklist` line); nothing was lost, and the hold would sit in the way. Checklist glides the line shut instead.
|
|
34
|
+
- Don't use it for permanent deletion without saying so: a deletion's receipt has no Undo.
|
|
35
|
+
|
|
36
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
37
|
+
## Example
|
|
38
|
+
|
|
39
|
+
`RemovalReceipt` as first used in its preview:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Button, RemovalReceipt, toast } from 'altitude-design';
|
|
43
|
+
|
|
44
|
+
<RemovalReceipt as="li" key={hook.id} removed={!!receipt} pending={receipt?.pending} arrive={back === hook.id}
|
|
45
|
+
title={receipt?.undoing ? `Restoring ${hook.name}…` : receipt?.pending ? `Removing ${hook.name}…` : `${hook.name} removed.`}
|
|
46
|
+
action={receipt && (!receipt.pending || receipt.undoing) &&
|
|
47
|
+
<Button size="sm" loading={!!receipt.undoing} aria-label={`Undo removing ${hook.name}`} onClick={() => undo(hook)}>Undo</Button>}
|
|
48
|
+
onClosed={() => {
|
|
49
|
+
put(hook.id, null); setHooks((list) => list.filter((h) => h.id !== hook.id));
|
|
50
|
+
toast({ title: `${hook.name} removed.`, action: { label: 'Undo', onClick: () => restore(hook) } });
|
|
51
|
+
}}>
|
|
52
|
+
<div className="rr-body"><div className="rr-name">{hook.name}</div><div className="rr-url">{hook.url}</div></div>
|
|
53
|
+
<Button size="sm" variant="ghost" aria-label={`Remove ${hook.name}`} onClick={() => remove(hook)}>Remove</Button>
|
|
54
|
+
</RemovalReceipt>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The full preview, as rendered in Storybook and checked in CI (`rr`, `rr-hint`, `rr-body`, `rr-name`, `rr-url`, `rr-empty` are the preview page's own layout classes, not Altitude's):
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import * as React from 'react';
|
|
61
|
+
import { Button, RemovalReceipt, Toaster, toast } from 'altitude-design';
|
|
62
|
+
|
|
63
|
+
const HOOKS = [
|
|
64
|
+
{ id: 'h1', name: 'Deploy notifications', url: 'https://hooks.example.com/deploys' },
|
|
65
|
+
{ id: 'h2', name: 'Billing events', url: 'https://hooks.example.com/billing' },
|
|
66
|
+
{ id: 'h3', name: 'New sign-ups', url: 'https://hooks.example.com/signups' },
|
|
67
|
+
{ id: 'h4', name: 'Error alerts', url: 'https://hooks.example.com/errors' },
|
|
68
|
+
];
|
|
69
|
+
// A fictional service: removing and restoring a webhook each take 400ms.
|
|
70
|
+
const later = (value) => new Promise((resolve) => setTimeout(() => resolve(value), 400));
|
|
71
|
+
export default function App() {
|
|
72
|
+
const [hooks, setHooks] = React.useState(HOOKS);
|
|
73
|
+
const [receipts, setReceipts] = React.useState({});
|
|
74
|
+
const [back, setBack] = React.useState(null);
|
|
75
|
+
const put = (id, value) => setReceipts((r) => { const next = { ...r }; if (value) next[id] = value; else delete next[id]; return next; });
|
|
76
|
+
const remove = async (hook) => { put(hook.id, { pending: true }); await later(); put(hook.id, { pending: false }); };
|
|
77
|
+
const undo = async (hook) => { put(hook.id, { pending: true, undoing: true }); await later(); put(hook.id, null); };
|
|
78
|
+
const restore = async (hook) => {
|
|
79
|
+
await later();
|
|
80
|
+
setHooks((list) => HOOKS.filter((h) => h.id === hook.id || list.some((x) => x.id === h.id)));
|
|
81
|
+
setBack(hook.id);
|
|
82
|
+
};
|
|
83
|
+
return (
|
|
84
|
+
<section className="rr" aria-labelledby="rr-title">
|
|
85
|
+
<h3 id="rr-title">Webhooks</h3>
|
|
86
|
+
<p className="rr-hint">Remove one: its result stays in its place, then the list closes up. Point at the result to keep it.</p>
|
|
87
|
+
<ul>
|
|
88
|
+
{hooks.map((hook) => {
|
|
89
|
+
const receipt = receipts[hook.id];
|
|
90
|
+
return (
|
|
91
|
+
<RemovalReceipt as="li" key={hook.id} removed={!!receipt} pending={receipt?.pending} arrive={back === hook.id}
|
|
92
|
+
title={receipt?.undoing ? `Restoring ${hook.name}…` : receipt?.pending ? `Removing ${hook.name}…` : `${hook.name} removed.`}
|
|
93
|
+
action={receipt && (!receipt.pending || receipt.undoing) &&
|
|
94
|
+
<Button size="sm" loading={!!receipt.undoing} aria-label={`Undo removing ${hook.name}`} onClick={() => undo(hook)}>Undo</Button>}
|
|
95
|
+
onClosed={() => {
|
|
96
|
+
put(hook.id, null); setHooks((list) => list.filter((h) => h.id !== hook.id));
|
|
97
|
+
toast({ title: `${hook.name} removed.`, action: { label: 'Undo', onClick: () => restore(hook) } });
|
|
98
|
+
}}>
|
|
99
|
+
<div className="rr-body"><div className="rr-name">{hook.name}</div><div className="rr-url">{hook.url}</div></div>
|
|
100
|
+
<Button size="sm" variant="ghost" aria-label={`Remove ${hook.name}`} onClick={() => remove(hook)}>Remove</Button>
|
|
101
|
+
</RemovalReceipt>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</ul>
|
|
105
|
+
{!hooks.length && <p className="rr-empty">No webhooks.</p>}
|
|
106
|
+
<Toaster />
|
|
107
|
+
</section>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
<!-- /example -->
|