banhaten 0.3.0 → 0.5.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 (348) hide show
  1. package/CHANGELOG.md +139 -73
  2. package/CONTRIBUTING.md +46 -44
  3. package/README.md +192 -190
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -0
  7. package/docs/design-system/README.md +22 -20
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +90 -76
  20. package/docs/design-system/components/accordion.md +53 -53
  21. package/docs/design-system/components/activity-feed.md +94 -94
  22. package/docs/design-system/components/activity-heatmap.md +57 -57
  23. package/docs/design-system/components/agent-input.md +85 -0
  24. package/docs/design-system/components/alert-dialog.md +72 -72
  25. package/docs/design-system/components/alert.md +98 -98
  26. package/docs/design-system/components/approval-card.md +134 -0
  27. package/docs/design-system/components/aspect-ratio.md +45 -45
  28. package/docs/design-system/components/attribute.md +89 -89
  29. package/docs/design-system/components/autocomplete.md +75 -75
  30. package/docs/design-system/components/avatar.md +54 -54
  31. package/docs/design-system/components/badge.md +54 -54
  32. package/docs/design-system/components/banner.md +87 -87
  33. package/docs/design-system/components/breadcrumbs.md +177 -176
  34. package/docs/design-system/components/button-group.md +98 -98
  35. package/docs/design-system/components/button.md +83 -80
  36. package/docs/design-system/components/card.md +88 -88
  37. package/docs/design-system/components/carousel.md +46 -46
  38. package/docs/design-system/components/catalog-components.md +47 -47
  39. package/docs/design-system/components/chart.md +95 -24
  40. package/docs/design-system/components/checkbox.md +54 -54
  41. package/docs/design-system/components/collapsible.md +67 -53
  42. package/docs/design-system/components/command.md +62 -62
  43. package/docs/design-system/components/context-menu.md +46 -46
  44. package/docs/design-system/components/date-picker.md +81 -79
  45. package/docs/design-system/components/divider.md +102 -102
  46. package/docs/design-system/components/empty-state.md +56 -56
  47. package/docs/design-system/components/field.md +70 -70
  48. package/docs/design-system/components/file-upload.md +187 -187
  49. package/docs/design-system/components/hover-card.md +48 -48
  50. package/docs/design-system/components/icons.md +48 -48
  51. package/docs/design-system/components/inline-citations.md +93 -0
  52. package/docs/design-system/components/input-group.md +58 -58
  53. package/docs/design-system/components/input-otp.md +56 -56
  54. package/docs/design-system/components/input.md +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -49
  57. package/docs/design-system/components/label.md +49 -49
  58. package/docs/design-system/components/menu.md +71 -61
  59. package/docs/design-system/components/menubar.md +47 -47
  60. package/docs/design-system/components/modal.md +105 -105
  61. package/docs/design-system/components/native-select.md +54 -54
  62. package/docs/design-system/components/navigation-menu.md +50 -50
  63. package/docs/design-system/components/onboarding-step-list-item.md +82 -82
  64. package/docs/design-system/components/page-header.md +93 -86
  65. package/docs/design-system/components/pagination.md +57 -57
  66. package/docs/design-system/components/popover.md +60 -60
  67. package/docs/design-system/components/progress-slider.md +50 -50
  68. package/docs/design-system/components/progress.md +76 -76
  69. package/docs/design-system/components/radio-card.md +51 -51
  70. package/docs/design-system/components/radio-group.md +57 -57
  71. package/docs/design-system/components/resizable.md +44 -44
  72. package/docs/design-system/components/rolling-count.md +61 -0
  73. package/docs/design-system/components/scroll-area.md +51 -51
  74. package/docs/design-system/components/segmented-control.md +54 -54
  75. package/docs/design-system/components/select.md +68 -62
  76. package/docs/design-system/components/sheet.md +67 -67
  77. package/docs/design-system/components/shimmer-text.md +67 -0
  78. package/docs/design-system/components/sidebar.md +127 -76
  79. package/docs/design-system/components/skeleton.md +75 -75
  80. package/docs/design-system/components/slideout.md +65 -65
  81. package/docs/design-system/components/slider.md +63 -63
  82. package/docs/design-system/components/social-button.md +50 -50
  83. package/docs/design-system/components/spinner.md +62 -62
  84. package/docs/design-system/components/steps.md +65 -65
  85. package/docs/design-system/components/streaming-text.md +82 -0
  86. package/docs/design-system/components/table.md +419 -414
  87. package/docs/design-system/components/tabs.md +59 -55
  88. package/docs/design-system/components/tag.md +80 -80
  89. package/docs/design-system/components/task-list.md +95 -0
  90. package/docs/design-system/components/textarea.md +50 -50
  91. package/docs/design-system/components/thinking-reasoning.md +72 -0
  92. package/docs/design-system/components/thinking-state.md +67 -0
  93. package/docs/design-system/components/timeline.md +83 -83
  94. package/docs/design-system/components/toast.md +58 -58
  95. package/docs/design-system/components/toggle.md +81 -81
  96. package/docs/design-system/components/toolbar.md +92 -87
  97. package/docs/design-system/components/tooltip.md +94 -92
  98. package/docs/design-system/components/typography.md +18 -18
  99. package/docs/design-system/density-and-elevation.md +65 -61
  100. package/docs/design-system/design-system-test-missing-items.md +368 -368
  101. package/docs/design-system/direction-and-localization.md +23 -23
  102. package/docs/design-system/form-recipes.md +73 -73
  103. package/docs/design-system/gap-promotion-policy.md +21 -21
  104. package/docs/design-system/icons.md +74 -69
  105. package/docs/design-system/installation.md +70 -68
  106. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  107. package/docs/design-system/registry-and-cli.md +64 -64
  108. package/docs/design-system/release-policy.md +30 -30
  109. package/docs/design-system/roadmap.md +14 -14
  110. package/docs/design-system/support-matrix.md +23 -23
  111. package/docs/design-system/tabs.md +54 -53
  112. package/docs/design-system/template-gap-audit.md +32 -0
  113. package/docs/design-system/templates.md +59 -0
  114. package/docs/design-system/token-governance.md +72 -60
  115. package/package.json +98 -98
  116. package/registry/component-contract.json +40 -40
  117. package/registry/components/accordion.tsx +155 -155
  118. package/registry/components/agent-input-parts.tsx +552 -0
  119. package/registry/components/agent-input-state.ts +156 -0
  120. package/registry/components/agent-input.css +307 -0
  121. package/registry/components/agent-input.tsx +382 -0
  122. package/registry/components/alert-dialog.tsx +297 -297
  123. package/registry/components/alert.tsx +293 -293
  124. package/registry/components/approval-card.css +409 -0
  125. package/registry/components/approval-card.tsx +401 -0
  126. package/registry/components/approval-frame.tsx +108 -0
  127. package/registry/components/approval-questions.tsx +559 -0
  128. package/registry/components/aspect-ratio.tsx +30 -30
  129. package/registry/components/attribute.tsx +454 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  132. package/registry/components/badge.tsx +629 -629
  133. package/registry/components/button-group.tsx +565 -565
  134. package/registry/components/button.tsx +127 -123
  135. package/registry/components/card.tsx +638 -638
  136. package/registry/components/carousel.tsx +296 -296
  137. package/registry/components/chart-block-data.ts +177 -0
  138. package/registry/components/chart-block.css +171 -0
  139. package/registry/components/chart-block.tsx +260 -0
  140. package/registry/components/chart-interactions.ts +123 -0
  141. package/registry/components/chart-options.cartesian.ts +162 -0
  142. package/registry/components/chart-options.paint.ts +99 -0
  143. package/registry/components/chart-options.parts.ts +72 -0
  144. package/registry/components/chart-options.radial.ts +109 -0
  145. package/registry/components/chart-options.ts +44 -0
  146. package/registry/components/chart-options.types.ts +67 -0
  147. package/registry/components/chart.tsx +203 -203
  148. package/registry/components/checkbox.tsx +247 -247
  149. package/registry/components/collapsible.tsx +109 -69
  150. package/registry/components/command.tsx +208 -208
  151. package/registry/components/context-menu.tsx +236 -236
  152. package/registry/components/data-chart.tsx +40 -0
  153. package/registry/components/date-picker.tsx +1289 -1277
  154. package/registry/components/echart-theme.ts +246 -0
  155. package/registry/components/echart.css +49 -0
  156. package/registry/components/echart.tsx +178 -0
  157. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  158. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  159. package/registry/components/expanded/Banner.tsx +179 -179
  160. package/registry/components/expanded/Breadcrumbs.tsx +131 -131
  161. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  162. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  163. package/registry/components/expanded/EmptyState.tsx +155 -155
  164. package/registry/components/expanded/FileUpload.tsx +543 -543
  165. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -59
  166. package/registry/components/expanded/PageHeader.tsx +179 -166
  167. package/registry/components/expanded/Slideout.tsx +108 -108
  168. package/registry/components/expanded/Steps.tsx +402 -402
  169. package/registry/components/expanded/Table.tsx +1092 -1071
  170. package/registry/components/expanded/Timeline.tsx +203 -203
  171. package/registry/components/expanded/activityFeed.css +248 -248
  172. package/registry/components/expanded/activityHeatmap.css +66 -66
  173. package/registry/components/expanded/banner.css +260 -255
  174. package/registry/components/expanded/breadcrumbs.css +140 -140
  175. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  176. package/registry/components/expanded/divider.css +75 -75
  177. package/registry/components/expanded/emptyState.css +111 -111
  178. package/registry/components/expanded/fileUpload.css +457 -457
  179. package/registry/components/expanded/onboardingStepListItem.css +135 -126
  180. package/registry/components/expanded/pageHeader.css +248 -290
  181. package/registry/components/expanded/slideout.css +276 -276
  182. package/registry/components/expanded/steps.css +356 -356
  183. package/registry/components/expanded/table.css +606 -603
  184. package/registry/components/expanded/timeline.css +286 -286
  185. package/registry/components/field.tsx +283 -283
  186. package/registry/components/hover-card.tsx +48 -48
  187. package/registry/components/inline-citations.css +126 -0
  188. package/registry/components/inline-citations.tsx +292 -0
  189. package/registry/components/input-content.ts +107 -107
  190. package/registry/components/input-group.tsx +130 -130
  191. package/registry/components/input-otp.tsx +584 -584
  192. package/registry/components/input.tsx +990 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  195. package/registry/components/label.tsx +78 -78
  196. package/registry/components/menu.tsx +978 -986
  197. package/registry/components/menubar.tsx +226 -226
  198. package/registry/components/modal.tsx +872 -872
  199. package/registry/components/native-select.tsx +205 -205
  200. package/registry/components/navigation-menu.tsx +171 -171
  201. package/registry/components/pagination.css +36 -36
  202. package/registry/components/pagination.tsx +560 -555
  203. package/registry/components/popover.tsx +338 -338
  204. package/registry/components/progress-slider.tsx +216 -216
  205. package/registry/components/radio-card.tsx +676 -677
  206. package/registry/components/radio-group.tsx +570 -570
  207. package/registry/components/resizable.tsx +108 -108
  208. package/registry/components/rolling-count.css +37 -0
  209. package/registry/components/rolling-count.tsx +110 -0
  210. package/registry/components/scroll-area.tsx +91 -91
  211. package/registry/components/segmented-control.tsx +87 -87
  212. package/registry/components/select-content.tsx +218 -218
  213. package/registry/components/select.tsx +937 -932
  214. package/registry/components/sheet.tsx +305 -305
  215. package/registry/components/shimmer-text.css +69 -0
  216. package/registry/components/shimmer-text.tsx +43 -0
  217. package/registry/components/sidebar.tsx +1433 -358
  218. package/registry/components/skeleton.css +54 -54
  219. package/registry/components/skeleton.tsx +482 -482
  220. package/registry/components/slider.tsx +380 -380
  221. package/registry/components/social-button.tsx +387 -387
  222. package/registry/components/spinner.tsx +78 -78
  223. package/registry/components/streaming-text.css +45 -0
  224. package/registry/components/streaming-text.tsx +165 -0
  225. package/registry/components/table-elements.tsx +128 -128
  226. package/registry/components/tabs.css +222 -218
  227. package/registry/components/tabs.tsx +241 -241
  228. package/registry/components/tag.tsx +423 -423
  229. package/registry/components/task-list.css +328 -0
  230. package/registry/components/task-list.tsx +429 -0
  231. package/registry/components/textarea.tsx +610 -610
  232. package/registry/components/thinking-reasoning.css +178 -0
  233. package/registry/components/thinking-reasoning.tsx +253 -0
  234. package/registry/components/thinking-state.css +8 -0
  235. package/registry/components/thinking-state.tsx +46 -0
  236. package/registry/components/toast.tsx +136 -136
  237. package/registry/components/toggle.tsx +256 -256
  238. package/registry/components/toolbar.tsx +539 -539
  239. package/registry/components/tooltip.tsx +433 -431
  240. package/registry/examples/agent-input-demo.tsx +90 -0
  241. package/registry/examples/alert-demo.tsx +34 -34
  242. package/registry/examples/alert-dialog-demo.tsx +42 -42
  243. package/registry/examples/approval-card-demo.tsx +83 -0
  244. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  245. package/registry/examples/attribute-demo.tsx +65 -65
  246. package/registry/examples/autocomplete-demo.tsx +109 -109
  247. package/registry/examples/avatar-demo.tsx +132 -132
  248. package/registry/examples/badge-demo.tsx +69 -69
  249. package/registry/examples/button-demo.tsx +86 -83
  250. package/registry/examples/button-group-demo.tsx +63 -63
  251. package/registry/examples/card-demo.tsx +86 -86
  252. package/registry/examples/carousel-demo.tsx +43 -43
  253. package/registry/examples/chart-block-demo.tsx +5 -0
  254. package/registry/examples/chart-demo.tsx +18 -33
  255. package/registry/examples/checkbox-demo.tsx +62 -62
  256. package/registry/examples/collapsible-demo.tsx +16 -16
  257. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  258. package/registry/examples/command-demo.tsx +42 -42
  259. package/registry/examples/command-palette-demo.tsx +235 -236
  260. package/registry/examples/context-menu-demo.tsx +29 -29
  261. package/registry/examples/control-density-contract-demo.tsx +65 -61
  262. package/registry/examples/date-picker-demo.tsx +127 -127
  263. package/registry/examples/divider-demo.tsx +17 -17
  264. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  265. package/registry/examples/expanded/banner-demo.tsx +48 -48
  266. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  267. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  268. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  269. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  270. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  271. package/registry/examples/expanded/steps-demo.tsx +29 -29
  272. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  273. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  274. package/registry/examples/field-demo.tsx +51 -51
  275. package/registry/examples/hover-card-demo.tsx +23 -23
  276. package/registry/examples/inline-citations-demo.tsx +31 -0
  277. package/registry/examples/input-actions-demo.tsx +36 -0
  278. package/registry/examples/input-demo.tsx +87 -87
  279. package/registry/examples/input-group-demo.tsx +16 -16
  280. package/registry/examples/input-otp-demo.tsx +72 -72
  281. package/registry/examples/item-demo.tsx +46 -46
  282. package/registry/examples/kbd-demo.tsx +12 -11
  283. package/registry/examples/label-demo.tsx +20 -20
  284. package/registry/examples/menu-demo.tsx +122 -122
  285. package/registry/examples/menubar-demo.tsx +34 -34
  286. package/registry/examples/modal-demo.tsx +74 -74
  287. package/registry/examples/native-select-demo.tsx +16 -16
  288. package/registry/examples/navigation-menu-demo.tsx +29 -29
  289. package/registry/examples/pagination-demo.tsx +20 -20
  290. package/registry/examples/popover-demo.tsx +547 -546
  291. package/registry/examples/progress-demo.tsx +37 -37
  292. package/registry/examples/progress-slider-demo.tsx +29 -29
  293. package/registry/examples/radio-card-demo.tsx +51 -51
  294. package/registry/examples/radio-group-demo.tsx +62 -62
  295. package/registry/examples/resizable-demo.tsx +22 -22
  296. package/registry/examples/results-toolbar-demo.tsx +44 -44
  297. package/registry/examples/rolling-count-demo.tsx +39 -0
  298. package/registry/examples/scroll-area-demo.tsx +15 -15
  299. package/registry/examples/segmented-control-demo.tsx +39 -39
  300. package/registry/examples/select-demo.tsx +87 -87
  301. package/registry/examples/sheet-demo.tsx +47 -47
  302. package/registry/examples/shimmer-text-demo.tsx +20 -0
  303. package/registry/examples/sidebar-demo.tsx +101 -57
  304. package/registry/examples/slider-demo.tsx +31 -31
  305. package/registry/examples/social-button-demo.tsx +61 -61
  306. package/registry/examples/spinner-demo.tsx +59 -59
  307. package/registry/examples/streaming-text-demo.tsx +28 -0
  308. package/registry/examples/tabs-demo.tsx +13 -13
  309. package/registry/examples/tag-demo.tsx +29 -29
  310. package/registry/examples/task-list-demo.tsx +74 -0
  311. package/registry/examples/textarea-demo.tsx +79 -79
  312. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  313. package/registry/examples/thinking-state-demo.tsx +13 -0
  314. package/registry/examples/toast-demo.tsx +35 -35
  315. package/registry/examples/toggle-demo.tsx +59 -59
  316. package/registry/examples/tooltip-demo.tsx +115 -115
  317. package/registry/guidance/USAGE.md +27 -27
  318. package/registry/guidance/charts.LICENSE.txt +24 -0
  319. package/registry/hooks/use-controllable-state.ts +33 -0
  320. package/registry/hooks/use-direction.ts +207 -207
  321. package/registry/hooks/use-listbox-navigation.ts +73 -0
  322. package/registry/hooks/use-motion-end.ts +55 -0
  323. package/registry/hooks/use-reduced-motion.ts +28 -0
  324. package/registry/index.json +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  327. package/schema/config.schema.json +51 -51
  328. package/schema/registry.schema.json +194 -190
  329. package/schema/tokens.schema.json +167 -163
  330. package/src/cli/command-line.js +194 -194
  331. package/src/cli/config-editors.js +9 -9
  332. package/src/cli/doctor.js +399 -399
  333. package/src/cli/file-transaction.js +302 -295
  334. package/src/cli/global-css.js +153 -153
  335. package/src/cli/index.js +1002 -1009
  336. package/src/cli/install-plan.js +592 -592
  337. package/src/cli/install-receipt.js +171 -171
  338. package/src/cli/jsonc.js +203 -203
  339. package/src/cli/package-install-state.js +254 -254
  340. package/src/cli/package-manager.js +238 -238
  341. package/src/cli/package-plan.js +185 -185
  342. package/src/cli/package-version.js +97 -97
  343. package/src/cli/registry-query.js +207 -207
  344. package/src/cli/reporting.js +55 -55
  345. package/src/cli/schema-validation.js +19 -19
  346. package/src/cli/source-scanner.js +686 -686
  347. package/src/cli/vite-config.js +478 -478
  348. package/tokens/banhaten.tokens.json +458 -224
@@ -0,0 +1,177 @@
1
+ import type { ChartDatum, ChartSpec } from "./chart-options"
2
+
3
+ export type ChartBlockDefinition = {
4
+ title: string
5
+ description: string
6
+ spec: ChartSpec
7
+ periods?: { label: string; count: number }[]
8
+ }
9
+
10
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
11
+ const monthly = (keys: string[], values: number[][]): ChartDatum[] => values.map((row, index) => ({
12
+ name: months[index % 12] + (values.length > 12 ? ` '${24 + Math.floor(index / 12)}` : ""),
13
+ ...Object.fromEntries(keys.map((key, column) => [key, row[column]])),
14
+ }))
15
+ const monthPeriods = [{ label: "3 months", count: 3 }, { label: "6 months", count: 6 }, { label: "All", count: Infinity }]
16
+ const series = (key: string, label: string, colorIndex: number) => ({ key, label, colorIndex })
17
+
18
+ // Illustrative source datasets, adapted from EvilCharts' MIT-licensed ECharts blocks.
19
+ // All visual decisions live in the shared token-aware option builders and block CSS.
20
+ export const chartBlockDefinitions = {
21
+ latency: {
22
+ title: "Latency Percentiles",
23
+ description: "Request latency by percentile, in milliseconds.",
24
+ periods: [{ label: "5 minutes", count: 5 }, { label: "15 minutes", count: 15 }, { label: "All", count: Infinity }],
25
+ spec: {
26
+ family: "area", curve: "linear", variant: "gradient", hoverHighlight: true,
27
+ series: [series("p99", "P99 (ms)", 3), series("p95", "P95 (ms)", 2), series("p75", "P75 (ms)", 4), series("p50", "P50 (ms)", 0)],
28
+ data: [
29
+ [188, 92, 44, 20], [196, 95, 46, 21], [181, 89, 43, 20], [192, 94, 47, 22],
30
+ [205, 99, 45, 21], [187, 91, 44, 20], [179, 88, 42, 19], [198, 96, 46, 21],
31
+ [210, 101, 48, 22], [194, 93, 45, 21], [202, 97, 47, 22], [215, 104, 49, 23],
32
+ [231, 112, 52, 23], [278, 131, 57, 24], [306, 142, 61, 25], [289, 135, 58, 24],
33
+ [247, 118, 53, 23], [216, 105, 49, 22], [201, 97, 46, 21], [193, 94, 45, 21],
34
+ [186, 90, 44, 20], [199, 96, 46, 21], [207, 100, 47, 22], [191, 92, 45, 20],
35
+ [184, 89, 43, 20], [196, 95, 46, 21], [209, 101, 48, 22], [198, 96, 46, 21], [204, 98, 46, 21],
36
+ ].map(([p99, p95, p75, p50], index) => ({ name: `13:${String(index + 6).padStart(2, "0")}`, p99, p95, p75, p50 })),
37
+ },
38
+ },
39
+ portfolio: {
40
+ title: "Portfolio Comparison", description: "Compare the change in two example portfolios, in USD.",
41
+ periods: [{ label: "7 days", count: 7 }, { label: "14 days", count: 14 }, { label: "All", count: Infinity }],
42
+ spec: {
43
+ family: "area", curve: "step", variant: "dotted", hoverReveal: true,
44
+ series: [series("portfolioA", "Portfolio A (USD)", 0), series("portfolioB", "Portfolio B (USD)", 1)],
45
+ data: [
46
+ [53916, 53670], [54380, 53080], [54760, 52460], [54480, 51880], [54060, 51340],
47
+ [53820, 50900], [53520, 50520], [53240, 50200], [53080, 49960], [52840, 49760],
48
+ [52700, 49640], [52520, 49600], [52420, 49720], [52340, 49980], [52240, 50360],
49
+ [52140, 50820], [52080, 51340], [52020, 51900], [51960, 52440], [51900, 52960],
50
+ [51860, 53420], [51800, 53780], [51740, 54040], [51700, 54200], [51640, 54300],
51
+ [51580, 54320], [51538, 54287],
52
+ ].map(([portfolioA, portfolioB], index) => ({ name: index < 10 ? `Dec ${index + 22}` : `Jan ${index - 9}`, portfolioA, portfolioB })),
53
+ },
54
+ },
55
+ benchmark: {
56
+ title: "Growth Benchmark", description: "Weekly signups compared with the target.",
57
+ periods: [{ label: "Recent 4", count: 4 }, { label: "Recent 12", count: 12 }, { label: "All", count: Infinity }],
58
+ spec: {
59
+ family: "area", curve: "step", legend: true,
60
+ series: [{ ...series("actual", "Actual", 0), variant: "hatched" }, { ...series("target", "Target", 4), type: "line", stroke: "dashed" }],
61
+ data: [
62
+ ["Jan 1", 240, 125], ["Jan 8", 240, 125], ["Jan 15", 175, 200], ["Jan 22", 175, 200],
63
+ ["Feb 1", 175, 200], ["Feb 8", 260, 125], ["Feb 15", 260, 125], ["Feb 22", 260, 125],
64
+ ["Mar 1", 260, 190], ["Mar 8", 335, 190], ["Mar 15", 335, 190], ["Mar 22", 335, 390],
65
+ ["Apr 1", 335, 390], ["Apr 8", 335, 390], ["Apr 15", 190, 390], ["Apr 22", 215, 305],
66
+ ["May 1", 215, 305], ["May 8", 215, 175], ["May 15", 215, 175], ["May 22", 275, 175], ["Jun 1", 275, 245],
67
+ ].map(([name, actual, target]) => ({ name, actual, target })),
68
+ },
69
+ },
70
+ audience: {
71
+ title: "Audience Growth", description: "Monthly listeners across every show and episode.", periods: monthPeriods,
72
+ spec: { family: "area", curve: "monotone", variant: "gradient", series: [{ key: "listeners", label: "Listeners", colorIndices: [1, 4, 5] }],
73
+ data: monthly(["listeners"], [[2980], [3120], [3460], [3380], [3720], [4180], [4560]]) },
74
+ },
75
+ payouts: {
76
+ title: "Payout Trend", description: "Paid and pending amounts by month, in USD.", periods: monthPeriods,
77
+ spec: { family: "line", curve: "monotone", legend: true,
78
+ series: [{ key: "payouts", label: "Paid (USD)", colorIndices: [2, 7] }, { key: "pending", label: "Pending (USD)", colorIndices: [4, 5] }],
79
+ data: monthly(["payouts", "pending"], [[312, 548], [388, 502], [342, 561], [455, 470], [521, 398], [486, 441], [573, 372], [640, 316], [598, 358], [662, 284], [617, 331], [690, 262]]) },
80
+ },
81
+ shipments: {
82
+ title: "Shipments", description: "Orders shipped in corresponding slots this week and last week.",
83
+ periods: [{ label: "2 days", count: 8 }, { label: "4 days", count: 16 }, { label: "All", count: Infinity }],
84
+ spec: { family: "line", curve: "linear", legend: true,
85
+ series: [series("current", "This week", 0), { ...series("previous", "Last week", 4), stroke: "dashed" }],
86
+ data: [[14, 34], [9, 41], [18, 37], [26, 29], [31, 24], [27, 33], [19, 45], [12, 52], [16, 47], [22, 39], [15, 44], [11, 50], [17, 43], [24, 31], [20, 26], [13, 22], [21, 28], [29, 36], [34, 42], [28, 55], [23, 49], [30, 40], [36, 35], [32, 44]]
87
+ .map(([current, previous], index) => ({ name: `${["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][Math.floor(index / 4)]} ${index % 4 + 1}`, current, previous })) },
88
+ },
89
+ peak: {
90
+ title: "Peak Week", description: "Organic and paid signups, with the best week highlighted.",
91
+ periods: [{ label: "4 weeks", count: 4 }, { label: "8 weeks", count: 8 }, { label: "All", count: Infinity }],
92
+ spec: { family: "bar", stack: "stacked", highlightMax: true, legend: true,
93
+ series: [series("organic", "Organic", 0), series("paid", "Paid", 4)],
94
+ data: [[128, 74], [164, 91], [142, 66], [199, 108], [176, 84], [231, 167], [208, 96], [287, 158], [244, 112], [196, 88], [221, 103], [173, 79]]
95
+ .map(([organic, paid], index) => ({ name: `W${String(index + 1).padStart(2, "0")}`, organic, paid })) },
96
+ },
97
+ grid: {
98
+ title: "Grid Bar Chart", description: "Hourly sessions shown as columns of cells.",
99
+ periods: [{ label: "6 hours", count: 6 }, { label: "12 hours", count: 12 }, { label: "All", count: Infinity }],
100
+ spec: { family: "bar", variant: "blocks", series: [series("sessions", "Sessions", 0)],
101
+ data: [42, 28, 19, 14, 12, 18, 34, 66, 98, 124, 147, 163, 158, 171, 186, 174, 152, 138, 119, 96, 84, 71, 58, 47]
102
+ .map((sessions, index) => ({ name: `${String(index).padStart(2, "0")}:00`, sessions })) },
103
+ },
104
+ monospace: {
105
+ title: "Monospace Bar Chart", description: "Monthly sales with bars that expand on hover.", periods: monthPeriods,
106
+ spec: { family: "bar", variant: "expandable", series: [series("sales", "Sales (USD)", 0)],
107
+ data: monthly(["sales"], [342, 876, 512, 629, 458, 781, 394, 925, 647, 532, 803, 271, 388, 912, 564, 671, 499, 838, 427, 968, 702, 585, 861, 314].map((value) => [value])) },
108
+ },
109
+ "market-share": {
110
+ title: "Market Share", description: "Illustrative ecosystem value by company, in billions of USD.",
111
+ spec: { family: "pie", variant: "padded", series: [series("value", "Value (USD billions)", 0)],
112
+ data: [{ name: "Skyline", value: 27 }, { name: "Datawell", value: 21 }, { name: "Cloudpeak", value: 13 }, { name: "Taskbridge", value: 21 }, { name: "Insightloop", value: 6 }, { name: "Streamforge", value: 12 }] },
113
+ },
114
+ "progress-rings": {
115
+ title: "Progress Rings", description: "Where the workday leaks: illustrative findings from 1,240 survey responses.",
116
+ spec: { family: "pie", series: [series("value", "Respondents (%)", 0)], data: [
117
+ { name: "Additional support requests", value: 48 }, { name: "Inaccurate forecasts", value: 67 },
118
+ ] },
119
+ },
120
+ "revenue-mix": {
121
+ title: "Revenue Mix", description: "Revenue by sales channel, in USD.",
122
+ spec: { family: "pie", variant: "padded", series: [series("value", "Revenue (USD)", 0)], data: [
123
+ { name: "Direct", value: 52400 }, { name: "Marketplace", value: 38900 }, { name: "Wholesale", value: 24150 }, { name: "Affiliate", value: 16300 },
124
+ ] },
125
+ },
126
+ "reliability-score": {
127
+ title: "Reliability Score", description: "Delivery reliability: 842 out of 1,000, in the excellent band.",
128
+ spec: { family: "pie", variant: "padded", data: [
129
+ { name: "At risk", from: 0, value: 450 }, { name: "Fair", from: 450, value: 200 }, { name: "Good", from: 650, value: 170 }, { name: "Excellent", from: 820, value: 180 },
130
+ ], series: [series("value", "Band width", 0)] },
131
+ },
132
+ budget: {
133
+ title: "Monthly Budget", description: "The share and amount allocated to each monthly budget category.",
134
+ spec: { family: "radial", max: 100, series: [series("value", "Share (%)", 0)], data: [
135
+ { name: "Payroll", value: 46, amount: 920000 }, { name: "Infrastructure", value: 21, amount: 420000 }, { name: "Marketing", value: 14, amount: 280000 }, { name: "Tooling", value: 10, amount: 200000 }, { name: "Support", value: 9, amount: 180000 },
136
+ ] },
137
+ },
138
+ ride: {
139
+ title: "Ride Summary", description: "Distance, elevation, energy and cadence against their goals.",
140
+ spec: { family: "radial", data: [
141
+ { name: "Distance", value: 18.4, goal: 25, unit: "km" }, { name: "Elevation", value: 312, goal: 450, unit: "m" },
142
+ { name: "Energy", value: 684, goal: 1200, unit: "kJ" }, { name: "Cadence", value: 82, goal: 95, unit: "rpm" },
143
+ ] },
144
+ },
145
+ "cache-tiers": {
146
+ title: "Cache Tiers", description: "Request routing across four cache tiers, from a sample of 1,000 requests.",
147
+ spec: { family: "radial", variant: "semi", max: 100, series: [series("value", "Share (%)", 0)], data: [
148
+ { name: "L1 Memory", value: 61, count: 610 }, { name: "L2 Regional", value: 24, count: 240 },
149
+ { name: "Edge Overflow", value: 10, count: 100 }, { name: "Origin Fetch", value: 5, count: 50 },
150
+ ] },
151
+ },
152
+ allocation: {
153
+ title: "Fund Allocation", description: "Where the fund flows, in thousands of USD.",
154
+ spec: { family: "sankey", variant: "gradient", labels: "inside", data: [],
155
+ nodes: ["Inflow", "Equities", "Bonds", "Cash", "Growth", "Income", "Reserve"].map((name) => ({ name })),
156
+ links: [[0, 1, 78], [0, 2, 46], [0, 3, 24], [1, 4, 52], [1, 5, 26], [2, 5, 19], [2, 6, 27], [3, 4, 9], [3, 6, 15]]
157
+ .map(([source, target, value]) => ({ source, target, value })) },
158
+ },
159
+ pipeline: {
160
+ title: "Revenue Pipeline", description: "Four revenue sources flow into four operating allocations, in USD.",
161
+ spec: { family: "sankey", variant: "gradient", labels: "outside", data: [],
162
+ nodes: ["Retail", "Wholesale", "Licensing", "Services", "Pipeline", "Expansion", "Tooling", "Support", "Reserve"].map((name) => ({ name })),
163
+ links: [[0, 4, 31480], [1, 4, 46220], [2, 4, 14960], [3, 4, 28340], [4, 5, 52640], [4, 6, 9180], [4, 7, 12470], [4, 8, 46710]]
164
+ .map(([source, target, value]) => ({ source, target, value })) },
165
+ },
166
+ } satisfies Record<string, ChartBlockDefinition>
167
+
168
+ export type ChartBlockId = keyof typeof chartBlockDefinitions
169
+
170
+ export const chartBlocks = (Object.entries(chartBlockDefinitions) as [ChartBlockId, ChartBlockDefinition][])
171
+ .map(([id, definition]) => ({ id, title: definition.title, description: definition.description, family: definition.spec.family }))
172
+
173
+ export function getChartBlockSpec(block: ChartBlockId, period = "All"): ChartSpec {
174
+ const definition: ChartBlockDefinition = chartBlockDefinitions[block]
175
+ const count = definition.periods?.find((entry) => entry.label === period)?.count ?? Infinity
176
+ return { ...definition.spec, data: definition.spec.data.slice(-count) }
177
+ }
@@ -0,0 +1,171 @@
1
+ .bh-chart-block {
2
+ container-type: inline-size;
3
+ display: flex;
4
+ flex-direction: column;
5
+ min-inline-size: 0;
6
+ inline-size: 100%;
7
+ overflow: hidden;
8
+ border: var(--bh-border-width-default) solid var(--bh-border-subtle);
9
+ border-radius: var(--bh-radius-3xl-16);
10
+ background: var(--bh-bg-raised);
11
+ color: var(--bh-content-default);
12
+ font-family: var(--bh-font-family);
13
+ font-size: var(--bh-text-body-xs-regular-font-size);
14
+ font-weight: var(--bh-text-body-xs-regular-font-weight);
15
+ line-height: var(--bh-text-body-xs-regular-line-height);
16
+ letter-spacing: var(--bh-text-body-xs-regular-letter-spacing);
17
+ text-align: start;
18
+ }
19
+
20
+ .bh-chart-block-header {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ align-items: flex-start;
24
+ justify-content: space-between;
25
+ gap: var(--bh-space-3xl-16);
26
+ padding: var(--bh-space-5xl-24);
27
+ border-block-end: var(--bh-border-width-default) solid var(--bh-border-subtle);
28
+ }
29
+
30
+ .bh-chart-block-header > div:first-child { flex: 1 1 var(--bh-space-15xl-224); }
31
+ .bh-chart-block-header h3 {
32
+ margin: 0;
33
+ font-size: var(--bh-text-heading-xs-semibold-font-size);
34
+ font-weight: var(--bh-text-heading-xs-semibold-font-weight);
35
+ line-height: var(--bh-text-heading-xs-semibold-line-height);
36
+ letter-spacing: var(--bh-text-heading-xs-semibold-letter-spacing);
37
+ }
38
+ .bh-chart-block-header p { margin: var(--bh-space-xs-4) 0 0; color: var(--bh-content-subtle); }
39
+ .bh-chart-block-periods { display: flex; flex-wrap: wrap; gap: var(--bh-space-xxs-2); }
40
+ .bh-chart-block-body { display: flex; flex-direction: column; gap: var(--bh-space-5xl-24); padding: var(--bh-space-5xl-24); }
41
+ .bh-chart-block-chart { height: var(--bh-space-17xl-288); }
42
+
43
+ .bh-chart-block-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--bh-space-4xl-20); }
44
+ .bh-chart-block-stats[data-columns="3"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
45
+ .bh-chart-block-stats[data-columns="4"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
46
+ .bh-chart-block-stat { display: flex; flex-direction: column; gap: var(--bh-space-sm-6); min-inline-size: 0; }
47
+ .bh-chart-block-stat-label { display: flex; align-items: center; gap: var(--bh-space-sm-6); color: var(--bh-content-subtle); }
48
+ .bh-chart-block-value {
49
+ display: flex;
50
+ flex-wrap: wrap;
51
+ align-items: baseline;
52
+ gap: var(--bh-space-xs-4);
53
+ font-size: var(--bh-text-heading-lg-semibold-font-size);
54
+ font-weight: var(--bh-text-heading-lg-semibold-font-weight);
55
+ line-height: var(--bh-text-heading-lg-semibold-line-height);
56
+ letter-spacing: var(--bh-text-heading-lg-semibold-letter-spacing);
57
+ font-variant-numeric: tabular-nums;
58
+ }
59
+ .bh-chart-block-value small {
60
+ color: var(--bh-content-subtle);
61
+ font-size: var(--bh-text-body-sm-regular-font-size);
62
+ font-weight: var(--bh-text-body-sm-regular-font-weight);
63
+ line-height: var(--bh-text-body-sm-regular-line-height);
64
+ }
65
+ .bh-chart-block-detail { color: var(--bh-content-subtle); }
66
+ .bh-chart-block-swatch { display: inline-block; inline-size: var(--bh-space-md-8); block-size: var(--bh-space-md-8); border-radius: var(--bh-radius-xs-2); flex: none; }
67
+
68
+ .bh-chart-block-stat-button, button.bh-chart-block-list-row {
69
+ margin: 0;
70
+ border: 0;
71
+ background: transparent;
72
+ color: inherit;
73
+ font: inherit;
74
+ letter-spacing: inherit;
75
+ text-align: inherit;
76
+ cursor: pointer;
77
+ outline: none;
78
+ }
79
+ .bh-chart-block-stat-button { padding: var(--bh-space-md-8); border-radius: var(--bh-radius-lg-8); }
80
+ .bh-chart-block-stat-button:hover, button.bh-chart-block-list-row:hover { background: var(--bh-interactive-ghost-hover); }
81
+ .bh-chart-block-stat-button[aria-pressed="true"] { background: var(--bh-bg-neutral-subtle); }
82
+ .bh-chart-block [data-muted="true"] { opacity: var(--bh-opacity-muted); }
83
+ .bh-chart-block-stat-button:focus-visible, button.bh-chart-block-list-row:focus-visible,
84
+ .bh-chart-block-data summary:focus-visible, .bh-chart-block-table-scroll:focus-visible, .bh-chart-block-flow-viewport:focus-visible {
85
+ outline: var(--bh-focus-ring-width) solid var(--bh-border-focus);
86
+ outline-offset: var(--bh-space-xxs-2);
87
+ }
88
+
89
+ .bh-chart-block-list { display: grid; min-inline-size: 0; gap: var(--bh-space-xs-4); }
90
+ .bh-chart-block-list[data-columns="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--bh-space-5xl-24); }
91
+ .bh-chart-block-list-row { display: flex; align-items: center; gap: var(--bh-space-md-8); min-inline-size: 0; padding: var(--bh-space-lg-10) var(--bh-space-md-8); border-radius: var(--bh-radius-md-6); }
92
+ .bh-chart-block-list-row > span:not(.bh-chart-block-swatch) { min-inline-size: 0; overflow-wrap: anywhere; color: var(--bh-content-subtle); }
93
+ .bh-chart-block-list-row strong { margin-inline-start: auto; font-weight: var(--bh-text-body-xs-medium-font-weight); font-variant-numeric: tabular-nums; text-align: end; }
94
+ .bh-chart-block[data-block="budget"] .bh-chart-block-list-row:nth-child(odd) { background: var(--bh-bg-neutral-subtle); }
95
+ .bh-chart-block-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; gap: var(--bh-space-5xl-24); }
96
+ .bh-chart-block-donut-layout { display: flex; flex-direction: column; gap: var(--bh-space-3xl-16); }
97
+ .bh-chart-block-donut-layout > .bh-chart-block-list { padding-block-start: var(--bh-space-3xl-16); border-block-start: var(--bh-border-width-default) solid var(--bh-border-subtle); }
98
+ .bh-chart-block-donut { position: relative; min-inline-size: 0; }
99
+ .bh-chart-block-donut .bh-chart-block-chart { height: var(--bh-space-17xl-288); }
100
+ .bh-chart-block-ring-center { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--bh-space-xs-4); padding-inline: 23%; pointer-events: none; text-align: center; }
101
+ .bh-chart-block-ring-center strong {
102
+ font-size: var(--bh-text-heading-md-semibold-font-size);
103
+ font-weight: var(--bh-text-heading-md-semibold-font-weight);
104
+ line-height: var(--bh-text-heading-md-semibold-line-height);
105
+ letter-spacing: var(--bh-text-heading-md-semibold-letter-spacing);
106
+ font-variant-numeric: tabular-nums;
107
+ }
108
+ .bh-chart-block-ring-center span { color: var(--bh-content-subtle); font-size: var(--bh-text-body-2xs-regular-font-size); line-height: var(--bh-text-body-2xs-regular-line-height); }
109
+ .bh-chart-block-ring { position: relative; min-inline-size: 0; inline-size: 100%; aspect-ratio: 1; }
110
+ .bh-chart-block-ring > .bh-echart { height: 100%; }
111
+ .bh-chart-block-research-rings { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--bh-space-5xl-24); }
112
+ .bh-chart-block-research-rings > div { inline-size: 100%; max-inline-size: var(--bh-space-17xl-288); justify-self: center; }
113
+ .bh-chart-block-research-rings .bh-chart-block-ring-center strong { font-size: var(--bh-text-heading-lg-semibold-font-size); line-height: var(--bh-text-heading-lg-semibold-line-height); }
114
+ .bh-chart-block-budget-rings { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--bh-space-md-8); }
115
+ .bh-chart-block-budget-rings > div { display: flex; flex-direction: column; align-items: center; min-inline-size: 0; gap: var(--bh-space-xs-4); text-align: center; }
116
+ .bh-chart-block-budget-rings .bh-chart-block-ring { max-inline-size: var(--bh-space-11xl-96); }
117
+ .bh-chart-block-budget-rings .bh-chart-block-ring-center { padding: 0; }
118
+ .bh-chart-block-budget-rings strong { font-size: var(--bh-text-body-sm-medium-font-size); line-height: var(--bh-text-body-sm-medium-line-height); }
119
+ .bh-chart-block-budget-rings > div > span { color: var(--bh-content-subtle); overflow-wrap: anywhere; font-size: var(--bh-text-body-2xs-regular-font-size); line-height: var(--bh-text-body-2xs-regular-line-height); }
120
+ .bh-chart-block-score { position: relative; inline-size: 100%; max-inline-size: var(--bh-space-17xl-288); align-self: center; }
121
+ .bh-chart-block-score-caption { margin: calc(-1 * var(--bh-space-6xl-32)) 0 0; text-align: center; }
122
+ .bh-chart-block-range { display: flex; gap: var(--bh-space-xs-4); color: var(--bh-content-subtle); font-size: var(--bh-text-body-2xs-regular-font-size); line-height: var(--bh-text-body-2xs-regular-line-height); }
123
+ .bh-chart-block-range > div { display: flex; flex-direction: column; gap: var(--bh-space-xs-4); min-inline-size: 0; flex-basis: 0; }
124
+ .bh-chart-block-range i { block-size: var(--bh-space-sm-6); border-radius: var(--bh-radius-full); }
125
+ .bh-chart-block-range > div > span:last-child { overflow-wrap: anywhere; }
126
+
127
+ .bh-chart-block-ride { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); gap: var(--bh-space-5xl-24); }
128
+ .bh-chart-block-ride-main { display: flex; flex-direction: column; gap: var(--bh-space-3xl-16); }
129
+ .bh-chart-block-ride-distance { display: flex; align-items: center; gap: var(--bh-space-3xl-16); }
130
+ .bh-chart-block-ride-distance > svg { inline-size: var(--bh-space-6xl-32); block-size: var(--bh-space-6xl-32); color: var(--bh-content-brand-default); flex: none; }
131
+ .bh-chart-block-metric-rings { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--bh-space-md-8); }
132
+ .bh-chart-block-metric-rings > div { display: flex; flex-direction: column; align-items: center; gap: var(--bh-space-xs-4); text-align: center; }
133
+ .bh-chart-block-metric-rings .bh-chart-block-ring { max-inline-size: var(--bh-space-12xl-128); }
134
+ .bh-chart-block-metric-rings svg { inline-size: var(--bh-space-5xl-24); block-size: var(--bh-space-5xl-24); stroke-width: var(--bh-icon-stroke-200); }
135
+ .bh-chart-block-metric-rings strong { font-weight: var(--bh-text-body-xs-medium-font-weight); }
136
+ .bh-chart-block-metric-rings > div > span, .bh-chart-block-metric-rings small { color: var(--bh-content-subtle); font-size: var(--bh-text-body-2xs-regular-font-size); line-height: var(--bh-text-body-2xs-regular-line-height); }
137
+ .bh-chart-block-ride-splits { display: flex; flex-direction: column; justify-content: space-around; gap: var(--bh-space-3xl-16); border-inline-start: var(--bh-border-width-default) solid var(--bh-border-subtle); padding-inline-start: var(--bh-space-5xl-24); }
138
+ .bh-chart-block-flow-viewport { min-inline-size: 0; overflow-x: auto; }
139
+ .bh-chart-block-flow-canvas { min-inline-size: calc(var(--bh-space-19xl-384) + var(--bh-space-9xl-64)); }
140
+ .bh-chart-block-pipeline { position: relative; }
141
+ .bh-chart-block-flow-total { position: absolute; inset-block-start: 50%; inset-inline-start: 50%; transform: translate(-50%, -50%); padding: var(--bh-space-md-8); border-radius: var(--bh-radius-lg-8); background: var(--bh-bg-raised); text-align: center; pointer-events: none; }
142
+ .bh-chart-block:dir(rtl) .bh-chart-block-flow-total { transform: translate(50%, -50%); }
143
+ .bh-chart-block-flow-total .bh-chart-block-stat { align-items: center; }
144
+ .bh-chart-block[data-block="grid"] .bh-chart-block-stats, .bh-chart-block[data-block="monospace"] .bh-chart-block-stats { padding-block-end: var(--bh-space-4xl-20); border-block-end: var(--bh-border-width-default) dashed var(--bh-border-default); font-family: var(--bh-font-family-mono); }
145
+
146
+ .bh-chart-block-footer { padding: var(--bh-space-3xl-16) var(--bh-space-5xl-24); border-block-start: var(--bh-border-width-default) solid var(--bh-border-subtle); color: var(--bh-content-subtle); }
147
+ .bh-chart-block-footer > span { display: block; margin-block-end: var(--bh-space-md-8); font-size: var(--bh-text-body-2xs-regular-font-size); line-height: var(--bh-text-body-2xs-regular-line-height); }
148
+ .bh-chart-block-data summary { inline-size: fit-content; cursor: pointer; border-radius: var(--bh-radius-xs-2); color: var(--bh-content-default); }
149
+ .bh-chart-block-table-scroll { max-block-size: var(--bh-space-17xl-288); overflow: auto; margin-block-start: var(--bh-space-3xl-16); }
150
+ .bh-chart-block-data table { inline-size: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
151
+ .bh-chart-block-data caption { padding-block-end: var(--bh-space-xl-12); text-align: start; }
152
+ .bh-chart-block-data th, .bh-chart-block-data td { padding: var(--bh-space-md-8) var(--bh-space-xl-12); border-block-end: var(--bh-border-width-default) solid var(--bh-border-subtle); text-align: start; white-space: nowrap; }
153
+ .bh-chart-block-data th { font-weight: var(--bh-text-body-xs-medium-font-weight); color: var(--bh-content-default); }
154
+ .bh-chart-block-data thead { background: var(--bh-bg-neutral-subtle); }
155
+
156
+ @container (max-width: 560px) {
157
+ .bh-chart-block-header, .bh-chart-block-body { padding: var(--bh-space-3xl-16); }
158
+ .bh-chart-block-stats[data-columns="4"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
159
+ .bh-chart-block-split { grid-template-columns: minmax(0, 1fr); gap: var(--bh-space-3xl-16); }
160
+ .bh-chart-block-split .bh-chart-block-donut { max-inline-size: var(--bh-space-17xl-288); inline-size: 100%; justify-self: center; }
161
+ .bh-chart-block-list[data-columns="2"] { grid-template-columns: minmax(0, 1fr); }
162
+ .bh-chart-block-value { font-size: var(--bh-text-heading-md-semibold-font-size); line-height: var(--bh-text-heading-md-semibold-line-height); }
163
+ .bh-chart-block-ride { grid-template-columns: minmax(0, 1fr); }
164
+ .bh-chart-block-ride-splits { flex-direction: row; justify-content: flex-start; border-inline-start: 0; border-block-start: var(--bh-border-width-default) solid var(--bh-border-subtle); padding-inline-start: 0; padding-block-start: var(--bh-space-3xl-16); }
165
+ .bh-chart-block-ride-splits > div { flex: 1; }
166
+ .bh-chart-block-budget-rings { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: var(--bh-space-3xl-16); }
167
+ .bh-chart-block-research-rings { gap: var(--bh-space-md-8); }
168
+ .bh-chart-block-research-rings .bh-chart-block-ring-center span { font-size: var(--bh-text-body-3xs-regular-font-size); line-height: var(--bh-text-body-3xs-regular-line-height); }
169
+ .bh-chart-block[data-block="cache-tiers"] .bh-chart-block-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
170
+ .bh-chart-block-footer { padding: var(--bh-space-3xl-16); }
171
+ }
@@ -0,0 +1,260 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import type { EChartsOption, PieSeriesOption } from "echarts"
5
+ import { Bike, Gauge, Mountain, Zap } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { Button } from "./button"
9
+ import { Progress } from "./progress"
10
+ import { EChart, type EChartTheme } from "./echart"
11
+ import { createChartOption, type ChartDatum, type ChartSpec } from "./chart-options"
12
+ import { attachChartInteractions } from "./chart-interactions"
13
+ import { chartBlockDefinitions, getChartBlockSpec, type ChartBlockDefinition, type ChartBlockId } from "./chart-block-data"
14
+ import "./chart-block.css"
15
+
16
+ export { chartBlocks, type ChartBlockId } from "./chart-block-data"
17
+ export type ChartBlockProps = React.ComponentProps<"article"> & { block: ChartBlockId }
18
+
19
+ const number = (value: number) => value.toLocaleString("en-US", { maximumFractionDigits: 1 })
20
+ const money = (value: number) => `$${number(value)}`
21
+ const numeric = (row: ChartDatum, key: string) => Number(row[key] ?? 0)
22
+ const total = (rows: ChartDatum[], key: string) => rows.reduce((sum, row) => sum + numeric(row, key), 0)
23
+ const delta = (current: number, previous: number) => previous ? (current - previous) / previous * 100 : 0
24
+ const signed = (value: number) => `${value >= 0 ? "+" : ""}${number(value)}%`
25
+ const colorTokens = [
26
+ "--bh-chart-categorical-brand", "--bh-chart-categorical-success", "--bh-chart-categorical-warning",
27
+ "--bh-chart-categorical-danger", "--bh-chart-categorical-information", "--bh-chart-categorical-purple",
28
+ "--bh-chart-categorical-aqua", "--bh-chart-categorical-fuchsia",
29
+ ]
30
+
31
+ function Swatch({ index }: { index: number }) {
32
+ return <span className="bh-chart-block-swatch" aria-hidden="true" style={{ backgroundColor: `var(${colorTokens[index % colorTokens.length]})` }} />
33
+ }
34
+
35
+ function Stat({ label, value, detail, colorIndex }: { label: React.ReactNode; value: React.ReactNode; detail?: React.ReactNode; colorIndex?: number }) {
36
+ return <div className="bh-chart-block-stat">
37
+ <span className="bh-chart-block-stat-label">{colorIndex !== undefined && <Swatch index={colorIndex} />}{label}</span>
38
+ <strong className="bh-chart-block-value">{value}</strong>
39
+ {detail && <span className="bh-chart-block-detail">{detail}</span>}
40
+ </div>
41
+ }
42
+
43
+ function BlockChart({ block, spec, selected, onSelect, className }: { block: ChartBlockId; spec: ChartSpec; selected?: string | null; onSelect?: (key: string) => void; className?: string }) {
44
+ const option = React.useCallback((theme: EChartTheme): EChartsOption => {
45
+ const visible = block === "latency" && selected ? { ...spec, series: spec.series?.filter((item) => item.key === selected) } : spec
46
+ const result = createChartOption(theme, { ...visible, legend: visible.legend ?? false })
47
+ if (block === "market-share") {
48
+ const pie = (result.series as PieSeriesOption[])[0]
49
+ result.series = [{ ...pie, selectedMode: "single", data: spec.data.map((row, index) => ({
50
+ name: String(row.name), value: numeric(row, "value"), selected: row.name === selected,
51
+ itemStyle: { color: theme.palette[index % theme.palette.length], opacity: !selected || row.name === selected ? 1 : theme.opacity.muted },
52
+ })) }]
53
+ }
54
+ return result
55
+ }, [block, selected, spec])
56
+ const onReady = React.useCallback<NonNullable<React.ComponentProps<typeof EChart>["onReady"]>>((chart, theme) => {
57
+ const cleanup = attachChartInteractions(chart, theme, spec)
58
+ const click = (event: { name?: string; seriesId?: string }) => {
59
+ const key = block === "latency" ? event.seriesId : event.name
60
+ if (key) onSelect?.(key)
61
+ }
62
+ if (onSelect) chart.on("click", click)
63
+ return () => { cleanup(); if (onSelect) chart.off("click", click) }
64
+ }, [block, onSelect, spec])
65
+ return <EChart className={cn("bh-chart-block-chart", className)} label={chartBlockDefinitions[block].title} option={option} onReady={onReady} />
66
+ }
67
+
68
+ function TimeSeriesBlock({ block, spec, selected, onSelect }: {
69
+ block: ChartBlockId; spec: ChartSpec; selected: string | null; onSelect: (key: string) => void
70
+ }) {
71
+ const data = spec.data
72
+ const latest = data.at(-1)!
73
+ let statistics: React.ReactNode
74
+ if (block === "latency") {
75
+ statistics = <div className="bh-chart-block-stats" data-columns="4">{spec.series?.map((item) => <button
76
+ key={item.key} className="bh-chart-block-stat-button" type="button" aria-pressed={selected === item.key}
77
+ data-muted={selected !== null && selected !== item.key} onClick={() => onSelect(item.key)}
78
+ ><Stat label={item.label?.replace(" (ms)", "")} value={<>{number(numeric(latest, item.key))}<small>ms</small></>} colorIndex={item.colorIndex} detail="Latest sample" /></button>)}</div>
79
+ } else if (block === "portfolio") {
80
+ statistics = <div className="bh-chart-block-stats">{spec.series?.map((item) => {
81
+ const first = numeric(data[0], item.key)
82
+ const last = numeric(latest, item.key)
83
+ return <Stat key={item.key} label={item.label?.replace(" (USD)", "")} colorIndex={item.colorIndex}
84
+ value={signed(delta(last, first))} detail={`${last >= first ? "+" : "−"}${money(Math.abs(last - first))} over selected period`} />
85
+ })}</div>
86
+ } else if (block === "benchmark") {
87
+ statistics = <Stat label="Latest weekly signups" value={number(numeric(latest, "actual"))}
88
+ detail={`${signed(delta(numeric(latest, "actual"), numeric(latest, "target")))} vs target of ${number(numeric(latest, "target"))}`} />
89
+ } else if (block === "audience") {
90
+ statistics = <div className="bh-chart-block-stats"><Stat label="Total listener-months" value={number(total(data, "listeners"))} detail="Sum across the selected months" />
91
+ <Stat label="Latest month" value={number(numeric(latest, "listeners"))} detail={`${signed(delta(numeric(latest, "listeners"), numeric(data[0], "listeners")))} over selected period`} /></div>
92
+ } else if (block === "payouts") {
93
+ statistics = <div className="bh-chart-block-stats"><Stat label="Paid" colorIndex={2} value={money(total(data, "payouts"))} detail="Selected period" />
94
+ <Stat label="Pending" colorIndex={4} value={money(total(data, "pending"))} detail="Selected period" /></div>
95
+ } else if (block === "shipments") {
96
+ statistics = <Stat label="Orders shipped" value={number(total(data, "current"))}
97
+ detail={`${signed(delta(total(data, "current"), total(data, "previous")))} vs the same slots last week`} />
98
+ } else if (block === "peak") {
99
+ const peak = data.reduce((best, row) => numeric(row, "organic") + numeric(row, "paid") > numeric(best, "organic") + numeric(best, "paid") ? row : best)
100
+ statistics = <Stat label="Best week" value={number(numeric(peak, "organic") + numeric(peak, "paid"))} detail={`Signups in ${peak.name}`} />
101
+ } else {
102
+ const key = block === "grid" ? "sessions" : "sales"
103
+ const peak = data.reduce((best, row) => numeric(row, key) > numeric(best, key) ? row : best)
104
+ statistics = <div className="bh-chart-block-stats"><Stat label={block === "grid" ? "Total sessions" : "Total sales"}
105
+ value={block === "grid" ? number(total(data, key)) : money(total(data, key))} />
106
+ <Stat label={block === "grid" ? "Peak hour" : "Top month"} value={String(peak.name)} detail={`${number(numeric(peak, key))} ${key}`} /></div>
107
+ }
108
+ const chart = <BlockChart block={block} spec={spec} selected={selected} onSelect={block === "latency" ? onSelect : undefined} />
109
+ return <>{block === "payouts" ? <>{chart}{statistics}</> : <>{statistics}{chart}</>}</>
110
+ }
111
+
112
+ function ValueList({ data, format, columns = false, selected, onSelect }: {
113
+ data: ChartDatum[]; format: (row: ChartDatum) => string; columns?: boolean; selected?: string | null; onSelect?: (key: string) => void
114
+ }) {
115
+ return <div className="bh-chart-block-list" data-columns={columns ? "2" : undefined}>{data.map((row, index) => {
116
+ const content = <><Swatch index={index} /><span>{String(row.name)}</span><strong>{format(row)}</strong></>
117
+ return onSelect ? <button key={String(row.name)} className="bh-chart-block-list-row" type="button" aria-pressed={selected === row.name}
118
+ data-muted={Boolean(selected && selected !== row.name)} onClick={() => onSelect(String(row.name))}>{content}</button>
119
+ : <div key={String(row.name)} className="bh-chart-block-list-row">{content}</div>
120
+ })}</div>
121
+ }
122
+
123
+ function Ring({ label, value, max = 100, index, dotted = false, children }: {
124
+ label: string; value: number; max?: number; index: number; dotted?: boolean; children?: React.ReactNode
125
+ }) {
126
+ const option = React.useCallback((theme: EChartTheme): EChartsOption => {
127
+ const result = createChartOption(theme, { family: "radial", data: [{ name: label, value }], max, legend: false, series: [{ key: "value", colorIndex: index }] })
128
+ if (!dotted) return { ...result, polar: (Array.isArray(result.polar) ? result.polar : [result.polar]).map((polar) => ({ ...polar, radius: ["65%", "87%"], center: ["50%", "50%"] })) }
129
+ const filled = Math.round(value / max * 40)
130
+ return { ...result, tooltip: { show: false }, series: [{
131
+ type: "pie", radius: ["83%", "91%"], center: ["50%", "50%"], silent: true, animation: false,
132
+ label: { show: false }, labelLine: { show: false }, emphasis: { disabled: true },
133
+ data: Array.from({ length: 80 }, (_, position) => ({ value: 1, itemStyle: {
134
+ color: position % 2 ? "transparent" : position / 2 < filled ? theme.palette[index] : theme.neutral,
135
+ borderWidth: 0, borderRadius: theme.markRadius, shadowBlur: 0,
136
+ } })),
137
+ }] }
138
+ }, [dotted, index, label, max, value])
139
+ return <div className="bh-chart-block-ring"><EChart option={option} label={`${label}: ${number(value)} of ${number(max)}`} />
140
+ <div className="bh-chart-block-ring-center">{children ?? <strong>{number(value)}%</strong>}</div></div>
141
+ }
142
+
143
+ function PieBlock({ block, spec, selected, onSelect }: { block: ChartBlockId; spec: ChartSpec; selected: string | null; onSelect: (key: string) => void }) {
144
+ if (block === "progress-rings") return <div className="bh-chart-block-research-rings">{spec.data.map((row, index) => <div key={String(row.name)}>
145
+ <Ring label={String(row.name)} value={numeric(row, "value")} index={index} dotted><strong>{row.value}%</strong><span>{row.name}</span></Ring>
146
+ </div>)}</div>
147
+ const revenue = block === "revenue-mix"
148
+ const amount = total(spec.data, "value")
149
+ return <div className={revenue ? "bh-chart-block-split" : "bh-chart-block-donut-layout"}>
150
+ <div className="bh-chart-block-donut"><BlockChart block={block} spec={spec} selected={selected} onSelect={revenue ? undefined : onSelect} />
151
+ <div className="bh-chart-block-ring-center"><strong>{revenue ? money(amount / 1000) + "k" : money(amount) + "B"}</strong><span>{revenue ? "Total revenue" : "Ecosystem value"}</span></div>
152
+ </div>
153
+ <ValueList data={spec.data} columns={!revenue} selected={selected} onSelect={revenue ? undefined : onSelect}
154
+ format={(row) => revenue ? money(numeric(row, "value")) : `${money(numeric(row, "value"))}B · ${number(numeric(row, "value") / amount * 100)}%`} />
155
+ </div>
156
+ }
157
+
158
+ function ReliabilityBlock({ spec }: { spec: ChartSpec }) {
159
+ const bandColors = [3, 2, 4, 1]
160
+ const option = React.useCallback((theme: EChartTheme): EChartsOption => ({
161
+ ...createChartOption(theme, { ...spec, legend: false }), tooltip: { show: false }, series: [{
162
+ type: "pie", radius: ["69%", "87%"], startAngle: 210, endAngle: -30, clockwise: true,
163
+ padAngle: 4, silent: true, label: { show: false }, labelLine: { show: false },
164
+ itemStyle: { borderRadius: theme.markRadius, shadowBlur: 0 },
165
+ data: spec.data.map((row, index) => ({ name: String(row.name), value: numeric(row, "value"), itemStyle: { color: theme.palette[bandColors[index]] } })),
166
+ }],
167
+ }), [spec])
168
+ return <><div className="bh-chart-block-score"><EChart option={option} label="Reliability score 842 of 1000, excellent" />
169
+ <div className="bh-chart-block-ring-center"><strong>842</strong><span>out of 1,000</span></div></div>
170
+ <p className="bh-chart-block-score-caption">Reliability is <strong>excellent</strong></p>
171
+ <div className="bh-chart-block-range">{spec.data.map((row, index) => <div key={String(row.name)} style={{ flexGrow: numeric(row, "value") }}>
172
+ <span>{row.from}</span><i style={{ backgroundColor: `var(${colorTokens[bandColors[index]]})` }} /><span>{row.name}</span>
173
+ </div>)}<span>1,000</span></div>
174
+ </>
175
+ }
176
+
177
+ function BudgetBlock({ spec }: { spec: ChartSpec }) {
178
+ return <><Stat label="Monthly allocation" value={money(total(spec.data, "amount"))} />
179
+ <div className="bh-chart-block-budget-rings">{spec.data.map((row, index) => <div key={String(row.name)}>
180
+ <Ring label={String(row.name)} value={numeric(row, "value")} index={index} /><span>{row.name}</span>
181
+ </div>)}</div><ValueList data={spec.data} format={(row) => `${row.value}% · ${money(numeric(row, "amount"))}`} /></>
182
+ }
183
+
184
+ function RideBlock({ spec }: { spec: ChartSpec }) {
185
+ const [distance, ...metrics] = spec.data
186
+ const icons = [Mountain, Zap, Gauge]
187
+ return <div className="bh-chart-block-ride"><div className="bh-chart-block-ride-main">
188
+ <div className="bh-chart-block-ride-distance"><Bike aria-hidden="true" /><Stat label="Distance ridden" value={<>{distance.value}<small>km</small></>} detail={`Goal: ${distance.goal} km`} /></div>
189
+ <Progress aria-label="Distance goal" value={numeric(distance, "value")} max={numeric(distance, "goal")} labelPosition="none" showIndicator={false} />
190
+ <div className="bh-chart-block-metric-rings">{metrics.map((row, index) => {
191
+ const Icon = icons[index]
192
+ return <div key={String(row.name)}><Ring label={String(row.name)} value={numeric(row, "value")} max={numeric(row, "goal")} index={index + 1}><Icon aria-hidden="true" /></Ring>
193
+ <strong>{row.value} <span>{row.unit}</span></strong><span>{row.name}</span><small>Goal {row.goal} {row.unit}</small></div>
194
+ })}</div>
195
+ </div><div className="bh-chart-block-ride-splits"><Stat label="Sprints" value="7" detail="efforts" /><Stat label="Recovery" value="46" detail="minutes" /></div></div>
196
+ }
197
+
198
+ function CacheBlock({ spec }: { spec: ChartSpec }) {
199
+ return <><div className="bh-chart-block-split"><BlockChart block="cache-tiers" spec={spec} />
200
+ <div className="bh-chart-block-stats" data-columns="2"><Stat label="Served warm" value="9,150" /><Stat label="Revalidated" value="1,280" /><Stat label="Evictions" value="412" /><Stat label="Purges" value="96" /></div>
201
+ </div><ValueList data={spec.data} columns format={(row) => `${number(numeric(row, "count"))}/1,000 · ${row.value}%`} /></>
202
+ }
203
+
204
+ function FlowBlock({ block, spec }: { block: "allocation" | "pipeline"; spec: ChartSpec }) {
205
+ const amount = spec.links!.filter((link) => block === "pipeline" ? link.target === 4 : link.source === 0).reduce((sum, link) => sum + link.value, 0)
206
+ return <><div className="bh-chart-block-flow-viewport" tabIndex={0} role="region" aria-label={`${chartBlockDefinitions[block].title} flow diagram`}>
207
+ <div className={cn("bh-chart-block-flow-canvas", block === "pipeline" && "bh-chart-block-pipeline")}>
208
+ <BlockChart block={block} spec={spec} />
209
+ {block === "pipeline" && <div className="bh-chart-block-flow-total"><Stat label="Total booked" value={money(amount)} detail="4 sources · 4 routes" /></div>}
210
+ </div></div>
211
+ {block === "allocation" && <div className="bh-chart-block-stats" data-columns="3"><Stat label="Total inflow" value={money(amount) + "k"} />
212
+ <Stat label="Asset classes" value="3" /><Stat label="Destinations" value="3" /></div>}
213
+ </>
214
+ }
215
+
216
+ function BlockDataTable({ block, spec, period }: { block: ChartBlockId; spec: ChartSpec; period: string }) {
217
+ const rows: ChartDatum[] = spec.family === "sankey" ? spec.links!.map((link) => ({
218
+ source: typeof link.source === "number" ? spec.nodes![link.source].name : link.source,
219
+ target: typeof link.target === "number" ? spec.nodes![link.target].name : link.target,
220
+ value: link.value,
221
+ })) : spec.data
222
+ const keys = Object.keys(rows[0] ?? {})
223
+ const labels: Record<string, string> = { name: "Category / time", value: "Value", goal: "Goal", unit: "Unit", from: "Band starts at", amount: "Amount (USD)", count: "Requests", source: "Source", target: "Target" }
224
+ spec.series?.forEach((item) => { labels[item.key] = item.label ?? item.key })
225
+ if (spec.family === "sankey") labels.value = block === "allocation" ? "Amount (USD thousands)" : "Amount (USD)"
226
+ return <details className="bh-chart-block-data"><summary>View data table</summary><div className="bh-chart-block-table-scroll" tabIndex={0} role="region" aria-label={`${chartBlockDefinitions[block].title} data`}>
227
+ <table><caption>{chartBlockDefinitions[block].title} · {period === "All" ? "All available samples" : period} · Illustrative data</caption>
228
+ <thead><tr>{keys.map((key) => <th key={key} scope="col">{labels[key] ?? key}</th>)}</tr></thead>
229
+ <tbody>{rows.map((row, index) => <tr key={index}>{keys.map((key, column) => column === 0 ? <th scope="row" key={key}>{row[key]}</th> : <td key={key}>{typeof row[key] === "number" ? number(row[key]) : row[key]}</td>)}</tr>)}</tbody>
230
+ </table>
231
+ </div></details>
232
+ }
233
+
234
+ /** Ready-made examples: shared chart behavior, Banhaten tokens, and inspectable data. */
235
+ export function ChartBlock({ block, className, ...props }: ChartBlockProps) {
236
+ const definition: ChartBlockDefinition = chartBlockDefinitions[block]
237
+ const [period, setPeriod] = React.useState("All")
238
+ const [selection, setSelection] = React.useState<{ block: ChartBlockId; key: string } | null>(null)
239
+ const selected = selection?.block === block ? selection.key : null
240
+ const effectivePeriod = definition.periods?.some((entry) => entry.label === period) ? period : "All"
241
+ const spec = React.useMemo(() => getChartBlockSpec(block, effectivePeriod), [block, effectivePeriod])
242
+ const titleId = React.useId()
243
+ const select = (key: string) => setSelection(selected === key ? null : { block, key })
244
+ let content: React.ReactNode
245
+ if (definition.periods) content = <TimeSeriesBlock block={block} spec={spec} selected={selected} onSelect={select} />
246
+ else if (block === "reliability-score") content = <ReliabilityBlock spec={spec} />
247
+ else if (spec.family === "pie") content = <PieBlock block={block} spec={spec} selected={selected} onSelect={select} />
248
+ else if (block === "budget") content = <BudgetBlock spec={spec} />
249
+ else if (block === "ride") content = <RideBlock spec={spec} />
250
+ else if (block === "cache-tiers") content = <CacheBlock spec={spec} />
251
+ else if (block === "allocation" || block === "pipeline") content = <FlowBlock block={block} spec={spec} />
252
+ return <article className={cn("bh-chart-block", className)} data-slot="chart-block" data-block={block} aria-labelledby={titleId} {...props}>
253
+ <header className="bh-chart-block-header"><div><h3 id={titleId}>{definition.title}</h3><p>{definition.description}</p></div>
254
+ {definition.periods && <div className="bh-chart-block-periods" role="group" aria-label={`${definition.title} period`}>{definition.periods.map((entry) =>
255
+ <Button key={entry.label} type="button" size="xs" variant={effectivePeriod === entry.label ? "soft" : "ghost"} aria-pressed={effectivePeriod === entry.label} onClick={() => setPeriod(entry.label)}>{entry.label}</Button>)}</div>}
256
+ </header>
257
+ <div className="bh-chart-block-body">{content}</div>
258
+ <footer className="bh-chart-block-footer"><span>Illustrative data</span><BlockDataTable block={block} spec={spec} period={effectivePeriod} /></footer>
259
+ </article>
260
+ }