@teamofsilicons/silicon-ui 0.0.0-stage → 0.1.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 (352) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +63 -2
  3. package/components/accordion/accordion.d.ts +13 -0
  4. package/components/accordion/accordion.js +35 -0
  5. package/components/accordion/accordion.module.css +15 -0
  6. package/components/action-button/action-button.d.ts +11 -0
  7. package/components/action-button/action-button.js +57 -0
  8. package/components/action-button/action-button.module.css +15 -0
  9. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  10. package/components/activity-heatmap/activity-heatmap.js +303 -0
  11. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  12. package/components/alert/alert.d.ts +13 -0
  13. package/components/alert/alert.js +79 -0
  14. package/components/alert/alert.module.css +15 -0
  15. package/components/animated-counter/animated-counter.d.ts +13 -0
  16. package/components/animated-counter/animated-counter.js +73 -0
  17. package/components/animated-counter/animated-counter.module.css +12 -0
  18. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  19. package/components/announcement-bar/announcement-bar.js +187 -0
  20. package/components/announcement-bar/announcement-bar.module.css +60 -0
  21. package/components/avatar/avatar.d.ts +8 -0
  22. package/components/avatar/avatar.js +18 -0
  23. package/components/avatar/avatar.module.css +60 -0
  24. package/components/avatar-group/avatar-group.d.ts +13 -0
  25. package/components/avatar-group/avatar-group.js +24 -0
  26. package/components/avatar-group/avatar-group.module.css +27 -0
  27. package/components/badge/badge.d.ts +10 -0
  28. package/components/badge/badge.js +66 -0
  29. package/components/badge/badge.module.css +52 -0
  30. package/components/bar-chart/bar-chart.d.ts +32 -0
  31. package/components/bar-chart/bar-chart.js +327 -0
  32. package/components/bar-chart/bar-chart.module.css +49 -0
  33. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  34. package/components/billing-toggle/billing-toggle.js +83 -0
  35. package/components/billing-toggle/billing-toggle.module.css +139 -0
  36. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  37. package/components/bottom-sheet/bottom-sheet.js +386 -0
  38. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  39. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  40. package/components/breadcrumb/breadcrumb.js +19 -0
  41. package/components/breadcrumb/breadcrumb.module.css +12 -0
  42. package/components/brush-chart/brush-chart.d.ts +45 -0
  43. package/components/brush-chart/brush-chart.js +426 -0
  44. package/components/brush-chart/brush-chart.module.css +69 -0
  45. package/components/button/button.d.ts +10 -0
  46. package/components/button/button.js +97 -0
  47. package/components/button/button.module.css +39 -0
  48. package/components/button-group/button-group.d.ts +59 -0
  49. package/components/button-group/button-group.js +280 -0
  50. package/components/button-group/button-group.module.css +87 -0
  51. package/components/button-link/button-link.d.ts +24 -0
  52. package/components/button-link/button-link.js +11 -0
  53. package/components/button-link/button-link.module.css +7 -0
  54. package/components/calendar/calendar.d.ts +22 -0
  55. package/components/calendar/calendar.js +253 -0
  56. package/components/calendar/calendar.module.css +51 -0
  57. package/components/card/card.d.ts +19 -0
  58. package/components/card/card.js +108 -0
  59. package/components/card/card.module.css +51 -0
  60. package/components/card-stack/card-stack.d.ts +34 -0
  61. package/components/card-stack/card-stack.js +291 -0
  62. package/components/card-stack/card-stack.module.css +63 -0
  63. package/components/carousel/carousel.d.ts +28 -0
  64. package/components/carousel/carousel.js +332 -0
  65. package/components/carousel/carousel.module.css +42 -0
  66. package/components/chat-thread/chat-thread.d.ts +135 -0
  67. package/components/chat-thread/chat-thread.js +406 -0
  68. package/components/chat-thread/chat-thread.module.css +242 -0
  69. package/components/checkbox/checkbox.d.ts +7 -0
  70. package/components/checkbox/checkbox.js +23 -0
  71. package/components/checkbox/checkbox.module.css +15 -0
  72. package/components/chip-group/chip-group.d.ts +24 -0
  73. package/components/chip-group/chip-group.js +196 -0
  74. package/components/chip-group/chip-group.module.css +38 -0
  75. package/components/code-block/code-block.d.ts +11 -0
  76. package/components/code-block/code-block.js +133 -0
  77. package/components/code-block/code-block.module.css +43 -0
  78. package/components/color-picker/color-picker.d.ts +40 -0
  79. package/components/color-picker/color-picker.js +449 -0
  80. package/components/color-picker/color-picker.module.css +111 -0
  81. package/components/combobox/combobox.d.ts +19 -0
  82. package/components/combobox/combobox.js +167 -0
  83. package/components/combobox/combobox.module.css +28 -0
  84. package/components/command-palette/command-palette.d.ts +20 -0
  85. package/components/command-palette/command-palette.js +151 -0
  86. package/components/command-palette/command-palette.module.css +94 -0
  87. package/components/comment-thread/comment-thread.d.ts +120 -0
  88. package/components/comment-thread/comment-thread.js +343 -0
  89. package/components/comment-thread/comment-thread.module.css +195 -0
  90. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  91. package/components/confirm-morph/confirm-morph.js +355 -0
  92. package/components/confirm-morph/confirm-morph.module.css +146 -0
  93. package/components/context-menu/context-menu.d.ts +18 -0
  94. package/components/context-menu/context-menu.js +148 -0
  95. package/components/context-menu/context-menu.module.css +17 -0
  96. package/components/copy-button/copy-button.d.ts +10 -0
  97. package/components/copy-button/copy-button.js +69 -0
  98. package/components/copy-button/copy-button.module.css +21 -0
  99. package/components/countdown/countdown.d.ts +31 -0
  100. package/components/countdown/countdown.js +177 -0
  101. package/components/countdown/countdown.module.css +49 -0
  102. package/components/date-picker/date-picker.d.ts +17 -0
  103. package/components/date-picker/date-picker.js +100 -0
  104. package/components/date-picker/date-picker.module.css +34 -0
  105. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  106. package/components/date-range-picker/date-range-picker.js +534 -0
  107. package/components/date-range-picker/date-range-picker.module.css +214 -0
  108. package/components/dialog/dialog.d.ts +11 -0
  109. package/components/dialog/dialog.js +52 -0
  110. package/components/dialog/dialog.module.css +36 -0
  111. package/components/donut-chart/donut-chart.d.ts +46 -0
  112. package/components/donut-chart/donut-chart.js +382 -0
  113. package/components/donut-chart/donut-chart.module.css +48 -0
  114. package/components/drawer/drawer.d.ts +20 -0
  115. package/components/drawer/drawer.js +111 -0
  116. package/components/drawer/drawer.module.css +97 -0
  117. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  118. package/components/dropdown-menu/dropdown-menu.js +39 -0
  119. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  120. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  121. package/components/elastic-slider/elastic-slider.js +261 -0
  122. package/components/elastic-slider/elastic-slider.module.css +40 -0
  123. package/components/empty-state/empty-state.d.ts +12 -0
  124. package/components/empty-state/empty-state.js +69 -0
  125. package/components/empty-state/empty-state.module.css +13 -0
  126. package/components/expandable-card/expandable-card.d.ts +13 -0
  127. package/components/expandable-card/expandable-card.js +89 -0
  128. package/components/expandable-card/expandable-card.module.css +22 -0
  129. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  130. package/components/expanding-button-group/expanding-button-group.js +258 -0
  131. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  132. package/components/expanding-search/expanding-search.d.ts +38 -0
  133. package/components/expanding-search/expanding-search.js +242 -0
  134. package/components/expanding-search/expanding-search.module.css +52 -0
  135. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  136. package/components/file-dropzone/file-dropzone.js +379 -0
  137. package/components/file-dropzone/file-dropzone.module.css +115 -0
  138. package/components/file-upload/file-upload.d.ts +22 -0
  139. package/components/file-upload/file-upload.js +192 -0
  140. package/components/file-upload/file-upload.module.css +49 -0
  141. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  142. package/components/filter-toolbar/filter-toolbar.js +446 -0
  143. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  144. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  145. package/components/floating-button-group/floating-button-group.js +215 -0
  146. package/components/floating-button-group/floating-button-group.module.css +74 -0
  147. package/components/gauge/gauge.d.ts +18 -0
  148. package/components/gauge/gauge.js +101 -0
  149. package/components/gauge/gauge.module.css +22 -0
  150. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  151. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  152. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  153. package/components/hover-card/hover-card.d.ts +38 -0
  154. package/components/hover-card/hover-card.js +145 -0
  155. package/components/hover-card/hover-card.module.css +20 -0
  156. package/components/image-compare/image-compare.d.ts +27 -0
  157. package/components/image-compare/image-compare.js +267 -0
  158. package/components/image-compare/image-compare.module.css +25 -0
  159. package/components/in-view-title/in-view-title.d.ts +17 -0
  160. package/components/in-view-title/in-view-title.js +70 -0
  161. package/components/in-view-title/in-view-title.module.css +13 -0
  162. package/components/inline-edit/inline-edit.d.ts +27 -0
  163. package/components/inline-edit/inline-edit.js +315 -0
  164. package/components/inline-edit/inline-edit.module.css +73 -0
  165. package/components/input/input.d.ts +7 -0
  166. package/components/input/input.js +60 -0
  167. package/components/input/input.module.css +22 -0
  168. package/components/json-viewer/json-viewer.d.ts +48 -0
  169. package/components/json-viewer/json-viewer.js +463 -0
  170. package/components/json-viewer/json-viewer.module.css +92 -0
  171. package/components/line-chart/line-chart.d.ts +58 -0
  172. package/components/line-chart/line-chart.js +383 -0
  173. package/components/line-chart/line-chart.module.css +60 -0
  174. package/components/mention-input/mention-input.d.ts +72 -0
  175. package/components/mention-input/mention-input.js +417 -0
  176. package/components/mention-input/mention-input.module.css +134 -0
  177. package/components/metric-card/metric-card.d.ts +8 -0
  178. package/components/metric-card/metric-card.js +44 -0
  179. package/components/metric-card/metric-card.module.css +18 -0
  180. package/components/money-input/money-input.d.ts +48 -0
  181. package/components/money-input/money-input.js +503 -0
  182. package/components/money-input/money-input.module.css +77 -0
  183. package/components/morph-select/morph-select.d.ts +50 -0
  184. package/components/morph-select/morph-select.js +395 -0
  185. package/components/morph-select/morph-select.module.css +88 -0
  186. package/components/multi-select/multi-select.d.ts +18 -0
  187. package/components/multi-select/multi-select.js +105 -0
  188. package/components/multi-select/multi-select.module.css +37 -0
  189. package/components/notification-center/notification-center.d.ts +24 -0
  190. package/components/notification-center/notification-center.js +146 -0
  191. package/components/notification-center/notification-center.module.css +79 -0
  192. package/components/number-field/number-field.d.ts +41 -0
  193. package/components/number-field/number-field.js +455 -0
  194. package/components/number-field/number-field.module.css +71 -0
  195. package/components/otp-input/otp-input.d.ts +13 -0
  196. package/components/otp-input/otp-input.js +129 -0
  197. package/components/otp-input/otp-input.module.css +28 -0
  198. package/components/pagination/pagination.d.ts +7 -0
  199. package/components/pagination/pagination.js +60 -0
  200. package/components/pagination/pagination.module.css +16 -0
  201. package/components/password-field/password-field.d.ts +14 -0
  202. package/components/password-field/password-field.js +51 -0
  203. package/components/password-field/password-field.module.css +27 -0
  204. package/components/password-strength/password-strength.d.ts +107 -0
  205. package/components/password-strength/password-strength.js +161 -0
  206. package/components/password-strength/password-strength.module.css +57 -0
  207. package/components/phone-input/phone-input.d.ts +73 -0
  208. package/components/phone-input/phone-input.js +612 -0
  209. package/components/phone-input/phone-input.module.css +98 -0
  210. package/components/popover/popover.d.ts +6 -0
  211. package/components/popover/popover.js +19 -0
  212. package/components/popover/popover.module.css +32 -0
  213. package/components/progress/progress.d.ts +8 -0
  214. package/components/progress/progress.js +42 -0
  215. package/components/progress/progress.module.css +1 -0
  216. package/components/radio-cards/radio-cards.d.ts +35 -0
  217. package/components/radio-cards/radio-cards.js +110 -0
  218. package/components/radio-cards/radio-cards.module.css +84 -0
  219. package/components/radio-group/radio-group.d.ts +13 -0
  220. package/components/radio-group/radio-group.js +55 -0
  221. package/components/radio-group/radio-group.module.css +30 -0
  222. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  223. package/components/resizable-panels/resizable-panels.js +338 -0
  224. package/components/resizable-panels/resizable-panels.module.css +40 -0
  225. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  226. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  227. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  228. package/components/ridgeline/ridgeline.d.ts +37 -0
  229. package/components/ridgeline/ridgeline.js +346 -0
  230. package/components/ridgeline/ridgeline.module.css +58 -0
  231. package/components/scroll-area/scroll-area.d.ts +40 -0
  232. package/components/scroll-area/scroll-area.js +153 -0
  233. package/components/scroll-area/scroll-area.module.css +79 -0
  234. package/components/search-field/search-field.d.ts +7 -0
  235. package/components/search-field/search-field.js +20 -0
  236. package/components/search-field/search-field.module.css +17 -0
  237. package/components/segmented-control/segmented-control.d.ts +16 -0
  238. package/components/segmented-control/segmented-control.js +56 -0
  239. package/components/segmented-control/segmented-control.module.css +15 -0
  240. package/components/select/select.d.ts +15 -0
  241. package/components/select/select.js +36 -0
  242. package/components/select/select.module.css +35 -0
  243. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  244. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  245. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  246. package/components/signature-pad/signature-pad.d.ts +72 -0
  247. package/components/signature-pad/signature-pad.js +450 -0
  248. package/components/signature-pad/signature-pad.module.css +84 -0
  249. package/components/skeleton/skeleton.d.ts +13 -0
  250. package/components/skeleton/skeleton.js +53 -0
  251. package/components/skeleton/skeleton.module.css +11 -0
  252. package/components/slider/slider.d.ts +45 -0
  253. package/components/slider/slider.js +347 -0
  254. package/components/slider/slider.module.css +45 -0
  255. package/components/slope-chart/slope-chart.d.ts +38 -0
  256. package/components/slope-chart/slope-chart.js +183 -0
  257. package/components/slope-chart/slope-chart.module.css +51 -0
  258. package/components/slot-text/slot-text.d.ts +26 -0
  259. package/components/slot-text/slot-text.js +119 -0
  260. package/components/slot-text/slot-text.module.css +19 -0
  261. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  262. package/components/sortable-data-table/sortable-data-table.js +240 -0
  263. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  264. package/components/sparkline/sparkline.d.ts +19 -0
  265. package/components/sparkline/sparkline.js +239 -0
  266. package/components/sparkline/sparkline.module.css +26 -0
  267. package/components/split-button/split-button.d.ts +19 -0
  268. package/components/split-button/split-button.js +33 -0
  269. package/components/split-button/split-button.module.css +51 -0
  270. package/components/stepper/stepper.d.ts +36 -0
  271. package/components/stepper/stepper.js +108 -0
  272. package/components/stepper/stepper.module.css +74 -0
  273. package/components/streamgraph/streamgraph.d.ts +51 -0
  274. package/components/streamgraph/streamgraph.js +376 -0
  275. package/components/streamgraph/streamgraph.module.css +55 -0
  276. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  277. package/components/swipe-actions/swipe-actions.js +330 -0
  278. package/components/swipe-actions/swipe-actions.module.css +64 -0
  279. package/components/switch/switch.d.ts +7 -0
  280. package/components/switch/switch.js +45 -0
  281. package/components/switch/switch.module.css +12 -0
  282. package/components/tabs/tabs.d.ts +8 -0
  283. package/components/tabs/tabs.js +138 -0
  284. package/components/tabs/tabs.module.css +27 -0
  285. package/components/tag-input/tag-input.d.ts +15 -0
  286. package/components/tag-input/tag-input.js +150 -0
  287. package/components/tag-input/tag-input.module.css +34 -0
  288. package/components/text-morph/text-morph.d.ts +13 -0
  289. package/components/text-morph/text-morph.js +69 -0
  290. package/components/text-morph/text-morph.module.css +8 -0
  291. package/components/text-reveal/text-reveal.d.ts +14 -0
  292. package/components/text-reveal/text-reveal.js +20 -0
  293. package/components/text-reveal/text-reveal.module.css +13 -0
  294. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  295. package/components/text-shimmer/text-shimmer.js +90 -0
  296. package/components/text-shimmer/text-shimmer.module.css +9 -0
  297. package/components/textarea/textarea.d.ts +7 -0
  298. package/components/textarea/textarea.js +56 -0
  299. package/components/textarea/textarea.module.css +22 -0
  300. package/components/theme-switch/theme-switch.d.ts +11 -0
  301. package/components/theme-switch/theme-switch.js +37 -0
  302. package/components/theme-switch/theme-switch.module.css +31 -0
  303. package/components/time-picker/time-picker.d.ts +13 -0
  304. package/components/time-picker/time-picker.js +95 -0
  305. package/components/time-picker/time-picker.module.css +23 -0
  306. package/components/timeline/timeline.d.ts +47 -0
  307. package/components/timeline/timeline.js +171 -0
  308. package/components/timeline/timeline.module.css +51 -0
  309. package/components/toast/toast.d.ts +8 -0
  310. package/components/toast/toast.js +118 -0
  311. package/components/toast/toast.module.css +100 -0
  312. package/components/toast-stack/toast-stack.d.ts +57 -0
  313. package/components/toast-stack/toast-stack.js +399 -0
  314. package/components/toast-stack/toast-stack.module.css +61 -0
  315. package/components/tooltip/tooltip.d.ts +8 -0
  316. package/components/tooltip/tooltip.js +62 -0
  317. package/components/tooltip/tooltip.module.css +18 -0
  318. package/components/tree-view/tree-view.d.ts +17 -0
  319. package/components/tree-view/tree-view.js +147 -0
  320. package/components/tree-view/tree-view.module.css +22 -0
  321. package/components/treemap/treemap.d.ts +35 -0
  322. package/components/treemap/treemap.js +375 -0
  323. package/components/treemap/treemap.module.css +59 -0
  324. package/components/usage-meter/usage-meter.d.ts +24 -0
  325. package/components/usage-meter/usage-meter.js +296 -0
  326. package/components/usage-meter/usage-meter.module.css +61 -0
  327. package/components/user-menu/user-menu.d.ts +62 -0
  328. package/components/user-menu/user-menu.js +316 -0
  329. package/components/user-menu/user-menu.module.css +97 -0
  330. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  331. package/components/waffle-chart/waffle-chart.js +244 -0
  332. package/components/waffle-chart/waffle-chart.module.css +52 -0
  333. package/components.json +1330 -0
  334. package/lib/escape.d.ts +19 -0
  335. package/lib/escape.js +50 -0
  336. package/lib/media.d.ts +353 -0
  337. package/lib/media.js +65 -0
  338. package/lib/menu-highlight.d.ts +28 -0
  339. package/lib/menu-highlight.js +43 -0
  340. package/lib/motion-tokens.d.ts +60 -0
  341. package/lib/motion-tokens.js +25 -0
  342. package/lib/return-focus.d.ts +9 -0
  343. package/lib/return-focus.js +53 -0
  344. package/lib/silicon-provider.d.ts +60 -0
  345. package/lib/silicon-provider.js +47 -0
  346. package/lib/use-copy-feedback.d.ts +8 -0
  347. package/lib/use-copy-feedback.js +36 -0
  348. package/lib/use-today.d.ts +2 -0
  349. package/lib/use-today.js +29 -0
  350. package/package.json +66 -4
  351. package/styles.css +218 -0
  352. package/styles.css.d.ts +1 -0
@@ -0,0 +1,426 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { animate, motion, useInView, useMotionValue, useReducedMotion, useSpring } from "motion/react";
5
+ import { motionTokens } from "../../lib/motion-tokens.js";
6
+ import styles from "./brush-chart.module.css";
7
+ const DAY = 86400000;
8
+ const { spring } = motionTokens;
9
+ const physical = ({ visualDuration, bounce }, restDelta = .001) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
10
+ const settle = physical(spring.smooth);
11
+ const glide = physical(spring.snappy, .01);
12
+ const follow = { stiffness: glide.stiffness, damping: glide.damping, restDelta: .01 };
13
+ const draw = physical({ visualDuration: .9, bounce: 0 });
14
+ const TOP = 28, GAP = 14, GRIP = 14;
15
+ const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
16
+ const grouped = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
17
+ const utc = (options) => new Intl.DateTimeFormat("en-US", { timeZone: "UTC", ...options });
18
+ const fullDate = utc({ weekday: "short", month: "short", day: "numeric", year: "numeric" });
19
+ const dayTick = utc({ month: "short", day: "numeric" });
20
+ const monthTick = utc({ month: "short" });
21
+ const yearTick = utc({ year: "numeric" });
22
+ const shortDate = utc({ month: "short", day: "numeric", year: "numeric" });
23
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
24
+ const time = (date) => typeof date === "number" ? date : date.getTime();
25
+ const clean = (value) => Number(value.toPrecision(12));
26
+ /** Clean gridlines from zero: the smallest step of 1, 2, 2.5, or 5 that covers the data in four rows or fewer. */
27
+ function niceTop(high) {
28
+ const hi = Math.max(high, 1), magnitude = 10 ** Math.floor(Math.log10(hi / 4));
29
+ for (const factor of [1, 2, 2.5, 5, 10]) {
30
+ const step = factor * magnitude, top = Math.ceil(hi / step) * step;
31
+ if (top / step <= 4 + 1e-9)
32
+ return { top: clean(top), step: clean(step) };
33
+ }
34
+ return { top: hi, step: hi / 4 };
35
+ }
36
+ /** Calendar ticks that fit the width: days, weeks, then months, quarters, and years, each landing on a real boundary. */
37
+ function timeTicks(start, end, width) {
38
+ const room = Math.max(2, Math.floor(width / 76)), span = end - start;
39
+ for (const days of [1, 2, 7, 14]) {
40
+ if (span / (days * DAY) > room)
41
+ continue;
42
+ const ticks = [];
43
+ let t = Math.ceil(start / DAY) * DAY;
44
+ if (days >= 7)
45
+ while (new Date(t).getUTCDay() !== 1)
46
+ t += DAY;
47
+ for (; t <= end; t += days * DAY)
48
+ ticks.push({ t, label: dayTick.format(t) });
49
+ return ticks;
50
+ }
51
+ for (const months of [1, 2, 3, 6, 12]) {
52
+ if (span / (months * 30.44 * DAY) > room)
53
+ continue;
54
+ const ticks = [];
55
+ const first = new Date(start);
56
+ let y = first.getUTCFullYear(), m = first.getUTCMonth() + (first.getUTCDate() > 1 || first.getUTCHours() > 0 ? 1 : 0);
57
+ for (;;) {
58
+ y += Math.floor(m / 12);
59
+ m %= 12;
60
+ const t = Date.UTC(y, m, 1);
61
+ if (t > end)
62
+ break;
63
+ if (m % months === 0)
64
+ ticks.push({ t, label: m === 0 ? yearTick.format(t) : monthTick.format(t) });
65
+ m += 1;
66
+ }
67
+ return ticks;
68
+ }
69
+ const ticks = [];
70
+ for (let y = new Date(start).getUTCFullYear() + 1; Date.UTC(y, 0, 1) <= end; y++)
71
+ ticks.push({ t: Date.UTC(y, 0, 1), label: String(y) });
72
+ return ticks;
73
+ }
74
+ /** First index at or after t. */
75
+ function lowerBound(points, t) {
76
+ let low = 0, high = points.length;
77
+ while (low < high) {
78
+ const middle = (low + high) >> 1;
79
+ if (points[middle].t < t)
80
+ low = middle + 1;
81
+ else
82
+ high = middle;
83
+ }
84
+ return low;
85
+ }
86
+ /** A line through the points in view. Past two points per pixel, each pixel column keeps only its low and high so spikes survive. */
87
+ function linePath(points, from, to, x, y, width) {
88
+ const slice = points.slice(Math.max(0, from), Math.min(points.length, to));
89
+ if (!slice.length)
90
+ return "";
91
+ const parts = [];
92
+ if (slice.length <= width * 2) {
93
+ for (const point of slice)
94
+ parts.push(`${x(point.t).toFixed(1)},${y(point.value).toFixed(1)}`);
95
+ }
96
+ else {
97
+ let column = -1, low = null, high = null;
98
+ const flush = () => { if (!low || !high)
99
+ return; const pair = low.t < high.t ? [low, high] : [high, low]; for (const point of pair)
100
+ parts.push(`${x(point.t).toFixed(1)},${y(point.value).toFixed(1)}`); };
101
+ for (const point of slice) {
102
+ const at = Math.floor(x(point.t));
103
+ if (at !== column) {
104
+ flush();
105
+ column = at;
106
+ low = high = point;
107
+ continue;
108
+ }
109
+ if (point.value < low.value)
110
+ low = point;
111
+ if (point.value > high.value)
112
+ high = point;
113
+ }
114
+ flush();
115
+ }
116
+ return `M${parts.join("L")}`;
117
+ }
118
+ const subscribeNothing = () => () => { };
119
+ function useReducedMotionSafe() {
120
+ const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
121
+ return !!useReducedMotion() && hydrated;
122
+ }
123
+ function useWidth() {
124
+ const node = useRef(null);
125
+ const [width, setWidth] = useState(0);
126
+ useLayoutEffect(() => {
127
+ const element = node.current;
128
+ if (!element)
129
+ return;
130
+ setWidth(element.clientWidth);
131
+ if (typeof ResizeObserver === "undefined")
132
+ return;
133
+ const observer = new ResizeObserver(() => setWidth(element.clientWidth));
134
+ observer.observe(element);
135
+ return () => observer.disconnect();
136
+ }, []);
137
+ return [node, width];
138
+ }
139
+ export function BrushChart({ data, label, unit = "", formatValue = value => grouped.format(value), formatTick = value => compact.format(value), formatDate = date => fullDate.format(date), annotations = [], range, defaultRange, onRangeChange, minSpan = 7 * DAY, height = 240, overviewHeight = 52, emptyLabel = "No data yet", ref, className }) {
140
+ const reduced = useReducedMotionSafe();
141
+ const uid = `brush${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
142
+ const figure = useRef(null);
143
+ const tip = useRef(null);
144
+ useImperativeHandle(ref, () => figure.current);
145
+ const inView = useInView(figure, { once: true, amount: .3 });
146
+ const [plot, width] = useWidth();
147
+ const [strip, stripWidth] = useWidth();
148
+ const points = useMemo(() => data.map(item => ({ t: time(item.date), value: item.value })), [data]);
149
+ // Zoomed out, a centred seven point average and its low to high band replace the raw line, so weekly noise reads as texture instead of a solid block.
150
+ const smooth = useMemo(() => {
151
+ const around = (index, read) => { const out = []; for (let at = Math.max(0, index - 3); at <= Math.min(points.length - 1, index + 3); at++)
152
+ out.push(read(at)); return out; };
153
+ const raw = points.map((_, index) => { const values = around(index, (at) => [points[at].value]).map(([value]) => value); return { value: values.reduce((sum, value) => sum + value, 0) / values.length, low: Math.min(...values), high: Math.max(...values) }; });
154
+ // The band is averaged once more so its edges ease instead of stepping.
155
+ return points.map((point, index) => { const near = around(index, (at) => [raw[at].low, raw[at].high]); return { t: point.t, value: raw[index].value, low: near.reduce((sum, [low]) => sum + low, 0) / near.length, high: near.reduce((sum, [, high]) => sum + high, 0) / near.length }; });
156
+ }, [points]);
157
+ const events = useMemo(() => annotations.map(item => ({ ...item, t: time(item.date) })).sort((a, b) => a.t - b.t), [annotations]);
158
+ const empty = points.length < 2;
159
+ const first = points[0]?.t ?? 0, lastT = points[points.length - 1]?.t ?? 1;
160
+ const whole = [first, lastT];
161
+ const fit = ([start, end]) => {
162
+ const span = clamp(end - start, Math.min(minSpan, lastT - first), lastT - first);
163
+ const s = clamp(start, first, lastT - span);
164
+ return [s, s + span];
165
+ };
166
+ // The window: follows the finger 1:1 while dragged, glides on a spring when set from outside or reset.
167
+ const [view, setView] = useState(() => range ?? defaultRange ?? whole);
168
+ const viewRef = useRef(view);
169
+ const flight = useRef(null);
170
+ const place = (next) => { viewRef.current = next; setView(next); };
171
+ const glideTo = (next) => {
172
+ flight.current?.stop();
173
+ const from = viewRef.current;
174
+ if (reduced) {
175
+ place(next);
176
+ return;
177
+ }
178
+ flight.current = animate(0, 1, { ...settle, onUpdate: t => place([from[0] + (next[0] - from[0]) * t, from[1] + (next[1] - from[1]) * t]) });
179
+ };
180
+ const commit = (next, smooth) => {
181
+ const target = fit(next);
182
+ if (smooth)
183
+ glideTo(target);
184
+ else {
185
+ flight.current?.stop();
186
+ place(target);
187
+ }
188
+ onRangeChange?.(target);
189
+ };
190
+ const rangeKey = range ? `${range[0]}:${range[1]}` : "";
191
+ useEffect(() => {
192
+ if (!range || empty)
193
+ return;
194
+ const target = fit(range), now = viewRef.current;
195
+ if (Math.abs(target[0] - now[0]) + Math.abs(target[1] - now[1]) > 1000)
196
+ glideTo(target);
197
+ }, [rangeKey, empty]); // eslint-disable-line react-hooks/exhaustive-deps
198
+ // Data that no longer covers the window pulls it back inside.
199
+ useEffect(() => { if (!empty) {
200
+ const target = fit(viewRef.current);
201
+ if (target[0] !== viewRef.current[0] || target[1] !== viewRef.current[1])
202
+ place(target);
203
+ } }, [first, lastT]); // eslint-disable-line react-hooks/exhaustive-deps
204
+ const [start, end] = view;
205
+ const span = Math.max(1, end - start);
206
+ const from = Math.max(0, lowerBound(points, start) - 1), to = Math.min(points.length, lowerBound(points, end) + 1);
207
+ let peak = 0;
208
+ for (let index = from; index < to; index++)
209
+ peak = Math.max(peak, points[index].value);
210
+ const scale = niceTop(peak * 1.04);
211
+ // The value axis springs to the tallest point in view, so zooming into a quiet stretch fills the plot.
212
+ const [top, setTop] = useState(scale.top);
213
+ const topFlight = useRef(null);
214
+ const aimed = useRef(scale.top);
215
+ useEffect(() => {
216
+ if (aimed.current === scale.top)
217
+ return;
218
+ aimed.current = scale.top;
219
+ topFlight.current?.stop();
220
+ if (reduced)
221
+ return;
222
+ topFlight.current = animate(top, scale.top, { ...settle, onUpdate: setTop });
223
+ }, [reduced, scale.top]); // eslint-disable-line react-hooks/exhaustive-deps
224
+ useEffect(() => () => { flight.current?.stop(); topFlight.current?.stop(); }, []);
225
+ const plotH = height;
226
+ const x = (t) => ((t - start) / span) * width;
227
+ const topNow = reduced ? scale.top : top;
228
+ const y = (value) => TOP + (1 - value / (topNow || 1)) * (plotH - TOP);
229
+ const line = width && !empty ? linePath(points, from, to, x, y, width) : "";
230
+ const area = line ? `${line}L${x(points[to - 1].t).toFixed(1)},${plotH}L${x(points[from].t).toFixed(1)},${plotH}Z` : "";
231
+ const trend = width && !empty ? linePath(smooth, from, to, x, y, width * 4) : "";
232
+ const band = trend ? `${linePath(smooth.map(point => ({ t: point.t, value: point.high })), from, to, x, y, width * 4)}L${linePath(smooth.map(point => ({ t: point.t, value: point.low })), from, to, x, y, width * 4).slice(1).split("L").reverse().join("L")}Z` : "";
233
+ // How much of the raw line shows: all of it past about four pixels a point, none below about three.
234
+ const pxPerPoint = empty ? width : (width * (lastT - first)) / (points.length - 1) / span;
235
+ const detail = clamp((pxPerPoint - 2.75) / 1.5, 0, 1);
236
+ const ticks = scale.top > 0 ? Array.from({ length: Math.round(scale.top / scale.step) + 1 }, (_, index) => clean(index * scale.step)) : [];
237
+ const xTicks = width && !empty ? timeTicks(start, end, width) : [];
238
+ // Overview: the whole series, drawn once per width.
239
+ const ox = (t) => ((t - first) / (lastT - first || 1)) * stripWidth;
240
+ const overview = useMemo(() => {
241
+ if (!stripWidth || empty)
242
+ return { line: "", area: "", marks: [] };
243
+ const max = niceTop(smooth.reduce((m, point) => Math.max(m, point.value), 0)).top || 1;
244
+ const oy = (value) => 4 + (1 - value / max) * (overviewHeight - 4);
245
+ const path = linePath(smooth, 0, smooth.length, ox, oy, stripWidth * 4);
246
+ const marks = events.map(event => { const at = clamp(lowerBound(smooth, event.t), 0, smooth.length - 1); return { t: event.t, x: ox(event.t), y: oy(smooth[at].value) }; });
247
+ return { line: path, area: `${path}L${stripWidth},${overviewHeight}L0,${overviewHeight}Z`, marks };
248
+ }, [smooth, events, stripWidth, overviewHeight, empty]); // eslint-disable-line react-hooks/exhaustive-deps
249
+ const wx0 = ox(start), wx1 = ox(end);
250
+ // The line draws in from the left once, the first time it is seen.
251
+ const drawn = useMotionValue(0);
252
+ const [reveal, setReveal] = useState(0);
253
+ useEffect(() => {
254
+ if (empty)
255
+ return;
256
+ if (reduced) {
257
+ drawn.jump(1);
258
+ return;
259
+ }
260
+ if (!inView || drawn.get() >= 1)
261
+ return;
262
+ const controls = animate(drawn, 1, { ...draw, onUpdate: setReveal });
263
+ return () => controls.stop();
264
+ }, [drawn, empty, inView, reduced]);
265
+ // Crosshair on the nearest point in view; the tooltip glides beside it and never leaves the plot.
266
+ const [active, setActive] = useState(null);
267
+ const [hoverEvent, setHoverEvent] = useState(null);
268
+ const index = active !== null && active >= from && active < to ? active : null;
269
+ const nearest = (clientX) => {
270
+ const rect = plot.current?.getBoundingClientRect();
271
+ if (!rect?.width || empty)
272
+ return null;
273
+ const t = start + ((clientX - rect.left) / rect.width) * span;
274
+ const at = clamp(lowerBound(points, t), 1, points.length - 1);
275
+ return clamp(t - points[at - 1].t < points[at].t - t ? at - 1 : at, Math.max(0, from), to - 1);
276
+ };
277
+ const cx = index === null ? 0 : x(points[index].t), cy = index === null ? 0 : y(smooth[index].value + (points[index].value - smooth[index].value) * detail);
278
+ const eventAt = index === null ? null : events.find(event => Math.abs(event.t - points[index].t) < DAY / 2) ?? null;
279
+ const shownEvent = hoverEvent !== null ? events[hoverEvent] : null;
280
+ const tipX = useMotionValue(0), tipY = useMotionValue(0);
281
+ const tipSpringX = useSpring(tipX, follow), tipSpringY = useSpring(tipY, follow);
282
+ const tipOn = index !== null || shownEvent !== null;
283
+ const wasOn = useRef(false);
284
+ useLayoutEffect(() => {
285
+ const bubble = tip.current;
286
+ if (!tipOn || !bubble || !width) {
287
+ wasOn.current = false;
288
+ return;
289
+ }
290
+ const ax = shownEvent ? x(shownEvent.t) : cx, ay = shownEvent ? TOP : cy;
291
+ const tw = bubble.offsetWidth, th = bubble.offsetHeight;
292
+ let left = ax + GAP;
293
+ if (left + tw > width)
294
+ left = ax - GAP - tw;
295
+ let topY = ay - th - GAP;
296
+ if (topY < 0)
297
+ topY = ay + GAP;
298
+ tipX.set(clamp(left, 0, Math.max(0, width - tw)));
299
+ tipY.set(clamp(topY, 0, Math.max(0, plotH - th)));
300
+ if (!wasOn.current || reduced) {
301
+ tipSpringX.jump(tipX.get());
302
+ tipSpringY.jump(tipY.get());
303
+ }
304
+ wasOn.current = true;
305
+ });
306
+ const onPlotMove = (event) => { if (event.pointerType === "mouse" || event.buttons)
307
+ setActive(nearest(event.clientX)); };
308
+ const onPlotKey = (event) => {
309
+ if (empty)
310
+ return;
311
+ if (event.key === "Escape" && index !== null) {
312
+ event.preventDefault();
313
+ setActive(null);
314
+ return;
315
+ }
316
+ const current = index ?? to - 1, page = Math.max(1, Math.round((to - from) / 8));
317
+ const next = { ArrowLeft: current - 1, ArrowRight: index === null ? to - 1 : current + 1, PageDown: current - page, PageUp: current + page, Home: from, End: to - 1 }[event.key];
318
+ if (next === undefined)
319
+ return;
320
+ event.preventDefault();
321
+ setActive(clamp(next, Math.max(0, from), to - 1));
322
+ };
323
+ // Overview gestures: grab an edge to resize, the window to pan, or empty track to draw a new window. A tap recentres; a double tap resets.
324
+ const drag = useRef(null);
325
+ const toTime = (clientX) => { const rect = strip.current?.getBoundingClientRect(); return rect?.width ? first + clamp((clientX - rect.left) / rect.width, 0, 1) * (lastT - first) : first; };
326
+ const onStripDown = (event) => {
327
+ if (empty || event.button > 0)
328
+ return;
329
+ const rect = strip.current.getBoundingClientRect(), px = event.clientX - rect.left;
330
+ const mode = Math.abs(px - wx0) <= GRIP && px <= (wx0 + wx1) / 2 ? "start" : Math.abs(px - wx1) <= GRIP ? "end" : px > wx0 && px < wx1 ? "pan" : "new";
331
+ drag.current = { mode, pointerX: event.clientX, from: viewRef.current, anchor: toTime(event.clientX), moved: false };
332
+ event.currentTarget.setPointerCapture?.(event.pointerId);
333
+ flight.current?.stop();
334
+ };
335
+ const onStripMove = (event) => {
336
+ const current = drag.current;
337
+ if (!current)
338
+ return;
339
+ if (!current.moved && Math.abs(event.clientX - current.pointerX) < 3)
340
+ return;
341
+ current.moved = true;
342
+ const t = toTime(event.clientX), shift = t - current.anchor;
343
+ const [s, e] = current.from, least = Math.min(minSpan, lastT - first);
344
+ const next = current.mode === "pan" ? [s + shift, e + shift]
345
+ : current.mode === "start" ? [clamp(t, first, e - least), e]
346
+ : current.mode === "end" ? [s, clamp(t, s + least, lastT)]
347
+ : [Math.min(current.anchor, t), Math.max(current.anchor, t)];
348
+ if (current.mode === "new" && next[1] - next[0] < least) {
349
+ if (t >= current.anchor)
350
+ next[1] = next[0] + least;
351
+ else
352
+ next[0] = next[1] - least;
353
+ }
354
+ commit(next, false);
355
+ };
356
+ const onStripUp = () => {
357
+ const current = drag.current;
358
+ drag.current = null;
359
+ if (!current || current.moved || current.mode !== "new")
360
+ return;
361
+ const half = (viewRef.current[1] - viewRef.current[0]) / 2;
362
+ commit([current.anchor - half, current.anchor + half], true);
363
+ };
364
+ const reset = () => commit(whole, true);
365
+ const stepSize = Math.max(DAY, (lastT - first) / Math.max(1, points.length - 1));
366
+ const onHandleKey = (event, edge) => {
367
+ const [s, e] = viewRef.current, win = e - s;
368
+ const big = event.shiftKey || event.key.startsWith("Page");
369
+ const delta = { ArrowLeft: -1, ArrowDown: -1, PageDown: -1, ArrowRight: 1, ArrowUp: 1, PageUp: 1 }[event.key];
370
+ let next = null;
371
+ if (event.key === "Escape" || event.key === "0")
372
+ next = whole;
373
+ else if (edge === "window" && (event.key === "+" || event.key === "="))
374
+ next = [s + win / 4, e - win / 4];
375
+ else if (edge === "window" && (event.key === "-" || event.key === "_"))
376
+ next = [s - win / 2, e + win / 2];
377
+ else if (event.key === "Home")
378
+ next = edge === "end" ? [s, s + Math.min(minSpan, lastT - first)] : [first, edge === "window" ? first + win : e];
379
+ else if (event.key === "End")
380
+ next = edge === "start" ? [e - Math.min(minSpan, lastT - first), e] : [edge === "window" ? lastT - win : s, lastT];
381
+ else if (delta) {
382
+ const move = delta * (edge === "window" ? win * (big ? .5 : .1) : stepSize * (big ? 10 : 1));
383
+ next = edge === "window" ? [s + move, e + move] : edge === "start" ? [Math.min(s + move, e - minSpan), e] : [s, Math.max(e + move, s + minSpan)];
384
+ }
385
+ if (!next)
386
+ return;
387
+ event.preventDefault();
388
+ commit(next, false);
389
+ };
390
+ const format = (value) => `${formatValue(value)}${unit ? ` ${unit}` : ""}`;
391
+ const reading = index === null ? null : points[index];
392
+ const previous = index !== null && index >= 7 ? points[index - 7] : null;
393
+ const change = reading && previous && previous.value ? (reading.value - previous.value) / previous.value : null;
394
+ const windowText = `${shortDate.format(start)} to ${shortDate.format(end)}`;
395
+ const inWindow = events.filter(event => event.t >= start && event.t <= end);
396
+ // Event labels are placed left to right and skipped when they would collide with the one before.
397
+ const labelled = new Map();
398
+ let edge = -Infinity;
399
+ inWindow.forEach((event, at) => {
400
+ const px = x(event.t), w = event.label.length * 6.2 + 4, next = at + 1 < inWindow.length ? x(inWindow[at + 1].t) - 10 : width;
401
+ if (px + 14 >= edge && px + 14 + w <= next) {
402
+ labelled.set(event.t, false);
403
+ edge = px + 14 + w + 8;
404
+ return;
405
+ }
406
+ if (px - 14 - w >= Math.max(0, edge) && (at === 0 || px - 14 - w >= x(inWindow[at - 1].t) + 12)) {
407
+ labelled.set(event.t, true);
408
+ edge = px + 12;
409
+ return;
410
+ }
411
+ edge = Math.max(edge, px + 12);
412
+ });
413
+ const announce = reading ? `${formatDate(new Date(reading.t))}, ${format(reading.value)}${eventAt ? `. ${eventAt.label}` : ""}` : "";
414
+ const summary = empty ? `${label}. ${emptyLabel}.` : `${label}, ${shortDate.format(first)} to ${shortDate.format(lastT)}. Showing ${windowText}.`;
415
+ return _jsxs("figure", { ref: figure, className: [styles.figure, className].filter(Boolean).join(" "), "aria-label": label, children: [_jsxs("div", { className: styles.chart, "data-active": tipOn || undefined, children: [_jsxs("div", { ref: plot, className: styles.plot, style: { height: plotH }, role: "group", tabIndex: empty ? -1 : 0, "aria-roledescription": "chart", "aria-label": `${label}, ${windowText}. Use left and right arrows to read values.`, onPointerMove: onPlotMove, onPointerDown: event => { if (event.pointerType !== "mouse") {
416
+ event.currentTarget.setPointerCapture?.(event.pointerId);
417
+ setActive(nearest(event.clientX));
418
+ } }, onPointerUp: event => { if (event.pointerType !== "mouse")
419
+ setActive(null); }, onPointerCancel: () => setActive(null), onPointerLeave: event => { if (event.pointerType === "mouse")
420
+ setActive(null); }, onKeyDown: onPlotKey, onBlur: () => setActive(null), onDoubleClick: reset, onFocus: event => { if (event.currentTarget.matches(":focus-visible") && !empty)
421
+ setActive(current => current ?? to - 1); }, children: [_jsxs("svg", { className: styles.svg, width: "100%", height: plotH, "aria-hidden": "true", focusable: "false", children: [_jsx("defs", { children: _jsx("clipPath", { id: `${uid}-plot`, children: _jsx("rect", { x: 0, y: -8, width: Math.max(0, width * (reduced ? 1 : reveal)), height: plotH + 16 }) }) }), ticks.slice(1).map(value => _jsx("line", { className: styles.grid, x1: 0, x2: "100%", y1: Math.round(y(value)) + .5, y2: Math.round(y(value)) + .5 }, value)), _jsx("line", { className: styles.baseline, x1: 0, x2: "100%", y1: plotH - .5, y2: plotH - .5 }), inWindow.map(event => _jsx("line", { className: styles.eventRule, x1: Math.round(x(event.t)) + .5, x2: Math.round(x(event.t)) + .5, y1: TOP - 10, y2: plotH, "data-active": shownEvent?.t === event.t || eventAt?.t === event.t || undefined }, event.t)), _jsxs("g", { clipPath: `url(#${uid}-plot)`, children: [_jsx("path", { className: styles.band, d: band, style: { opacity: 1 - detail } }), _jsx("path", { className: styles.area, d: area, style: { opacity: detail } }), _jsx("path", { className: styles.trend, d: trend, style: { opacity: 1 - detail } }), _jsx("path", { className: styles.line, d: line, style: { opacity: detail } })] }), _jsx("line", { className: styles.crosshair, x1: Math.round(cx) + .5, x2: Math.round(cx) + .5, y1: TOP - 10, y2: plotH }), _jsx("circle", { className: styles.dot, cx: cx, cy: cy, r: 4 })] }), inWindow.map(event => {
422
+ const px = x(event.t), i = events.indexOf(event);
423
+ return _jsxs("span", { className: styles.event, style: { left: px }, "data-labelled": labelled.has(event.t) || undefined, "data-flip": labelled.get(event.t) || undefined, "data-active": shownEvent?.t === event.t || eventAt?.t === event.t || undefined, "aria-hidden": "true", onPointerEnter: () => setHoverEvent(i), onPointerLeave: () => setHoverEvent(null), onPointerDown: event => event.stopPropagation(), children: [_jsx("span", { className: styles.eventDot }), _jsx("span", { className: styles.eventLabel, children: event.label })] }, event.t);
424
+ }), _jsx(motion.div, { ref: tip, className: styles.tooltip, style: { x: reduced ? tipX : tipSpringX, y: reduced ? tipY : tipSpringY }, "aria-hidden": "true", children: shownEvent ? _jsxs(_Fragment, { children: [_jsx("p", { className: styles.tipTitle, children: formatDate(new Date(shownEvent.t)) }), _jsx("p", { className: styles.tipValue, children: shownEvent.label }), shownEvent.description && _jsx("p", { className: styles.tipNote, children: shownEvent.description })] }) : reading ? _jsxs(_Fragment, { children: [_jsx("p", { className: styles.tipTitle, children: formatDate(new Date(reading.t)) }), _jsx("p", { className: styles.tipValue, children: format(reading.value) }), detail < 1 && index !== null && _jsx("p", { className: styles.tipNote, children: `${formatValue(Math.round(smooth[index].value))} seven day average` }), change !== null && _jsx("p", { className: styles.tipNote, children: `${change >= 0 ? "+" : "\u2212"}${Math.abs(change * 100).toFixed(1)}% vs a week earlier` }), eventAt && _jsxs("p", { className: styles.tipEvent, children: [_jsx("span", { className: styles.tipEventDot }), eventAt.label] })] }) : null }), empty && _jsx("p", { className: styles.message, children: emptyLabel })] }), _jsx("div", { className: styles.gutter, "aria-hidden": "true", children: ticks.map(value => _jsx("span", { className: styles.tick, style: { top: y(value) }, children: formatTick(value) }, value)) }), _jsx("div", { className: styles.axis, "aria-hidden": "true", children: xTicks.map(tick => { const px = x(tick.t); return _jsx("span", { className: styles.axisLabel, style: { left: px, translate: `${px < 24 ? 0 : px > width - 24 ? -100 : -50}% 0` }, children: tick.label }, tick.t); }) }), _jsxs("div", { ref: strip, className: styles.strip, style: { height: overviewHeight }, onPointerDown: onStripDown, onPointerMove: onStripMove, onPointerUp: onStripUp, onPointerCancel: () => { drag.current = null; }, onDoubleClick: reset, children: [_jsxs("svg", { className: styles.svg, width: "100%", height: overviewHeight, "aria-hidden": "true", focusable: "false", children: [_jsx("defs", { children: _jsx("clipPath", { id: `${uid}-window`, children: _jsx("rect", { x: wx0, y: -2, width: Math.max(0, wx1 - wx0), height: overviewHeight + 4 }) }) }), _jsx("path", { className: styles.overviewArea, d: overview.area }), _jsx("path", { className: styles.overviewLine, d: overview.line }), _jsxs("g", { clipPath: `url(#${uid}-window)`, children: [_jsx("path", { className: styles.overviewAreaIn, d: overview.area }), _jsx("path", { className: styles.overviewLineIn, d: overview.line })] }), overview.marks.map(mark => _jsx("circle", { className: styles.overviewEvent, cx: mark.x, cy: mark.y, r: 2.5 }, mark.t))] }), !empty && _jsxs(_Fragment, { children: [_jsx("div", { className: styles.shade, "data-edge": "start", style: { left: 0, width: Math.max(0, wx0) } }), _jsx("div", { className: styles.shade, "data-edge": "end", style: { left: wx1, right: 0 } }), _jsx("div", { className: styles.window, style: { left: wx0, width: Math.max(0, wx1 - wx0) }, role: "slider", tabIndex: 0, "aria-label": `${label} window`, "aria-roledescription": "range window", "aria-valuemin": first, "aria-valuemax": lastT, "aria-valuenow": Math.round(start), "aria-valuetext": windowText, onKeyDown: event => onHandleKey(event, "window") }), _jsx("div", { className: styles.handle, style: { left: wx0 }, role: "slider", tabIndex: 0, "aria-label": "Window start", "aria-valuemin": first, "aria-valuemax": Math.round(end), "aria-valuenow": Math.round(start), "aria-valuetext": shortDate.format(start), onKeyDown: event => onHandleKey(event, "start"), children: _jsx("span", { className: styles.grip }) }), _jsx("div", { className: styles.handle, style: { left: wx1 }, role: "slider", tabIndex: 0, "aria-label": "Window end", "aria-valuemin": Math.round(start), "aria-valuemax": lastT, "aria-valuenow": Math.round(end), "aria-valuetext": shortDate.format(end), onKeyDown: event => onHandleKey(event, "end"), children: _jsx("span", { className: styles.grip }) })] })] })] }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", "aria-atomic": "true", children: announce }), _jsx("p", { className: styles.srOnly, children: summary }), !empty && _jsx("div", { className: styles.srOnly, children: _jsxs("table", { children: [_jsx("caption", { children: `${label}, ${windowText}` }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Date" }), _jsx("th", { scope: "col", children: label }), _jsx("th", { scope: "col", children: "Event" })] }) }), _jsx("tbody", { children: points.slice(lowerBound(points, start), lowerBound(points, end + 1)).map(point => { const event = events.find(item => Math.abs(item.t - point.t) < DAY / 2); return _jsxs("tr", { children: [_jsx("th", { scope: "row", children: shortDate.format(point.t) }), _jsx("td", { children: format(point.value) }), _jsx("td", { children: event ? `${event.label}${event.description ? `. ${event.description}` : ""}` : "" })] }, point.t); }) })] }) })] });
425
+ }
426
+ export default BrushChart;
@@ -0,0 +1,69 @@
1
+ .figure { display: grid; min-width: 0; margin: 0; color: var(--foreground); font-family: var(--font-body); letter-spacing: var(--tracking-body); }
2
+
3
+ /* Series colors (--series-1 to --series-4) come from registry/foundation.css. */
4
+
5
+ /* Plot with a right-hand value axis, the date axis beneath, and the overview strip under both. */
6
+ .chart { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 44px; grid-template-rows: auto 28px auto; row-gap: 0; min-width: 0; }
7
+ .plot { position: relative; grid-area: 1 / 1; min-width: 0; outline: none; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; cursor: crosshair; }
8
+ .svg { display: block; overflow: visible; }
9
+ .grid { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; }
10
+ .baseline { stroke: var(--border-strong); stroke-width: 1; shape-rendering: crispEdges; }
11
+ .line { fill: none; stroke: var(--series-1); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
12
+ .area { fill: color-mix(in oklab, var(--series-1) 10%, transparent); stroke: none; }
13
+ .trend { fill: none; stroke: var(--series-1); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
14
+ .band { fill: color-mix(in oklab, var(--series-1) 14%, transparent); stroke: none; }
15
+ .crosshair { stroke: var(--foreground); stroke-opacity: .45; stroke-width: 1; shape-rendering: crispEdges; opacity: 0; transition: opacity var(--duration-instant) var(--ease-standard); pointer-events: none; }
16
+ .dot { fill: var(--series-1); stroke: var(--surface); stroke-width: 2; opacity: 0; transition: opacity var(--duration-instant) var(--ease-standard); pointer-events: none; }
17
+ .chart[data-active] .crosshair, .chart[data-active] .dot { opacity: 1; }
18
+
19
+ /* Events: a hairline through the plot, a dot in the second chart series on top, and a label when there is room for it. */
20
+ .eventRule { stroke: var(--border-strong); stroke-width: 1; shape-rendering: crispEdges; transition: stroke var(--duration-fast) var(--ease-standard); }
21
+ .eventRule[data-active] { stroke: var(--series-2); }
22
+ .event { position: absolute; top: 6px; display: flex; box-sizing: border-box; min-height: 24px; align-items: center; gap: 6px; padding: 0 6px 0 0; margin-left: -12px; border: 0; background: transparent; color: var(--text-secondary); cursor: default; font: inherit; font-size: var(--text-xs); line-height: 1; white-space: nowrap; -webkit-tap-highlight-color: transparent; }
23
+ .event[data-flip] { flex-direction: row-reverse; padding: 0 0 0 6px; margin-left: 0; translate: calc(-100% + 12px) 0; }
24
+ .eventDot { display: grid; width: 24px; height: 24px; flex: none; place-items: center; }
25
+ .eventDot::before { width: 7px; height: 7px; border-radius: 50%; corner-shape: round; background: var(--series-2); box-shadow: 0 0 0 2px var(--surface); content: ""; transition: transform var(--duration-fast) var(--ease-standard); }
26
+ .event[data-active] .eventDot::before { transform: scale(1.3); }
27
+ .eventLabel { opacity: 0; transition: opacity var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
28
+ .event[data-labelled] .eventLabel { opacity: 1; }
29
+ .event[data-active] .eventLabel { color: var(--foreground); }
30
+
31
+ .tooltip { position: absolute; top: 0; left: 0; z-index: 2; display: grid; min-width: 132px; max-width: min(220px, 100%); gap: 2px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); opacity: 0; pointer-events: none; scale: .96; transition: opacity var(--duration-fast) var(--ease-standard), scale var(--duration-fast) var(--ease-standard); }
32
+ .chart[data-active] .tooltip { opacity: 1; scale: 1; }
33
+ .tipTitle { margin: 0 0 2px; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); white-space: nowrap; font-variant-numeric: tabular-nums; }
34
+ .tipValue { margin: 0; color: var(--foreground); font-size: var(--text-base); font-weight: 500; line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
35
+ .tipNote { margin: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
36
+ .tipEvent { display: flex; align-items: center; gap: 6px; margin: 4px 0 0; color: var(--foreground); font-size: var(--text-xs); line-height: var(--leading-body); }
37
+ .tipEventDot { width: 7px; height: 7px; flex: none; border-radius: 50%; corner-shape: round; background: var(--series-2); }
38
+
39
+ .message { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--text-muted); font-size: var(--text-sm); pointer-events: none; }
40
+
41
+ .gutter { position: relative; grid-area: 1 / 2; min-width: 0; }
42
+ .tick { position: absolute; left: 10px; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; translate: 0 -50%; pointer-events: none; }
43
+ .axis { position: relative; grid-area: 2 / 1; min-width: 0; }
44
+ .axisLabel { position: absolute; top: 9px; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; }
45
+
46
+ /* Overview: the whole history, with everything outside the window stepped back. */
47
+ .strip { --strip-radius: var(--radius-detail); position: relative; grid-area: 3 / 1; min-width: 0; margin-top: 8px; border-radius: var(--strip-radius); background: color-mix(in oklab, var(--foreground) 2.5%, transparent); cursor: crosshair; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
48
+ /* The overview drawing is clipped to the strip's corners; the handles stay outside the clip so they can overhang the ends. */
49
+ .strip > .svg { clip-path: inset(0 round var(--strip-radius)); }
50
+ .overviewLine { fill: none; stroke: color-mix(in oklab, var(--foreground) 28%, var(--surface)); stroke-width: 1; stroke-linejoin: round; }
51
+ .overviewArea { fill: color-mix(in oklab, var(--foreground) 4%, transparent); }
52
+ .overviewLineIn { fill: none; stroke: var(--series-1); stroke-width: 1; stroke-linejoin: round; }
53
+ .overviewAreaIn { fill: color-mix(in oklab, var(--series-1) 12%, transparent); }
54
+ .overviewEvent { fill: var(--series-2); stroke: var(--surface); stroke-width: 1.5; }
55
+ .shade { position: absolute; top: 0; bottom: 0; background: color-mix(in oklab, var(--surface) 45%, transparent); pointer-events: none; }
56
+ .shade[data-edge="start"] { border-radius: var(--strip-radius) 0 0 var(--strip-radius); }
57
+ .shade[data-edge="end"] { border-radius: 0 var(--strip-radius) var(--strip-radius) 0; }
58
+ .window { position: absolute; top: 0; bottom: 0; border-radius: var(--strip-radius); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--series-1) 45%, transparent); cursor: grab; outline: none; transition: box-shadow var(--duration-fast) var(--ease-standard); }
59
+ .window:active { cursor: grabbing; }
60
+ .handle { position: absolute; top: 50%; display: grid; width: 28px; height: 44px; place-items: center; margin-left: -14px; translate: 0 -50%; cursor: ew-resize; outline: none; touch-action: none; }
61
+ .grip { width: 6px; height: 22px; border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: 0 0 0 1px color-mix(in oklab, var(--foreground) 26%, transparent), var(--shadow-resting); transition: transform var(--duration-instant) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
62
+ .handle:active .grip { transform: scaleY(1.12); }
63
+ @media (hover: hover) and (pointer: fine) { .handle:hover .grip { transform: scaleY(1.12); } }
64
+
65
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
66
+
67
+ @media (prefers-reduced-motion: reduce) {
68
+ .crosshair, .dot, .tooltip, .eventRule, .eventLabel, .eventDot::before, .grip, .window { transition: none; }
69
+ }
@@ -0,0 +1,10 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
3
+ export type ButtonSize = "sm" | "md" | "lg";
4
+ export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onDrag" | "onDragEnd" | "onDragStart" | "onAnimationStart"> {
5
+ variant?: ButtonVariant;
6
+ size?: ButtonSize;
7
+ loading?: boolean;
8
+ }
9
+ export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
10
+ export default Button;
@@ -0,0 +1,97 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, isValidElement, useCallback, useEffect, useLayoutEffect, useRef } from "react";
4
+ import { AnimatePresence, animate, motion, useIsPresent, useMotionValue, useReducedMotion } from "motion/react";
5
+ import { motionTokens } from "../../lib/motion-tokens.js";
6
+ import styles from "./button.module.css";
7
+ /** Icon buttons press a little deeper, wide buttons a little less, so every size reads as the same push. */
8
+ const pressVariants = {
9
+ pressed: (button) => {
10
+ const width = button.current?.offsetWidth ?? 0;
11
+ return { scale: width > 220 ? .985 : width && width <= 48 ? .96 : .97, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } };
12
+ },
13
+ };
14
+ const rest = { opacity: 1, y: 0, scale: 1, filter: "blur(0px)" };
15
+ /** Text rises about .3em out of a soft blur; the outgoing label lifts away a little faster. */
16
+ const textIn = { opacity: 0, y: 4, filter: `blur(${motionTokens.blur.soft}px)` };
17
+ const textOut = { opacity: 0, y: -3, filter: `blur(${motionTokens.blur.soft}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
18
+ const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
19
+ const iconOut = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
20
+ const fadeIn = { ...rest, opacity: 0 };
21
+ const fadeOut = { opacity: 0, transition: { duration: motionTokens.duration.instant } };
22
+ /** Scale rides the spring; opacity and blur tween so blur never overshoots below zero. */
23
+ const iconEnter = { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } };
24
+ /** A key for the label content: text plus element names, so a new label or icon crossfades while prop-only updates stay in place. */
25
+ function labelKey(node) {
26
+ if (node == null || typeof node === "boolean")
27
+ return "";
28
+ if (typeof node === "string" || typeof node === "number" || typeof node === "bigint")
29
+ return String(node);
30
+ if (Array.isArray(node))
31
+ return node.map(labelKey).join("");
32
+ if (!isValidElement(node))
33
+ return "";
34
+ const type = node.type;
35
+ return `<${typeof type === "string" ? type : type?.displayName ?? type?.name ?? ""}>${labelKey(node.props.children)}`;
36
+ }
37
+ /** Springs the slot to the natural width of the incoming label when it changes, so a new label never snaps the button's size.
38
+ * The outgoing label is popped out of flow at once, so it never holds the old width. Other resizes (a late web font, a parent reflow) jump straight to the new width, so nothing wobbles on first paint. */
39
+ function useMorphWidth(content, key, reduced) {
40
+ const width = useMotionValue("auto");
41
+ const lastKey = useRef(key), armedUntil = useRef(0);
42
+ useLayoutEffect(() => {
43
+ if (lastKey.current === key)
44
+ return;
45
+ lastKey.current = key;
46
+ armedUntil.current = performance.now() + 700;
47
+ }, [key]);
48
+ useEffect(() => {
49
+ const node = content.current, slot = node?.parentElement;
50
+ if (!node || !slot || typeof ResizeObserver === "undefined")
51
+ return;
52
+ let measured = false;
53
+ const observer = new ResizeObserver(([entry]) => {
54
+ if (!entry)
55
+ return;
56
+ const next = entry.contentRect.width;
57
+ if (!next || !measured || reduced || performance.now() > armedUntil.current) {
58
+ measured = next > 0;
59
+ width.jump(next || "auto");
60
+ delete slot.dataset.morphing;
61
+ return;
62
+ }
63
+ slot.dataset.morphing = "";
64
+ animate(width, next, { ...motionTokens.spring.morph, onComplete: () => { delete slot.dataset.morphing; } });
65
+ });
66
+ observer.observe(node);
67
+ return () => observer.disconnect();
68
+ }, [content, reduced, width]);
69
+ return width;
70
+ }
71
+ function LabelPhase({ children, icon, reduced, ref }) {
72
+ const present = useIsPresent();
73
+ return _jsx(motion.span, { ref: ref, className: styles.labelPhase, "aria-hidden": present ? undefined : true, initial: reduced ? fadeIn : icon ? iconIn : textIn, animate: rest, exit: reduced ? fadeOut : icon ? iconOut : textOut, transition: reduced ? { duration: motionTokens.duration.instant } : icon ? iconEnter : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: children });
74
+ }
75
+ export const Button = forwardRef(function Button({ className, variant = "primary", size = "md", loading = false, disabled, children, onClick, ...props }, ref) {
76
+ const reduceMotion = useReducedMotion() ?? false;
77
+ const buttonRef = useRef(null);
78
+ const contentRef = useRef(null);
79
+ const key = labelKey(children);
80
+ const width = useMorphWidth(contentRef, key, reduceMotion);
81
+ const setRefs = useCallback((node) => {
82
+ buttonRef.current = node;
83
+ if (typeof ref === "function")
84
+ ref(node);
85
+ else if (ref)
86
+ ref.current = node;
87
+ }, [ref]);
88
+ // A trigger that anchors a menu, popover, or dialog keeps its rect still while pressed, so the layer never measures a scaled anchor.
89
+ // Radix triggers (asChild) pass aria-haspopup or data-state through; the stylesheet repeats the guard.
90
+ const popup = props["aria-haspopup"];
91
+ const anchorsLayer = (popup !== undefined && popup !== false && popup !== "false") || props.role === "combobox" || props["data-state"] !== undefined;
92
+ const inert = disabled || loading || props["aria-disabled"] === true || props["aria-disabled"] === "true";
93
+ const classes = [styles.button, styles[variant], styles[size], className].filter(Boolean).join(" ");
94
+ return (_jsxs(motion.button, { ref: setRefs, tabIndex: props.tabIndex ?? 0, className: classes, "data-variant": variant, disabled: disabled, "aria-busy": loading || undefined, custom: buttonRef, variants: pressVariants, whileTap: reduceMotion || anchorsLayer || inert ? undefined : "pressed", transition: motionTokens.spring.snappy, ...props, "aria-disabled": loading || props["aria-disabled"] || undefined, onClick: loading ? event => event.preventDefault() : onClick, children: [_jsx(AnimatePresence, { initial: false, children: loading ? _jsx(motion.span, { className: styles.loader, "aria-hidden": "true", initial: reduceMotion ? { opacity: 0 } : { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: reduceMotion ? fadeOut : { ...iconOut, scale: .8 }, transition: reduceMotion ? { duration: motionTokens.duration.instant } : iconEnter, children: _jsx("span", { className: styles.spinner }) }, "loader") : null }), _jsx(motion.span, { className: [styles.labelSlot, loading ? styles.loadingLabel : ""].filter(Boolean).join(" "), style: { width }, children: _jsx("span", { ref: contentRef, className: styles.labelContent, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(LabelPhase, { icon: !/\S/.test(key.replace(/<[^>]*>/g, "")), reduced: reduceMotion, children: children }, key) }) }) })] }));
95
+ });
96
+ Button.displayName = "Button";
97
+ export default Button;