division-ds 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -2
- package/README.md +11 -0
- package/division-ds-0.2.0.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +205 -0
- package/public/docs/components/ai-label.md +88 -0
- package/public/docs/components/alert-dialog.md +290 -0
- package/public/docs/components/alert.md +173 -0
- package/public/docs/components/approval-request.md +146 -0
- package/public/docs/components/auth-form.md +130 -0
- package/public/docs/components/avatar-group.md +150 -0
- package/public/docs/components/avatar.md +177 -0
- package/public/docs/components/badge.md +176 -0
- package/public/docs/components/breadcrumbs.md +110 -0
- package/public/docs/components/button.md +202 -0
- package/public/docs/components/card.md +255 -0
- package/public/docs/components/carousel.md +128 -0
- package/public/docs/components/cart.md +170 -0
- package/public/docs/components/chart-panel.md +356 -0
- package/public/docs/components/chat-message.md +154 -0
- package/public/docs/components/chat-thread.md +242 -0
- package/public/docs/components/checkbox.md +147 -0
- package/public/docs/components/checkout-form.md +126 -0
- package/public/docs/components/command-palette.md +145 -0
- package/public/docs/components/data-table.md +399 -0
- package/public/docs/components/description-list.md +101 -0
- package/public/docs/components/dialog.md +195 -0
- package/public/docs/components/divider.md +95 -0
- package/public/docs/components/dropdown-menu.md +220 -0
- package/public/docs/components/empty-state.md +143 -0
- package/public/docs/components/footer.md +395 -0
- package/public/docs/components/form-field.md +213 -0
- package/public/docs/components/icon-button.md +125 -0
- package/public/docs/components/input.md +198 -0
- package/public/docs/components/kanban-board.md +298 -0
- package/public/docs/components/kbd.md +70 -0
- package/public/docs/components/link.md +96 -0
- package/public/docs/components/list-item.md +238 -0
- package/public/docs/components/loading-animation.md +70 -0
- package/public/docs/components/multi-select.md +200 -0
- package/public/docs/components/navbar.md +142 -0
- package/public/docs/components/newsletter-form.md +110 -0
- package/public/docs/components/page-header.md +149 -0
- package/public/docs/components/pagination.md +210 -0
- package/public/docs/components/product-card.md +169 -0
- package/public/docs/components/progress.md +99 -0
- package/public/docs/components/prompt-input.md +149 -0
- package/public/docs/components/quantity-stepper.md +138 -0
- package/public/docs/components/radio.md +140 -0
- package/public/docs/components/rating.md +104 -0
- package/public/docs/components/score-ring.md +93 -0
- package/public/docs/components/search.md +114 -0
- package/public/docs/components/section-heading.md +111 -0
- package/public/docs/components/segmented-control.md +124 -0
- package/public/docs/components/select.md +222 -0
- package/public/docs/components/sheet.md +143 -0
- package/public/docs/components/sidebar.md +191 -0
- package/public/docs/components/skeleton.md +133 -0
- package/public/docs/components/slider.md +112 -0
- package/public/docs/components/sparkline.md +126 -0
- package/public/docs/components/spinner.md +96 -0
- package/public/docs/components/stat.md +104 -0
- package/public/docs/components/status-tracker.md +143 -0
- package/public/docs/components/stepper.md +145 -0
- package/public/docs/components/switch.md +151 -0
- package/public/docs/components/tabs.md +178 -0
- package/public/docs/components/tag.md +103 -0
- package/public/docs/components/textarea.md +146 -0
- package/public/docs/components/timeline.md +150 -0
- package/public/docs/components/toast.md +192 -0
- package/public/docs/components/tool-call.md +105 -0
- package/public/docs/components/tooltip.md +106 -0
- package/public/manifest/components.json +182 -6
- package/public/manifest/tokens.json +39 -0
- package/scripts/division-ds.mjs +74 -38
- package/scripts/lib/agent-docs.mjs +53 -1
- package/scripts/lib/ai-copy.mjs +106 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +44 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +115 -0
- package/scripts/lib/mcp-tools.mjs +241 -0
- package/scripts/lib/props-validator.mjs +1 -1
- package/scripts/lib/rules-installer.mjs +42 -4
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +7 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +19 -0
- package/skills/division-ds/topics/layout.md +34 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# ChartPanel
|
|
2
|
+
|
|
3
|
+
> One chart in a card. A sentence says what it shows, and the same numbers sit in a table. The data below is sample data.
|
|
4
|
+
|
|
5
|
+
- **Best for:** One chart (a line over time, bars by category, a donut of one whole, or a radar of several measures) with a summary sentence and the numbers as a table.
|
|
6
|
+
- **Not for:** One number and its change: use Stat. One score from 0 to 100: use ScoreRing. Two measures on two scales: use two panels.
|
|
7
|
+
- Organism. Source: `src/components/organisms/chart-panel/ChartPanel.tsx`
|
|
8
|
+
- Rendered docs page: `/components/chart-panel`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { ChartPanel } from 'src/components/organisms/chart-panel/ChartPanel'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Props
|
|
17
|
+
|
|
18
|
+
| Prop | Type | Default | Description |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `title` | `string` | | Required. The panel's heading and its name. |
|
|
21
|
+
| `summary` | `string` | | Required. One sentence, the chart's caption. |
|
|
22
|
+
| `kind` | `'line' \| 'bar' \| 'donut' \| 'radar'` | | Required. Line for change over time. Bar to compare categories. Donut for parts of one whole (one series, a slice per row). Radar for several measures of one or two things (an axis per row). |
|
|
23
|
+
| `data` | `Row[]` | | Required. One object per category. |
|
|
24
|
+
| `xKey` | `keyof Row` | | Required. The category. Its values must be unique. |
|
|
25
|
+
| `xLabel` | `string` | | Required. The header of the table's first column. |
|
|
26
|
+
| `series` | `[ChartSeries] \| [ChartSeries, ChartSeries]` | | Required. One or two series, coloured chart.series-1 then chart.series-2. Passing a third is a type error, and so is a ChartSeries.key that isn't one of Row's own numeric columns. |
|
|
27
|
+
| `area` | `boolean` | `false` | Line only. Fills under each line with a gradient of its colour that fades to nothing. |
|
|
28
|
+
| `curve` | `'linear' \| 'smooth'` | `'linear'` | Line only. smooth draws a monotone curve, so it never overshoots a data point. |
|
|
29
|
+
| `grid` | `boolean` | `true` | The dashed horizontal grid (on a radar, its web). false also drops the axis lines but keeps the tick labels. |
|
|
30
|
+
| `size` | `'md' \| 'sm'` | `'md'` | sm is the stat-card size: a short sparkline with no axes or grid, and the caption under it. Pair it with headline. |
|
|
31
|
+
| `headline` | `string` | | A formatted figure printed large under the title. |
|
|
32
|
+
| `centerLabel` | `string` | | Donut only. The caption under the total in the centre of the ring. |
|
|
33
|
+
| `formatValue` | `(value: number) => string` | `Intl.NumberFormat('en-IE')` | Formats numbers in the tooltip and the table. It formats the y-axis too, unless you give formatAxisValue. |
|
|
34
|
+
| `formatAxisValue` | `(value: number) => string` | `formatValue` | Formats only the y-axis tick labels. Use a shorter, compact formatter when formatValue's output is too wide for the axis. SpendChart's compact euro formatter is one example. |
|
|
35
|
+
| `axisWidth` | `number` | `48` | The width the y-axis reserves, in px (pixels). 48 is Recharts' own YAxis default. Make it bigger if formatAxisValue's labels still get cut off. |
|
|
36
|
+
| `bands` | `{ from: string \| number; to: string \| number; label?: string }[]` | `[]` | Line and bar only. Shades stretches of the x-axis, such as night hours on a 24-hour chart. from and to are x-axis values (values of xKey), both included. When from comes after to in the data, the band wraps past the end: 22:00 to 06:00 shades 22:00 to the last point and the first point to 06:00. A band naming a value that isn't in the data is skipped. Every band is listed under the chart with its label, so screen readers hear it too. |
|
|
37
|
+
| `limits` | `{ value: number; label?: string }[]` | `[]` | Line and bar only, not at size sm. Draws a dashed horizontal line at a y value, such as an alert limit of 35 µg/m³. The y-axis grows to include it, so a limit above every data point stays in view. Every limit is listed under the chart with its label and its value formatted by formatValue, so screen readers hear it too. |
|
|
38
|
+
| `headingLevel` | `2 \| 3 \| 4` | `3` | Pick the level that fits the page's heading outline. |
|
|
39
|
+
| `className` | `string` | | Merged with the Card classes. Later Tailwind utilities win. |
|
|
40
|
+
| `ref` | `Ref<HTMLElement>` | | Points at the section element. |
|
|
41
|
+
|
|
42
|
+
## Types and helpers
|
|
43
|
+
|
|
44
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export type ChartSeries<Row> = { key: NumericKey<Row>; label: string }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Examples
|
|
51
|
+
|
|
52
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
53
|
+
|
|
54
|
+
### Line, two series
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
type Week = { week: string; online: number; store: number }
|
|
58
|
+
|
|
59
|
+
const WEEKS: Week[] = [
|
|
60
|
+
{ week: 'W1', online: 120, store: 160 },
|
|
61
|
+
{ week: 'W2', online: 131, store: 158 },
|
|
62
|
+
{ week: 'W3', online: 142, store: 163 },
|
|
63
|
+
{ week: 'W4', online: 150, store: 155 },
|
|
64
|
+
{ week: 'W5', online: 171, store: 162 },
|
|
65
|
+
{ week: 'W6', online: 188, store: 159 },
|
|
66
|
+
{ week: 'W7', online: 197, store: 166 },
|
|
67
|
+
{ week: 'W8', online: 214, store: 161 },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
<ChartPanel<Week>
|
|
71
|
+
title="Weekly orders"
|
|
72
|
+
summary="Online orders rose from 120 to 214 over eight weeks and passed in-store orders in week 5."
|
|
73
|
+
kind="line"
|
|
74
|
+
data={WEEKS}
|
|
75
|
+
xKey="week"
|
|
76
|
+
xLabel="Week"
|
|
77
|
+
series={[
|
|
78
|
+
{ key: 'online', label: 'Online' },
|
|
79
|
+
{ key: 'store', label: 'In store' },
|
|
80
|
+
]}
|
|
81
|
+
headingLevel={4}
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Bar, one series
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
type Category = { category: string; returns: number }
|
|
89
|
+
|
|
90
|
+
const RETURNS: Category[] = [
|
|
91
|
+
{ category: 'Replicas', returns: 18 },
|
|
92
|
+
{ category: 'Optics', returns: 11 },
|
|
93
|
+
{ category: 'Parts', returns: 26 },
|
|
94
|
+
{ category: 'Ammo', returns: 7 },
|
|
95
|
+
{ category: 'Tactical', returns: 4 },
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
<ChartPanel<Category>
|
|
99
|
+
title="Returns by category"
|
|
100
|
+
summary="Parts have the most returns (26), then replicas (18); tactical equipment has the fewest (4)."
|
|
101
|
+
kind="bar"
|
|
102
|
+
data={RETURNS}
|
|
103
|
+
xKey="category"
|
|
104
|
+
xLabel="Category"
|
|
105
|
+
series={[{ key: 'returns', label: 'Returns' }]}
|
|
106
|
+
headingLevel={4}
|
|
107
|
+
/>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Area under a smooth line
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
type Day = { day: string; visits: number; orders: number }
|
|
114
|
+
|
|
115
|
+
const DAYS: Day[] = [
|
|
116
|
+
{ day: 'Mon', visits: 1840, orders: 61 },
|
|
117
|
+
{ day: 'Tue', visits: 2210, orders: 74 },
|
|
118
|
+
{ day: 'Wed', visits: 1960, orders: 58 },
|
|
119
|
+
{ day: 'Thu', visits: 2650, orders: 92 },
|
|
120
|
+
{ day: 'Fri', visits: 3120, orders: 118 },
|
|
121
|
+
{ day: 'Sat', visits: 3890, orders: 141 },
|
|
122
|
+
{ day: 'Sun', visits: 3370, orders: 126 },
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
<ChartPanel<Day>
|
|
126
|
+
title="Visits this week"
|
|
127
|
+
summary="Visits climbed from 1,840 on Monday to a Saturday peak of 3,890."
|
|
128
|
+
kind="line"
|
|
129
|
+
area
|
|
130
|
+
curve="smooth"
|
|
131
|
+
data={DAYS}
|
|
132
|
+
xKey="day"
|
|
133
|
+
xLabel="Day"
|
|
134
|
+
series={[{ key: 'visits', label: 'Visits' }]}
|
|
135
|
+
headline="19,040"
|
|
136
|
+
headingLevel={4}
|
|
137
|
+
/>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### No grid lines: only the labels along the axes
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
type Week = { week: string; online: number; store: number }
|
|
144
|
+
|
|
145
|
+
const WEEKS: Week[] = [
|
|
146
|
+
{ week: 'W1', online: 120, store: 160 },
|
|
147
|
+
{ week: 'W2', online: 131, store: 158 },
|
|
148
|
+
{ week: 'W3', online: 142, store: 163 },
|
|
149
|
+
{ week: 'W4', online: 150, store: 155 },
|
|
150
|
+
{ week: 'W5', online: 171, store: 162 },
|
|
151
|
+
{ week: 'W6', online: 188, store: 159 },
|
|
152
|
+
{ week: 'W7', online: 197, store: 166 },
|
|
153
|
+
{ week: 'W8', online: 214, store: 161 },
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
<ChartPanel<Week>
|
|
157
|
+
title="Weekly orders, gridless"
|
|
158
|
+
summary="Online orders rose from 120 to 214 over eight weeks and passed in-store orders in week 5."
|
|
159
|
+
kind="line"
|
|
160
|
+
area
|
|
161
|
+
curve="smooth"
|
|
162
|
+
grid={false}
|
|
163
|
+
data={WEEKS}
|
|
164
|
+
xKey="week"
|
|
165
|
+
xLabel="Week"
|
|
166
|
+
series={[
|
|
167
|
+
{ key: 'online', label: 'Online' },
|
|
168
|
+
{ key: 'store', label: 'In store' },
|
|
169
|
+
]}
|
|
170
|
+
headingLevel={4}
|
|
171
|
+
/>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### A shaded band: night hours on a 24-hour series (the band wraps past midnight)
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
type Hour = { hour: string; pm25: number }
|
|
178
|
+
|
|
179
|
+
const PM25 = [9, 8, 7, 7, 8, 11, 17, 26, 31, 24, 18, 15, 14, 13, 14, 16, 21, 29, 34, 27, 19, 14, 11, 10]
|
|
180
|
+
|
|
181
|
+
const HOURS: Hour[] = PM25.map((pm25, hour) => ({ hour: `${String(hour).padStart(2, '0')}:00`, pm25 }))
|
|
182
|
+
|
|
183
|
+
<ChartPanel<Hour>
|
|
184
|
+
title="Fine particles today"
|
|
185
|
+
summary="PM2.5 peaked at the morning and evening rush hours, 31 and 34 µg/m³, and stayed lowest overnight."
|
|
186
|
+
kind="line"
|
|
187
|
+
area
|
|
188
|
+
curve="smooth"
|
|
189
|
+
data={HOURS}
|
|
190
|
+
xKey="hour"
|
|
191
|
+
xLabel="Hour"
|
|
192
|
+
series={[{ key: 'pm25', label: 'PM2.5 (µg/m³)' }]}
|
|
193
|
+
bands={[{ from: '22:00', to: '06:00', label: 'Night' }]}
|
|
194
|
+
limits={[{ value: 35, label: 'Alert limit (µg/m³)' }]}
|
|
195
|
+
headingLevel={4}
|
|
196
|
+
/>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Small: stat cards, each with a sparkline (a tiny line chart with no axes)
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
type Week = { week: string; online: number; store: number }
|
|
203
|
+
|
|
204
|
+
type Day = { day: string; visits: number; orders: number }
|
|
205
|
+
|
|
206
|
+
const WEEKS: Week[] = [
|
|
207
|
+
{ week: 'W1', online: 120, store: 160 },
|
|
208
|
+
{ week: 'W2', online: 131, store: 158 },
|
|
209
|
+
{ week: 'W3', online: 142, store: 163 },
|
|
210
|
+
{ week: 'W4', online: 150, store: 155 },
|
|
211
|
+
{ week: 'W5', online: 171, store: 162 },
|
|
212
|
+
{ week: 'W6', online: 188, store: 159 },
|
|
213
|
+
{ week: 'W7', online: 197, store: 166 },
|
|
214
|
+
{ week: 'W8', online: 214, store: 161 },
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
const DAYS: Day[] = [
|
|
218
|
+
{ day: 'Mon', visits: 1840, orders: 61 },
|
|
219
|
+
{ day: 'Tue', visits: 2210, orders: 74 },
|
|
220
|
+
{ day: 'Wed', visits: 1960, orders: 58 },
|
|
221
|
+
{ day: 'Thu', visits: 2650, orders: 92 },
|
|
222
|
+
{ day: 'Fri', visits: 3120, orders: 118 },
|
|
223
|
+
{ day: 'Sat', visits: 3890, orders: 141 },
|
|
224
|
+
{ day: 'Sun', visits: 3370, orders: 126 },
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
<ChartPanel<Day>
|
|
228
|
+
title="Visits"
|
|
229
|
+
headline="19,040"
|
|
230
|
+
summary="Up every day but Wednesday and Sunday."
|
|
231
|
+
kind="line"
|
|
232
|
+
area
|
|
233
|
+
curve="smooth"
|
|
234
|
+
size="sm"
|
|
235
|
+
data={DAYS}
|
|
236
|
+
xKey="day"
|
|
237
|
+
xLabel="Day"
|
|
238
|
+
series={[{ key: 'visits', label: 'Visits' }]}
|
|
239
|
+
headingLevel={4}
|
|
240
|
+
/>
|
|
241
|
+
<ChartPanel<Day>
|
|
242
|
+
title="Orders"
|
|
243
|
+
headline="670"
|
|
244
|
+
summary="Saturday took 141, the most of the week."
|
|
245
|
+
kind="bar"
|
|
246
|
+
size="sm"
|
|
247
|
+
data={DAYS}
|
|
248
|
+
xKey="day"
|
|
249
|
+
xLabel="Day"
|
|
250
|
+
series={[{ key: 'orders', label: 'Orders' }]}
|
|
251
|
+
headingLevel={4}
|
|
252
|
+
/>
|
|
253
|
+
<ChartPanel<Week>
|
|
254
|
+
title="Online orders"
|
|
255
|
+
headline="214"
|
|
256
|
+
summary="Eight weeks of growth, from 120 a week."
|
|
257
|
+
kind="line"
|
|
258
|
+
curve="smooth"
|
|
259
|
+
size="sm"
|
|
260
|
+
data={WEEKS}
|
|
261
|
+
xKey="week"
|
|
262
|
+
xLabel="Week"
|
|
263
|
+
series={[{ key: 'online', label: 'Online' }]}
|
|
264
|
+
headingLevel={4}
|
|
265
|
+
/>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Donut with a centre total
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
type Slice = { channel: string; orders: number }
|
|
272
|
+
|
|
273
|
+
const CHANNELS: Slice[] = [
|
|
274
|
+
{ channel: 'Web shop', orders: 412 },
|
|
275
|
+
{ channel: 'Field store', orders: 236 },
|
|
276
|
+
{ channel: 'Club orders', orders: 148 },
|
|
277
|
+
{ channel: 'Marketplace', orders: 87 },
|
|
278
|
+
{ channel: 'Phone', orders: 41 },
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
<ChartPanel<Slice>
|
|
282
|
+
title="Orders by channel"
|
|
283
|
+
summary="The web shop took 412 of 924 orders this month, nearly half; phone orders are 4%."
|
|
284
|
+
kind="donut"
|
|
285
|
+
data={CHANNELS}
|
|
286
|
+
xKey="channel"
|
|
287
|
+
xLabel="Channel"
|
|
288
|
+
series={[{ key: 'orders', label: 'Orders' }]}
|
|
289
|
+
centerLabel="Orders"
|
|
290
|
+
headingLevel={4}
|
|
291
|
+
/>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Radar, two series
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
type Profile = { measure: string; m4: number; ak: number }
|
|
298
|
+
|
|
299
|
+
const PROFILES: Profile[] = [
|
|
300
|
+
{ measure: 'Range', m4: 78, ak: 64 },
|
|
301
|
+
{ measure: 'Accuracy', m4: 82, ak: 70 },
|
|
302
|
+
{ measure: 'Rate of fire', m4: 74, ak: 86 },
|
|
303
|
+
{ measure: 'Weight', m4: 58, ak: 44 },
|
|
304
|
+
{ measure: 'Reliability', m4: 70, ak: 90 },
|
|
305
|
+
{ measure: 'Upgrades', m4: 92, ak: 62 },
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
<ChartPanel<Profile>
|
|
309
|
+
title="Replica profiles"
|
|
310
|
+
summary="The M4 replica leads on range, accuracy and upgrades; the AK replica on rate of fire and reliability."
|
|
311
|
+
kind="radar"
|
|
312
|
+
data={PROFILES}
|
|
313
|
+
xKey="measure"
|
|
314
|
+
xLabel="Measure"
|
|
315
|
+
series={[
|
|
316
|
+
{ key: 'm4', label: 'M4 replica' },
|
|
317
|
+
{ key: 'ak', label: 'AK replica' },
|
|
318
|
+
]}
|
|
319
|
+
headingLevel={4}
|
|
320
|
+
/>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Keyboard
|
|
324
|
+
|
|
325
|
+
| Keys | Result |
|
|
326
|
+
|---|---|
|
|
327
|
+
| `Tab` | Skips the chart, which isn't a tab stop (a place the Tab key lands), and goes to Show as table. |
|
|
328
|
+
| `Enter` `Space` | On Show as table, shows or hides the table. |
|
|
329
|
+
|
|
330
|
+
## Do
|
|
331
|
+
|
|
332
|
+
- Write the summary as the finding. Don't just describe the axes.
|
|
333
|
+
- Use a line for change over time and bars to compare categories.
|
|
334
|
+
- Keep to two series. Give a third its own panel.
|
|
335
|
+
- In print or greyscale, you can rely on the second line series being dashed. It doesn't depend on colour alone.
|
|
336
|
+
- Use size sm with a headline for a stat card. The caption and the table stay.
|
|
337
|
+
- Use bands for context such as night hours or a sale, and give each one a label.
|
|
338
|
+
- Past six slices, fold a donut's smallest slices into one Other row.
|
|
339
|
+
|
|
340
|
+
## Don't
|
|
341
|
+
|
|
342
|
+
- Don't give a chart two y-axes. Two measures means two panels.
|
|
343
|
+
- Don't colour a series with a status colour. Those colours mean state.
|
|
344
|
+
- Don't put a number on every point. The table has them all.
|
|
345
|
+
- Don't use a donut to compare categories over time. That's a job for a line or bars.
|
|
346
|
+
|
|
347
|
+
## Anatomy
|
|
348
|
+
|
|
349
|
+
- **Title**: The panel's heading. It's also the accessible name, the name a screen reader announces for the panel.
|
|
350
|
+
- **Summary**: One sentence that works as the chart's caption: what a reader should take away.
|
|
351
|
+
- **Legend**: Names both series when there are two. With one series, the title names it. A donut lists every slice with its value and share.
|
|
352
|
+
- **Headline**: Optional. The figure the chart is about, printed large under the title.
|
|
353
|
+
- **Chart**: A line, bar, donut or radar chart, with a tooltip that follows the pointer. It's hidden from assistive tech (screen readers and similar tools), which get the table instead.
|
|
354
|
+
- **Centre total**: Donut only. The sum of the slices, with centerLabel under it.
|
|
355
|
+
- **Shaded ranges**: Optional, line and bar only. A quiet wash behind the chart for each band, listed under it with its label and range.
|
|
356
|
+
- **Show as table**: A button that opens and closes a DataTable with the same rows. It uses aria-expanded, an ARIA (Accessible Rich Internet Applications) attribute that tells screen readers whether it's open.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# ChatMessage
|
|
2
|
+
|
|
3
|
+
> One turn in a conversation with an assistant: the person's words in a bubble, the reply at full width, or a centred system line. While a reply is still streaming in, it sets aria-busy (an ARIA, Accessible Rich Internet Applications, attribute that tells screen readers the content isn't finished) and shows a caret that doesn't blink.
|
|
4
|
+
|
|
5
|
+
- **Best for:** Each turn inside a ChatThread.
|
|
6
|
+
- **Not for:** Comments or an activity history between people: use Timeline.
|
|
7
|
+
- Molecule. Source: `src/components/molecules/chat-message/ChatMessage.tsx`
|
|
8
|
+
- Rendered docs page: `/components/chat-message`
|
|
9
|
+
|
|
10
|
+
## Import
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { ChatMessage } from 'src/components/molecules/chat-message/ChatMessage'
|
|
14
|
+
import { ResponseFeedback } from 'src/components/molecules/chat-message/ResponseFeedback'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Requires
|
|
18
|
+
|
|
19
|
+
- A ChatThread around it. Outside one, a message still shows, but nothing announces it when it finishes.
|
|
20
|
+
|
|
21
|
+
## Props
|
|
22
|
+
|
|
23
|
+
| Prop | Type | Default | Description |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| `role` | `'user' \| 'assistant' \| 'system'` | | Required. Who's speaking. system is a quiet, centred status line. |
|
|
26
|
+
| `author` | `string` | | Required. "You", "Assistant". It names the message. |
|
|
27
|
+
| `time` | `string` | | The date and time in ISO format (the international standard way of writing them), for <time dateTime>. |
|
|
28
|
+
| `timeLabel` | `string` | | The time as shown on screen: "09:40". |
|
|
29
|
+
| `streaming` | `boolean` | `false` | The text is still arriving: sets aria-busy and shows a caret. Inside a ChatThread, the finished text is announced once, when this turns false. |
|
|
30
|
+
| `actions` | `ReactNode` | | Small buttons under an assistant message. They aren't shown on a user message. |
|
|
31
|
+
| `className` | `string` | | Goes on the article. |
|
|
32
|
+
| `onSubmit (ResponseFeedback)` | `(vote: 'helpful' \| 'not-helpful') => void \| Promise<void>` | | Required. Runs on a thumbs up or down. Return a promise to show "Submitting feedback" until it settles; a rejection puts the buttons back. |
|
|
33
|
+
| `onDetail (ResponseFeedback)` | `(text: string) => void` | | Offers an optional, dismissable "what was wrong" box after a Not helpful vote. Leave it out for no such step. |
|
|
34
|
+
| `className (ResponseFeedback)` | `string` | | Goes on the group. |
|
|
35
|
+
|
|
36
|
+
## Types and helpers
|
|
37
|
+
|
|
38
|
+
Exported beside the component, from the same file. Import a type with `import type { ... }`.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
export type ChatMessageRole = 'user' | 'assistant' | 'system'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export type FeedbackVote = 'helpful' | 'not-helpful'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
Each is compiled and rendered on the docs page. Imports are omitted; see above.
|
|
51
|
+
|
|
52
|
+
### The three roles
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
<div className="flex w-full max-w-xl flex-col gap-6">
|
|
56
|
+
<ChatMessage role="user" author="You" time="2026-10-15T09:40:00+03:00" timeLabel="09:40">
|
|
57
|
+
Which replicas are running low at Depot 4?
|
|
58
|
+
</ChatMessage>
|
|
59
|
+
<ChatMessage
|
|
60
|
+
role="assistant"
|
|
61
|
+
author="Assistant"
|
|
62
|
+
time="2026-10-15T09:40:00+03:00"
|
|
63
|
+
timeLabel="09:40"
|
|
64
|
+
actions={
|
|
65
|
+
<>
|
|
66
|
+
<Button size="xs" variant="ghost" iconStart={<Copy />}>
|
|
67
|
+
Copy
|
|
68
|
+
</Button>
|
|
69
|
+
<Button size="xs" variant="ghost" iconStart={<RotateCcw />}>
|
|
70
|
+
Retry
|
|
71
|
+
</Button>
|
|
72
|
+
</>
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
Two are under their reorder point: the K-41 field radio (6 left, reorder at 10) and the trail
|
|
76
|
+
pack (3 left, reorder at 8). Everything else has at least two weeks of cover.
|
|
77
|
+
</ChatMessage>
|
|
78
|
+
<ChatMessage role="system" author="Status">
|
|
79
|
+
Assistant stopped
|
|
80
|
+
</ChatMessage>
|
|
81
|
+
</div>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### While the reply is still streaming in: aria-busy, an ARIA (Accessible Rich Internet Applications) attribute that tells screen readers the message isn't finished, and a cursor that doesn't blink
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<div className="flex w-full max-w-xl flex-col gap-6">
|
|
88
|
+
<ChatMessage role="assistant" author="Assistant" streaming>
|
|
89
|
+
Depot 4 holds 1,240 replicas across 38 lines. The busiest
|
|
90
|
+
</ChatMessage>
|
|
91
|
+
</div>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Feedback on a reply: thumbs up or down, one vote, and an optional detail step after Not helpful
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
const pretendSend = () => new Promise<void>(resolve => setTimeout(resolve, 800))
|
|
98
|
+
|
|
99
|
+
<div className="flex w-full max-w-xl flex-col gap-6">
|
|
100
|
+
<ChatMessage
|
|
101
|
+
role="assistant"
|
|
102
|
+
author="Assistant"
|
|
103
|
+
actions={<ResponseFeedback onSubmit={pretendSend} onDetail={() => {}} />}
|
|
104
|
+
>
|
|
105
|
+
Depot 4 holds 1,240 replicas. Eleven are below their reorder point.
|
|
106
|
+
</ChatMessage>
|
|
107
|
+
</div>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Keyboard
|
|
111
|
+
|
|
112
|
+
| Keys | Result |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `Tab` | Moves to the controls inside the message: a ToolCall's details, an approval's buttons, the actions. |
|
|
115
|
+
|
|
116
|
+
## Do
|
|
117
|
+
|
|
118
|
+
- Keep streaming true until the last piece of text has arrived, then set it to false once.
|
|
119
|
+
- Put tool calls and approvals in the assistant message where they happened, in order with its text.
|
|
120
|
+
- Use a system line for events in the conversation itself: stopped, retried, context cleared.
|
|
121
|
+
- Give every message an author, including a system line ("Status"). It names the article.
|
|
122
|
+
- Put ResponseFeedback in the actions of a generated reply once it has finished streaming, so a vote rates the whole reply.
|
|
123
|
+
|
|
124
|
+
## Don't
|
|
125
|
+
|
|
126
|
+
- Don't animate the text in yourself. useStreamedText or your stream supplies it, and the thread handles announcing it.
|
|
127
|
+
- Don't put actions on the person's own message. Editing a sent message is a different flow.
|
|
128
|
+
- Don't colour the assistant's text. The bubble, the alignment and the author line already tell the roles apart.
|
|
129
|
+
|
|
130
|
+
## Common mistakes
|
|
131
|
+
|
|
132
|
+
Wrong:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<ChatMessage role="assistant" author="Assistant">{partial}<span className="animate-pulse">▍</span></ChatMessage>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Right:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<ChatMessage role="assistant" author="Assistant" streaming={!done}>{partial}</ChatMessage>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
streaming sets aria-busy, draws a caret that doesn't blink, and tells the thread to announce the text once, when it's finished. A hand-made blinking caret flashes for the whole stream.
|
|
145
|
+
|
|
146
|
+
## Anatomy
|
|
147
|
+
|
|
148
|
+
- **Article**: Named by the author line, so a screen reader hears who's speaking first. Has aria-busy while streaming.
|
|
149
|
+
- **Author line**: The author as a label in monospaced type, then the time in a <time dateTime> element.
|
|
150
|
+
- **Bubble (user)**: The person's words on surface.sunken, aligned right, at most 85% of the width.
|
|
151
|
+
- **Body (assistant)**: Full width, with no bubble: text, paragraphs, and ToolCall and ApprovalRequest blocks stacked in a column.
|
|
152
|
+
- **Caret**: A solid block in the signal colour after the last paragraph while streaming, drawn with the CSS ::after rule. It never blinks.
|
|
153
|
+
- **Actions**: Small buttons under an assistant message: Copy, Retry, and ResponseFeedback (thumbs up and down).
|
|
154
|
+
- **System line**: A centred label in monospaced type between two thin lines: "Assistant stopped". Its author is there for screen readers only.
|