@seekalfred/primitives 0.6.0 → 0.7.3

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 (346) hide show
  1. package/README.md +214 -15
  2. package/dist/activity-heatmap/activity-heatmap.d.ts +92 -0
  3. package/dist/activity-heatmap/activity-heatmap.js +699 -0
  4. package/dist/activity-heatmap/activity-heatmap.js.map +1 -0
  5. package/dist/activity-heatmap/index.d.ts +2 -0
  6. package/dist/activity-heatmap/index.js +2 -0
  7. package/dist/alert/alert.d.ts +53 -4
  8. package/dist/alert/alert.js +100 -11
  9. package/dist/alert/alert.js.map +1 -1
  10. package/dist/alert/index.d.ts +2 -2
  11. package/dist/alert/index.js +2 -2
  12. package/dist/alert-dialog/alert-dialog.d.ts +58 -18
  13. package/dist/alert-dialog/alert-dialog.js +152 -9
  14. package/dist/alert-dialog/alert-dialog.js.map +1 -1
  15. package/dist/alert-dialog/index.d.ts +2 -2
  16. package/dist/alert-dialog/index.js +2 -2
  17. package/dist/animated-counter/animated-counter.d.ts +38 -0
  18. package/dist/animated-counter/animated-counter.js +230 -0
  19. package/dist/animated-counter/animated-counter.js.map +1 -0
  20. package/dist/animated-counter/index.d.ts +2 -0
  21. package/dist/animated-counter/index.js +2 -0
  22. package/dist/announcement-bar/announcement-bar.d.ts +67 -0
  23. package/dist/announcement-bar/announcement-bar.js +364 -0
  24. package/dist/announcement-bar/announcement-bar.js.map +1 -0
  25. package/dist/announcement-bar/index.d.ts +2 -0
  26. package/dist/announcement-bar/index.js +2 -0
  27. package/dist/badge/badge.d.ts +1 -1
  28. package/dist/bar-chart/bar-chart.d.ts +51 -0
  29. package/dist/bar-chart/bar-chart.js +565 -0
  30. package/dist/bar-chart/bar-chart.js.map +1 -0
  31. package/dist/bar-chart/index.d.ts +2 -0
  32. package/dist/bar-chart/index.js +2 -0
  33. package/dist/breadcrumb/breadcrumb.d.ts +2 -2
  34. package/dist/breadcrumb/breadcrumb.js +12 -3
  35. package/dist/breadcrumb/breadcrumb.js.map +1 -1
  36. package/dist/brush-chart/brush-chart.d.ts +64 -0
  37. package/dist/brush-chart/brush-chart.js +1056 -0
  38. package/dist/brush-chart/brush-chart.js.map +1 -0
  39. package/dist/brush-chart/index.d.ts +2 -0
  40. package/dist/brush-chart/index.js +2 -0
  41. package/dist/button/button.d.ts +1 -1
  42. package/dist/button/button.js +31 -21
  43. package/dist/button/button.js.map +1 -1
  44. package/dist/button-group/button-group.js +3 -4
  45. package/dist/button-group/button-group.js.map +1 -1
  46. package/dist/calendar/calendar.d.ts +1 -1
  47. package/dist/card/card.d.ts +22 -1
  48. package/dist/card/card.js +60 -7
  49. package/dist/card/card.js.map +1 -1
  50. package/dist/card-stack/card-stack.d.ts +54 -0
  51. package/dist/card-stack/card-stack.js +492 -0
  52. package/dist/card-stack/card-stack.js.map +1 -0
  53. package/dist/card-stack/index.d.ts +2 -0
  54. package/dist/card-stack/index.js +2 -0
  55. package/dist/carousel/carousel.d.ts +77 -0
  56. package/dist/carousel/carousel.js +415 -0
  57. package/dist/carousel/carousel.js.map +1 -0
  58. package/dist/carousel/index.d.ts +2 -0
  59. package/dist/carousel/index.js +2 -0
  60. package/dist/checkbox/checkbox.js +4 -4
  61. package/dist/checkbox/checkbox.js.map +1 -1
  62. package/dist/chip-group/chip-group.d.ts +37 -0
  63. package/dist/chip-group/chip-group.js +148 -0
  64. package/dist/chip-group/chip-group.js.map +1 -0
  65. package/dist/chip-group/index.d.ts +2 -0
  66. package/dist/chip-group/index.js +2 -0
  67. package/dist/copy-button/copy-button.d.ts +18 -2
  68. package/dist/copy-button/copy-button.js +29 -8
  69. package/dist/copy-button/copy-button.js.map +1 -1
  70. package/dist/countdown/countdown.d.ts +45 -0
  71. package/dist/countdown/countdown.js +298 -0
  72. package/dist/countdown/countdown.js.map +1 -0
  73. package/dist/countdown/index.d.ts +2 -0
  74. package/dist/countdown/index.js +2 -0
  75. package/dist/date-picker/picker-parts.js +1 -1
  76. package/dist/date-picker/picker-parts.js.map +1 -1
  77. package/dist/dialog/dialog.js +1 -1
  78. package/dist/dialog/dialog.js.map +1 -1
  79. package/dist/donut-chart/donut-chart.d.ts +86 -0
  80. package/dist/donut-chart/donut-chart.js +593 -0
  81. package/dist/donut-chart/donut-chart.js.map +1 -0
  82. package/dist/donut-chart/index.d.ts +2 -0
  83. package/dist/donut-chart/index.js +2 -0
  84. package/dist/empty-state/empty-state.d.ts +42 -10
  85. package/dist/empty-state/empty-state.js +44 -26
  86. package/dist/empty-state/empty-state.js.map +1 -1
  87. package/dist/empty-state/index.d.ts +2 -2
  88. package/dist/empty-state/index.js +2 -2
  89. package/dist/file-drop-zone/file-drop-zone.js +1 -1
  90. package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
  91. package/dist/filter-bar/filter-bar.d.ts +50 -26
  92. package/dist/filter-bar/filter-bar.js +317 -157
  93. package/dist/filter-bar/filter-bar.js.map +1 -1
  94. package/dist/gauge/gauge.d.ts +45 -0
  95. package/dist/gauge/gauge.js +219 -0
  96. package/dist/gauge/gauge.js.map +1 -0
  97. package/dist/gauge/index.d.ts +2 -0
  98. package/dist/gauge/index.js +2 -0
  99. package/dist/in-view-title/in-view-title.d.ts +33 -0
  100. package/dist/in-view-title/in-view-title.js +119 -0
  101. package/dist/in-view-title/in-view-title.js.map +1 -0
  102. package/dist/in-view-title/index.d.ts +2 -0
  103. package/dist/in-view-title/index.js +2 -0
  104. package/dist/index.d.ts +53 -10
  105. package/dist/index.js +52 -12
  106. package/dist/input/input.js +4 -4
  107. package/dist/input/input.js.map +1 -1
  108. package/dist/lib/cn.js +11 -1
  109. package/dist/lib/cn.js.map +1 -1
  110. package/dist/lib/field-message.js +28 -10
  111. package/dist/lib/field-message.js.map +1 -1
  112. package/dist/lib/field-shell.js +10 -3
  113. package/dist/lib/field-shell.js.map +1 -1
  114. package/dist/lib/field-surface.js +31 -9
  115. package/dist/lib/field-surface.js.map +1 -1
  116. package/dist/lib/flag-emoji.js +50 -0
  117. package/dist/lib/flag-emoji.js.map +1 -0
  118. package/dist/lib/flip.js +11 -3
  119. package/dist/lib/flip.js.map +1 -1
  120. package/dist/lib/menu-highlight.js +64 -0
  121. package/dist/lib/menu-highlight.js.map +1 -0
  122. package/dist/lib/motion.js +9 -2
  123. package/dist/lib/motion.js.map +1 -1
  124. package/dist/lib/rolling-value.js +1 -1
  125. package/dist/lib/rolling-value.js.map +1 -1
  126. package/dist/lib/spring-tween.js +500 -0
  127. package/dist/lib/spring-tween.js.map +1 -0
  128. package/dist/lib/swap-text.js +3 -1
  129. package/dist/lib/swap-text.js.map +1 -1
  130. package/dist/lib/use-copy-feedback.js +17 -3
  131. package/dist/lib/use-copy-feedback.js.map +1 -1
  132. package/dist/line-chart/index.d.ts +2 -0
  133. package/dist/line-chart/index.js +2 -0
  134. package/dist/line-chart/line-chart.d.ts +116 -0
  135. package/dist/line-chart/line-chart.js +740 -0
  136. package/dist/line-chart/line-chart.js.map +1 -0
  137. package/dist/mention-input/index.d.ts +2 -0
  138. package/dist/mention-input/index.js +2 -0
  139. package/dist/mention-input/mention-input.d.ts +98 -0
  140. package/dist/mention-input/mention-input.js +576 -0
  141. package/dist/mention-input/mention-input.js.map +1 -0
  142. package/dist/menu/menu.d.ts +2 -0
  143. package/dist/menu/menu.js +23 -12
  144. package/dist/menu/menu.js.map +1 -1
  145. package/dist/money-input/index.d.ts +2 -0
  146. package/dist/money-input/index.js +2 -0
  147. package/dist/money-input/money-input.d.ts +86 -0
  148. package/dist/money-input/money-input.js +613 -0
  149. package/dist/money-input/money-input.js.map +1 -0
  150. package/dist/morph-select/index.d.ts +2 -0
  151. package/dist/morph-select/index.js +2 -0
  152. package/dist/morph-select/morph-select.d.ts +89 -0
  153. package/dist/morph-select/morph-select.js +712 -0
  154. package/dist/morph-select/morph-select.js.map +1 -0
  155. package/dist/multi-select/index.d.ts +2 -0
  156. package/dist/multi-select/index.js +2 -0
  157. package/dist/multi-select/multi-select.d.ts +77 -0
  158. package/dist/multi-select/multi-select.js +209 -0
  159. package/dist/multi-select/multi-select.js.map +1 -0
  160. package/dist/number-field/index.d.ts +2 -0
  161. package/dist/number-field/index.js +2 -0
  162. package/dist/number-field/number-field.d.ts +42 -0
  163. package/dist/number-field/number-field.js +199 -0
  164. package/dist/number-field/number-field.js.map +1 -0
  165. package/dist/onboarding-checklist/index.d.ts +2 -0
  166. package/dist/onboarding-checklist/index.js +2 -0
  167. package/dist/onboarding-checklist/onboarding-checklist.d.ts +55 -0
  168. package/dist/onboarding-checklist/onboarding-checklist.js +577 -0
  169. package/dist/onboarding-checklist/onboarding-checklist.js.map +1 -0
  170. package/dist/password-field/password-field.d.ts +16 -0
  171. package/dist/password-field/password-field.js +39 -10
  172. package/dist/password-field/password-field.js.map +1 -1
  173. package/dist/phone-input/index.d.ts +3 -0
  174. package/dist/phone-input/index.js +3 -0
  175. package/dist/phone-input/phone-countries.d.ts +29 -0
  176. package/dist/phone-input/phone-countries.js +170 -0
  177. package/dist/phone-input/phone-countries.js.map +1 -0
  178. package/dist/phone-input/phone-input.d.ts +66 -0
  179. package/dist/phone-input/phone-input.js +440 -0
  180. package/dist/phone-input/phone-input.js.map +1 -0
  181. package/dist/popover/index.d.ts +2 -2
  182. package/dist/popover/index.js +2 -2
  183. package/dist/popover/popover.d.ts +51 -2
  184. package/dist/popover/popover.js +59 -8
  185. package/dist/popover/popover.js.map +1 -1
  186. package/dist/progress/progress.d.ts +16 -2
  187. package/dist/progress/progress.js +16 -9
  188. package/dist/progress/progress.js.map +1 -1
  189. package/dist/radio-group/index.d.ts +2 -2
  190. package/dist/radio-group/index.js +2 -2
  191. package/dist/radio-group/radio-group.d.ts +33 -3
  192. package/dist/radio-group/radio-group.js +110 -10
  193. package/dist/radio-group/radio-group.js.map +1 -1
  194. package/dist/reactions/index.d.ts +2 -0
  195. package/dist/reactions/index.js +2 -0
  196. package/dist/reactions/reactions.d.ts +50 -0
  197. package/dist/reactions/reactions.js +315 -0
  198. package/dist/reactions/reactions.js.map +1 -0
  199. package/dist/rich-text-editor/index.d.ts +3 -0
  200. package/dist/rich-text-editor/index.js +2 -0
  201. package/dist/rich-text-editor/rich-text-editor.d.ts +70 -0
  202. package/dist/rich-text-editor/rich-text-editor.js +909 -0
  203. package/dist/rich-text-editor/rich-text-editor.js.map +1 -0
  204. package/dist/rich-text-editor/rich-text-engine.d.ts +16 -0
  205. package/dist/rich-text-editor/rich-text-engine.js +366 -0
  206. package/dist/rich-text-editor/rich-text-engine.js.map +1 -0
  207. package/dist/rich-text-editor/rich-text-html.js +174 -0
  208. package/dist/rich-text-editor/rich-text-html.js.map +1 -0
  209. package/dist/rich-text-editor/rich-text-icons.js +43 -0
  210. package/dist/rich-text-editor/rich-text-icons.js.map +1 -0
  211. package/dist/ridgeline/index.d.ts +2 -0
  212. package/dist/ridgeline/index.js +2 -0
  213. package/dist/ridgeline/ridgeline.d.ts +55 -0
  214. package/dist/ridgeline/ridgeline.js +701 -0
  215. package/dist/ridgeline/ridgeline.js.map +1 -0
  216. package/dist/scroll-area/index.d.ts +2 -2
  217. package/dist/scroll-area/index.js +2 -2
  218. package/dist/scroll-area/scroll-area.d.ts +68 -31
  219. package/dist/scroll-area/scroll-area.js +212 -62
  220. package/dist/scroll-area/scroll-area.js.map +1 -1
  221. package/dist/scrub-input/evaluate.d.ts +14 -0
  222. package/dist/scrub-input/evaluate.js +72 -0
  223. package/dist/scrub-input/evaluate.js.map +1 -0
  224. package/dist/scrub-input/index.d.ts +3 -0
  225. package/dist/scrub-input/index.js +3 -0
  226. package/dist/scrub-input/scrub-input.d.ts +74 -0
  227. package/dist/scrub-input/scrub-input.js +486 -0
  228. package/dist/scrub-input/scrub-input.js.map +1 -0
  229. package/dist/search-field/search-field.d.ts +24 -2
  230. package/dist/search-field/search-field.js +24 -9
  231. package/dist/search-field/search-field.js.map +1 -1
  232. package/dist/select/select.d.ts +3 -1
  233. package/dist/select/select.js +28 -14
  234. package/dist/select/select.js.map +1 -1
  235. package/dist/separator/separator.d.ts +22 -10
  236. package/dist/separator/separator.js +33 -11
  237. package/dist/separator/separator.js.map +1 -1
  238. package/dist/sheet/sheet.js +2 -1
  239. package/dist/sheet/sheet.js.map +1 -1
  240. package/dist/shortcut-recorder/index.d.ts +3 -0
  241. package/dist/shortcut-recorder/index.js +3 -0
  242. package/dist/shortcut-recorder/shortcut-model.d.ts +27 -0
  243. package/dist/shortcut-recorder/shortcut-model.js +206 -0
  244. package/dist/shortcut-recorder/shortcut-model.js.map +1 -0
  245. package/dist/shortcut-recorder/shortcut-recorder.d.ts +93 -0
  246. package/dist/shortcut-recorder/shortcut-recorder.js +489 -0
  247. package/dist/shortcut-recorder/shortcut-recorder.js.map +1 -0
  248. package/dist/slider/index.d.ts +2 -2
  249. package/dist/slider/slider.d.ts +46 -14
  250. package/dist/slider/slider.js +151 -67
  251. package/dist/slider/slider.js.map +1 -1
  252. package/dist/slope-chart/index.d.ts +2 -0
  253. package/dist/slope-chart/index.js +2 -0
  254. package/dist/slope-chart/slope-chart.d.ts +54 -0
  255. package/dist/slope-chart/slope-chart.js +568 -0
  256. package/dist/slope-chart/slope-chart.js.map +1 -0
  257. package/dist/sparkline/index.d.ts +2 -0
  258. package/dist/sparkline/index.js +2 -0
  259. package/dist/sparkline/sparkline.d.ts +46 -0
  260. package/dist/sparkline/sparkline.js +390 -0
  261. package/dist/sparkline/sparkline.js.map +1 -0
  262. package/dist/split-bar/index.d.ts +2 -0
  263. package/dist/split-bar/index.js +2 -0
  264. package/dist/split-bar/split-bar.d.ts +62 -0
  265. package/dist/split-bar/split-bar.js +513 -0
  266. package/dist/split-bar/split-bar.js.map +1 -0
  267. package/dist/split-button/split-button.d.ts +1 -1
  268. package/dist/split-button/split-button.js +12 -11
  269. package/dist/split-button/split-button.js.map +1 -1
  270. package/dist/stat-card/index.d.ts +2 -2
  271. package/dist/stat-card/index.js +2 -2
  272. package/dist/stat-card/stat-card.d.ts +58 -5
  273. package/dist/stat-card/stat-card.js +144 -17
  274. package/dist/stat-card/stat-card.js.map +1 -1
  275. package/dist/stepper/index.d.ts +2 -0
  276. package/dist/stepper/index.js +2 -0
  277. package/dist/stepper/stepper.d.ts +51 -0
  278. package/dist/stepper/stepper.js +215 -0
  279. package/dist/stepper/stepper.js.map +1 -0
  280. package/dist/streamgraph/index.d.ts +2 -0
  281. package/dist/streamgraph/index.js +2 -0
  282. package/dist/streamgraph/streamgraph.d.ts +70 -0
  283. package/dist/streamgraph/streamgraph.js +613 -0
  284. package/dist/streamgraph/streamgraph.js.map +1 -0
  285. package/dist/styles.css +1 -1
  286. package/dist/swipe-actions/index.d.ts +2 -0
  287. package/dist/swipe-actions/index.js +2 -0
  288. package/dist/swipe-actions/swipe-actions.d.ts +67 -0
  289. package/dist/swipe-actions/swipe-actions.js +552 -0
  290. package/dist/swipe-actions/swipe-actions.js.map +1 -0
  291. package/dist/switch/switch.js +2 -2
  292. package/dist/switch/switch.js.map +1 -1
  293. package/dist/table/index.d.ts +2 -2
  294. package/dist/table/index.js +2 -2
  295. package/dist/table/table.d.ts +55 -3
  296. package/dist/table/table.js +340 -165
  297. package/dist/table/table.js.map +1 -1
  298. package/dist/tabs/tabs.js +2 -2
  299. package/dist/tabs/tabs.js.map +1 -1
  300. package/dist/tag-input/index.d.ts +2 -0
  301. package/dist/tag-input/index.js +2 -0
  302. package/dist/tag-input/tag-input.d.ts +40 -0
  303. package/dist/tag-input/tag-input.js +269 -0
  304. package/dist/tag-input/tag-input.js.map +1 -0
  305. package/dist/text-morph/index.d.ts +2 -0
  306. package/dist/text-morph/index.js +2 -0
  307. package/dist/text-morph/text-morph.d.ts +23 -0
  308. package/dist/text-morph/text-morph.js +21 -0
  309. package/dist/text-morph/text-morph.js.map +1 -0
  310. package/dist/text-reveal/index.d.ts +2 -0
  311. package/dist/text-reveal/index.js +2 -0
  312. package/dist/text-reveal/text-reveal.d.ts +27 -0
  313. package/dist/text-reveal/text-reveal.js +51 -0
  314. package/dist/text-reveal/text-reveal.js.map +1 -0
  315. package/dist/text-shimmer/index.d.ts +2 -0
  316. package/dist/text-shimmer/index.js +2 -0
  317. package/dist/text-shimmer/text-shimmer.d.ts +29 -0
  318. package/dist/text-shimmer/text-shimmer.js +29 -0
  319. package/dist/text-shimmer/text-shimmer.js.map +1 -0
  320. package/dist/time-picker/time-picker.d.ts +36 -1
  321. package/dist/time-picker/time-picker.js +35 -7
  322. package/dist/time-picker/time-picker.js.map +1 -1
  323. package/dist/toast/index.d.ts +2 -2
  324. package/dist/toast/index.js +2 -2
  325. package/dist/toast/toast.d.ts +7 -1
  326. package/dist/toast/toast.js +82 -40
  327. package/dist/toast/toast.js.map +1 -1
  328. package/dist/tooltip/tooltip.d.ts +9 -2
  329. package/dist/tooltip/tooltip.js +7 -7
  330. package/dist/tooltip/tooltip.js.map +1 -1
  331. package/dist/treemap/index.d.ts +2 -0
  332. package/dist/treemap/index.js +2 -0
  333. package/dist/treemap/treemap.d.ts +53 -0
  334. package/dist/treemap/treemap.js +699 -0
  335. package/dist/treemap/treemap.js.map +1 -0
  336. package/dist/usage-meter/index.d.ts +2 -0
  337. package/dist/usage-meter/index.js +2 -0
  338. package/dist/usage-meter/usage-meter.d.ts +80 -0
  339. package/dist/usage-meter/usage-meter.js +458 -0
  340. package/dist/usage-meter/usage-meter.js.map +1 -0
  341. package/dist/waffle-chart/index.d.ts +2 -0
  342. package/dist/waffle-chart/index.js +2 -0
  343. package/dist/waffle-chart/waffle-chart.d.ts +57 -0
  344. package/dist/waffle-chart/waffle-chart.js +426 -0
  345. package/dist/waffle-chart/waffle-chart.js.map +1 -0
  346. package/package.json +165 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-meter.js","names":[],"sources":["../../src/usage-meter/usage-meter.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type KeyboardEvent,\n type PointerEvent,\n} from \"react\";\n\nimport { Badge } from \"../badge/badge.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { useCountTo } from \"../lib/count-to.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { prefersReducedMotion, useIsomorphicLayoutEffect } from \"../lib/motion.js\";\nimport { FILL, live, retarget, SMOOTH, type Live } from \"../lib/spring-tween.js\";\nimport { useSvgId } from \"../lib/svg-id.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * UsageMeter: a fixed allowance, such as storage or seats, when people need to\n * see what takes the space and how close they are to the limit.\n *\n * The segments grow into place from the left once the meter is seen, each a\n * beat after the one before. After that every change re-flows them: a segment\n * that grows pushes the ones after it along in the same frame. The bar spans\n * the limit, or everything used once that is more; past the limit the overflow\n * keeps its colours but is struck through with a hatch, and a marker shows\n * where the limit was, so \"over\" never rests on colour alone. The headline and\n * every legend value count to their new number, and the status mark swaps its\n * words and icon while its width follows.\n *\n * Hover previews a category, focus previews it too, and a press pins it until\n * pressed again or Escape. The legend is the keyboard path: arrow keys move\n * between categories. The bar names every amount for assistive technology,\n * and a change of status (almost full, over, back under) is announced.\n *\n * Plain DOM and CSS; one small spring tween writes the segments straight to\n * their elements.\n */\nexport interface UsageMeterSegment {\n /** Stable identity. Keep it the same while the value changes, so the segment re-flows instead of being replaced. */\n id: string;\n label: string;\n value: number;\n}\n\nexport interface UsageMeterProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** What is measured, such as \"Workspace storage\". */\n label: string;\n /** Used amounts in display order. Up to four read clearly. */\n segments: UsageMeterSegment[];\n /** The plan limit, in the same unit as the segments. */\n limit: number;\n /** Unit after each number, such as \"GB\". */\n unit?: string;\n /** Digits after the decimal point. @default 1 */\n decimals?: number;\n /** Name of the unused share. @default \"Free\" */\n freeLabel?: string;\n /** Name of the share past the limit. @default \"Over limit\" */\n overLabel?: string;\n /** Share of the limit at which the meter warns that it is almost full. @default 0.9 */\n warnAt?: number;\n /**\n * The state as the caller (usually the backend) reads it, overriding the one\n * `warnAt` and the limit would give. `ok` and `warn` look as the meter's own\n * under-limit and almost-full states; `full` is the allowance used up\n * (\"Limit reached\"); `exhausted` is nothing left to draw on at all (\"None\n * left\"). Left out, the meter works it out itself.\n *\n * The bar and legend agree with it: under `full` or `exhausted` the free\n * share is shown as none (the bar fills, the free item reads 0), and past\n * the limit `ok` and `warn` give way to the meter's own over state, since\n * the hatch already says over.\n */\n status?: UsageMeterStatus;\n /**\n * The product's words for the status, such as \"Credits paused\". They replace\n * the badge's words (including \"Unlimited\"), the status in the bar's name and\n * the announcement; the tone and icon still follow the status.\n */\n statusLabel?: string;\n /**\n * No limit: the bar shows each category's share of what is used, with no\n * free share, limit marker or overflow, and the badge reads \"Unlimited\"\n * unless `statusLabel` is set. `limit`, `warnAt` and `status` are ignored.\n */\n unlimited?: boolean;\n}\n\n/**\n * A status the caller can impose: under the limit, almost full, allowance used\n * up, and nothing left at all.\n */\nexport type UsageMeterStatus = \"ok\" | \"warn\" | \"full\" | \"exhausted\";\n\ntype Status = \"ok\" | \"near\" | \"over\" | \"full\" | \"exhausted\";\n\n/** The imposed status as `data-status` names it: `warn` is the meter's own `near`. */\nconst IMPOSED: Record<UsageMeterStatus, Status> = {\n ok: \"ok\",\n warn: \"near\",\n full: \"full\",\n exhausted: \"exhausted\",\n};\n\n/** A segment stops two pixels short of the next, so neighbours read apart without an outline. */\nconst GAP = 2;\n/** Stagger of each segment's first growth. */\nconst STAGGER_MS = 70;\nconst formats = new Map<number, Intl.NumberFormat>();\nfunction numberFormat(decimals: number) {\n let format = formats.get(decimals);\n if (!format) {\n format = new Intl.NumberFormat(\"en-US\", {\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n });\n formats.set(decimals, format);\n }\n return format;\n}\n\n/** A number that counts to each new value on the smooth spring. useCountTo counts whole steps, so decimals are counted in units of the last place. */\nfunction Count({ value, decimals }: { value: number; decimals: number }) {\n const scale = 10 ** decimals;\n const shown = useCountTo(Math.round(value * scale));\n return <>{numberFormat(decimals).format(shown / scale)}</>;\n}\n\nfunction AlertCircleIcon() {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-3.5\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 8v4M12 16h.01\" />\n </svg>\n );\n}\n\nfunction AlertTriangleIcon() {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n className=\"size-3.5\"\n >\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n <path d=\"M12 9v4M12 17h.01\" />\n </svg>\n );\n}\n\nexport const UsageMeter = forwardRef<HTMLDivElement, UsageMeterProps>(function UsageMeter(\n {\n label,\n segments,\n limit,\n unit = \"\",\n decimals = 1,\n freeLabel = \"Free\",\n overLabel = \"Over limit\",\n warnAt = 0.9,\n status: imposed,\n statusLabel,\n unlimited = false,\n className,\n ...props\n },\n ref,\n) {\n const root = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, root), [ref]);\n const bar = useRef<HTMLDivElement>(null);\n const track = useRef<HTMLDivElement>(null);\n const marker = useRef<HTMLSpanElement>(null);\n const overflow = useRef<SVGSVGElement>(null);\n const legend = useRef<HTMLDivElement>(null);\n const titleId = useId();\n const hatchId = `hatch${useSvgId()}`;\n\n const round = (value: number) => Math.round(value * 10 ** decimals) / 10 ** decimals;\n const total = round(segments.reduce((sum, segment) => sum + segment.value, 0));\n const over = round(Math.max(0, total - limit));\n // What the numbers say (it decides the free or over share), and what the meter shows (it may be imposed).\n const past = !unlimited && total > limit;\n // Past the limit an imposed calm status would contradict the hatch, so the numbers win.\n const calm = imposed === \"ok\" || imposed === \"warn\";\n const status: Status = unlimited\n ? \"ok\"\n : imposed && !(past && calm)\n ? IMPOSED[imposed]\n : past\n ? \"over\"\n : total >= limit * warnAt\n ? \"near\"\n : \"ok\";\n // An imposed used-up status under the limit: no free share, and the bar spans what is used.\n const capped = !unlimited && !past && (status === \"full\" || status === \"exhausted\");\n const ceiling = capped ? total : limit;\n const free = round(Math.max(0, ceiling - total));\n const danger = status === \"over\" || status === \"full\" || status === \"exhausted\";\n const format = (value: number) =>\n `${numberFormat(decimals).format(value)}${unit ? ` ${unit}` : \"\"}`;\n const limitText = format(limit).replace(/\\.0+(?= |$)/, \"\");\n\n // Seen: the segments grow once a good part of the meter is in view.\n const [ready, setReady] = useState(false);\n useEffect(() => {\n const node = root.current;\n if (!node || prefersReducedMotion() || typeof IntersectionObserver === \"undefined\") {\n setReady(true);\n return;\n }\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (!entry?.isIntersecting) return;\n observer.disconnect();\n setReady(true);\n },\n { threshold: 0.4 },\n );\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n // One live value per segment id, kept across renders so a changed value springs from where it is.\n const [amounts] = useState(() => new Map<string, Live>());\n // Unlimited, the bar spans what is used: the limit springs to nothing and is never \"over\".\n // Used up, the bar spans what is used: the limit springs to the total and nothing is \"over\".\n const shownLimit = useRef(live(unlimited ? 0 : ceiling));\n const width = useRef(0);\n const bounded = useRef(!unlimited && !capped);\n // Before the paint effects below, which read it.\n useIsomorphicLayoutEffect(() => {\n bounded.current = !unlimited && !capped;\n });\n\n /** One writer: every segment's place and length, the limit marker and the overflow hatch. */\n const paint = useCallback(() => {\n const frame = track.current;\n if (!frame) return;\n const w = width.current;\n const nodes = frame.querySelectorAll<HTMLElement>(\"[data-segment]\");\n let used = 0;\n for (const node of nodes) used += amounts.get(node.dataset.segment!)?.value ?? 0;\n const limitNow = shownLimit.current.value;\n // The bar spans the limit, or everything used once that is more; both follow their springs, so crossing the limit is continuous.\n const span = Math.max(limitNow, used, 1e-6);\n let start = 0;\n for (const node of nodes) {\n const amount = amounts.get(node.dataset.segment!)?.value ?? 0;\n const x = (start / span) * w;\n const size = (amount / span) * w;\n const rest = w - x - size;\n const scale = w ? Math.max(0, size - Math.min(GAP, size, Math.max(0, rest))) / w : 0;\n node.style.transform = `translateX(${x.toFixed(2)}px) scaleX(${scale.toFixed(4)})`;\n start += amount;\n }\n const limitX = Math.round((limitNow / span) * w);\n if (marker.current) marker.current.style.translate = `${limitX}px 0`;\n if (overflow.current) overflow.current.style.clipPath = `inset(0 0 0 ${limitX + GAP}px)`;\n if (bar.current) {\n if (bounded.current && used > limitNow) bar.current.setAttribute(\"data-over\", \"\");\n else bar.current.removeAttribute(\"data-over\");\n }\n }, [amounts]);\n\n useIsomorphicLayoutEffect(() => {\n const node = track.current;\n if (!node) return;\n const read = () => {\n width.current = node.clientWidth;\n paint();\n };\n read();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(read);\n observer.observe(node);\n return () => observer.disconnect();\n }, [paint]);\n useIsomorphicLayoutEffect(paint);\n\n // Segments grow into place once seen, left to right; later changes re-flow every segment on one spring each.\n const values = segments.map((segment) => `${segment.id}:${segment.value}`).join(\"|\");\n const revealed = useRef(false);\n const latest = useRef(segments);\n useIsomorphicLayoutEffect(() => {\n latest.current = segments;\n });\n useEffect(() => {\n if (!ready) return;\n const first = !revealed.current;\n revealed.current = true;\n latest.current.forEach((segment, index) => {\n let amount = amounts.get(segment.id);\n if (!amount) {\n amount = live(0);\n amounts.set(segment.id, amount);\n }\n retarget(amount, segment.value, first ? FILL : SMOOTH, paint, first ? index * STAGGER_MS : 0);\n });\n // `values` stands for every segment id and value.\n }, [values, ready, paint, amounts]);\n useEffect(() => {\n retarget(shownLimit.current, unlimited ? 0 : ceiling, SMOOTH, paint);\n }, [ceiling, unlimited, paint]);\n useEffect(() => {\n const bound = shownLimit.current;\n return () => {\n for (const amount of amounts.values()) amount.stop();\n bound.stop();\n };\n }, [amounts]);\n\n // Hover previews a category, focus previews it too, and a press pins it until pressed again or Escape.\n const [hovered, setHovered] = useState<string | null>(null);\n const [focused, setFocused] = useState<string | null>(null);\n const [pinned, setPinned] = useState<string | null>(null);\n const [roving, setRoving] = useState(0);\n const items = [\n ...segments.map((segment) => ({ id: segment.id, label: segment.label, value: segment.value })),\n // Unlimited has no free share to name.\n ...(unlimited\n ? []\n : [{ id: \"free\", label: past ? overLabel : freeLabel, value: past ? over : free }]),\n ];\n // The list can shrink under the roving item (unlimited drops the free share), so keep it in range.\n const rove = Math.min(roving, Math.max(0, items.length - 1));\n const active = hovered ?? focused ?? pinned;\n const activeItem = items.find((item) => item.id === active) ?? null;\n\n const idAt = (clientX: number) => {\n const rect = track.current?.getBoundingClientRect();\n if (!rect?.width) return null;\n const at =\n ((clientX - rect.left) / rect.width) * (unlimited ? total : Math.max(ceiling, total));\n // Past the limit the hatch stands for the overage, so pointing there highlights it rather than the category beneath.\n if (!unlimited && at > ceiling) return \"free\";\n let end = 0;\n for (const segment of segments) {\n end += segment.value;\n if (at <= end) return segment.id;\n }\n return unlimited ? null : \"free\";\n };\n const onTrackMove = (event: PointerEvent<HTMLDivElement>) => {\n if (event.pointerType === \"mouse\") setHovered(idAt(event.clientX));\n };\n const onTrackTap = (event: PointerEvent<HTMLDivElement>) => {\n if (event.pointerType === \"mouse\") return;\n const id = idAt(event.clientX);\n setPinned((current) => (current === id ? null : id));\n };\n const onLegendKey = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"Escape\") {\n if (pinned || focused) {\n event.preventDefault();\n setPinned(null);\n setFocused(null);\n }\n return;\n }\n const next = (\n {\n ArrowRight: rove + 1,\n ArrowDown: rove + 1,\n ArrowLeft: rove - 1,\n ArrowUp: rove - 1,\n Home: 0,\n End: items.length - 1,\n } as Record<string, number>\n )[event.key];\n if (next === undefined || !items.length) return;\n event.preventDefault();\n const index = (next + items.length) % items.length;\n setRoving(index);\n legend.current?.querySelectorAll<HTMLButtonElement>(\"button\")[index]?.focus();\n };\n\n const headline = ready ? (activeItem ? activeItem.value : total) : 0;\n // Unlimited never shows the share, and a zero limit has none.\n const share =\n activeItem && !unlimited && limit > 0 ? Math.round((activeItem.value / limit) * 100) : 0;\n const caption = !activeItem\n ? unlimited\n ? `${unit} used · no limit`\n : `${unit} of ${limitText} used`\n : activeItem.id === \"free\"\n ? `${unit} ${past ? \"over the limit\" : \"free\"}`\n : unlimited\n ? `${unit} in ${activeItem.label}`\n : `${unit} in ${activeItem.label} · ${share}% of plan`;\n const badge =\n statusLabel ??\n (unlimited\n ? \"Unlimited\"\n : status === \"over\"\n ? `${format(over)} over`\n : status === \"near\"\n ? \"Almost full\"\n : status === \"full\"\n ? \"Limit reached\"\n : status === \"exhausted\"\n ? \"None left\"\n : `${format(free)} free`);\n const breakdown = segments\n .map((segment) => `${segment.label} ${format(segment.value)}`)\n .join(\", \");\n // statusLabel is the status in words wherever the meter states it: badge, bar name and announcement.\n const labelled = statusLabel ? ` ${statusLabel}.` : \"\";\n const summary = unlimited\n ? `${label}: ${format(total)} used, no limit. ${breakdown}.${labelled}`\n : `${label}: ${format(total)} of ${limitText} used. ${breakdown}. ${\n status === \"over\"\n ? `Over the limit by ${format(over)}.`\n : `${format(free)} free${\n statusLabel\n ? \"\"\n : status === \"near\"\n ? \", almost full\"\n : status === \"full\"\n ? \", limit reached\"\n : status === \"exhausted\"\n ? \", none left\"\n : \"\"\n }.`\n }${labelled}`;\n\n // Status changes are announced, and so is a growing overage; routine changes inside a status stay quiet.\n const notice = `${unlimited ? \"unlimited\" : status === \"over\" ? `over:${over}` : status}|${statusLabel ?? \"\"}`;\n const [announced, setAnnounced] = useState({ notice, message: \"\" });\n if (announced.notice !== notice) {\n setAnnounced({\n notice,\n message: unlimited\n ? `No limit.${labelled}`\n : status === \"over\"\n ? `Over the plan limit by ${format(over)}.${labelled}`\n : statusLabel\n ? `${statusLabel}. ${format(free)} free.`\n : status === \"near\"\n ? `Almost full. ${format(free)} free.`\n : status === \"full\"\n ? \"Limit reached.\"\n : status === \"exhausted\"\n ? \"None left.\"\n : `Under the limit. ${format(free)} free.`,\n });\n }\n\n return (\n <div\n ref={refs}\n {...props}\n role=\"group\"\n aria-labelledby={titleId}\n data-slot=\"usage-meter\"\n data-status={status}\n data-unlimited={unlimited || undefined}\n className={cn(\"usage-meter\", className)}\n >\n <div className=\"flex min-h-7 min-w-0 items-center justify-between gap-3\">\n <span id={titleId} className=\"min-w-0 text-base font-medium text-foreground text-balance\">\n {label}\n </span>\n <Badge\n tone={danger ? \"danger\" : status === \"near\" ? \"warning\" : \"neutral\"}\n shape=\"pill\"\n size=\"lg\"\n icon={\n danger ? <AlertTriangleIcon /> : status === \"near\" ? <AlertCircleIcon /> : undefined\n }\n >\n {badge}\n </Badge>\n </div>\n <p className=\"m-0 flex min-w-0 items-baseline gap-2\">\n <span className=\"sr-only\">\n {unlimited ? `${format(total)} used, no limit` : `${format(total)} of ${limitText} used`}\n </span>\n <span\n data-slot=\"usage-meter-amount\"\n aria-hidden=\"true\"\n className=\"flex-none font-display text-h2 font-medium tabular-nums\"\n >\n <Count value={headline} decimals={decimals} />\n </span>\n <span aria-hidden=\"true\" className=\"min-w-0 flex-1 text-sm text-text-secondary\">\n <SwapText swapKey={caption} className=\"block truncate\">\n {caption}\n </SwapText>\n </span>\n </p>\n <div ref={bar} className=\"usage-meter-bar\" data-slot=\"usage-meter-bar\">\n <div\n ref={track}\n role=\"img\"\n aria-label={summary}\n data-active={active ?? undefined}\n onPointerMove={onTrackMove}\n onPointerLeave={() => setHovered(null)}\n onPointerUp={onTrackTap}\n className=\"usage-meter-track\"\n >\n {segments.map((segment, index) => (\n <span\n key={segment.id}\n data-segment={segment.id}\n data-index={index}\n data-highlight={active ? (active === segment.id ? \"on\" : \"off\") : undefined}\n className=\"usage-meter-segment\"\n />\n ))}\n {/* Past the limit the bar is struck through: the overflow keeps its colours, hatched so it reads as over without colour. */}\n {unlimited ? null : (\n <svg\n ref={overflow}\n aria-hidden=\"true\"\n focusable=\"false\"\n data-highlight={active ? (active === \"free\" ? \"on\" : \"off\") : undefined}\n className=\"usage-meter-overflow\"\n >\n <defs>\n <pattern\n id={hatchId}\n width={6}\n height={6}\n patternUnits=\"userSpaceOnUse\"\n patternTransform=\"rotate(45)\"\n >\n <rect width={2.5} height={6} className=\"usage-meter-hatch\" />\n </pattern>\n </defs>\n <rect width=\"100%\" height=\"100%\" className=\"usage-meter-over-tint\" />\n <rect width=\"100%\" height=\"100%\" fill={`url(#${hatchId})`} />\n </svg>\n )}\n </div>\n {unlimited ? null : <span ref={marker} aria-hidden=\"true\" className=\"usage-meter-limit\" />}\n </div>\n <div\n ref={legend}\n role=\"group\"\n aria-label={`${label} by category`}\n onKeyDown={onLegendKey}\n className=\"usage-meter-legend\"\n >\n {items.map((item, index) => {\n const warning = item.id === \"free\" && past;\n return (\n <button\n key={item.id}\n type=\"button\"\n data-slot=\"usage-meter-item\"\n data-highlight={active ? (active === item.id ? \"on\" : \"off\") : undefined}\n aria-pressed={pinned === item.id}\n aria-label={`${item.label}, ${format(item.value)}`}\n tabIndex={index === rove ? 0 : -1}\n onClick={() => {\n setRoving(index);\n setPinned((current) => (current === item.id ? null : item.id));\n }}\n onFocus={(event) => {\n setRoving(index);\n if (event.currentTarget.matches(\":focus-visible\")) setFocused(item.id);\n }}\n onBlur={() => setFocused(null)}\n onPointerEnter={(event) => {\n if (event.pointerType === \"mouse\") setHovered(item.id);\n }}\n onPointerLeave={(event) => {\n if (event.pointerType === \"mouse\") setHovered(null);\n }}\n className=\"usage-meter-item\"\n >\n <span className=\"flex min-w-0 max-w-full items-center gap-2 text-sm text-text-secondary\">\n <span aria-hidden=\"true\" className=\"usage-meter-key\">\n <SwapText\n swapKey={warning ? \"alert\" : \"swatch\"}\n motion=\"glyph\"\n className=\"grid place-items-center\"\n >\n {warning ? (\n <span className=\"text-destructive-strong\">\n <AlertTriangleIcon />\n </span>\n ) : (\n <span\n data-index={item.id === \"free\" ? \"free\" : index}\n className=\"usage-meter-swatch\"\n />\n )}\n </SwapText>\n </span>\n <span className=\"min-w-0 flex-1\">\n <SwapText swapKey={item.label} className=\"block truncate\">\n {item.label}\n </SwapText>\n </span>\n </span>\n <span\n aria-hidden=\"true\"\n className=\"flex items-baseline gap-1 ps-5 text-base font-medium text-foreground tabular-nums\"\n >\n <Count value={item.value} decimals={decimals} />\n {unit ? (\n <span className=\"text-sm font-normal text-text-secondary\">{unit}</span>\n ) : null}\n </span>\n </button>\n );\n })}\n </div>\n <p className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n {announced.message}\n </p>\n </div>\n );\n});\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { WaffleCategory, WaffleChart, WaffleChartProps } from "./waffle-chart.js";
2
+ export { type WaffleCategory, WaffleChart, type WaffleChartProps };
@@ -0,0 +1,2 @@
1
+ import { WaffleChart } from "./waffle-chart.js";
2
+ export { WaffleChart };
@@ -0,0 +1,57 @@
1
+ //#region src/waffle-chart/waffle-chart.d.ts
2
+ /**
3
+ * WaffleChart: parts of a whole as countable squares. Every cell is one share of
4
+ * the total (a 10 by 10 grid is one cell per percent), filled column by column
5
+ * from the bottom-left corner like a glass filling up.
6
+ *
7
+ * The cells cascade in once the chart is a third in view. New data hands cells
8
+ * to categories while moving as few as possible: each cell that changes category
9
+ * travels to its new place (dipping as it flies) and changes colour on the same
10
+ * stagger. Hovering a cell, a legend row or arrowing through the grid lifts that
11
+ * category and dims the rest; the legend shares count to their new values.
12
+ * Neutral categories differ in shape as well as lightness (square, round,
13
+ * framed), so colour is never the only channel.
14
+ *
15
+ * Plain CSS transitions place the cells, with nothing measured: a cell's place is
16
+ * two unitless numbers and its size comes from the width of the grid.
17
+ */
18
+ export interface WaffleCategory {
19
+ /** Stable identity. Cells keep following their category when the data changes, so a new dataset flows instead of repainting. */
20
+ key: string;
21
+ /** Name in the legend, the tooltip and the data table. */
22
+ label: string;
23
+ /** Raw amount. Shares are computed from the total; negatives count as zero. */
24
+ value: number;
25
+ /** Any CSS colour. Defaults to chart-1 for the accent, chart-2 to chart-4 for the next three, then neutral steps. */
26
+ color?: string;
27
+ }
28
+ export interface WaffleChartProps {
29
+ /** Categories in fill order. The first fills from the bottom-left corner, column by column. */
30
+ data: WaffleCategory[];
31
+ /** What the whole is, such as "Electricity generation, 2023". Names the chart for assistive technology. */
32
+ label: string;
33
+ /** Unit after each raw value in the tooltip and table, such as "TWh". */
34
+ unit?: string;
35
+ formatValue?: (value: number, category: WaffleCategory) => string;
36
+ /** @default 10 */
37
+ rows?: number;
38
+ /** rows times columns cells make the whole; 10 by 10 is one cell per percent. @default 10 */
39
+ columns?: number;
40
+ /** The category painted in chart-1. Pass null to keep every category on its own step. Defaults to the first category. */
41
+ accentKey?: string | null;
42
+ /** Controlled pinned category. Others dim while one is pinned. */
43
+ activeKey?: string | null;
44
+ defaultActiveKey?: string | null;
45
+ onActiveChange?: (key: string | null) => void;
46
+ /** The category list beside or below the grid, with shares that count to their new value. @default true */
47
+ legend?: boolean;
48
+ /** Decimal places for shares. @default 0 */
49
+ decimals?: number;
50
+ /** @default "No data" */
51
+ emptyLabel?: string;
52
+ /** Extra classes for the root. Merged last. */
53
+ className?: string;
54
+ }
55
+ export declare const WaffleChart: import("react").ForwardRefExoticComponent<WaffleChartProps & import("react").RefAttributes<HTMLElement>>;
56
+ //#endregion
57
+ //# sourceMappingURL=waffle-chart.d.ts.map
@@ -0,0 +1,426 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { mergeRefs } from "../lib/merge-refs.js";
4
+ import { prefersReducedMotion, useIsomorphicLayoutEffect } from "../lib/motion.js";
5
+ import { useCountTo } from "../lib/count-to.js";
6
+ import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
7
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/waffle-chart/waffle-chart.tsx
9
+ /** Space between cells, in pixels; the same number is in the waffle-chart-grid utility. */
10
+ const GAP = 3;
11
+ /** Lightness steps for categories past the four series colours; they alternate so neighbours differ. */
12
+ const SHADES = [
13
+ 40,
14
+ 20,
15
+ 30,
16
+ 14
17
+ ];
18
+ /** The colour-free second channel. */
19
+ const SHAPES = [
20
+ "square",
21
+ "round",
22
+ "framed"
23
+ ];
24
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
25
+ const GROUPED = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
26
+ /** Whole cells per category by largest remainder, so the counts always add up to the grid. */
27
+ function allocate(data, total, cells) {
28
+ if (total <= 0) return data.map(() => 0);
29
+ const exact = data.map((item) => Math.max(0, item.value) / total * cells);
30
+ const counts = exact.map(Math.floor);
31
+ let left = cells - counts.reduce((sum, count) => sum + count, 0);
32
+ const order = exact.map((value, index) => ({
33
+ index,
34
+ rest: value - Math.floor(value)
35
+ })).sort((a, b) => b.rest - a.rest);
36
+ for (let k = 0; left > 0 && k < order.length; k++, left--) counts[order[k].index]++;
37
+ return counts;
38
+ }
39
+ /**
40
+ * Hands cells to categories while moving as few as possible: each category keeps the cells it already had,
41
+ * surplus cells join the categories that grew, and every block takes its cells in their previous order so paths never cross.
42
+ */
43
+ function reassign(previous, data, counts) {
44
+ const bySlot = [...previous].sort((a, b) => a.slot - b.slot);
45
+ const kept = /* @__PURE__ */ new Map();
46
+ const pool = [];
47
+ const want = new Map(data.map((item, index) => [item.key, counts[index]]));
48
+ for (const cell of bySlot) {
49
+ const list = cell.key !== null ? kept.get(cell.key) ?? [] : [];
50
+ if (cell.key !== null && list.length < (want.get(cell.key) ?? 0)) {
51
+ list.push(cell);
52
+ kept.set(cell.key, list);
53
+ } else pool.push(cell);
54
+ }
55
+ const next = [];
56
+ let slot = 0;
57
+ data.forEach((item, index) => {
58
+ const own = kept.get(item.key) ?? [];
59
+ while (own.length < counts[index] && pool.length) own.push(pool.shift());
60
+ own.sort((a, b) => a.slot - b.slot).forEach((cell) => next.push({
61
+ id: cell.id,
62
+ key: item.key,
63
+ slot: slot++
64
+ }));
65
+ });
66
+ pool.forEach((cell) => next.push({
67
+ id: cell.id,
68
+ key: null,
69
+ slot: slot++
70
+ }));
71
+ return next.sort((a, b) => a.id - b.id);
72
+ }
73
+ /** A share that counts to its new value and holds its width with tabular figures. */
74
+ function Share({ value, decimals }) {
75
+ const scale = 10 ** decimals;
76
+ const shown = useCountTo(Math.round(value * scale));
77
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [(shown / scale).toFixed(decimals), "%"] });
78
+ }
79
+ const WaffleChart = forwardRef(function WaffleChart({ data, label, unit = "", formatValue, rows = 10, columns = 10, accentKey, activeKey, defaultActiveKey = null, onActiveChange, legend = true, decimals = 0, emptyLabel = "No data", className }, ref) {
80
+ const figure = useRef(null);
81
+ const refs = useMemo(() => mergeRefs(ref, figure), [ref]);
82
+ const board = useRef(null);
83
+ const grid = useRef(null);
84
+ const tip = useRef(null);
85
+ const count = Math.max(1, rows * columns);
86
+ const total = data.reduce((sum, item) => sum + Math.max(0, item.value), 0);
87
+ const empty = !data.length || total <= 0;
88
+ const accent = accentKey === void 0 ? data[0]?.key ?? null : accentKey;
89
+ const signature = `${count}|${data.map((item) => `${item.key}:${item.value}`).join(",")}`;
90
+ const counts = useMemo(() => allocate(data, total, count), [signature]);
91
+ const [layout, setLayout] = useState(() => ({
92
+ cells: reassign(Array.from({ length: count }, (_, id) => ({
93
+ id,
94
+ key: null,
95
+ slot: id
96
+ })), data, counts),
97
+ moved: /* @__PURE__ */ new Set(),
98
+ version: 0
99
+ }));
100
+ const [seen, setSeen] = useState(signature);
101
+ if (seen !== signature) {
102
+ setSeen(signature);
103
+ const base = Array.from({ length: count }, (_, id) => layout.cells[id] ?? {
104
+ id,
105
+ key: null,
106
+ slot: id
107
+ });
108
+ const next = reassign(base, data, counts);
109
+ setLayout({
110
+ cells: next,
111
+ moved: new Set(next.filter((cell) => base[cell.id].slot !== cell.slot || base[cell.id].key !== cell.key).map((cell) => cell.id)),
112
+ version: layout.version + 1
113
+ });
114
+ }
115
+ const { cells, moved, version } = layout;
116
+ const [shown, setShown] = useState(false);
117
+ useEffect(() => {
118
+ const node = figure.current;
119
+ if (!node || prefersReducedMotion() || typeof IntersectionObserver === "undefined") {
120
+ setShown(true);
121
+ return;
122
+ }
123
+ const observer = new IntersectionObserver(([entry]) => {
124
+ if (!entry?.isIntersecting) return;
125
+ observer.disconnect();
126
+ setShown(true);
127
+ }, { threshold: .35 });
128
+ observer.observe(node);
129
+ return () => observer.disconnect();
130
+ }, []);
131
+ const [ownPinned, setOwnPinned] = useState(defaultActiveKey);
132
+ const pinned = activeKey !== void 0 ? activeKey : ownPinned;
133
+ const pin = (key) => {
134
+ if (activeKey === void 0) setOwnPinned(key);
135
+ onActiveChange?.(key);
136
+ };
137
+ const [reading, setReading] = useState(null);
138
+ const [preview, setPreview] = useState(null);
139
+ const focusKey = reading?.key ?? preview ?? pinned;
140
+ const active = focusKey && data.some((item) => item.key === focusKey) ? focusKey : null;
141
+ const rank = (key) => data.filter((entry) => entry.key !== accent).findIndex((entry) => entry.key === key);
142
+ const colorOf = (key) => {
143
+ const item = data.find((entry) => entry.key === key);
144
+ if (!item) return "transparent";
145
+ if (item.color) return item.color;
146
+ if (key === accent) return "var(--color-chart-1)";
147
+ const at = rank(key);
148
+ return at < 3 ? `var(--color-chart-${at + 2})` : `color-mix(in oklab, var(--color-foreground) ${SHADES[(at - 3) % SHADES.length]}%, var(--color-surface))`;
149
+ };
150
+ const shapeOf = (key) => key === null || key === accent || rank(key) < 0 ? "square" : SHAPES[rank(key) % SHAPES.length];
151
+ const place = (slot) => ({
152
+ x: Math.floor(slot / rows),
153
+ y: rows - 1 - slot % rows
154
+ });
155
+ const filled = cells.filter((cell) => cell.key !== null).length;
156
+ const bubble = reading ? data.find((item) => item.key === reading.key) ?? null : null;
157
+ const shownBefore = useRef(false);
158
+ useIsomorphicLayoutEffect(() => {
159
+ const node = tip.current;
160
+ const box = grid.current?.getBoundingClientRect();
161
+ const host = board.current?.getBoundingClientRect();
162
+ if (!reading || !node || !box?.width || !host) {
163
+ shownBefore.current = false;
164
+ return;
165
+ }
166
+ const side = (box.width - GAP * (columns - 1)) / columns;
167
+ const { x, y } = place(reading.slot);
168
+ const cx = box.left - host.left + x * (side + GAP) + side / 2;
169
+ const cy = box.top - host.top + y * (side + GAP);
170
+ const tw = node.offsetWidth;
171
+ const th = node.offsetHeight;
172
+ let top = cy - th - 10;
173
+ if (top < 0) top = cy + side + 10;
174
+ if (!shownBefore.current) node.style.transition = "none";
175
+ node.style.translate = `${clamp(cx - tw / 2, 0, Math.max(0, host.width - tw))}px ${clamp(top, 0, Math.max(0, host.height - th))}px`;
176
+ if (!shownBefore.current) {
177
+ node.offsetWidth;
178
+ node.style.transition = "";
179
+ }
180
+ shownBefore.current = true;
181
+ });
182
+ const slotAt = (clientX, clientY) => {
183
+ const box = grid.current?.getBoundingClientRect();
184
+ if (!box?.width) return null;
185
+ const side = (box.width - GAP * (columns - 1)) / columns;
186
+ const col = clamp(Math.floor((clientX - box.left) / (side + GAP)), 0, columns - 1);
187
+ const row = clamp(Math.floor((clientY - box.top) / (side + GAP)), 0, rows - 1);
188
+ return col * rows + (rows - 1 - row);
189
+ };
190
+ const readingAt = (slot) => {
191
+ if (slot === null) return null;
192
+ const cell = cells.find((item) => item.slot === slot);
193
+ return cell?.key ? {
194
+ key: cell.key,
195
+ slot
196
+ } : null;
197
+ };
198
+ const show = (next) => setReading((now) => now?.slot === next?.slot ? now : next);
199
+ const onPointerMove = (event) => {
200
+ if (event.pointerType === "mouse" || event.buttons) show(readingAt(slotAt(event.clientX, event.clientY)));
201
+ };
202
+ const onPointerDown = (event) => {
203
+ if (event.pointerType !== "mouse") event.currentTarget.setPointerCapture?.(event.pointerId);
204
+ show(readingAt(slotAt(event.clientX, event.clientY)));
205
+ };
206
+ const onKeyDown = (event) => {
207
+ if (empty) return;
208
+ if (event.key === "Escape" && reading) {
209
+ event.preventDefault();
210
+ setReading(null);
211
+ return;
212
+ }
213
+ const at = reading?.slot ?? 0;
214
+ const col = Math.floor(at / rows);
215
+ const row = at % rows;
216
+ const blocks = counts.map((size, index) => ({
217
+ start: counts.slice(0, index).reduce((sum, value) => sum + value, 0),
218
+ size
219
+ })).filter((block) => block.size > 0);
220
+ const block = Math.max(0, blocks.findIndex((item) => at >= item.start && at < item.start + item.size));
221
+ const move = {
222
+ ArrowUp: () => col * rows + Math.min(rows - 1, row + 1),
223
+ ArrowDown: () => col * rows + Math.max(0, row - 1),
224
+ ArrowRight: () => Math.min(columns - 1, col + 1) * rows + row,
225
+ ArrowLeft: () => Math.max(0, col - 1) * rows + row,
226
+ PageDown: () => blocks[Math.min(blocks.length - 1, block + 1)].start,
227
+ PageUp: () => at > blocks[block].start ? blocks[block].start : blocks[Math.max(0, block - 1)].start,
228
+ Home: () => 0,
229
+ End: () => filled - 1
230
+ }[event.key];
231
+ if (!move) return;
232
+ event.preventDefault();
233
+ show(readingAt(clamp(reading ? move() : 0, 0, Math.max(0, filled - 1))));
234
+ };
235
+ const suffix = unit ? ` ${unit}` : "";
236
+ const format = (value, item) => `${formatValue ? formatValue(value, item) : GROUPED.format(value)}${suffix}`;
237
+ const share = (item) => total > 0 ? Math.max(0, item.value) / total * 100 : 0;
238
+ const describe = (item) => `${item.label}, ${share(item).toFixed(decimals)}%, ${format(item.value, item)}`;
239
+ const summary = empty ? `${label}. ${emptyLabel}.` : `${label}. ${data.map((item) => `${item.label} ${share(item).toFixed(decimals)}%`).join(", ")}.`;
240
+ return /* @__PURE__ */ jsxs("figure", {
241
+ ref: refs,
242
+ "data-slot": "waffle-chart",
243
+ "aria-label": label,
244
+ "data-legend": legend || void 0,
245
+ "data-scrubbing": reading !== null || void 0,
246
+ className: cn("waffle-chart box-border font-sans text-foreground", className),
247
+ children: [
248
+ /* @__PURE__ */ jsxs("div", {
249
+ ref: board,
250
+ className: "relative min-w-0",
251
+ children: [
252
+ /* @__PURE__ */ jsx("div", {
253
+ ref: grid,
254
+ "data-slot": "waffle-chart-grid",
255
+ "data-active": active ? "" : void 0,
256
+ role: empty ? void 0 : "slider",
257
+ tabIndex: empty ? void 0 : 0,
258
+ "aria-label": empty ? void 0 : `${label}, explore cells`,
259
+ "aria-valuemin": empty ? void 0 : 1,
260
+ "aria-valuemax": empty ? void 0 : Math.max(1, filled),
261
+ "aria-valuenow": empty ? void 0 : (reading?.slot ?? 0) + 1,
262
+ "aria-valuetext": empty ? void 0 : bubble ? describe(bubble) : summary,
263
+ "aria-hidden": empty || void 0,
264
+ className: "waffle-chart-grid",
265
+ style: {
266
+ "--c": columns,
267
+ "--r": rows
268
+ },
269
+ onPointerMove,
270
+ onPointerDown,
271
+ onPointerUp: (event) => {
272
+ if (event.pointerType !== "mouse") setReading(null);
273
+ },
274
+ onPointerCancel: () => setReading(null),
275
+ onPointerLeave: (event) => {
276
+ if (event.pointerType === "mouse") setReading(null);
277
+ },
278
+ onKeyDown,
279
+ onBlur: () => setReading(null),
280
+ onFocus: (event) => {
281
+ if (event.currentTarget.matches(":focus-visible") && !empty) setReading((now) => now ?? readingAt(0));
282
+ },
283
+ children: cells.map((cell) => {
284
+ const { x, y } = place(cell.slot);
285
+ return /* @__PURE__ */ jsx("span", {
286
+ "aria-hidden": "true",
287
+ "data-cell": cell.slot,
288
+ "data-shown": shown || void 0,
289
+ "data-on": cell.key !== null && cell.key === active || void 0,
290
+ "data-cursor": reading?.slot === cell.slot || void 0,
291
+ "data-empty": cell.key === null || void 0,
292
+ "data-shape": shapeOf(cell.key),
293
+ "data-hop": moved.has(cell.id) && shown ? version % 2 ? "a" : "b" : void 0,
294
+ className: "waffle-chart-cell",
295
+ style: {
296
+ "--x": x,
297
+ "--y": y,
298
+ "--series": colorOf(cell.key),
299
+ "--delay": `${Math.round(cell.slot / count * 320)}ms`
300
+ }
301
+ }, cell.id);
302
+ })
303
+ }),
304
+ empty ? null : /* @__PURE__ */ jsxs("div", {
305
+ ref: tip,
306
+ "aria-hidden": "true",
307
+ "data-slot": "waffle-chart-tooltip",
308
+ className: "line-chart-tooltip",
309
+ children: [/* @__PURE__ */ jsxs("p", {
310
+ className: "m-0 flex min-w-0 items-center gap-2 text-sm",
311
+ children: [
312
+ /* @__PURE__ */ jsx("span", {
313
+ "data-shape": shapeOf(bubble?.key ?? null),
314
+ style: { "--series": colorOf(bubble?.key ?? null) },
315
+ className: "chart-key-swatch"
316
+ }),
317
+ /* @__PURE__ */ jsx("span", {
318
+ className: "min-w-0 flex-1 truncate text-text-secondary",
319
+ children: bubble?.label ?? ""
320
+ }),
321
+ /* @__PURE__ */ jsx("span", {
322
+ className: "ms-2 flex-none font-medium tabular-nums",
323
+ children: bubble ? `${share(bubble).toFixed(decimals)}%` : ""
324
+ })
325
+ ]
326
+ }), /* @__PURE__ */ jsx("p", {
327
+ className: "m-0 ms-4.5 whitespace-nowrap text-xs tabular-nums text-muted-foreground",
328
+ children: bubble ? format(bubble.value, bubble) : ""
329
+ })]
330
+ }),
331
+ empty ? /* @__PURE__ */ jsx("p", {
332
+ className: "m-0 grid min-h-40 place-items-center text-sm text-muted-foreground",
333
+ children: emptyLabel
334
+ }) : null
335
+ ]
336
+ }),
337
+ legend && !empty ? /* @__PURE__ */ jsx("ul", {
338
+ "aria-label": `${label}, categories`,
339
+ onPointerLeave: () => setPreview(null),
340
+ className: "waffle-chart-legend",
341
+ children: data.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
342
+ type: "button",
343
+ "aria-pressed": pinned === item.key,
344
+ "data-on": active === item.key || void 0,
345
+ "data-dim": active !== null && active !== item.key || void 0,
346
+ className: "chart-key",
347
+ onClick: () => pin(pinned === item.key ? null : item.key),
348
+ onPointerEnter: (event) => {
349
+ if (event.pointerType === "mouse") setPreview(item.key);
350
+ },
351
+ onFocus: (event) => {
352
+ if (event.currentTarget.matches(":focus-visible")) setPreview(item.key);
353
+ },
354
+ onBlur: () => setPreview(null),
355
+ children: [
356
+ /* @__PURE__ */ jsx("span", {
357
+ "aria-hidden": "true",
358
+ "data-shape": shapeOf(item.key),
359
+ style: { "--series": colorOf(item.key) },
360
+ className: "chart-key-swatch"
361
+ }),
362
+ /* @__PURE__ */ jsx("span", {
363
+ className: "min-w-0 truncate",
364
+ children: item.label
365
+ }),
366
+ /* @__PURE__ */ jsx("span", {
367
+ "aria-hidden": "true",
368
+ className: "min-w-13 text-end text-base font-medium tabular-nums text-foreground",
369
+ children: /* @__PURE__ */ jsx(Share, {
370
+ value: share(item),
371
+ decimals
372
+ })
373
+ })
374
+ ]
375
+ }) }, item.key))
376
+ }) : null,
377
+ /* @__PURE__ */ jsx("p", {
378
+ className: "sr-only",
379
+ children: summary
380
+ }),
381
+ /* @__PURE__ */ jsx("p", {
382
+ className: "sr-only",
383
+ "aria-live": "polite",
384
+ "aria-atomic": "true",
385
+ children: bubble ? describe(bubble) : ""
386
+ }),
387
+ empty ? null : /* @__PURE__ */ jsx("div", {
388
+ className: "sr-only",
389
+ children: /* @__PURE__ */ jsxs("table", { children: [
390
+ /* @__PURE__ */ jsx("caption", { children: label }),
391
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
392
+ /* @__PURE__ */ jsx("th", {
393
+ scope: "col",
394
+ children: "Category"
395
+ }),
396
+ /* @__PURE__ */ jsx("th", {
397
+ scope: "col",
398
+ children: "Value"
399
+ }),
400
+ /* @__PURE__ */ jsx("th", {
401
+ scope: "col",
402
+ children: "Share"
403
+ }),
404
+ /* @__PURE__ */ jsx("th", {
405
+ scope: "col",
406
+ children: "Cells"
407
+ })
408
+ ] }) }),
409
+ /* @__PURE__ */ jsx("tbody", { children: data.map((item, index) => /* @__PURE__ */ jsxs("tr", { children: [
410
+ /* @__PURE__ */ jsx("th", {
411
+ scope: "row",
412
+ children: item.label
413
+ }),
414
+ /* @__PURE__ */ jsx("td", { children: format(item.value, item) }),
415
+ /* @__PURE__ */ jsxs("td", { children: [share(item).toFixed(decimals), "%"] }),
416
+ /* @__PURE__ */ jsx("td", { children: counts[index] })
417
+ ] }, item.key)) })
418
+ ] })
419
+ })
420
+ ]
421
+ });
422
+ });
423
+ //#endregion
424
+ export { WaffleChart };
425
+
426
+ //# sourceMappingURL=waffle-chart.js.map