division-ds 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/AGENTS.md +23 -3
  2. package/README.md +11 -0
  3. package/division-ds-0.2.1.tgz +0 -0
  4. package/package.json +2 -2
  5. package/public/docs/components/accordion.md +207 -0
  6. package/public/docs/components/ai-label.md +90 -0
  7. package/public/docs/components/alert-dialog.md +292 -0
  8. package/public/docs/components/alert.md +175 -0
  9. package/public/docs/components/approval-request.md +148 -0
  10. package/public/docs/components/auth-form.md +132 -0
  11. package/public/docs/components/avatar-group.md +152 -0
  12. package/public/docs/components/avatar.md +179 -0
  13. package/public/docs/components/badge.md +178 -0
  14. package/public/docs/components/breadcrumbs.md +112 -0
  15. package/public/docs/components/button.md +204 -0
  16. package/public/docs/components/card.md +257 -0
  17. package/public/docs/components/carousel.md +130 -0
  18. package/public/docs/components/cart.md +172 -0
  19. package/public/docs/components/chart-panel.md +358 -0
  20. package/public/docs/components/chat-message.md +157 -0
  21. package/public/docs/components/chat-thread.md +244 -0
  22. package/public/docs/components/checkbox.md +149 -0
  23. package/public/docs/components/checkout-form.md +128 -0
  24. package/public/docs/components/command-palette.md +147 -0
  25. package/public/docs/components/data-table.md +401 -0
  26. package/public/docs/components/description-list.md +103 -0
  27. package/public/docs/components/dialog.md +197 -0
  28. package/public/docs/components/divider.md +97 -0
  29. package/public/docs/components/dropdown-menu.md +222 -0
  30. package/public/docs/components/empty-state.md +145 -0
  31. package/public/docs/components/footer.md +397 -0
  32. package/public/docs/components/form-field.md +216 -0
  33. package/public/docs/components/icon-button.md +127 -0
  34. package/public/docs/components/input.md +200 -0
  35. package/public/docs/components/kanban-board.md +300 -0
  36. package/public/docs/components/kbd.md +72 -0
  37. package/public/docs/components/link.md +98 -0
  38. package/public/docs/components/list-item.md +241 -0
  39. package/public/docs/components/loading-animation.md +72 -0
  40. package/public/docs/components/multi-select.md +202 -0
  41. package/public/docs/components/navbar.md +144 -0
  42. package/public/docs/components/newsletter-form.md +112 -0
  43. package/public/docs/components/page-header.md +151 -0
  44. package/public/docs/components/pagination.md +212 -0
  45. package/public/docs/components/product-card.md +172 -0
  46. package/public/docs/components/progress.md +101 -0
  47. package/public/docs/components/prompt-input.md +151 -0
  48. package/public/docs/components/quantity-stepper.md +140 -0
  49. package/public/docs/components/radio.md +177 -0
  50. package/public/docs/components/rating.md +106 -0
  51. package/public/docs/components/score-ring.md +95 -0
  52. package/public/docs/components/search.md +116 -0
  53. package/public/docs/components/section-heading.md +113 -0
  54. package/public/docs/components/segmented-control.md +126 -0
  55. package/public/docs/components/select.md +224 -0
  56. package/public/docs/components/sheet.md +145 -0
  57. package/public/docs/components/sidebar.md +193 -0
  58. package/public/docs/components/skeleton.md +135 -0
  59. package/public/docs/components/slider.md +114 -0
  60. package/public/docs/components/sparkline.md +128 -0
  61. package/public/docs/components/spinner.md +98 -0
  62. package/public/docs/components/stat.md +106 -0
  63. package/public/docs/components/status-tracker.md +145 -0
  64. package/public/docs/components/stepper.md +147 -0
  65. package/public/docs/components/switch.md +153 -0
  66. package/public/docs/components/tabs.md +180 -0
  67. package/public/docs/components/tag.md +105 -0
  68. package/public/docs/components/textarea.md +148 -0
  69. package/public/docs/components/timeline.md +152 -0
  70. package/public/docs/components/toast.md +194 -0
  71. package/public/docs/components/tool-call.md +107 -0
  72. package/public/docs/components/tooltip.md +108 -0
  73. package/public/manifest/components.json +198 -15
  74. package/public/manifest/tokens.json +75 -5
  75. package/scripts/division-ds.mjs +78 -42
  76. package/scripts/lib/agent-docs.mjs +80 -6
  77. package/scripts/lib/ai-copy.mjs +120 -0
  78. package/scripts/lib/button-spacing.mjs +5 -5
  79. package/scripts/lib/check-text.mjs +57 -0
  80. package/scripts/lib/contrast.mjs +111 -0
  81. package/scripts/lib/host-inline.mjs +59 -0
  82. package/scripts/lib/mcp-server.mjs +116 -0
  83. package/scripts/lib/mcp-tools.mjs +266 -0
  84. package/scripts/lib/props-validator.mjs +3 -2
  85. package/scripts/lib/rules-installer.mjs +42 -4
  86. package/scripts/lib/token-discipline.mjs +10 -3
  87. package/skills/division-ds/SKILL.md +90 -0
  88. package/skills/division-ds/commands/add.md +25 -0
  89. package/skills/division-ds/commands/build.md +46 -0
  90. package/skills/division-ds/commands/fix.md +28 -0
  91. package/skills/division-ds/commands/review.md +25 -0
  92. package/skills/division-ds/topics/ai.md +8 -0
  93. package/skills/division-ds/topics/colour.md +11 -0
  94. package/skills/division-ds/topics/feedback.md +9 -0
  95. package/skills/division-ds/topics/forms.md +22 -0
  96. package/skills/division-ds/topics/layout.md +36 -0
  97. package/skills/division-ds/topics/motion.md +10 -0
  98. package/skills/division-ds/topics/overlays.md +9 -0
@@ -0,0 +1,130 @@
1
+ # Carousel
2
+
3
+ > A row of items that scrolls sideways. The browser does the scrolling, and Previous and Next move it a page at a time. Nothing ever moves on its own.
4
+
5
+ - **Best for:** A sideways preview of more items than fit, with a link to the full list nearby.
6
+ - **Not for:** A few items that fit: use a grid. And it's not for the only copy of anything.
7
+ - Organism. Source: `src/components/organisms/carousel/Carousel.tsx`
8
+ - Rendered docs page: `/components/carousel`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Carousel } from 'src/components/organisms/carousel/Carousel'
14
+ ```
15
+
16
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ |---|---|---|---|
22
+ | `label` | `string` | | Required. The region's accessible name (the name a screen reader announces), for example "Offers". Leaving it out is a compile error. |
23
+ | `children` | `ReactNode` | | Each child becomes one list item and keeps its own key. |
24
+ | `itemWidth` | `string` | `'min(18rem, 80%)'` | Any CSS length. A percentage is a share of the track's width. Keep it under the full width, so the edge of the next item shows. |
25
+ | `gutter` | `string` | `'0.5rem'` | The space between the track's edge and the first and last item, in any CSS length (rem is a unit tied to the base text size). It's never less than 0.5rem, whatever you pass, because an edge item draws its focus ring there. Set it to the page gutter to run the track edge to edge. |
26
+ | `className` | `string` | | Merged onto the region. |
27
+ | `ref` | `Ref<HTMLElement>` | | Forwarded to the region. |
28
+
29
+ ## Examples
30
+
31
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
32
+
33
+ ### Product cards
34
+
35
+ ```tsx
36
+ const photo = (id: string) => ({ src: `/media/products/${id}.webp` })
37
+
38
+ const OFFERS: Product[] = [
39
+ { id: 'kestrel-k', name: 'Kestrel K compact replica', href: '#kestrel-k', price: 12900, compareAtPrice: 14900, badge: 'Sale' },
40
+ { id: 'field-goggles', name: 'Field goggles', href: '#field-goggles', price: 3400, compareAtPrice: 3900, badge: 'Sale' },
41
+ { id: 'beam-600', name: 'Beam 600 rail light', href: '#beam-600', price: 2400, compareAtPrice: 2900 },
42
+ { id: 'speed-loader', name: 'Speed loader, 150 BBs', href: '#speed-loader', price: 790, compareAtPrice: 990 },
43
+ { id: 'magazine-amber', name: 'Amber magazine, 48 BBs', href: '#magazine-amber', price: 890, compareAtPrice: 1090 },
44
+ { id: 'knee-pads', name: 'Knee pads, pair', href: '#knee-pads', price: 2400, compareAtPrice: 2900 },
45
+ ].map(product => ({ ...product, image: photo(product.id) }))
46
+
47
+ function ProductCardsExample() {
48
+ const { toast } = useToast()
49
+ return (
50
+ <Carousel label="Offers" itemWidth="min(16rem, 80%)">
51
+ {OFFERS.map(product => (
52
+ <ProductCard
53
+ key={product.id}
54
+ product={product}
55
+ headingLevel={4}
56
+ onAddToCart={added => toast({ title: 'Added to cart', description: added.name, tone: 'ok' })}
57
+ />
58
+ ))}
59
+ </Carousel>
60
+ )
61
+ }
62
+
63
+ <ProductCardsExample />
64
+ ```
65
+
66
+ ### Nothing inside takes keyboard focus, so the Tab key stops on the track itself
67
+
68
+ ```tsx
69
+ const STEPS = ['Unload', 'Disconnect', 'Wipe down', 'Clean the barrel', 'Check the hop-up', 'Store']
70
+
71
+ <Carousel label="Cleaning steps" itemWidth="12rem">
72
+ {STEPS.map((step, index) => (
73
+ <div
74
+ key={step}
75
+ className="flex h-28 flex-col justify-between border border-[var(--color-border-hairline-strong)] p-4"
76
+ >
77
+ <span className="font-mono text-xs tabular-nums text-[var(--color-text-secondary)]">
78
+ {String(index + 1).padStart(2, '0')}
79
+ </span>
80
+ <span className="font-display font-semibold text-[var(--color-text-primary)]">{step}</span>
81
+ </div>
82
+ ))}
83
+ </Carousel>
84
+ ```
85
+
86
+ ### Three short items: both buttons do nothing while everything fits
87
+
88
+ ```tsx
89
+ <Carousel label="Kestrel lengths" itemWidth="8rem">
90
+ {['Kestrel K', 'Kestrel C', 'Kestrel L'].map(name => (
91
+ <div
92
+ key={name}
93
+ className="flex h-20 items-center justify-center border border-[var(--color-border-hairline-strong)] font-display font-semibold text-[var(--color-text-primary)]"
94
+ >
95
+ {name}
96
+ </div>
97
+ ))}
98
+ </Carousel>
99
+ ```
100
+
101
+ ## Keyboard
102
+
103
+ | Keys | Result |
104
+ |---|---|
105
+ | `Tab` | Moves to Previous, then Next, then through anything focusable in each item. An item scrolls into view as it takes focus. |
106
+ | `Enter` `Space` | On Previous or Next, scrolls one page of items. Does nothing (it's inert) at the matching end. |
107
+ | `←` `→` | Scroll the track when the track itself has focus. The track is only a tab stop when no item holds anything focusable. |
108
+
109
+ ## Do
110
+
111
+ - Name it for what it holds: "Offers", "Top-rated accessories".
112
+ - Give it enough items to scroll. Three that fit are a grid.
113
+ - Link to the full list nearby. A carousel is a preview, so it shouldn't be the only way to reach the rest.
114
+ - Set the gutter to the page gutter when the track should run to the edge of the page.
115
+
116
+ ## Don't
117
+
118
+ - Don't add autoplay. Content that moves on its own needs a pause control, and it pulls attention away from whoever's reading the page.
119
+ - Don't put the only copy of anything in it. What's off to the side is easy to miss.
120
+ - Don't hide the buttons on touch screens. A pointer, a switch device or a screen magnifier may be all someone has.
121
+ - Don't wrap it in an element with overflow hidden to tidy the edges. That clips the focus ring the gutter is there to show.
122
+
123
+ ## Anatomy
124
+
125
+ - **Region**: A section with aria-roledescription="carousel", named by the label. (ARIA, Accessible Rich Internet Applications, is a set of attributes that tell screen readers what an element is.)
126
+ - **Position**: Monospaced text such as "1 to 3 of 8", saying which items are showing. It updates on every scroll, but it isn't a live region, so screen readers don't read it out each time.
127
+ - **Previous / Next**: Secondary IconButtons, placed before the items in the Tab order. At the ends they're marked aria-disabled instead of disabled, so each stays a tab stop (a place the Tab key lands).
128
+ - **Track**: A list that's the browser's own scroll container, with scroll snap and no visible scrollbar. It's padded on all four sides, so an item at the edge draws its focus ring inside the track.
129
+ - **Item**: A list item, one per child, at the item width. The edge of the next one always shows.
130
+ - **Status**: Hidden from view, read by screen readers. Says where the track stopped after a press on Previous or Next.
@@ -0,0 +1,172 @@
1
+ # Cart
2
+
3
+ > The lines someone means to buy, with quantities, line totals and a subtotal that screen readers announce as it changes. Removing a line can be undone, and focus (where keyboard input goes) is never lost.
4
+
5
+ - **Best for:** The lines someone means to buy: quantities, line totals, a subtotal and removal that can be undone.
6
+ - **Not for:** Collecting shipping and payment: use CheckoutForm.
7
+ - Organism. Source: `src/components/organisms/cart/Cart.tsx`
8
+ - Rendered docs page: `/components/cart`
9
+
10
+ ## Import
11
+
12
+ ```tsx
13
+ import { Cart } from 'src/components/organisms/cart/Cart'
14
+ ```
15
+
16
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ |---|---|---|---|
22
+ | `lines` | `CartLine[]` | | Required. The cart doesn't keep any lines of its own. |
23
+ | `onQuantityChange` | `(id: string, quantity: number) => void` | | Required. Called when a line's QuantityStepper changes. |
24
+ | `onRemove` | `(id: string) => void` | | Required. Called when Remove is pressed. |
25
+ | `onRestore` | `(line: CartLine, index: number) => void` | | Puts a removed line back at its old position. When you pass it, the removal toast offers Undo. |
26
+ | `title` | `string` | `'Cart'` | The heading's text, and the cart's accessible name (the name a screen reader announces). |
27
+ | `headingLevel` | `2 \| 3 \| 4` | `2` | Match the page's heading outline. |
28
+ | `emptyMessage` | `string` | `'Your cart is empty.'` | Shown in place of the list. |
29
+ | `footer` | `ReactNode` | | Under the subtotal, such as a checkout button. Hidden while the cart's empty. |
30
+ | `currency` | `string` | `'EUR'` | Passed to formatMoney. |
31
+ | `locale` | `string` | `'en-IE'` | Passed to formatMoney. |
32
+ | `renderLink` | `(props: LinkRenderProps) => ReactNode` | `a plain anchor` | Draws a line name that has an href. |
33
+ | `className` | `string` | | Merged with the root classes. Later Tailwind utilities win. |
34
+ | `ref` | `Ref<HTMLElement>` | | The section. |
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 CartLine = {
42
+ /** Unique within lines: rows are keyed by it, and callbacks identify lines by it. */
43
+ id: string
44
+ name: string
45
+ href?: string
46
+ /** Size, colour and the like: "Size M · Ember". */
47
+ detail?: string
48
+ /** In the currency's minor unit (cents for EUR). */
49
+ unitPrice: number
50
+ quantity: number
51
+ /** @default 99 */
52
+ maxQuantity?: number
53
+ }
54
+ ```
55
+
56
+ ## Examples
57
+
58
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
59
+
60
+ ### In the page
61
+
62
+ ```tsx
63
+ const LINES: CartLine[] = [
64
+ { id: 'kestrel-c', name: 'Kestrel C carbine replica', href: '#kestrel-c', detail: 'Black', unitPrice: 13900, quantity: 1 },
65
+ {
66
+ id: 'prism-holo-sight',
67
+ name: 'Prism holo sight',
68
+ href: '#prism-holo-sight',
69
+ unitPrice: 4900,
70
+ quantity: 2,
71
+ },
72
+ { id: 'beam-600', name: 'Beam 600 rail light', href: '#beam-600', unitPrice: 2400, quantity: 1 },
73
+ ]
74
+
75
+ function useCart(initial: CartLine[]) {
76
+ const [lines, setLines] = useState(initial)
77
+ return {
78
+ lines,
79
+ onQuantityChange: (id: string, quantity: number) =>
80
+ setLines(all => all.map(line => (line.id === id ? { ...line, quantity } : line))),
81
+ onRemove: (id: string) => setLines(all => all.filter(line => line.id !== id)),
82
+ onRestore: (line: CartLine, index: number) => setLines(all => [...all.slice(0, index), line, ...all.slice(index)]),
83
+ }
84
+ }
85
+
86
+ function InThePageExample() {
87
+ const cart = useCart(LINES)
88
+ return <Cart {...cart} title="Cart" headingLevel={4} footer={<Button>Check out</Button>} />
89
+ }
90
+
91
+ <InThePageExample />
92
+ ```
93
+
94
+ ### In a sheet
95
+
96
+ ```tsx
97
+ const LINES: CartLine[] = [
98
+ { id: 'kestrel-c', name: 'Kestrel C carbine replica', href: '#kestrel-c', detail: 'Black', unitPrice: 13900, quantity: 1 },
99
+ {
100
+ id: 'prism-holo-sight',
101
+ name: 'Prism holo sight',
102
+ href: '#prism-holo-sight',
103
+ unitPrice: 4900,
104
+ quantity: 2,
105
+ },
106
+ { id: 'beam-600', name: 'Beam 600 rail light', href: '#beam-600', unitPrice: 2400, quantity: 1 },
107
+ ]
108
+
109
+ function useCart(initial: CartLine[]) {
110
+ const [lines, setLines] = useState(initial)
111
+ return {
112
+ lines,
113
+ onQuantityChange: (id: string, quantity: number) =>
114
+ setLines(all => all.map(line => (line.id === id ? { ...line, quantity } : line))),
115
+ onRemove: (id: string) => setLines(all => all.filter(line => line.id !== id)),
116
+ onRestore: (line: CartLine, index: number) => setLines(all => [...all.slice(0, index), line, ...all.slice(index)]),
117
+ }
118
+ }
119
+
120
+ function InASheetExample() {
121
+ const cart = useCart(LINES)
122
+ return (
123
+ <Sheet>
124
+ <SheetTrigger asChild>
125
+ <Button variant="secondary">Open cart</Button>
126
+ </SheetTrigger>
127
+ <SheetContent title="Your cart">
128
+ <Cart {...cart} title="Items" headingLevel={3} footer={<Button>Check out</Button>} />
129
+ </SheetContent>
130
+ </Sheet>
131
+ )
132
+ }
133
+
134
+ <InASheetExample />
135
+ ```
136
+
137
+ ### Empty
138
+
139
+ ```tsx
140
+ <Cart lines={[]} onQuantityChange={() => {}} onRemove={() => {}} headingLevel={4} title="Saved for later" />
141
+ ```
142
+
143
+ ## Keyboard
144
+
145
+ | Keys | Result |
146
+ |---|---|
147
+ | `Tab` | Moves through each line's link, quantity and Remove. |
148
+ | `↑` `↓` | On a quantity, steps it by one. |
149
+ | `Enter` | On Remove, removes the line. Focus moves to the next line. |
150
+ | `F8` | Jumps to the removal toast, so you can reach Undo. |
151
+
152
+ ## Do
153
+
154
+ - Pass onRestore, so a removal can be undone.
155
+ - Store prices in the currency's smallest unit (cents for EUR).
156
+ - Put the cart in a Sheet on narrow screens. The Undo toast shows inside it.
157
+ - Remove a line on the page, then open a sheet: the Undo toast moves into the sheet and still works there.
158
+ - Expect Undo pressed there to leave focus on the sheet's own toast list, inside its focus trap (the sheet keeps keyboard focus inside itself), never on the page body. The page's cart is hidden and can't take it.
159
+
160
+ ## Don't
161
+
162
+ - Don't let a quantity reach zero. Remove is how a line leaves.
163
+ - Don't remove a line without telling the cart. It's controlled: it shows only the lines you pass in.
164
+
165
+ ## Anatomy
166
+
167
+ - **Heading**: Names the cart. Focus lands here once it's empty.
168
+ - **Line**: Name (a link when it has an href), detail, unit price, quantity, line total and Remove.
169
+ - **Quantity**: A QuantityStepper named '{name} quantity', starting at 1.
170
+ - **Remove**: A ghost icon button named 'Remove {name}'.
171
+ - **Subtotal**: A status line visible only to screen readers (sr-only), plus a visual twin. Screen readers announce the status as it changes. The twin, in tabular figures, is what sighted readers see, and it's aria-hidden (hidden from screen readers) so nothing is announced twice.
172
+ - **Footer**: Optional actions, such as Check out.
@@ -0,0 +1,358 @@
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
+ Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ |---|---|---|---|
22
+ | `title` | `string` | | Required. The panel's heading and its name. |
23
+ | `summary` | `string` | | Required. One sentence, the chart's caption. |
24
+ | `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). |
25
+ | `data` | `Row[]` | | Required. One object per category. |
26
+ | `xKey` | `keyof Row` | | Required. The category. Its values must be unique. |
27
+ | `xLabel` | `string` | | Required. The header of the table's first column. |
28
+ | `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. |
29
+ | `area` | `boolean` | `false` | Line only. Fills under each line with a gradient of its colour that fades to nothing. |
30
+ | `curve` | `'linear' \| 'smooth'` | `'linear'` | Line only. smooth draws a monotone curve, so it never overshoots a data point. |
31
+ | `grid` | `boolean` | `true` | The dashed horizontal grid (on a radar, its web). false also drops the axis lines but keeps the tick labels. |
32
+ | `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. |
33
+ | `headline` | `string` | | A formatted figure printed large under the title. |
34
+ | `centerLabel` | `string` | | Donut only. The caption under the total in the centre of the ring. |
35
+ | `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. |
36
+ | `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. |
37
+ | `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. |
38
+ | `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. |
39
+ | `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. |
40
+ | `headingLevel` | `2 \| 3 \| 4` | `3` | Pick the level that fits the page's heading outline. |
41
+ | `className` | `string` | | Merged with the Card classes. Later Tailwind utilities win. |
42
+ | `ref` | `Ref<HTMLElement>` | | Points at the section element. |
43
+
44
+ ## Types and helpers
45
+
46
+ Exported beside the component, from the same file. Import a type with `import type { ... }`.
47
+
48
+ ```ts
49
+ export type ChartSeries<Row> = { key: NumericKey<Row>; label: string }
50
+ ```
51
+
52
+ ## Examples
53
+
54
+ Each is compiled and rendered on the docs page. Imports are omitted; see above.
55
+
56
+ ### Line, two series
57
+
58
+ ```tsx
59
+ type Week = { week: string; online: number; store: number }
60
+
61
+ const WEEKS: Week[] = [
62
+ { week: 'W1', online: 120, store: 160 },
63
+ { week: 'W2', online: 131, store: 158 },
64
+ { week: 'W3', online: 142, store: 163 },
65
+ { week: 'W4', online: 150, store: 155 },
66
+ { week: 'W5', online: 171, store: 162 },
67
+ { week: 'W6', online: 188, store: 159 },
68
+ { week: 'W7', online: 197, store: 166 },
69
+ { week: 'W8', online: 214, store: 161 },
70
+ ]
71
+
72
+ <ChartPanel<Week>
73
+ title="Weekly orders"
74
+ summary="Online orders rose from 120 to 214 over eight weeks and passed in-store orders in week 5."
75
+ kind="line"
76
+ data={WEEKS}
77
+ xKey="week"
78
+ xLabel="Week"
79
+ series={[
80
+ { key: 'online', label: 'Online' },
81
+ { key: 'store', label: 'In store' },
82
+ ]}
83
+ headingLevel={4}
84
+ />
85
+ ```
86
+
87
+ ### Bar, one series
88
+
89
+ ```tsx
90
+ type Category = { category: string; returns: number }
91
+
92
+ const RETURNS: Category[] = [
93
+ { category: 'Replicas', returns: 18 },
94
+ { category: 'Optics', returns: 11 },
95
+ { category: 'Parts', returns: 26 },
96
+ { category: 'Ammo', returns: 7 },
97
+ { category: 'Tactical', returns: 4 },
98
+ ]
99
+
100
+ <ChartPanel<Category>
101
+ title="Returns by category"
102
+ summary="Parts have the most returns (26), then replicas (18); tactical equipment has the fewest (4)."
103
+ kind="bar"
104
+ data={RETURNS}
105
+ xKey="category"
106
+ xLabel="Category"
107
+ series={[{ key: 'returns', label: 'Returns' }]}
108
+ headingLevel={4}
109
+ />
110
+ ```
111
+
112
+ ### Area under a smooth line
113
+
114
+ ```tsx
115
+ type Day = { day: string; visits: number; orders: number }
116
+
117
+ const DAYS: Day[] = [
118
+ { day: 'Mon', visits: 1840, orders: 61 },
119
+ { day: 'Tue', visits: 2210, orders: 74 },
120
+ { day: 'Wed', visits: 1960, orders: 58 },
121
+ { day: 'Thu', visits: 2650, orders: 92 },
122
+ { day: 'Fri', visits: 3120, orders: 118 },
123
+ { day: 'Sat', visits: 3890, orders: 141 },
124
+ { day: 'Sun', visits: 3370, orders: 126 },
125
+ ]
126
+
127
+ <ChartPanel<Day>
128
+ title="Visits this week"
129
+ summary="Visits climbed from 1,840 on Monday to a Saturday peak of 3,890."
130
+ kind="line"
131
+ area
132
+ curve="smooth"
133
+ data={DAYS}
134
+ xKey="day"
135
+ xLabel="Day"
136
+ series={[{ key: 'visits', label: 'Visits' }]}
137
+ headline="19,040"
138
+ headingLevel={4}
139
+ />
140
+ ```
141
+
142
+ ### No grid lines: only the labels along the axes
143
+
144
+ ```tsx
145
+ type Week = { week: string; online: number; store: number }
146
+
147
+ const WEEKS: Week[] = [
148
+ { week: 'W1', online: 120, store: 160 },
149
+ { week: 'W2', online: 131, store: 158 },
150
+ { week: 'W3', online: 142, store: 163 },
151
+ { week: 'W4', online: 150, store: 155 },
152
+ { week: 'W5', online: 171, store: 162 },
153
+ { week: 'W6', online: 188, store: 159 },
154
+ { week: 'W7', online: 197, store: 166 },
155
+ { week: 'W8', online: 214, store: 161 },
156
+ ]
157
+
158
+ <ChartPanel<Week>
159
+ title="Weekly orders, gridless"
160
+ summary="Online orders rose from 120 to 214 over eight weeks and passed in-store orders in week 5."
161
+ kind="line"
162
+ area
163
+ curve="smooth"
164
+ grid={false}
165
+ data={WEEKS}
166
+ xKey="week"
167
+ xLabel="Week"
168
+ series={[
169
+ { key: 'online', label: 'Online' },
170
+ { key: 'store', label: 'In store' },
171
+ ]}
172
+ headingLevel={4}
173
+ />
174
+ ```
175
+
176
+ ### A shaded band: night hours on a 24-hour series (the band wraps past midnight)
177
+
178
+ ```tsx
179
+ type Hour = { hour: string; pm25: number }
180
+
181
+ 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]
182
+
183
+ const HOURS: Hour[] = PM25.map((pm25, hour) => ({ hour: `${String(hour).padStart(2, '0')}:00`, pm25 }))
184
+
185
+ <ChartPanel<Hour>
186
+ title="Fine particles today"
187
+ summary="PM2.5 peaked at the morning and evening rush hours, 31 and 34 µg/m³, and stayed lowest overnight."
188
+ kind="line"
189
+ area
190
+ curve="smooth"
191
+ data={HOURS}
192
+ xKey="hour"
193
+ xLabel="Hour"
194
+ series={[{ key: 'pm25', label: 'PM2.5 (µg/m³)' }]}
195
+ bands={[{ from: '22:00', to: '06:00', label: 'Night' }]}
196
+ limits={[{ value: 35, label: 'Alert limit (µg/m³)' }]}
197
+ headingLevel={4}
198
+ />
199
+ ```
200
+
201
+ ### Small: stat cards, each with a sparkline (a tiny line chart with no axes)
202
+
203
+ ```tsx
204
+ type Week = { week: string; online: number; store: number }
205
+
206
+ type Day = { day: string; visits: number; orders: number }
207
+
208
+ const WEEKS: Week[] = [
209
+ { week: 'W1', online: 120, store: 160 },
210
+ { week: 'W2', online: 131, store: 158 },
211
+ { week: 'W3', online: 142, store: 163 },
212
+ { week: 'W4', online: 150, store: 155 },
213
+ { week: 'W5', online: 171, store: 162 },
214
+ { week: 'W6', online: 188, store: 159 },
215
+ { week: 'W7', online: 197, store: 166 },
216
+ { week: 'W8', online: 214, store: 161 },
217
+ ]
218
+
219
+ const DAYS: Day[] = [
220
+ { day: 'Mon', visits: 1840, orders: 61 },
221
+ { day: 'Tue', visits: 2210, orders: 74 },
222
+ { day: 'Wed', visits: 1960, orders: 58 },
223
+ { day: 'Thu', visits: 2650, orders: 92 },
224
+ { day: 'Fri', visits: 3120, orders: 118 },
225
+ { day: 'Sat', visits: 3890, orders: 141 },
226
+ { day: 'Sun', visits: 3370, orders: 126 },
227
+ ]
228
+
229
+ <ChartPanel<Day>
230
+ title="Visits"
231
+ headline="19,040"
232
+ summary="Up every day but Wednesday and Sunday."
233
+ kind="line"
234
+ area
235
+ curve="smooth"
236
+ size="sm"
237
+ data={DAYS}
238
+ xKey="day"
239
+ xLabel="Day"
240
+ series={[{ key: 'visits', label: 'Visits' }]}
241
+ headingLevel={4}
242
+ />
243
+ <ChartPanel<Day>
244
+ title="Orders"
245
+ headline="670"
246
+ summary="Saturday took 141, the most of the week."
247
+ kind="bar"
248
+ size="sm"
249
+ data={DAYS}
250
+ xKey="day"
251
+ xLabel="Day"
252
+ series={[{ key: 'orders', label: 'Orders' }]}
253
+ headingLevel={4}
254
+ />
255
+ <ChartPanel<Week>
256
+ title="Online orders"
257
+ headline="214"
258
+ summary="Eight weeks of growth, from 120 a week."
259
+ kind="line"
260
+ curve="smooth"
261
+ size="sm"
262
+ data={WEEKS}
263
+ xKey="week"
264
+ xLabel="Week"
265
+ series={[{ key: 'online', label: 'Online' }]}
266
+ headingLevel={4}
267
+ />
268
+ ```
269
+
270
+ ### Donut with a centre total
271
+
272
+ ```tsx
273
+ type Slice = { channel: string; orders: number }
274
+
275
+ const CHANNELS: Slice[] = [
276
+ { channel: 'Web shop', orders: 412 },
277
+ { channel: 'Field store', orders: 236 },
278
+ { channel: 'Club orders', orders: 148 },
279
+ { channel: 'Marketplace', orders: 87 },
280
+ { channel: 'Phone', orders: 41 },
281
+ ]
282
+
283
+ <ChartPanel<Slice>
284
+ title="Orders by channel"
285
+ summary="The web shop took 412 of 924 orders this month, nearly half; phone orders are 4%."
286
+ kind="donut"
287
+ data={CHANNELS}
288
+ xKey="channel"
289
+ xLabel="Channel"
290
+ series={[{ key: 'orders', label: 'Orders' }]}
291
+ centerLabel="Orders"
292
+ headingLevel={4}
293
+ />
294
+ ```
295
+
296
+ ### Radar, two series
297
+
298
+ ```tsx
299
+ type Profile = { measure: string; m4: number; ak: number }
300
+
301
+ const PROFILES: Profile[] = [
302
+ { measure: 'Range', m4: 78, ak: 64 },
303
+ { measure: 'Accuracy', m4: 82, ak: 70 },
304
+ { measure: 'Rate of fire', m4: 74, ak: 86 },
305
+ { measure: 'Weight', m4: 58, ak: 44 },
306
+ { measure: 'Reliability', m4: 70, ak: 90 },
307
+ { measure: 'Upgrades', m4: 92, ak: 62 },
308
+ ]
309
+
310
+ <ChartPanel<Profile>
311
+ title="Replica profiles"
312
+ summary="The M4 replica leads on range, accuracy and upgrades; the AK replica on rate of fire and reliability."
313
+ kind="radar"
314
+ data={PROFILES}
315
+ xKey="measure"
316
+ xLabel="Measure"
317
+ series={[
318
+ { key: 'm4', label: 'M4 replica' },
319
+ { key: 'ak', label: 'AK replica' },
320
+ ]}
321
+ headingLevel={4}
322
+ />
323
+ ```
324
+
325
+ ## Keyboard
326
+
327
+ | Keys | Result |
328
+ |---|---|
329
+ | `Tab` | Skips the chart, which isn't a tab stop (a place the Tab key lands), and goes to Show as table. |
330
+ | `Enter` `Space` | On Show as table, shows or hides the table. |
331
+
332
+ ## Do
333
+
334
+ - Write the summary as the finding. Don't just describe the axes.
335
+ - Use a line for change over time and bars to compare categories.
336
+ - Keep to two series. Give a third its own panel.
337
+ - In print or greyscale, you can rely on the second line series being dashed. It doesn't depend on colour alone.
338
+ - Use size sm with a headline for a stat card. The caption and the table stay.
339
+ - Use bands for context such as night hours or a sale, and give each one a label.
340
+ - Past six slices, fold a donut's smallest slices into one Other row.
341
+
342
+ ## Don't
343
+
344
+ - Don't give a chart two y-axes. Two measures means two panels.
345
+ - Don't colour a series with a status colour. Those colours mean state.
346
+ - Don't put a number on every point. The table has them all.
347
+ - Don't use a donut to compare categories over time. That's a job for a line or bars.
348
+
349
+ ## Anatomy
350
+
351
+ - **Title**: The panel's heading. It's also the accessible name, the name a screen reader announces for the panel.
352
+ - **Summary**: One sentence that works as the chart's caption: what a reader should take away.
353
+ - **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.
354
+ - **Headline**: Optional. The figure the chart is about, printed large under the title.
355
+ - **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.
356
+ - **Centre total**: Donut only. The sum of the slices, with centerLabel under it.
357
+ - **Shaded ranges**: Optional, line and bar only. A quiet wash behind the chart for each band, listed under it with its label and range.
358
+ - **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.