@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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 (354) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/check.mjs +236 -0
  4. package/components/accordion/accordion.d.ts +13 -0
  5. package/components/accordion/accordion.js +35 -0
  6. package/components/accordion/accordion.module.css +15 -0
  7. package/components/action-button/action-button.d.ts +11 -0
  8. package/components/action-button/action-button.js +57 -0
  9. package/components/action-button/action-button.module.css +15 -0
  10. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  11. package/components/activity-heatmap/activity-heatmap.js +303 -0
  12. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  13. package/components/alert/alert.d.ts +13 -0
  14. package/components/alert/alert.js +79 -0
  15. package/components/alert/alert.module.css +15 -0
  16. package/components/animated-counter/animated-counter.d.ts +13 -0
  17. package/components/animated-counter/animated-counter.js +73 -0
  18. package/components/animated-counter/animated-counter.module.css +12 -0
  19. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  20. package/components/announcement-bar/announcement-bar.js +187 -0
  21. package/components/announcement-bar/announcement-bar.module.css +60 -0
  22. package/components/avatar/avatar.d.ts +8 -0
  23. package/components/avatar/avatar.js +18 -0
  24. package/components/avatar/avatar.module.css +60 -0
  25. package/components/avatar-group/avatar-group.d.ts +13 -0
  26. package/components/avatar-group/avatar-group.js +24 -0
  27. package/components/avatar-group/avatar-group.module.css +27 -0
  28. package/components/badge/badge.d.ts +10 -0
  29. package/components/badge/badge.js +66 -0
  30. package/components/badge/badge.module.css +52 -0
  31. package/components/bar-chart/bar-chart.d.ts +32 -0
  32. package/components/bar-chart/bar-chart.js +327 -0
  33. package/components/bar-chart/bar-chart.module.css +49 -0
  34. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  35. package/components/billing-toggle/billing-toggle.js +83 -0
  36. package/components/billing-toggle/billing-toggle.module.css +139 -0
  37. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  38. package/components/bottom-sheet/bottom-sheet.js +386 -0
  39. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  40. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  41. package/components/breadcrumb/breadcrumb.js +19 -0
  42. package/components/breadcrumb/breadcrumb.module.css +12 -0
  43. package/components/brush-chart/brush-chart.d.ts +45 -0
  44. package/components/brush-chart/brush-chart.js +426 -0
  45. package/components/brush-chart/brush-chart.module.css +69 -0
  46. package/components/button/button.d.ts +10 -0
  47. package/components/button/button.js +97 -0
  48. package/components/button/button.module.css +39 -0
  49. package/components/button-group/button-group.d.ts +59 -0
  50. package/components/button-group/button-group.js +280 -0
  51. package/components/button-group/button-group.module.css +87 -0
  52. package/components/button-link/button-link.d.ts +24 -0
  53. package/components/button-link/button-link.js +11 -0
  54. package/components/button-link/button-link.module.css +7 -0
  55. package/components/calendar/calendar.d.ts +22 -0
  56. package/components/calendar/calendar.js +253 -0
  57. package/components/calendar/calendar.module.css +51 -0
  58. package/components/card/card.d.ts +19 -0
  59. package/components/card/card.js +108 -0
  60. package/components/card/card.module.css +51 -0
  61. package/components/card-stack/card-stack.d.ts +34 -0
  62. package/components/card-stack/card-stack.js +291 -0
  63. package/components/card-stack/card-stack.module.css +63 -0
  64. package/components/carousel/carousel.d.ts +28 -0
  65. package/components/carousel/carousel.js +332 -0
  66. package/components/carousel/carousel.module.css +42 -0
  67. package/components/chat-thread/chat-thread.d.ts +135 -0
  68. package/components/chat-thread/chat-thread.js +406 -0
  69. package/components/chat-thread/chat-thread.module.css +242 -0
  70. package/components/checkbox/checkbox.d.ts +7 -0
  71. package/components/checkbox/checkbox.js +23 -0
  72. package/components/checkbox/checkbox.module.css +15 -0
  73. package/components/chip-group/chip-group.d.ts +24 -0
  74. package/components/chip-group/chip-group.js +196 -0
  75. package/components/chip-group/chip-group.module.css +38 -0
  76. package/components/code-block/code-block.d.ts +11 -0
  77. package/components/code-block/code-block.js +133 -0
  78. package/components/code-block/code-block.module.css +43 -0
  79. package/components/color-picker/color-picker.d.ts +40 -0
  80. package/components/color-picker/color-picker.js +449 -0
  81. package/components/color-picker/color-picker.module.css +111 -0
  82. package/components/combobox/combobox.d.ts +19 -0
  83. package/components/combobox/combobox.js +167 -0
  84. package/components/combobox/combobox.module.css +28 -0
  85. package/components/command-palette/command-palette.d.ts +20 -0
  86. package/components/command-palette/command-palette.js +151 -0
  87. package/components/command-palette/command-palette.module.css +94 -0
  88. package/components/comment-thread/comment-thread.d.ts +120 -0
  89. package/components/comment-thread/comment-thread.js +343 -0
  90. package/components/comment-thread/comment-thread.module.css +195 -0
  91. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  92. package/components/confirm-morph/confirm-morph.js +355 -0
  93. package/components/confirm-morph/confirm-morph.module.css +146 -0
  94. package/components/context-menu/context-menu.d.ts +18 -0
  95. package/components/context-menu/context-menu.js +148 -0
  96. package/components/context-menu/context-menu.module.css +17 -0
  97. package/components/copy-button/copy-button.d.ts +10 -0
  98. package/components/copy-button/copy-button.js +69 -0
  99. package/components/copy-button/copy-button.module.css +21 -0
  100. package/components/countdown/countdown.d.ts +31 -0
  101. package/components/countdown/countdown.js +177 -0
  102. package/components/countdown/countdown.module.css +49 -0
  103. package/components/date-picker/date-picker.d.ts +17 -0
  104. package/components/date-picker/date-picker.js +100 -0
  105. package/components/date-picker/date-picker.module.css +34 -0
  106. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  107. package/components/date-range-picker/date-range-picker.js +534 -0
  108. package/components/date-range-picker/date-range-picker.module.css +214 -0
  109. package/components/dialog/dialog.d.ts +11 -0
  110. package/components/dialog/dialog.js +52 -0
  111. package/components/dialog/dialog.module.css +36 -0
  112. package/components/donut-chart/donut-chart.d.ts +46 -0
  113. package/components/donut-chart/donut-chart.js +382 -0
  114. package/components/donut-chart/donut-chart.module.css +48 -0
  115. package/components/drawer/drawer.d.ts +20 -0
  116. package/components/drawer/drawer.js +111 -0
  117. package/components/drawer/drawer.module.css +97 -0
  118. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  119. package/components/dropdown-menu/dropdown-menu.js +39 -0
  120. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  121. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  122. package/components/elastic-slider/elastic-slider.js +261 -0
  123. package/components/elastic-slider/elastic-slider.module.css +40 -0
  124. package/components/empty-state/empty-state.d.ts +12 -0
  125. package/components/empty-state/empty-state.js +69 -0
  126. package/components/empty-state/empty-state.module.css +13 -0
  127. package/components/expandable-card/expandable-card.d.ts +13 -0
  128. package/components/expandable-card/expandable-card.js +89 -0
  129. package/components/expandable-card/expandable-card.module.css +22 -0
  130. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  131. package/components/expanding-button-group/expanding-button-group.js +258 -0
  132. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  133. package/components/expanding-search/expanding-search.d.ts +38 -0
  134. package/components/expanding-search/expanding-search.js +242 -0
  135. package/components/expanding-search/expanding-search.module.css +52 -0
  136. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  137. package/components/file-dropzone/file-dropzone.js +379 -0
  138. package/components/file-dropzone/file-dropzone.module.css +115 -0
  139. package/components/file-upload/file-upload.d.ts +22 -0
  140. package/components/file-upload/file-upload.js +192 -0
  141. package/components/file-upload/file-upload.module.css +49 -0
  142. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  143. package/components/filter-toolbar/filter-toolbar.js +446 -0
  144. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  145. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  146. package/components/floating-button-group/floating-button-group.js +215 -0
  147. package/components/floating-button-group/floating-button-group.module.css +74 -0
  148. package/components/gauge/gauge.d.ts +18 -0
  149. package/components/gauge/gauge.js +101 -0
  150. package/components/gauge/gauge.module.css +22 -0
  151. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  152. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  153. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  154. package/components/hover-card/hover-card.d.ts +38 -0
  155. package/components/hover-card/hover-card.js +145 -0
  156. package/components/hover-card/hover-card.module.css +20 -0
  157. package/components/image-compare/image-compare.d.ts +27 -0
  158. package/components/image-compare/image-compare.js +267 -0
  159. package/components/image-compare/image-compare.module.css +25 -0
  160. package/components/in-view-title/in-view-title.d.ts +17 -0
  161. package/components/in-view-title/in-view-title.js +70 -0
  162. package/components/in-view-title/in-view-title.module.css +13 -0
  163. package/components/inline-edit/inline-edit.d.ts +27 -0
  164. package/components/inline-edit/inline-edit.js +315 -0
  165. package/components/inline-edit/inline-edit.module.css +73 -0
  166. package/components/input/input.d.ts +7 -0
  167. package/components/input/input.js +60 -0
  168. package/components/input/input.module.css +22 -0
  169. package/components/json-viewer/json-viewer.d.ts +48 -0
  170. package/components/json-viewer/json-viewer.js +463 -0
  171. package/components/json-viewer/json-viewer.module.css +92 -0
  172. package/components/line-chart/line-chart.d.ts +58 -0
  173. package/components/line-chart/line-chart.js +383 -0
  174. package/components/line-chart/line-chart.module.css +60 -0
  175. package/components/mention-input/mention-input.d.ts +72 -0
  176. package/components/mention-input/mention-input.js +417 -0
  177. package/components/mention-input/mention-input.module.css +134 -0
  178. package/components/metric-card/metric-card.d.ts +8 -0
  179. package/components/metric-card/metric-card.js +44 -0
  180. package/components/metric-card/metric-card.module.css +18 -0
  181. package/components/money-input/money-input.d.ts +48 -0
  182. package/components/money-input/money-input.js +503 -0
  183. package/components/money-input/money-input.module.css +77 -0
  184. package/components/morph-select/morph-select.d.ts +50 -0
  185. package/components/morph-select/morph-select.js +395 -0
  186. package/components/morph-select/morph-select.module.css +88 -0
  187. package/components/multi-select/multi-select.d.ts +18 -0
  188. package/components/multi-select/multi-select.js +105 -0
  189. package/components/multi-select/multi-select.module.css +37 -0
  190. package/components/notification-center/notification-center.d.ts +24 -0
  191. package/components/notification-center/notification-center.js +146 -0
  192. package/components/notification-center/notification-center.module.css +79 -0
  193. package/components/number-field/number-field.d.ts +41 -0
  194. package/components/number-field/number-field.js +455 -0
  195. package/components/number-field/number-field.module.css +71 -0
  196. package/components/otp-input/otp-input.d.ts +13 -0
  197. package/components/otp-input/otp-input.js +129 -0
  198. package/components/otp-input/otp-input.module.css +28 -0
  199. package/components/pagination/pagination.d.ts +7 -0
  200. package/components/pagination/pagination.js +60 -0
  201. package/components/pagination/pagination.module.css +16 -0
  202. package/components/password-field/password-field.d.ts +14 -0
  203. package/components/password-field/password-field.js +51 -0
  204. package/components/password-field/password-field.module.css +27 -0
  205. package/components/password-strength/password-strength.d.ts +107 -0
  206. package/components/password-strength/password-strength.js +161 -0
  207. package/components/password-strength/password-strength.module.css +57 -0
  208. package/components/phone-input/phone-input.d.ts +73 -0
  209. package/components/phone-input/phone-input.js +612 -0
  210. package/components/phone-input/phone-input.module.css +98 -0
  211. package/components/popover/popover.d.ts +6 -0
  212. package/components/popover/popover.js +19 -0
  213. package/components/popover/popover.module.css +32 -0
  214. package/components/progress/progress.d.ts +8 -0
  215. package/components/progress/progress.js +42 -0
  216. package/components/progress/progress.module.css +1 -0
  217. package/components/radio-cards/radio-cards.d.ts +35 -0
  218. package/components/radio-cards/radio-cards.js +110 -0
  219. package/components/radio-cards/radio-cards.module.css +84 -0
  220. package/components/radio-group/radio-group.d.ts +13 -0
  221. package/components/radio-group/radio-group.js +55 -0
  222. package/components/radio-group/radio-group.module.css +30 -0
  223. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  224. package/components/resizable-panels/resizable-panels.js +338 -0
  225. package/components/resizable-panels/resizable-panels.module.css +40 -0
  226. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  227. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  228. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  229. package/components/ridgeline/ridgeline.d.ts +37 -0
  230. package/components/ridgeline/ridgeline.js +346 -0
  231. package/components/ridgeline/ridgeline.module.css +58 -0
  232. package/components/scroll-area/scroll-area.d.ts +40 -0
  233. package/components/scroll-area/scroll-area.js +153 -0
  234. package/components/scroll-area/scroll-area.module.css +79 -0
  235. package/components/search-field/search-field.d.ts +7 -0
  236. package/components/search-field/search-field.js +20 -0
  237. package/components/search-field/search-field.module.css +17 -0
  238. package/components/segmented-control/segmented-control.d.ts +16 -0
  239. package/components/segmented-control/segmented-control.js +56 -0
  240. package/components/segmented-control/segmented-control.module.css +15 -0
  241. package/components/select/select.d.ts +15 -0
  242. package/components/select/select.js +36 -0
  243. package/components/select/select.module.css +35 -0
  244. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  245. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  246. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  247. package/components/signature-pad/signature-pad.d.ts +72 -0
  248. package/components/signature-pad/signature-pad.js +450 -0
  249. package/components/signature-pad/signature-pad.module.css +84 -0
  250. package/components/skeleton/skeleton.d.ts +13 -0
  251. package/components/skeleton/skeleton.js +53 -0
  252. package/components/skeleton/skeleton.module.css +11 -0
  253. package/components/slider/slider.d.ts +45 -0
  254. package/components/slider/slider.js +347 -0
  255. package/components/slider/slider.module.css +45 -0
  256. package/components/slope-chart/slope-chart.d.ts +38 -0
  257. package/components/slope-chart/slope-chart.js +183 -0
  258. package/components/slope-chart/slope-chart.module.css +51 -0
  259. package/components/slot-text/slot-text.d.ts +26 -0
  260. package/components/slot-text/slot-text.js +119 -0
  261. package/components/slot-text/slot-text.module.css +19 -0
  262. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  263. package/components/sortable-data-table/sortable-data-table.js +240 -0
  264. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  265. package/components/sparkline/sparkline.d.ts +19 -0
  266. package/components/sparkline/sparkline.js +239 -0
  267. package/components/sparkline/sparkline.module.css +26 -0
  268. package/components/split-button/split-button.d.ts +19 -0
  269. package/components/split-button/split-button.js +33 -0
  270. package/components/split-button/split-button.module.css +51 -0
  271. package/components/stepper/stepper.d.ts +36 -0
  272. package/components/stepper/stepper.js +108 -0
  273. package/components/stepper/stepper.module.css +74 -0
  274. package/components/streamgraph/streamgraph.d.ts +51 -0
  275. package/components/streamgraph/streamgraph.js +376 -0
  276. package/components/streamgraph/streamgraph.module.css +55 -0
  277. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  278. package/components/swipe-actions/swipe-actions.js +330 -0
  279. package/components/swipe-actions/swipe-actions.module.css +64 -0
  280. package/components/switch/switch.d.ts +7 -0
  281. package/components/switch/switch.js +45 -0
  282. package/components/switch/switch.module.css +12 -0
  283. package/components/tabs/tabs.d.ts +8 -0
  284. package/components/tabs/tabs.js +138 -0
  285. package/components/tabs/tabs.module.css +27 -0
  286. package/components/tag-input/tag-input.d.ts +15 -0
  287. package/components/tag-input/tag-input.js +150 -0
  288. package/components/tag-input/tag-input.module.css +34 -0
  289. package/components/text-morph/text-morph.d.ts +13 -0
  290. package/components/text-morph/text-morph.js +69 -0
  291. package/components/text-morph/text-morph.module.css +8 -0
  292. package/components/text-reveal/text-reveal.d.ts +14 -0
  293. package/components/text-reveal/text-reveal.js +20 -0
  294. package/components/text-reveal/text-reveal.module.css +13 -0
  295. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  296. package/components/text-shimmer/text-shimmer.js +90 -0
  297. package/components/text-shimmer/text-shimmer.module.css +9 -0
  298. package/components/textarea/textarea.d.ts +7 -0
  299. package/components/textarea/textarea.js +56 -0
  300. package/components/textarea/textarea.module.css +22 -0
  301. package/components/theme-switch/theme-switch.d.ts +11 -0
  302. package/components/theme-switch/theme-switch.js +37 -0
  303. package/components/theme-switch/theme-switch.module.css +31 -0
  304. package/components/time-picker/time-picker.d.ts +13 -0
  305. package/components/time-picker/time-picker.js +95 -0
  306. package/components/time-picker/time-picker.module.css +23 -0
  307. package/components/timeline/timeline.d.ts +47 -0
  308. package/components/timeline/timeline.js +171 -0
  309. package/components/timeline/timeline.module.css +51 -0
  310. package/components/toast/toast.d.ts +8 -0
  311. package/components/toast/toast.js +118 -0
  312. package/components/toast/toast.module.css +100 -0
  313. package/components/toast-stack/toast-stack.d.ts +57 -0
  314. package/components/toast-stack/toast-stack.js +399 -0
  315. package/components/toast-stack/toast-stack.module.css +61 -0
  316. package/components/tooltip/tooltip.d.ts +8 -0
  317. package/components/tooltip/tooltip.js +62 -0
  318. package/components/tooltip/tooltip.module.css +18 -0
  319. package/components/tree-view/tree-view.d.ts +17 -0
  320. package/components/tree-view/tree-view.js +147 -0
  321. package/components/tree-view/tree-view.module.css +22 -0
  322. package/components/treemap/treemap.d.ts +35 -0
  323. package/components/treemap/treemap.js +375 -0
  324. package/components/treemap/treemap.module.css +59 -0
  325. package/components/usage-meter/usage-meter.d.ts +24 -0
  326. package/components/usage-meter/usage-meter.js +296 -0
  327. package/components/usage-meter/usage-meter.module.css +61 -0
  328. package/components/user-menu/user-menu.d.ts +62 -0
  329. package/components/user-menu/user-menu.js +316 -0
  330. package/components/user-menu/user-menu.module.css +97 -0
  331. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  332. package/components/waffle-chart/waffle-chart.js +244 -0
  333. package/components/waffle-chart/waffle-chart.module.css +52 -0
  334. package/components.json +1330 -0
  335. package/eslint.js +53 -0
  336. package/lib/escape.d.ts +19 -0
  337. package/lib/escape.js +50 -0
  338. package/lib/media.d.ts +353 -0
  339. package/lib/media.js +65 -0
  340. package/lib/menu-highlight.d.ts +28 -0
  341. package/lib/menu-highlight.js +43 -0
  342. package/lib/motion-tokens.d.ts +60 -0
  343. package/lib/motion-tokens.js +25 -0
  344. package/lib/return-focus.d.ts +9 -0
  345. package/lib/return-focus.js +53 -0
  346. package/lib/silicon-provider.d.ts +60 -0
  347. package/lib/silicon-provider.js +47 -0
  348. package/lib/use-copy-feedback.d.ts +8 -0
  349. package/lib/use-copy-feedback.js +36 -0
  350. package/lib/use-today.d.ts +2 -0
  351. package/lib/use-today.js +29 -0
  352. package/package.json +76 -4
  353. package/styles.css +218 -0
  354. package/styles.css.d.ts +1 -0
@@ -0,0 +1,201 @@
1
+ .root {
2
+ position: relative;
3
+ width: 100%;
4
+ container-type: inline-size;
5
+ color: var(--foreground);
6
+ font-family: var(--font-body);
7
+ letter-spacing: var(--tracking-body);
8
+ }
9
+
10
+ /* The writing surface. Everything inside is plain semantic HTML, styled here so pasted and typed content look the same. */
11
+ .editor {
12
+ min-height: 8rem;
13
+ padding: 2px 0;
14
+ outline: none;
15
+ color: var(--foreground);
16
+ font-size: var(--text-base);
17
+ line-height: 1.65;
18
+ overflow-wrap: break-word;
19
+ white-space: pre-wrap;
20
+ word-break: break-word;
21
+ caret-color: var(--accent);
22
+ }
23
+ .editor[contenteditable="false"] { caret-color: transparent; }
24
+ .editor::selection,
25
+ .editor *::selection { background: color-mix(in oklab, var(--accent) 22%, transparent); }
26
+
27
+ .editor > * { margin: 0; }
28
+ .editor > * + * { margin-top: .6em; }
29
+ .editor p { margin: 0; }
30
+ .editor h1,
31
+ .editor h2,
32
+ .editor h3 { font-weight: 500; letter-spacing: -.02em; line-height: 1.25; }
33
+ .editor h1 { font-size: var(--text-2xl); }
34
+ .editor h2 { font-size: var(--text-xl); }
35
+ .editor h3 { font-size: var(--text-lg); }
36
+ .editor > :is(h1, h2, h3) { margin-top: 1.1em; }
37
+ .editor > :is(h1, h2, h3):first-child { margin-top: 0; }
38
+ .editor > :is(h1, h2, h3) + * { margin-top: .45em; }
39
+
40
+ .editor strong { font-weight: 600; }
41
+ .editor s { color: var(--text-secondary); text-decoration-thickness: 1px; }
42
+ .editor a { color: var(--accent-strong); text-decoration: underline; text-decoration-color: color-mix(in oklab, var(--accent) 45%, transparent); text-decoration-thickness: 1px; text-underline-offset: 3px; cursor: text; }
43
+ .editor code {
44
+ padding: .12em .38em;
45
+ border-radius: var(--radius-detail);
46
+ background: var(--surface-muted);
47
+ font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
48
+ font-size: .86em;
49
+ }
50
+
51
+ .editor blockquote { padding-left: 14px; border-left: 2px solid var(--border-strong); color: var(--text-secondary); }
52
+
53
+ .editor ul,
54
+ .editor ol { margin-block: 0; padding-left: 1.45em; }
55
+ /* Set the markers explicitly: app resets that strip list styles would otherwise leave lists unmarked. */
56
+ .editor ul { list-style: disc; }
57
+ .editor ul ul { list-style: circle; }
58
+ .editor ol { list-style: decimal; }
59
+ .editor li { padding-left: .2em; }
60
+ .editor li + li { margin-top: .2em; }
61
+ .editor li > :is(ul, ol) { margin-top: .2em; }
62
+ .editor li::marker { color: var(--text-muted); }
63
+ .editor ol > li::marker { font-variant-numeric: tabular-nums; }
64
+
65
+ .editor pre {
66
+ padding: 12px 14px;
67
+ overflow-x: auto;
68
+ border: 1px solid var(--border-subtle);
69
+ border-radius: var(--radius-control);
70
+ background: var(--surface-muted);
71
+ font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
72
+ font-size: var(--text-sm);
73
+ line-height: 1.6;
74
+ tab-size: 2;
75
+ white-space: pre;
76
+ }
77
+
78
+ .editor hr { height: 1px; margin-block: 1.1em; border: 0; background: var(--border); }
79
+
80
+ /* Placeholders: the whole document when empty, and a quiet hint on the empty line holding the caret. */
81
+ .editor[data-empty] > :first-child::before,
82
+ .editor:focus [data-hint]::before {
83
+ float: left;
84
+ height: 0;
85
+ color: var(--text-muted);
86
+ pointer-events: none;
87
+ content: var(--placeholder);
88
+ }
89
+ .editor:focus:not([data-empty]) [data-hint]::before { content: var(--hint); }
90
+
91
+ /* Floating toolbar. One surface whose size springs between its two faces. */
92
+ .toolbar {
93
+ position: absolute;
94
+ z-index: 40;
95
+ translate: -50% -100%;
96
+ overflow: hidden;
97
+ border: 1px solid var(--border);
98
+ border-radius: var(--radius-control);
99
+ background: var(--surface-raised);
100
+ box-shadow: var(--shadow-floating);
101
+ }
102
+ .toolbar[data-below] { translate: -50% 0; }
103
+ .face { position: absolute; top: 0; left: 0; width: max-content; }
104
+ .face[inert] { pointer-events: none; }
105
+
106
+ .row,
107
+ .linkRow { display: flex; align-items: center; gap: 2px; padding: 4px; }
108
+ .blockTools { display: flex; align-items: center; gap: 2px; }
109
+ /* Narrow editors keep the inline marks; block types stay one slash away. */
110
+ @container (max-width: 330px) { .blockTools { display: none; } }
111
+ .divider { width: 1px; height: 18px; margin: 0 4px; background: var(--border); }
112
+
113
+ .tool {
114
+ display: grid;
115
+ width: 32px;
116
+ height: 32px;
117
+ flex: none;
118
+ place-items: center;
119
+ padding: 0;
120
+ border: 0;
121
+ border-radius: var(--radius-detail);
122
+ background: transparent;
123
+ color: var(--text-secondary);
124
+ cursor: pointer;
125
+ -webkit-tap-highlight-color: transparent;
126
+ transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
127
+ }
128
+ .tool:active { transform: scale(.92); }
129
+ .tool[aria-pressed="true"] { background: var(--accent-subtle); color: var(--accent-strong); }
130
+ .tool:focus-visible { background: var(--surface-muted); color: var(--foreground); }
131
+ @media (hover: hover) and (pointer: fine) { .tool:hover { background: var(--surface-muted); color: var(--foreground); } .tool[aria-pressed="true"]:hover { background: var(--accent-subtle); color: var(--accent-strong); } }
132
+
133
+ .linkInput {
134
+ width: min(15rem, 60vw);
135
+ height: 32px;
136
+ min-width: 0;
137
+ padding: 0 8px;
138
+ border: 0;
139
+ background: transparent;
140
+ color: var(--foreground);
141
+ font: inherit;
142
+ font-size: var(--text-sm);
143
+ outline: none;
144
+ }
145
+ .linkInput::placeholder { color: var(--text-muted); }
146
+ .apply {
147
+ display: grid;
148
+ width: 32px;
149
+ height: 32px;
150
+ flex: none;
151
+ place-items: center;
152
+ padding: 0;
153
+ border: 0;
154
+ border-radius: var(--radius-detail);
155
+ background: var(--surface-muted);
156
+ color: var(--text-muted);
157
+ cursor: pointer;
158
+ transition: background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
159
+ }
160
+ .apply[data-ready] { background: var(--accent); color: var(--accent-foreground); }
161
+ .apply:active { transform: scale(.92); }
162
+
163
+ /* Slash menu. */
164
+ .slash {
165
+ position: absolute;
166
+ z-index: 40;
167
+ width: 240px;
168
+ max-width: calc(100% - 8px);
169
+ overflow-x: hidden;
170
+ overflow-y: auto;
171
+ overscroll-behavior: contain;
172
+ scrollbar-width: none;
173
+ border: 1px solid var(--border);
174
+ border-radius: var(--radius-control);
175
+ background: var(--surface-raised);
176
+ box-shadow: var(--shadow-floating);
177
+ }
178
+ .slash[data-above] { translate: 0 -100%; }
179
+ .slashBody { padding: 6px; }
180
+ .slashList { position: relative; display: grid; margin: 0; padding: 0; list-style: none; }
181
+ .slashHighlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
182
+ .slashOption {
183
+ position: relative;
184
+ display: flex;
185
+ height: 34px;
186
+ align-items: center;
187
+ gap: 10px;
188
+ padding: 0 10px;
189
+ border-radius: var(--radius-control);
190
+ color: var(--text-secondary);
191
+ cursor: pointer;
192
+ font-size: var(--text-sm);
193
+ -webkit-tap-highlight-color: transparent;
194
+ transition: color var(--duration-fast) var(--ease-standard);
195
+ }
196
+ .slashOption[aria-selected="true"] { color: var(--foreground); }
197
+ .slashIcon { display: grid; width: 20px; flex: none; place-items: center; }
198
+ .slashLabel { min-width: 0; flex: 1; overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
199
+ .slashHint { flex: none; color: var(--text-muted); font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; font-size: var(--text-xs); }
200
+
201
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@@ -0,0 +1,37 @@
1
+ import { type Ref } from "react";
2
+ export interface RidgelineSeries {
3
+ /** Stable identity. A ridge that survives a data change morphs into its new shape. */
4
+ id: string;
5
+ label: string;
6
+ /** Raw observations. The ridge is a smoothed density of these values. */
7
+ values: number[];
8
+ }
9
+ /** Use a ridgeline to compare the shape of many distributions at once, such as daily temperatures by month or response times by region. */
10
+ export interface RidgelineProps {
11
+ /** One ridge per series, drawn top to bottom. */
12
+ series: RidgelineSeries[];
13
+ /** What is measured, such as "Daily highs in Zurich". Names the chart for assistive technology. */
14
+ label: string;
15
+ /** Unit after each value, such as "°C". */
16
+ unit?: string;
17
+ formatValue?: (value: number) => string;
18
+ /** Value range of the axis. Defaults to the data with a little room on each side. */
19
+ domain?: [number, number];
20
+ /** How far a ridge may rise into the rows above, in row heights. */
21
+ overlap?: number;
22
+ /** Height of one row in pixels. */
23
+ rowHeight?: number;
24
+ /** Smoothing in value units. Defaults to Silverman's rule per series. */
25
+ bandwidth?: number;
26
+ /** Shades each ridge by its median, from a light tint to the full first series color, and shows the scale under the axis. */
27
+ tint?: boolean;
28
+ /** Controlled id of the lifted ridge. */
29
+ active?: string | null;
30
+ defaultActive?: string | null;
31
+ onActiveChange?: (id: string | null) => void;
32
+ emptyLabel?: string;
33
+ ref?: Ref<HTMLElement>;
34
+ className?: string;
35
+ }
36
+ export declare function Ridgeline({ series, label, unit, formatValue, domain, overlap, rowHeight, bandwidth, tint, active, defaultActive, onActiveChange, emptyLabel, ref, className }: RidgelineProps): import("react").JSX.Element;
37
+ export default Ridgeline;
@@ -0,0 +1,346 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, 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 "./ridgeline.module.css";
7
+ const { spring } = motionTokens;
8
+ const physical = ({ visualDuration, bounce }, restDelta = .0005) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };
9
+ const morph = physical({ visualDuration: .7, bounce: .08 });
10
+ const reveal = physical({ visualDuration: 1, bounce: 0 });
11
+ const glide = physical(spring.snappy, .01);
12
+ const follow = { stiffness: glide.stiffness, damping: glide.damping, restDelta: .01 };
13
+ const SAMPLES = 96, AXIS = 28, TIP = 14, LIFT = 6;
14
+ const grouped = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
15
+ const percent = new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 0 });
16
+ const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
17
+ const mix = (a, b, t) => a + (b - a) * t;
18
+ function quantile(sorted, q) {
19
+ if (!sorted.length)
20
+ return 0;
21
+ const at = (sorted.length - 1) * q, low = Math.floor(at), high = Math.ceil(at);
22
+ return sorted[low] + (sorted[high] - sorted[low]) * (at - low);
23
+ }
24
+ function statsOf(values) {
25
+ const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
26
+ return { sorted, n: sorted.length, min: sorted[0] ?? 0, max: sorted[sorted.length - 1] ?? 0, q1: quantile(sorted, .25), median: quantile(sorted, .5), q3: quantile(sorted, .75) };
27
+ }
28
+ function silverman(stats) {
29
+ if (stats.n < 2)
30
+ return 1;
31
+ const mean = stats.sorted.reduce((sum, value) => sum + value, 0) / stats.n;
32
+ const sd = Math.sqrt(stats.sorted.reduce((sum, value) => sum + (value - mean) ** 2, 0) / (stats.n - 1));
33
+ const spread = Math.min(sd, (stats.q3 - stats.q1) / 1.34 || sd);
34
+ return Math.max(1e-6, .9 * (spread || 1) * Math.pow(stats.n, -.2));
35
+ }
36
+ function niceStep(span, count) {
37
+ const raw = span / Math.max(1, count) || 1, power = Math.pow(10, Math.floor(Math.log10(raw))), unit = raw / power;
38
+ return (unit >= 5 ? 5 : unit >= 2 ? 2 : 1) * power;
39
+ }
40
+ /** Samples a Gaussian kernel density across the domain. */
41
+ function density(stats, lo, hi, bandwidth) {
42
+ const xs = [], ys = [];
43
+ const norm = 1 / (Math.max(1, stats.n) * bandwidth * Math.sqrt(2 * Math.PI));
44
+ for (let i = 0; i < SAMPLES; i++) {
45
+ const x = lo + ((hi - lo) * i) / (SAMPLES - 1);
46
+ let sum = 0;
47
+ for (const value of stats.sorted) {
48
+ const u = (x - value) / bandwidth;
49
+ if (u > -5 && u < 5)
50
+ sum += Math.exp(-.5 * u * u);
51
+ }
52
+ xs.push(x);
53
+ ys.push(sum * norm);
54
+ }
55
+ return { xs, ys };
56
+ }
57
+ const heightAt = (shape, x) => {
58
+ const { xs, ys } = shape;
59
+ if (x <= xs[0] || x >= xs[xs.length - 1])
60
+ return 0;
61
+ let i = 1;
62
+ while (i < xs.length - 1 && xs[i] < x)
63
+ i++;
64
+ const t = (x - xs[i - 1]) / (xs[i] - xs[i - 1] || 1);
65
+ return mix(ys[i - 1], ys[i], t);
66
+ };
67
+ const subscribeNothing = () => () => { };
68
+ function useReducedMotionSafe() {
69
+ const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
70
+ return !!useReducedMotion() && hydrated;
71
+ }
72
+ export function Ridgeline({ series, label, unit = "", formatValue, domain, overlap = 2.4, rowHeight = 30, bandwidth, tint = true, active, defaultActive = null, onActiveChange, emptyLabel = "No data yet", ref, className }) {
73
+ const reduced = useReducedMotionSafe();
74
+ const figure = useRef(null);
75
+ const plot = useRef(null);
76
+ const tip = useRef(null);
77
+ const fills = useRef(new Map());
78
+ const lines = useRef(new Map());
79
+ const medians = useRef(new Map());
80
+ const lifted = useRef({ fill: null, band: null, line: null, median: null, group: null });
81
+ const tickRefs = useRef(new Map());
82
+ const cross = useRef(null);
83
+ useImperativeHandle(ref, () => figure.current);
84
+ const inView = useInView(figure, { once: true, amount: .3 });
85
+ // A true minus sign keeps negative values tight and lined up with the tabular digits.
86
+ const format = (value) => formatValue ? formatValue(value) : `${grouped.format(value).replace("-", "\u2212")}${unit}`;
87
+ const [width, setWidth] = useState(0);
88
+ useLayoutEffect(() => {
89
+ const node = plot.current;
90
+ if (!node || typeof ResizeObserver === "undefined")
91
+ return;
92
+ const observer = new ResizeObserver(() => setWidth(node.clientWidth));
93
+ observer.observe(node);
94
+ setWidth(node.clientWidth);
95
+ return () => observer.disconnect();
96
+ }, []);
97
+ // Targets: one density per series on a shared domain, scaled so the tallest peak of all rises `overlap` rows.
98
+ const signature = JSON.stringify([series, domain, bandwidth]);
99
+ const model = useMemo(() => {
100
+ const stats = series.map(entry => statsOf(entry.values));
101
+ const widths = stats.map(entry => bandwidth ?? silverman(entry));
102
+ const filled = stats.map((entry, index) => ({ entry, room: 2.5 * widths[index] })).filter(({ entry }) => entry.n);
103
+ let lo = domain?.[0] ?? Math.min(...filled.map(({ entry, room }) => entry.min - room));
104
+ let hi = domain?.[1] ?? Math.max(...filled.map(({ entry, room }) => entry.max + room));
105
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo === hi) {
106
+ lo = 0;
107
+ hi = 1;
108
+ }
109
+ if (!domain) {
110
+ const step = niceStep(hi - lo, 5);
111
+ lo = Math.floor(lo / step) * step;
112
+ hi = Math.ceil(hi / step) * step;
113
+ }
114
+ const curves = stats.map((entry, index) => entry.n ? density(entry, lo, hi, widths[index]) : { xs: Array.from({ length: SAMPLES }, (_, i) => lo + ((hi - lo) * i) / (SAMPLES - 1)), ys: new Array(SAMPLES).fill(0) });
115
+ const peak = Math.max(1e-9, ...curves.flatMap(curve => curve.ys));
116
+ const shapes = new Map(series.map((entry, index) => { const curve = curves[index], stat = stats[index]; return [entry.id, { xs: curve.xs, ys: curve.ys.map(y => y / peak), row: index, q1: stat.q1, median: stat.median, q3: stat.q3 }]; }));
117
+ return { stats: new Map(series.map((entry, index) => [entry.id, stats[index]])), shapes, lo, hi };
118
+ }, [signature]); // eslint-disable-line react-hooks/exhaustive-deps
119
+ const [ownActive, setOwnActive] = useState(defaultActive);
120
+ const current = active !== undefined ? active : ownActive;
121
+ const setCurrent = (id) => { if (active === undefined)
122
+ setOwnActive(id); if (id !== current)
123
+ onActiveChange?.(id); };
124
+ const [cursor, setCursor] = useState(null);
125
+ const narrow = width > 0 && width < 420;
126
+ const gutter = narrow ? 36 : 52;
127
+ const top = Math.ceil((overlap - 1) * rowHeight) + LIFT + 8;
128
+ const height = top + series.length * rowHeight + AXIS;
129
+ const plotWidth = Math.max(40, width - gutter - 4);
130
+ const baselineOf = (row) => top + (row + 1) * rowHeight;
131
+ // Drawn geometry lives outside React; a spring interpolates it from what is on screen to the new targets.
132
+ const drawn = useRef({ shapes: new Map(), lo: 0, hi: 1 });
133
+ const live = useRef({ gutter, plotWidth, rowHeight, overlap, top, current });
134
+ useLayoutEffect(() => { live.current = { gutter, plotWidth, rowHeight, overlap, top, current }; });
135
+ const xOf = (value) => { const d = drawn.current, L = live.current; return L.gutter + ((value - d.lo) / (d.hi - d.lo || 1)) * L.plotWidth; };
136
+ const paint = () => {
137
+ const d = drawn.current, L = live.current;
138
+ const sx = (value) => L.gutter + ((value - d.lo) / (d.hi - d.lo || 1)) * L.plotWidth;
139
+ const lift = L.rowHeight * L.overlap;
140
+ const curvePath = (shape, closed, from = -Infinity, to = Infinity) => {
141
+ const base = L.top + (shape.row + 1) * L.rowHeight;
142
+ const points = [];
143
+ const push = (x, y) => points.push(`${sx(x).toFixed(2)},${(base - y * lift).toFixed(2)}`);
144
+ if (from > -Infinity)
145
+ push(from, heightAt(shape, from));
146
+ shape.xs.forEach((x, i) => { if (x > from && x < to)
147
+ push(x, shape.ys[i]); });
148
+ if (to < Infinity)
149
+ push(to, heightAt(shape, to));
150
+ if (!points.length)
151
+ return "";
152
+ const first = from > -Infinity ? from : shape.xs[0], last = to < Infinity ? to : shape.xs[shape.xs.length - 1];
153
+ return closed ? `M${sx(first).toFixed(2)},${base}L${points.join("L")}L${sx(last).toFixed(2)},${base}Z` : `M${points.join("L")}`;
154
+ };
155
+ for (const [id, shape] of d.shapes) {
156
+ const fill = fills.current.get(id), line = lines.current.get(id), median = medians.current.get(id);
157
+ fill?.setAttribute("d", curvePath(shape, true));
158
+ line?.setAttribute("d", curvePath(shape, false));
159
+ if (median) {
160
+ const x = sx(shape.median), base = L.top + (shape.row + 1) * L.rowHeight;
161
+ median.setAttribute("x1", x.toFixed(2));
162
+ median.setAttribute("x2", x.toFixed(2));
163
+ median.setAttribute("y1", String(base));
164
+ median.setAttribute("y2", (base - heightAt(shape, shape.median) * lift).toFixed(2));
165
+ }
166
+ }
167
+ const shape = L.current ? d.shapes.get(L.current) : undefined, lens = lifted.current;
168
+ if (shape && lens.fill && lens.line && lens.band && lens.median) {
169
+ lens.fill.setAttribute("d", curvePath(shape, true));
170
+ lens.line.setAttribute("d", curvePath(shape, false));
171
+ lens.band.setAttribute("d", curvePath(shape, true, shape.q1, shape.q3));
172
+ const x = sx(shape.median), base = L.top + (shape.row + 1) * L.rowHeight;
173
+ lens.median.setAttribute("x1", x.toFixed(2));
174
+ lens.median.setAttribute("x2", x.toFixed(2));
175
+ lens.median.setAttribute("y1", String(base));
176
+ lens.median.setAttribute("y2", (base - heightAt(shape, shape.median) * lift).toFixed(2));
177
+ }
178
+ for (const [value, tick] of tickRefs.current)
179
+ tick.style.transform = `translate3d(${sx(value).toFixed(2)}px, 0, 0)`;
180
+ };
181
+ const paintRef = useRef(paint);
182
+ useLayoutEffect(() => { paintRef.current = paint; });
183
+ const shown = inView || reduced;
184
+ useEffect(() => {
185
+ if (!shown)
186
+ return;
187
+ const d = drawn.current;
188
+ const first = d.shapes.size === 0;
189
+ const from = new Map();
190
+ for (const [id, target] of model.shapes) {
191
+ const now = d.shapes.get(id);
192
+ from.set(id, now ? { ...now, xs: [...now.xs], ys: [...now.ys] } : { ...target, ys: target.ys.map(() => 0), q1: target.median, q3: target.median });
193
+ }
194
+ const fromLo = first ? model.lo : d.lo, fromHi = first ? model.hi : d.hi;
195
+ const finish = () => { d.shapes = new Map([...model.shapes].map(([id, shape]) => [id, { ...shape }])); d.lo = model.lo; d.hi = model.hi; paintRef.current(); };
196
+ if (reduced) {
197
+ finish();
198
+ return;
199
+ }
200
+ d.shapes = new Map([...from].map(([id, shape]) => [id, { ...shape, xs: [...shape.xs], ys: [...shape.ys] }]));
201
+ const controls = animate(0, 1, { ...(first ? reveal : morph), onUpdate: t => {
202
+ d.lo = mix(fromLo, model.lo, t);
203
+ d.hi = mix(fromHi, model.hi, t);
204
+ for (const [id, to] of model.shapes) {
205
+ const f = from.get(id), out = d.shapes.get(id);
206
+ // Stagger the reveal a little from the top row down, so the ridges roll in like a wave.
207
+ const local = first ? clamp((t - to.row * .025) / (1 - Math.min(.5, model.shapes.size * .025)), 0, 1) : t;
208
+ for (let i = 0; i < SAMPLES; i++) {
209
+ out.xs[i] = mix(f.xs[i], to.xs[i], local);
210
+ out.ys[i] = mix(f.ys[i], to.ys[i], local);
211
+ }
212
+ out.row = mix(f.row, to.row, local);
213
+ out.q1 = mix(f.q1, to.q1, local);
214
+ out.median = mix(f.median, to.median, local);
215
+ out.q3 = mix(f.q3, to.q3, local);
216
+ }
217
+ paintRef.current();
218
+ }, onComplete: finish });
219
+ return () => controls.stop();
220
+ }, [shown, model, reduced]);
221
+ useLayoutEffect(() => { paintRef.current(); });
222
+ // Axis ticks for the target domain; their positions follow the drawn domain every frame.
223
+ const tickCount = Math.max(2, Math.floor(plotWidth / 72));
224
+ const step = niceStep(model.hi - model.lo, tickCount);
225
+ const ticks = [];
226
+ for (let value = Math.ceil(model.lo / step) * step; value <= model.hi + 1e-9; value += step)
227
+ ticks.push(Math.round(value * 1e6) / 1e6 || 0);
228
+ const activeStats = current ? model.stats.get(current) : undefined;
229
+ const activeSeries = current ? series.find(entry => entry.id === current) : undefined;
230
+ const reading = cursor && activeStats ? { value: cursor.value, below: activeStats.n ? activeStats.sorted.filter(value => value <= cursor.value).length / activeStats.n : 0 } : null;
231
+ // The tooltip keeps its last content while it fades out, so it never collapses to an empty box on leave.
232
+ const [held, setHeld] = useState(null);
233
+ if (current && (held?.id !== current || held.value !== (reading?.value ?? null)))
234
+ setHeld({ id: current, value: reading?.value ?? null });
235
+ const tipSeries = held ? series.find(entry => entry.id === held.id) : undefined;
236
+ const tipStats = held ? model.stats.get(held.id) : undefined;
237
+ const tipBelow = held && held.value !== null && tipStats?.n ? tipStats.sorted.filter(value => value <= held.value).length / tipStats.n : 0;
238
+ // The tooltip trails the pointer or the keyboard cursor and stays inside the chart.
239
+ const tipX = useMotionValue(0), tipY = useMotionValue(0);
240
+ const tipSpringX = useSpring(tipX, follow), tipSpringY = useSpring(tipY, follow);
241
+ const wasOn = useRef(false);
242
+ useLayoutEffect(() => {
243
+ const bubble = tip.current, shape = current ? model.shapes.get(current) : undefined;
244
+ if (cross.current)
245
+ cross.current.style.transform = `translate3d(${reading ? xOf(reading.value).toFixed(2) : 0}px, 0, 0)`;
246
+ if (!bubble || !shape || !width) {
247
+ wasOn.current = false;
248
+ return;
249
+ }
250
+ const tw = bubble.offsetWidth, th = bubble.offsetHeight;
251
+ const ax = reading ? xOf(reading.value) : xOf(shape.median);
252
+ const ay = cursor?.pointerY ?? baselineOf(shape.row) - heightAt(shape, shape.median) * rowHeight * overlap - LIFT;
253
+ let left = ax + TIP;
254
+ if (left + tw > width)
255
+ left = ax - TIP - tw;
256
+ let y = ay - th - TIP;
257
+ if (y < 0)
258
+ y = ay + TIP;
259
+ tipX.set(clamp(left, 0, Math.max(0, width - tw)));
260
+ tipY.set(clamp(y, 0, Math.max(0, height - th)));
261
+ if (!wasOn.current || reduced) {
262
+ tipSpringX.jump(tipX.get());
263
+ tipSpringY.jump(tipY.get());
264
+ }
265
+ wasOn.current = true;
266
+ });
267
+ const valueAt = (x) => { const d = drawn.current; return d.lo + clamp((x - gutter) / plotWidth, 0, 1) * (d.hi - d.lo); };
268
+ const pick = (x, y) => {
269
+ const value = valueAt(x), lift = rowHeight * overlap;
270
+ let best = null, bestId = null;
271
+ for (const [id, shape] of drawn.current.shapes) {
272
+ const base = baselineOf(shape.row), h = heightAt(shape, value) * lift;
273
+ if (y <= base + 2 && y >= base - h - 4 && (!best || shape.row > best.row)) {
274
+ best = shape;
275
+ bestId = id;
276
+ }
277
+ }
278
+ if (!bestId) {
279
+ const row = clamp(Math.floor((y - top) / rowHeight), 0, series.length - 1);
280
+ bestId = series[row]?.id ?? null;
281
+ }
282
+ return { id: bestId, value };
283
+ };
284
+ const onPointerMove = (event) => {
285
+ const rect = event.currentTarget.getBoundingClientRect(), x = event.clientX - rect.left, y = event.clientY - rect.top;
286
+ if (x < gutter - 8 || y > height - AXIS + 4) {
287
+ if (event.pointerType === "mouse") {
288
+ setCurrent(null);
289
+ setCursor(null);
290
+ }
291
+ return;
292
+ }
293
+ const hit = pick(x, y);
294
+ setCurrent(hit.id);
295
+ setCursor({ value: hit.value, pointerY: y });
296
+ };
297
+ const onKeyDown = (event) => {
298
+ const at = current ? series.findIndex(entry => entry.id === current) : -1;
299
+ const stats = (id) => id ? model.stats.get(id) : undefined;
300
+ const choose = (index) => { event.preventDefault(); const next = series[clamp(index, 0, series.length - 1)]; if (!next)
301
+ return; setCurrent(next.id); setCursor({ value: cursor?.value ?? stats(next.id)?.median ?? model.lo, pointerY: null }); };
302
+ const nudge = (direction) => { event.preventDefault(); if (!current) {
303
+ choose(0);
304
+ return;
305
+ } const base = cursor?.value ?? stats(current)?.median ?? model.lo; const next = clamp((Math.round(base / step) + direction) * step, model.lo, model.hi); setCursor({ value: next, pointerY: null }); };
306
+ if (event.key === "ArrowDown")
307
+ choose(at + 1);
308
+ else if (event.key === "ArrowUp")
309
+ choose(at < 0 ? 0 : at - 1);
310
+ else if (event.key === "Home")
311
+ choose(0);
312
+ else if (event.key === "End")
313
+ choose(series.length - 1);
314
+ else if (event.key === "ArrowRight")
315
+ nudge(1);
316
+ else if (event.key === "ArrowLeft")
317
+ nudge(-1);
318
+ else if (event.key === "Escape" && current) {
319
+ event.preventDefault();
320
+ setCurrent(null);
321
+ setCursor(null);
322
+ }
323
+ };
324
+ const empty = !series.length || series.every(entry => !entry.values.length);
325
+ /** Ridge tone: one hue, the first series color, from a light tint (low median) to full strength (high median), so the scale never passes through a muddy middle. */
326
+ const toneOf = (t) => `color-mix(in oklab, var(--series-1) ${Math.round(38 + clamp(t, 0, 1) * 62)}%, var(--surface))`;
327
+ const ridgeColor = (value) => tint ? toneOf((value - model.lo) / (model.hi - model.lo || 1)) : "var(--series-1)";
328
+ const describe = (entry) => { const s = model.stats.get(entry.id); return `${entry.label}: median ${format(s.median)}, middle half ${format(s.q1)} to ${format(s.q3)}, range ${format(s.min)} to ${format(s.max)}, ${s.n} values`; };
329
+ return _jsxs("figure", { ref: figure, className: [styles.figure, className].filter(Boolean).join(" "), "aria-label": label, children: [_jsxs("div", { ref: plot, className: styles.plot, style: { height }, "data-active": current ? true : undefined, "data-cursor": reading ? true : undefined, role: "group", tabIndex: empty ? -1 : 0, "aria-roledescription": "ridgeline chart", "aria-label": `${label}. Up and down arrows choose a ridge, left and right arrows move along the values.`, onPointerMove: onPointerMove, onPointerDown: onPointerMove, onPointerLeave: event => { if (event.pointerType === "mouse") {
330
+ setCurrent(null);
331
+ setCursor(null);
332
+ } }, onKeyDown: onKeyDown, onBlur: () => { setCurrent(null); setCursor(null); }, children: [_jsx("div", { className: styles.ticks, "aria-hidden": "true", style: { height: height - AXIS + 22 }, children: ticks.map(value => _jsx("div", { ref: element => { if (element)
333
+ tickRefs.current.set(value, element);
334
+ else
335
+ tickRefs.current.delete(value); }, className: styles.tick, style: { top: top - rowHeight * .5 }, children: _jsx("span", { className: styles.tickLabel, children: format(value) }) }, value)) }), _jsx("div", { ref: cross, className: styles.crosshair, style: { top: top - rowHeight * .5, height: height - AXIS - top + rowHeight * .5 }, "aria-hidden": "true" }), _jsxs("svg", { className: styles.svg, width: width || undefined, height: height, viewBox: `0 0 ${Math.max(1, width)} ${height}`, "aria-hidden": "true", focusable: "false", children: [series.map(entry => _jsxs("g", { className: styles.ridge, "data-dim": current !== null && current !== entry.id || undefined, "data-lifted": current === entry.id || undefined, style: { "--ridge": ridgeColor(model.stats.get(entry.id)?.median ?? model.lo) }, children: [_jsx("path", { ref: element => { if (element)
336
+ fills.current.set(entry.id, element);
337
+ else
338
+ fills.current.delete(entry.id); }, className: styles.fill }), _jsx("line", { ref: element => { if (element)
339
+ medians.current.set(entry.id, element);
340
+ else
341
+ medians.current.delete(entry.id); }, className: styles.median }), _jsx("path", { ref: element => { if (element)
342
+ lines.current.set(entry.id, element);
343
+ else
344
+ lines.current.delete(entry.id); }, className: styles.line })] }, entry.id)), _jsxs("g", { ref: element => { lifted.current.group = element; }, className: styles.lens, "data-on": current ? true : undefined, style: tipStats ? { "--ridge": ridgeColor(tipStats.median) } : undefined, children: [_jsx("path", { ref: element => { lifted.current.fill = element; }, className: styles.lensFill }), _jsx("path", { ref: element => { lifted.current.band = element; }, className: styles.lensBand }), _jsx("line", { ref: element => { lifted.current.median = element; }, className: styles.lensMedian }), _jsx("path", { ref: element => { lifted.current.line = element; }, className: styles.lensLine })] })] }), _jsx("div", { className: styles.labels, "aria-hidden": "true", children: series.map((entry, index) => _jsx("span", { className: styles.rowLabel, "data-active": current === entry.id || undefined, style: { top: baselineOf(index) - 18, width: gutter - 10 }, children: entry.label }, entry.id)) }), empty && _jsx("p", { className: styles.message, children: emptyLabel }), _jsx(motion.div, { ref: tip, className: styles.tooltip, style: { x: reduced ? tipX : tipSpringX, y: reduced ? tipY : tipSpringY }, "aria-hidden": "true", children: tipSeries && tipStats && _jsxs(_Fragment, { children: [_jsx("p", { className: styles.tipTitle, children: tipSeries.label }), _jsxs("p", { className: styles.tipValue, children: [_jsx("span", { className: styles.tipKey, children: "Median" }), _jsx("span", { children: format(tipStats.median) })] }), _jsxs("p", { className: styles.tipRow, children: [_jsx("span", { className: styles.tipKey, children: "Middle half" }), _jsxs("span", { children: [format(tipStats.q1), " to ", format(tipStats.q3)] })] }), _jsxs("p", { className: styles.tipRow, children: [_jsx("span", { className: styles.tipKey, children: "Range" }), _jsxs("span", { children: [format(tipStats.min), " to ", format(tipStats.max)] })] }), held?.value != null && _jsxs("p", { className: styles.tipNote, children: [percent.format(tipBelow), " at or below ", format(held.value)] })] }) })] }), tint && !empty && _jsxs("div", { className: styles.scale, "aria-hidden": "true", children: [_jsx("span", { className: styles.scaleLabel, children: "Median" }), _jsx("span", { children: format(model.lo) }), _jsx("span", { className: styles.steps, children: [0, .25, .5, .75, 1].map(step => _jsx("span", { className: styles.step, style: { "--ridge": toneOf(step) } }, step)) }), _jsx("span", { children: format(model.hi) })] }), _jsx("p", { className: styles.srOnly, "aria-live": "polite", "aria-atomic": "true", children: activeSeries ? `${describe(activeSeries)}${reading ? `. ${percent.format(reading.below)} at or below ${format(reading.value)}` : ""}` : "" }), !empty && _jsx("div", { className: styles.srOnly, children: _jsxs("table", { children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Series" }), _jsx("th", { scope: "col", children: "Median" }), _jsx("th", { scope: "col", children: "First quartile" }), _jsx("th", { scope: "col", children: "Third quartile" }), _jsx("th", { scope: "col", children: "Lowest" }), _jsx("th", { scope: "col", children: "Highest" }), _jsx("th", { scope: "col", children: "Count" })] }) }), _jsx("tbody", { children: series.map(entry => { const s = model.stats.get(entry.id); return _jsxs("tr", { children: [_jsx("th", { scope: "row", children: entry.label }), _jsx("td", { children: format(s.median) }), _jsx("td", { children: format(s.q1) }), _jsx("td", { children: format(s.q3) }), _jsx("td", { children: format(s.min) }), _jsx("td", { children: format(s.max) }), _jsx("td", { children: s.n })] }, entry.id); }) })] }) })] });
345
+ }
346
+ export default Ridgeline;
@@ -0,0 +1,58 @@
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 { position: relative; min-width: 0; outline: none; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; cursor: crosshair; }
6
+ .svg { position: absolute; inset: 0; display: block; overflow: visible; }
7
+
8
+ /* Ridges are opaque, so each one hides the valleys of the row behind it. */
9
+ .ridge { transition: opacity var(--duration-standard) var(--ease-standard); }
10
+ .fill { fill: color-mix(in oklab, var(--ridge) 34%, var(--surface)); stroke: none; transition: fill var(--duration-considered) var(--ease-standard); }
11
+ .line { fill: none; stroke: var(--ridge); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; transition: stroke var(--duration-considered) var(--ease-standard); }
12
+ .median { stroke: color-mix(in oklab, var(--ridge) 55%, var(--surface)); stroke-width: 1; transition: stroke var(--duration-considered) var(--ease-standard); }
13
+ .plot[data-active] .ridge[data-dim] { opacity: .38; }
14
+ .ridge[data-lifted] { opacity: 0; transition-duration: var(--duration-instant); }
15
+
16
+ /* The lifted ridge is a copy drawn above the rest, so it can rise over its neighbours. */
17
+ .lens { opacity: 0; pointer-events: none; transform: translateY(0); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
18
+ .lens[data-on] { opacity: 1; transform: translateY(-6px); }
19
+ .lensFill { fill: color-mix(in oklab, var(--ridge) 18%, var(--surface)); stroke: none; }
20
+ .lensBand { fill: color-mix(in oklab, var(--ridge) 48%, var(--surface)); stroke: none; }
21
+ .lensMedian { stroke: var(--foreground); stroke-width: 1.5; stroke-linecap: round; }
22
+ .lensLine { fill: none; stroke: var(--ridge); stroke-width: 2.25; stroke-linejoin: round; stroke-linecap: round; }
23
+
24
+ .ticks { position: absolute; top: 0; right: 0; left: 0; pointer-events: none; }
25
+ .tick { position: absolute; bottom: 0; left: 0; width: 1px; background: var(--border-subtle); will-change: transform; animation: tickIn var(--duration-standard) var(--ease-standard); }
26
+ .tickLabel { position: absolute; bottom: 0; left: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; translate: -50% 0; }
27
+ @keyframes tickIn { from { opacity: 0; } }
28
+
29
+ .crosshair { position: absolute; left: 0; width: 1px; background: var(--foreground); opacity: 0; pointer-events: none; transition: opacity var(--duration-instant) var(--ease-standard); }
30
+ .plot[data-cursor] .crosshair { opacity: .35; }
31
+
32
+ .labels { position: absolute; inset: 0; pointer-events: none; }
33
+ .rowLabel { position: absolute; left: 0; overflow: hidden; color: var(--text-secondary); font-size: var(--text-xs); line-height: 18px; text-overflow: ellipsis; white-space: nowrap; transition: color var(--duration-fast) var(--ease-standard); }
34
+ .rowLabel[data-active] { color: var(--foreground); font-weight: 500; }
35
+
36
+ .message { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--text-muted); font-size: var(--text-sm); pointer-events: none; }
37
+
38
+ .tooltip { position: absolute; top: 0; left: 0; z-index: 2; display: grid; min-width: 170px; max-width: min(240px, 100%); gap: 2px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-control); 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); }
39
+ .plot[data-active] .tooltip { opacity: 1; scale: 1; }
40
+ .tipTitle { margin: 0 0 2px; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
41
+ .tipValue, .tipRow { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin: 0; font-variant-numeric: tabular-nums; white-space: nowrap; }
42
+ .tipValue { color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: 1.4; }
43
+ .tipRow { color: var(--foreground); font-size: var(--text-xs); line-height: var(--leading-body); }
44
+ .tipKey { color: var(--text-muted); font-weight: 400; }
45
+ .tipValue .tipKey { font-size: var(--text-xs); }
46
+ .tipNote { margin: 4px 0 0; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
47
+
48
+ .scale { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; margin-top: var(--space-group); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
49
+ .scaleLabel { margin-right: 4px; color: var(--text-secondary); }
50
+ .steps { display: inline-flex; gap: 2px; }
51
+ .step { width: 18px; height: 10px; border-radius: var(--radius-detail); background: var(--ridge); }
52
+
53
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
54
+
55
+ @media (prefers-reduced-motion: reduce) {
56
+ .ridge, .lens, .crosshair, .tooltip, .rowLabel { transition: none; }
57
+ .tick { animation: none; }
58
+ }