altitude-design 0.4.0 → 0.4.2
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 +1 -0
- package/dist/guidance/components/AILabel/README.md +35 -0
- package/dist/guidance/components/Accordion/README.md +25 -0
- package/dist/guidance/components/AgentFleet/README.md +48 -1
- package/dist/guidance/components/AgentLanes/README.md +32 -0
- package/dist/guidance/components/AgentRequests/README.md +47 -0
- package/dist/guidance/components/Alert/README.md +49 -0
- package/dist/guidance/components/AltitudeProvider/README.md +63 -0
- package/dist/guidance/components/AppShell/README.md +70 -0
- package/dist/guidance/components/AreaChart/README.md +58 -0
- package/dist/guidance/components/Artifact/README.md +69 -0
- package/dist/guidance/components/Avatar/README.md +41 -0
- package/dist/guidance/components/Await/README.md +48 -0
- package/dist/guidance/components/Badge/README.md +44 -0
- package/dist/guidance/components/BarChart/README.md +62 -0
- package/dist/guidance/components/BarList/README.md +58 -0
- package/dist/guidance/components/Board/README.md +67 -0
- package/dist/guidance/components/Breadcrumb/README.md +31 -0
- package/dist/guidance/components/Button/README.md +41 -0
- package/dist/guidance/components/Card/README.md +38 -0
- package/dist/guidance/components/ChartBrush/README.md +57 -0
- package/dist/guidance/components/ChartContainer/README.md +77 -0
- package/dist/guidance/components/ChartLegend/README.md +43 -0
- package/dist/guidance/components/ChartReference/README.md +74 -0
- package/dist/guidance/components/ChartTooltip/README.md +63 -0
- package/dist/guidance/components/Checkbox/README.md +35 -0
- package/dist/guidance/components/Checklist/README.md +66 -0
- package/dist/guidance/components/CodeBlock/README.md +44 -0
- package/dist/guidance/components/ColorPicker/README.md +39 -0
- package/dist/guidance/components/Combobox/README.md +35 -0
- package/dist/guidance/components/CommandPalette/README.md +177 -0
- package/dist/guidance/components/CommentThread/README.md +51 -0
- package/dist/guidance/components/Confirmation/README.md +60 -0
- package/dist/guidance/components/ContextMenu/README.md +51 -0
- package/dist/guidance/components/Conversation/README.md +124 -0
- package/dist/guidance/components/CopyButton/README.md +31 -0
- package/dist/guidance/components/DataGrid/README.md +75 -0
- package/dist/guidance/components/DatePicker/README.md +49 -0
- package/dist/guidance/components/Dialog/README.md +175 -0
- package/dist/guidance/components/DiffView/README.md +77 -0
- package/dist/guidance/components/DotPlot/README.md +44 -0
- package/dist/guidance/components/Drilldown/README.md +28 -0
- package/dist/guidance/components/DropZone/README.md +50 -0
- package/dist/guidance/components/DropdownMenu/README.md +44 -0
- package/dist/guidance/components/EditRegion/README.md +159 -0
- package/dist/guidance/components/EmptyState/README.md +49 -0
- package/dist/guidance/components/FeatureGate/README.md +39 -0
- package/dist/guidance/components/Field/README.md +36 -0
- package/dist/guidance/components/FilterBar/README.md +63 -0
- package/dist/guidance/components/FormattedNumber/README.md +44 -0
- package/dist/guidance/components/Heatmap/README.md +71 -0
- package/dist/guidance/components/HoldButton/README.md +32 -0
- package/dist/guidance/components/HoverCard/README.md +53 -0
- package/dist/guidance/components/InlineCitation/README.md +32 -0
- package/dist/guidance/components/InlineEdit/README.md +247 -1
- package/dist/guidance/components/Input/README.md +36 -0
- package/dist/guidance/components/InputOTP/README.md +46 -0
- package/dist/guidance/components/ItemGroup/README.md +100 -0
- package/dist/guidance/components/Kbd/README.md +27 -0
- package/dist/guidance/components/LineChart/README.md +62 -0
- package/dist/guidance/components/Link/README.md +41 -0
- package/dist/guidance/components/Markdown/README.md +81 -0
- package/dist/guidance/components/Meter/README.md +52 -0
- package/dist/guidance/components/NotificationCenter/README.md +39 -0
- package/dist/guidance/components/NumberInput/README.md +37 -0
- package/dist/guidance/components/PageHeader/README.md +45 -0
- package/dist/guidance/components/Pagination/README.md +33 -0
- package/dist/guidance/components/PasswordInput/README.md +36 -0
- package/dist/guidance/components/PermissionMatrix/README.md +40 -0
- package/dist/guidance/components/Pile/README.md +42 -0
- package/dist/guidance/components/PlanPicker/README.md +37 -0
- package/dist/guidance/components/Popover/README.md +36 -0
- package/dist/guidance/components/Progress/README.md +31 -0
- package/dist/guidance/components/PromptInput/README.md +98 -0
- package/dist/guidance/components/PropertyList/README.md +80 -0
- package/dist/guidance/components/Question/README.md +54 -0
- package/dist/guidance/components/RadioGroup/README.md +37 -0
- package/dist/guidance/components/RelativeTime/README.md +47 -0
- package/dist/guidance/components/RemovalReceipt/README.md +77 -0
- package/dist/guidance/components/Resizable/README.md +29 -0
- package/dist/guidance/components/SaveBar/README.md +38 -0
- package/dist/guidance/components/SavedViews/README.md +58 -0
- package/dist/guidance/components/ScatterChart/README.md +63 -0
- package/dist/guidance/components/Schedule/README.md +66 -0
- package/dist/guidance/components/ScrollArea/README.md +28 -0
- package/dist/guidance/components/SecretField/README.md +46 -0
- package/dist/guidance/components/Select/README.md +70 -0
- package/dist/guidance/components/Separator/README.md +44 -0
- package/dist/guidance/components/Sheet/README.md +94 -0
- package/dist/guidance/components/Skeleton/README.md +36 -0
- package/dist/guidance/components/Slider/README.md +35 -0
- package/dist/guidance/components/SortableList/README.md +34 -0
- package/dist/guidance/components/Sparkline/README.md +58 -0
- package/dist/guidance/components/SplitButton/README.md +39 -0
- package/dist/guidance/components/Stat/README.md +61 -0
- package/dist/guidance/components/Stepper/README.md +61 -0
- package/dist/guidance/components/Suggestions/README.md +58 -0
- package/dist/guidance/components/Surface/README.md +28 -0
- package/dist/guidance/components/Switch/README.md +29 -0
- package/dist/guidance/components/SyncIndicator/README.md +54 -0
- package/dist/guidance/components/Table/README.md +62 -0
- package/dist/guidance/components/Tabs/README.md +55 -0
- package/dist/guidance/components/TagInput/README.md +50 -0
- package/dist/guidance/components/Terminal/README.md +42 -0
- package/dist/guidance/components/ThreadList/README.md +73 -0
- package/dist/guidance/components/TimeInput/README.md +37 -0
- package/dist/guidance/components/Timeline/README.md +55 -0
- package/dist/guidance/components/Toaster/README.md +38 -0
- package/dist/guidance/components/Toggle/README.md +52 -0
- package/dist/guidance/components/Toolbar/README.md +69 -0
- package/dist/guidance/components/Tooltip/README.md +29 -0
- package/dist/guidance/components/Tour/README.md +44 -0
- package/dist/guidance/components/Trace/README.md +92 -0
- package/dist/guidance/components/Tree/README.md +85 -0
- package/dist/guidance/index.json +117 -117
- package/dist/guidance/llms.txt +1 -1
- package/dist/guidance/manifest.json +120 -120
- package/dist/guidance/visual/ActionsInPlace.md +5 -5
- package/dist/guidance/visual/Distinctiveness.md +6 -2
- package/dist/guidance/visual/Research.md +14 -2
- package/dist/guidance/visual/Theming.md +1 -1
- 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
|
@@ -32,3 +32,63 @@ Approve or reject an action an agent proposes, before anything happens: "Send 21
|
|
|
32
32
|
- Do name the outcome on Approve.
|
|
33
33
|
- Don't draw a proposal as pending.
|
|
34
34
|
- Don't remove a rejected proposal; keep it in the record.
|
|
35
|
+
|
|
36
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
37
|
+
## Example
|
|
38
|
+
|
|
39
|
+
`Confirmation` as first used in its preview:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Confirmation } from 'altitude-design';
|
|
43
|
+
|
|
44
|
+
<Confirmation state={state} title={title} consequence={consequence} escalate={escalate} approveLabel={approveLabel} rejectLabel={rejectLabel}
|
|
45
|
+
runningLabel={runningLabel} doneLabel={doneLabel} failedLabel={failedLabel} resolvedBy="you" resolvedAt={at} scopes={scopes} resolvedScope={scope}
|
|
46
|
+
onApprove={approve} onReject={() => { setState('rejected'); setAt(new Date()); }} onRetry={() => { setState('running'); setTimeout(() => { setState('done'); setAt(new Date()); }, 1000); }} />
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The full preview, as rendered in Storybook and checked in CI (`col` is the preview page's own layout class, not Altitude's):
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import * as React from 'react';
|
|
53
|
+
import { Confirmation, Field, Input, Select } from 'altitude-design';
|
|
54
|
+
|
|
55
|
+
function Proposal({ escalate, title, consequence, approveLabel, rejectLabel, runningLabel, doneLabel, failedLabel, fails, scopes }) {
|
|
56
|
+
const [state, setState] = React.useState('requested');
|
|
57
|
+
const [scope, setScope] = React.useState();
|
|
58
|
+
const [at, setAt] = React.useState();
|
|
59
|
+
const approve = (s) => { setScope(s); setState('running'); setTimeout(() => { setState(fails ? 'failed' : 'done'); setAt(new Date()); }, 1400); };
|
|
60
|
+
return <Confirmation state={state} title={title} consequence={consequence} escalate={escalate} approveLabel={approveLabel} rejectLabel={rejectLabel}
|
|
61
|
+
runningLabel={runningLabel} doneLabel={doneLabel} failedLabel={failedLabel} resolvedBy="you" resolvedAt={at} scopes={scopes} resolvedScope={scope}
|
|
62
|
+
onApprove={approve} onReject={() => { setState('rejected'); setAt(new Date()); }} onRetry={() => { setState('running'); setTimeout(() => { setState('done'); setAt(new Date()); }, 1000); }} />;
|
|
63
|
+
}
|
|
64
|
+
// Edit what's proposed before approving: the consequence and the button follow the edited values.
|
|
65
|
+
function EditableProposal() {
|
|
66
|
+
const original = { subject: 'October product update', segment: 'Pro customers', n: 214 };
|
|
67
|
+
const [v, setV] = React.useState(original);
|
|
68
|
+
const [state, setState] = React.useState('requested');
|
|
69
|
+
const [at, setAt] = React.useState();
|
|
70
|
+
const counts = { 'Pro customers': 214, 'All customers': 1180, 'Trial users': 96 };
|
|
71
|
+
const edited = v.subject !== original.subject || v.segment !== original.segment;
|
|
72
|
+
return <Confirmation state={state} title="Send campaign email" consequence={`${v.n} ${v.segment} will receive “${v.subject}”.`}
|
|
73
|
+
approveLabel={`Send ${v.n} emails`} rejectLabel="Don't send" runningLabel="Sending…" doneLabel="Sent" resolvedBy="you" resolvedAt={at} edited={edited}
|
|
74
|
+
edit={<>
|
|
75
|
+
<Field label="Subject"><Input value={v.subject} onChange={(e) => setV({ ...v, subject: e.target.value })} /></Field>
|
|
76
|
+
<Field label="Segment"><Select value={v.segment} onValueChange={(segment) => setV({ ...v, segment, n: counts[segment] })} items={Object.keys(counts).map((k) => ({ value: k, label: k }))} /></Field>
|
|
77
|
+
</>}
|
|
78
|
+
onEditCancel={() => setV(original)}
|
|
79
|
+
onApprove={() => { setState('running'); setTimeout(() => { setState('done'); setAt(new Date()); }, 1200); }} onReject={() => { setState('rejected'); setAt(new Date()); }} />;
|
|
80
|
+
}
|
|
81
|
+
export default function App() {
|
|
82
|
+
return (
|
|
83
|
+
<div className="col">
|
|
84
|
+
<Proposal title="Send campaign email" consequence="214 Pro customers will receive “October product update”." approveLabel="Send 214 emails" rejectLabel="Don't send" runningLabel="Sending…" doneLabel="Sent" failedLabel="Couldn't send" />
|
|
85
|
+
<Proposal title="Delete 3 stale branches" consequence="release/2.3, spike/maps and fix/old-auth will be deleted from the repository." approveLabel="Delete 3 branches" rejectLabel="Keep them" runningLabel="Deleting…" doneLabel="Deleted" escalate="hold" />
|
|
86
|
+
<Proposal title="Archive the Atlas workspace" consequence="12 people lose access; it can't be restored after 30 days." approveLabel="Archive workspace" rejectLabel="Don't archive" runningLabel="Archiving…" failedLabel="Couldn't archive" escalate="dialog" fails />
|
|
87
|
+
<Proposal title="Run npm test" consequence="Runs the test suite in this workspace." approveLabel="Run once" rejectLabel="Don't run" runningLabel="Running…" doneLabel="Ran"
|
|
88
|
+
scopes={[{ value: 'session', label: 'Allow npm test for this session' }, { value: 'always', label: 'Always allow npm test', description: 'Saved to this project’s settings' }]} />
|
|
89
|
+
<EditableProposal />
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
<!-- /example -->
|
|
@@ -28,3 +28,54 @@ Actions on a thing (a file, a row, a board card, a tree node, a canvas object),
|
|
|
28
28
|
- Do offer the same actions in a visible menu.
|
|
29
29
|
- Don't nest menus; open a `Dialog` for a long choice.
|
|
30
30
|
- Don't raise the thing while its menu is open; it's outlined.
|
|
31
|
+
|
|
32
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
33
|
+
## Example
|
|
34
|
+
|
|
35
|
+
`ContextMenu` as first used in its preview:
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { Button, ContextMenu, DropdownMenu, Icons } from 'altitude-design';
|
|
39
|
+
|
|
40
|
+
<ContextMenu key={f} items={() => actions(f)}>
|
|
41
|
+
<div className="file" role="listitem" tabIndex={0}>
|
|
42
|
+
<span className="name">{f}</span><span className="meta">Edited 2 days ago</span>
|
|
43
|
+
<DropdownMenu align="end" items={actions(f)} trigger={<Button variant="ghost" size="sm" square aria-label={`Actions for ${f}`}><Icons.More /></Button>} />
|
|
44
|
+
</div>
|
|
45
|
+
</ContextMenu>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The full preview, as rendered in Storybook and checked in CI (`col`, `cm2`, `file`, `name`, `meta`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import * as React from 'react';
|
|
52
|
+
import { Button, ContextMenu, DropdownMenu, Icons } from 'altitude-design';
|
|
53
|
+
|
|
54
|
+
export default function App() {
|
|
55
|
+
const [files, setFiles] = React.useState(['Q3 board deck.pdf', 'Invoices 2026.xlsx', 'Brand guidelines.fig']);
|
|
56
|
+
const [said, setSaid] = React.useState('');
|
|
57
|
+
const actions = (f) => [
|
|
58
|
+
{ label: 'Open', onSelect: () => setSaid(`Opened ${f}.`) },
|
|
59
|
+
{ label: 'Rename', onSelect: () => setSaid(`Renaming ${f}…`) },
|
|
60
|
+
{ label: 'Duplicate', onSelect: () => setSaid(`Duplicated ${f}.`) },
|
|
61
|
+
{ type: 'separator' },
|
|
62
|
+
{ label: 'Delete file', tone: 'critical', onSelect: () => { setFiles((xs) => xs.filter((x) => x !== f)); setSaid(`Deleted ${f}.`); } },
|
|
63
|
+
];
|
|
64
|
+
return (
|
|
65
|
+
<div className="col">
|
|
66
|
+
<div className="cm2" role="list" aria-label="Files">
|
|
67
|
+
{files.map((f) => (
|
|
68
|
+
<ContextMenu key={f} items={() => actions(f)}>
|
|
69
|
+
<div className="file" role="listitem" tabIndex={0}>
|
|
70
|
+
<span className="name">{f}</span><span className="meta">Edited 2 days ago</span>
|
|
71
|
+
<DropdownMenu align="end" items={actions(f)} trigger={<Button variant="ghost" size="sm" square aria-label={`Actions for ${f}`}><Icons.More /></Button>} />
|
|
72
|
+
</div>
|
|
73
|
+
</ContextMenu>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
<p className="cap" role="status">{said || 'Right-click a file (or long-press, or focus it and press Shift+F10). The same actions are behind each row\'s ⋯ button, because a context menu is never the only way.'}</p>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
<!-- /example -->
|
|
@@ -43,3 +43,127 @@ The thread between a person and an assistant: their messages, the assistant's an
|
|
|
43
43
|
- Do keep earlier versions when regenerating.
|
|
44
44
|
- Don't fade in text word by word or glow a streaming answer.
|
|
45
45
|
- Don't make a streaming answer hover; it isn't a pending write.
|
|
46
|
+
|
|
47
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
48
|
+
## Example
|
|
49
|
+
|
|
50
|
+
`Conversation` as first used in its preview:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { Conversation, ConversationCheckpoint, ConversationContent, ConversationScrollButton, Message, MessageActions, MessageAttachments, MessageContent } from 'altitude-design';
|
|
54
|
+
|
|
55
|
+
<Conversation aria-label="Assistant">
|
|
56
|
+
<ConversationContent>
|
|
57
|
+
{msgs.map((m) => m.kind === 'checkpoint' ? (
|
|
58
|
+
<ConversationCheckpoint key={m.id} label={m.label} at={m.at} onRestore={() => restore(m.id)} onReturn={giveBack}
|
|
59
|
+
consequence="The messages after this will be set aside. You can return to them." />
|
|
60
|
+
) : (
|
|
61
|
+
<Message key={m.id} from={m.from} status={m.status} edited={m.v > 0} onRetry={m.status === 'stopped' ? () => { setStatus('streaming'); stream(m.id); } : undefined}>
|
|
62
|
+
{m.files && <MessageAttachments items={m.files} />}
|
|
63
|
+
<MessageContent>{m.text}</MessageContent>
|
|
64
|
+
{m.from === 'assistant' ? <MessageActions onCopy onRegenerate={() => {}} onFeedback={() => {}} />
|
|
65
|
+
: <MessageActions onEdit={(t) => edit(m.id, t)} versions={m.versions && { index: m.v + 1, total: m.versions.length, onChange: (k) => showVersion(m.id, k - 1) }} />}
|
|
66
|
+
</Message>
|
|
67
|
+
))}
|
|
68
|
+
</ConversationContent>
|
|
69
|
+
<ConversationScrollButton />
|
|
70
|
+
</Conversation>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The full preview, as rendered in Storybook and checked in CI (`cv` is the preview page's own layout class, not Altitude's):
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import * as React from 'react';
|
|
77
|
+
import { Conversation, ConversationCheckpoint, ConversationContent, ConversationQueue, ConversationScrollButton, Message, MessageActions, MessageAttachments, MessageContent, PromptInput, PromptInputSubmit, PromptInputTextarea, PromptInputToolbar } from 'altitude-design';
|
|
78
|
+
|
|
79
|
+
const answer = 'Last week the team shipped 14 changes. Two risks are open: the billing migration is two days behind, and the EU data residency review needs a decision from legal by Friday.';
|
|
80
|
+
export default function App() {
|
|
81
|
+
const [msgs, setMsgs] = React.useState([
|
|
82
|
+
{ id: 1, from: 'user', text: 'What changed in the Atlas workspace this month?', files: [
|
|
83
|
+
{ name: 'atlas-usage.png', type: 'image/svg+xml', url: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='64'%3E%3Crect width='120' height='64' fill='%23e8edf5'/%3E%3Cpath d='M8 52 L32 36 L56 42 L80 20 L112 12' stroke='%233b6fd8' stroke-width='3' fill='none'/%3E%3C/svg%3E", onOpen: () => {} },
|
|
84
|
+
{ name: 'Atlas changelog.pdf', type: 'application/pdf', size: 482113, href: '#changelog' },
|
|
85
|
+
] },
|
|
86
|
+
{ id: 'cp1', kind: 'checkpoint', label: 'Before the Atlas summary', at: '2026-10-30T14:14:00' },
|
|
87
|
+
{ id: 2, from: 'assistant', text: 'Atlas moved to the new permission model on 3 October, and its storage limit was raised to 2 TB. Three people joined: Maya, Theo and Ada.', status: 'complete' },
|
|
88
|
+
]);
|
|
89
|
+
const [status, setStatus] = React.useState('ready');
|
|
90
|
+
const timer = React.useRef();
|
|
91
|
+
// Messages written while the assistant works wait in the queue; the first is sent when the turn ends.
|
|
92
|
+
const [queued, setQueued] = React.useState([]);
|
|
93
|
+
const queuedRef = React.useRef(queued); queuedRef.current = queued;
|
|
94
|
+
const sendRef = React.useRef();
|
|
95
|
+
const stream = (id) => {
|
|
96
|
+
let n = 0;
|
|
97
|
+
const tick = () => {
|
|
98
|
+
n += 6;
|
|
99
|
+
setMsgs((ms) => ms.map((m) => (m.id === id ? { ...m, text: answer.slice(0, n), status: n >= answer.length ? 'complete' : 'streaming' } : m)));
|
|
100
|
+
if (n < answer.length) timer.current = setTimeout(tick, 60);
|
|
101
|
+
else {
|
|
102
|
+
setStatus('ready');
|
|
103
|
+
const next = queuedRef.current[0];
|
|
104
|
+
if (next) { setQueued((q) => q.slice(1)); setTimeout(() => sendRef.current({ text: next.text }), 0); }
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
tick();
|
|
108
|
+
};
|
|
109
|
+
const enqueue = ({ text }) => setQueued((q) => [...q, { id: String(Date.now()), text }]);
|
|
110
|
+
const send = ({ text }) => {
|
|
111
|
+
const id = Date.now();
|
|
112
|
+
setMsgs((ms) => [...ms, { id: id - 1, from: 'user', text }, { id, from: 'assistant', text: '', status: 'streaming' }]);
|
|
113
|
+
setStatus('streaming');
|
|
114
|
+
stream(id);
|
|
115
|
+
};
|
|
116
|
+
// Editing a sent message starts a branch from it; the earlier exchange is kept as version 1.
|
|
117
|
+
const edit = (id, text) => {
|
|
118
|
+
const i = msgs.findIndex((m) => m.id === id), m = msgs[i];
|
|
119
|
+
const versions = (m.versions ?? [{ text: m.text, files: m.files }]).map((v, k) => (k === (m.v ?? 0) ? { ...v, rest: msgs.slice(i + 1) } : v));
|
|
120
|
+
const aid = Date.now();
|
|
121
|
+
setMsgs([...msgs.slice(0, i), { ...m, text, files: undefined, versions: [...versions, { text }], v: versions.length }, { id: aid, from: 'assistant', text: '', status: 'streaming' }]);
|
|
122
|
+
setStatus('streaming');
|
|
123
|
+
stream(aid);
|
|
124
|
+
};
|
|
125
|
+
const showVersion = (id, k) => {
|
|
126
|
+
const i = msgs.findIndex((m) => m.id === id), m = msgs[i];
|
|
127
|
+
const versions = m.versions.map((v, j) => (j === m.v ? { ...v, rest: msgs.slice(i + 1) } : v));
|
|
128
|
+
setMsgs([...msgs.slice(0, i), { ...m, text: versions[k].text, files: versions[k].files, versions, v: k }, ...(versions[k].rest ?? [])]);
|
|
129
|
+
};
|
|
130
|
+
const sendNow = (id) => {
|
|
131
|
+
const q = queued.find((x) => x.id === id);
|
|
132
|
+
setQueued((qs) => qs.filter((x) => x.id !== id));
|
|
133
|
+
stop();
|
|
134
|
+
setTimeout(() => sendRef.current({ text: q.text }), 0);
|
|
135
|
+
};
|
|
136
|
+
// Restoring a checkpoint sets the turns after it aside; returning brings them back.
|
|
137
|
+
const aside = React.useRef([]);
|
|
138
|
+
const restore = (id) => new Promise((done) => setTimeout(() => { setMsgs((ms) => { const i = ms.findIndex((m) => m.id === id); aside.current = ms.slice(i + 1); return ms.slice(0, i + 1); }); done(); }, 900));
|
|
139
|
+
const giveBack = () => { setMsgs((ms) => [...ms, ...aside.current]); aside.current = []; };
|
|
140
|
+
const stop = () => { clearTimeout(timer.current); setStatus('ready'); setMsgs((ms) => ms.map((m) => (m.status === 'streaming' ? { ...m, status: 'stopped' } : m))); };
|
|
141
|
+
sendRef.current = send;
|
|
142
|
+
return (
|
|
143
|
+
<div className="cv">
|
|
144
|
+
<Conversation aria-label="Assistant">
|
|
145
|
+
<ConversationContent>
|
|
146
|
+
{msgs.map((m) => m.kind === 'checkpoint' ? (
|
|
147
|
+
<ConversationCheckpoint key={m.id} label={m.label} at={m.at} onRestore={() => restore(m.id)} onReturn={giveBack}
|
|
148
|
+
consequence="The messages after this will be set aside. You can return to them." />
|
|
149
|
+
) : (
|
|
150
|
+
<Message key={m.id} from={m.from} status={m.status} edited={m.v > 0} onRetry={m.status === 'stopped' ? () => { setStatus('streaming'); stream(m.id); } : undefined}>
|
|
151
|
+
{m.files && <MessageAttachments items={m.files} />}
|
|
152
|
+
<MessageContent>{m.text}</MessageContent>
|
|
153
|
+
{m.from === 'assistant' ? <MessageActions onCopy onRegenerate={() => {}} onFeedback={() => {}} />
|
|
154
|
+
: <MessageActions onEdit={(t) => edit(m.id, t)} versions={m.versions && { index: m.v + 1, total: m.versions.length, onChange: (k) => showVersion(m.id, k - 1) }} />}
|
|
155
|
+
</Message>
|
|
156
|
+
))}
|
|
157
|
+
</ConversationContent>
|
|
158
|
+
<ConversationScrollButton />
|
|
159
|
+
</Conversation>
|
|
160
|
+
<ConversationQueue items={queued} onSendNow={sendNow} onEdit={(id, text) => setQueued((qs) => qs.map((q) => (q.id === id ? { ...q, text } : q)))} onRemove={(id) => setQueued((qs) => qs.filter((q) => q.id !== id))} />
|
|
161
|
+
<PromptInput onSubmit={send} status={status} onStop={stop} onQueue={enqueue}>
|
|
162
|
+
<PromptInputTextarea placeholder="Ask about this workspace" />
|
|
163
|
+
<PromptInputToolbar><PromptInputSubmit /></PromptInputToolbar>
|
|
164
|
+
</PromptInput>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
<!-- /example -->
|
|
@@ -21,3 +21,34 @@ Copies a value and says so: API keys, install commands, invite links, IDs, webho
|
|
|
21
21
|
- Do put it next to the value it copies, so people can see what they got.
|
|
22
22
|
- Don't confirm a copy with a toast; the button says it where you're looking.
|
|
23
23
|
- Don't make a primary CopyButton: copying is rarely the main thing on a page.
|
|
24
|
+
|
|
25
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
`CopyButton` as first used in its preview:
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
import { CopyButton } from 'altitude-design';
|
|
32
|
+
|
|
33
|
+
<CopyButton size="sm" value="sk_live_51Hc8x2Lw9Qe7TfR4mNpZa" label="Copy API key" />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The full preview, as rendered in Storybook and checked in CI (`cb`, `key`, `row`, `cap` are the preview page's own layout classes, not Altitude's):
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { CopyButton } from 'altitude-design';
|
|
40
|
+
|
|
41
|
+
export default function App() {
|
|
42
|
+
return (
|
|
43
|
+
<div className="cb">
|
|
44
|
+
<div className="key"><code>sk_live_51Hc8x2Lw9Qe7TfR4mNpZa</code><CopyButton size="sm" value="sk_live_51Hc8x2Lw9Qe7TfR4mNpZa" label="Copy API key" /></div>
|
|
45
|
+
<div className="row">
|
|
46
|
+
<CopyButton variant="secondary" value={() => 'https://app.example.com/invite/7Fq2'}>Copy invite link</CopyButton>
|
|
47
|
+
<CopyButton value={() => Promise.reject(new Error('denied'))}>Copy install command</CopyButton>
|
|
48
|
+
</div>
|
|
49
|
+
<p className="cap">Copied: pressed to the ground with a check and "Copied" for two seconds, announced politely; the button keeps its name and its width. The last one fails on purpose, to show what a failure says.</p>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
<!-- /example -->
|
|
@@ -80,3 +80,78 @@ The rows, the writes and their concurrency guards, and undo. Virtualisation, a f
|
|
|
80
80
|
The `DataGrid` section of `build/previews.txt` is an inventory grid: the fictional service locks the Desk lamp's price, prices must be 0 or more, and Status uses a Select. `node checks/data-grid.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers the grid keys, type-to-replace, Enter, Tab, F2, Escape, validation, IME Enter, click-to-commit, a refused save and its retry draft, read-only cells, ranges, copy, clearing a range, undo, a paste with one invalid cell, the Select column, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
|
|
81
81
|
**On a phone.** Under 480px of the frame's width, columns keep at least 6rem (or their `minWidth`) and the table scrolls inside its frame, fading the edge with more columns, instead of squeezing cells. `sticky: true` keeps a column in view over a flush 1px edge. Wide, nothing changes.
|
|
82
82
|
|
|
83
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
84
|
+
## Example
|
|
85
|
+
|
|
86
|
+
`DataGrid` as first used in its preview:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { Badge, DataGrid, Select } from 'altitude-design';
|
|
90
|
+
|
|
91
|
+
<DataGrid aria-label="Inventory" rows={rows} getRowId={r => r.id} getRowLabel={r => r.name}
|
|
92
|
+
columns={[
|
|
93
|
+
{ key: 'name', header: 'Name', editable: true, validate: v => String(v).trim() ? undefined : 'Enter a name.' },
|
|
94
|
+
{ key: 'sku', header: 'SKU' },
|
|
95
|
+
{ key: 'price', header: 'Price', align: 'end', editable: true, parse: t => t.trim() === '' ? '' : Number(t), format: dgMoney,
|
|
96
|
+
validate: v => v === '' || !Number.isFinite(v) || v < 0 ? 'Use a price of 0 or more.' : undefined, cell: r => `$${r.price}` },
|
|
97
|
+
{ key: 'stock', header: 'Stock', align: 'end', editable: true, parse: t => t.trim() === '' ? 0 : Number(t), format: dgMoney,
|
|
98
|
+
validate: v => Number.isInteger(v) && v >= 0 ? undefined : 'Use a whole number, 0 or more.' },
|
|
99
|
+
{ key: 'status', header: 'Status', editable: true, format: v => DG_STATUS.find(s => s.value === v)?.label ?? '',
|
|
100
|
+
cell: r => <Badge tone={r.status === 'active' ? 'positive' : r.status === 'paused' ? 'caution' : 'neutral'}>{DG_STATUS.find(s => s.value === r.status).label}</Badge>,
|
|
101
|
+
editor: control => <Select {...control} items={DG_STATUS} size="sm" defaultOpen /> },
|
|
102
|
+
]}
|
|
103
|
+
onCellSave={async change => {
|
|
104
|
+
await dgWait(250);
|
|
105
|
+
// The demo service locks the lamp's price during a sale.
|
|
106
|
+
if (change.rowId === 'p1' && change.key === 'price') return { ok: false, error: 'The Desk lamp price is locked during the sale.' };
|
|
107
|
+
apply([change]); return { ok: true };
|
|
108
|
+
}}
|
|
109
|
+
onCellsSave={async changes => { await dgWait(300); apply(changes); return { ok: true }; }}
|
|
110
|
+
onUndo={last => apply(last.map(c => ({ ...c, draft: c.previousValue })))} />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The full preview, as rendered in Storybook and checked in CI (`dg-demo` is the preview page's own layout class, not Altitude's):
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import * as React from 'react';
|
|
117
|
+
import { Badge, DataGrid, Select } from 'altitude-design';
|
|
118
|
+
|
|
119
|
+
const DG_STATUS = [{ value: 'active', label: 'Active' }, { value: 'paused', label: 'Paused' }, { value: 'retired', label: 'Retired' }];
|
|
120
|
+
const DG_SEED = [
|
|
121
|
+
{ id: 'p1', name: 'Desk lamp', sku: 'LMP-204', price: 49, stock: 120, status: 'active' },
|
|
122
|
+
{ id: 'p2', name: 'Monitor arm', sku: 'ARM-310', price: 89, stock: 34, status: 'active' },
|
|
123
|
+
{ id: 'p3', name: 'Cable tray', sku: 'TRY-118', price: 24, stock: 0, status: 'paused' },
|
|
124
|
+
{ id: 'p4', name: 'Footrest', sku: 'FTR-052', price: 39, stock: 58, status: 'active' },
|
|
125
|
+
{ id: 'p5', name: 'Laptop stand', sku: 'STD-077', price: 59, stock: 12, status: 'retired' },
|
|
126
|
+
];
|
|
127
|
+
const dgWait = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
128
|
+
const dgMoney = v => v === '' || v === null || v === undefined ? '' : String(v);
|
|
129
|
+
export default function App() {
|
|
130
|
+
const [rows, setRows] = React.useState(DG_SEED);
|
|
131
|
+
const apply = changes => setRows(list => list.map(row => changes.reduce((r, c) => c.rowId === r.id ? { ...r, [c.key]: c.draft } : r, row)));
|
|
132
|
+
return <div className="dg-demo">
|
|
133
|
+
<DataGrid aria-label="Inventory" rows={rows} getRowId={r => r.id} getRowLabel={r => r.name}
|
|
134
|
+
columns={[
|
|
135
|
+
{ key: 'name', header: 'Name', editable: true, validate: v => String(v).trim() ? undefined : 'Enter a name.' },
|
|
136
|
+
{ key: 'sku', header: 'SKU' },
|
|
137
|
+
{ key: 'price', header: 'Price', align: 'end', editable: true, parse: t => t.trim() === '' ? '' : Number(t), format: dgMoney,
|
|
138
|
+
validate: v => v === '' || !Number.isFinite(v) || v < 0 ? 'Use a price of 0 or more.' : undefined, cell: r => `$${r.price}` },
|
|
139
|
+
{ key: 'stock', header: 'Stock', align: 'end', editable: true, parse: t => t.trim() === '' ? 0 : Number(t), format: dgMoney,
|
|
140
|
+
validate: v => Number.isInteger(v) && v >= 0 ? undefined : 'Use a whole number, 0 or more.' },
|
|
141
|
+
{ key: 'status', header: 'Status', editable: true, format: v => DG_STATUS.find(s => s.value === v)?.label ?? '',
|
|
142
|
+
cell: r => <Badge tone={r.status === 'active' ? 'positive' : r.status === 'paused' ? 'caution' : 'neutral'}>{DG_STATUS.find(s => s.value === r.status).label}</Badge>,
|
|
143
|
+
editor: control => <Select {...control} items={DG_STATUS} size="sm" defaultOpen /> },
|
|
144
|
+
]}
|
|
145
|
+
onCellSave={async change => {
|
|
146
|
+
await dgWait(250);
|
|
147
|
+
// The demo service locks the lamp's price during a sale.
|
|
148
|
+
if (change.rowId === 'p1' && change.key === 'price') return { ok: false, error: 'The Desk lamp price is locked during the sale.' };
|
|
149
|
+
apply([change]); return { ok: true };
|
|
150
|
+
}}
|
|
151
|
+
onCellsSave={async changes => { await dgWait(300); apply(changes); return { ok: true }; }}
|
|
152
|
+
onUndo={last => apply(last.map(c => ({ ...c, draft: c.previousValue })))} />
|
|
153
|
+
<p>Arrows move, type to replace, Enter or F2 to edit, Tab and Enter save and move. Shift+arrows select; copy and paste a block from a spreadsheet. Ctrl+Z undoes.</p>
|
|
154
|
+
</div>;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
<!-- /example -->
|
|
@@ -65,3 +65,52 @@ The value and the rules (limits, unavailable days). Time of day isn't covered (t
|
|
|
65
65
|
## Demo and verification
|
|
66
66
|
|
|
67
67
|
The `DatePicker` section of `build/previews.txt` shows a due date (from today, weekends unavailable, en-GB, open), a trip range and an en-US date. `node checks/date-picker.mjs` (from `build/`; `ALT_FIXTURE_ROOT` selects React 19 fixtures) covers the APG grid keys, RTL arrows, unavailable days, Escape, focus return, segment typing and stepping, impossible dates, Alt+↓, the range flow with its preview, and 320/960px layouts in both schemes under reduced motion, forced colours and `contrast="more"`.
|
|
68
|
+
|
|
69
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
70
|
+
## Example
|
|
71
|
+
|
|
72
|
+
`DatePicker` as first used in its preview:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { DatePicker } from 'altitude-design';
|
|
76
|
+
|
|
77
|
+
<DatePicker value={due} onValueChange={setDue} min={DP_TODAY} isUnavailable={weekend} locale="en-GB" defaultOpen />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The full preview, as rendered in Storybook and checked in CI (`dp-demo`, `dp-wide` are the preview page's own layout classes, not Altitude's):
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import * as React from 'react';
|
|
84
|
+
import { Button, DatePicker, DateRangePicker, Field, datePresets } from 'altitude-design';
|
|
85
|
+
|
|
86
|
+
const DP_TODAY = (() => { const t = new Date(); return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`; })();
|
|
87
|
+
const dpShift = (iso, days) => { const [y, m, d] = iso.split('-').map(Number); const t = new Date(Date.UTC(y, m - 1, d + days)); return t.toISOString().slice(0, 10); };
|
|
88
|
+
export default function App() {
|
|
89
|
+
const [due, setDue] = React.useState(dpShift(DP_TODAY, 3));
|
|
90
|
+
const [trip, setTrip] = React.useState({ start: dpShift(DP_TODAY, 9), end: dpShift(DP_TODAY, 13) });
|
|
91
|
+
const [start, setStart] = React.useState(null);
|
|
92
|
+
const [dpToday, setDpToday] = React.useState('2026-10-27');
|
|
93
|
+
const [period, setPeriod] = React.useState({ start: '2026-09-28', end: '2026-10-27' });
|
|
94
|
+
const [preset, setPreset] = React.useState('last-30-days');
|
|
95
|
+
const weekend = iso => { const [y, m, d] = iso.split('-').map(Number); const w = new Date(Date.UTC(y, m - 1, d)).getUTCDay(); return w === 0 || w === 6; };
|
|
96
|
+
return <div className="dp-demo">
|
|
97
|
+
<Field label="Due date" hint="Weekends aren’t available.">
|
|
98
|
+
<DatePicker value={due} onValueChange={setDue} min={DP_TODAY} isUnavailable={weekend} locale="en-GB" defaultOpen />
|
|
99
|
+
</Field>
|
|
100
|
+
<div>
|
|
101
|
+
<Field label="Trip"><DateRangePicker value={trip} onValueChange={setTrip} min={DP_TODAY} locale="en-GB" /></Field>
|
|
102
|
+
<div style={{ height: 24 }} />
|
|
103
|
+
<Field label="Start date (US format)"><DatePicker value={start} onValueChange={setStart} locale="en-US" /></Field>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="dp-wide">
|
|
106
|
+
<Field label="Reporting period" hint={`Presets resolve against today (${dpToday}); they stay relative when it changes.`}>
|
|
107
|
+
<DateRangePicker value={period} onValueChange={setPeriod} preset={preset} onPresetChange={setPreset} today={dpToday} locale="en-GB"
|
|
108
|
+
presets={[datePresets.today(), datePresets.lastDays(7), datePresets.lastDays(30), datePresets.thisMonth(), datePresets.thisQuarter(), datePresets.lastQuarter(),
|
|
109
|
+
{ key: 'launch', label: 'Since launch', range: { start: '2026-03-04', end: dpToday } }]} />
|
|
110
|
+
</Field>
|
|
111
|
+
<div style={{ marginTop: 8 }}><Button size="sm" variant="ghost" onClick={() => setDpToday((t) => dpShift(t, 1))}>Next day</Button></div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
<!-- /example -->
|
|
@@ -27,3 +27,178 @@ This fragment shows presentation only. Use the executable [confirmation recipe](
|
|
|
27
27
|
- Name the action in the confirm button, such as “Delete permanently”. For irreversible actions, initially focus Cancel.
|
|
28
28
|
- Keep essential errors and progress inside the dialog. Background toast actions are temporarily inert; notification text is routed into its live region.
|
|
29
29
|
- Keep one summit. A Dialog may temporarily suspend a modal Sheet. Competing Dialog/CommandPalette requests are rejected, warn in development, and require `open=false` before a fresh attempt. They never queue. A palette command may close the palette and open a Dialog in the same callback.
|
|
30
|
+
|
|
31
|
+
<!-- example: generated from build/previews.txt by build/previews.mjs. Edit the preview, not this section. -->
|
|
32
|
+
## Example
|
|
33
|
+
|
|
34
|
+
`Dialog` as first used in its preview:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { Alert, Button, Dialog } from 'altitude-design';
|
|
38
|
+
|
|
39
|
+
<Dialog open={open} onOpenChange={setOpen} background={background} size="sm" title="Delete this file permanently?"
|
|
40
|
+
description={`“${target?.name || ''}” will be removed. You won’t be able to restore it.`} focusSelector="[data-cancel]" returnFocus={() => heading.current}
|
|
41
|
+
footer={<><Button data-cancel variant="ghost" onClick={() => setOpen(false)}>{busy ? 'Close' : 'Cancel'}</Button>
|
|
42
|
+
<Button data-confirm variant="danger" disabled={busy || operation?.state === 'conflict'} onClick={confirm}>{busy ? operation?.state === 'checking' ? 'Checking deletion…' : 'Deleting…' : failed ? 'Try deleting again' : 'Delete permanently'}</Button></>}>
|
|
43
|
+
<p className="body dialog-target">{target?.name}<br /><span className="cap">{target?.folder}</span></p>
|
|
44
|
+
{busy && <p role="status">Deletion is in progress. Closing this dialog won’t cancel it.</p>}
|
|
45
|
+
{failed && <Alert tone="critical" title="File not deleted">{operation.message}{operation.state === 'conflict' && <p>Latest file: {operation.current?.name || 'No longer available'}</p>}</Alert>}
|
|
46
|
+
</Dialog>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The full preview, as rendered in Storybook and checked in CI (`dialog-demo`, `cap`, `body`, `dialog-file`, `dialog-target`, `row` are the preview page's own layout classes, not Altitude's):
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import * as React from 'react';
|
|
53
|
+
import { Alert, AltitudeProvider, Button, CommandPalette, Dialog, Input, Sheet, SheetHeader, SheetTitle, Toaster, toast, useAltitude } from 'altitude-design';
|
|
54
|
+
|
|
55
|
+
const dialogFiles = [
|
|
56
|
+
{ id: 'file-brand', name: 'Brand guidelines.pdf', folder: 'Brand', revision: 1 },
|
|
57
|
+
{ id: 'file-brand-draft', name: 'Brand guidelines draft.pdf', folder: 'Brand', revision: 1 },
|
|
58
|
+
{ id: 'file-launch', name: 'Launch checklist.md', folder: 'Launch', revision: 1 },
|
|
59
|
+
];
|
|
60
|
+
function ModalFrameFixture() {
|
|
61
|
+
const [open, setOpen] = React.useState(false), [sheet, setSheet] = React.useState(false);
|
|
62
|
+
React.useEffect(() => { window.altitudeModalFrameFixture = { setOpen, setSheet }; return () => { delete window.altitudeModalFrameFixture; }; }, []);
|
|
63
|
+
return <AltitudeProvider><p>Independent document fixture</p><Button data-frame-trigger onClick={() => setOpen(true)}>Open frame palette</Button>
|
|
64
|
+
<CommandPalette open={open} onOpenChange={setOpen} items={[{ items: [{ label: 'Frame command' }] }]} />
|
|
65
|
+
<Sheet open={sheet} onOpenChange={setSheet}><SheetHeader><SheetTitle>Frame sheet</SheetTitle></SheetHeader><Input aria-label="Frame input" /></Sheet>
|
|
66
|
+
</AltitudeProvider>;
|
|
67
|
+
}
|
|
68
|
+
export default function App() {
|
|
69
|
+
const altitude = useAltitude();
|
|
70
|
+
const releaseLegacy = React.useRef(null);
|
|
71
|
+
const [rows, setRows] = React.useState(dialogFiles);
|
|
72
|
+
const server = React.useRef(dialogFiles.map(file => ({ ...file })));
|
|
73
|
+
const heading = React.useRef(null);
|
|
74
|
+
const [target, setTarget] = React.useState(null);
|
|
75
|
+
const [open, setOpen] = React.useState(false);
|
|
76
|
+
const opened = React.useRef(false); opened.current = open;
|
|
77
|
+
const [background, setBackground] = React.useState('stationary');
|
|
78
|
+
const [fixture, setFixture] = React.useState(new URLSearchParams(window.location.search).get('fixture') || 'success-100');
|
|
79
|
+
const [operation, setOperation] = React.useState(null);
|
|
80
|
+
const [outcome, setOutcome] = React.useState('');
|
|
81
|
+
const [present, setPresent] = React.useState(true);
|
|
82
|
+
const session = React.useRef(0), sequence = React.useRef(0), pending = React.useRef(new Set());
|
|
83
|
+
const failUsed = React.useRef(false), accepted = React.useRef([]);
|
|
84
|
+
const invocation = React.useRef(null), mounted = React.useRef(false), epoch = React.useRef(0);
|
|
85
|
+
const [frame, setFrame] = React.useState(false);
|
|
86
|
+
const frameRoot = React.useRef(null);
|
|
87
|
+
const [mode, setMode] = React.useState('');
|
|
88
|
+
const [sheet, setSheet] = React.useState(false), [nonmodal, setNonmodal] = React.useState(false);
|
|
89
|
+
const [palette, setPalette] = React.useState(false), [competing, setCompeting] = React.useState(false);
|
|
90
|
+
const [controls, setControls] = React.useState(true), [inserted, setInserted] = React.useState(false);
|
|
91
|
+
const [nestedPresent, setNestedPresent] = React.useState(true);
|
|
92
|
+
const [nested, setNested] = React.useState(false), [toastActions, setToastActions] = React.useState(0);
|
|
93
|
+
const latest = React.useRef({});
|
|
94
|
+
React.useEffect(() => { mounted.current = true; return () => { mounted.current = false; epoch.current++; }; }, []);
|
|
95
|
+
const begin = (file, invoker) => {
|
|
96
|
+
if (pending.current.has(file.id)) { setOutcome(`Deletion is already in progress for “${file.name}”.`); return; }
|
|
97
|
+
session.current++; invocation.current = invoker || document.activeElement;
|
|
98
|
+
setTarget({ ...file, session: session.current }); setOperation(null); setOpen(true); setPresent(true);
|
|
99
|
+
};
|
|
100
|
+
const finish = (captured, id, result, ownEpoch) => {
|
|
101
|
+
pending.current.delete(captured.id);
|
|
102
|
+
if (!mounted.current || ownEpoch !== epoch.current) return;
|
|
103
|
+
const matching = session.current === captured.session;
|
|
104
|
+
if (result === 'success') {
|
|
105
|
+
const restoreRemoved = !opened.current && document.activeElement === invocation.current && matching;
|
|
106
|
+
setRows(server.current.map(file => ({ ...file })));
|
|
107
|
+
setOutcome(`Deleted “${captured.name}”.`);
|
|
108
|
+
if (matching) { setOperation({ id, session: captured.session, state: 'done' }); setOpen(false); }
|
|
109
|
+
if (restoreRemoved) requestAnimationFrame(() => heading.current?.focus({ preventScroll: true }));
|
|
110
|
+
} else {
|
|
111
|
+
const message = result === 'conflict' ? 'This file changed. Close this dialog and review the latest file before deleting.' : 'The file was not deleted. Try again.';
|
|
112
|
+
if (matching && opened.current) setOperation({ id, session: captured.session, state: result, message, current: server.current.find(file => file.id === captured.id) });
|
|
113
|
+
else setOutcome(`“${captured.name}”: ${message}`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const confirm = async () => {
|
|
117
|
+
if (!target || pending.current.has(target.id)) return;
|
|
118
|
+
const captured = { ...target }, id = `delete-${++sequence.current}`, ownEpoch = epoch.current, behavior = fixture;
|
|
119
|
+
pending.current.add(captured.id); setOperation({ id, session: captured.session, state: 'pending' });
|
|
120
|
+
const request = { id: captured.id, expectedRevision: captured.revision, operationId: id, outcome: 'pending' };
|
|
121
|
+
accepted.current.push(request);
|
|
122
|
+
const reject = behavior === 'fail-once' && !failUsed.current;
|
|
123
|
+
if (reject) failUsed.current = true;
|
|
124
|
+
if (behavior === 'unmount') setTimeout(() => { if (epoch.current === ownEpoch && mounted.current) { setPresent(false); setOpen(false); } }, 50);
|
|
125
|
+
if (behavior === 'interrupted-after-save') {
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
127
|
+
if (ownEpoch !== epoch.current) return;
|
|
128
|
+
const record = server.current.find(file => file.id === captured.id);
|
|
129
|
+
if (record?.revision === captured.revision) server.current = server.current.filter(file => file.id !== captured.id);
|
|
130
|
+
await new Promise(resolve => setTimeout(resolve, 1100));
|
|
131
|
+
} else await new Promise(resolve => setTimeout(resolve, behavior === 'success-1200' || reject || behavior === 'conflict' ? 1200 : 100));
|
|
132
|
+
if (ownEpoch !== epoch.current) return;
|
|
133
|
+
if (behavior === 'conflict') { server.current = server.current.map(file => file.id === captured.id ? { ...file, name: 'Brand guidelines revised.pdf', revision: file.revision + 1 } : file); setRows(server.current); }
|
|
134
|
+
const current = server.current.find(file => file.id === captured.id);
|
|
135
|
+
let result = !current || current.revision !== captured.revision ? 'conflict' : reject || behavior === 'interrupted-before-save' ? 'failure' : 'success';
|
|
136
|
+
if (result === 'success') server.current = server.current.filter(file => file.id !== captured.id);
|
|
137
|
+
if (behavior.startsWith('interrupted')) {
|
|
138
|
+
if (mounted.current && session.current === captured.session && opened.current) setOperation({ id, session: captured.session, state: 'checking' });
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
140
|
+
result = server.current.some(file => file.id === captured.id) ? 'failure' : 'success';
|
|
141
|
+
}
|
|
142
|
+
request.outcome = result;
|
|
143
|
+
finish(captured, id, result, ownEpoch);
|
|
144
|
+
};
|
|
145
|
+
const reset = () => {
|
|
146
|
+
epoch.current++; sequence.current = 0; failUsed.current = false; accepted.current = []; pending.current.clear(); server.current = dialogFiles.map(file => ({ ...file }));
|
|
147
|
+
setRows(server.current); setOpen(false); setOutcome(''); setOperation(null); setPresent(true); setMode(''); setSheet(false); setNonmodal(false); setPalette(false); setCompeting(false); setNested(false);
|
|
148
|
+
};
|
|
149
|
+
latest.current = { legacyVote: () => { releaseLegacy.current = altitude.pushLayer(); }, legacyRelease: () => releaseLegacy.current?.(), begin, reset, setFrame, setMode, setSheet, setNonmodal, setPalette, setCompeting, setNested, setNestedPresent, setControls, setInserted, setPresent, setBackground, setFixture,
|
|
150
|
+
close: () => setOpen(false), confirm, openFirst: () => begin(server.current[0]),
|
|
151
|
+
changeTarget: () => { server.current = server.current.map(file => file.id === target?.id ? { ...file, name: file.name.replace('.pdf', ' revised.pdf'), revision: file.revision + 1 } : file); setRows(server.current); },
|
|
152
|
+
notify: () => toast({ title: 'Background update', duration: 1200, action: { label: 'Undo background change', onClick: () => setToastActions(n => n + 1) } }),
|
|
153
|
+
snapshot: () => ({ rows, operation, requests: sequence.current, accepted: accepted.current, toastActions, session: session.current, open }) };
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
window.altitudeModalFixture = new Proxy({}, { get: (_, key) => latest.current[key] });
|
|
156
|
+
return () => { delete window.altitudeModalFixture; };
|
|
157
|
+
}, []);
|
|
158
|
+
const busy = operation?.session === target?.session && ['pending', 'checking'].includes(operation?.state);
|
|
159
|
+
const failed = operation?.session === target?.session && ['failure', 'conflict'].includes(operation?.state);
|
|
160
|
+
return <div className="dialog-demo">
|
|
161
|
+
<header><p className="cap">Files / Archived</p><h1 ref={heading} tabIndex={-1}>Archived files</h1></header>
|
|
162
|
+
<p className="body">Archived files can be restored. Permanently deleted files cannot.</p>
|
|
163
|
+
<section aria-label="Archived files">{rows.map(file => <div className="dialog-file" key={file.id}>
|
|
164
|
+
<div><strong>{file.name}</strong><p className="cap">{file.folder}</p></div>
|
|
165
|
+
<Button data-file={file.id} onClick={event => begin(file, event.currentTarget)}>Delete permanently</Button>
|
|
166
|
+
</div>)}</section>
|
|
167
|
+
<p role="status" data-outcome>{outcome}</p>
|
|
168
|
+
{present && <Dialog open={open} onOpenChange={setOpen} background={background} size="sm" title="Delete this file permanently?"
|
|
169
|
+
description={`“${target?.name || ''}” will be removed. You won’t be able to restore it.`} focusSelector="[data-cancel]" returnFocus={() => heading.current}
|
|
170
|
+
footer={<><Button data-cancel variant="ghost" onClick={() => setOpen(false)}>{busy ? 'Close' : 'Cancel'}</Button>
|
|
171
|
+
<Button data-confirm variant="danger" disabled={busy || operation?.state === 'conflict'} onClick={confirm}>{busy ? operation?.state === 'checking' ? 'Checking deletion…' : 'Deleting…' : failed ? 'Try deleting again' : 'Delete permanently'}</Button></>}>
|
|
172
|
+
<p className="body dialog-target">{target?.name}<br /><span className="cap">{target?.folder}</span></p>
|
|
173
|
+
{busy && <p role="status">Deletion is in progress. Closing this dialog won’t cancel it.</p>}
|
|
174
|
+
{failed && <Alert tone="critical" title="File not deleted">{operation.message}{operation.state === 'conflict' && <p>Latest file: {operation.current?.name || 'No longer available'}</p>}</Alert>}
|
|
175
|
+
</Dialog>}
|
|
176
|
+
<details><summary>Demo controls</summary>
|
|
177
|
+
<div className="row"><label>Background <select aria-label="Dialog background" value={background} onChange={event => setBackground(event.target.value)}><option value="stationary">Stationary</option><option value="recede">Default recession</option></select></label>
|
|
178
|
+
<label>Service fixture <select aria-label="Service fixture" value={fixture} onChange={event => setFixture(event.target.value)}><option value="success-100">Success · 100 ms</option><option value="success-1200">Slow success · 1,200 ms</option><option value="fail-once">Fail once, then retry</option><option value="conflict">Record changes before save</option><option value="interrupted-after-save">Connection interrupted after save</option><option value="interrupted-before-save">Connection interrupted before save</option><option value="unmount">Unmount presentation during write</option></select></label>
|
|
179
|
+
<Button onClick={reset}>Reset fixtures</Button></div>
|
|
180
|
+
<div className="row"><Button onClick={() => { setControls(true); setMode('title'); }}>Read a structured dialog</Button><Button onClick={() => setMode('field')}>Edit in a dialog</Button><Button onClick={() => setMode('panel')}>Panel-only dialog</Button><Button onClick={() => setSheet(true)}>Open modal sheet</Button><Button onClick={() => setNonmodal(true)}>Open nonmodal sheet</Button><Button onClick={() => setPalette(true)}>Open command palette</Button><Button onClick={() => setNested(true)}>Open nested-provider dialog</Button></div>
|
|
181
|
+
<p className="cap">Requests: {sequence.current}. Accepted operation log: <output data-operation-log>{JSON.stringify(accepted.current)}</output></p>
|
|
182
|
+
<p className="cap">Deterministic fictional data. Pending close, failure, conflict, unmount and ownership cases are exercised by the canonical regression harness. <a href="https://github.com/richlaconte/altitude/blob/main/build/previews.txt">Example source</a> · <a href="../../ActionsInPlace.md">Actions in place recipe</a></p>
|
|
183
|
+
<button data-background-control onClick={() => setOutcome('Background action')}>Background control</button>
|
|
184
|
+
{inserted && <button data-late-background>New background control</button>}
|
|
185
|
+
{frame && <iframe srcDoc="<!doctype html><html><head></head><body></body></html>" title="Independent document" style={{ width: '100%', height: 360 }} onLoad={event => {
|
|
186
|
+
const doc = event.currentTarget.contentDocument;
|
|
187
|
+
for (const style of document.querySelectorAll('style')) doc.head.append(style.cloneNode(true));
|
|
188
|
+
frameRoot.current = ReactDOM.createRoot(doc.body); frameRoot.current.render(<ModalFrameFixture />);
|
|
189
|
+
}} />}
|
|
190
|
+
</details>
|
|
191
|
+
<Dialog open={!!mode} onOpenChange={value => { if (!value) setMode(''); }} title="Modal focus fixture" initialFocus={mode === 'title' ? 'title' : mode === 'panel' ? false : 'first-field'} dismissible={mode !== 'panel'} background={background}
|
|
192
|
+
footer={mode !== 'panel' && controls ? <><button data-first>First control</button><button data-last>Last control</button></> : null}>
|
|
193
|
+
{mode === 'field' && controls && <Input aria-label="Fixture field" />}<button hidden>Hidden control</button><button disabled>Disabled control</button>
|
|
194
|
+
</Dialog>
|
|
195
|
+
<Dialog open={competing} onOpenChange={setCompeting} title="Competing summit" />
|
|
196
|
+
<Sheet open={sheet} onOpenChange={setSheet}><SheetHeader><SheetTitle>File details</SheetTitle></SheetHeader><Button data-sheet-dialog onClick={() => setMode('title')}>Read details in dialog</Button></Sheet>
|
|
197
|
+
<Sheet open={nonmodal} onOpenChange={setNonmodal} modal={false}><SheetHeader><SheetTitle>File filters</SheetTitle></SheetHeader><Button data-nonmodal-close onClick={() => setNonmodal(false)}>Close filters</Button></Sheet>
|
|
198
|
+
<CommandPalette open={palette} onOpenChange={setPalette} items={[{ items: [{ label: 'Read file details', onSelect: () => setMode('title') }] }]} />
|
|
199
|
+
{nestedPresent && <AltitudeProvider scheme="nightfall" recede={false}><div data-nested-background hidden>Nested workspace</div><Dialog open={nested} onOpenChange={setNested} title="Nested dialog" initialFocus="title" /></AltitudeProvider>}
|
|
200
|
+
<Toaster />
|
|
201
|
+
</div>;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
<!-- /example -->
|