@eifi1/ui-kit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +498 -57
  3. package/dist/chart.d.ts +10 -0
  4. package/dist/chart.js +10 -0
  5. package/dist/chart.js.map +1 -0
  6. package/dist/components/account-settings.d.ts +11 -0
  7. package/dist/components/account-settings.js +19 -12
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/alert-banner.d.ts +10 -6
  10. package/dist/components/alert-banner.js +20 -16
  11. package/dist/components/alert-banner.js.map +1 -1
  12. package/dist/components/amount-input.d.ts +30 -4
  13. package/dist/components/amount-input.js +36 -14
  14. package/dist/components/amount-input.js.map +1 -1
  15. package/dist/components/calculator.d.ts +24 -7
  16. package/dist/components/calculator.js +37 -20
  17. package/dist/components/calculator.js.map +1 -1
  18. package/dist/components/chart-zoom.d.ts +156 -0
  19. package/dist/components/chart-zoom.js +262 -0
  20. package/dist/components/chart-zoom.js.map +1 -0
  21. package/dist/components/chart.d.ts +28 -3
  22. package/dist/components/chart.js +71 -32
  23. package/dist/components/chart.js.map +1 -1
  24. package/dist/components/checkbox.d.ts +51 -0
  25. package/dist/components/checkbox.js +123 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip.d.ts +128 -0
  28. package/dist/components/chip.js +357 -0
  29. package/dist/components/chip.js.map +1 -0
  30. package/dist/components/choice-parts.d.ts +24 -0
  31. package/dist/components/choice-parts.js +18 -0
  32. package/dist/components/choice-parts.js.map +1 -0
  33. package/dist/components/combobox-core.d.ts +52 -9
  34. package/dist/components/combobox-core.js +91 -20
  35. package/dist/components/combobox-core.js.map +1 -1
  36. package/dist/components/combobox.d.ts +43 -33
  37. package/dist/components/combobox.js +409 -340
  38. package/dist/components/combobox.js.map +1 -1
  39. package/dist/components/currency-select.d.ts +48 -8
  40. package/dist/components/currency-select.js +107 -65
  41. package/dist/components/currency-select.js.map +1 -1
  42. package/dist/components/data-table-filter-popover.d.ts +15 -3
  43. package/dist/components/data-table-filter-popover.js +23 -17
  44. package/dist/components/data-table-filter-popover.js.map +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +3 -2
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +53 -2
  49. package/dist/components/data-table-labels.js +31 -2
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table-pagination.d.ts +3 -1
  52. package/dist/components/data-table-pagination.js +16 -10
  53. package/dist/components/data-table-pagination.js.map +1 -1
  54. package/dist/components/data-table-sort.js +1 -0
  55. package/dist/components/data-table-sort.js.map +1 -1
  56. package/dist/components/data-table.d.ts +1 -1
  57. package/dist/components/data-table.js +201 -85
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/date-picker.d.ts +25 -9
  60. package/dist/components/date-picker.js +232 -94
  61. package/dist/components/date-picker.js.map +1 -1
  62. package/dist/components/dropdown.d.ts +96 -20
  63. package/dist/components/dropdown.js +77 -33
  64. package/dist/components/dropdown.js.map +1 -1
  65. package/dist/components/entity-combobox.d.ts +5 -4
  66. package/dist/components/entity-combobox.js +107 -76
  67. package/dist/components/entity-combobox.js.map +1 -1
  68. package/dist/components/facing-pair.d.ts +36 -0
  69. package/dist/components/facing-pair.js +40 -0
  70. package/dist/components/facing-pair.js.map +1 -0
  71. package/dist/components/field-sync.d.ts +164 -0
  72. package/dist/components/field-sync.js +264 -0
  73. package/dist/components/field-sync.js.map +1 -0
  74. package/dist/components/file-dropzone.d.ts +38 -6
  75. package/dist/components/file-dropzone.js +34 -9
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/full-bleed-dialog.d.ts +37 -26
  78. package/dist/components/full-bleed-dialog.js +20 -5
  79. package/dist/components/full-bleed-dialog.js.map +1 -1
  80. package/dist/components/grouped-picker.d.ts +21 -10
  81. package/dist/components/grouped-picker.js +66 -52
  82. package/dist/components/grouped-picker.js.map +1 -1
  83. package/dist/components/hover-menu.d.ts +20 -4
  84. package/dist/components/hover-menu.js +15 -4
  85. package/dist/components/hover-menu.js.map +1 -1
  86. package/dist/components/mini-calendar.d.ts +102 -14
  87. package/dist/components/mini-calendar.js +256 -70
  88. package/dist/components/mini-calendar.js.map +1 -1
  89. package/dist/components/modal.d.ts +9 -3
  90. package/dist/components/modal.js +34 -45
  91. package/dist/components/modal.js.map +1 -1
  92. package/dist/components/month-picker.d.ts +79 -0
  93. package/dist/components/month-picker.js +342 -0
  94. package/dist/components/month-picker.js.map +1 -0
  95. package/dist/components/multi-entity-combobox.d.ts +7 -6
  96. package/dist/components/multi-entity-combobox.js +112 -72
  97. package/dist/components/multi-entity-combobox.js.map +1 -1
  98. package/dist/components/multi-select.d.ts +13 -11
  99. package/dist/components/multi-select.js +161 -84
  100. package/dist/components/multi-select.js.map +1 -1
  101. package/dist/components/number-field.d.ts +92 -0
  102. package/dist/components/number-field.js +111 -0
  103. package/dist/components/number-field.js.map +1 -0
  104. package/dist/components/number-input.d.ts +28 -2
  105. package/dist/components/number-input.js +12 -2
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +32 -4
  108. package/dist/components/numpad-sheet.js +30 -13
  109. package/dist/components/numpad-sheet.js.map +1 -1
  110. package/dist/components/page-contents.d.ts +90 -0
  111. package/dist/components/page-contents.js +170 -0
  112. package/dist/components/page-contents.js.map +1 -0
  113. package/dist/components/password-strength.d.ts +106 -0
  114. package/dist/components/password-strength.js +132 -0
  115. package/dist/components/password-strength.js.map +1 -0
  116. package/dist/components/picker-sheet.d.ts +23 -15
  117. package/dist/components/picker-sheet.js +34 -14
  118. package/dist/components/picker-sheet.js.map +1 -1
  119. package/dist/components/popover.d.ts +62 -4
  120. package/dist/components/popover.js +52 -2
  121. package/dist/components/popover.js.map +1 -1
  122. package/dist/components/search-field.d.ts +25 -7
  123. package/dist/components/search-field.js +7 -5
  124. package/dist/components/search-field.js.map +1 -1
  125. package/dist/components/series-chart-labels.d.ts +14 -0
  126. package/dist/components/series-chart-labels.js +11 -0
  127. package/dist/components/series-chart-labels.js.map +1 -0
  128. package/dist/components/series-chart.d.ts +207 -0
  129. package/dist/components/series-chart.js +223 -0
  130. package/dist/components/series-chart.js.map +1 -0
  131. package/dist/components/settings-fields.d.ts +14 -4
  132. package/dist/components/settings-fields.js +19 -8
  133. package/dist/components/settings-fields.js.map +1 -1
  134. package/dist/components/signature-pad.d.ts +119 -0
  135. package/dist/components/signature-pad.js +405 -0
  136. package/dist/components/signature-pad.js.map +1 -0
  137. package/dist/components/slider.d.ts +80 -0
  138. package/dist/components/slider.js +155 -0
  139. package/dist/components/slider.js.map +1 -0
  140. package/dist/components/sparkline.d.ts +93 -0
  141. package/dist/components/sparkline.js +205 -0
  142. package/dist/components/sparkline.js.map +1 -0
  143. package/dist/components/stat-tile.d.ts +151 -0
  144. package/dist/components/stat-tile.js +236 -0
  145. package/dist/components/stat-tile.js.map +1 -0
  146. package/dist/components/swipeable-row.d.ts +25 -2
  147. package/dist/components/swipeable-row.js +31 -0
  148. package/dist/components/swipeable-row.js.map +1 -1
  149. package/dist/components/switch.d.ts +60 -0
  150. package/dist/components/switch.js +81 -0
  151. package/dist/components/switch.js.map +1 -0
  152. package/dist/components/time-input.d.ts +58 -0
  153. package/dist/components/time-input.js +50 -0
  154. package/dist/components/time-input.js.map +1 -0
  155. package/dist/components/toggle-group.d.ts +18 -3
  156. package/dist/components/toggle-group.js +10 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +115 -0
  159. package/dist/components/toggle-legend.js +114 -0
  160. package/dist/components/toggle-legend.js.map +1 -0
  161. package/dist/components/tooltip.d.ts +40 -11
  162. package/dist/components/tooltip.js +77 -20
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/treemap.d.ts +132 -0
  165. package/dist/components/treemap.js +179 -0
  166. package/dist/components/treemap.js.map +1 -0
  167. package/dist/components/ui.d.ts +159 -46
  168. package/dist/components/ui.js +220 -115
  169. package/dist/components/ui.js.map +1 -1
  170. package/dist/components/use-mobile-reveal.js +1 -0
  171. package/dist/components/use-mobile-reveal.js.map +1 -1
  172. package/dist/components/use-table-state.d.ts +1 -1
  173. package/dist/components/use-table-state.js +38 -17
  174. package/dist/components/use-table-state.js.map +1 -1
  175. package/dist/components/user-avatar.d.ts +15 -4
  176. package/dist/components/user-avatar.js +5 -4
  177. package/dist/components/user-avatar.js.map +1 -1
  178. package/dist/components/wizard-stepper.js +3 -2
  179. package/dist/components/wizard-stepper.js.map +1 -1
  180. package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
  181. package/dist/data-table.d.ts +7 -0
  182. package/dist/data-table.js +12 -0
  183. package/dist/data-table.js.map +1 -0
  184. package/dist/feedback/feedback-attachment.js +12 -11
  185. package/dist/feedback/feedback-attachment.js.map +1 -1
  186. package/dist/feedback/feedback-dialog.js +2 -1
  187. package/dist/feedback/feedback-dialog.js.map +1 -1
  188. package/dist/feedback/feedback-inbox.d.ts +33 -7
  189. package/dist/feedback/feedback-inbox.js +49 -37
  190. package/dist/feedback/feedback-inbox.js.map +1 -1
  191. package/dist/feedback.d.ts +5 -0
  192. package/dist/feedback.js +5 -0
  193. package/dist/feedback.js.map +1 -0
  194. package/dist/hooks/use-anchored-panel.js +1 -0
  195. package/dist/hooks/use-anchored-panel.js.map +1 -1
  196. package/dist/hooks/use-anchored-rect.js +1 -0
  197. package/dist/hooks/use-anchored-rect.js.map +1 -1
  198. package/dist/hooks/use-announce.d.ts +46 -0
  199. package/dist/hooks/use-announce.js +37 -0
  200. package/dist/hooks/use-announce.js.map +1 -0
  201. package/dist/hooks/use-body-scroll-lock.js +1 -0
  202. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  203. package/dist/hooks/use-close-transition.js +1 -0
  204. package/dist/hooks/use-close-transition.js.map +1 -1
  205. package/dist/hooks/use-dismiss.js +1 -0
  206. package/dist/hooks/use-dismiss.js.map +1 -1
  207. package/dist/hooks/use-focus-trap.d.ts +16 -0
  208. package/dist/hooks/use-focus-trap.js +67 -0
  209. package/dist/hooks/use-focus-trap.js.map +1 -0
  210. package/dist/hooks/use-media-query.js +1 -0
  211. package/dist/hooks/use-media-query.js.map +1 -1
  212. package/dist/hooks/use-overlay-history.js +27 -1
  213. package/dist/hooks/use-overlay-history.js.map +1 -1
  214. package/dist/hooks/use-row-swipe.js +1 -0
  215. package/dist/hooks/use-row-swipe.js.map +1 -1
  216. package/dist/i18n/defaults.d.ts +31 -0
  217. package/dist/i18n/defaults.js +63 -0
  218. package/dist/i18n/defaults.js.map +1 -0
  219. package/dist/i18n/kit-labels.d.ts +250 -0
  220. package/dist/i18n/kit-labels.js +184 -0
  221. package/dist/i18n/kit-labels.js.map +1 -0
  222. package/dist/index.d.ts +47 -22
  223. package/dist/index.js +28 -0
  224. package/dist/index.js.map +1 -1
  225. package/dist/lib/calc.js +1 -0
  226. package/dist/lib/calc.js.map +1 -1
  227. package/dist/lib/cn.js +1 -0
  228. package/dist/lib/cn.js.map +1 -1
  229. package/dist/lib/dates.js +1 -0
  230. package/dist/lib/dates.js.map +1 -1
  231. package/dist/lib/logger.js +1 -0
  232. package/dist/lib/logger.js.map +1 -1
  233. package/dist/lib/safe-storage.js +1 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/search/command-palette.d.ts +11 -1
  236. package/dist/search/command-palette.js +21 -15
  237. package/dist/search/command-palette.js.map +1 -1
  238. package/dist/search.d.ts +2 -0
  239. package/dist/search.js +3 -0
  240. package/dist/search.js.map +1 -0
  241. package/dist/shell/app-shell.d.ts +33 -4
  242. package/dist/shell/app-shell.js +275 -78
  243. package/dist/shell/app-shell.js.map +1 -1
  244. package/dist/shell/option-switcher-menu.js +3 -2
  245. package/dist/shell/option-switcher-menu.js.map +1 -1
  246. package/dist/shell/role-switcher.d.ts +6 -0
  247. package/dist/shell/role-switcher.js +7 -4
  248. package/dist/shell/role-switcher.js.map +1 -1
  249. package/dist/shell/top-bar.d.ts +15 -7
  250. package/dist/shell/top-bar.js +3 -5
  251. package/dist/shell/top-bar.js.map +1 -1
  252. package/dist/shell/topbar-action-menu.js +3 -2
  253. package/dist/shell/topbar-action-menu.js.map +1 -1
  254. package/dist/shell/topbar-controls.d.ts +5 -2
  255. package/dist/shell/topbar-controls.js +20 -15
  256. package/dist/shell/topbar-controls.js.map +1 -1
  257. package/dist/shell.d.ts +13 -0
  258. package/dist/shell.js +8 -0
  259. package/dist/shell.js.map +1 -0
  260. package/dist/theme/chart-palette.d.ts +19 -12
  261. package/dist/theme/chart-palette.js +24 -30
  262. package/dist/theme/chart-palette.js.map +1 -1
  263. package/dist/theme/color.d.ts +78 -0
  264. package/dist/theme/color.js +148 -0
  265. package/dist/theme/color.js.map +1 -0
  266. package/dist/theme/palette-derive.d.ts +140 -0
  267. package/dist/theme/palette-derive.js +376 -0
  268. package/dist/theme/palette-derive.js.map +1 -0
  269. package/dist/theme/palette-presets.d.ts +27 -5
  270. package/dist/theme/palette-presets.js +231 -33
  271. package/dist/theme/palette-presets.js.map +1 -1
  272. package/dist/theme/palette-store.js +1 -0
  273. package/dist/theme/palette-store.js.map +1 -1
  274. package/dist/theme/theme-store.js +1 -0
  275. package/dist/theme/theme-store.js.map +1 -1
  276. package/dist/tour/tour.d.ts +2 -1
  277. package/dist/tour/tour.js +15 -9
  278. package/dist/tour/tour.js.map +1 -1
  279. package/dist/tour.d.ts +2 -0
  280. package/dist/tour.js +3 -0
  281. package/dist/tour.js.map +1 -0
  282. package/dist/wizard/stepper-nav.js +10 -8
  283. package/dist/wizard/stepper-nav.js.map +1 -1
  284. package/dist/wizard/types.d.ts +18 -2
  285. package/dist/wizard/types.js +4 -1
  286. package/dist/wizard/types.js.map +1 -1
  287. package/dist/wizard/use-wizard.js +12 -4
  288. package/dist/wizard/use-wizard.js.map +1 -1
  289. package/dist/wizard/validation.js +1 -0
  290. package/dist/wizard/validation.js.map +1 -1
  291. package/dist/wizard/wizard-context.js +1 -0
  292. package/dist/wizard/wizard-context.js.map +1 -1
  293. package/dist/wizard/wizard-step.js +1 -0
  294. package/dist/wizard/wizard-step.js.map +1 -1
  295. package/dist/wizard/wizard-summary.js +5 -3
  296. package/dist/wizard/wizard-summary.js.map +1 -1
  297. package/dist/wizard.d.ts +8 -0
  298. package/dist/wizard.js +9 -0
  299. package/dist/wizard.js.map +1 -0
  300. package/package.json +67 -14
  301. package/src/chart.ts +22 -0
  302. package/src/components/account-settings.tsx +275 -0
  303. package/src/components/alert-banner.tsx +75 -0
  304. package/src/components/amount-input.tsx +471 -0
  305. package/src/components/calculator.tsx +252 -0
  306. package/src/components/chart-zoom.tsx +499 -0
  307. package/src/components/chart.tsx +363 -0
  308. package/src/components/checkbox.tsx +222 -0
  309. package/src/components/chip.tsx +570 -0
  310. package/src/components/choice-parts.ts +33 -0
  311. package/src/components/combobox-core.tsx +540 -0
  312. package/src/components/combobox.tsx +1026 -0
  313. package/src/components/currency-select.tsx +269 -0
  314. package/src/components/data-table-filter-popover.tsx +252 -0
  315. package/src/components/data-table-filters.ts +176 -0
  316. package/src/components/data-table-labels.ts +190 -0
  317. package/src/components/data-table-pagination.tsx +126 -0
  318. package/src/components/data-table-sort.ts +70 -0
  319. package/src/components/data-table.tsx +1740 -0
  320. package/src/components/date-picker.tsx +631 -0
  321. package/src/components/dropdown.tsx +341 -0
  322. package/src/components/entity-combobox.tsx +215 -0
  323. package/src/components/facing-pair.ts +86 -0
  324. package/src/components/field-sync.tsx +472 -0
  325. package/src/components/file-dropzone.tsx +181 -0
  326. package/src/components/full-bleed-dialog.tsx +179 -0
  327. package/src/components/grouped-picker.tsx +157 -0
  328. package/src/components/hover-menu.tsx +183 -0
  329. package/src/components/mini-calendar.tsx +554 -0
  330. package/src/components/modal.tsx +287 -0
  331. package/src/components/month-picker.tsx +549 -0
  332. package/src/components/multi-entity-combobox.tsx +226 -0
  333. package/src/components/multi-select.tsx +303 -0
  334. package/src/components/number-field.tsx +225 -0
  335. package/src/components/number-input.tsx +211 -0
  336. package/src/components/numpad-sheet.tsx +264 -0
  337. package/src/components/page-contents.tsx +289 -0
  338. package/src/components/password-strength.tsx +258 -0
  339. package/src/components/picker-sheet.tsx +251 -0
  340. package/src/components/popover.tsx +195 -0
  341. package/src/components/search-field.tsx +146 -0
  342. package/src/components/series-chart-labels.ts +26 -0
  343. package/src/components/series-chart.tsx +481 -0
  344. package/src/components/settings-fields.tsx +68 -0
  345. package/src/components/signature-pad.tsx +630 -0
  346. package/src/components/slider.tsx +313 -0
  347. package/src/components/sparkline.tsx +329 -0
  348. package/src/components/stat-tile.tsx +448 -0
  349. package/src/components/swipeable-row.tsx +271 -0
  350. package/src/components/switch.tsx +170 -0
  351. package/src/components/time-input.tsx +107 -0
  352. package/src/components/toggle-group.tsx +153 -0
  353. package/src/components/toggle-legend.tsx +229 -0
  354. package/src/components/tooltip.tsx +477 -0
  355. package/src/components/treemap.tsx +390 -0
  356. package/src/components/ui.tsx +1284 -0
  357. package/src/components/use-mobile-reveal.ts +54 -0
  358. package/src/components/use-table-state.ts +295 -0
  359. package/src/components/user-avatar.tsx +61 -0
  360. package/src/components/wizard-stepper.tsx +37 -0
  361. package/src/data-table.ts +19 -0
  362. package/src/feedback/feedback-attachment.tsx +246 -0
  363. package/src/feedback/feedback-dialog.tsx +209 -0
  364. package/src/feedback/feedback-inbox.tsx +615 -0
  365. package/src/feedback.ts +8 -0
  366. package/src/hooks/use-anchored-panel.ts +169 -0
  367. package/src/hooks/use-anchored-rect.ts +78 -0
  368. package/src/hooks/use-announce.ts +85 -0
  369. package/src/hooks/use-body-scroll-lock.ts +94 -0
  370. package/src/hooks/use-close-transition.ts +99 -0
  371. package/src/hooks/use-dismiss.ts +77 -0
  372. package/src/hooks/use-focus-trap.ts +160 -0
  373. package/src/hooks/use-media-query.ts +23 -0
  374. package/src/hooks/use-overlay-history.ts +345 -0
  375. package/src/hooks/use-row-swipe.ts +212 -0
  376. package/src/i18n/defaults.ts +68 -0
  377. package/src/i18n/kit-labels.tsx +447 -0
  378. package/src/index.ts +169 -0
  379. package/src/lib/calc.ts +309 -0
  380. package/src/lib/cn.ts +5 -0
  381. package/src/lib/dates.ts +187 -0
  382. package/src/lib/logger.ts +73 -0
  383. package/src/lib/safe-storage.ts +37 -0
  384. package/src/search/command-palette.tsx +342 -0
  385. package/src/search.ts +3 -0
  386. package/src/shell/app-shell.tsx +772 -0
  387. package/src/shell/option-switcher-menu.tsx +72 -0
  388. package/src/shell/role-switcher.tsx +46 -0
  389. package/src/shell/top-bar.tsx +39 -0
  390. package/src/shell/topbar-action-menu.tsx +91 -0
  391. package/src/shell/topbar-controls.tsx +229 -0
  392. package/src/shell.ts +10 -0
  393. package/src/theme/chart-palette.ts +145 -0
  394. package/src/theme/color.ts +266 -0
  395. package/src/theme/palette-derive.ts +681 -0
  396. package/src/theme/palette-presets.ts +760 -0
  397. package/src/theme/palette-store.ts +94 -0
  398. package/src/theme/theme-store.ts +145 -0
  399. package/src/tour/tour.tsx +586 -0
  400. package/src/tour.ts +3 -0
  401. package/src/vite-env.d.ts +26 -0
  402. package/src/wizard/stepper-nav.tsx +221 -0
  403. package/src/wizard/types.ts +178 -0
  404. package/src/wizard/use-wizard.ts +345 -0
  405. package/src/wizard/validation.ts +37 -0
  406. package/src/wizard/wizard-context.tsx +29 -0
  407. package/src/wizard/wizard-step.tsx +14 -0
  408. package/src/wizard/wizard-summary.tsx +69 -0
  409. package/src/wizard.ts +14 -0
  410. package/tokens.css +222 -20
@@ -0,0 +1,223 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import { CartesianGrid, Label, Line, LineChart, ReferenceLine, XAxis, YAxis } from "recharts";
5
+ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart.js";
6
+ import { DEFAULT_Y_AXIS, withChartZoom } from "./chart-zoom.js";
7
+ import { STEP_DASH, strokeDash } from "./toggle-legend.js";
8
+ import { DEFAULT_SERIES_CHART_LABELS } from "./series-chart-labels.js";
9
+ import { paletteFor } from "../theme/chart-palette.js";
10
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
11
+ import { cn } from "../lib/cn.js";
12
+ const AXIS_TITLE_STRIP = 16;
13
+ const AXIS_TITLE_OFFSET = 10;
14
+ const AXIS_TICK_WIDTH = 48;
15
+ const AUTO_PAD = 0.04;
16
+ function paddedDomain(values) {
17
+ let low = Infinity;
18
+ let high = -Infinity;
19
+ for (const value of values) {
20
+ if (!Number.isFinite(value)) continue;
21
+ if (value < low) low = value;
22
+ if (value > high) high = value;
23
+ }
24
+ return padBand(low, high);
25
+ }
26
+ function padBand(low, high) {
27
+ if (!(low <= high)) return void 0;
28
+ const pad = (high - low) * AUTO_PAD || Math.abs(high) * AUTO_PAD || 1;
29
+ return [low - pad, high + pad];
30
+ }
31
+ function axisBandWidth(tickWidth, titled) {
32
+ return (tickWidth ?? AXIS_TICK_WIDTH) + (titled ? AXIS_TITLE_STRIP : 0);
33
+ }
34
+ function soleSeriesColor(axis, series) {
35
+ const mine = series.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axis.id);
36
+ return mine.length === 1 ? axis.color : void 0;
37
+ }
38
+ function oneAxis(format, title, width, orientation) {
39
+ return [{ id: DEFAULT_Y_AXIS, format, title, width, orientation }];
40
+ }
41
+ function mergeSeries(sources) {
42
+ const byAbscissa = /* @__PURE__ */ new Map();
43
+ for (const source of sources) {
44
+ source.x.forEach((value, index) => {
45
+ const row = byAbscissa.get(value) ?? { x: value };
46
+ for (const [key, channel] of Object.entries(source.channels)) row[key] = channel[index];
47
+ byAbscissa.set(value, row);
48
+ });
49
+ }
50
+ return [...byAbscissa.values()].sort((a, b) => a.x - b.x);
51
+ }
52
+ function seriesKey(...parts) {
53
+ const key = parts.map((part) => String(part).replace(/[^A-Za-z0-9_-]/g, "_")).join("_");
54
+ return /^[A-Za-z_]/.test(key) ? key : `_${key}`;
55
+ }
56
+ function useDefaultFormat(localeProp) {
57
+ const locale = useKitLocale(localeProp);
58
+ return useMemo(() => {
59
+ const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
60
+ return (value) => format.format(value);
61
+ }, [locale]);
62
+ }
63
+ const ONE_UNTITLED_AXIS = [{ id: DEFAULT_Y_AXIS, title: "" }];
64
+ function StaticSeriesChart({
65
+ rows,
66
+ x = {},
67
+ series,
68
+ axes = ONE_UNTITLED_AXIS,
69
+ valueFormat,
70
+ height = "h-72",
71
+ connectNulls,
72
+ empty,
73
+ animationMs = 0,
74
+ spans,
75
+ zoom,
76
+ labels: labelsProp,
77
+ locale,
78
+ className
79
+ }) {
80
+ const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, labelsProp);
81
+ const number = useDefaultFormat(locale);
82
+ if (!rows.length || !series.length) {
83
+ return /* @__PURE__ */ jsx("div", { className: cn("flex w-full items-center justify-center px-2 text-center", height), children: empty !== void 0 ? empty : /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: labels.empty }) });
84
+ }
85
+ const xKey = x.key ?? "x";
86
+ const xFormat = x.format ?? number;
87
+ const config = Object.fromEntries(
88
+ series.map((entry, index) => [
89
+ entry.key,
90
+ { label: entry.label, color: entry.color ?? paletteFor(index) }
91
+ ])
92
+ );
93
+ const visible = axes.filter((axis) => !axis.hide);
94
+ const gridAxis = (visible[0] ?? axes[0])?.id ?? DEFAULT_Y_AXIS;
95
+ const onLeft = visible.some((axis) => (axis.orientation ?? "left") === "left");
96
+ const onRight = visible.some((axis) => axis.orientation === "right");
97
+ const margin = {
98
+ top: 8,
99
+ right: onRight ? 0 : 10,
100
+ left: onLeft ? 0 : 10,
101
+ // Under the ticks, not under the whole axis: whatever comes next owns the gap.
102
+ bottom: x.title ? 18 : 2
103
+ };
104
+ const fittedX = zoom?.xDomain ?? paddedDomain(rows.map((row) => row[xKey]));
105
+ const fittedY = (axis) => zoom?.yDomains[axis.id] ?? axis.domain ?? paddedDomain(
106
+ series.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axis.id).flatMap((entry) => rows.map((row) => row[entry.key]))
107
+ );
108
+ return /* @__PURE__ */ jsx(ChartContainer, { config, className: cn("w-full", height, className), children: /* @__PURE__ */ jsxs(LineChart, { data: rows, margin, children: [
109
+ /* @__PURE__ */ jsx(CartesianGrid, { yAxisId: gridAxis }),
110
+ /* @__PURE__ */ jsx(
111
+ XAxis,
112
+ {
113
+ dataKey: xKey,
114
+ type: "number",
115
+ domain: fittedX ?? ["dataMin", "dataMax"],
116
+ allowDataOverflow: zoom?.xDomain !== void 0,
117
+ tickLine: false,
118
+ axisLine: false,
119
+ minTickGap: 32,
120
+ tick: x.ticks === false ? false : void 0,
121
+ height: x.ticks === false && !x.title ? 4 : void 0,
122
+ tickFormatter: xFormat,
123
+ children: x.title && /* @__PURE__ */ jsx(
124
+ Label,
125
+ {
126
+ value: x.title,
127
+ position: "insideBottom",
128
+ offset: -12,
129
+ className: "fill-[var(--text-muted)] text-[11px]"
130
+ }
131
+ )
132
+ }
133
+ ),
134
+ axes.map((axis) => /* @__PURE__ */ jsx(
135
+ YAxis,
136
+ {
137
+ yAxisId: axis.id,
138
+ hide: axis.hide,
139
+ domain: fittedY(axis),
140
+ allowDataOverflow: zoom?.yDomains[axis.id] !== void 0,
141
+ orientation: axis.orientation ?? "left",
142
+ width: axisBandWidth(axis.width, Boolean(axis.title) && !axis.hide),
143
+ tickLine: false,
144
+ axisLine: false,
145
+ tickFormatter: axis.format ?? number,
146
+ tick: axis.color ? { fill: axis.color } : void 0,
147
+ children: axis.title && !axis.hide && /* @__PURE__ */ jsx(
148
+ Label,
149
+ {
150
+ value: axis.title,
151
+ angle: axis.orientation === "right" ? 90 : -90,
152
+ position: axis.orientation === "right" ? "insideRight" : "insideLeft",
153
+ style: { textAnchor: "middle", fill: soleSeriesColor(axis, series) },
154
+ offset: AXIS_TITLE_OFFSET,
155
+ className: "fill-[var(--text-muted)] text-[11px]"
156
+ }
157
+ )
158
+ },
159
+ axis.id
160
+ )),
161
+ /* @__PURE__ */ jsx(
162
+ ChartTooltip,
163
+ {
164
+ content: /* @__PURE__ */ jsx(
165
+ ChartTooltipContent,
166
+ {
167
+ labelFormatter: (value) => (x.label ?? xFormat)(Number(value)),
168
+ valueFormatter: valueFormat ?? number
169
+ }
170
+ )
171
+ }
172
+ ),
173
+ series.map((entry) => /* @__PURE__ */ jsx(
174
+ Line,
175
+ {
176
+ yAxisId: entry.axis ?? DEFAULT_Y_AXIS,
177
+ type: entry.step ? "stepAfter" : "monotone",
178
+ dataKey: entry.key,
179
+ stroke: `var(--color-${entry.key})`,
180
+ strokeWidth: entry.step ? 1.5 : 2,
181
+ strokeDasharray: strokeDash(
182
+ entry.step ? STEP_DASH : entry.dash ?? (entry.dashed ? 1 : 0)
183
+ ),
184
+ dot: false,
185
+ connectNulls,
186
+ isAnimationActive: animationMs > 0,
187
+ animationDuration: animationMs,
188
+ animationEasing: "ease-out"
189
+ },
190
+ entry.key
191
+ )),
192
+ spans?.map((span) => /* @__PURE__ */ jsx(
193
+ ReferenceLine,
194
+ {
195
+ yAxisId: span.axis ?? DEFAULT_Y_AXIS,
196
+ segment: [
197
+ { x: span.x, y: span.from },
198
+ { x: span.x, y: span.to }
199
+ ],
200
+ stroke: span.color ?? "var(--text-muted)",
201
+ strokeWidth: 2,
202
+ ifOverflow: "discard"
203
+ },
204
+ span.key
205
+ )),
206
+ zoom?.layer
207
+ ] }) });
208
+ }
209
+ const SeriesChart = withChartZoom(StaticSeriesChart);
210
+ export {
211
+ AXIS_TICK_WIDTH,
212
+ AXIS_TITLE_STRIP,
213
+ SeriesChart,
214
+ StaticSeriesChart,
215
+ axisBandWidth,
216
+ mergeSeries,
217
+ oneAxis,
218
+ padBand,
219
+ paddedDomain,
220
+ seriesKey,
221
+ soleSeriesColor
222
+ };
223
+ //# sourceMappingURL=series-chart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/series-chart.tsx"],"sourcesContent":["// The measurement plot: series against a swept numeric abscissa, any number of y axes,\n// dash patterns, vertical spans, and drag-to-zoom. Lifted out of lenkbank, where every\n// chart on every screen is this one arrangement of the kit's shell — so the arrangement\n// lives here once instead of as thirteen hand-assembled copies drifting apart.\n//\n// Built on `ChartContainer` + `ChartTooltipContent`, so it themes with the rest of the\n// chart kit: series default to the `--chart-N` ramp, axis text and grid take the\n// shell's tokens. It stays domain-free — every unit, title and number format is the\n// caller's, and the few words it can say itself are the `seriesChart` label namespace.\n//\n// **Every series may keep its own scale.** Position runs in millimetres and jerk in\n// hundreds of thousands of them per second cubed; on one axis the position line is\n// flat against the bottom. Normalising each series to its peak makes the y axis mean\n// nothing, so a caller declares an axis per incomparable channel instead, and may hide\n// all but the ones worth a column of numbers.\n//\n// Charts are drawn left-to-right in every writing direction — an abscissa is a number\n// line, not text — so `orientation` below is physical, and only the HTML chrome (the\n// reset button) is placed by logical side.\nimport { useMemo } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { CartesianGrid, Label, Line, LineChart, ReferenceLine, XAxis, YAxis } from \"recharts\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { DEFAULT_Y_AXIS, withChartZoom, type ZoomBinding } from \"./chart-zoom\";\nimport { STEP_DASH, strokeDash } from \"./toggle-legend\";\nimport { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from \"./series-chart-labels\";\nimport { paletteFor } from \"../theme/chart-palette\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\n\nexport interface SeriesChartSeries {\n /** The row key this line reads — and the `--color-<key>` it is painted with, so it\n * must be a CSS identifier. Build keys out of data with {@link seriesKey}. */\n key: string;\n label: ReactNode;\n /** Any CSS colour the chart shell accepts (hex, rgb/hsl, `var(--token)`). Default:\n * the kit's categorical ramp by position, `paletteFor(index)`. */\n color?: string;\n /** Marks the PAIRED line of a pair — the return stroke against the out-stroke, the\n * reference against the measurement. Shorthand for `dash: 1`. */\n dashed?: boolean;\n /** Which of {@link STROKE_PATTERNS} this line takes, for charts where the stroke says\n * WHICH QUANTITY and the colour says WHICH MEASUREMENT. */\n dash?: number;\n /** `stepAfter`, for a whole-number channel that jumps rather than travels — a\n * straight line between index 0 and index 1 draws an index of 0.5, which does not\n * exist. Drawn in the {@link STEP_DASH} pattern, whatever `dash` says. */\n step?: boolean;\n /** Which of `axes` this line is measured on. Default: `\"y\"`, the single one. */\n axis?: string;\n}\n\ninterface SeriesChartAxisShape {\n id: string;\n /**\n * Which side of the plot the ticks stand on. Left unless said otherwise — and said\n * otherwise for the right-hand chart of a FACING pair (see `facingAxes`), whose\n * scales go on the outside so neither column of numbers stands between two pictures\n * being compared.\n */\n orientation?: \"left\" | \"right\";\n /** Tints the ticks — and the title too, when exactly one series is measured on this\n * axis (see {@link soleSeriesColor}). */\n color?: string;\n /** What the TICKS need, in px. The title's strip is added on top. Default 48. */\n width?: number;\n /**\n * Pin the scale instead of fitting it to this chart's data. For a ROW of charts\n * read against each other: auto-fitted, a 200 N loop and a 2000 N one draw the\n * identical picture, and the comparison the layout promises is the one it fails.\n */\n domain?: [number, number];\n /** A tick, formatted — as a BARE number. The unit is said once, in the title; the\n * tooltip gets it from `valueFormat`. Default: `Intl.NumberFormat` in the kit's\n * locale, up to two fraction digits. */\n format?: (value: number) => string;\n}\n\n/**\n * A y axis — and a VISIBLE one says what it measures.\n *\n * A union rather than an optional field, so the rule is the type's: a column of numbers\n * whose quantity and unit are not written down is a column somebody reads as whatever\n * they expected. `title: \"\"` is the explicit opt-out (the right-hand chart of a facing\n * pair, whose neighbour already said it). A HIDDEN axis scales its lines and draws\n * nothing, so there is nowhere for a title to go.\n */\nexport type SeriesChartAxis = SeriesChartAxisShape &\n ({ title: string; hide?: false } | { title?: string; hide: true });\n\nexport interface SeriesChartX {\n /** Row key of the abscissa. Default `\"x\"`, which is what {@link mergeSeries} writes. */\n key?: string;\n /** What the abscissa is, with its unit. Drawn under the axis. */\n title?: string;\n /** A tick, formatted — bare, like the y axes'. */\n format?: (value: number) => string;\n /**\n * Whether this chart prints the abscissa's ticks. On by default; off for every chart\n * of a stack sharing one x window but the bottom one, which prints them for all. It\n * costs no alignment: the x mapping is decided by the margins and the y bands.\n */\n ticks?: boolean;\n /** The tooltip's heading. Default: the abscissa value through `format`. */\n label?: (value: number) => ReactNode;\n}\n\n/**\n * A vertical segment in data coordinates — two y at one x, which a row-per-abscissa\n * chart structurally cannot draw. For CLOSING a shape the series already drew: the\n * turnarounds of a hysteresis loop, where the abscissa stands still while the value\n * falls from one branch to the other.\n */\nexport interface SeriesChartSpan {\n /** Distinct within the chart. */\n key: string;\n /** Which y axis `from`/`to` are measured on. Default `\"y\"`. */\n axis?: string;\n x: number;\n from: number;\n to: number;\n /** Default: `var(--text-muted)`. */\n color?: string;\n}\n\nexport interface SeriesChartProps {\n /** One row per abscissa value. A missing key is a hole (see `connectNulls`). */\n rows: Record<string, number>[];\n series: SeriesChartSeries[];\n /** Default: one untitled axis, `\"y\"`. */\n axes?: SeriesChartAxis[];\n x?: SeriesChartX;\n /** A value in the tooltip, where a number stands on its own and needs its unit.\n * Default: `Intl.NumberFormat` in the kit's locale. */\n valueFormat?: (value: number) => string;\n /** Tailwind height class. Default `h-72`. The empty state takes it too, so a chart\n * losing its last line does not relayout the page under it. */\n height?: string;\n /** Bridge holes left by sources sampled on different grids. See {@link mergeSeries}. */\n connectNulls?: boolean;\n /** Shown, centred at the chart's height, instead of an empty chart. Default: the\n * `seriesChart.empty` label. `null` shows nothing. */\n empty?: ReactNode;\n /**\n * Milliseconds for a line to draw itself in. Off by default: a chart beside a live\n * editor re-renders on every keystroke, and an animation restarted mid-flight reads\n * as the curve twitching. Turn it on where series come and go by choice.\n */\n animationMs?: number;\n spans?: SeriesChartSpan[];\n /** Supplied by `withChartZoom` and by nothing else. */\n zoom?: ZoomBinding;\n /** Per-chart strings over `<UiKitProvider labels={{ seriesChart }}>`. */\n labels?: Partial<SeriesChartLabels>;\n /** BCP 47 tag for the default number formats. Default: the provider's. */\n locale?: string;\n /** Onto the chart's root (`ChartContainer`) — e.g. an app's own grid ink. */\n className?: string;\n}\n\n/** How much of an axis band the rotated title takes, in px. Reserved rather than\n * shared with the ticks: a title merely offset inside the band prints on top of a\n * five-figure tick, and which gives way depends on the data. */\nexport const AXIS_TITLE_STRIP = 16;\n\n/** Where inside its strip the rotated title sits. */\nconst AXIS_TITLE_OFFSET = 10;\n\n/** The width the ticks of a y axis get when a caller does not say. */\nexport const AXIS_TICK_WIDTH = 48;\n\n/**\n * How much air a FITTED axis leaves between the data and its frame, as a fraction of\n * the range. Recharts fits `[dataMin, dataMax]`, which draws a peak half a stroke\n * outside the plot and a curve running to its limit indistinguishable from the frame.\n */\nconst AUTO_PAD = 0.04;\n\n/** The band a fitted axis draws, padded — and a real band for constant data, which is\n * a zero-height domain recharts cannot scale into. `undefined` when nothing finite. */\nexport function paddedDomain(values: Iterable<number>): [number, number] | undefined {\n let low = Infinity;\n let high = -Infinity;\n for (const value of values) {\n if (!Number.isFinite(value)) continue;\n if (value < low) low = value;\n if (value > high) high = value;\n }\n return padBand(low, high);\n}\n\n/**\n * {@link paddedDomain} for a caller that has already walked its own data — so a row\n * of charts can share one scale without building an array to find two numbers in, and\n * without a second padding rule. `low > high` means \"nothing was seen\": `undefined`.\n */\nexport function padBand(low: number, high: number): [number, number] | undefined {\n if (!(low <= high)) return undefined;\n const pad = (high - low) * AUTO_PAD || Math.abs(high) * AUTO_PAD || 1;\n return [low - pad, high + pad];\n}\n\n/** The whole band a y axis occupies: its ticks, and its title when it draws one. For\n * lining a heading up over a PLOT rather than over its column. */\nexport function axisBandWidth(tickWidth: number | undefined, titled: boolean): number {\n return (tickWidth ?? AXIS_TICK_WIDTH) + (titled ? AXIS_TITLE_STRIP : 0);\n}\n\n/**\n * The colour an axis TITLE takes, or `undefined` for the neutral one.\n *\n * An axis measuring exactly one line is that line's, and saying so in the title lets a\n * reader find a channel's scale without counting axes. An axis measuring several is\n * nobody's — a title in the first one's colour would be claiming it. Counted off the\n * series, so toggling a second line onto an axis gives the colour up in the same render.\n */\nexport function soleSeriesColor(\n axis: Pick<SeriesChartAxisShape, \"id\" | \"color\">,\n series: readonly Pick<SeriesChartSeries, \"axis\">[],\n): string | undefined {\n const mine = series.filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axis.id);\n return mine.length === 1 ? axis.color : undefined;\n}\n\n/** The single-axis case, which is most charts. `width` is what the TICKS need;\n * `orientation` is which side they stand on. */\nexport function oneAxis(\n format: ((value: number) => string) | undefined,\n title: string,\n width?: number,\n orientation?: \"left\" | \"right\",\n): SeriesChartAxis[] {\n return [{ id: DEFAULT_Y_AXIS, format, title, width, orientation }];\n}\n\n/** One source's samples: an abscissa and the channels measured against it. */\nexport interface SeriesSource {\n x: number[];\n channels: Record<string, number[]>;\n}\n\n/**\n * Merge sources sampled on different grids into rows the chart can draw.\n *\n * Merged ON THE ABSCISSA VALUE ITSELF, never interpolated onto a common grid: a line\n * through invented points looks exactly like one through measured ones. Where a source\n * has no sample the key is absent, and `connectNulls` bridges the hole with the same\n * straight segment an interpolation would have drawn — without claiming a measurement.\n */\nexport function mergeSeries(sources: SeriesSource[]): Record<string, number>[] {\n const byAbscissa = new Map<number, Record<string, number>>();\n for (const source of sources) {\n source.x.forEach((value, index) => {\n const row = byAbscissa.get(value) ?? { x: value };\n for (const [key, channel] of Object.entries(source.channels)) row[key] = channel[index];\n byAbscissa.set(value, row);\n });\n }\n return [...byAbscissa.values()].sort((a, b) => a.x - b.x);\n}\n\n/**\n * A series key built out of parts — as a CSS identifier.\n *\n * The chart shell paints each series through `--color-<key>`, and skips (with a\n * console warning) any key that is not `[A-Za-z_][A-Za-z0-9_-]*`. A line whose colour\n * was skipped has no stroke at all: an axis, a grid and nothing between them. So a key\n * assembled out of DATA — an id, an enum, a side — goes through here: every other\n * character becomes `_`, the parts are joined with `_`, and a key that would start with\n * a digit or a hyphen gets a leading `_`.\n */\nexport function seriesKey(...parts: (string | number)[]): string {\n const key = parts.map((part) => String(part).replace(/[^A-Za-z0-9_-]/g, \"_\")).join(\"_\");\n return /^[A-Za-z_]/.test(key) ? key : `_${key}`;\n}\n\n/** The number format every default falls back to. */\nfunction useDefaultFormat(localeProp: string | undefined): (value: number) => string {\n const locale = useKitLocale(localeProp);\n return useMemo(() => {\n const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });\n return (value: number) => format.format(value);\n }, [locale]);\n}\n\n/** The Y-axis list a chart draws when the caller gave none. */\nconst ONE_UNTITLED_AXIS: SeriesChartAxis[] = [{ id: DEFAULT_Y_AXIS, title: \"\" }];\n\n/**\n * {@link SeriesChart} without the zoom: the same picture, no drag layer, no reset\n * button. For a thumbnail, a print view, or a chart the consumer wraps in its own\n * interaction.\n */\nexport function StaticSeriesChart({\n rows,\n x = {},\n series,\n axes = ONE_UNTITLED_AXIS,\n valueFormat,\n height = \"h-72\",\n connectNulls,\n empty,\n animationMs = 0,\n spans,\n zoom,\n labels: labelsProp,\n locale,\n className,\n}: SeriesChartProps) {\n const labels = useKitLabels(\"seriesChart\", DEFAULT_SERIES_CHART_LABELS, labelsProp);\n const number = useDefaultFormat(locale);\n\n // The empty state takes the chart's own height and is centred in it: a chart that\n // shrank to its \"nothing to draw\" sentence moved everything under it up the page\n // the moment a legend entry was switched off.\n if (!rows.length || !series.length) {\n return (\n <div className={cn(\"flex w-full items-center justify-center px-2 text-center\", height)}>\n {empty !== undefined ? (\n empty\n ) : (\n <p className=\"text-xs text-[var(--text-muted)]\">{labels.empty}</p>\n )}\n </div>\n );\n }\n\n const xKey = x.key ?? \"x\";\n const xFormat = x.format ?? number;\n const config: ChartConfig = Object.fromEntries(\n series.map((entry, index) => [\n entry.key,\n { label: entry.label, color: entry.color ?? paletteFor(index) },\n ]),\n );\n const visible = axes.filter((axis) => !axis.hide);\n // The grid hangs its horizontal rules off ONE y axis, and recharts looks for the one\n // whose id is its own default (`0`). Every axis here is named, so without saying\n // which, it finds none and draws no horizontal rules at all. The first visible axis,\n // because those are the ticks a reader puts a ruler on.\n const gridAxis = (visible[0] ?? axes[0])?.id ?? DEFAULT_Y_AXIS;\n const onLeft = visible.some((axis) => (axis.orientation ?? \"left\") === \"left\");\n const onRight = visible.some((axis) => axis.orientation === \"right\");\n // The margins are what is left once the axes have their bands: an axis band IS the\n // gutter on its side, so a margin beside one is a second gutter. What remains is the\n // overhang of the first/last x tick where no y axis covers it.\n const margin = {\n top: 8,\n right: onRight ? 0 : 10,\n left: onLeft ? 0 : 10,\n // Under the ticks, not under the whole axis: whatever comes next owns the gap.\n bottom: x.title ? 18 : 2,\n };\n\n // Fitted with air around it, unless the reader has zoomed or the caller pinned it.\n const fittedX = zoom?.xDomain ?? paddedDomain(rows.map((row) => row[xKey]));\n const fittedY = (axis: SeriesChartAxis) =>\n zoom?.yDomains[axis.id] ??\n axis.domain ??\n paddedDomain(\n series\n .filter((entry) => (entry.axis ?? DEFAULT_Y_AXIS) === axis.id)\n .flatMap((entry) => rows.map((row) => row[entry.key])),\n );\n\n return (\n <ChartContainer config={config} className={cn(\"w-full\", height, className)}>\n <LineChart data={rows} margin={margin}>\n {/* Both ways: a measurement plot is read by putting a ruler on it, and a\n horizontal-only grid answers half of those questions. */}\n <CartesianGrid yAxisId={gridAxis} />\n <XAxis\n dataKey={xKey}\n // Numeric, not categorical: a measured sweep is unevenly spaced, and a\n // category axis would straighten exactly the curvature the chart is for.\n type=\"number\"\n domain={fittedX ?? [\"dataMin\", \"dataMax\"]}\n // Clip the lines to a zoom window instead of recharts widening it back out.\n allowDataOverflow={zoom?.xDomain !== undefined}\n tickLine={false}\n axisLine={false}\n minTickGap={32}\n tick={x.ticks === false ? false : undefined}\n // With neither ticks nor title there is nothing to reserve the band for, and\n // recharts' own 30 px would leave a gap under every chart of a stack.\n height={x.ticks === false && !x.title ? 4 : undefined}\n tickFormatter={xFormat}\n >\n {x.title && (\n <Label\n value={x.title}\n position=\"insideBottom\"\n offset={-12}\n className=\"fill-[var(--text-muted)] text-[11px]\"\n />\n )}\n </XAxis>\n {axes.map((axis) => (\n <YAxis\n key={axis.id}\n yAxisId={axis.id}\n hide={axis.hide}\n domain={fittedY(axis)}\n allowDataOverflow={zoom?.yDomains[axis.id] !== undefined}\n orientation={axis.orientation ?? \"left\"}\n width={axisBandWidth(axis.width, Boolean(axis.title) && !axis.hide)}\n tickLine={false}\n axisLine={false}\n tickFormatter={axis.format ?? number}\n // With an axis per channel the tint is the only thing saying which\n // numbers belong to which line.\n tick={axis.color ? { fill: axis.color } : undefined}\n >\n {axis.title && !axis.hide && (\n <Label\n value={axis.title}\n // Rotated so both sides read upward from the bottom of their axis.\n angle={axis.orientation === \"right\" ? 90 : -90}\n position={axis.orientation === \"right\" ? \"insideRight\" : \"insideLeft\"}\n // Inline, so a sole series' colour beats the class's neutral fill —\n // and absent for a shared axis, so the class draws it.\n style={{ textAnchor: \"middle\", fill: soleSeriesColor(axis, series) }}\n offset={AXIS_TITLE_OFFSET}\n className=\"fill-[var(--text-muted)] text-[11px]\"\n />\n )}\n </YAxis>\n ))}\n <ChartTooltip\n content={\n <ChartTooltipContent\n labelFormatter={(value) => (x.label ?? xFormat)(Number(value))}\n valueFormatter={valueFormat ?? number}\n />\n }\n />\n {series.map((entry) => (\n <Line\n key={entry.key}\n yAxisId={entry.axis ?? DEFAULT_Y_AXIS}\n type={entry.step ? \"stepAfter\" : \"monotone\"}\n dataKey={entry.key}\n stroke={`var(--color-${entry.key})`}\n strokeWidth={entry.step ? 1.5 : 2}\n strokeDasharray={strokeDash(\n entry.step ? STEP_DASH : (entry.dash ?? (entry.dashed ? 1 : 0)),\n )}\n dot={false}\n connectNulls={connectNulls}\n isAnimationActive={animationMs > 0}\n animationDuration={animationMs}\n animationEasing=\"ease-out\"\n />\n ))}\n {spans?.map((span) => (\n <ReferenceLine\n key={span.key}\n yAxisId={span.axis ?? DEFAULT_Y_AXIS}\n segment={[\n { x: span.x, y: span.from },\n { x: span.x, y: span.to },\n ]}\n stroke={span.color ?? \"var(--text-muted)\"}\n strokeWidth={2}\n // Discarded rather than clamped when zoomed past: a span clamped to the\n // window's edge draws a vertical line at an abscissa it never stood at.\n ifOverflow=\"discard\"\n />\n ))}\n {zoom?.layer}\n </LineChart>\n </ChartContainer>\n );\n}\n\n/**\n * The series chart, zoomable. Drag across the plot to zoom (the drag's shape picks the\n * axes), double-click or press the reset button to go back; wrap several in\n * `SharedXZoom` to move their x windows together.\n */\nexport const SeriesChart = withChartZoom(StaticSeriesChart);\n"],"mappings":";AAiUU,cA8CJ,YA9CI;AA9SV,SAAS,eAAe;AAExB,SAAS,eAAe,OAAO,MAAM,WAAW,eAAe,OAAO,aAAa;AACnF,SAAS,gBAAgB,cAAc,2BAA6C;AACpF,SAAS,gBAAgB,qBAAuC;AAChE,SAAS,WAAW,kBAAkB;AACtC,SAAS,mCAA2D;AACpE,SAAS,kBAAkB;AAC3B,SAAS,cAAc,oBAAoB;AAC3C,SAAS,UAAU;AAuIZ,MAAM,mBAAmB;AAGhC,MAAM,oBAAoB;AAGnB,MAAM,kBAAkB;AAO/B,MAAM,WAAW;AAIV,SAAS,aAAa,QAAwD;AACnF,MAAI,MAAM;AACV,MAAI,OAAO;AACX,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,OAAO,SAAS,KAAK,EAAG;AAC7B,QAAI,QAAQ,IAAK,OAAM;AACvB,QAAI,QAAQ,KAAM,QAAO;AAAA,EAC3B;AACA,SAAO,QAAQ,KAAK,IAAI;AAC1B;AAOO,SAAS,QAAQ,KAAa,MAA4C;AAC/E,MAAI,EAAE,OAAO,MAAO,QAAO;AAC3B,QAAM,OAAO,OAAO,OAAO,YAAY,KAAK,IAAI,IAAI,IAAI,YAAY;AACpE,SAAO,CAAC,MAAM,KAAK,OAAO,GAAG;AAC/B;AAIO,SAAS,cAAc,WAA+B,QAAyB;AACpF,UAAQ,aAAa,oBAAoB,SAAS,mBAAmB;AACvE;AAUO,SAAS,gBACd,MACA,QACoB;AACpB,QAAM,OAAO,OAAO,OAAO,CAAC,WAAW,MAAM,QAAQ,oBAAoB,KAAK,EAAE;AAChF,SAAO,KAAK,WAAW,IAAI,KAAK,QAAQ;AAC1C;AAIO,SAAS,QACd,QACA,OACA,OACA,aACmB;AACnB,SAAO,CAAC,EAAE,IAAI,gBAAgB,QAAQ,OAAO,OAAO,YAAY,CAAC;AACnE;AAgBO,SAAS,YAAY,SAAmD;AAC7E,QAAM,aAAa,oBAAI,IAAoC;AAC3D,aAAW,UAAU,SAAS;AAC5B,WAAO,EAAE,QAAQ,CAAC,OAAO,UAAU;AACjC,YAAM,MAAM,WAAW,IAAI,KAAK,KAAK,EAAE,GAAG,MAAM;AAChD,iBAAW,CAAC,KAAK,OAAO,KAAK,OAAO,QAAQ,OAAO,QAAQ,EAAG,KAAI,GAAG,IAAI,QAAQ,KAAK;AACtF,iBAAW,IAAI,OAAO,GAAG;AAAA,IAC3B,CAAC;AAAA,EACH;AACA,SAAO,CAAC,GAAG,WAAW,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1D;AAYO,SAAS,aAAa,OAAoC;AAC/D,QAAM,MAAM,MAAM,IAAI,CAAC,SAAS,OAAO,IAAI,EAAE,QAAQ,mBAAmB,GAAG,CAAC,EAAE,KAAK,GAAG;AACtF,SAAO,aAAa,KAAK,GAAG,IAAI,MAAM,IAAI,GAAG;AAC/C;AAGA,SAAS,iBAAiB,YAA2D;AACnF,QAAM,SAAS,aAAa,UAAU;AACtC,SAAO,QAAQ,MAAM;AACnB,UAAM,SAAS,IAAI,KAAK,aAAa,QAAQ,EAAE,uBAAuB,EAAE,CAAC;AACzE,WAAO,CAAC,UAAkB,OAAO,OAAO,KAAK;AAAA,EAC/C,GAAG,CAAC,MAAM,CAAC;AACb;AAGA,MAAM,oBAAuC,CAAC,EAAE,IAAI,gBAAgB,OAAO,GAAG,CAAC;AAOxE,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA,IAAI,CAAC;AAAA,EACL;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,SAAS,aAAa,eAAe,6BAA6B,UAAU;AAClF,QAAM,SAAS,iBAAiB,MAAM;AAKtC,MAAI,CAAC,KAAK,UAAU,CAAC,OAAO,QAAQ;AAClC,WACE,oBAAC,SAAI,WAAW,GAAG,4DAA4D,MAAM,GAClF,oBAAU,SACT,QAEA,oBAAC,OAAE,WAAU,oCAAoC,iBAAO,OAAM,GAElE;AAAA,EAEJ;AAEA,QAAM,OAAO,EAAE,OAAO;AACtB,QAAM,UAAU,EAAE,UAAU;AAC5B,QAAM,SAAsB,OAAO;AAAA,IACjC,OAAO,IAAI,CAAC,OAAO,UAAU;AAAA,MAC3B,MAAM;AAAA,MACN,EAAE,OAAO,MAAM,OAAO,OAAO,MAAM,SAAS,WAAW,KAAK,EAAE;AAAA,IAChE,CAAC;AAAA,EACH;AACA,QAAM,UAAU,KAAK,OAAO,CAAC,SAAS,CAAC,KAAK,IAAI;AAKhD,QAAM,YAAY,QAAQ,CAAC,KAAK,KAAK,CAAC,IAAI,MAAM;AAChD,QAAM,SAAS,QAAQ,KAAK,CAAC,UAAU,KAAK,eAAe,YAAY,MAAM;AAC7E,QAAM,UAAU,QAAQ,KAAK,CAAC,SAAS,KAAK,gBAAgB,OAAO;AAInE,QAAM,SAAS;AAAA,IACb,KAAK;AAAA,IACL,OAAO,UAAU,IAAI;AAAA,IACrB,MAAM,SAAS,IAAI;AAAA;AAAA,IAEnB,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACzB;AAGA,QAAM,UAAU,MAAM,WAAW,aAAa,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC;AAC1E,QAAM,UAAU,CAAC,SACf,MAAM,SAAS,KAAK,EAAE,KACtB,KAAK,UACL;AAAA,IACE,OACG,OAAO,CAAC,WAAW,MAAM,QAAQ,oBAAoB,KAAK,EAAE,EAC5D,QAAQ,CAAC,UAAU,KAAK,IAAI,CAAC,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC;AAAA,EACzD;AAEF,SACE,oBAAC,kBAAe,QAAgB,WAAW,GAAG,UAAU,QAAQ,SAAS,GACvE,+BAAC,aAAU,MAAM,MAAM,QAGrB;AAAA,wBAAC,iBAAc,SAAS,UAAU;AAAA,IAClC;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QAGT,MAAK;AAAA,QACL,QAAQ,WAAW,CAAC,WAAW,SAAS;AAAA,QAExC,mBAAmB,MAAM,YAAY;AAAA,QACrC,UAAU;AAAA,QACV,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,MAAM,EAAE,UAAU,QAAQ,QAAQ;AAAA,QAGlC,QAAQ,EAAE,UAAU,SAAS,CAAC,EAAE,QAAQ,IAAI;AAAA,QAC5C,eAAe;AAAA,QAEd,YAAE,SACD;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,EAAE;AAAA,YACT,UAAS;AAAA,YACT,QAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ;AAAA;AAAA,IAEJ;AAAA,IACC,KAAK,IAAI,CAAC,SACT;AAAA,MAAC;AAAA;AAAA,QAEC,SAAS,KAAK;AAAA,QACd,MAAM,KAAK;AAAA,QACX,QAAQ,QAAQ,IAAI;AAAA,QACpB,mBAAmB,MAAM,SAAS,KAAK,EAAE,MAAM;AAAA,QAC/C,aAAa,KAAK,eAAe;AAAA,QACjC,OAAO,cAAc,KAAK,OAAO,QAAQ,KAAK,KAAK,KAAK,CAAC,KAAK,IAAI;AAAA,QAClE,UAAU;AAAA,QACV,UAAU;AAAA,QACV,eAAe,KAAK,UAAU;AAAA,QAG9B,MAAM,KAAK,QAAQ,EAAE,MAAM,KAAK,MAAM,IAAI;AAAA,QAEzC,eAAK,SAAS,CAAC,KAAK,QACnB;AAAA,UAAC;AAAA;AAAA,YACC,OAAO,KAAK;AAAA,YAEZ,OAAO,KAAK,gBAAgB,UAAU,KAAK;AAAA,YAC3C,UAAU,KAAK,gBAAgB,UAAU,gBAAgB;AAAA,YAGzD,OAAO,EAAE,YAAY,UAAU,MAAM,gBAAgB,MAAM,MAAM,EAAE;AAAA,YACnE,QAAQ;AAAA,YACR,WAAU;AAAA;AAAA,QACZ;AAAA;AAAA,MAzBG,KAAK;AAAA,IA2BZ,CACD;AAAA,IACD;AAAA,MAAC;AAAA;AAAA,QACC,SACE;AAAA,UAAC;AAAA;AAAA,YACC,gBAAgB,CAAC,WAAW,EAAE,SAAS,SAAS,OAAO,KAAK,CAAC;AAAA,YAC7D,gBAAgB,eAAe;AAAA;AAAA,QACjC;AAAA;AAAA,IAEJ;AAAA,IACC,OAAO,IAAI,CAAC,UACX;AAAA,MAAC;AAAA;AAAA,QAEC,SAAS,MAAM,QAAQ;AAAA,QACvB,MAAM,MAAM,OAAO,cAAc;AAAA,QACjC,SAAS,MAAM;AAAA,QACf,QAAQ,eAAe,MAAM,GAAG;AAAA,QAChC,aAAa,MAAM,OAAO,MAAM;AAAA,QAChC,iBAAiB;AAAA,UACf,MAAM,OAAO,YAAa,MAAM,SAAS,MAAM,SAAS,IAAI;AAAA,QAC9D;AAAA,QACA,KAAK;AAAA,QACL;AAAA,QACA,mBAAmB,cAAc;AAAA,QACjC,mBAAmB;AAAA,QACnB,iBAAgB;AAAA;AAAA,MAbX,MAAM;AAAA,IAcb,CACD;AAAA,IACA,OAAO,IAAI,CAAC,SACX;AAAA,MAAC;AAAA;AAAA,QAEC,SAAS,KAAK,QAAQ;AAAA,QACtB,SAAS;AAAA,UACP,EAAE,GAAG,KAAK,GAAG,GAAG,KAAK,KAAK;AAAA,UAC1B,EAAE,GAAG,KAAK,GAAG,GAAG,KAAK,GAAG;AAAA,QAC1B;AAAA,QACA,QAAQ,KAAK,SAAS;AAAA,QACtB,aAAa;AAAA,QAGb,YAAW;AAAA;AAAA,MAVN,KAAK;AAAA,IAWZ,CACD;AAAA,IACA,MAAM;AAAA,KACT,GACF;AAEJ;AAOO,MAAM,cAAc,cAAc,iBAAiB;","names":[]}
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
+ import { SelectProps } from './ui.js';
3
4
  import { ThemePreference } from '../theme/theme-store.js';
4
5
  import 'zustand/middleware';
5
6
  import 'zustand';
@@ -11,7 +12,14 @@ import 'zustand';
11
12
  * The app owns WHERE these render (its own settings page) and any app-specific
12
13
  * fields around them.
13
14
  */
14
- interface ThemeSettingProps {
15
+ /**
16
+ * Everything a {@link Select} takes, minus the two this component owns: the value is a
17
+ * {@link ThemePreference} rather than a string, and `onChange` hands over the preference
18
+ * itself because no caller of this wants the event. The rest — `disabled`, `required`,
19
+ * `invalid`, `error`, a `data-tour` anchor — passes straight through, which is what makes
20
+ * this a settings FIELD rather than a fixed widget.
21
+ */
22
+ interface ThemeSettingProps extends Omit<SelectProps, "value" | "onChange" | "children"> {
15
23
  value: ThemePreference;
16
24
  onChange: (value: ThemePreference) => void;
17
25
  label: ReactNode;
@@ -23,8 +31,10 @@ interface ThemeSettingProps {
23
31
  };
24
32
  id?: string;
25
33
  }
26
- declare function ThemeSetting({ value, onChange, label, optionLabels, id }: ThemeSettingProps): react.JSX.Element;
27
- interface LanguageSettingProps {
34
+ declare function ThemeSetting({ value, onChange, label, optionLabels, id, ...rest }: ThemeSettingProps): react.JSX.Element;
35
+ /** See {@link ThemeSettingProps}: a `Select`'s props with the value and the change
36
+ * handler re-typed to the language CODE the caller stores. */
37
+ interface LanguageSettingProps extends Omit<SelectProps, "value" | "onChange" | "children"> {
28
38
  value: string;
29
39
  onChange: (code: string) => void;
30
40
  label: ReactNode;
@@ -34,6 +44,6 @@ interface LanguageSettingProps {
34
44
  }[];
35
45
  id?: string;
36
46
  }
37
- declare function LanguageSetting({ value, onChange, label, options, id }: LanguageSettingProps): react.JSX.Element;
47
+ declare function LanguageSetting({ value, onChange, label, options, id, ...rest }: LanguageSettingProps): react.JSX.Element;
38
48
 
39
49
  export { LanguageSetting, type LanguageSettingProps, ThemeSetting, type ThemeSettingProps };
@@ -1,14 +1,25 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
3
  import { Select } from "./ui.js";
3
- function ThemeSetting({ value, onChange, label, optionLabels, id }) {
4
- return /* @__PURE__ */ jsxs(Select, { id, label, value, onChange: (e) => onChange(e.target.value), children: [
5
- /* @__PURE__ */ jsx("option", { value: "system", children: optionLabels.system }),
6
- /* @__PURE__ */ jsx("option", { value: "light", children: optionLabels.light }),
7
- /* @__PURE__ */ jsx("option", { value: "dark", children: optionLabels.dark })
8
- ] });
4
+ function ThemeSetting({ value, onChange, label, optionLabels, id, ...rest }) {
5
+ return /* @__PURE__ */ jsxs(
6
+ Select,
7
+ {
8
+ ...rest,
9
+ id,
10
+ label,
11
+ value,
12
+ onChange: (e) => onChange(e.target.value),
13
+ children: [
14
+ /* @__PURE__ */ jsx("option", { value: "system", children: optionLabels.system }),
15
+ /* @__PURE__ */ jsx("option", { value: "light", children: optionLabels.light }),
16
+ /* @__PURE__ */ jsx("option", { value: "dark", children: optionLabels.dark })
17
+ ]
18
+ }
19
+ );
9
20
  }
10
- function LanguageSetting({ value, onChange, label, options, id }) {
11
- return /* @__PURE__ */ jsx(Select, { id, label, value, onChange: (e) => onChange(e.target.value), children: options.map((o) => /* @__PURE__ */ jsx("option", { value: o.code, children: o.label }, o.code)) });
21
+ function LanguageSetting({ value, onChange, label, options, id, ...rest }) {
22
+ return /* @__PURE__ */ jsx(Select, { ...rest, id, label, value, onChange: (e) => onChange(e.target.value), children: options.map((o) => /* @__PURE__ */ jsx("option", { value: o.code, children: o.label }, o.code)) });
12
23
  }
13
24
  export {
14
25
  LanguageSetting,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/settings-fields.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Select } from \"./ui\";\nimport type { ThemePreference } from \"../theme/theme-store\";\n\n/**\n * App-agnostic settings-field components (feedback #333): the language + theme\n * controls that every app's settings page needs, so they live in @hb/ui and the\n * app just supplies the current value, a change handler and translated labels.\n * The app owns WHERE these render (its own settings page) and any app-specific\n * fields around them.\n */\n\nexport interface ThemeSettingProps {\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n label: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\nexport function ThemeSetting({ value, onChange, label, optionLabels, id }: ThemeSettingProps) {\n return (\n <Select id={id} label={label} value={value} onChange={(e) => onChange(e.target.value as ThemePreference)}>\n <option value=\"system\">{optionLabels.system}</option>\n <option value=\"light\">{optionLabels.light}</option>\n <option value=\"dark\">{optionLabels.dark}</option>\n </Select>\n );\n}\n\nexport interface LanguageSettingProps {\n value: string;\n onChange: (code: string) => void;\n label: ReactNode;\n options: { code: string; label: string }[];\n id?: string;\n}\n\nexport function LanguageSetting({ value, onChange, label, options, id }: LanguageSettingProps) {\n return (\n <Select id={id} label={label} value={value} onChange={(e) => onChange(e.target.value)}>\n {options.map((o) => (\n <option key={o.code} value={o.code}>\n {o.label}\n </option>\n ))}\n </Select>\n );\n}\n"],"mappings":"AAuBI,SACE,KADF;AAtBJ,SAAS,cAAc;AAoBhB,SAAS,aAAa,EAAE,OAAO,UAAU,OAAO,cAAc,GAAG,GAAsB;AAC5F,SACE,qBAAC,UAAO,IAAQ,OAAc,OAAc,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAwB,GACrG;AAAA,wBAAC,YAAO,OAAM,UAAU,uBAAa,QAAO;AAAA,IAC5C,oBAAC,YAAO,OAAM,SAAS,uBAAa,OAAM;AAAA,IAC1C,oBAAC,YAAO,OAAM,QAAQ,uBAAa,MAAK;AAAA,KAC1C;AAEJ;AAUO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,SAAS,GAAG,GAAyB;AAC7F,SACE,oBAAC,UAAO,IAAQ,OAAc,OAAc,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GACjF,kBAAQ,IAAI,CAAC,MACZ,oBAAC,YAAoB,OAAO,EAAE,MAC3B,YAAE,SADQ,EAAE,IAEf,CACD,GACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/settings-fields.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { Select } from \"./ui\";\nimport type { SelectProps } from \"./ui\";\nimport type { ThemePreference } from \"../theme/theme-store\";\n\n/**\n * App-agnostic settings-field components (feedback #333): the language + theme\n * controls that every app's settings page needs, so they live in @hb/ui and the\n * app just supplies the current value, a change handler and translated labels.\n * The app owns WHERE these render (its own settings page) and any app-specific\n * fields around them.\n */\n\n/**\n * Everything a {@link Select} takes, minus the two this component owns: the value is a\n * {@link ThemePreference} rather than a string, and `onChange` hands over the preference\n * itself because no caller of this wants the event. The rest — `disabled`, `required`,\n * `invalid`, `error`, a `data-tour` anchor — passes straight through, which is what makes\n * this a settings FIELD rather than a fixed widget.\n */\nexport interface ThemeSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n value: ThemePreference;\n onChange: (value: ThemePreference) => void;\n label: ReactNode;\n /** Translated option labels. */\n optionLabels: { system: string; light: string; dark: string };\n id?: string;\n}\n\nexport function ThemeSetting({ value, onChange, label, optionLabels, id, ...rest }: ThemeSettingProps) {\n return (\n <Select\n {...rest}\n id={id}\n label={label}\n value={value}\n onChange={(e) => onChange(e.target.value as ThemePreference)}\n >\n <option value=\"system\">{optionLabels.system}</option>\n <option value=\"light\">{optionLabels.light}</option>\n <option value=\"dark\">{optionLabels.dark}</option>\n </Select>\n );\n}\n\n/** See {@link ThemeSettingProps}: a `Select`'s props with the value and the change\n * handler re-typed to the language CODE the caller stores. */\nexport interface LanguageSettingProps\n extends Omit<SelectProps, \"value\" | \"onChange\" | \"children\"> {\n value: string;\n onChange: (code: string) => void;\n label: ReactNode;\n options: { code: string; label: string }[];\n id?: string;\n}\n\nexport function LanguageSetting({ value, onChange, label, options, id, ...rest }: LanguageSettingProps) {\n return (\n <Select {...rest} id={id} label={label} value={value} onChange={(e) => onChange(e.target.value)}>\n {options.map((o) => (\n <option key={o.code} value={o.code}>\n {o.label}\n </option>\n ))}\n </Select>\n );\n}\n"],"mappings":";AAgCI,SAOE,KAPF;AA/BJ,SAAS,cAAc;AA6BhB,SAAS,aAAa,EAAE,OAAO,UAAU,OAAO,cAAc,IAAI,GAAG,KAAK,GAAsB;AACrG,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAwB;AAAA,MAE3D;AAAA,4BAAC,YAAO,OAAM,UAAU,uBAAa,QAAO;AAAA,QAC5C,oBAAC,YAAO,OAAM,SAAS,uBAAa,OAAM;AAAA,QAC1C,oBAAC,YAAO,OAAM,QAAQ,uBAAa,MAAK;AAAA;AAAA;AAAA,EAC1C;AAEJ;AAaO,SAAS,gBAAgB,EAAE,OAAO,UAAU,OAAO,SAAS,IAAI,GAAG,KAAK,GAAyB;AACtG,SACE,oBAAC,UAAQ,GAAG,MAAM,IAAQ,OAAc,OAAc,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAC3F,kBAAQ,IAAI,CAAC,MACZ,oBAAC,YAAoB,OAAO,EAAE,MAC3B,YAAE,SADQ,EAAE,IAEf,CACD,GACH;AAEJ;","names":[]}
@@ -0,0 +1,119 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
+
4
+ /**
5
+ * A drawn signature, captured on a canvas and handed back as a PNG data URL.
6
+ *
7
+ * Kastlan wrote the first one (`features/handover/components/signature-pad.tsx`) for
8
+ * the handover protocol, where tenant and inspector each sign on the same phone. This
9
+ * keeps its contract — draw, get a PNG — and fixes what a copy per app would keep
10
+ * getting wrong:
11
+ *
12
+ * 1. **Strokes are data, not pixels.** Each stroke is kept as a list of points, and
13
+ * the canvas is a VIEW of them. That is what lets a resize (a phone rotating
14
+ * mid-signature) keep the drawing instead of wiping it — setting `canvas.width`
15
+ * clears the bitmap, so a pixels-only pad loses everything — and what makes undo
16
+ * one `pop()`. The pixel buffer is sized to `devicePixelRatio`, so a stroke is
17
+ * sharp on a 3× screen rather than upscaled from a 1× bitmap.
18
+ * 2. **Smooth, and pressure-aware.** Segments are quadratic curves through the
19
+ * midpoints between samples, so a fast stroke is a curve, not a polyline; a pen
20
+ * that reports pressure varies the width, a mouse or finger (whose `pressure` is a
21
+ * constant 0.5, or junk) draws at the base width.
22
+ * 3. **Two inks.** On screen the ink is the computed `color` of the canvas, which is
23
+ * `--text-primary` — so it reads in the dark theme, where a hard-coded near-black
24
+ * stroke vanished into the surface. The exported PNG is re-rendered from the same
25
+ * strokes in `exportInk` (near-black by default) on a transparent background. A
26
+ * signature is a DOCUMENT artefact: it is printed on the protocol, shown on a white
27
+ * card, embedded in a PDF — none of which follow the signer's theme. Exporting
28
+ * the on-screen ink would have produced a pale-grey-on-transparent PNG for every
29
+ * dark-mode signer, invisible on white paper.
30
+ *
31
+ * **Accessibility.** A canvas has no content a screen reader or a keyboard can reach,
32
+ * and no amount of ARIA changes that — drawing is a pointer act. What the pad does:
33
+ * the canvas is `role="img"`, named by the visible label and described by the
34
+ * instructions and by its state ("Nothing drawn yet" / "Signature drawn"), so a
35
+ * screen-reader user knows what the box is and whether it holds anything; undo and
36
+ * clear are real buttons, and their effect is announced. What it cannot do is let
37
+ * someone sign without a pointer — hence `allowTypedName`, which offers a typed name
38
+ * instead. It is OPT-IN, because whether a typed name counts as a signature is a
39
+ * product and legal decision, not a UI one (Kastlan's handover accepts one; a flow
40
+ * that needs a drawn mark would not). Where a drawn mark is not strictly required,
41
+ * turn it on.
42
+ */
43
+ /** Every string the pad renders or speaks. */
44
+ interface SignaturePadLabels {
45
+ /** Visible label above the pad, and the canvas's accessible name. The `label`
46
+ * prop overrides it per instance. */
47
+ label: string;
48
+ /** Under the label, and part of the canvas's description. */
49
+ instructions: string;
50
+ /** Appended to the instructions when `allowTypedName` is on. */
51
+ typedFallbackHint: string;
52
+ /** The canvas's state for a screen reader. */
53
+ empty: string;
54
+ signed: string;
55
+ undo: string;
56
+ clear: string;
57
+ /** Only rendered when `onSave` is given. */
58
+ save: string;
59
+ /** The mode switch (`allowTypedName`). */
60
+ useTyped: string;
61
+ useDrawn: string;
62
+ /** Label of the typed-name field. */
63
+ typedName: string;
64
+ /** Live-region messages after the matching button. */
65
+ cleared: string;
66
+ undone: string;
67
+ }
68
+ declare const DEFAULT_SIGNATURE_PAD_LABELS: SignaturePadLabels;
69
+ /** How the value handed back was made. */
70
+ interface SignatureDetail {
71
+ method: "drawn" | "typed";
72
+ /** The name as typed, when `method` is `"typed"` — the PNG shows it, but a host
73
+ * that stores a typed name as TEXT (Kastlan does) wants the string. */
74
+ typedName?: string;
75
+ }
76
+ /** The imperative handle: `ref.current.toDataURL()` at submit time, for a form that
77
+ * does not want to mirror the value into state on every stroke. */
78
+ interface SignaturePadHandle {
79
+ /** Remove every stroke and the typed name. Fires `onChange(null)`. */
80
+ clear: () => void;
81
+ /** The PNG, rendered in `exportInk` — or `null` while nothing is signed. */
82
+ toDataURL: () => string | null;
83
+ isEmpty: () => boolean;
84
+ }
85
+ interface SignaturePadProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
86
+ /** Visible label; defaults to `labels.label`. */
87
+ label?: ReactNode;
88
+ /** Replaces `labels.instructions` for this instance (e.g. a legal note). */
89
+ description?: ReactNode;
90
+ /**
91
+ * After every finished stroke, undo, clear and typed-name edit: the PNG, or `null`
92
+ * once the pad is empty. Not called mid-stroke — encoding a PNG per pointer move
93
+ * would stall the drawing on a phone.
94
+ */
95
+ onChange?: (dataUrl: string | null, detail: SignatureDetail) => void;
96
+ /** Renders a Save button, enabled once something is signed. */
97
+ onSave?: (dataUrl: string, detail: SignatureDetail) => void;
98
+ disabled?: boolean;
99
+ /** Paints the pad as wrong and sets `aria-invalid`. See {@link Input}'s `invalid`. */
100
+ invalid?: boolean;
101
+ /** Message under the pad; implies `invalid` and is attached with
102
+ * `aria-describedby`, like the kit's other fields. */
103
+ error?: ReactNode;
104
+ /** Offer a typed name as an alternative to drawing. See the accessibility note. */
105
+ allowTypedName?: boolean;
106
+ /** Ink of the exported PNG. Default near-black, legible on white. */
107
+ exportInk?: string;
108
+ /** Fill behind the exported PNG. Default transparent. */
109
+ exportBackground?: string;
110
+ /** Base stroke width in CSS pixels. Default 2.5. */
111
+ lineWidth?: number;
112
+ /** Classes for the canvas — its height, chiefly (default `h-40`). */
113
+ canvasClassName?: string;
114
+ /** User-facing strings; see {@link SignaturePadLabels}. */
115
+ labels?: Partial<SignaturePadLabels>;
116
+ }
117
+ declare const SignaturePad: react.ForwardRefExoticComponent<SignaturePadProps & react.RefAttributes<SignaturePadHandle>>;
118
+
119
+ export { DEFAULT_SIGNATURE_PAD_LABELS, type SignatureDetail, SignaturePad, type SignaturePadHandle, type SignaturePadLabels, type SignaturePadProps };