cortena-ui 1.3.0 → 1.4.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.
Files changed (280) hide show
  1. package/README.md +74 -3
  2. package/dist/a2ui/catalogue-doc.d.ts +44 -0
  3. package/dist/a2ui/catalogue-doc.js +169 -0
  4. package/dist/a2ui/catalogue-doc.js.map +1 -0
  5. package/dist/a2ui/catalogue.d.ts +44 -0
  6. package/dist/a2ui/catalogue.js +942 -0
  7. package/dist/a2ui/catalogue.js.map +1 -0
  8. package/dist/a2ui/index.d.ts +7 -0
  9. package/dist/a2ui/message.d.ts +232 -0
  10. package/dist/a2ui/message.js +365 -0
  11. package/dist/a2ui/message.js.map +1 -0
  12. package/dist/a2ui/renderer.d.ts +46 -0
  13. package/dist/a2ui/renderer.js +252 -0
  14. package/dist/a2ui/renderer.js.map +1 -0
  15. package/dist/a2ui/views.d.ts +80 -0
  16. package/dist/a2ui/views.js +596 -0
  17. package/dist/a2ui/views.js.map +1 -0
  18. package/dist/a2ui.d.ts +8 -0
  19. package/dist/a2ui.js +7 -0
  20. package/dist/chart.d.ts +9 -0
  21. package/dist/chart.js +9 -0
  22. package/dist/components/accordion.d.ts +22 -0
  23. package/dist/components/accordion.js +63 -0
  24. package/dist/components/accordion.js.map +1 -0
  25. package/dist/components/alert.d.ts +15 -0
  26. package/dist/components/alert.js +64 -0
  27. package/dist/components/alert.js.map +1 -0
  28. package/dist/components/avatar.d.ts +18 -0
  29. package/dist/components/avatar.js +40 -0
  30. package/dist/components/avatar.js.map +1 -0
  31. package/dist/components/badge.d.ts +12 -0
  32. package/dist/components/badge.js +47 -0
  33. package/dist/components/badge.js.map +1 -0
  34. package/dist/components/breadcrumb.d.ts +71 -0
  35. package/dist/components/breadcrumb.js +123 -0
  36. package/dist/components/breadcrumb.js.map +1 -0
  37. package/dist/components/button-link.d.ts +28 -0
  38. package/dist/components/button-link.js +24 -0
  39. package/dist/components/button-link.js.map +1 -0
  40. package/dist/components/button.d.ts +24 -0
  41. package/dist/components/button.js +65 -0
  42. package/dist/components/button.js.map +1 -0
  43. package/dist/components/calendar.d.ts +28 -0
  44. package/dist/components/calendar.js +123 -0
  45. package/dist/components/calendar.js.map +1 -0
  46. package/dist/components/card.d.ts +32 -0
  47. package/dist/components/card.js +57 -0
  48. package/dist/components/card.js.map +1 -0
  49. package/dist/components/chart/chart.d.ts +71 -0
  50. package/dist/components/chart/chart.js +167 -0
  51. package/dist/components/chart/chart.js.map +1 -0
  52. package/dist/components/chart/container.d.ts +88 -0
  53. package/dist/components/chart/container.js +189 -0
  54. package/dist/components/chart/container.js.map +1 -0
  55. package/dist/components/chart/data.js +128 -0
  56. package/dist/components/chart/data.js.map +1 -0
  57. package/dist/components/chart/index.d.ts +9 -0
  58. package/dist/components/chart/index.js +10 -0
  59. package/dist/components/chart/nivo-charts.js +225 -0
  60. package/dist/components/chart/nivo-charts.js.map +1 -0
  61. package/dist/components/chart/nivo-lazy.js +50 -0
  62. package/dist/components/chart/nivo-lazy.js.map +1 -0
  63. package/dist/components/chart/nivo-theme.d.ts +156 -0
  64. package/dist/components/chart/nivo-theme.js +161 -0
  65. package/dist/components/chart/nivo-theme.js.map +1 -0
  66. package/dist/components/chart/recharts-charts.js +276 -0
  67. package/dist/components/chart/recharts-charts.js.map +1 -0
  68. package/dist/components/chart/recharts-lazy.js +58 -0
  69. package/dist/components/chart/recharts-lazy.js.map +1 -0
  70. package/dist/components/chart/recharts-parts.d.ts +10 -0
  71. package/dist/components/chart/recharts-parts.js +25 -0
  72. package/dist/components/chart/recharts-parts.js.map +1 -0
  73. package/dist/components/chart/tokens.d.ts +62 -0
  74. package/dist/components/chart/tokens.js +192 -0
  75. package/dist/components/chart/tokens.js.map +1 -0
  76. package/dist/components/chart/types.d.ts +114 -0
  77. package/dist/components/chart/types.js +27 -0
  78. package/dist/components/chart/types.js.map +1 -0
  79. package/dist/components/checkbox.d.ts +20 -0
  80. package/dist/components/checkbox.js +24 -0
  81. package/dist/components/checkbox.js.map +1 -0
  82. package/dist/components/chip.d.ts +17 -0
  83. package/dist/components/chip.js +47 -0
  84. package/dist/components/chip.js.map +1 -0
  85. package/dist/components/combobox.d.ts +76 -0
  86. package/dist/components/combobox.js +201 -0
  87. package/dist/components/combobox.js.map +1 -0
  88. package/dist/components/command.d.ts +50 -0
  89. package/dist/components/command.js +117 -0
  90. package/dist/components/command.js.map +1 -0
  91. package/dist/components/data-table/data-table.d.ts +65 -0
  92. package/dist/components/data-table/data-table.js +588 -0
  93. package/dist/components/data-table/data-table.js.map +1 -0
  94. package/dist/components/data-table/export.d.ts +23 -0
  95. package/dist/components/data-table/export.js +95 -0
  96. package/dist/components/data-table/export.js.map +1 -0
  97. package/dist/components/data-table/features.d.ts +66 -0
  98. package/dist/components/data-table/features.js +103 -0
  99. package/dist/components/data-table/features.js.map +1 -0
  100. package/dist/components/data-table/index.d.ts +8 -0
  101. package/dist/components/data-table/parts.d.ts +79 -0
  102. package/dist/components/data-table/parts.js +400 -0
  103. package/dist/components/data-table/parts.js.map +1 -0
  104. package/dist/components/data-table/system-columns.js +84 -0
  105. package/dist/components/data-table/system-columns.js.map +1 -0
  106. package/dist/components/data-table/types.d.ts +172 -0
  107. package/dist/components/data-table/use-data-table.d.ts +19 -0
  108. package/dist/components/data-table/use-data-table.js +171 -0
  109. package/dist/components/data-table/use-data-table.js.map +1 -0
  110. package/dist/components/data-table/use-server-source.js +200 -0
  111. package/dist/components/data-table/use-server-source.js.map +1 -0
  112. package/dist/components/date-field.d.ts +54 -0
  113. package/dist/components/date-field.js +235 -0
  114. package/dist/components/date-field.js.map +1 -0
  115. package/dist/components/date-picker.d.ts +42 -0
  116. package/dist/components/date-picker.js +125 -0
  117. package/dist/components/date-picker.js.map +1 -0
  118. package/dist/components/dialog.d.ts +36 -0
  119. package/dist/components/dialog.js +97 -0
  120. package/dist/components/dialog.js.map +1 -0
  121. package/dist/components/dropdown-menu.d.ts +54 -0
  122. package/dist/components/dropdown-menu.js +191 -0
  123. package/dist/components/dropdown-menu.js.map +1 -0
  124. package/dist/components/dropzone.d.ts +48 -0
  125. package/dist/components/dropzone.js +185 -0
  126. package/dist/components/dropzone.js.map +1 -0
  127. package/dist/components/empty-state.d.ts +22 -0
  128. package/dist/components/empty-state.js +46 -0
  129. package/dist/components/empty-state.js.map +1 -0
  130. package/dist/components/error-banner.d.ts +17 -0
  131. package/dist/components/error-banner.js +52 -0
  132. package/dist/components/error-banner.js.map +1 -0
  133. package/dist/components/field.d.ts +64 -0
  134. package/dist/components/field.js +133 -0
  135. package/dist/components/field.js.map +1 -0
  136. package/dist/components/form.d.ts +37 -0
  137. package/dist/components/form.js +103 -0
  138. package/dist/components/form.js.map +1 -0
  139. package/dist/components/input.d.ts +17 -0
  140. package/dist/components/input.js +26 -0
  141. package/dist/components/input.js.map +1 -0
  142. package/dist/components/kbd.d.ts +11 -0
  143. package/dist/components/kbd.js +20 -0
  144. package/dist/components/kbd.js.map +1 -0
  145. package/dist/components/label.d.ts +14 -0
  146. package/dist/components/label.js +28 -0
  147. package/dist/components/label.js.map +1 -0
  148. package/dist/components/loading-skeleton.d.ts +22 -0
  149. package/dist/components/loading-skeleton.js +123 -0
  150. package/dist/components/loading-skeleton.js.map +1 -0
  151. package/dist/components/markdown.d.ts +44 -0
  152. package/dist/components/markdown.js +231 -0
  153. package/dist/components/markdown.js.map +1 -0
  154. package/dist/components/page-header.d.ts +26 -0
  155. package/dist/components/page-header.js +53 -0
  156. package/dist/components/page-header.js.map +1 -0
  157. package/dist/components/popover.d.ts +29 -0
  158. package/dist/components/popover.js +74 -0
  159. package/dist/components/popover.js.map +1 -0
  160. package/dist/components/progress.d.ts +22 -0
  161. package/dist/components/progress.js +49 -0
  162. package/dist/components/progress.js.map +1 -0
  163. package/dist/components/radio-group.d.ts +20 -0
  164. package/dist/components/radio-group.js +29 -0
  165. package/dist/components/radio-group.js.map +1 -0
  166. package/dist/components/scroll-area.d.ts +25 -0
  167. package/dist/components/scroll-area.js +39 -0
  168. package/dist/components/scroll-area.js.map +1 -0
  169. package/dist/components/section-card.d.ts +29 -0
  170. package/dist/components/section-card.js +55 -0
  171. package/dist/components/section-card.js.map +1 -0
  172. package/dist/components/segmented.d.ts +27 -0
  173. package/dist/components/segmented.js +32 -0
  174. package/dist/components/segmented.js.map +1 -0
  175. package/dist/components/select.d.ts +62 -0
  176. package/dist/components/select.js +97 -0
  177. package/dist/components/select.js.map +1 -0
  178. package/dist/components/separator.d.ts +19 -0
  179. package/dist/components/separator.js +23 -0
  180. package/dist/components/separator.js.map +1 -0
  181. package/dist/components/sheet.d.ts +35 -0
  182. package/dist/components/sheet.js +131 -0
  183. package/dist/components/sheet.js.map +1 -0
  184. package/dist/components/skeleton.d.ts +12 -0
  185. package/dist/components/skeleton.js +22 -0
  186. package/dist/components/skeleton.js.map +1 -0
  187. package/dist/components/sortable-list.d.ts +67 -0
  188. package/dist/components/sortable-list.js +206 -0
  189. package/dist/components/sortable-list.js.map +1 -0
  190. package/dist/components/spinner.d.ts +29 -0
  191. package/dist/components/spinner.js +34 -0
  192. package/dist/components/spinner.js.map +1 -0
  193. package/dist/components/status-dot.d.ts +26 -0
  194. package/dist/components/status-dot.js +47 -0
  195. package/dist/components/status-dot.js.map +1 -0
  196. package/dist/components/switch.d.ts +21 -0
  197. package/dist/components/switch.js +21 -0
  198. package/dist/components/switch.js.map +1 -0
  199. package/dist/components/tabs.d.ts +24 -0
  200. package/dist/components/tabs.js +60 -0
  201. package/dist/components/tabs.js.map +1 -0
  202. package/dist/components/textarea.d.ts +19 -0
  203. package/dist/components/textarea.js +47 -0
  204. package/dist/components/textarea.js.map +1 -0
  205. package/dist/components/toast.d.ts +47 -0
  206. package/dist/components/toast.js +106 -0
  207. package/dist/components/toast.js.map +1 -0
  208. package/dist/components/toolbar.d.ts +19 -0
  209. package/dist/components/toolbar.js +45 -0
  210. package/dist/components/toolbar.js.map +1 -0
  211. package/dist/components/tooltip.d.ts +28 -0
  212. package/dist/components/tooltip.js +59 -0
  213. package/dist/components/tooltip.js.map +1 -0
  214. package/dist/core.d.ts +44 -0
  215. package/dist/core.js +44 -0
  216. package/dist/data-table.d.ts +9 -0
  217. package/dist/data-table.js +7 -0
  218. package/dist/form.d.ts +7 -0
  219. package/dist/form.js +7 -0
  220. package/dist/hooks/use-cortena-theme.d.ts +98 -0
  221. package/dist/hooks/use-cortena-theme.js +145 -0
  222. package/dist/hooks/use-cortena-theme.js.map +1 -0
  223. package/dist/index.d.ts +75 -2678
  224. package/dist/index.js +66 -8598
  225. package/dist/lib/cn.d.ts +10 -0
  226. package/dist/lib/cn.js +15 -0
  227. package/dist/lib/cn.js.map +1 -0
  228. package/dist/lib/render.d.ts +23 -0
  229. package/dist/lib/render.js +20 -0
  230. package/dist/lib/render.js.map +1 -0
  231. package/dist/lib/shakeable.js +39 -0
  232. package/dist/lib/shakeable.js.map +1 -0
  233. package/dist/markdown.d.ts +3 -0
  234. package/dist/markdown.js +3 -0
  235. package/dist/sortable-list.d.ts +3 -0
  236. package/dist/sortable-list.js +3 -0
  237. package/package.json +37 -14
  238. package/src/a2ui/catalogue.ts +140 -103
  239. package/src/a2ui/views.tsx +5 -2
  240. package/src/components/alert.tsx +1 -1
  241. package/src/components/badge.tsx +1 -1
  242. package/src/components/button.tsx +1 -1
  243. package/src/components/chart/chart.tsx +72 -25
  244. package/src/components/chart/container.tsx +24 -39
  245. package/src/components/chart/index.tsx +4 -2
  246. package/src/components/chart/nivo-charts.tsx +41 -13
  247. package/src/components/chart/nivo-lazy.tsx +52 -0
  248. package/src/components/chart/recharts-charts.tsx +98 -3
  249. package/src/components/chart/recharts-lazy.tsx +72 -0
  250. package/src/components/chart/recharts-parts.tsx +34 -0
  251. package/src/components/chart/tokens.ts +1 -1
  252. package/src/components/chart/types.ts +14 -0
  253. package/src/components/chip.tsx +1 -1
  254. package/src/components/combobox.tsx +2 -2
  255. package/src/components/data-table/export.ts +18 -2
  256. package/src/components/data-table/features.ts +77 -45
  257. package/src/components/data-table/parts.tsx +15 -0
  258. package/src/components/data-table/use-data-table.ts +7 -2
  259. package/src/components/data-table/use-server-source.ts +17 -4
  260. package/src/components/date-field.tsx +1 -1
  261. package/src/components/error-banner.tsx +1 -1
  262. package/src/components/field.tsx +2 -2
  263. package/src/components/form.tsx +2 -2
  264. package/src/components/input.tsx +1 -1
  265. package/src/components/label.tsx +1 -1
  266. package/src/components/markdown.tsx +5 -4
  267. package/src/components/sheet.tsx +2 -2
  268. package/src/components/sortable-list.tsx +2 -2
  269. package/src/components/textarea.tsx +1 -1
  270. package/src/components/toast.tsx +1 -1
  271. package/src/entries/a2ui.ts +11 -0
  272. package/src/entries/chart.ts +10 -0
  273. package/src/entries/core.ts +91 -0
  274. package/src/entries/data-table.ts +9 -0
  275. package/src/entries/form.ts +24 -0
  276. package/src/entries/markdown.ts +9 -0
  277. package/src/entries/sortable-list.ts +13 -0
  278. package/src/index.ts +32 -98
  279. package/src/lib/shakeable.ts +33 -0
  280. package/dist/index.js.map +0 -1
@@ -4,12 +4,15 @@ import * as React from "react";
4
4
  import { cn } from "@/lib/cn";
5
5
  import { ChartContainer, ChartEmpty, ChartError, ChartLoading, type ChartContainerProps } from "./container";
6
6
  import { configFor, isEmptyData, isMatrix, isSeriesArray, toRows, toSlices } from "./data";
7
- import { NivoChart, type NivoType } from "./nivo-charts";
8
- import { RechartsChart, type RechartsType } from "./recharts-charts";
7
+ import type { NivoType } from "./nivo-charts";
8
+ import { NivoChartLazy, resetNivoChunks } from "./nivo-lazy";
9
+ import type { RechartsType } from "./recharts-charts";
10
+ import { RechartsChartLazy, resetRechartsChunks } from "./recharts-lazy";
9
11
  import {
10
12
  chartEngine,
11
13
  type ChartConfig,
12
14
  type ChartDataFor,
15
+ type ChartLabels,
13
16
  type ChartMatrix,
14
17
  type ChartRenderer,
15
18
  type ChartRow,
@@ -44,6 +47,12 @@ export interface ChartProps<T extends ChartType = ChartType>
44
47
  grid?: boolean;
45
48
  /** Show a tooltip on hover. Default true. */
46
49
  tooltip?: boolean;
50
+ /**
51
+ * Per-arc labels for `pie` and `donut`; ignored by every other type. Default
52
+ * `percent`, which is what the reader almost always wants and what cortenaweb's
53
+ * donuts showed before this package existed. See {@link ChartLabels}.
54
+ */
55
+ labels?: ChartLabels;
47
56
  /** Format values in axes, tooltips and labels. */
48
57
  valueFormatter?: (value: number) => string;
49
58
  /** Calendar range (ISO dates); defaults to the data's extent. */
@@ -82,25 +91,59 @@ function seriesKeys(type: ChartType, data: unknown, indexBy?: string, keys?: str
82
91
  }
83
92
  }
84
93
 
85
- class ChunkBoundary extends React.Component<
86
- { children: React.ReactNode; fallback: React.ReactNode },
87
- { failed: boolean }
88
- > {
89
- state = { failed: false };
94
+ interface ChunkBoundaryProps {
95
+ children: React.ReactNode;
96
+ /** Changing it clears a previous failure and renders the children again. */
97
+ resetKey: string;
98
+ fallback: (retry: () => void) => React.ReactNode;
99
+ }
100
+
101
+ /**
102
+ * Catches a Nivo chunk that failed to load. Recovering takes two things,
103
+ * because `React.lazy` memoises a rejection for the life of the page: fresh
104
+ * lazy components ({@link resetNivoChunks}) and a boundary that leaves its
105
+ * failed state. `resetKey` does the second on its own when the chart changes,
106
+ * and the fallback gets a `retry` that does both.
107
+ */
108
+ class ChunkBoundary extends React.Component<ChunkBoundaryProps, { failed: boolean; key: string }> {
109
+ state = { failed: false, key: this.props.resetKey };
90
110
  static getDerivedStateFromError() {
91
111
  return { failed: true };
92
112
  }
113
+ static getDerivedStateFromProps(props: ChunkBoundaryProps, state: { failed: boolean; key: string }) {
114
+ return props.resetKey === state.key ? null : { failed: false, key: props.resetKey };
115
+ }
116
+ retry = () => {
117
+ resetNivoChunks();
118
+ resetRechartsChunks();
119
+ this.setState({ failed: false });
120
+ };
93
121
  render() {
94
- return this.state.failed ? this.props.fallback : this.props.children;
122
+ return this.state.failed ? this.props.fallback(this.retry) : this.props.children;
95
123
  }
96
124
  }
97
125
 
126
+ /** Shown when an engine chunk could not be fetched, with a way back. */
127
+ function chunkFallback(retry: () => void): React.ReactNode {
128
+ return (
129
+ <ChartError>
130
+ Chart could not be loaded.{" "}
131
+ <button type="button" data-slot="chart-retry" onClick={retry} className="underline underline-offset-2">
132
+ Retry
133
+ </button>
134
+ </ChartError>
135
+ );
136
+ }
137
+
98
138
  /**
99
139
  * Chart — one component for every chart type the fleet draws.
100
140
  *
101
- * `type` picks the engine: Recharts (bundled) for bar, line, area, pie, donut,
102
- * scatter, radar and funnel; Nivo, lazy-loaded per package, for heatmap,
103
- * calendar, treemap and sunburst, with `renderer="canvas"` for large series.
141
+ * `type` picks the engine: Recharts for bar, line, area, pie, donut, scatter,
142
+ * radar and funnel; Nivo for heatmap, calendar, treemap and sunburst, with
143
+ * `renderer="canvas"` for large series. Both are loaded on the first render of
144
+ * a chart that needs them — Recharts as one chunk, each `@nivo/*` package as
145
+ * its own — so importing `Chart` costs nothing until a chart is drawn, and a
146
+ * screen that draws a bar chart never fetches Nivo.
104
147
  * Colours, fonts, axes, grid, tooltip, legend and the empty, loading and
105
148
  * error states all come from cortena-design tokens; a consumer passes data
106
149
  * and, optionally, a `config` of labels.
@@ -125,6 +168,7 @@ function Chart<T extends ChartType>({
125
168
  legend,
126
169
  grid = true,
127
170
  tooltip = true,
171
+ labels = "percent",
128
172
  valueFormatter,
129
173
  from,
130
174
  to,
@@ -151,9 +195,9 @@ function Chart<T extends ChartType>({
151
195
  else if (empty) body = <ChartEmpty>{emptyMessage}</ChartEmpty>;
152
196
  else if (engine === "nivo") {
153
197
  body = (
154
- <ChunkBoundary fallback={<ChartError>Chart could not be loaded</ChartError>}>
198
+ <ChunkBoundary resetKey={`${type}|${renderer}`} fallback={chunkFallback}>
155
199
  <React.Suspense fallback={<ChartLoading />}>
156
- <NivoChart
200
+ <NivoChartLazy
157
201
  type={type as NivoType}
158
202
  data={data}
159
203
  config={fullConfig}
@@ -170,18 +214,21 @@ function Chart<T extends ChartType>({
170
214
  } else {
171
215
  responsive = true;
172
216
  body = (
173
- <RechartsChart
174
- type={type as RechartsType}
175
- data={data}
176
- config={fullConfig}
177
- indexBy={indexBy}
178
- keys={keys}
179
- stacked={stacked}
180
- legend={showLegend}
181
- grid={grid}
182
- tooltip={tooltip}
183
- valueFormatter={valueFormatter}
184
- />
217
+ <ChunkBoundary resetKey={type} fallback={chunkFallback}>
218
+ <RechartsChartLazy
219
+ type={type as RechartsType}
220
+ data={data}
221
+ config={fullConfig}
222
+ indexBy={indexBy}
223
+ keys={keys}
224
+ stacked={stacked}
225
+ legend={showLegend}
226
+ grid={grid}
227
+ tooltip={tooltip}
228
+ labels={labels}
229
+ valueFormatter={valueFormatter}
230
+ />
231
+ </ChunkBoundary>
185
232
  );
186
233
  }
187
234
 
@@ -1,15 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import {
5
- Legend as RechartsLegend,
6
- ResponsiveContainer,
7
- Tooltip as RechartsTooltip,
8
- type LegendPayload,
9
- type LegendProps,
10
- type TooltipContentProps,
11
- type TooltipProps,
12
- } from "recharts";
4
+ // Types only. The two Recharts *values* this file used to reach for — `Tooltip`
5
+ // and `Legend` — are in `recharts-parts.tsx`, so that nothing on the eager path
6
+ // imports the engine; see the note there.
7
+ import type { LegendPayload, TooltipContentProps } from "recharts";
8
+ import { RechartsResponsive } from "./recharts-lazy";
13
9
  import { cn } from "@/lib/cn";
14
10
  import { Skeleton } from "@/components/skeleton";
15
11
  import { vizVar } from "./tokens";
@@ -23,7 +19,7 @@ interface ChartContextValue {
23
19
  element: HTMLDivElement | null;
24
20
  }
25
21
 
26
- const ChartContext = React.createContext<ChartContextValue | null>(null);
22
+ const ChartContext = /* @__PURE__ */ React.createContext<ChartContextValue | null>(null);
27
23
 
28
24
  /** The nearest ChartContainer's config and element; null outside one. */
29
25
  export function useChart(): ChartContextValue | null {
@@ -143,14 +139,23 @@ function ChartContainer({
143
139
  {...props}
144
140
  >
145
141
  {responsive ? (
146
- <ResponsiveContainer
147
- width="100%"
148
- height="100%"
149
- minWidth={testMode ? TEST_WIDTH : undefined}
150
- minHeight={testMode ? TEST_HEIGHT : undefined}
151
- >
152
- {children as React.ReactElement}
153
- </ResponsiveContainer>
142
+ // Recharts' ResponsiveContainer arrives with the engine chunk, so
143
+ // this suspends on the first chart drawn on the page — and so does
144
+ // whatever it wraps, which is why the skeleton lives here rather than
145
+ // in `Chart`: this is the innermost boundary, so it is the one that
146
+ // catches. A `ChartContainer` used directly, to compose a one-off
147
+ // Recharts chart, gets the same treatment and needs no boundary of
148
+ // its own.
149
+ <React.Suspense fallback={<ChartLoading />}>
150
+ <RechartsResponsive
151
+ width="100%"
152
+ height="100%"
153
+ minWidth={testMode ? TEST_WIDTH : undefined}
154
+ minHeight={testMode ? TEST_HEIGHT : undefined}
155
+ >
156
+ {children as React.ReactElement}
157
+ </RechartsResponsive>
158
+ </React.Suspense>
154
159
  ) : (
155
160
  children
156
161
  )}
@@ -286,11 +291,6 @@ function ChartTooltipContent({
286
291
  );
287
292
  }
288
293
 
289
- /** Recharts' Tooltip with the token-styled content and a subtle cursor. */
290
- function ChartTooltip(props: TooltipProps) {
291
- return <RechartsTooltip content={<ChartTooltipContent />} {...props} />;
292
- }
293
-
294
294
  /* ── legend ──────────────────────────────────────────────────────────────── */
295
295
 
296
296
  export interface ChartLegendContentProps {
@@ -337,19 +337,4 @@ function ChartLegendContent({ payload, verticalAlign = "bottom", className, hide
337
337
  );
338
338
  }
339
339
 
340
- /** Recharts' Legend with the token-styled content. */
341
- function ChartLegend(props: LegendProps) {
342
- return <RechartsLegend content={<ChartLegendContent />} {...props} />;
343
- }
344
-
345
- export {
346
- ChartContainer,
347
- ChartContext,
348
- ChartEmpty,
349
- ChartError,
350
- ChartLoading,
351
- ChartTooltip,
352
- ChartTooltipContent,
353
- ChartLegend,
354
- ChartLegendContent,
355
- };
340
+ export { ChartContainer, ChartContext, ChartEmpty, ChartError, ChartLoading, ChartTooltipContent, ChartLegendContent };
@@ -14,14 +14,15 @@ export {
14
14
  ChartContainer,
15
15
  ChartEmpty,
16
16
  ChartError,
17
- ChartLegend,
18
17
  ChartLegendContent,
19
18
  ChartLoading,
20
- ChartTooltip,
21
19
  ChartTooltipContent,
22
20
  seriesColor,
23
21
  useChart,
24
22
  } from "./container";
23
+ // Recharts elements, so importing either is what makes an app pay for the
24
+ // engine; see the note in recharts-parts.tsx.
25
+ export { ChartLegend, ChartTooltip } from "./recharts-parts";
25
26
  export type { ChartContainerProps, ChartLegendContentProps, ChartTooltipContentProps } from "./container";
26
27
  export { nivoTheme, useNivoTheme } from "./nivo-theme";
27
28
  export type { NivoTheme } from "./nivo-theme";
@@ -41,6 +42,7 @@ export type {
41
42
  ChartDataFor,
42
43
  ChartDay,
43
44
  ChartEngine,
45
+ ChartLabels,
44
46
  ChartMatrix,
45
47
  ChartPoint,
46
48
  ChartRenderer,
@@ -18,16 +18,42 @@ export type NivoType = Extract<ChartType, "heatmap" | "calendar" | "treemap" | "
18
18
  */
19
19
  type AnyComponent = React.ComponentType<Record<string, unknown>>;
20
20
 
21
- const lazy = (load: () => Promise<AnyComponent>) =>
22
- React.lazy(async () => ({ default: await load() }));
21
+ const loaders = {
22
+ "heatmap-svg": async () => (await import("@nivo/heatmap")).ResponsiveHeatMap as AnyComponent,
23
+ "heatmap-canvas": async () => (await import("@nivo/heatmap")).ResponsiveHeatMapCanvas as AnyComponent,
24
+ "calendar-svg": async () => (await import("@nivo/calendar")).ResponsiveCalendar as unknown as AnyComponent,
25
+ "calendar-canvas": async () =>
26
+ (await import("@nivo/calendar")).ResponsiveCalendarCanvas as unknown as AnyComponent,
27
+ "treemap-svg": async () => (await import("@nivo/treemap")).ResponsiveTreeMap as AnyComponent,
28
+ "treemap-canvas": async () => (await import("@nivo/treemap")).ResponsiveTreeMapCanvas as AnyComponent,
29
+ "sunburst-svg": async () => (await import("@nivo/sunburst")).ResponsiveSunburst as AnyComponent,
30
+ } satisfies Record<string, () => Promise<AnyComponent>>;
23
31
 
24
- const HeatMapSvg = lazy(async () => (await import("@nivo/heatmap")).ResponsiveHeatMap as AnyComponent);
25
- const HeatMapCanvas = lazy(async () => (await import("@nivo/heatmap")).ResponsiveHeatMapCanvas as AnyComponent);
26
- const CalendarSvg = lazy(async () => (await import("@nivo/calendar")).ResponsiveCalendar as unknown as AnyComponent);
27
- const CalendarCanvas = lazy(async () => (await import("@nivo/calendar")).ResponsiveCalendarCanvas as unknown as AnyComponent);
28
- const TreeMapSvg = lazy(async () => (await import("@nivo/treemap")).ResponsiveTreeMap as AnyComponent);
29
- const TreeMapCanvas = lazy(async () => (await import("@nivo/treemap")).ResponsiveTreeMapCanvas as AnyComponent);
30
- const SunburstSvg = lazy(async () => (await import("@nivo/sunburst")).ResponsiveSunburst as AnyComponent);
32
+ type ChunkName = keyof typeof loaders;
33
+
34
+ /*
35
+ * `React.lazy` memoises the outcome of its loader, a rejection included: a
36
+ * chunk that fails once — an offline moment, a deploy that moved the file —
37
+ * throws the same error for the life of the page, and no re-render retries it.
38
+ * The lazy components are therefore cached here rather than frozen at module
39
+ * scope, and `resetNivoChunks` drops the cache so the next render builds fresh
40
+ * ones and asks for the chunk again. `Chart`'s ChunkBoundary calls it.
41
+ */
42
+ let chunks = new Map<ChunkName, AnyComponent>();
43
+
44
+ function chunk(name: ChunkName): AnyComponent {
45
+ let component = chunks.get(name);
46
+ if (!component) {
47
+ component = React.lazy(async () => ({ default: await loaders[name]() })) as unknown as AnyComponent;
48
+ chunks.set(name, component);
49
+ }
50
+ return component;
51
+ }
52
+
53
+ /** Forget every loaded-or-failed Nivo chunk, so the next render loads it again. */
54
+ export function resetNivoChunks(): void {
55
+ chunks = new Map();
56
+ }
31
57
 
32
58
  export interface NivoRenderProps {
33
59
  type: NivoType;
@@ -93,7 +119,7 @@ export function NivoChart({ type, data, config, renderer, scale, legend, valueFo
93
119
 
94
120
  switch (type) {
95
121
  case "heatmap": {
96
- const Component = canvas ? HeatMapCanvas : HeatMapSvg;
122
+ const Component = chunk(canvas ? "heatmap-canvas" : "heatmap-svg");
97
123
  // Either shape lands on Nivo's `[{ id, data: [{ x, y }] }]`.
98
124
  const rows = isMatrix(data) ? matrixToSeries(data as ChartMatrix) : (data as ChartSeries[]);
99
125
  // Labels only when cells are big enough to carry them.
@@ -139,7 +165,7 @@ export function NivoChart({ type, data, config, renderer, scale, legend, valueFo
139
165
  }
140
166
 
141
167
  case "calendar": {
142
- const Component = canvas ? CalendarCanvas : CalendarSvg;
168
+ const Component = chunk(canvas ? "calendar-canvas" : "calendar-svg");
143
169
  const days = data as ChartDay[];
144
170
  const range = calendarRange(days, from, to);
145
171
  return (
@@ -177,7 +203,7 @@ export function NivoChart({ type, data, config, renderer, scale, legend, valueFo
177
203
  }
178
204
 
179
205
  case "treemap": {
180
- const Component = canvas ? TreeMapCanvas : TreeMapSvg;
206
+ const Component = chunk(canvas ? "treemap-canvas" : "treemap-svg");
181
207
  const tree = data as ChartTree;
182
208
  return (
183
209
  <Component
@@ -202,9 +228,11 @@ export function NivoChart({ type, data, config, renderer, scale, legend, valueFo
202
228
  }
203
229
 
204
230
  case "sunburst": {
231
+ // Sunburst has no canvas renderer; SVG either way.
232
+ const Component = chunk("sunburst-svg");
205
233
  const tree = data as ChartTree;
206
234
  return (
207
- <SunburstSvg
235
+ <Component
208
236
  data={tree}
209
237
  id={treeIdentity(tree)}
210
238
  value="value"
@@ -0,0 +1,52 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Nivo, loaded on first render of a Nivo chart type.
5
+ *
6
+ * `nivo-charts.tsx` already loads each `@nivo/*` engine dynamically, but it was
7
+ * itself a static import, and it pulls in `nivo-theme.ts` — which reads
8
+ * `@nivo/core`, `@nivo/legends`, `@nivo/theming` and friends for real, about
9
+ * 63 kB of them. So a bundle that could reach `Chart` shipped Nivo's theming
10
+ * layer whether or not it ever drew a Nivo chart. One more dynamic boundary
11
+ * here puts the theme on the same footing as the engines it themes.
12
+ *
13
+ * Two levels of laziness, deliberately: this chunk, then the per-package
14
+ * loaders inside it, so a screen that draws a heatmap fetches the heatmap
15
+ * package and not the calendar, treemap and sunburst ones as well.
16
+ */
17
+
18
+ import * as React from "react";
19
+ import type { NivoRenderProps } from "./nivo-charts";
20
+
21
+ type AnyComponent = React.ComponentType<Record<string, unknown>>;
22
+
23
+ /**
24
+ * The loaded module, kept so {@link resetNivoChunks} can reach the inner
25
+ * per-package cache. Before the chunk loads there is nothing inside it to
26
+ * reset, and resetting the outer lazy component is enough to make the next
27
+ * render ask for it again.
28
+ */
29
+ let loaded: typeof import("./nivo-charts") | null = null;
30
+ let outer: AnyComponent | null = null;
31
+
32
+ function chunk(): AnyComponent {
33
+ outer ??= React.lazy(async () => {
34
+ const module = await import("./nivo-charts");
35
+ loaded = module;
36
+ return { default: module.NivoChart as unknown as React.ComponentType };
37
+ }) as unknown as AnyComponent;
38
+ return outer;
39
+ }
40
+
41
+ /** Forget every loaded-or-failed Nivo chunk, outer and per-package, so the next render loads it again. */
42
+ export function resetNivoChunks(): void {
43
+ outer = null;
44
+ loaded?.resetNivoChunks();
45
+ loaded = null;
46
+ }
47
+
48
+ /** `NivoChart` behind its chunk. Suspends on first render of a Nivo type. */
49
+ export function NivoChartLazy(props: NivoRenderProps): React.ReactElement {
50
+ const Component = chunk();
51
+ return <Component {...(props as unknown as Record<string, unknown>)} />;
52
+ }
@@ -24,9 +24,11 @@ import {
24
24
  XAxis,
25
25
  YAxis,
26
26
  } from "recharts";
27
- import { ChartLegend, ChartTooltip, seriesColor } from "./container";
27
+ import { seriesColor, useChart } from "./container";
28
+ import { ChartLegend, ChartTooltip } from "./recharts-parts";
28
29
  import { toRows, toSlices } from "./data";
29
- import type { ChartConfig, ChartRow, ChartSeries, ChartSlice, ChartType } from "./types";
30
+ import { contrastLabel, useThemeVersion } from "./tokens";
31
+ import type { ChartConfig, ChartLabels, ChartRow, ChartSeries, ChartSlice, ChartType } from "./types";
30
32
 
31
33
  export type RechartsType = Extract<
32
34
  ChartType,
@@ -43,11 +45,91 @@ export interface RechartsRenderProps {
43
45
  legend: boolean;
44
46
  grid: boolean;
45
47
  tooltip: boolean;
48
+ /** Per-arc labels for pie and donut. Ignored by every other type. */
49
+ labels: ChartLabels;
46
50
  valueFormatter?: (value: number) => string;
47
51
  }
48
52
 
49
53
  const MARGIN = { top: 8, right: 8, bottom: 0, left: 0 };
50
54
 
55
+ /**
56
+ * Where an arc label sits, as a fraction of the way from the inner radius to
57
+ * the outer one. Just inside the middle of the band: the arc is widest towards
58
+ * its outer edge, so a label pushed slightly out has more room than a centred
59
+ * one, without ever touching the rim.
60
+ */
61
+ const ARC_LABEL_RADIUS = 0.58;
62
+
63
+ /**
64
+ * Arcs smaller than this share of the whole are left unlabelled.
65
+ *
66
+ * Below roughly a twentieth the arc is narrower than the text it would carry,
67
+ * so the label either overflows into its neighbours or has to be dragged
68
+ * outside on a leader line. Leader lines on a small chart are worse than no
69
+ * label — they collide with each other and with the legend, and the reader
70
+ * still has to trace them. The tooltip covers the slices that fall out.
71
+ */
72
+ const ARC_LABEL_MIN_FRACTION = 0.05;
73
+
74
+ /**
75
+ * What Recharts hands a `<Pie label={…}>` renderer. Every geometry field is
76
+ * optional on its side — the same callback types the label of a chart that has
77
+ * not measured itself yet — so each is checked before it is used rather than
78
+ * asserted away.
79
+ */
80
+ interface ArcLabelProps {
81
+ cx?: number;
82
+ cy?: number;
83
+ midAngle?: number;
84
+ innerRadius?: number;
85
+ outerRadius?: number;
86
+ percent?: number;
87
+ value?: number;
88
+ index?: number;
89
+ }
90
+
91
+ /**
92
+ * A per-arc label, drawn inside the arc it belongs to.
93
+ *
94
+ * Inside rather than outside so there is nothing to collide: each label is
95
+ * bounded by its own sector, and a sector too thin to hold one simply does not
96
+ * get one. The text colour comes from `contrastLabel`, the same measured
97
+ * ink-or-white choice the Nivo charts make, so a label stays legible on a pale
98
+ * series colour and on a dark one, in either theme.
99
+ */
100
+ function arcLabel(
101
+ labels: Exclude<ChartLabels, "none">,
102
+ colourOn: (fill: string) => string,
103
+ fillOf: (index: number) => string,
104
+ valueFormatter?: (value: number) => string,
105
+ ) {
106
+ return function ArcLabel({ cx, cy, midAngle, innerRadius, outerRadius, percent, value, index }: ArcLabelProps) {
107
+ if (cx == null || cy == null || midAngle == null || innerRadius == null || outerRadius == null) return null;
108
+ if (!(percent != null && percent >= ARC_LABEL_MIN_FRACTION)) return null;
109
+ const radius = innerRadius + (outerRadius - innerRadius) * ARC_LABEL_RADIUS;
110
+ // Recharts measures angles anticlockwise from the positive x-axis, SVG's y
111
+ // grows downward, so the sine is negated to land on the drawn arc.
112
+ const radians = (-midAngle * Math.PI) / 180;
113
+ const text =
114
+ labels === "percent"
115
+ ? `${Math.round(percent * 100)}%`
116
+ : (valueFormatter?.(Number(value)) ?? String(value ?? ""));
117
+ return (
118
+ <text
119
+ data-slot="chart-arc-label"
120
+ x={cx + radius * Math.cos(radians)}
121
+ y={cy + radius * Math.sin(radians)}
122
+ fill={colourOn(fillOf(index ?? 0))}
123
+ textAnchor="middle"
124
+ dominantBaseline="central"
125
+ className="pointer-events-none text-[length:var(--ds-text-caption)] font-medium tabular-nums"
126
+ >
127
+ {text}
128
+ </text>
129
+ );
130
+ };
131
+ }
132
+
51
133
  /**
52
134
  * The Recharts side of `Chart`. Returns a chart element for the
53
135
  * ResponsiveContainer inside ChartContainer; colours come from `seriesColor`
@@ -64,8 +146,17 @@ export function RechartsChart({
64
146
  legend,
65
147
  grid,
66
148
  tooltip,
149
+ labels,
67
150
  valueFormatter,
68
151
  }: RechartsRenderProps): React.ReactElement {
152
+ // Arc labels sit on top of a series colour, so their ink depends on which
153
+ // colour and on the theme. `useThemeVersion` is the cache key for that DOM
154
+ // read; `element` scopes it to a nested theme rather than the document.
155
+ const themeVersion = useThemeVersion();
156
+ const element = useChart()?.element ?? null;
157
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- themeVersion is the cache key for the DOM read
158
+ const colourOn = React.useMemo(() => contrastLabel(element), [element, themeVersion]);
159
+
69
160
  const tip = tooltip ? (
70
161
  <ChartTooltip
71
162
  formatter={valueFormatter ? (value: unknown) => valueFormatter(Number(value)) : undefined}
@@ -152,6 +243,7 @@ export function RechartsChart({
152
243
  case "pie":
153
244
  case "donut": {
154
245
  const slices = toSlices(data as ChartSlice[]);
246
+ const fillOf = (i: number) => seriesColor(config, slices[i]?.id ?? String(i), i);
155
247
  return (
156
248
  <PieChart margin={MARGIN}>
157
249
  {tip}
@@ -164,9 +256,12 @@ export function RechartsChart({
164
256
  outerRadius="80%"
165
257
  paddingAngle={type === "donut" ? 2 : 0}
166
258
  strokeWidth={1}
259
+ label={labels === "none" ? undefined : arcLabel(labels, colourOn, fillOf, valueFormatter)}
260
+ // The labels are inside their arcs, so there is nothing to lead to.
261
+ labelLine={false}
167
262
  >
168
263
  {slices.map((slice, i) => (
169
- <Cell key={slice.id} fill={seriesColor(config, slice.id, i)} />
264
+ <Cell key={slice.id} fill={fillOf(i)} />
170
265
  ))}
171
266
  </Pie>
172
267
  </PieChart>
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Recharts, loaded on first render of a chart.
5
+ *
6
+ * Recharts is 774 kB of the package's dependency weight — more than everything
7
+ * else in `cortena-ui` combined — and until this existed it was in the static
8
+ * graph of anything that could reach `Chart`, the A2UI catalogue included. An
9
+ * app that imported the catalogue to render a text card paid for a charting
10
+ * engine it never drew with.
11
+ *
12
+ * Both engines are now on the same footing: `@nivo/*` in `nivo-charts.tsx` and
13
+ * Recharts here are dynamic imports, so a chart type costs its engine when it
14
+ * is first drawn and nothing before. `ChartContainer`'s responsive wrapper is
15
+ * here for the same reason — it is Recharts' `ResponsiveContainer`, and
16
+ * `ChartContainer` is on the eager path because `Chart` renders it before it
17
+ * knows which engine will draw.
18
+ *
19
+ * The chunk cache repeats the shape in `nivo-charts.tsx` for the same reason:
20
+ * `React.lazy` memoises a rejection for the life of the page, so the lazy
21
+ * components are cached here rather than frozen at module scope and
22
+ * {@link resetRechartsChunks} drops them, letting `Chart`'s ChunkBoundary
23
+ * retry a chunk that failed once.
24
+ */
25
+
26
+ import * as React from "react";
27
+ import type { RechartsRenderProps } from "./recharts-charts";
28
+
29
+ type AnyComponent = React.ComponentType<Record<string, unknown>>;
30
+
31
+ const loaders = {
32
+ chart: async () => (await import("./recharts-charts")).RechartsChart as unknown as AnyComponent,
33
+ responsive: async () => (await import("recharts")).ResponsiveContainer as unknown as AnyComponent,
34
+ } satisfies Record<string, () => Promise<AnyComponent>>;
35
+
36
+ type ChunkName = keyof typeof loaders;
37
+
38
+ let chunks = new Map<ChunkName, AnyComponent>();
39
+
40
+ function chunk(name: ChunkName): AnyComponent {
41
+ let component = chunks.get(name);
42
+ if (!component) {
43
+ component = React.lazy(async () => ({ default: await loaders[name]() })) as unknown as AnyComponent;
44
+ chunks.set(name, component);
45
+ }
46
+ return component;
47
+ }
48
+
49
+ /** Forget every loaded-or-failed Recharts chunk, so the next render loads it again. */
50
+ export function resetRechartsChunks(): void {
51
+ chunks = new Map();
52
+ }
53
+
54
+ /** `RechartsChart` behind its chunk. Suspends on first render of a Recharts type. */
55
+ export function RechartsChartLazy(props: RechartsRenderProps): React.ReactElement {
56
+ const Component = chunk("chart");
57
+ return <Component {...(props as unknown as Record<string, unknown>)} />;
58
+ }
59
+
60
+ export interface RechartsResponsiveProps {
61
+ width: string | number;
62
+ height: string | number;
63
+ minWidth?: number;
64
+ minHeight?: number;
65
+ children: React.ReactElement;
66
+ }
67
+
68
+ /** Recharts' `ResponsiveContainer` behind its chunk. */
69
+ export function RechartsResponsive(props: RechartsResponsiveProps): React.ReactElement {
70
+ const Component = chunk("responsive");
71
+ return <Component {...(props as unknown as Record<string, unknown>)} />;
72
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ /**
4
+ * The two parts of the chart surface that are Recharts elements rather than
5
+ * plain markup: its `Tooltip` and its `Legend`, each given the token-styled
6
+ * content from `container.tsx`.
7
+ *
8
+ * They live here and not in `container.tsx` for one reason. `container.tsx` is
9
+ * on the eager path — `Chart` renders `ChartContainer` before it knows which
10
+ * engine will draw — and `recharts-charts.tsx`, the lazy engine chunk, needs
11
+ * both of these. A binding the lazy chunk imports from an eager module is
12
+ * pulled into the eager chunk, and these two carry `recharts` with them: with
13
+ * them in `container.tsx`, importing `Chart` shipped all 774 kB of Recharts up
14
+ * front however lazy the rest of the chunking was.
15
+ *
16
+ * With them here, the only static importer is the lazy chunk, so they travel
17
+ * with it. `index.tsx` still re-exports them, so a consumer composing a one-off
18
+ * chart imports them from `cortena-ui/chart` exactly as before — and pays for
19
+ * Recharts, which is the point of asking for them.
20
+ */
21
+
22
+ import * as React from "react";
23
+ import { Legend as RechartsLegend, Tooltip as RechartsTooltip, type LegendProps, type TooltipProps } from "recharts";
24
+ import { ChartLegendContent, ChartTooltipContent } from "./container";
25
+
26
+ /** Recharts' Tooltip with the token-styled content and a subtle cursor. */
27
+ export function ChartTooltip(props: TooltipProps): React.ReactElement {
28
+ return <RechartsTooltip content={<ChartTooltipContent />} {...props} />;
29
+ }
30
+
31
+ /** Recharts' Legend with the token-styled content. */
32
+ export function ChartLegend(props: LegendProps): React.ReactElement {
33
+ return <RechartsLegend content={<ChartLegendContent />} {...props} />;
34
+ }